overleaf/services/web/frontend/js/features/history/utils/file-tree.ts
M Fahru 0648b8aa6c Implement deleted file restore for history react (#12753)
* Add strikethrough to deleted file tree item in history file tree

* Add "restore file" button on toolbar if the selected file have a `removed` operation.

* Implement "restore file" functionality on removed file:

- Refactor the `Selection` object in history context value since we need the `deletedAtV` data which currently is not passed through the state.
- Refactor and clean up file tree type system to support passing through the whole `FileDiff` object for getting the `deletedAtV` data which only appear on `removed` operation
- Implement `postJSON` with file restoration API and pass it on restore file onClick handler at toolbar

* Implement loading behaviour while restoring file is inflight:

- Add `loadingRestoreFile` to `LoadingState`
- Change restore file button to `Restoring...` when in loading state.

* Refactor:

- Rename `DiffOperation` to `FileOperation`
- Extract `isFileRemoved` and `isFileRenamed` to its own file
- Extract `Toolbar` components into small files

GitOrigin-RevId: 2e32ebd2165f73fc6533ff282a9c084162efd682
2023-04-28 08:04:59 +00:00

119 lines
3.3 KiB
TypeScript

import _ from 'lodash'
import type { FileDiff, FileRenamed } from '../services/types/file'
import { isFileRemoved } from './file-diff'
export type FileTreeEntity = {
name?: string
type?: 'file' | 'folder'
children?: FileTreeEntity[]
} & FileDiff
export function reducePathsToTree(
currentFileTree: FileTreeEntity[],
fileObject: FileTreeEntity
) {
const filePathParts = fileObject?.pathname?.split('/') ?? ''
let currentFileTreeLocation = currentFileTree
for (let index = 0; index < filePathParts.length; index++) {
const pathPart = filePathParts[index]
const isFile = index === filePathParts.length - 1
if (isFile) {
const fileTreeEntity: FileTreeEntity = _.clone(fileObject)
fileTreeEntity.name = pathPart
fileTreeEntity.type = 'file'
currentFileTreeLocation.push(fileTreeEntity)
} else {
let fileTreeEntity: FileTreeEntity | undefined = _.find(
currentFileTreeLocation,
entity => entity.name === pathPart
)
if (fileTreeEntity === undefined) {
fileTreeEntity = {
name: pathPart,
type: 'folder',
children: <FileTreeEntity[]>[],
pathname: pathPart,
}
currentFileTreeLocation.push(fileTreeEntity)
}
currentFileTreeLocation = fileTreeEntity.children ?? []
}
}
return currentFileTree
}
export type HistoryDoc = {
name: string
} & FileDiff
export type HistoryFileTree = {
docs?: HistoryDoc[]
folders: HistoryFileTree[]
name: string
}
export function fileTreeDiffToFileTreeData(
fileTreeDiff: FileTreeEntity[],
currentFolderName = 'rootFolder' // default value from angular version
): HistoryFileTree {
const folders: HistoryFileTree[] = []
const docs: HistoryDoc[] = []
for (const file of fileTreeDiff) {
if (file.type === 'file') {
const deletedAtV = isFileRemoved(file) ? file.deletedAtV : undefined
let newDoc: HistoryDoc = {
pathname: file.pathname ?? '',
name: file.name ?? '',
deletedAtV,
}
if ('operation' in file) {
newDoc = {
...newDoc,
operation: file.operation,
}
}
docs.push(newDoc)
} else if (file.type === 'folder') {
if (file.children) {
const folder = fileTreeDiffToFileTreeData(file.children, file.name)
folders.push(folder)
}
}
}
return {
docs,
folders,
name: 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,
}
}