file-tree-data-context.jsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import {
  2. createContext,
  3. useCallback,
  4. useReducer,
  5. useContext,
  6. useMemo,
  7. useState,
  8. } from 'react'
  9. import PropTypes from 'prop-types'
  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. const FileTreeDataContext = createContext()
  22. const fileTreeDataPropType = PropTypes.shape({
  23. _id: PropTypes.string.isRequired,
  24. name: PropTypes.string.isRequired,
  25. docs: PropTypes.array.isRequired,
  26. fileRefs: PropTypes.array.isRequired,
  27. folders: PropTypes.array.isRequired,
  28. })
  29. FileTreeDataContext.Provider.propTypes = {
  30. value: PropTypes.shape({
  31. // fileTreeData is the up-to-date representation of the files list, updated
  32. // by the file tree
  33. fileTreeData: fileTreeDataPropType,
  34. hasFolders: PropTypes.bool,
  35. }),
  36. }
  37. const ACTION_TYPES = {
  38. RENAME: 'RENAME',
  39. RESET: 'RESET',
  40. DELETE: 'DELETE',
  41. MOVE: 'MOVE',
  42. CREATE: 'CREATE',
  43. }
  44. function fileTreeMutableReducer({ fileTreeData }, action) {
  45. switch (action.type) {
  46. case ACTION_TYPES.RESET: {
  47. const newFileTreeData = action.fileTreeData
  48. return {
  49. fileTreeData: newFileTreeData,
  50. fileCount: countFiles(newFileTreeData),
  51. }
  52. }
  53. case ACTION_TYPES.RENAME: {
  54. const newFileTreeData = renameInTree(fileTreeData, action.id, {
  55. newName: action.newName,
  56. })
  57. return {
  58. fileTreeData: newFileTreeData,
  59. fileCount: countFiles(newFileTreeData),
  60. }
  61. }
  62. case ACTION_TYPES.DELETE: {
  63. const newFileTreeData = deleteInTree(fileTreeData, action.id)
  64. return {
  65. fileTreeData: newFileTreeData,
  66. fileCount: countFiles(newFileTreeData),
  67. }
  68. }
  69. case ACTION_TYPES.MOVE: {
  70. const newFileTreeData = moveInTree(
  71. fileTreeData,
  72. action.entityId,
  73. action.toFolderId
  74. )
  75. return {
  76. fileTreeData: newFileTreeData,
  77. fileCount: countFiles(newFileTreeData),
  78. }
  79. }
  80. case ACTION_TYPES.CREATE: {
  81. const newFileTreeData = createEntityInTree(
  82. fileTreeData,
  83. action.parentFolderId,
  84. action.entity
  85. )
  86. return {
  87. fileTreeData: newFileTreeData,
  88. fileCount: countFiles(newFileTreeData),
  89. }
  90. }
  91. default: {
  92. throw new Error(`Unknown mutable file tree action type: ${action.type}`)
  93. }
  94. }
  95. }
  96. const initialState = rootFolder => {
  97. const fileTreeData = rootFolder?.[0]
  98. return {
  99. fileTreeData,
  100. fileCount: countFiles(fileTreeData),
  101. }
  102. }
  103. export function useFileTreeData(propTypes) {
  104. const context = useContext(FileTreeDataContext)
  105. if (!context) {
  106. throw new Error(
  107. 'useFileTreeData is only available inside FileTreeDataProvider'
  108. )
  109. }
  110. PropTypes.checkPropTypes(
  111. propTypes,
  112. context,
  113. 'data',
  114. 'FileTreeDataContext.Provider'
  115. )
  116. return context
  117. }
  118. export function FileTreeDataProvider({ children }) {
  119. const [project] = useScopeValue('project')
  120. const [openDocId] = useScopeValue('editor.open_doc_id')
  121. const [, setOpenDocName] = useScopeValueSetterOnly('editor.open_doc_name')
  122. const { rootFolder } = project || {}
  123. const [{ fileTreeData, fileCount }, dispatch] = useReducer(
  124. fileTreeMutableReducer,
  125. rootFolder,
  126. initialState
  127. )
  128. const [selectedEntities, setSelectedEntities] = useState([])
  129. const docs = useMemo(
  130. () => (fileTreeData ? docsInFolder(fileTreeData) : undefined),
  131. [fileTreeData]
  132. )
  133. useDeepCompareEffect(() => {
  134. dispatch({
  135. type: ACTION_TYPES.RESET,
  136. fileTreeData: rootFolder?.[0],
  137. })
  138. }, [rootFolder])
  139. const dispatchCreateFolder = useCallback((parentFolderId, entity) => {
  140. entity.type = 'folder'
  141. dispatch({
  142. type: ACTION_TYPES.CREATE,
  143. parentFolderId,
  144. entity,
  145. })
  146. }, [])
  147. const dispatchCreateDoc = useCallback((parentFolderId, entity) => {
  148. entity.type = 'doc'
  149. dispatch({
  150. type: ACTION_TYPES.CREATE,
  151. parentFolderId,
  152. entity,
  153. })
  154. }, [])
  155. const dispatchCreateFile = useCallback((parentFolderId, entity) => {
  156. entity.type = 'fileRef'
  157. dispatch({
  158. type: ACTION_TYPES.CREATE,
  159. parentFolderId,
  160. entity,
  161. })
  162. }, [])
  163. const dispatchRename = useCallback(
  164. (id, newName) => {
  165. dispatch({
  166. type: ACTION_TYPES.RENAME,
  167. newName,
  168. id,
  169. })
  170. if (id === openDocId) {
  171. setOpenDocName(newName)
  172. }
  173. },
  174. [openDocId, setOpenDocName]
  175. )
  176. const dispatchDelete = useCallback(id => {
  177. dispatch({ type: ACTION_TYPES.DELETE, id })
  178. }, [])
  179. const dispatchMove = useCallback((entityId, toFolderId) => {
  180. dispatch({ type: ACTION_TYPES.MOVE, entityId, toFolderId })
  181. }, [])
  182. const value = useMemo(() => {
  183. return {
  184. dispatchCreateDoc,
  185. dispatchCreateFile,
  186. dispatchCreateFolder,
  187. dispatchDelete,
  188. dispatchMove,
  189. dispatchRename,
  190. fileCount,
  191. fileTreeData,
  192. hasFolders: fileTreeData?.folders.length > 0,
  193. selectedEntities,
  194. setSelectedEntities,
  195. docs,
  196. }
  197. }, [
  198. dispatchCreateDoc,
  199. dispatchCreateFile,
  200. dispatchCreateFolder,
  201. dispatchDelete,
  202. dispatchMove,
  203. dispatchRename,
  204. fileCount,
  205. fileTreeData,
  206. selectedEntities,
  207. setSelectedEntities,
  208. docs,
  209. ])
  210. return (
  211. <FileTreeDataContext.Provider value={value}>
  212. {children}
  213. </FileTreeDataContext.Provider>
  214. )
  215. }
  216. FileTreeDataProvider.propTypes = {
  217. children: PropTypes.any,
  218. }