project-list-context.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import _ from 'lodash'
  2. import {
  3. createContext,
  4. ReactNode,
  5. useCallback,
  6. useContext,
  7. useEffect,
  8. useMemo,
  9. useState,
  10. } from 'react'
  11. import { Tag } from '../../../../../app/src/Features/Tags/types'
  12. import {
  13. GetProjectsResponseBody,
  14. Project,
  15. Sort,
  16. } from '../../../../../types/project/dashboard/api'
  17. import getMeta from '../../../utils/meta'
  18. import useAsync from '../../../shared/hooks/use-async'
  19. import { getProjects } from '../util/api'
  20. export type Filter = 'all' | 'owned' | 'shared' | 'archived' | 'trashed'
  21. type FilterMap = {
  22. [key in Filter]: Partial<Project> | ((project: Project) => boolean) // eslint-disable-line no-unused-vars
  23. }
  24. const filters: FilterMap = {
  25. all: {
  26. archived: false,
  27. trashed: false,
  28. },
  29. owned: {
  30. accessLevel: 'owner',
  31. archived: false,
  32. trashed: false,
  33. },
  34. shared: project => {
  35. return (
  36. project.accessLevel !== 'owner' && !project.archived && !project.trashed
  37. )
  38. },
  39. archived: {
  40. archived: true,
  41. trashed: false,
  42. },
  43. trashed: {
  44. trashed: true,
  45. },
  46. }
  47. type ProjectListContextValue = {
  48. visibleProjects: Project[]
  49. setVisibleProjects: React.Dispatch<React.SetStateAction<Project[]>>
  50. totalProjectsCount: number
  51. error: Error | null
  52. isLoading: ReturnType<typeof useAsync>['isLoading']
  53. loadProgress: number
  54. sort: Sort
  55. setSort: React.Dispatch<React.SetStateAction<Sort>>
  56. tags: Tag[]
  57. untaggedProjectsCount: number
  58. filter: Filter
  59. selectFilter: (filter: Filter) => void
  60. selectedTag?: string | null
  61. selectTag: (tagName: string | null) => void
  62. addTag: (tag: Tag) => void
  63. renameTag: (tagId: string, newTagName: string) => void
  64. deleteTag: (tagId: string) => void
  65. updateProjectViewData: (project: Project) => void
  66. removeProjectFromView: (project: Project) => void
  67. setSearchText: React.Dispatch<React.SetStateAction<string>>
  68. }
  69. export const ProjectListContext = createContext<
  70. ProjectListContextValue | undefined
  71. >(undefined)
  72. type ProjectListProviderProps = {
  73. children: ReactNode
  74. }
  75. export function ProjectListProvider({ children }: ProjectListProviderProps) {
  76. const [loadedProjects, setLoadedProjects] = useState<Project[]>([])
  77. const [visibleProjects, setVisibleProjects] = useState<Project[]>([])
  78. const [loadProgress, setLoadProgress] = useState(20)
  79. const [totalProjectsCount, setTotalProjectsCount] = useState<number>(0)
  80. const [sort, setSort] = useState<Sort>({
  81. by: 'lastUpdated',
  82. order: 'desc',
  83. })
  84. const [filter, setFilter] = useState<Filter>('all')
  85. const [selectedTag, setSelectedTag] = useState<string | null>()
  86. const [tags, setTags] = useState<Tag[]>([])
  87. const [searchText, setSearchText] = useState('')
  88. const {
  89. isLoading: loading,
  90. isIdle,
  91. error,
  92. runAsync,
  93. } = useAsync<GetProjectsResponseBody>()
  94. const isLoading = isIdle ? true : loading
  95. useEffect(() => setTags(getMeta('ol-tags', []) as Tag[]), [])
  96. useEffect(() => {
  97. setLoadProgress(40)
  98. runAsync(getProjects({ by: 'lastUpdated', order: 'desc' }))
  99. .then(data => {
  100. setLoadedProjects(data.projects)
  101. setTotalProjectsCount(data.totalSize)
  102. })
  103. .catch(error => console.error(error))
  104. .finally(() => {
  105. setLoadProgress(100)
  106. })
  107. }, [runAsync])
  108. useEffect(() => {
  109. let filteredProjects = [...loadedProjects]
  110. if (searchText.length) {
  111. filteredProjects = filteredProjects.filter(project =>
  112. project.name.toLowerCase().includes(searchText.toLowerCase())
  113. )
  114. }
  115. if (selectedTag !== undefined) {
  116. if (selectedTag === null) {
  117. const taggedProjectIds = _.uniq(
  118. _.flatten(tags.map(tag => tag.project_ids))
  119. )
  120. filteredProjects = filteredProjects.filter(
  121. project =>
  122. !project.archived &&
  123. !project.trashed &&
  124. !taggedProjectIds.includes(project.id)
  125. )
  126. } else {
  127. const tag = _.find(tags, tag => tag._id === selectedTag)
  128. filteredProjects = filteredProjects.filter(project =>
  129. tag?.project_ids?.includes(project.id)
  130. )
  131. }
  132. } else {
  133. filteredProjects = _.filter(filteredProjects, filters[filter])
  134. }
  135. setVisibleProjects(filteredProjects)
  136. }, [loadedProjects, tags, filter, selectedTag, searchText])
  137. const untaggedProjectsCount = useMemo(() => {
  138. const taggedProjectIds = _.uniq(_.flatten(tags.map(tag => tag.project_ids)))
  139. return loadedProjects.filter(
  140. project =>
  141. !project.archived &&
  142. !project.trashed &&
  143. !taggedProjectIds.includes(project.id)
  144. ).length
  145. }, [tags, loadedProjects])
  146. const selectFilter = useCallback((filter: Filter) => {
  147. setFilter(filter)
  148. setSelectedTag(undefined)
  149. }, [])
  150. const selectTag = useCallback((tagId: string | null) => {
  151. setSelectedTag(tagId)
  152. }, [])
  153. const addTag = useCallback((tag: Tag) => {
  154. setTags(tags => _.uniqBy(_.concat(tags, [tag]), '_id'))
  155. }, [])
  156. const renameTag = useCallback((tagId: string, newTagName: string) => {
  157. setTags(tags => {
  158. const newTags = _.cloneDeep(tags)
  159. const tag = _.find(newTags, ['_id', tagId])
  160. if (tag) {
  161. tag.name = newTagName
  162. }
  163. return newTags
  164. })
  165. }, [])
  166. const deleteTag = useCallback(
  167. (tagId: string | null) => {
  168. setTags(tags => tags.filter(tag => tag._id !== tagId))
  169. },
  170. [setTags]
  171. )
  172. const updateProjectViewData = useCallback(
  173. (project: Project) => {
  174. const projects = loadedProjects.map((p: Project) => {
  175. if (p.id === project.id) {
  176. p = project
  177. }
  178. return p
  179. })
  180. setLoadedProjects(projects)
  181. },
  182. [loadedProjects]
  183. )
  184. const removeProjectFromView = useCallback(
  185. (project: Project) => {
  186. const projects = loadedProjects.filter(
  187. (p: Project) => p.id !== project.id
  188. )
  189. setLoadedProjects(projects)
  190. },
  191. [loadedProjects]
  192. )
  193. const value = useMemo<ProjectListContextValue>(
  194. () => ({
  195. addTag,
  196. deleteTag,
  197. error,
  198. filter,
  199. isLoading,
  200. loadProgress,
  201. renameTag,
  202. selectedTag,
  203. selectFilter,
  204. selectTag,
  205. setSearchText,
  206. setSort,
  207. setVisibleProjects,
  208. sort,
  209. tags,
  210. totalProjectsCount,
  211. untaggedProjectsCount,
  212. updateProjectViewData,
  213. visibleProjects,
  214. removeProjectFromView,
  215. }),
  216. [
  217. addTag,
  218. deleteTag,
  219. error,
  220. filter,
  221. isLoading,
  222. loadProgress,
  223. renameTag,
  224. selectedTag,
  225. selectFilter,
  226. selectTag,
  227. setSearchText,
  228. setSort,
  229. setVisibleProjects,
  230. sort,
  231. tags,
  232. totalProjectsCount,
  233. untaggedProjectsCount,
  234. updateProjectViewData,
  235. visibleProjects,
  236. removeProjectFromView,
  237. ]
  238. )
  239. return (
  240. <ProjectListContext.Provider value={value}>
  241. {children}
  242. </ProjectListContext.Provider>
  243. )
  244. }
  245. export function useProjectListContext() {
  246. const context = useContext(ProjectListContext)
  247. if (!context) {
  248. throw new Error(
  249. 'ProjectListContext is only available inside ProjectListProvider'
  250. )
  251. }
  252. return context
  253. }