Merge pull request #12636 from overleaf/td-history-autoselect-file

Simplify file autoselect in React history view

GitOrigin-RevId: 43f070b26ab0a75af13063712b5cf05778da4064
This commit is contained in:
Tim Down 2023-04-14 12:57:41 +02:00 committed by Copybot
parent 38998afa8e
commit 23b9c078b8
6 changed files with 64 additions and 176 deletions

View file

@ -37,8 +37,6 @@ function useHistory() {
const [loadingFileTree, setLoadingFileTree] = const [loadingFileTree, setLoadingFileTree] =
useState<HistoryContextValue['loadingFileTree']>(true) useState<HistoryContextValue['loadingFileTree']>(true)
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const [viewMode, setViewMode] =
useState<HistoryContextValue['viewMode']>('point_in_time')
const [nextBeforeTimestamp, setNextBeforeTimestamp] = const [nextBeforeTimestamp, setNextBeforeTimestamp] =
useState<HistoryContextValue['nextBeforeTimestamp']>() useState<HistoryContextValue['nextBeforeTimestamp']>()
const [atEnd, setAtEnd] = useState<HistoryContextValue['atEnd']>(false) const [atEnd, setAtEnd] = useState<HistoryContextValue['atEnd']>(false)
@ -50,21 +48,6 @@ function useHistory() {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const [userHasFullFeature, setUserHasFullFeature] = const [userHasFullFeature, setUserHasFullFeature] =
useState<HistoryContextValue['userHasFullFeature']>(undefined) useState<HistoryContextValue['userHasFullFeature']>(undefined)
const [selection, setSelection] = useState<HistoryContextValue['selection']>({
docs: {},
pathname: null,
range: {
fromV: null,
toV: null,
},
hoveredRange: {
fromV: null,
toV: null,
},
diff: null,
files: [],
file: null,
})
/* eslint-enable no-unused-vars */ /* eslint-enable no-unused-vars */
const fetchNextBatchOfUpdates = useCallback(() => { const fetchNextBatchOfUpdates = useCallback(() => {
@ -180,14 +163,7 @@ function useHistory() {
const { fromV, toV } = updateSelection.update const { fromV, toV } = updateSelection.update
diffFiles(projectId, fromV, toV).then(({ diff: files }) => { diffFiles(projectId, fromV, toV).then(({ diff: files }) => {
const defaultSelection = autoSelectFile( const pathname = autoSelectFile(files, updateSelection, updates)
files,
selection,
viewMode,
updates
)
// TODO Infer default file sensibly
const pathname = defaultSelection.pathname
const newFiles = files.map(file => { const newFiles = files.map(file => {
if (isFileRenamed(file) && file.newPathname) { if (isFileRenamed(file) && file.newPathname) {
return renamePathnameKey(file) return renamePathnameKey(file)
@ -197,33 +173,17 @@ function useHistory() {
}) })
setFileSelection({ files: newFiles, pathname }) setFileSelection({ files: newFiles, pathname })
}) })
}, [updateSelection, projectId, selection, updates, viewMode]) }, [updateSelection, projectId, updates])
useEffect(() => { useEffect(() => {
// Set update selection if there isn't one // Set update selection if there isn't one
if (updates.length && !updateSelection) { if (updates.length && !updateSelection) {
setUpdateSelection({ setUpdateSelection({
update: { update: updates[0],
...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, 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>( const value = useMemo<HistoryContextValue>(
() => ({ () => ({
@ -234,10 +194,8 @@ function useHistory() {
labels, labels,
loadingFileTree, loadingFileTree,
nextBeforeTimestamp, nextBeforeTimestamp,
selection,
updates, updates,
userHasFullFeature, userHasFullFeature,
viewMode,
projectId, projectId,
fileSelection, fileSelection,
setFileSelection, setFileSelection,
@ -252,10 +210,8 @@ function useHistory() {
labels, labels,
loadingFileTree, loadingFileTree,
nextBeforeTimestamp, nextBeforeTimestamp,
selection,
updates, updates,
userHasFullFeature, userHasFullFeature,
viewMode,
projectId, projectId,
fileSelection, fileSelection,
setFileSelection, setFileSelection,

View file

@ -2,16 +2,19 @@ import { useCallback, useMemo } from 'react'
import { useHistoryContext } from '../history-context' import { useHistoryContext } from '../history-context'
export function useFileTreeItemSelection(pathname: string) { export function useFileTreeItemSelection(pathname: string) {
const { fileSelection, setFileSelection, selection } = useHistoryContext() const { fileSelection, setFileSelection } = useHistoryContext()
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (pathname !== fileSelection?.pathname) { if (!fileSelection) {
return
}
if (pathname !== fileSelection.pathname) {
setFileSelection({ setFileSelection({
files: fileSelection?.files || selection.files, files: fileSelection.files,
pathname, pathname,
}) })
} }
}, [fileSelection, pathname, selection, setFileSelection]) }, [fileSelection, pathname, setFileSelection])
const isSelected = useMemo( const isSelected = useMemo(
() => fileSelection?.pathname === pathname, () => fileSelection?.pathname === pathname,

View file

@ -1,18 +1,14 @@
import { Nullable } from '../../../../../../types/utils' import { Nullable } from '../../../../../../types/utils'
import { LoadedUpdate, UpdateSelection } from '../../services/types/update' import { LoadedUpdate, UpdateSelection } from '../../services/types/update'
import { LoadedLabel } from '../../services/types/label' import { LoadedLabel } from '../../services/types/label'
import { Selection } from '../../../../../../types/history/selection'
import { FileSelection } from '../../services/types/file' import { FileSelection } from '../../services/types/file'
import { ViewMode } from '../../services/types/view-mode'
export type HistoryContextValue = { export type HistoryContextValue = {
updates: LoadedUpdate[] updates: LoadedUpdate[]
viewMode: ViewMode
nextBeforeTimestamp: number | undefined nextBeforeTimestamp: number | undefined
atEnd: boolean atEnd: boolean
userHasFullFeature: boolean | undefined userHasFullFeature: boolean | undefined
freeHistoryLimitHit: boolean freeHistoryLimitHit: boolean
selection: Selection
isLoading: boolean isLoading: boolean
error: Nullable<unknown> error: Nullable<unknown>
labels: Nullable<LoadedLabel[]> labels: Nullable<LoadedLabel[]>

View file

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

View file

@ -5,23 +5,6 @@ import type { FileDiff } from '../services/types/file'
import type { DiffOperation } from '../services/types/diff-operation' import type { DiffOperation } from '../services/types/diff-operation'
import type { Update } from '../services/types/update' 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( function getUpdateForVersion(
version: Update['toV'], version: Update['toV'],
updates: HistoryContextValue['updates'] updates: HistoryContextValue['updates']
@ -35,13 +18,19 @@ type FileWithOps = {
} }
function getFilesWithOps( function getFilesWithOps(
viewMode: HistoryContextValue['viewMode'], files: FileDiff[],
selection: HistoryContextValue['selection'], updateSelection: HistoryContextValue['updateSelection'],
updates: HistoryContextValue['updates'] updates: HistoryContextValue['updates']
): FileWithOps[] { ): FileWithOps[] {
if (selection.range.toV && viewMode === 'point_in_time') { if (!updateSelection) {
return []
}
if (updateSelection.update.toV && !updateSelection.comparing) {
const filesWithOps: FileWithOps[] = [] const filesWithOps: FileWithOps[] = []
const currentUpdate = getUpdateForVersion(selection.range.toV, updates) const currentUpdate = getUpdateForVersion(
updateSelection.update.toV,
updates
)
if (currentUpdate !== null) { if (currentUpdate !== null) {
for (const pathname of currentUpdate.pathnames) { for (const pathname of currentUpdate.pathnames) {
@ -80,7 +69,7 @@ function getFilesWithOps(
return filesWithOps return filesWithOps
} else { } else {
const filesWithOps = _.reduce( const filesWithOps = _.reduce(
selection.files, files,
(curFilesWithOps, file) => { (curFilesWithOps, file) => {
if ('operation' in file) { if ('operation' in file) {
curFilesWithOps.push({ curFilesWithOps.push({
@ -106,13 +95,12 @@ const orderedOpTypes: DiffOperation[] = [
export function autoSelectFile( export function autoSelectFile(
files: FileDiff[], files: FileDiff[],
selection: HistoryContextValue['selection'], updateSelection: HistoryContextValue['updateSelection'],
viewMode: HistoryContextValue['viewMode'],
updates: HistoryContextValue['updates'] updates: HistoryContextValue['updates']
) { ) {
let fileToSelect: Nullable<FileDiff> = null let fileToSelect: Nullable<FileDiff> = null
const filesWithOps = getFilesWithOps(viewMode, selection, updates) const filesWithOps = getFilesWithOps(files, updateSelection, updates)
for (const opType of orderedOpTypes) { for (const opType of orderedOpTypes) {
const fileWithMatchingOpType = _.find(filesWithOps, { const fileWithMatchingOpType = _.find(filesWithOps, {
operation: opType, operation: opType,
@ -148,5 +136,5 @@ export function autoSelectFile(
} }
} }
return selectFile(fileToSelect, selection) return fileToSelect.pathname
} }

View file

@ -3,6 +3,7 @@ import type { HistoryContextValue } from '../../../../../frontend/js/features/hi
import type { FileDiff } from '../../../../../frontend/js/features/history/services/types/file' import type { FileDiff } from '../../../../../frontend/js/features/history/services/types/file'
import { autoSelectFile } from '../../../../../frontend/js/features/history/utils/auto-select-file' import { autoSelectFile } from '../../../../../frontend/js/features/history/utils/auto-select-file'
import type { User } from '../../../../../frontend/js/features/history/services/types/shared' import type { User } from '../../../../../frontend/js/features/history/services/types/shared'
import { UpdateSelection } from '../../../../../frontend/js/features/history/services/types/update'
describe('autoSelectFile', function () { describe('autoSelectFile', function () {
const historyUsers: User[] = [ const historyUsers: User[] = [
@ -14,24 +15,8 @@ describe('autoSelectFile', function () {
}, },
] ]
const emptySelection: HistoryContextValue['selection'] = { describe('for comparing version with previous', function () {
docs: {}, const comparing = false
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 () { it('return the file with `edited` as the last operation', function () {
const files: FileDiff[] = [ const files: FileDiff[] = [
@ -56,14 +41,6 @@ describe('autoSelectFile', function () {
}, },
] ]
const selection: HistoryContextValue['selection'] = {
...emptySelection,
range: {
fromV: 26,
toV: 26,
},
}
const updates: HistoryContextValue['updates'] = [ const updates: HistoryContextValue['updates'] = [
{ {
fromV: 25, fromV: 25,
@ -283,15 +260,16 @@ describe('autoSelectFile', function () {
}, },
] ]
const defaultSelection = autoSelectFile( const updateSelection: UpdateSelection = {
files, update: updates[0],
selection, comparing,
viewMode, }
updates
)
expect(defaultSelection.pathname).to.equal('newfolder1/newfile10.tex') const pathname = autoSelectFile(files, updateSelection, updates)
expect(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 () { 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[] = [ const files: FileDiff[] = [
{ {
@ -312,14 +290,6 @@ describe('autoSelectFile', function () {
}, },
] ]
const selection: HistoryContextValue['selection'] = {
...emptySelection,
range: {
fromV: 4,
toV: 4,
},
}
const updates: HistoryContextValue['updates'] = [ const updates: HistoryContextValue['updates'] = [
{ {
fromV: 0, fromV: 0,
@ -360,14 +330,14 @@ describe('autoSelectFile', function () {
}, },
] ]
const defaultSelection = autoSelectFile( const updateSelection: UpdateSelection = {
files, update: updates[0],
selection, comparing,
viewMode, }
updates
)
expect(defaultSelection.pathname).to.equal('newfile1.tex') const pathname = autoSelectFile(files, updateSelection, updates)
expect(pathname).to.equal('newfile1.tex')
}) })
it('return the last non-`removed` operation with the highest `atV` value', function () { it('return the last non-`removed` operation with the highest `atV` value', function () {
@ -390,14 +360,6 @@ describe('autoSelectFile', function () {
}, },
] ]
const selection: HistoryContextValue['selection'] = {
...emptySelection,
range: {
fromV: 7,
toV: 7,
},
}
const updates: HistoryContextValue['updates'] = [ const updates: HistoryContextValue['updates'] = [
{ {
fromV: 4, fromV: 4,
@ -469,14 +431,14 @@ describe('autoSelectFile', function () {
}, },
] ]
const defaultSelection = autoSelectFile( const updateSelection: UpdateSelection = {
files, update: updates[0],
selection, comparing,
viewMode, }
updates
)
expect(defaultSelection.pathname).to.equal('main3.tex') const pathname = autoSelectFile(files, updateSelection, updates)
expect(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 () { 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 () {
@ -498,14 +460,6 @@ describe('autoSelectFile', function () {
}, },
] ]
const selection: HistoryContextValue['selection'] = {
...emptySelection,
range: {
fromV: 11,
toV: 11,
},
}
const updates: HistoryContextValue['updates'] = [ const updates: HistoryContextValue['updates'] = [
{ {
fromV: 9, fromV: 9,
@ -648,14 +602,14 @@ describe('autoSelectFile', function () {
}, },
] ]
const defaultSelection = autoSelectFile( const updateSelection: UpdateSelection = {
files, update: updates[0],
selection, comparing,
viewMode, }
updates
)
expect(defaultSelection.pathname).to.equal('main.tex') const pathname = autoSelectFile(files, updateSelection, updates)
expect(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 () { 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 () {
@ -671,14 +625,6 @@ describe('autoSelectFile', function () {
}, },
] ]
const selection: HistoryContextValue['selection'] = {
...emptySelection,
range: {
fromV: 8,
toV: 8,
},
}
const updates: HistoryContextValue['updates'] = [ const updates: HistoryContextValue['updates'] = [
{ {
fromV: 7, fromV: 7,
@ -764,14 +710,14 @@ describe('autoSelectFile', function () {
}, },
] ]
const defaultSelection = autoSelectFile( const updateSelection: UpdateSelection = {
files, update: updates[0],
selection, comparing,
viewMode, }
updates
)
expect(defaultSelection.pathname).to.equal('certainly_not_main.tex') const pathname = autoSelectFile(files, updateSelection, updates)
expect(pathname).to.equal('certainly_not_main.tex')
}) })
}) })
}) })