mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-08 23:31:12 +00:00
Add react file tree UI to history view (#12357)
* Add react file tree UI to history view * Use history data from API to render react file tree in history view GitOrigin-RevId: 2b1eb5422f0c91fdf5e87e21d1e5d06defd45e98
This commit is contained in:
parent
f2e0a41f9c
commit
568092e16b
9 changed files with 217 additions and 39 deletions
services/web
app/views/project/editor
frontend/js
types
|
@ -1,3 +1,12 @@
|
|||
aside.editor-sidebar.full-size#history-file-tree(
|
||||
ng-controller="ReactFileTreeController"
|
||||
ng-show="history.isReact && ui.view == 'history'"
|
||||
)
|
||||
.history-file-tree-inner.file-tree
|
||||
history-file-tree-react(
|
||||
on-select="onSelect"
|
||||
ref-providers="refProviders"
|
||||
reindex-references="reindexReferences"
|
||||
set-ref-provider-enabled="setRefProviderEnabled"
|
||||
set-started-free-trial="setStartedFreeTrial"
|
||||
)
|
||||
|
|
|
@ -1,18 +1,53 @@
|
|||
import _ from 'lodash'
|
||||
import FileTreeContext from '../../file-tree/components/file-tree-context'
|
||||
import FileTreeFolderList from '../../file-tree/components/file-tree-folder-list'
|
||||
import { useHistoryContext } from '../context/history-context'
|
||||
import {
|
||||
fileTreeDiffToFileTreeData,
|
||||
reducePathsToTree,
|
||||
} from '../utils/file-tree'
|
||||
|
||||
function HistoryFileTree() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { fileSelection, setFileSelection } = useHistoryContext()
|
||||
|
||||
return fileSelection ? (
|
||||
<ol>
|
||||
{fileSelection.files.map(file => (
|
||||
<li key={file.pathname}>{file.pathname}</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<div>No files</div>
|
||||
)
|
||||
type HistoryFileTreeProps = {
|
||||
setRefProviderEnabled: any
|
||||
setStartedFreeTrial: any
|
||||
reindexReferences: any
|
||||
onSelect: any
|
||||
refProviders: any
|
||||
}
|
||||
|
||||
export default HistoryFileTree
|
||||
export default function HistoryFileTree({
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
reindexReferences,
|
||||
onSelect,
|
||||
refProviders,
|
||||
}: HistoryFileTreeProps) {
|
||||
const { fileSelection } = useHistoryContext()
|
||||
|
||||
if (!fileSelection) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fileTree = _.reduce(fileSelection.files, reducePathsToTree, [])
|
||||
|
||||
const mappedFileTree = fileTreeDiffToFileTreeData(fileTree)
|
||||
|
||||
return (
|
||||
<FileTreeContext
|
||||
refProviders={refProviders}
|
||||
setRefProviderEnabled={setRefProviderEnabled}
|
||||
setStartedFreeTrial={setStartedFreeTrial}
|
||||
reindexReferences={reindexReferences}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<FileTreeFolderList
|
||||
folders={mappedFileTree.folders}
|
||||
docs={mappedFileTree.docs ?? []}
|
||||
files={[]}
|
||||
classes={{ root: 'file-tree-list' }}
|
||||
>
|
||||
<li className="bottom-buffer" />
|
||||
</FileTreeFolderList>
|
||||
</FileTreeContext>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,27 +1,20 @@
|
|||
import { createPortal } from 'react-dom'
|
||||
import HistoryFileTree from './history-file-tree'
|
||||
import ChangeList from './change-list/change-list'
|
||||
import Editor from './editor/editor'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useHistoryContext } from '../context/history-context'
|
||||
|
||||
const fileTreeContainer = document.getElementById('history-file-tree')
|
||||
|
||||
export default function HistoryRoot() {
|
||||
const { view } = useLayoutContext()
|
||||
const { updates } = useHistoryContext()
|
||||
|
||||
if (view !== 'history' || updates.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileTreeContainer
|
||||
? createPortal(<HistoryFileTree />, fileTreeContainer)
|
||||
: null}
|
||||
{view === 'history' && updates.length > 0 && (
|
||||
<div className="history-react">
|
||||
<Editor />
|
||||
<ChangeList />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<div className="history-react">
|
||||
<Editor />
|
||||
<ChangeList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { HistoryContextValue } from './types/history-context-value'
|
|||
import { Update, UpdateSelection } from '../services/types/update'
|
||||
import { FileSelection } from '../services/types/file'
|
||||
import { diffFiles, fetchUpdates } from '../services/api'
|
||||
import { renamePathnameKey, isFileRenamed } from '../utils/file-tree'
|
||||
|
||||
function useHistory() {
|
||||
const { view } = useLayoutContext()
|
||||
|
@ -68,16 +69,15 @@ function useHistory() {
|
|||
|
||||
diffFiles(projectId, fromV, toV).then(({ diff: files }) => {
|
||||
// TODO Infer default file sensibly
|
||||
let pathname = null
|
||||
for (const file of files) {
|
||||
if (file.pathname.endsWith('.tex')) {
|
||||
pathname = file.pathname
|
||||
break
|
||||
} else if (!pathname) {
|
||||
pathname = file.pathname
|
||||
const pathname = null
|
||||
const newFiles = files.map(file => {
|
||||
if (isFileRenamed(file) && file.newPathname) {
|
||||
return renamePathnameKey(file)
|
||||
}
|
||||
}
|
||||
setFileSelection({ files, pathname })
|
||||
|
||||
return file
|
||||
})
|
||||
setFileSelection({ files: newFiles, pathname })
|
||||
})
|
||||
}, [updateSelection, projectId])
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import HistoryFileTree from '../components/history-file-tree'
|
||||
|
||||
App.component(
|
||||
'historyFileTreeReact',
|
||||
react2angular(rootContext.use(HistoryFileTree), [
|
||||
'refProviders',
|
||||
'setRefProviderEnabled',
|
||||
'setStartedFreeTrial',
|
||||
'reindexReferences',
|
||||
'onSelect',
|
||||
])
|
||||
)
|
|
@ -12,7 +12,8 @@ export interface FileRemoved extends FileUnchanged {
|
|||
}
|
||||
|
||||
export interface FileRenamed extends FileUnchanged {
|
||||
newPathname: string
|
||||
newPathname?: string
|
||||
oldPathname?: string
|
||||
operation: 'renamed'
|
||||
}
|
||||
|
||||
|
|
114
services/web/frontend/js/features/history/utils/file-tree.ts
Normal file
114
services/web/frontend/js/features/history/utils/file-tree.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
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
|
||||
}
|
||||
|
||||
type HistoryFileTree = {
|
||||
docs?: Doc[]
|
||||
folders: HistoryFileTree[]
|
||||
name: string
|
||||
// `id` and `fileRefs` are both required from react file tree.
|
||||
// TODO: update react file tree to make the data optional so we can delete these keys
|
||||
id: ''
|
||||
fileRefs: []
|
||||
}
|
||||
|
||||
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: '',
|
||||
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: '',
|
||||
fileRefs: [],
|
||||
}
|
||||
}
|
||||
|
||||
// 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'
|
||||
}
|
|
@ -69,6 +69,7 @@ import './features/source-editor/controllers/grammarly-warning-controller'
|
|||
import './features/outline/controllers/documentation-button-controller'
|
||||
import './features/onboarding/controllers/onboarding-video-tour-modal-controller'
|
||||
import './features/history/controllers/history-controller'
|
||||
import './features/history/controllers/history-file-tree-controller'
|
||||
import { cleanupServiceWorker } from './utils/service-worker-cleanup'
|
||||
import { reportCM6Perf } from './infrastructure/cm6-performance'
|
||||
import { reportAcePerf } from './ide/editor/ace-performance'
|
||||
|
|
10
services/web/types/file-tree.ts
Normal file
10
services/web/types/file-tree.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import type { FileRef } from './file-ref'
|
||||
import type { Doc } from './doc'
|
||||
|
||||
export type FileTree = {
|
||||
_id: string
|
||||
name: string
|
||||
folders: FileTree[]
|
||||
fileRefs: FileRef[]
|
||||
docs: Doc[]
|
||||
}
|
Loading…
Add table
Reference in a new issue