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')
|
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 }))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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('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', {})
|
||||||
|
|
Loading…
Reference in a new issue