file-tree-data-context.tsx 8.0 KB

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