diff --git a/services/web/app/views/project/editor/file-tree-history-react.pug b/services/web/app/views/project/editor/file-tree-history-react.pug
index 072eaca47a..02dfe6ea25 100644
--- a/services/web/app/views/project/editor/file-tree-history-react.pug
+++ b/services/web/app/views/project/editor/file-tree-history-react.pug
@@ -1,3 +1,12 @@
aside.editor-sidebar.full-size#history-file-tree(
+ ng-controller="ReactFileTreeController"
ng-show="history.isReact && ui.view == 'history'"
)
+ .history-file-tree-inner.file-tree
+ history-file-tree-react(
+ on-select="onSelect"
+ ref-providers="refProviders"
+ reindex-references="reindexReferences"
+ set-ref-provider-enabled="setRefProviderEnabled"
+ set-started-free-trial="setStartedFreeTrial"
+ )
diff --git a/services/web/frontend/js/features/history/components/history-file-tree.tsx b/services/web/frontend/js/features/history/components/history-file-tree.tsx
index 4c7edafab5..5aecbeb149 100644
--- a/services/web/frontend/js/features/history/components/history-file-tree.tsx
+++ b/services/web/frontend/js/features/history/components/history-file-tree.tsx
@@ -1,18 +1,53 @@
+import _ from 'lodash'
+import FileTreeContext from '../../file-tree/components/file-tree-context'
+import FileTreeFolderList from '../../file-tree/components/file-tree-folder-list'
import { useHistoryContext } from '../context/history-context'
+import {
+ fileTreeDiffToFileTreeData,
+ reducePathsToTree,
+} from '../utils/file-tree'
-function HistoryFileTree() {
- // eslint-disable-next-line no-unused-vars
- const { fileSelection, setFileSelection } = useHistoryContext()
-
- return fileSelection ? (
-
- {fileSelection.files.map(file => (
- - {file.pathname}
- ))}
-
- ) : (
- No files
- )
+type HistoryFileTreeProps = {
+ setRefProviderEnabled: any
+ setStartedFreeTrial: any
+ reindexReferences: any
+ onSelect: any
+ refProviders: any
}
-export default HistoryFileTree
+export default function HistoryFileTree({
+ setRefProviderEnabled,
+ setStartedFreeTrial,
+ reindexReferences,
+ onSelect,
+ refProviders,
+}: HistoryFileTreeProps) {
+ const { fileSelection } = useHistoryContext()
+
+ if (!fileSelection) {
+ return null
+ }
+
+ const fileTree = _.reduce(fileSelection.files, reducePathsToTree, [])
+
+ const mappedFileTree = fileTreeDiffToFileTreeData(fileTree)
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/history/components/history-root.tsx b/services/web/frontend/js/features/history/components/history-root.tsx
index e3df17a53d..02755b7cf7 100644
--- a/services/web/frontend/js/features/history/components/history-root.tsx
+++ b/services/web/frontend/js/features/history/components/history-root.tsx
@@ -1,27 +1,20 @@
-import { createPortal } from 'react-dom'
-import HistoryFileTree from './history-file-tree'
import ChangeList from './change-list/change-list'
import Editor from './editor/editor'
import { useLayoutContext } from '../../../shared/context/layout-context'
import { useHistoryContext } from '../context/history-context'
-const fileTreeContainer = document.getElementById('history-file-tree')
-
export default function HistoryRoot() {
const { view } = useLayoutContext()
const { updates } = useHistoryContext()
+ if (view !== 'history' || updates.length === 0) {
+ return null
+ }
+
return (
- <>
- {fileTreeContainer
- ? createPortal(, fileTreeContainer)
- : null}
- {view === 'history' && updates.length > 0 && (
-
-
-
-
- )}
- >
+
+
+
+
)
}
diff --git a/services/web/frontend/js/features/history/context/history-context.tsx b/services/web/frontend/js/features/history/context/history-context.tsx
index 0a75f3f0fe..10e6033a62 100644
--- a/services/web/frontend/js/features/history/context/history-context.tsx
+++ b/services/web/frontend/js/features/history/context/history-context.tsx
@@ -6,6 +6,7 @@ import { HistoryContextValue } from './types/history-context-value'
import { Update, UpdateSelection } from '../services/types/update'
import { FileSelection } from '../services/types/file'
import { diffFiles, fetchUpdates } from '../services/api'
+import { renamePathnameKey, isFileRenamed } from '../utils/file-tree'
function useHistory() {
const { view } = useLayoutContext()
@@ -68,16 +69,15 @@ function useHistory() {
diffFiles(projectId, fromV, toV).then(({ diff: files }) => {
// TODO Infer default file sensibly
- let pathname = null
- for (const file of files) {
- if (file.pathname.endsWith('.tex')) {
- pathname = file.pathname
- break
- } else if (!pathname) {
- pathname = file.pathname
+ const pathname = null
+ const newFiles = files.map(file => {
+ if (isFileRenamed(file) && file.newPathname) {
+ return renamePathnameKey(file)
}
- }
- setFileSelection({ files, pathname })
+
+ return file
+ })
+ setFileSelection({ files: newFiles, pathname })
})
}, [updateSelection, projectId])
diff --git a/services/web/frontend/js/features/history/controllers/history-file-tree-controller.js b/services/web/frontend/js/features/history/controllers/history-file-tree-controller.js
new file mode 100644
index 0000000000..2775e66e8d
--- /dev/null
+++ b/services/web/frontend/js/features/history/controllers/history-file-tree-controller.js
@@ -0,0 +1,15 @@
+import App from '../../../base'
+import { react2angular } from 'react2angular'
+import { rootContext } from '../../../shared/context/root-context'
+import HistoryFileTree from '../components/history-file-tree'
+
+App.component(
+ 'historyFileTreeReact',
+ react2angular(rootContext.use(HistoryFileTree), [
+ 'refProviders',
+ 'setRefProviderEnabled',
+ 'setStartedFreeTrial',
+ 'reindexReferences',
+ 'onSelect',
+ ])
+)
diff --git a/services/web/frontend/js/features/history/services/types/file.ts b/services/web/frontend/js/features/history/services/types/file.ts
index 95faaeec5b..d590e0e790 100644
--- a/services/web/frontend/js/features/history/services/types/file.ts
+++ b/services/web/frontend/js/features/history/services/types/file.ts
@@ -12,7 +12,8 @@ export interface FileRemoved extends FileUnchanged {
}
export interface FileRenamed extends FileUnchanged {
- newPathname: string
+ newPathname?: string
+ oldPathname?: string
operation: 'renamed'
}
diff --git a/services/web/frontend/js/features/history/utils/file-tree.ts b/services/web/frontend/js/features/history/utils/file-tree.ts
new file mode 100644
index 0000000000..bea2596228
--- /dev/null
+++ b/services/web/frontend/js/features/history/utils/file-tree.ts
@@ -0,0 +1,114 @@
+import _ from 'lodash'
+import type { Doc } from '../../../../../types/doc'
+import type { FileDiff, FileRenamed } from '../services/types/file'
+
+// `Partial` because the `reducePathsToTree` function was copied directly
+// from a javascript file without proper type system and the logic is not typescript-friendly.
+// TODO: refactor the function to have a proper type system
+type FileTreeEntity = Partial<{
+ name: string
+ type: 'file' | 'folder'
+ oldPathname: string
+ newPathname: string
+ pathname: string
+ children: FileTreeEntity[]
+ operation: 'edited' | 'added' | 'renamed' | 'removed'
+}>
+
+export function reducePathsToTree(
+ currentFileTree: FileTreeEntity[],
+ fileObject: FileTreeEntity
+) {
+ const filePathParts = fileObject?.pathname?.split('/') ?? ''
+ let currentFileTreeLocation = currentFileTree
+ for (let index = 0; index < filePathParts.length; index++) {
+ let fileTreeEntity: FileTreeEntity | null = {}
+ const pathPart = filePathParts[index]
+ const isFile = index === filePathParts.length - 1
+ if (isFile) {
+ fileTreeEntity = _.clone(fileObject)
+ fileTreeEntity.name = pathPart
+ fileTreeEntity.type = 'file'
+ currentFileTreeLocation.push(fileTreeEntity)
+ } else {
+ fileTreeEntity =
+ _.find(currentFileTreeLocation, entity => entity.name === pathPart) ??
+ null
+ if (fileTreeEntity == null) {
+ fileTreeEntity = {
+ name: pathPart,
+ type: 'folder',
+ children: [],
+ }
+ currentFileTreeLocation.push(fileTreeEntity)
+ }
+ currentFileTreeLocation = fileTreeEntity.children ?? []
+ }
+ }
+ return currentFileTree
+}
+
+type HistoryFileTree = {
+ docs?: Doc[]
+ folders: HistoryFileTree[]
+ name: string
+ // `id` and `fileRefs` are both required from react file tree.
+ // TODO: update react file tree to make the data optional so we can delete these keys
+ id: ''
+ fileRefs: []
+}
+
+export function fileTreeDiffToFileTreeData(
+ fileTreeDiff: FileTreeEntity[],
+ currentFolderName = 'rootFolder' // default value from angular version
+): HistoryFileTree {
+ const folders: HistoryFileTree[] = []
+ const docs: Doc[] = []
+
+ for (const file of fileTreeDiff) {
+ if (file.type === 'file') {
+ docs.push({
+ _id: '',
+ name: file.name ?? '',
+ })
+ } else if (file.type === 'folder') {
+ if (file.children) {
+ const folder = fileTreeDiffToFileTreeData(file.children, file.name)
+ folders.push(folder)
+ }
+ }
+ }
+
+ return {
+ docs,
+ folders,
+ name: currentFolderName,
+ id: '',
+ fileRefs: [],
+ }
+}
+
+// TODO: refactor the oldPathname/newPathname data
+// It's an artifact from the angular version.
+// Our API returns `pathname` and `newPathname` for `renamed` operation
+// In the angular version, we change the key of the data:
+// 1. `pathname` -> `oldPathname`
+// 2. `newPathname` -> `pathname`
+// 3. Delete the `newPathname` key from the object
+// This is because the angular version wants to generalize the API usage
+// In the operation other than the `renamed` operation, the diff API (/project/:id/diff) consumes the `pathname`
+// But the `renamed` operation consumes the `newPathname` instead of the `pathname` data
+//
+// This behaviour can be refactored by introducing a conditional when calling the API
+// i.e if `renamed` -> use `newPathname`, else -> use `pathname`
+export function renamePathnameKey(file: FileRenamed): FileRenamed {
+ return {
+ oldPathname: file.pathname,
+ pathname: file.newPathname as string,
+ operation: file.operation,
+ }
+}
+
+export function isFileRenamed(fileDiff: FileDiff): fileDiff is FileRenamed {
+ return (fileDiff as FileRenamed).operation === 'renamed'
+}
diff --git a/services/web/frontend/js/ide.js b/services/web/frontend/js/ide.js
index aef77df081..160f76ff7c 100644
--- a/services/web/frontend/js/ide.js
+++ b/services/web/frontend/js/ide.js
@@ -69,6 +69,7 @@ import './features/source-editor/controllers/grammarly-warning-controller'
import './features/outline/controllers/documentation-button-controller'
import './features/onboarding/controllers/onboarding-video-tour-modal-controller'
import './features/history/controllers/history-controller'
+import './features/history/controllers/history-file-tree-controller'
import { cleanupServiceWorker } from './utils/service-worker-cleanup'
import { reportCM6Perf } from './infrastructure/cm6-performance'
import { reportAcePerf } from './ide/editor/ace-performance'
diff --git a/services/web/types/file-tree.ts b/services/web/types/file-tree.ts
new file mode 100644
index 0000000000..b92950451c
--- /dev/null
+++ b/services/web/types/file-tree.ts
@@ -0,0 +1,10 @@
+import type { FileRef } from './file-ref'
+import type { Doc } from './doc'
+
+export type FileTree = {
+ _id: string
+ name: string
+ folders: FileTree[]
+ fileRefs: FileRef[]
+ docs: Doc[]
+}