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

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

GitOrigin-RevId: b323be2a1104af54d3af9c9610b584fc0ab24c10
This commit is contained in:
ilkin-overleaf 2023-01-09 16:32:45 +02:00 committed by Copybot
parent bde79780a7
commit 0d0e855c96
5 changed files with 112 additions and 10 deletions

View file

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

View file

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

View file

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

View file

@ -12,8 +12,14 @@ import {
makeLongProjectList, makeLongProjectList,
} from '../fixtures/projects-data' } from '../fixtures/projects-data'
const { fullList, currentList, trashedList, leavableList, deletableList } = const {
makeLongProjectList(40) fullList,
currentList,
archivedList,
trashedList,
leavableList,
deletableList,
} = makeLongProjectList(40)
const userId = owner.id 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 () { describe('tags dropdown', function () {
beforeEach(async function () { beforeEach(async function () {
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox') allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')

View file

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