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 => ( -
  1. {file.pathname}
  2. - ))} -
- ) : ( -
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[] +}