Răsfoiți Sursa

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
M Fahru 3 ani în urmă
părinte
comite
568092e16b

+ 9 - 0
services/web/app/views/project/editor/file-tree-history-react.pug

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

+ 48 - 13
services/web/frontend/js/features/history/components/history-file-tree.tsx

@@ -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()
+type HistoryFileTreeProps = {
+  setRefProviderEnabled: any
+  setStartedFreeTrial: any
+  reindexReferences: any
+  onSelect: any
+  refProviders: any
+}
+
+export default function HistoryFileTree({
+  setRefProviderEnabled,
+  setStartedFreeTrial,
+  reindexReferences,
+  onSelect,
+  refProviders,
+}: HistoryFileTreeProps) {
+  const { fileSelection } = useHistoryContext()
+
+  if (!fileSelection) {
+    return null
+  }
 
-  return fileSelection ? (
-    <ol>
-      {fileSelection.files.map(file => (
-        <li key={file.pathname}>{file.pathname}</li>
-      ))}
-    </ol>
-  ) : (
-    <div>No files</div>
+  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>
   )
 }
-
-export default HistoryFileTree

+ 8 - 15
services/web/frontend/js/features/history/components/history-root.tsx

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

+ 9 - 9
services/web/frontend/js/features/history/context/history-context.tsx

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

+ 15 - 0
services/web/frontend/js/features/history/controllers/history-file-tree-controller.js

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

+ 2 - 1
services/web/frontend/js/features/history/services/types/file.ts

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

+ 114 - 0
services/web/frontend/js/features/history/utils/file-tree.ts

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

+ 1 - 0
services/web/frontend/js/ide.js

@@ -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 - 0
services/web/types/file-tree.ts

@@ -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[]
+}