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 }