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
This commit is contained in:
M Fahru 2023-04-11 11:08:56 -07:00 committed by Copybot
parent e780a09a15
commit fb6746a887
17 changed files with 1039 additions and 152 deletions

View file

@ -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 (
<li
role="treeitem"
{...selectableEntityProps}
className={classNames({ selected: isSelected })}
onClick={onClick}
onKeyDown={onClick}
aria-selected={isSelected}
aria-label={name}
tabIndex={0}
>

View file

@ -24,7 +24,7 @@ export default function HistoryFileTreeFolderList({
{folders.sort(compareFunction).map(folder => {
return (
<HistoryFileTreeFolder
key={folder._id}
key={folder.name}
name={folder.name}
folders={folder.folders}
docs={folder.docs ?? []}
@ -34,9 +34,9 @@ export default function HistoryFileTreeFolderList({
{docs.sort(compareFunction).map(doc => {
return (
<HistoryFileTreeDoc
key={doc._id}
key={doc.pathname}
name={doc.name}
id={doc._id}
pathname={doc.pathname}
operation={doc.operation}
/>
)

View file

@ -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({

View file

@ -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

View file

@ -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 (
<HistoryFileTreeSelectableProvider onSelectFile={handleSelectFile}>
<HistoryFileTreeFolderList
folders={mappedFileTree.folders}
docs={mappedFileTree.docs ?? []}
rootClassName="file-tree-list"
>
<li className="bottom-buffer" />
</HistoryFileTreeFolderList>
</HistoryFileTreeSelectableProvider>
<HistoryFileTreeFolderList
folders={mappedFileTree.folders}
docs={mappedFileTree.docs ?? []}
rootClassName="file-tree-list"
>
<li className="bottom-buffer" />
</HistoryFileTreeFolderList>
)
}

View file

@ -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<LoadedUpdate[]>([])
const [loadingFileTree, setLoadingFileTree] =
useState<HistoryContextValue['loadingFileTree']>(true)
// eslint-disable-next-line no-unused-vars
const [viewMode, setViewMode] =
useState<HistoryContextValue['viewMode']>('point_in_time')
const [nextBeforeTimestamp, setNextBeforeTimestamp] =
useState<HistoryContextValue['nextBeforeTimestamp']>()
const [atEnd, setAtEnd] = useState<HistoryContextValue['atEnd']>(false)
@ -43,8 +47,7 @@ function useHistory() {
const [labels, setLabels] = useState<HistoryContextValue['labels']>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
/* eslint-disable no-unused-vars */
const [viewMode, setViewMode] = useState<HistoryContextValue['viewMode']>('')
// eslint-disable-next-line no-unused-vars
const [userHasFullFeature, setUserHasFullFeature] =
useState<HistoryContextValue['userHasFullFeature']>(undefined)
const [selection, setSelection] = useState<HistoryContextValue['selection']>({
@ -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<HistoryContextValue>(
() => ({

View file

@ -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<string>
}
const FileTreeSelectableContext = createContext<Context>({
select: () => {},
selectedFile: null,
})
type HistoryFileTreeSelectableProviderProps = {
onSelectFile: (id: string) => void
children: ReactNode
}
export function HistoryFileTreeSelectableProvider({
onSelectFile,
children,
}: HistoryFileTreeSelectableProviderProps) {
const [selectedFile, setSelectedFile] =
useState<Context['selectedFile']>(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 (
<FileTreeSelectableContext.Provider value={value}>
{children}
</FileTreeSelectableContext.Provider>
)
}
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 }
}

View file

@ -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 }
}

View file

@ -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

View file

@ -1,23 +1,35 @@
import { DiffOperation } from './diff-operation'
export interface FileUnchanged {
pathname: string
}
export interface FileAdded extends FileUnchanged {
operation: 'added'
operation: Extract<DiffOperation, 'added'>
}
export interface FileRemoved extends FileUnchanged {
operation: 'removed'
operation: Extract<DiffOperation, 'removed'>
newPathname?: string
deletedAtV: number
}
export interface FileEdited extends FileUnchanged {
operation: Extract<DiffOperation, 'edited'>
}
export interface FileRenamed extends FileUnchanged {
newPathname?: string
oldPathname?: string
operation: 'renamed'
operation: Extract<DiffOperation, 'renamed'>
}
export type FileDiff = FileAdded | FileRemoved | FileRenamed | FileUnchanged
export type FileDiff =
| FileAdded
| FileRemoved
| FileEdited
| FileRenamed
| FileUnchanged
export interface FileSelection {
files: FileDiff[]

View file

@ -0,0 +1 @@
export type ViewMode = 'compare' | 'point_in_time'

View file

@ -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<FileDiff>,
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<Update> {
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<FileWithOps> = 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
},
<FileWithOps[]>[]
)
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<FileDiff> = 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)
}

View file

@ -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<FileTreeEntity, 'operation'>
export type HistoryDoc = {
pathname: string
name: string
} & Pick<FileTreeEntity, 'operation'>
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,
}
}

View file

@ -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')
})
})
})

View file

@ -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[]
}

View file

@ -1,10 +1,11 @@
import { FileDiff } from '../../frontend/js/features/history/services/types/file'
import { Nullable } from '../utils'
type Docs = Record<string, unknown>
interface Range {
fromV: Nullable<unknown>
toV: Nullable<unknown>
fromV: Nullable<number>
toV: Nullable<number>
}
interface HoveredRange {
@ -18,6 +19,6 @@ export interface Selection {
range: Range
hoveredRange: HoveredRange
diff: Nullable<unknown>
files: unknown[]
files: FileDiff[]
file: Nullable<unknown>
}