overleaf/services/web/frontend/js/features/history/utils/file-tree.ts
M Fahru 9a55bbf325 Port editor react file tree to history file tree (#12453)
This new history file tree is mostly copied from the editor file tree, with some of the features stripped away:

1. Remove multiple selections
2. Remove drag and drop ability
3. Remove the ability to rename files & folders
4. No more right-click hijacking (context menu)
5. No more triple dots menu on a file tree item shown
6. No file references, since history doesn't have the data to differentiate between real files and linked file
7. etc (some other small changes that are not too important to be listed)

Other notable changes:

1. Simplify the selectable provider (the only context provider being copied from react file tree)
2. Convert to typescript

GitOrigin-RevId: 1017e545b2bd99775e01307a9b7eac2daf454014
2023-04-04 08:05:30 +00:00

110 lines
3.5 KiB
TypeScript

import _ from 'lodash'
import type { Doc } from '../../../../../types/doc'
import type { FileDiff, FileRenamed } from '../services/types/file'
// `Partial` because the `reducePathsToTree` function was copied directly
// from a javascript file without proper type system and the logic is not typescript-friendly.
// TODO: refactor the function to have a proper type system
type FileTreeEntity = Partial<{
name: string
type: 'file' | 'folder'
oldPathname: string
newPathname: string
pathname: string
children: FileTreeEntity[]
operation: 'edited' | 'added' | 'renamed' | 'removed'
}>
export function reducePathsToTree(
currentFileTree: FileTreeEntity[],
fileObject: FileTreeEntity
) {
const filePathParts = fileObject?.pathname?.split('/') ?? ''
let currentFileTreeLocation = currentFileTree
for (let index = 0; index < filePathParts.length; index++) {
let fileTreeEntity: FileTreeEntity | null = {}
const pathPart = filePathParts[index]
const isFile = index === filePathParts.length - 1
if (isFile) {
fileTreeEntity = _.clone(fileObject)
fileTreeEntity.name = pathPart
fileTreeEntity.type = 'file'
currentFileTreeLocation.push(fileTreeEntity)
} else {
fileTreeEntity =
_.find(currentFileTreeLocation, entity => entity.name === pathPart) ??
null
if (fileTreeEntity == null) {
fileTreeEntity = {
name: pathPart,
type: 'folder',
children: [],
}
currentFileTreeLocation.push(fileTreeEntity)
}
currentFileTreeLocation = fileTreeEntity.children ?? []
}
}
return currentFileTree
}
export type HistoryFileTree = {
docs?: Doc[]
folders: HistoryFileTree[]
name: string
_id: string
}
export function fileTreeDiffToFileTreeData(
fileTreeDiff: FileTreeEntity[],
currentFolderName = 'rootFolder' // default value from angular version
): HistoryFileTree {
const folders: HistoryFileTree[] = []
const docs: Doc[] = []
for (const file of fileTreeDiff) {
if (file.type === 'file') {
docs.push({
_id: file.pathname as string,
name: file.name ?? '',
})
} else if (file.type === 'folder') {
if (file.children) {
const folder = fileTreeDiffToFileTreeData(file.children, file.name)
folders.push(folder)
}
}
}
return {
docs,
folders,
name: currentFolderName,
_id: currentFolderName,
}
}
// TODO: refactor the oldPathname/newPathname data
// It's an artifact from the angular version.
// Our API returns `pathname` and `newPathname` for `renamed` operation
// In the angular version, we change the key of the data:
// 1. `pathname` -> `oldPathname`
// 2. `newPathname` -> `pathname`
// 3. Delete the `newPathname` key from the object
// This is because the angular version wants to generalize the API usage
// In the operation other than the `renamed` operation, the diff API (/project/:id/diff) consumes the `pathname`
// But the `renamed` operation consumes the `newPathname` instead of the `pathname` data
//
// This behaviour can be refactored by introducing a conditional when calling the API
// i.e if `renamed` -> use `newPathname`, else -> use `pathname`
export function renamePathnameKey(file: FileRenamed): FileRenamed {
return {
oldPathname: file.pathname,
pathname: file.newPathname as string,
operation: file.operation,
}
}
export function isFileRenamed(fileDiff: FileDiff): fileDiff is FileRenamed {
return (fileDiff as FileRenamed).operation === 'renamed'
}