editor-and-sidebar.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import React, { useCallback, useEffect, useRef, useState } from 'react'
  2. import TwoColumnMainContent from '@/features/ide-react/components/layout/two-column-main-content'
  3. import EditorSidebar from '@/features/ide-react/components/editor-sidebar'
  4. import History from '@/features/ide-react/components/history'
  5. import { HistoryProvider } from '@/features/history/context/history-context'
  6. import { useProjectContext } from '@/shared/context/project-context'
  7. import { useLayoutContext } from '@/shared/context/layout-context'
  8. import {
  9. FileTreeDeleteHandler,
  10. FileTreeDocumentFindResult,
  11. FileTreeFileRefFindResult,
  12. FileTreeFindResult,
  13. } from '@/features/ide-react/types/file-tree'
  14. import usePersistedState from '@/shared/hooks/use-persisted-state'
  15. import FileView from '@/features/file-view/components/file-view'
  16. import { FileRef } from '../../../../../types/file-ref'
  17. import { EditorPane } from '@/features/ide-react/components/editor/editor-pane'
  18. import EditorAndPdf from '@/features/ide-react/components/editor-and-pdf'
  19. import MultipleSelectionPane from '@/features/ide-react/components/editor/multiple-selection-pane'
  20. import NoSelectionPane from '@/features/ide-react/components/editor/no-selection-pane'
  21. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  22. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  23. import NoOpenDocPane from '@/features/ide-react/components/editor/no-open-doc-pane'
  24. import { debugConsole } from '@/utils/debugging'
  25. import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
  26. type EditorAndSidebarProps = {
  27. shouldPersistLayout: boolean
  28. leftColumnDefaultSize: number
  29. setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
  30. }
  31. // `FileViewHeader`, which is TypeScript, expects a BinaryFile, which has a
  32. // `created` property of type `Date`, while `TPRFileViewInfo`, written in JS,
  33. // into which `FileViewHeader` passes its BinaryFile, expects a file object with
  34. // `created` property of type `string`, which is a mismatch. `TPRFileViewInfo`
  35. // is the only one making runtime complaints and it seems that other uses of
  36. // `FileViewHeader` pass in a string for `created`, so that's what this function
  37. // does too.
  38. function fileViewFile(fileRef: FileRef) {
  39. return {
  40. _id: fileRef._id,
  41. name: fileRef.name,
  42. id: fileRef._id,
  43. type: 'file',
  44. selected: true,
  45. linkedFileData: fileRef.linkedFileData,
  46. created: fileRef.created,
  47. }
  48. }
  49. export function EditorAndSidebar({
  50. shouldPersistLayout,
  51. leftColumnDefaultSize,
  52. setLeftColumnDefaultSize,
  53. }: EditorAndSidebarProps) {
  54. const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
  55. const { rootDocId, _id: projectId } = useProjectContext()
  56. const { eventEmitter } = useIdeReactContext()
  57. const { openDocId: openDocWithId, currentDocumentId } =
  58. useEditorManagerContext()
  59. const { view } = useLayoutContext()
  60. const historyIsOpen = view === 'history'
  61. // Persist the open document ID in local storage
  62. const [openDocId, setOpenDocId] = usePersistedState(
  63. `doc.open_id.${projectId}`,
  64. rootDocId
  65. )
  66. const [openEntity, setOpenEntity] = useState<
  67. FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
  68. >(null)
  69. const [selectedEntityCount, setSelectedEntityCount] = useState(0)
  70. const [fileTreeReady, setFileTreeReady] = useState(false)
  71. const handleFileTreeInit = useCallback(() => {
  72. setFileTreeReady(true)
  73. }, [])
  74. const handleFileTreeSelect = useCallback(
  75. (selectedEntities: FileTreeFindResult[]) => {
  76. debugConsole.log('File tree selection changed', selectedEntities)
  77. setSelectedEntityCount(selectedEntities.length)
  78. if (selectedEntities.length !== 1) {
  79. setOpenEntity(null)
  80. return
  81. }
  82. const [selected] = selectedEntities
  83. if (selected.type === 'folder') {
  84. return
  85. }
  86. setOpenEntity(selected)
  87. if (selected.type === 'doc') {
  88. setOpenDocId(selected.entity._id)
  89. }
  90. },
  91. [setOpenDocId]
  92. )
  93. const handleFileTreeDelete: FileTreeDeleteHandler = useCallback(
  94. entity => {
  95. eventEmitter.emit('entity:deleted', entity)
  96. // Select the root document if the current document was deleted
  97. if (entity.entity._id === openDocId) {
  98. openDocWithId(rootDocId)
  99. }
  100. },
  101. [eventEmitter, openDocId, openDocWithId, rootDocId]
  102. )
  103. // Synchronize the file tree when openDoc or openDocId is called on the editor
  104. // manager context from elsewhere. If the file tree does change, it will
  105. // trigger the onSelect handler in this component, which will update the local
  106. // state.
  107. useEffect(() => {
  108. debugConsole.log(
  109. `currentDocumentId changed to ${currentDocumentId}. Updating file tree`
  110. )
  111. if (currentDocumentId === null) {
  112. return
  113. }
  114. window.dispatchEvent(
  115. new CustomEvent('editor.openDoc', { detail: currentDocumentId })
  116. )
  117. }, [currentDocumentId])
  118. // Store openDocWithId, which is unstable, in a ref and keep the ref
  119. // synchronized with the source
  120. const openDocWithIdRef = useRef(openDocWithId)
  121. useEffect(() => {
  122. openDocWithIdRef.current = openDocWithId
  123. }, [openDocWithId])
  124. // Open a document in the editor when the local document ID changes
  125. useEffect(() => {
  126. if (!fileTreeReady || !openDocId) {
  127. return
  128. }
  129. debugConsole.log(
  130. `Observed change in local state. Opening document with ID ${openDocId}`
  131. )
  132. openDocWithIdRef.current(openDocId)
  133. }, [fileTreeReady, openDocId])
  134. const leftColumnContent = historyIsOpen ? (
  135. <HistorySidebar />
  136. ) : (
  137. <EditorSidebar
  138. shouldPersistLayout={shouldPersistLayout}
  139. onFileTreeInit={handleFileTreeInit}
  140. onFileTreeSelect={handleFileTreeSelect}
  141. onFileTreeDelete={handleFileTreeDelete}
  142. />
  143. )
  144. let rightColumnContent
  145. if (historyIsOpen) {
  146. rightColumnContent = (
  147. <HistoryProvider>
  148. <History />
  149. </HistoryProvider>
  150. )
  151. } else {
  152. let editorContent = null
  153. // Always have the editor mounted when not in history view, and hide and
  154. // show it as necessary
  155. const editorPane = (
  156. <EditorPane
  157. shouldPersistLayout={shouldPersistLayout}
  158. show={openEntity?.type === 'doc' && selectedEntityCount === 1}
  159. />
  160. )
  161. if (openDocId === undefined) {
  162. rightColumnContent = <NoOpenDocPane />
  163. } else if (selectedEntityCount === 0) {
  164. rightColumnContent = (
  165. <>
  166. {editorPane}
  167. <NoSelectionPane />
  168. </>
  169. )
  170. } else if (selectedEntityCount > 1) {
  171. editorContent = (
  172. <>
  173. {editorPane}
  174. <MultipleSelectionPane selectedEntityCount={selectedEntityCount} />
  175. </>
  176. )
  177. } else if (openEntity) {
  178. editorContent =
  179. openEntity.type === 'doc' ? (
  180. editorPane
  181. ) : (
  182. <>
  183. {editorPane}
  184. <FileView file={fileViewFile(openEntity.entity)} />
  185. </>
  186. )
  187. }
  188. if (editorContent) {
  189. rightColumnContent = (
  190. <EditorAndPdf
  191. editorContent={editorContent}
  192. shouldPersistLayout={shouldPersistLayout}
  193. />
  194. )
  195. }
  196. }
  197. return (
  198. <TwoColumnMainContent
  199. leftColumnId="editor-left-column"
  200. leftColumnContent={leftColumnContent}
  201. leftColumnDefaultSize={leftColumnDefaultSize}
  202. setLeftColumnDefaultSize={setLeftColumnDefaultSize}
  203. rightColumnContent={rightColumnContent}
  204. leftColumnIsOpen={leftColumnIsOpen}
  205. setLeftColumnIsOpen={setLeftColumnIsOpen}
  206. shouldPersistLayout={shouldPersistLayout}
  207. />
  208. )
  209. }