file-tree-data-context.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {
  2. createContext,
  3. useCallback,
  4. useReducer,
  5. useContext,
  6. useMemo,
  7. useState,
  8. FC,
  9. } from 'react'
  10. import useScopeValue from '../hooks/use-scope-value'
  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 useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only'
  21. import { Folder } from '../../../../types/folder'
  22. import { Project } from '../../../../types/project'
  23. import { MainDocument } from '../../../../types/project-settings'
  24. import { FindResult } from '@/features/file-tree/util/path'
  25. const FileTreeDataContext = createContext<
  26. | {
  27. // fileTreeData is the up-to-date representation of the files list, updated
  28. // by the file tree
  29. fileTreeData: Folder
  30. fileCount: { value: number; status: string; limit: number } | number
  31. hasFolders: boolean
  32. selectedEntities: FindResult[]
  33. setSelectedEntities: (selectedEntities: FindResult[]) => void
  34. dispatchRename: (id: string, name: string) => void
  35. dispatchMove: (id: string, target: string) => void
  36. dispatchDelete: (id: string) => void
  37. dispatchCreateFolder: (name: string, folder: any) => void
  38. dispatchCreateDoc: (name: string, doc: any) => void
  39. dispatchCreateFile: (name: string, file: any) => void
  40. docs?: MainDocument[]
  41. }
  42. | undefined
  43. >(undefined)
  44. /* eslint-disable no-unused-vars */
  45. enum ACTION_TYPES {
  46. RENAME = 'RENAME',
  47. RESET = 'RESET',
  48. DELETE = 'DELETE',
  49. MOVE = 'MOVE',
  50. CREATE = 'CREATE',
  51. }
  52. /* eslint-enable no-unused-vars */
  53. type Action =
  54. | {
  55. type: ACTION_TYPES.RESET
  56. fileTreeData?: Folder
  57. }
  58. | {
  59. type: ACTION_TYPES.RENAME
  60. id: string
  61. newName: string
  62. }
  63. | {
  64. type: ACTION_TYPES.DELETE
  65. id: string
  66. }
  67. | {
  68. type: ACTION_TYPES.MOVE
  69. entityId: string
  70. toFolderId: string
  71. }
  72. | {
  73. type: typeof ACTION_TYPES.CREATE
  74. parentFolderId: string
  75. entity: any // TODO
  76. }
  77. function fileTreeMutableReducer(
  78. { fileTreeData }: { fileTreeData: Folder },
  79. action: Action
  80. ) {
  81. switch (action.type) {
  82. case ACTION_TYPES.RESET: {
  83. const newFileTreeData = action.fileTreeData
  84. return {
  85. fileTreeData: newFileTreeData,
  86. fileCount: countFiles(newFileTreeData),
  87. }
  88. }
  89. case ACTION_TYPES.RENAME: {
  90. const newFileTreeData = renameInTree(fileTreeData, action.id, {
  91. newName: action.newName,
  92. })
  93. return {
  94. fileTreeData: newFileTreeData,
  95. fileCount: countFiles(newFileTreeData),
  96. }
  97. }
  98. case ACTION_TYPES.DELETE: {
  99. const newFileTreeData = deleteInTree(fileTreeData, action.id)
  100. return {
  101. fileTreeData: newFileTreeData,
  102. fileCount: countFiles(newFileTreeData),
  103. }
  104. }
  105. case ACTION_TYPES.MOVE: {
  106. const newFileTreeData = moveInTree(
  107. fileTreeData,
  108. action.entityId,
  109. action.toFolderId
  110. )
  111. return {
  112. fileTreeData: newFileTreeData,
  113. fileCount: countFiles(newFileTreeData),
  114. }
  115. }
  116. case ACTION_TYPES.CREATE: {
  117. const newFileTreeData = createEntityInTree(
  118. fileTreeData,
  119. action.parentFolderId,
  120. action.entity
  121. )
  122. return {
  123. fileTreeData: newFileTreeData,
  124. fileCount: countFiles(newFileTreeData),
  125. }
  126. }
  127. default: {
  128. throw new Error(
  129. `Unknown mutable file tree action type: ${(action as Action).type}`
  130. )
  131. }
  132. }
  133. }
  134. const initialState = (rootFolder?: Folder[]) => {
  135. const fileTreeData = rootFolder?.[0]
  136. return {
  137. fileTreeData,
  138. fileCount: countFiles(fileTreeData),
  139. }
  140. }
  141. export function useFileTreeData() {
  142. const context = useContext(FileTreeDataContext)
  143. if (!context) {
  144. throw new Error(
  145. 'useFileTreeData is only available inside FileTreeDataProvider'
  146. )
  147. }
  148. return context
  149. }
  150. export const FileTreeDataProvider: FC = ({ children }) => {
  151. const [project] = useScopeValue<Project>('project')
  152. const [openDocId] = useScopeValue('editor.open_doc_id')
  153. const [, setOpenDocName] = useScopeValueSetterOnly('editor.open_doc_name')
  154. const { rootFolder } = project || {}
  155. const [{ fileTreeData, fileCount }, dispatch] = useReducer(
  156. fileTreeMutableReducer,
  157. rootFolder,
  158. initialState
  159. )
  160. const [selectedEntities, setSelectedEntities] = useState<FindResult[]>([])
  161. const docs = useMemo(
  162. () => (fileTreeData ? docsInFolder(fileTreeData) : undefined),
  163. [fileTreeData]
  164. )
  165. useDeepCompareEffect(() => {
  166. dispatch({
  167. type: ACTION_TYPES.RESET,
  168. fileTreeData: rootFolder?.[0],
  169. })
  170. }, [rootFolder])
  171. const dispatchCreateFolder = useCallback((parentFolderId, entity) => {
  172. entity.type = 'folder'
  173. dispatch({
  174. type: ACTION_TYPES.CREATE,
  175. parentFolderId,
  176. entity,
  177. })
  178. }, [])
  179. const dispatchCreateDoc = useCallback(
  180. (parentFolderId: string, entity: any) => {
  181. entity.type = 'doc'
  182. dispatch({
  183. type: ACTION_TYPES.CREATE,
  184. parentFolderId,
  185. entity,
  186. })
  187. },
  188. []
  189. )
  190. const dispatchCreateFile = useCallback(
  191. (parentFolderId: string, entity: any) => {
  192. entity.type = 'fileRef'
  193. dispatch({
  194. type: ACTION_TYPES.CREATE,
  195. parentFolderId,
  196. entity,
  197. })
  198. },
  199. []
  200. )
  201. const dispatchRename = useCallback(
  202. (id: string, newName: string) => {
  203. dispatch({
  204. type: ACTION_TYPES.RENAME,
  205. newName,
  206. id,
  207. })
  208. if (id === openDocId) {
  209. setOpenDocName(newName)
  210. }
  211. },
  212. [openDocId, setOpenDocName]
  213. )
  214. const dispatchDelete = useCallback((id: string) => {
  215. dispatch({ type: ACTION_TYPES.DELETE, id })
  216. }, [])
  217. const dispatchMove = useCallback((entityId: string, toFolderId: string) => {
  218. dispatch({ type: ACTION_TYPES.MOVE, entityId, toFolderId })
  219. }, [])
  220. const value = useMemo(() => {
  221. return {
  222. dispatchCreateDoc,
  223. dispatchCreateFile,
  224. dispatchCreateFolder,
  225. dispatchDelete,
  226. dispatchMove,
  227. dispatchRename,
  228. fileCount,
  229. fileTreeData,
  230. hasFolders: fileTreeData?.folders.length > 0,
  231. selectedEntities,
  232. setSelectedEntities,
  233. docs,
  234. }
  235. }, [
  236. dispatchCreateDoc,
  237. dispatchCreateFile,
  238. dispatchCreateFolder,
  239. dispatchDelete,
  240. dispatchMove,
  241. dispatchRename,
  242. fileCount,
  243. fileTreeData,
  244. selectedEntities,
  245. setSelectedEntities,
  246. docs,
  247. ])
  248. return (
  249. <FileTreeDataContext.Provider value={value}>
  250. {children}
  251. </FileTreeDataContext.Provider>
  252. )
  253. }