Merge pull request #15581 from overleaf/td-ide-page-restore-file

React IDE page: implement file restore and simplify some state

GitOrigin-RevId: ff63eb4f649156b58d9f8c4573cb6bd5b516a299
This commit is contained in:
Tim Down 2023-11-03 12:15:36 +00:00 committed by Copybot
parent 101e7e0c28
commit a4b9947fe4
7 changed files with 127 additions and 55 deletions

View file

@ -38,13 +38,14 @@ export function useRestoreDeletedFile() {
setState('complete') setState('complete')
const { _id: id } = result.entity const { _id: id } = result.entity
setView('editor') setView('editor')
// Once Angular is gone, these can be replaced with calls to context
// methods
if (restoredFileMetadata.type === 'doc') { if (restoredFileMetadata.type === 'doc') {
ide.editorManager.openDocId(id) ide.editorManager.openDocId(id)
} else { } else {
ide.binaryFilesManager.openFileWithId(id) ide.binaryFilesManager.openFileWithId(id)
} }
// Get the file tree to select the entity that has just been restored
window.dispatchEvent(new CustomEvent('editor.openDoc', { detail: id }))
} }
} }
}, [ }, [

View file

@ -11,7 +11,6 @@ import {
FileTreeFileRefFindResult, FileTreeFileRefFindResult,
FileTreeFindResult, FileTreeFindResult,
} from '@/features/ide-react/types/file-tree' } from '@/features/ide-react/types/file-tree'
import usePersistedState from '@/shared/hooks/use-persisted-state'
import FileView from '@/features/file-view/components/file-view' import FileView from '@/features/file-view/components/file-view'
import { FileRef } from '../../../../../types/file-ref' import { FileRef } from '../../../../../types/file-ref'
import { EditorPane } from '@/features/ide-react/components/editor/editor-pane' import { EditorPane } from '@/features/ide-react/components/editor/editor-pane'
@ -23,6 +22,8 @@ import { useEditorManagerContext } from '@/features/ide-react/context/editor-man
import NoOpenDocPane from '@/features/ide-react/components/editor/no-open-doc-pane' import NoOpenDocPane from '@/features/ide-react/components/editor/no-open-doc-pane'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { HistorySidebar } from '@/features/ide-react/components/history-sidebar' import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
import { BinaryFile } from '@/features/file-view/types/binary-file'
import useScopeValue from '@/shared/hooks/use-scope-value'
type EditorAndSidebarProps = { type EditorAndSidebarProps = {
shouldPersistLayout: boolean shouldPersistLayout: boolean
@ -30,6 +31,18 @@ type EditorAndSidebarProps = {
setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>> setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
} }
function convertFileRefToBinaryFile(fileRef: FileRef): BinaryFile {
return {
_id: fileRef._id,
name: fileRef.name,
id: fileRef._id,
type: 'file',
selected: true,
linkedFileData: fileRef.linkedFileData,
created: fileRef.created ? new Date(fileRef.created) : new Date(),
}
}
// `FileViewHeader`, which is TypeScript, expects a BinaryFile, which has a // `FileViewHeader`, which is TypeScript, expects a BinaryFile, which has a
// `created` property of type `Date`, while `TPRFileViewInfo`, written in JS, // `created` property of type `Date`, while `TPRFileViewInfo`, written in JS,
// into which `FileViewHeader` passes its BinaryFile, expects a file object with // into which `FileViewHeader` passes its BinaryFile, expects a file object with
@ -39,12 +52,7 @@ type EditorAndSidebarProps = {
// does too. // does too.
function fileViewFile(fileRef: FileRef) { function fileViewFile(fileRef: FileRef) {
return { return {
_id: fileRef._id, ...convertFileRefToBinaryFile(fileRef),
name: fileRef.name,
id: fileRef._id,
type: 'file',
selected: true,
linkedFileData: fileRef.linkedFileData,
created: fileRef.created, created: fileRef.created,
} }
} }
@ -55,18 +63,18 @@ export function EditorAndSidebar({
setLeftColumnDefaultSize, setLeftColumnDefaultSize,
}: EditorAndSidebarProps) { }: EditorAndSidebarProps) {
const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true) const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
const { rootDocId, _id: projectId } = useProjectContext() const { rootDocId } = useProjectContext()
const { eventEmitter } = useIdeReactContext() const { eventEmitter } = useIdeReactContext()
const { openDocId: openDocWithId, currentDocumentId } = const {
useEditorManagerContext() openDocId: openDocWithId,
currentDocumentId: openDocId,
openInitialDoc,
} = useEditorManagerContext()
const { view } = useLayoutContext() const { view } = useLayoutContext()
const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
const historyIsOpen = view === 'history' const historyIsOpen = view === 'history'
// Persist the open document ID in local storage
const [openDocId, setOpenDocId] = usePersistedState(
`doc.open_id.${projectId}`,
rootDocId
)
const [openEntity, setOpenEntity] = useState< const [openEntity, setOpenEntity] = useState<
FileTreeDocumentFindResult | FileTreeFileRefFindResult | null FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
>(null) >(null)
@ -92,11 +100,21 @@ export function EditorAndSidebar({
} }
setOpenEntity(selected) setOpenEntity(selected)
if (selected.type === 'doc') { if (selected.type === 'doc' && fileTreeReady) {
setOpenDocId(selected.entity._id) openDocWithId(selected.entity._id)
}
// Keep openFile scope value in sync with the file tree
const openFile =
selected.type === 'fileRef'
? convertFileRefToBinaryFile(selected.entity)
: null
setOpenFile(openFile)
if (openFile) {
window.dispatchEvent(new CustomEvent('file-view:file-opened'))
} }
}, },
[setOpenDocId] [fileTreeReady, setOpenFile, openDocWithId]
) )
const handleFileTreeDelete: FileTreeDeleteHandler = useCallback( const handleFileTreeDelete: FileTreeDeleteHandler = useCallback(
@ -115,46 +133,38 @@ export function EditorAndSidebar({
// trigger the onSelect handler in this component, which will update the local // trigger the onSelect handler in this component, which will update the local
// state. // state.
useEffect(() => { useEffect(() => {
debugConsole.log( debugConsole.log(`openDocId changed to ${openDocId}`)
`currentDocumentId changed to ${currentDocumentId}. Updating file tree` if (openDocId === null) {
)
if (currentDocumentId === null) {
return return
} }
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('editor.openDoc', { detail: currentDocumentId }) new CustomEvent('editor.openDoc', { detail: openDocId })
) )
}, [currentDocumentId]) }, [openDocId])
// Store openDocWithId, which is unstable, in a ref and keep the ref
// synchronized with the source
const openDocWithIdRef = useRef(openDocWithId)
// Open a document once the file tree is ready
const initialOpenDoneRef = useRef(false)
useEffect(() => { useEffect(() => {
openDocWithIdRef.current = openDocWithId if (fileTreeReady && !initialOpenDoneRef.current) {
}, [openDocWithId]) initialOpenDoneRef.current = true
openInitialDoc(rootDocId)
// Open a document in the editor when the local document ID changes
useEffect(() => {
if (!fileTreeReady || !openDocId) {
return
} }
debugConsole.log( }, [fileTreeReady, openInitialDoc, rootDocId])
`Observed change in local state. Opening document with ID ${openDocId}`
)
openDocWithIdRef.current(openDocId)
}, [fileTreeReady, openDocId])
const leftColumnContent = historyIsOpen ? ( // Keep the editor file tree around so that it is available and up to date
<HistorySidebar /> // when restoring a file
) : ( const leftColumnContent = (
<EditorSidebar <>
shouldPersistLayout={shouldPersistLayout} <EditorSidebar
onFileTreeInit={handleFileTreeInit} shouldShow={!historyIsOpen}
onFileTreeSelect={handleFileTreeSelect} shouldPersistLayout={shouldPersistLayout}
onFileTreeDelete={handleFileTreeDelete} onFileTreeInit={handleFileTreeInit}
/> onFileTreeSelect={handleFileTreeSelect}
onFileTreeDelete={handleFileTreeDelete}
/>
{historyIsOpen ? <HistorySidebar /> : null}
</>
) )
let rightColumnContent let rightColumnContent

View file

@ -6,8 +6,10 @@ import {
FileTreeDeleteHandler, FileTreeDeleteHandler,
FileTreeSelectHandler, FileTreeSelectHandler,
} from '@/features/ide-react/types/file-tree' } from '@/features/ide-react/types/file-tree'
import classNames from 'classnames'
type EditorSidebarProps = { type EditorSidebarProps = {
shouldShow: boolean
shouldPersistLayout: boolean shouldPersistLayout: boolean
onFileTreeInit: () => void onFileTreeInit: () => void
onFileTreeSelect: FileTreeSelectHandler onFileTreeSelect: FileTreeSelectHandler
@ -15,6 +17,7 @@ type EditorSidebarProps = {
} }
export default function EditorSidebar({ export default function EditorSidebar({
shouldShow,
shouldPersistLayout, shouldPersistLayout,
onFileTreeInit, onFileTreeInit,
onFileTreeSelect, onFileTreeSelect,
@ -22,7 +25,11 @@ export default function EditorSidebar({
}: EditorSidebarProps) { }: EditorSidebarProps) {
return ( return (
<> <>
<aside className="ide-react-editor-sidebar"> <aside
className={classNames('ide-react-editor-sidebar', {
hidden: !shouldShow,
})}
>
<PanelGroup <PanelGroup
autoSaveId={ autoSaveId={
shouldPersistLayout ? 'ide-react-editor-sidebar-layout' : undefined shouldPersistLayout ? 'ide-react-editor-sidebar-layout' : undefined

View file

@ -10,6 +10,7 @@ import EditorNavigationToolbar from '@/features/ide-react/components/editor-navi
import ChatPane from '@/features/chat/components/chat-pane' import ChatPane from '@/features/chat/components/chat-pane'
import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking' import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners' import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners'
import { useOpenFile } from '@/features/ide-react/hooks/use-open-file'
// This is filled with placeholder content while the real content is migrated // This is filled with placeholder content while the real content is migrated
// away from Angular // away from Angular
@ -17,6 +18,12 @@ export default function IdePage() {
useLayoutEventTracking() useLayoutEventTracking()
useSocketListeners() useSocketListeners()
// This returns a function to open a binary file but for now we just use the
// fact that it also patches in ide.binaryFilesManager. Once Angular is gone,
// we can remove this hook from here and use it in the history file restore
// component instead.
useOpenFile()
const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20) const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
const { registerUserActivity } = useConnectionContext() const { registerUserActivity } = useConnectionContext()

View file

@ -27,6 +27,7 @@ import { findDocEntityById } from '@/features/ide-react/util/find-doc-entity-by-
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter' import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
import { useModalsContext } from '@/features/ide-react/context/modals-context' import { useModalsContext } from '@/features/ide-react/context/modals-context'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import customLocalStorage from '@/infrastructure/local-storage'
interface GotoOffsetOptions { interface GotoOffsetOptions {
gotoOffset: number gotoOffset: number
@ -50,6 +51,7 @@ type EditorManager = {
stopIgnoringExternalUpdates: () => void stopIgnoringExternalUpdates: () => void
openDocId: (docId: string, options?: OpenDocOptions) => void openDocId: (docId: string, options?: OpenDocOptions) => void
openDoc: (document: Doc, options?: OpenDocOptions) => void openDoc: (document: Doc, options?: OpenDocOptions) => void
openInitialDoc: (docId: string) => void
jumpToLine: (options: GotoLineOptions) => void jumpToLine: (options: GotoLineOptions) => void
} }
@ -109,6 +111,7 @@ const EditorManagerContext = createContext<EditorManager | undefined>(undefined)
export const EditorManagerProvider: FC = ({ children }) => { export const EditorManagerProvider: FC = ({ children }) => {
const { t } = useTranslation() const { t } = useTranslation()
const ide = useIdeContext() const ide = useIdeContext()
const { projectId } = useIdeReactContext()
const { reportError, eventEmitter, eventLog } = useIdeReactContext() const { reportError, eventEmitter, eventLog } = useIdeReactContext()
const { socket, disconnect } = useConnectionContext() const { socket, disconnect } = useConnectionContext()
const { view, setView } = useLayoutContext() const { view, setView } = useLayoutContext()
@ -136,7 +139,6 @@ export const EditorManagerProvider: FC = ({ children }) => {
const { fileTreeData } = useFileTreeData() const { fileTreeData } = useFileTreeData()
// eslint-disable-next-line no-unused-vars
const [ignoringExternalUpdates, setIgnoringExternalUpdates] = useState(false) const [ignoringExternalUpdates, setIgnoringExternalUpdates] = useState(false)
const [globalEditorWatchdogManager] = useState( const [globalEditorWatchdogManager] = useState(
@ -177,6 +179,15 @@ export const EditorManagerProvider: FC = ({ children }) => {
) )
) )
const openDocIdStorageKey = `doc.open_id.${projectId}`
// Persist the open document ID to local storage
useEffect(() => {
if (openDocId) {
customLocalStorage.setItem(openDocIdStorageKey, openDocId)
}
}, [openDocId, openDocIdStorageKey])
const editorOpenDocEpochRef = useRef(0) const editorOpenDocEpochRef = useRef(0)
// TODO: This looks dodgy because it wraps a state setter and is itself // TODO: This looks dodgy because it wraps a state setter and is itself
@ -485,6 +496,17 @@ export const EditorManagerProvider: FC = ({ children }) => {
[fileTreeData, openDoc] [fileTreeData, openDoc]
) )
const openInitialDoc = useCallback(
(fallbackDocId: string) => {
const docId =
customLocalStorage.getItem(openDocIdStorageKey) || fallbackDocId
if (docId) {
openDocWithId(docId)
}
},
[openDocIdStorageKey, openDocWithId]
)
useEffect(() => { useEffect(() => {
if (docError) { if (docError) {
const { doc, document, error, meta } = docError const { doc, document, error, meta } = docError
@ -574,6 +596,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
stopIgnoringExternalUpdates, stopIgnoringExternalUpdates,
openDocId: openDocWithId, openDocId: openDocWithId,
openDoc, openDoc,
openInitialDoc,
jumpToLine, jumpToLine,
}), }),
[ [
@ -587,6 +610,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
stopIgnoringExternalUpdates, stopIgnoringExternalUpdates,
openDocWithId, openDocWithId,
openDoc, openDoc,
openInitialDoc,
jumpToLine, jumpToLine,
] ]
) )

View file

@ -0,0 +1,25 @@
import { useIdeContext } from '@/shared/context/ide-context'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useCallback } from 'react'
import { findInTree } from '@/features/file-tree/util/find-in-tree'
export function useOpenFile() {
const ide = useIdeContext()
const { fileTreeData } = useFileTreeData()
const openFileWithId = useCallback(
(id: string) => {
const result = findInTree(fileTreeData, id)
if (result?.type === 'fileRef') {
window.dispatchEvent(new CustomEvent('editor.openDoc', { detail: id }))
}
},
[fileTreeData]
)
// Expose BinaryFilesManager via ide object solely for the benefit of the file
// restore feature in history. This can be removed once Angular is gone.
ide.binaryFilesManager = { openFileWithId }
return openFileWithId
}

View file

@ -17,8 +17,6 @@ export default function populateReviewPanelScope(store: ReactScopeValueStore) {
}) })
store.set('users', {}) store.set('users', {})
store.set('reviewPanel.resolvedComments', {}) store.set('reviewPanel.resolvedComments', {})
store.set('editor.wantTrackChanges', false)
store.set('editor.open_doc_id', null)
store.set('reviewPanel.fullTCStateCollapsed', true) store.set('reviewPanel.fullTCStateCollapsed', true)
store.set('reviewPanel.rendererData.lineHeight', 0) store.set('reviewPanel.rendererData.lineHeight', 0)
store.set('reviewPanel.trackChangesState', {}) store.set('reviewPanel.trackChangesState', {})