From fb6746a8875b91683e01dbb8e94c7e719b6c45fa Mon Sep 17 00:00:00 2001 From: M Fahru Date: Tue, 11 Apr 2023 11:08:56 -0700 Subject: [PATCH] Add default pathname logic on history react file tree (#12505) On history react, when the initial screen has been loaded, no default pathname is selected. This PR adds logic for selecting default pathname and getting the diff for that pathname. Also, add some other small changes, the notable ones are: - Refactor some type naming and file structure related to the history file tree - Refactor file tree selectable hooks (merge selectable context provider into the main provider) - prevent clicking on the same file tree item by checking the current pathname before invoking the handler function GitOrigin-RevId: 73c36e9ed918ae3d92dd47108fbe8542a7571bdd --- .../file-tree/history-file-tree-doc.tsx | 16 +- .../history-file-tree-folder-list.tsx | 6 +- .../file-tree/history-file-tree-folder.tsx | 5 +- .../file-tree/history-file-tree-item.tsx | 2 +- .../history/components/history-file-tree.tsx | 29 +- .../history/context/history-context.tsx | 42 +- .../context/history-file-tree-selectable.tsx | 87 -- .../hooks/use-file-tree-item-selection.tsx | 22 + .../context/types/history-context-value.ts | 3 +- .../types/{file-tree.ts => diff-operation.ts} | 0 .../features/history/services/types/file.ts | 20 +- .../history/services/types/view-mode.ts | 1 + .../history/utils/auto-select-file.ts | 152 ++++ .../js/features/history/utils/file-tree.ts | 12 +- .../history/utils/auto-select-file.test.ts | 777 ++++++++++++++++++ services/web/types/file-tree.ts | 10 - services/web/types/history/selection.ts | 7 +- 17 files changed, 1039 insertions(+), 152 deletions(-) delete mode 100644 services/web/frontend/js/features/history/context/history-file-tree-selectable.tsx create mode 100644 services/web/frontend/js/features/history/context/hooks/use-file-tree-item-selection.tsx rename services/web/frontend/js/features/history/services/types/{file-tree.ts => diff-operation.ts} (100%) create mode 100644 services/web/frontend/js/features/history/services/types/view-mode.ts create mode 100644 services/web/frontend/js/features/history/utils/auto-select-file.ts create mode 100644 services/web/test/frontend/features/history/utils/auto-select-file.test.ts delete mode 100644 services/web/types/file-tree.ts diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx index d121ab067d..c2e8404a68 100644 --- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx +++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx @@ -1,26 +1,30 @@ import HistoryFileTreeItem from './history-file-tree-item' import iconTypeFromName from '../../../file-tree/util/icon-type-from-name' import Icon from '../../../../shared/components/icon' -import { useSelectableEntity } from '../../context/history-file-tree-selectable' -import { DiffOperation } from '../../services/types/file-tree' +import { useFileTreeItemSelection } from '../../context/hooks/use-file-tree-item-selection' +import { DiffOperation } from '../../services/types/diff-operation' +import classNames from 'classnames' type HistoryFileTreeDocProps = { name: string - id: string + pathname: string operation?: DiffOperation } export default function HistoryFileTreeDoc({ name, - id, + pathname, operation, }: HistoryFileTreeDocProps) { - const { props: selectableEntityProps } = useSelectableEntity(id) + const { isSelected, onClick } = useFileTreeItemSelection(pathname) return (
  • diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx index 5408ac2c0a..b88564901b 100644 --- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx +++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx @@ -24,7 +24,7 @@ export default function HistoryFileTreeFolderList({ {folders.sort(compareFunction).map(folder => { return ( { return ( ) diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx index 4307521836..d34389804d 100644 --- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx +++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx @@ -5,13 +5,12 @@ import HistoryFileTreeItem from './history-file-tree-item' import HistoryFileTreeFolderList from './history-file-tree-folder-list' import Icon from '../../../../shared/components/icon' -import type { Doc } from '../../../../../../types/doc' -import type { HistoryFileTree } from '../../utils/file-tree' +import type { HistoryDoc, HistoryFileTree } from '../../utils/file-tree' type HistoryFileTreeFolderProps = { name: string folders: HistoryFileTree[] - docs: Doc[] + docs: HistoryDoc[] } export default function HistoryFileTreeFolder({ diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx index 9aa69b77ab..cfb089eb21 100644 --- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx +++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { DiffOperation } from '../../services/types/file-tree' +import { DiffOperation } from '../../services/types/diff-operation' type FileTreeItemProps = { name: string 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 b8ebae1486..623926472f 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,7 +1,5 @@ import _ from 'lodash' -import { useCallback } from 'react' import { useHistoryContext } from '../context/history-context' -import { HistoryFileTreeSelectableProvider } from '../context/history-file-tree-selectable' import { fileTreeDiffToFileTreeData, reducePathsToTree, @@ -9,16 +7,7 @@ import { import HistoryFileTreeFolderList from './file-tree/history-file-tree-folder-list' export default function HistoryFileTree() { - const { fileSelection, setFileSelection } = useHistoryContext() - - const handleSelectFile = useCallback( - (pathname: string) => { - if (fileSelection) { - setFileSelection({ files: fileSelection.files, pathname }) - } - }, - [fileSelection, setFileSelection] - ) + const { fileSelection } = useHistoryContext() if (!fileSelection) { return null @@ -29,14 +18,12 @@ export default function HistoryFileTree() { const mappedFileTree = fileTreeDiffToFileTreeData(fileTree) return ( - - -
  • - - + +
  • + ) } 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 ab36cce25a..01ccd49a8a 100644 --- a/services/web/frontend/js/features/history/context/history-context.tsx +++ b/services/web/frontend/js/features/history/context/history-context.tsx @@ -14,6 +14,7 @@ import { HistoryContextValue } from './types/history-context-value' import { diffFiles, fetchLabels, fetchUpdates } from '../services/api' import { renamePathnameKey, isFileRenamed } from '../utils/file-tree' import { loadLabels } from '../utils/label' +import { autoSelectFile } from '../utils/auto-select-file' import ColorManager from '../../../ide/colors/ColorManager' import moment from 'moment' import * as eventTracking from '../../../infrastructure/event-tracking' @@ -35,6 +36,9 @@ function useHistory() { const [updates, setUpdates] = useState([]) const [loadingFileTree, setLoadingFileTree] = useState(true) + // eslint-disable-next-line no-unused-vars + const [viewMode, setViewMode] = + useState('point_in_time') const [nextBeforeTimestamp, setNextBeforeTimestamp] = useState() const [atEnd, setAtEnd] = useState(false) @@ -43,8 +47,7 @@ function useHistory() { const [labels, setLabels] = useState(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) - /* eslint-disable no-unused-vars */ - const [viewMode, setViewMode] = useState('') + // eslint-disable-next-line no-unused-vars const [userHasFullFeature, setUserHasFullFeature] = useState(undefined) const [selection, setSelection] = useState({ @@ -177,8 +180,14 @@ function useHistory() { const { fromV, toV } = updateSelection.update diffFiles(projectId, fromV, toV).then(({ diff: files }) => { + const defaultSelection = autoSelectFile( + files, + selection, + viewMode, + updates + ) // TODO Infer default file sensibly - const pathname = null + const pathname = defaultSelection.pathname const newFiles = files.map(file => { if (isFileRenamed(file) && file.newPathname) { return renamePathnameKey(file) @@ -188,14 +197,33 @@ function useHistory() { }) setFileSelection({ files: newFiles, pathname }) }) - }, [updateSelection, projectId]) + }, [updateSelection, projectId, selection, updates, viewMode]) - // Set update selection if there isn't one useEffect(() => { + // Set update selection if there isn't one if (updates.length && !updateSelection) { - setUpdateSelection({ update: updates[0], comparing: false }) + setUpdateSelection({ + update: { + ...updates[0], + // Set fromV and toV for initial load as the latest version + // This is to mimic angular history behaviour when selecting default pathname on initial history load + fromV: updates[0].toV, + }, + comparing: false, + }) } - }, [setUpdateSelection, updateSelection, updates]) + + // Set default selection if there isn't one + if (updates.length && selection.range.fromV === null) { + setSelection({ + ...selection, + range: { + fromV: updates[0].toV, + toV: updates[0].toV, + }, + }) + } + }, [setUpdateSelection, updateSelection, updates, selection]) const value = useMemo( () => ({ diff --git a/services/web/frontend/js/features/history/context/history-file-tree-selectable.tsx b/services/web/frontend/js/features/history/context/history-file-tree-selectable.tsx deleted file mode 100644 index a4dd5ad555..0000000000 --- a/services/web/frontend/js/features/history/context/history-file-tree-selectable.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { - createContext, - type ReactNode, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import classNames from 'classnames' -import _ from 'lodash' - -import usePreviousValue from '../../../shared/hooks/use-previous-value' -import { Nullable } from '../../../../../types/utils' - -type Context = { - select: (id: string) => void - selectedFile: Nullable -} - -const FileTreeSelectableContext = createContext({ - select: () => {}, - selectedFile: null, -}) - -type HistoryFileTreeSelectableProviderProps = { - onSelectFile: (id: string) => void - children: ReactNode -} - -export function HistoryFileTreeSelectableProvider({ - onSelectFile, - children, -}: HistoryFileTreeSelectableProviderProps) { - const [selectedFile, setSelectedFile] = - useState(null) - - const previousSelectedFile = usePreviousValue(selectedFile) - - useEffect(() => { - if (!selectedFile) { - return - } - - if (_.isEqual(selectedFile, previousSelectedFile)) { - return - } - - onSelectFile(selectedFile) - }, [selectedFile, previousSelectedFile, onSelectFile]) - - const select = useCallback(id => { - setSelectedFile(id) - }, []) - - const value = { - selectedFile, - select, - } - - return ( - - {children} - - ) -} - -export function useSelectableEntity(id: string) { - const { selectedFile, select } = useContext(FileTreeSelectableContext) - - const handleClick = useCallback(() => { - select(id) - }, [id, select]) - - const isSelected = selectedFile === id - - const props = useMemo( - () => ({ - className: classNames({ selected: isSelected }), - 'aria-selected': isSelected, - onClick: handleClick, - }), - [handleClick, isSelected] - ) - - return { isSelected, props } -} diff --git a/services/web/frontend/js/features/history/context/hooks/use-file-tree-item-selection.tsx b/services/web/frontend/js/features/history/context/hooks/use-file-tree-item-selection.tsx new file mode 100644 index 0000000000..d94b220748 --- /dev/null +++ b/services/web/frontend/js/features/history/context/hooks/use-file-tree-item-selection.tsx @@ -0,0 +1,22 @@ +import { useCallback, useMemo } from 'react' +import { useHistoryContext } from '../history-context' + +export function useFileTreeItemSelection(pathname: string) { + const { fileSelection, setFileSelection, selection } = useHistoryContext() + + const handleClick = useCallback(() => { + if (pathname !== fileSelection?.pathname) { + setFileSelection({ + files: fileSelection?.files || selection.files, + pathname, + }) + } + }, [fileSelection, pathname, selection, setFileSelection]) + + const isSelected = useMemo( + () => fileSelection?.pathname === pathname, + [fileSelection, pathname] + ) + + return { isSelected, onClick: handleClick } +} diff --git a/services/web/frontend/js/features/history/context/types/history-context-value.ts b/services/web/frontend/js/features/history/context/types/history-context-value.ts index dabcb7a11b..6bd4a8c589 100644 --- a/services/web/frontend/js/features/history/context/types/history-context-value.ts +++ b/services/web/frontend/js/features/history/context/types/history-context-value.ts @@ -7,10 +7,11 @@ import { } from '../../services/types/update' import { Selection } from '../../../../../../types/history/selection' import { FileSelection } from '../../services/types/file' +import { ViewMode } from '../../services/types/view-mode' export type HistoryContextValue = { updates: LoadedUpdate[] - viewMode: string + viewMode: ViewMode nextBeforeTimestamp: number | undefined atEnd: boolean userHasFullFeature: boolean | undefined diff --git a/services/web/frontend/js/features/history/services/types/file-tree.ts b/services/web/frontend/js/features/history/services/types/diff-operation.ts similarity index 100% rename from services/web/frontend/js/features/history/services/types/file-tree.ts rename to services/web/frontend/js/features/history/services/types/diff-operation.ts 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 d590e0e790..c507bbb278 100644 --- a/services/web/frontend/js/features/history/services/types/file.ts +++ b/services/web/frontend/js/features/history/services/types/file.ts @@ -1,23 +1,35 @@ +import { DiffOperation } from './diff-operation' + export interface FileUnchanged { pathname: string } export interface FileAdded extends FileUnchanged { - operation: 'added' + operation: Extract } export interface FileRemoved extends FileUnchanged { - operation: 'removed' + operation: Extract + newPathname?: string deletedAtV: number } +export interface FileEdited extends FileUnchanged { + operation: Extract +} + export interface FileRenamed extends FileUnchanged { newPathname?: string oldPathname?: string - operation: 'renamed' + operation: Extract } -export type FileDiff = FileAdded | FileRemoved | FileRenamed | FileUnchanged +export type FileDiff = + | FileAdded + | FileRemoved + | FileEdited + | FileRenamed + | FileUnchanged export interface FileSelection { files: FileDiff[] diff --git a/services/web/frontend/js/features/history/services/types/view-mode.ts b/services/web/frontend/js/features/history/services/types/view-mode.ts new file mode 100644 index 0000000000..194d1bfc74 --- /dev/null +++ b/services/web/frontend/js/features/history/services/types/view-mode.ts @@ -0,0 +1 @@ +export type ViewMode = 'compare' | 'point_in_time' diff --git a/services/web/frontend/js/features/history/utils/auto-select-file.ts b/services/web/frontend/js/features/history/utils/auto-select-file.ts new file mode 100644 index 0000000000..cfc26ff7ee --- /dev/null +++ b/services/web/frontend/js/features/history/utils/auto-select-file.ts @@ -0,0 +1,152 @@ +import _ from 'lodash' +import type { Nullable } from '../../../../../types/utils' +import type { HistoryContextValue } from '../context/types/history-context-value' +import type { FileDiff } from '../services/types/file' +import type { DiffOperation } from '../services/types/diff-operation' +import type { Update } from '../services/types/update' + +function selectFile( + file: Nullable, + selection: HistoryContextValue['selection'] +) { + if (file === null) { + return selection + } + + const newSelection = { + ...selection, + pathname: file.pathname, + file, + } + + return newSelection +} + +function getUpdateForVersion( + version: Update['toV'], + updates: HistoryContextValue['updates'] +): Nullable { + return updates.filter(update => update.toV === version)?.[0] ?? null +} + +type FileWithOps = { + pathname: FileDiff['pathname'] + operation: DiffOperation +} + +function getFilesWithOps( + viewMode: HistoryContextValue['viewMode'], + selection: HistoryContextValue['selection'], + updates: HistoryContextValue['updates'] +): FileWithOps[] { + if (selection.range.toV && viewMode === 'point_in_time') { + const filesWithOps: FileWithOps[] = [] + const currentUpdate = getUpdateForVersion(selection.range.toV, updates) + + if (currentUpdate !== null) { + for (const pathname of currentUpdate.pathnames) { + filesWithOps.push({ + pathname, + operation: 'edited', + }) + } + + for (const op of currentUpdate.project_ops) { + let fileWithOps: Nullable = null + + if (op.add) { + fileWithOps = { + pathname: op.add.pathname, + operation: 'added', + } + } else if (op.remove) { + fileWithOps = { + pathname: op.remove.pathname, + operation: 'removed', + } + } else if (op.rename) { + fileWithOps = { + pathname: op.rename.newPathname, + operation: 'renamed', + } + } + + if (fileWithOps !== null) { + filesWithOps.push(fileWithOps) + } + } + } + + return filesWithOps + } else { + const filesWithOps = _.reduce( + selection.files, + (curFilesWithOps, file) => { + if ('operation' in file) { + curFilesWithOps.push({ + pathname: file.pathname, + operation: file.operation, + }) + } + return curFilesWithOps + }, + [] + ) + + return filesWithOps + } +} + +const orderedOpTypes: DiffOperation[] = [ + 'edited', + 'added', + 'renamed', + 'removed', +] + +export function autoSelectFile( + files: FileDiff[], + selection: HistoryContextValue['selection'], + viewMode: HistoryContextValue['viewMode'], + updates: HistoryContextValue['updates'] +) { + let fileToSelect: Nullable = null + + const filesWithOps = getFilesWithOps(viewMode, selection, updates) + for (const opType of orderedOpTypes) { + const fileWithMatchingOpType = _.find(filesWithOps, { + operation: opType, + }) + + if (fileWithMatchingOpType != null) { + fileToSelect = + _.find(files, { + pathname: fileWithMatchingOpType.pathname, + }) ?? null + + break + } + } + + if (!fileToSelect) { + const mainFile = _.find(files, function (file) { + return /main\.tex$/.test(file.pathname) + }) + + if (mainFile) { + fileToSelect = mainFile + } else { + const anyTeXFile = _.find(files, function (file) { + return /\.tex$/.test(file.pathname) + }) + + if (anyTeXFile) { + fileToSelect = anyTeXFile + } else { + fileToSelect = files[0] + } + } + } + + return selectFile(fileToSelect, selection) +} diff --git a/services/web/frontend/js/features/history/utils/file-tree.ts b/services/web/frontend/js/features/history/utils/file-tree.ts index 47c99b6dbb..9c4586e603 100644 --- a/services/web/frontend/js/features/history/utils/file-tree.ts +++ b/services/web/frontend/js/features/history/utils/file-tree.ts @@ -1,7 +1,6 @@ import _ from 'lodash' -import type { Doc } from '../../../../../types/doc' import type { FileDiff, FileRenamed } from '../services/types/file' -import type { DiffOperation } from '../services/types/file-tree' +import type { DiffOperation } from '../services/types/diff-operation' // `Partial` because the `reducePathsToTree` function was copied directly // from a javascript file without proper type system and the logic is not typescript-friendly. @@ -49,13 +48,15 @@ export function reducePathsToTree( return currentFileTree } -export type HistoryDoc = Doc & Pick +export type HistoryDoc = { + pathname: string + name: string +} & Pick export type HistoryFileTree = { docs?: HistoryDoc[] folders: HistoryFileTree[] name: string - _id: string } export function fileTreeDiffToFileTreeData( @@ -68,7 +69,7 @@ export function fileTreeDiffToFileTreeData( for (const file of fileTreeDiff) { if (file.type === 'file') { docs.push({ - _id: file.pathname ?? '', + pathname: file.pathname ?? '', name: file.name ?? '', operation: file.operation, }) @@ -84,7 +85,6 @@ export function fileTreeDiffToFileTreeData( docs, folders, name: currentFolderName, - _id: currentFolderName, } } diff --git a/services/web/test/frontend/features/history/utils/auto-select-file.test.ts b/services/web/test/frontend/features/history/utils/auto-select-file.test.ts new file mode 100644 index 0000000000..2c2dfd5083 --- /dev/null +++ b/services/web/test/frontend/features/history/utils/auto-select-file.test.ts @@ -0,0 +1,777 @@ +import { expect } from 'chai' +import type { HistoryContextValue } from '../../../../../frontend/js/features/history/context/types/history-context-value' +import type { FileDiff } from '../../../../../frontend/js/features/history/services/types/file' +import { autoSelectFile } from '../../../../../frontend/js/features/history/utils/auto-select-file' +import type { User } from '../../../../../frontend/js/features/history/services/types/shared' + +describe('autoSelectFile', function () { + const historyUsers: User[] = [ + { + first_name: 'first_name', + last_name: 'last_name', + email: 'email@overleaf.com', + id: '6266xb6b7a366460a66186xx', + }, + ] + + const emptySelection: HistoryContextValue['selection'] = { + docs: {}, + pathname: null, + range: { + fromV: null, + toV: null, + }, + hoveredRange: { + fromV: null, + toV: null, + }, + diff: null, + files: [], + file: null, + } + + describe('for `point_in_time` view mode', function () { + const viewMode: HistoryContextValue['viewMode'] = 'point_in_time' + + it('return the file with `edited` as the last operation', function () { + const files: FileDiff[] = [ + { + pathname: 'main.tex', + }, + { + pathname: 'sample.bib', + }, + { + pathname: 'frog.jpg', + }, + { + pathname: 'newfile5.tex', + }, + { + pathname: 'newfolder1/newfolder2/newfile2.tex', + }, + { + pathname: 'newfolder1/newfile10.tex', + operation: 'edited', + }, + ] + + const selection: HistoryContextValue['selection'] = { + ...emptySelection, + range: { + fromV: 26, + toV: 26, + }, + } + + const updates: HistoryContextValue['updates'] = [ + { + fromV: 25, + toV: 26, + meta: { + users: historyUsers, + start_ts: 1680888731881, + end_ts: 1680888731881, + }, + labels: [], + pathnames: ['newfolder1/newfile10.tex'], + project_ops: [], + }, + { + fromV: 23, + toV: 25, + meta: { + users: historyUsers, + start_ts: 1680888725098, + end_ts: 1680888729123, + }, + labels: [], + pathnames: [], + project_ops: [ + { + rename: { + pathname: 'newfolder1/newfile3.tex', + newPathname: 'newfolder1/newfile10.tex', + }, + atV: 24, + }, + { + rename: { + pathname: 'newfile3.tex', + newPathname: 'newfolder1/newfile3.tex', + }, + atV: 23, + }, + ], + }, + { + fromV: 22, + toV: 23, + meta: { + users: historyUsers, + start_ts: 1680888721015, + end_ts: 1680888721015, + }, + labels: [], + pathnames: ['newfile3.tex'], + project_ops: [], + }, + { + fromV: 19, + toV: 22, + meta: { + users: historyUsers, + start_ts: 1680888715364, + end_ts: 1680888718726, + }, + labels: [], + pathnames: [], + project_ops: [ + { + rename: { + pathname: 'newfolder1/newfolder2/newfile3.tex', + newPathname: 'newfile3.tex', + }, + atV: 21, + }, + { + rename: { + pathname: 'newfolder1/newfile2.tex', + newPathname: 'newfolder1/newfolder2/newfile2.tex', + }, + atV: 20, + }, + { + rename: { + pathname: 'newfolder1/newfile5.tex', + newPathname: 'newfile5.tex', + }, + atV: 19, + }, + ], + }, + { + fromV: 16, + toV: 19, + meta: { + users: historyUsers, + start_ts: 1680888705042, + end_ts: 1680888712662, + }, + labels: [], + pathnames: [ + 'main.tex', + 'newfolder1/newfile2.tex', + 'newfolder1/newfile5.tex', + ], + project_ops: [], + }, + { + fromV: 0, + toV: 16, + meta: { + users: historyUsers, + start_ts: 1680888456499, + end_ts: 1680888640774, + }, + labels: [], + pathnames: [], + project_ops: [ + { + add: { + pathname: 'newfolder1/newfile2.tex', + }, + atV: 15, + }, + { + remove: { + pathname: 'newfile2.tex', + }, + atV: 14, + }, + { + rename: { + pathname: 'newfolder1/frog.jpg', + newPathname: 'frog.jpg', + }, + atV: 13, + }, + { + rename: { + pathname: 'newfolder1/newfile2.tex', + newPathname: 'newfile2.tex', + }, + atV: 12, + }, + { + rename: { + pathname: 'newfile5.tex', + newPathname: 'newfolder1/newfile5.tex', + }, + atV: 11, + }, + { + rename: { + pathname: 'newfile4.tex', + newPathname: 'newfile5.tex', + }, + atV: 10, + }, + { + add: { + pathname: 'newfile4.tex', + }, + atV: 9, + }, + { + remove: { + pathname: 'newfolder1/newfolder2/newfile1.tex', + }, + atV: 8, + }, + { + rename: { + pathname: 'frog.jpg', + newPathname: 'newfolder1/frog.jpg', + }, + atV: 7, + }, + { + add: { + pathname: 'newfolder1/newfolder2/newfile3.tex', + }, + atV: 6, + }, + { + add: { + pathname: 'newfolder1/newfile2.tex', + }, + atV: 5, + }, + { + rename: { + pathname: 'newfolder1/newfile1.tex', + newPathname: 'newfolder1/newfolder2/newfile1.tex', + }, + atV: 4, + }, + { + add: { + pathname: 'newfolder1/newfile1.tex', + }, + atV: 3, + }, + { + add: { + pathname: 'frog.jpg', + }, + atV: 2, + }, + { + add: { + pathname: 'sample.bib', + }, + atV: 1, + }, + { + add: { + pathname: 'main.tex', + }, + atV: 0, + }, + ], + }, + ] + + const defaultSelection = autoSelectFile( + files, + selection, + viewMode, + updates + ) + + expect(defaultSelection.pathname).to.equal('newfolder1/newfile10.tex') + }) + it('return file with `added` operation on highest `atV` value if no other operation is available on the latest `updates` entry', function () { + const files: FileDiff[] = [ + { + pathname: 'main.tex', + operation: 'added', + }, + { + pathname: 'sample.bib', + operation: 'added', + }, + { + pathname: 'frog.jpg', + operation: 'added', + }, + { + pathname: 'newfile1.tex', + operation: 'added', + }, + ] + + const selection: HistoryContextValue['selection'] = { + ...emptySelection, + range: { + fromV: 4, + toV: 4, + }, + } + + const updates: HistoryContextValue['updates'] = [ + { + fromV: 0, + toV: 4, + meta: { + users: historyUsers, + start_ts: 1680861468999, + end_ts: 1680861491861, + }, + labels: [], + pathnames: [], + project_ops: [ + { + add: { + pathname: 'newfile1.tex', + }, + atV: 3, + }, + { + add: { + pathname: 'frog.jpg', + }, + atV: 2, + }, + { + add: { + pathname: 'sample.bib', + }, + atV: 1, + }, + { + add: { + pathname: 'main.tex', + }, + atV: 0, + }, + ], + }, + ] + + const defaultSelection = autoSelectFile( + files, + selection, + viewMode, + updates + ) + + expect(defaultSelection.pathname).to.equal('newfile1.tex') + }) + + it('return the last non-`removed` operation with the highest `atV` value', function () { + const files: FileDiff[] = [ + { + pathname: 'main.tex', + operation: 'removed', + deletedAtV: 6, + }, + { + pathname: 'sample.bib', + }, + { + pathname: 'main2.tex', + operation: 'added', + }, + { + pathname: 'main3.tex', + operation: 'added', + }, + ] + + const selection: HistoryContextValue['selection'] = { + ...emptySelection, + range: { + fromV: 7, + toV: 7, + }, + } + + const updates: HistoryContextValue['updates'] = [ + { + fromV: 4, + toV: 7, + meta: { + users: historyUsers, + start_ts: 1680874742389, + end_ts: 1680874755552, + }, + labels: [], + pathnames: [], + project_ops: [ + { + remove: { + pathname: 'main.tex', + }, + atV: 6, + }, + { + add: { + pathname: 'main3.tex', + }, + atV: 5, + }, + { + add: { + pathname: 'main2.tex', + }, + atV: 4, + }, + ], + }, + { + fromV: 0, + toV: 4, + meta: { + users: historyUsers, + start_ts: 1680861975947, + end_ts: 1680861988442, + }, + labels: [], + pathnames: [], + project_ops: [ + { + remove: { + pathname: 'frog.jpg', + }, + atV: 3, + }, + { + add: { + pathname: 'frog.jpg', + }, + atV: 2, + }, + { + add: { + pathname: 'sample.bib', + }, + atV: 1, + }, + { + add: { + pathname: 'main.tex', + }, + atV: 0, + }, + ], + }, + ] + + const defaultSelection = autoSelectFile( + files, + selection, + viewMode, + updates + ) + + expect(defaultSelection.pathname).to.equal('main3.tex') + }) + + it('if `removed` is the last operation, and no other operation is available on the latest `updates` entry, with `main.tex` available as a file name somewhere in the file tree, return `main.tex`', function () { + const files: FileDiff[] = [ + { + pathname: 'main.tex', + }, + { + pathname: 'sample.bib', + }, + { + pathname: 'frog.jpg', + }, + { + pathname: 'newfolder/maybewillbedeleted.tex', + newPathname: 'newfolder2/maybewillbedeleted.tex', + operation: 'removed', + deletedAtV: 10, + }, + ] + + const selection: HistoryContextValue['selection'] = { + ...emptySelection, + range: { + fromV: 11, + toV: 11, + }, + } + + const updates: HistoryContextValue['updates'] = [ + { + fromV: 9, + toV: 11, + meta: { + users: historyUsers, + start_ts: 1680904414419, + end_ts: 1680904417538, + }, + labels: [], + pathnames: [], + project_ops: [ + { + remove: { + pathname: 'newfolder2/maybewillbedeleted.tex', + }, + atV: 10, + }, + { + rename: { + pathname: 'newfolder/maybewillbedeleted.tex', + newPathname: 'newfolder2/maybewillbedeleted.tex', + }, + atV: 9, + }, + ], + }, + { + fromV: 8, + toV: 9, + meta: { + users: historyUsers, + start_ts: 1680904410333, + end_ts: 1680904410333, + }, + labels: [], + pathnames: ['newfolder/maybewillbedeleted.tex'], + project_ops: [], + }, + { + fromV: 7, + toV: 8, + meta: { + users: historyUsers, + start_ts: 1680904407448, + end_ts: 1680904407448, + }, + labels: [], + pathnames: [], + project_ops: [ + { + rename: { + pathname: 'newfolder/tobedeleted.tex', + newPathname: 'newfolder/maybewillbedeleted.tex', + }, + atV: 7, + }, + ], + }, + { + fromV: 6, + toV: 7, + meta: { + users: historyUsers, + start_ts: 1680904400839, + end_ts: 1680904400839, + }, + labels: [], + pathnames: ['newfolder/tobedeleted.tex'], + project_ops: [], + }, + { + fromV: 5, + toV: 6, + meta: { + users: historyUsers, + start_ts: 1680904398544, + end_ts: 1680904398544, + }, + labels: [], + pathnames: [], + project_ops: [ + { + rename: { + pathname: 'tobedeleted.tex', + newPathname: 'newfolder/tobedeleted.tex', + }, + atV: 5, + }, + ], + }, + { + fromV: 4, + toV: 5, + meta: { + users: historyUsers, + start_ts: 1680904389891, + end_ts: 1680904389891, + }, + labels: [], + pathnames: ['tobedeleted.tex'], + project_ops: [], + }, + { + fromV: 0, + toV: 4, + meta: { + users: historyUsers, + start_ts: 1680904363778, + end_ts: 1680904385308, + }, + labels: [], + pathnames: [], + project_ops: [ + { + add: { + pathname: 'tobedeleted.tex', + }, + atV: 3, + }, + { + add: { + pathname: 'frog.jpg', + }, + atV: 2, + }, + { + add: { + pathname: 'sample.bib', + }, + atV: 1, + }, + { + add: { + pathname: 'main.tex', + }, + atV: 0, + }, + ], + }, + ] + + const defaultSelection = autoSelectFile( + files, + selection, + viewMode, + updates + ) + + expect(defaultSelection.pathname).to.equal('main.tex') + }) + + it('if `removed` is the last operation, and no other operation is available on the latest `updates` entry, with `main.tex` is not available as a file name somewhere in the file tree, return any tex file based on ascending alphabetical order', function () { + const files: FileDiff[] = [ + { + pathname: 'certainly_not_main.tex', + }, + { + pathname: 'newfile.tex', + }, + { + pathname: 'file2.tex', + }, + ] + + const selection: HistoryContextValue['selection'] = { + ...emptySelection, + range: { + fromV: 8, + toV: 8, + }, + } + + const updates: HistoryContextValue['updates'] = [ + { + fromV: 7, + toV: 8, + meta: { + users: historyUsers, + start_ts: 1680905536168, + end_ts: 1680905536168, + }, + labels: [], + pathnames: [], + project_ops: [ + { + remove: { + pathname: 'newfolder/tobedeleted.txt', + }, + atV: 7, + }, + ], + }, + { + fromV: 6, + toV: 7, + meta: { + users: historyUsers, + start_ts: 1680905531816, + end_ts: 1680905531816, + }, + labels: [], + pathnames: ['newfolder/tobedeleted.txt'], + project_ops: [], + }, + { + fromV: 0, + toV: 6, + meta: { + users: historyUsers, + start_ts: 1680905492130, + end_ts: 1680905529186, + }, + labels: [], + pathnames: [], + project_ops: [ + { + rename: { + pathname: 'tobedeleted.txt', + newPathname: 'newfolder/tobedeleted.txt', + }, + atV: 5, + }, + { + add: { + pathname: 'file2.tex', + }, + atV: 4, + }, + { + add: { + pathname: 'newfile.tex', + }, + atV: 3, + }, + { + add: { + pathname: 'tobedeleted.txt', + }, + atV: 2, + }, + { + rename: { + pathname: 'main.tex', + newPathname: 'certainly_not_main.tex', + }, + atV: 1, + }, + { + add: { + pathname: 'main.tex', + }, + atV: 0, + }, + ], + }, + ] + + const defaultSelection = autoSelectFile( + files, + selection, + viewMode, + updates + ) + + expect(defaultSelection.pathname).to.equal('certainly_not_main.tex') + }) + }) +}) diff --git a/services/web/types/file-tree.ts b/services/web/types/file-tree.ts deleted file mode 100644 index b92950451c..0000000000 --- a/services/web/types/file-tree.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { FileRef } from './file-ref' -import type { Doc } from './doc' - -export type FileTree = { - _id: string - name: string - folders: FileTree[] - fileRefs: FileRef[] - docs: Doc[] -} diff --git a/services/web/types/history/selection.ts b/services/web/types/history/selection.ts index e7effcbb42..a040d6cdff 100644 --- a/services/web/types/history/selection.ts +++ b/services/web/types/history/selection.ts @@ -1,10 +1,11 @@ +import { FileDiff } from '../../frontend/js/features/history/services/types/file' import { Nullable } from '../utils' type Docs = Record interface Range { - fromV: Nullable - toV: Nullable + fromV: Nullable + toV: Nullable } interface HoveredRange { @@ -18,6 +19,6 @@ export interface Selection { range: Range hoveredRange: HoveredRange diff: Nullable - files: unknown[] + files: FileDiff[] file: Nullable }