mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
101e7e0c28
commit
a4b9947fe4
7 changed files with 127 additions and 55 deletions
|
@ -38,13 +38,14 @@ export function useRestoreDeletedFile() {
|
|||
setState('complete')
|
||||
const { _id: id } = result.entity
|
||||
setView('editor')
|
||||
|
||||
// Once Angular is gone, these can be replaced with calls to context
|
||||
// methods
|
||||
if (restoredFileMetadata.type === 'doc') {
|
||||
ide.editorManager.openDocId(id)
|
||||
} else {
|
||||
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 }))
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
FileTreeFileRefFindResult,
|
||||
FileTreeFindResult,
|
||||
} 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 { FileRef } from '../../../../../types/file-ref'
|
||||
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 { debugConsole } from '@/utils/debugging'
|
||||
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 = {
|
||||
shouldPersistLayout: boolean
|
||||
|
@ -30,6 +31,18 @@ type EditorAndSidebarProps = {
|
|||
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
|
||||
// `created` property of type `Date`, while `TPRFileViewInfo`, written in JS,
|
||||
// into which `FileViewHeader` passes its BinaryFile, expects a file object with
|
||||
|
@ -39,12 +52,7 @@ type EditorAndSidebarProps = {
|
|||
// does too.
|
||||
function fileViewFile(fileRef: FileRef) {
|
||||
return {
|
||||
_id: fileRef._id,
|
||||
name: fileRef.name,
|
||||
id: fileRef._id,
|
||||
type: 'file',
|
||||
selected: true,
|
||||
linkedFileData: fileRef.linkedFileData,
|
||||
...convertFileRefToBinaryFile(fileRef),
|
||||
created: fileRef.created,
|
||||
}
|
||||
}
|
||||
|
@ -55,18 +63,18 @@ export function EditorAndSidebar({
|
|||
setLeftColumnDefaultSize,
|
||||
}: EditorAndSidebarProps) {
|
||||
const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
|
||||
const { rootDocId, _id: projectId } = useProjectContext()
|
||||
const { rootDocId } = useProjectContext()
|
||||
const { eventEmitter } = useIdeReactContext()
|
||||
const { openDocId: openDocWithId, currentDocumentId } =
|
||||
useEditorManagerContext()
|
||||
const {
|
||||
openDocId: openDocWithId,
|
||||
currentDocumentId: openDocId,
|
||||
openInitialDoc,
|
||||
} = useEditorManagerContext()
|
||||
const { view } = useLayoutContext()
|
||||
const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
|
||||
|
||||
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<
|
||||
FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
|
||||
>(null)
|
||||
|
@ -92,11 +100,21 @@ export function EditorAndSidebar({
|
|||
}
|
||||
|
||||
setOpenEntity(selected)
|
||||
if (selected.type === 'doc') {
|
||||
setOpenDocId(selected.entity._id)
|
||||
if (selected.type === 'doc' && fileTreeReady) {
|
||||
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(
|
||||
|
@ -115,46 +133,38 @@ export function EditorAndSidebar({
|
|||
// trigger the onSelect handler in this component, which will update the local
|
||||
// state.
|
||||
useEffect(() => {
|
||||
debugConsole.log(
|
||||
`currentDocumentId changed to ${currentDocumentId}. Updating file tree`
|
||||
)
|
||||
if (currentDocumentId === null) {
|
||||
debugConsole.log(`openDocId changed to ${openDocId}`)
|
||||
if (openDocId === null) {
|
||||
return
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('editor.openDoc', { detail: currentDocumentId })
|
||||
new CustomEvent('editor.openDoc', { detail: openDocId })
|
||||
)
|
||||
}, [currentDocumentId])
|
||||
|
||||
// Store openDocWithId, which is unstable, in a ref and keep the ref
|
||||
// synchronized with the source
|
||||
const openDocWithIdRef = useRef(openDocWithId)
|
||||
}, [openDocId])
|
||||
|
||||
// Open a document once the file tree is ready
|
||||
const initialOpenDoneRef = useRef(false)
|
||||
useEffect(() => {
|
||||
openDocWithIdRef.current = openDocWithId
|
||||
}, [openDocWithId])
|
||||
|
||||
// Open a document in the editor when the local document ID changes
|
||||
useEffect(() => {
|
||||
if (!fileTreeReady || !openDocId) {
|
||||
return
|
||||
if (fileTreeReady && !initialOpenDoneRef.current) {
|
||||
initialOpenDoneRef.current = true
|
||||
openInitialDoc(rootDocId)
|
||||
}
|
||||
debugConsole.log(
|
||||
`Observed change in local state. Opening document with ID ${openDocId}`
|
||||
)
|
||||
openDocWithIdRef.current(openDocId)
|
||||
}, [fileTreeReady, openDocId])
|
||||
}, [fileTreeReady, openInitialDoc, rootDocId])
|
||||
|
||||
const leftColumnContent = historyIsOpen ? (
|
||||
<HistorySidebar />
|
||||
) : (
|
||||
// Keep the editor file tree around so that it is available and up to date
|
||||
// when restoring a file
|
||||
const leftColumnContent = (
|
||||
<>
|
||||
<EditorSidebar
|
||||
shouldShow={!historyIsOpen}
|
||||
shouldPersistLayout={shouldPersistLayout}
|
||||
onFileTreeInit={handleFileTreeInit}
|
||||
onFileTreeSelect={handleFileTreeSelect}
|
||||
onFileTreeDelete={handleFileTreeDelete}
|
||||
/>
|
||||
{historyIsOpen ? <HistorySidebar /> : null}
|
||||
</>
|
||||
)
|
||||
|
||||
let rightColumnContent
|
||||
|
|
|
@ -6,8 +6,10 @@ import {
|
|||
FileTreeDeleteHandler,
|
||||
FileTreeSelectHandler,
|
||||
} from '@/features/ide-react/types/file-tree'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type EditorSidebarProps = {
|
||||
shouldShow: boolean
|
||||
shouldPersistLayout: boolean
|
||||
onFileTreeInit: () => void
|
||||
onFileTreeSelect: FileTreeSelectHandler
|
||||
|
@ -15,6 +17,7 @@ type EditorSidebarProps = {
|
|||
}
|
||||
|
||||
export default function EditorSidebar({
|
||||
shouldShow,
|
||||
shouldPersistLayout,
|
||||
onFileTreeInit,
|
||||
onFileTreeSelect,
|
||||
|
@ -22,7 +25,11 @@ export default function EditorSidebar({
|
|||
}: EditorSidebarProps) {
|
||||
return (
|
||||
<>
|
||||
<aside className="ide-react-editor-sidebar">
|
||||
<aside
|
||||
className={classNames('ide-react-editor-sidebar', {
|
||||
hidden: !shouldShow,
|
||||
})}
|
||||
>
|
||||
<PanelGroup
|
||||
autoSaveId={
|
||||
shouldPersistLayout ? 'ide-react-editor-sidebar-layout' : undefined
|
||||
|
|
|
@ -10,6 +10,7 @@ import EditorNavigationToolbar from '@/features/ide-react/components/editor-navi
|
|||
import ChatPane from '@/features/chat/components/chat-pane'
|
||||
import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
|
||||
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
|
||||
// away from Angular
|
||||
|
@ -17,6 +18,12 @@ export default function IdePage() {
|
|||
useLayoutEventTracking()
|
||||
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 { registerUserActivity } = useConnectionContext()
|
||||
|
||||
|
|
|
@ -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 { useModalsContext } from '@/features/ide-react/context/modals-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
|
||||
interface GotoOffsetOptions {
|
||||
gotoOffset: number
|
||||
|
@ -50,6 +51,7 @@ type EditorManager = {
|
|||
stopIgnoringExternalUpdates: () => void
|
||||
openDocId: (docId: string, options?: OpenDocOptions) => void
|
||||
openDoc: (document: Doc, options?: OpenDocOptions) => void
|
||||
openInitialDoc: (docId: string) => void
|
||||
jumpToLine: (options: GotoLineOptions) => void
|
||||
}
|
||||
|
||||
|
@ -109,6 +111,7 @@ const EditorManagerContext = createContext<EditorManager | undefined>(undefined)
|
|||
export const EditorManagerProvider: FC = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
const ide = useIdeContext()
|
||||
const { projectId } = useIdeReactContext()
|
||||
const { reportError, eventEmitter, eventLog } = useIdeReactContext()
|
||||
const { socket, disconnect } = useConnectionContext()
|
||||
const { view, setView } = useLayoutContext()
|
||||
|
@ -136,7 +139,6 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [ignoringExternalUpdates, setIgnoringExternalUpdates] = useState(false)
|
||||
|
||||
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)
|
||||
|
||||
// TODO: This looks dodgy because it wraps a state setter and is itself
|
||||
|
@ -485,6 +496,17 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
[fileTreeData, openDoc]
|
||||
)
|
||||
|
||||
const openInitialDoc = useCallback(
|
||||
(fallbackDocId: string) => {
|
||||
const docId =
|
||||
customLocalStorage.getItem(openDocIdStorageKey) || fallbackDocId
|
||||
if (docId) {
|
||||
openDocWithId(docId)
|
||||
}
|
||||
},
|
||||
[openDocIdStorageKey, openDocWithId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (docError) {
|
||||
const { doc, document, error, meta } = docError
|
||||
|
@ -574,6 +596,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
stopIgnoringExternalUpdates,
|
||||
openDocId: openDocWithId,
|
||||
openDoc,
|
||||
openInitialDoc,
|
||||
jumpToLine,
|
||||
}),
|
||||
[
|
||||
|
@ -587,6 +610,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
stopIgnoringExternalUpdates,
|
||||
openDocWithId,
|
||||
openDoc,
|
||||
openInitialDoc,
|
||||
jumpToLine,
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -17,8 +17,6 @@ export default function populateReviewPanelScope(store: ReactScopeValueStore) {
|
|||
})
|
||||
store.set('users', {})
|
||||
store.set('reviewPanel.resolvedComments', {})
|
||||
store.set('editor.wantTrackChanges', false)
|
||||
store.set('editor.open_doc_id', null)
|
||||
store.set('reviewPanel.fullTCStateCollapsed', true)
|
||||
store.set('reviewPanel.rendererData.lineHeight', 0)
|
||||
store.set('reviewPanel.trackChangesState', {})
|
||||
|
|
Loading…
Reference in a new issue