file-tree-data-context.tsx 7.7 KB

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