1
0
Fork 0
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 ()

* 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:
M Fahru 2023-03-28 09:12:32 -07:00 committed by Copybot
parent f2e0a41f9c
commit 568092e16b
9 changed files with 217 additions and 39 deletions
services/web
app/views/project/editor
frontend/js
types

View file

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

View file

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

View file

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

View file

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

View file

@ -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',
])
)

View file

@ -12,7 +12,8 @@ export interface FileRemoved extends FileUnchanged {
}
export interface FileRenamed extends FileUnchanged {
newPathname: string
newPathname?: string
oldPathname?: string
operation: 'renamed'
}

View 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'
}

View file

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

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