file-tree-data-context.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import {
  2. createContext,
  3. useCallback,
  4. useReducer,
  5. useContext,
  6. useMemo,
  7. useState,
  8. FC,
  9. useEffect,
  10. } from 'react'
  11. import useScopeValue from '../hooks/use-scope-value'
  12. import {
  13. renameInTree,
  14. deleteInTree,
  15. moveInTree,
  16. createEntityInTree,
  17. } from '../../features/file-tree/util/mutate-in-tree'
  18. import { countFiles } from '../../features/file-tree/util/count-in-tree'
  19. import useDeepCompareEffect from '../../shared/hooks/use-deep-compare-effect'
  20. import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
  21. import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
  22. import { Folder } from '../../../../types/folder'
  23. import { Project } from '../../../../types/project'
  24. import { MainDocument } from '../../../../types/project-settings'
  25. import { FindResult } from '@/features/file-tree/util/path'
  26. import {
  27. StubSnapshotUtils,
  28. useSnapshotContext,
  29. } from '@/features/ide-react/context/snapshot-context'
  30. import importOverleafModules from '../../../macros/import-overleaf-module.macro'
  31. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  32. const { buildFileTree, createFolder } =
  33. (importOverleafModules('snapshotUtils')[0]
  34. ?.import as typeof StubSnapshotUtils) || StubSnapshotUtils
  35. const FileTreeDataContext = createContext<
  36. | {
  37. // fileTreeData is the up-to-date representation of the files list, updated
  38. // by the file tree
  39. fileTreeData: Folder
  40. fileCount: { value: number; status: string; limit: number } | number
  41. fileTreeReadOnly: boolean
  42. hasFolders: boolean
  43. selectedEntities: FindResult[]
  44. setSelectedEntities: (selectedEntities: FindResult[]) => void
  45. dispatchRename: (id: string, name: string) => void
  46. dispatchMove: (id: string, target: string) => void
  47. dispatchDelete: (id: string) => void
  48. dispatchCreateFolder: (name: string, folder: any) => void
  49. dispatchCreateDoc: (name: string, doc: any) => void
  50. dispatchCreateFile: (name: string, file: any) => void
  51. docs?: MainDocument[]
  52. }
  53. | undefined
  54. >(undefined)
  55. /* eslint-disable no-unused-vars */
  56. enum ACTION_TYPES {
  57. RENAME = 'RENAME',
  58. RESET = 'RESET',
  59. DELETE = 'DELETE',
  60. MOVE = 'MOVE',
  61. CREATE = 'CREATE',
  62. }
  63. /* eslint-enable no-unused-vars */
  64. type Action =
  65. | {
  66. type: ACTION_TYPES.RESET
  67. fileTreeData?: Folder
  68. }
  69. | {
  70. type: ACTION_TYPES.RENAME
  71. id: string
  72. newName: string
  73. }
  74. | {
  75. type: ACTION_TYPES.DELETE
  76. id: string
  77. }
  78. | {
  79. type: ACTION_TYPES.MOVE
  80. entityId: string
  81. toFolderId: string
  82. }
  83. | {
  84. type: typeof ACTION_TYPES.CREATE
  85. parentFolderId: string
  86. entity: any // TODO
  87. }
  88. function fileTreeMutableReducer(
  89. { fileTreeData }: { fileTreeData: Folder },
  90. action: Action
  91. ) {
  92. switch (action.type) {
  93. case ACTION_TYPES.RESET: {
  94. const newFileTreeData = action.fileTreeData
  95. return {
  96. fileTreeData: newFileTreeData,
  97. fileCount: countFiles(newFileTreeData),
  98. }
  99. }
  100. case ACTION_TYPES.RENAME: {
  101. const newFileTreeData = renameInTree(fileTreeData, action.id, {
  102. newName: action.newName,
  103. })
  104. return {
  105. fileTreeData: newFileTreeData,
  106. fileCount: countFiles(newFileTreeData),
  107. }
  108. }
  109. case ACTION_TYPES.DELETE: {
  110. const newFileTreeData = deleteInTree(fileTreeData, action.id)
  111. return {
  112. fileTreeData: newFileTreeData,
  113. fileCount: countFiles(newFileTreeData),
  114. }
  115. }
  116. case ACTION_TYPES.MOVE: {
  117. const newFileTreeData = moveInTree(
  118. fileTreeData,
  119. action.entityId,
  120. action.toFolderId
  121. )
  122. return {
  123. fileTreeData: newFileTreeData,
  124. fileCount: countFiles(newFileTreeData),
  125. }
  126. }
  127. case ACTION_TYPES.CREATE: {
  128. const newFileTreeData = createEntityInTree(
  129. fileTreeData,
  130. action.parentFolderId,
  131. action.entity
  132. )
  133. return {
  134. fileTreeData: newFileTreeData,
  135. fileCount: countFiles(newFileTreeData),
  136. }
  137. }
  138. default: {
  139. throw new Error(
  140. `Unknown mutable file tree action type: ${(action as Action).type}`
  141. )
  142. }
  143. }
  144. }
  145. const initialState = (rootFolder?: Folder[]) => {
  146. const fileTreeData = rootFolder?.[0]
  147. return {
  148. fileTreeData,
  149. fileCount: countFiles(fileTreeData),
  150. }
  151. }
  152. export function useFileTreeData() {
  153. const context = useContext(FileTreeDataContext)
  154. if (!context) {
  155. throw new Error(
  156. 'useFileTreeData is only available inside FileTreeDataProvider'
  157. )
  158. }
  159. return context
  160. }
  161. export const FileTreeDataProvider: FC<React.PropsWithChildren> = ({
  162. children,
  163. }) => {
  164. const [project] = useScopeValue<Project>('project')
  165. const { currentDocumentId, setOpenDocName } = useEditorOpenDocContext()
  166. const { permissionsLevel } = useIdeReactContext()
  167. const { fileTreeFromHistory, snapshot, snapshotVersion } =
  168. useSnapshotContext()
  169. const fileTreeReadOnly =
  170. permissionsLevel === 'readOnly' || fileTreeFromHistory
  171. const [rootFolder, setRootFolder] = useState(project?.rootFolder)
  172. useEffect(() => {
  173. if (fileTreeFromHistory) return
  174. setRootFolder(project?.rootFolder)
  175. }, [project, fileTreeFromHistory])
  176. useEffect(() => {
  177. if (!fileTreeFromHistory) return
  178. if (!rootFolder || rootFolder?.[0]?._id) {
  179. // Init or replace mongo rootFolder with stub while we load the snapshot.
  180. // In the future, project:joined should only fire once the snapshot is ready.
  181. setRootFolder([createFolder('', '')])
  182. }
  183. }, [fileTreeFromHistory, rootFolder])
  184. useEffect(() => {
  185. if (!fileTreeFromHistory || !snapshot) return
  186. setRootFolder([buildFileTree(snapshot)])
  187. }, [fileTreeFromHistory, snapshot, snapshotVersion])
  188. const [{ fileTreeData, fileCount }, dispatch] = useReducer(
  189. fileTreeMutableReducer,
  190. rootFolder,
  191. initialState
  192. )
  193. const [selectedEntities, setSelectedEntities] = useState<FindResult[]>([])
  194. const docs = useMemo(
  195. () => (fileTreeData ? docsInFolder(fileTreeData) : undefined),
  196. [fileTreeData]
  197. )
  198. useDeepCompareEffect(() => {
  199. dispatch({
  200. type: ACTION_TYPES.RESET,
  201. fileTreeData: rootFolder?.[0],
  202. })
  203. }, [rootFolder])
  204. const dispatchCreateFolder = useCallback(
  205. (parentFolderId: string, entity: any) => {
  206. entity.type = 'folder'
  207. dispatch({
  208. type: ACTION_TYPES.CREATE,
  209. parentFolderId,
  210. entity,
  211. })
  212. },
  213. []
  214. )
  215. const dispatchCreateDoc = useCallback(
  216. (parentFolderId: string, entity: any) => {
  217. entity.type = 'doc'
  218. dispatch({
  219. type: ACTION_TYPES.CREATE,
  220. parentFolderId,
  221. entity,
  222. })
  223. },
  224. []
  225. )
  226. const dispatchCreateFile = useCallback(
  227. (parentFolderId: string, entity: any) => {
  228. entity.type = 'fileRef'
  229. dispatch({
  230. type: ACTION_TYPES.CREATE,
  231. parentFolderId,
  232. entity,
  233. })
  234. },
  235. []
  236. )
  237. const dispatchRename = useCallback(
  238. (id: string, newName: string) => {
  239. dispatch({
  240. type: ACTION_TYPES.RENAME,
  241. newName,
  242. id,
  243. })
  244. if (id === currentDocumentId) {
  245. setOpenDocName(newName)
  246. }
  247. },
  248. [currentDocumentId, setOpenDocName]
  249. )
  250. const dispatchDelete = useCallback((id: string) => {
  251. dispatch({ type: ACTION_TYPES.DELETE, id })
  252. }, [])
  253. const dispatchMove = useCallback((entityId: string, toFolderId: string) => {
  254. dispatch({ type: ACTION_TYPES.MOVE, entityId, toFolderId })
  255. }, [])
  256. const value = useMemo(() => {
  257. return {
  258. dispatchCreateDoc,
  259. dispatchCreateFile,
  260. dispatchCreateFolder,
  261. dispatchDelete,
  262. dispatchMove,
  263. dispatchRename,
  264. fileCount,
  265. fileTreeData,
  266. fileTreeReadOnly,
  267. hasFolders: fileTreeData?.folders.length > 0,
  268. selectedEntities,
  269. setSelectedEntities,
  270. docs,
  271. }
  272. }, [
  273. dispatchCreateDoc,
  274. dispatchCreateFile,
  275. dispatchCreateFolder,
  276. dispatchDelete,
  277. dispatchMove,
  278. dispatchRename,
  279. fileCount,
  280. fileTreeData,
  281. fileTreeReadOnly,
  282. selectedEntities,
  283. setSelectedEntities,
  284. docs,
  285. ])
  286. return (
  287. <FileTreeDataContext.Provider value={value}>
  288. {children}
  289. </FileTreeDataContext.Provider>
  290. )
  291. }