mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 10:43:33 -05:00
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:
parent
bde79780a7
commit
0d0e855c96
5 changed files with 112 additions and 10 deletions
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue