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
}