Преглед изворни кода

Merge pull request #11074 from overleaf/ii-tagged-trashed-projects

[web] Project dashboard archived and trashed projects appearing not hiding

GitOrigin-RevId: b323be2a1104af54d3af9c9610b584fc0ab24c10
ilkin-overleaf пре 3 година
родитељ
комит
0d0e855c96

+ 12 - 5
services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx

@@ -11,8 +11,13 @@ import useTag from '../../hooks/use-tag'
 
 export default function TagsList() {
   const { t } = useTranslation()
-  const { tags, untaggedProjectsCount, selectedTagId, selectTag } =
-    useProjectListContext()
+  const {
+    tags,
+    projectsPerTag,
+    untaggedProjectsCount,
+    selectedTagId,
+    selectTag,
+  } = useProjectListContext()
   const {
     handleSelectTag,
     openCreateTagModal,
@@ -34,11 +39,11 @@ export default function TagsList() {
           <span className="name">{t('new_folder')}</span>
         </Button>
       </li>
-      {sortBy(tags, tag => tag.name.toLowerCase()).map((tag, index) => {
+      {sortBy(tags, tag => tag.name.toLowerCase()).map(tag => {
         return (
           <li
             className={`tag ${selectedTagId === tag._id ? 'active' : ''}`}
-            key={index}
+            key={tag._id}
           >
             <Button
               className="tag-name"
@@ -59,7 +64,9 @@ export default function TagsList() {
               </span>
               <span className="name">
                 {tag.name}{' '}
-                <span className="subdued"> ({tag.project_ids?.length})</span>
+                <span className="subdued">
+                  ({projectsPerTag[tag._id].length})
+                </span>
               </span>
             </Button>
             <span className="dropdown tag-menu">

+ 19 - 3
services/web/frontend/js/features/project-list/context/project-list-context.tsx

@@ -29,7 +29,11 @@ import getMeta from '../../../utils/meta'
 import useAsync from '../../../shared/hooks/use-async'
 import { getProjects } from '../util/api'
 import sortProjects from '../util/sort-projects'
-import { isDeletableProject, isLeavableProject } from '../util/project'
+import {
+  isArchivedOrTrashed,
+  isDeletableProject,
+  isLeavableProject,
+} from '../util/project'
 
 const MAX_PROJECT_PER_PAGE = 20
 
@@ -75,6 +79,7 @@ export type ProjectListContextValue = {
   setSort: React.Dispatch<React.SetStateAction<Sort>>
   tags: Tag[]
   untaggedProjectsCount: number
+  projectsPerTag: Record<Tag['_id'], Project[]>
   filter: Filter
   selectFilter: (filter: Filter) => void
   selectedTagId?: string | undefined
@@ -182,8 +187,8 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
       } else {
         const tag = tags.find(tag => tag._id === selectedTagId)
         if (tag) {
-          filteredProjects = filteredProjects.filter(project =>
-            tag?.project_ids?.includes(project.id)
+          filteredProjects = filteredProjects.filter(
+            p => !isArchivedOrTrashed(p) && tag?.project_ids?.includes(p.id)
           )
         } else {
           setSelectedTagId(undefined)
@@ -277,6 +282,15 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
     ).length
   }, [tags, loadedProjects])
 
+  const projectsPerTag = useMemo(() => {
+    return tags.reduce<Record<Tag['_id'], Project[]>>((prev, curTag) => {
+      const tagProjects = loadedProjects.filter(p => {
+        return !isArchivedOrTrashed(p) && curTag.project_ids?.includes(p.id)
+      })
+      return { ...prev, [curTag._id]: tagProjects }
+    }, {})
+  }, [tags, loadedProjects])
+
   const selectFilter = useCallback(
     (filter: Filter) => {
       setFilter(filter)
@@ -432,6 +446,7 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
       totalProjectsCount,
       untaggedProjectsCount,
       updateProjectViewData,
+      projectsPerTag,
       visibleProjects,
     }),
     [
@@ -465,6 +480,7 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
       totalProjectsCount,
       untaggedProjectsCount,
       updateProjectViewData,
+      projectsPerTag,
       visibleProjects,
     ]
   )

+ 4 - 0
services/web/frontend/js/features/project-list/util/project.ts

@@ -20,3 +20,7 @@ export function isDeletableProject(project: Project) {
 export function isLeavableProject(project: Project) {
   return project.accessLevel !== 'owner' && project.trashed
 }
+
+export function isArchivedOrTrashed(project: Project) {
+  return project.archived || project.trashed
+}

+ 76 - 2
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

@@ -12,8 +12,14 @@ import {
   makeLongProjectList,
 } from '../fixtures/projects-data'
 
-const { fullList, currentList, trashedList, leavableList, deletableList } =
-  makeLongProjectList(40)
+const {
+  fullList,
+  currentList,
+  archivedList,
+  trashedList,
+  leavableList,
+  deletableList,
+} = makeLongProjectList(40)
 
 const userId = owner.id
 
@@ -543,6 +549,74 @@ describe('<ProjectListRoot />', function () {
         })
       })
 
+      describe('tags', function () {
+        it('does not show archived or trashed project', async function () {
+          this.unmount()
+          fetchMock.restore()
+          window.metaAttributesCache.set('ol-tags', [
+            {
+              _id: this.tagId,
+              name: this.tagName,
+              project_ids: [
+                projectsData[0].id,
+                projectsData[1].id,
+                ...archivedList.map(p => p.id),
+                ...trashedList.map(p => p.id),
+              ],
+            },
+          ])
+
+          const trashProjectMock = fetchMock.post(
+            `express:/project/:projectId/trash`,
+            { status: 200 }
+          )
+
+          renderWithProjectListContext(<ProjectListRoot />, {
+            projects: fullList,
+          })
+
+          await screen.findByRole('table')
+
+          let visibleProjectsCount = 2
+          const [tagBtn] = screen.getAllByRole('button', {
+            name: `${this.tagName} (${visibleProjectsCount})`,
+          })
+          fireEvent.click(tagBtn)
+
+          const nonArchivedAndTrashedProjects = [
+            projectsData[0],
+            projectsData[1],
+          ]
+          nonArchivedAndTrashedProjects.forEach(p => {
+            screen.getByText(p.name)
+          })
+          const archivedAndTrashedProjects = [...archivedList, ...trashedList]
+          archivedAndTrashedProjects.forEach(p => {
+            expect(screen.queryByText(p.name)).to.be.null
+          })
+
+          const trashBtns = screen.getAllByRole('button', { name: 'Trash' })
+          for (const [index, trashBtn] of trashBtns.entries()) {
+            fireEvent.click(trashBtn)
+            fireEvent.click(screen.getByText<HTMLButtonElement>('Confirm'))
+            await waitFor(() => {
+              expect(
+                trashProjectMock.called(
+                  `/project/${projectsData[index].id}/trash`
+                )
+              ).to.be.true
+            })
+            expect(
+              screen.queryAllByText(projectsData[index].name)
+            ).to.have.length(0)
+
+            screen.getAllByRole('button', {
+              name: `${this.tagName} (${--visibleProjectsCount})`,
+            })
+          }
+        })
+      })
+
       describe('tags dropdown', function () {
         beforeEach(async function () {
           allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')

+ 1 - 0
services/web/test/frontend/features/project-list/fixtures/projects-data.ts

@@ -162,6 +162,7 @@ export const makeLongProjectList = (listLength: number) => {
       ({ archived, trashed }) => !archived && !trashed
     ),
     trashedList: longList.filter(({ trashed }) => trashed),
+    archivedList: longList.filter(({ archived }) => archived),
     leavableList: longList.filter(isLeavableProject),
     deletableList: longList.filter(isDeletableProject),
   }