file-tree-data-context.js 4.9 KB

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