mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #10802 from overleaf/ii-dashboard-leave-button
[web] Project dashboard leave button GitOrigin-RevId: 6c472ffe9b3d07f103f32e07fec9996a6d45caef
This commit is contained in:
parent
12af54069c
commit
7650e06074
22 changed files with 762 additions and 139 deletions
|
@ -128,6 +128,8 @@
|
|||
"delete_account_confirmation_label": "",
|
||||
"delete_account_warning_message_3": "",
|
||||
"delete_acct_no_existing_pw": "",
|
||||
"delete_and_leave": "",
|
||||
"delete_and_leave_projects": "",
|
||||
"delete_folder": "",
|
||||
"delete_projects": "",
|
||||
"delete_your_account": "",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ProjectsActionModal from './projects-action-modal'
|
||||
import ProjectsList from './projects-list'
|
||||
|
||||
type ArchiveProjectModalProps = Pick<
|
||||
React.ComponentProps<typeof ProjectsActionModal>,
|
||||
|
@ -13,29 +15,42 @@ function ArchiveProjectModal({
|
|||
handleCloseModal,
|
||||
}: ArchiveProjectModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const [projectsToDisplay, setProjectsToDisplay] = useState<typeof projects>(
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
setProjectsToDisplay(displayProjects => {
|
||||
return displayProjects.length ? displayProjects : projects
|
||||
})
|
||||
} else {
|
||||
setProjectsToDisplay([])
|
||||
}
|
||||
}, [showModal, projects])
|
||||
|
||||
return (
|
||||
<ProjectsActionModal
|
||||
action="archive"
|
||||
actionHandler={actionHandler}
|
||||
title={t('archive_projects')}
|
||||
bodyTop={<p>{t('about_to_archive_projects')}</p>}
|
||||
bodyBottom={
|
||||
<p>
|
||||
{t('archiving_projects_wont_affect_collaborators')}{' '}
|
||||
<a
|
||||
href="https://www.overleaf.com/blog/new-feature-using-archive-and-trash-to-keep-your-projects-organized"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('find_out_more_nt')}
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
projects={projects}
|
||||
/>
|
||||
>
|
||||
<p>{t('about_to_archive_projects')}</p>
|
||||
<ProjectsList projects={projects} projectsToDisplay={projectsToDisplay} />
|
||||
<p>
|
||||
{t('archiving_projects_wont_affect_collaborators')}{' '}
|
||||
<a
|
||||
href="https://www.overleaf.com/blog/new-feature-using-archive-and-trash-to-keep-your-projects-organized"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('find_out_more_nt')}
|
||||
</a>
|
||||
</p>
|
||||
</ProjectsActionModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ProjectsActionModal from './projects-action-modal'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import ProjectsList from './projects-list'
|
||||
import { isLeavableProject, isDeletableProject } from '../../util/project'
|
||||
|
||||
type DeleteLeaveProjectModalProps = Pick<
|
||||
React.ComponentProps<typeof ProjectsActionModal>,
|
||||
'projects' | 'actionHandler' | 'showModal' | 'handleCloseModal'
|
||||
>
|
||||
|
||||
function DeleteLeaveProjectModal({
|
||||
projects,
|
||||
actionHandler,
|
||||
showModal,
|
||||
handleCloseModal,
|
||||
}: DeleteLeaveProjectModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const [projectsToDeleteDisplay, setProjectsToDeleteDisplay] = useState<
|
||||
typeof projects
|
||||
>([])
|
||||
const [projectsToLeaveDisplay, setProjectsToLeaveDisplay] = useState<
|
||||
typeof projects
|
||||
>([])
|
||||
|
||||
const projectsToDelete = useMemo(() => {
|
||||
return projects.filter(isDeletableProject)
|
||||
}, [projects])
|
||||
const projectsToLeave = useMemo(() => {
|
||||
return projects.filter(isLeavableProject)
|
||||
}, [projects])
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
setProjectsToDeleteDisplay(displayProjects => {
|
||||
return displayProjects.length ? displayProjects : projectsToDelete
|
||||
})
|
||||
} else {
|
||||
setProjectsToDeleteDisplay([])
|
||||
}
|
||||
}, [showModal, projectsToDelete])
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
setProjectsToLeaveDisplay(displayProjects => {
|
||||
return displayProjects.length ? displayProjects : projectsToLeave
|
||||
})
|
||||
} else {
|
||||
setProjectsToLeaveDisplay([])
|
||||
}
|
||||
}, [showModal, projectsToLeave])
|
||||
|
||||
return (
|
||||
<ProjectsActionModal
|
||||
action="leaveOrDelete"
|
||||
actionHandler={actionHandler}
|
||||
title={t('delete_and_leave_projects')}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
projects={[...projectsToDelete, ...projectsToLeave]}
|
||||
>
|
||||
<p>{t('about_to_delete_projects')}</p>
|
||||
<ProjectsList
|
||||
projects={projectsToDelete}
|
||||
projectsToDisplay={projectsToDeleteDisplay}
|
||||
/>
|
||||
<p>{t('about_to_leave_projects')}</p>
|
||||
<ProjectsList
|
||||
projects={projectsToLeave}
|
||||
projectsToDisplay={projectsToLeaveDisplay}
|
||||
/>
|
||||
<div className="project-action-alert alert alert-warning">
|
||||
<Icon type="exclamation-triangle" fw />{' '}
|
||||
{t('this_action_cannot_be_undone')}
|
||||
</div>
|
||||
</ProjectsActionModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteLeaveProjectModal
|
|
@ -1,6 +1,8 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ProjectsActionModal from './projects-action-modal'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import ProjectsList from './projects-list'
|
||||
|
||||
type DeleteProjectModalProps = Pick<
|
||||
React.ComponentProps<typeof ProjectsActionModal>,
|
||||
|
@ -14,23 +16,36 @@ function DeleteProjectModal({
|
|||
handleCloseModal,
|
||||
}: DeleteProjectModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const [projectsToDisplay, setProjectsToDisplay] = useState<typeof projects>(
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
setProjectsToDisplay(displayProjects => {
|
||||
return displayProjects.length ? displayProjects : projects
|
||||
})
|
||||
} else {
|
||||
setProjectsToDisplay([])
|
||||
}
|
||||
}, [showModal, projects])
|
||||
|
||||
return (
|
||||
<ProjectsActionModal
|
||||
action="delete"
|
||||
actionHandler={actionHandler}
|
||||
title={t('delete_projects')}
|
||||
bodyTop={<p>{t('about_to_delete_projects')}</p>}
|
||||
bodyBottom={
|
||||
<div className="project-action-alert alert alert-warning">
|
||||
<Icon type="exclamation-triangle" fw />{' '}
|
||||
{t('this_action_cannot_be_undone')}
|
||||
</div>
|
||||
}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
projects={projects}
|
||||
/>
|
||||
>
|
||||
<p>{t('about_to_delete_projects')}</p>
|
||||
<ProjectsList projects={projects} projectsToDisplay={projectsToDisplay} />
|
||||
<div className="project-action-alert alert alert-warning">
|
||||
<Icon type="exclamation-triangle" fw />{' '}
|
||||
{t('this_action_cannot_be_undone')}
|
||||
</div>
|
||||
</ProjectsActionModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ProjectsActionModal from './projects-action-modal'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import ProjectsList from './projects-list'
|
||||
|
||||
type LeaveProjectModalProps = Pick<
|
||||
React.ComponentProps<typeof ProjectsActionModal>,
|
||||
|
@ -14,23 +16,36 @@ function LeaveProjectModal({
|
|||
handleCloseModal,
|
||||
}: LeaveProjectModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const [projectsToDisplay, setProjectsToDisplay] = useState<typeof projects>(
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
setProjectsToDisplay(displayProjects => {
|
||||
return displayProjects.length ? displayProjects : projects
|
||||
})
|
||||
} else {
|
||||
setProjectsToDisplay([])
|
||||
}
|
||||
}, [showModal, projects])
|
||||
|
||||
return (
|
||||
<ProjectsActionModal
|
||||
action="leave"
|
||||
actionHandler={actionHandler}
|
||||
title={t('leave_projects')}
|
||||
bodyTop={<p>{t('about_to_leave_projects')}</p>}
|
||||
bodyBottom={
|
||||
<div className="project-action-alert alert alert-warning">
|
||||
<Icon type="exclamation-triangle" fw />{' '}
|
||||
{t('this_action_cannot_be_undone')}
|
||||
</div>
|
||||
}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
projects={projects}
|
||||
/>
|
||||
>
|
||||
<p>{t('about_to_leave_projects')}</p>
|
||||
<ProjectsList projects={projects} projectsToDisplay={projectsToDisplay} />
|
||||
<div className="project-action-alert alert alert-warning">
|
||||
<Icon type="exclamation-triangle" fw />{' '}
|
||||
{t('this_action_cannot_be_undone')}
|
||||
</div>
|
||||
</ProjectsActionModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { Alert, Modal } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Project } from '../../../../../../types/project/dashboard/api'
|
||||
|
@ -9,13 +9,12 @@ import * as eventTracking from '../../../../infrastructure/event-tracking'
|
|||
|
||||
type ProjectsActionModalProps = {
|
||||
title?: string
|
||||
action: 'archive' | 'trash' | 'delete' | 'leave'
|
||||
action: 'archive' | 'trash' | 'delete' | 'leave' | 'leaveOrDelete'
|
||||
actionHandler: (project: Project) => Promise<void>
|
||||
handleCloseModal: () => void
|
||||
bodyTop?: React.ReactNode
|
||||
bodyBottom?: React.ReactNode
|
||||
projects: Array<Project>
|
||||
showModal: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
function ProjectsActionModal({
|
||||
|
@ -23,16 +22,13 @@ function ProjectsActionModal({
|
|||
action,
|
||||
actionHandler,
|
||||
handleCloseModal,
|
||||
bodyTop,
|
||||
bodyBottom,
|
||||
showModal,
|
||||
projects,
|
||||
children,
|
||||
}: ProjectsActionModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const [errors, setErrors] = useState<Array<any>>([])
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [projectsToDisplay, setProjectsToDisplay] = useState<Project[]>([])
|
||||
const projectsRef = useRef<Project[]>([])
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
async function handleActionForProjects(projects: Array<Project>) {
|
||||
|
@ -66,21 +62,8 @@ function ProjectsActionModal({
|
|||
'project action',
|
||||
action
|
||||
)
|
||||
|
||||
if (projectsRef.current.length > 0) {
|
||||
// maintain the original list in the display of the modal,
|
||||
// even after some project actions have completed
|
||||
setProjectsToDisplay(projectsRef.current)
|
||||
} else {
|
||||
projectsRef.current = projects
|
||||
setProjectsToDisplay(projects)
|
||||
}
|
||||
} else {
|
||||
projectsRef.current = []
|
||||
}
|
||||
}, [action, projects, projectsRef, showModal])
|
||||
|
||||
const projectIdsRemainingToProcess = projects.map(p => p.id)
|
||||
}, [action, showModal])
|
||||
|
||||
return (
|
||||
<AccessibleModal
|
||||
|
@ -93,25 +76,7 @@ function ProjectsActionModal({
|
|||
<Modal.Header closeButton>
|
||||
<Modal.Title>{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{bodyTop}
|
||||
<ul>
|
||||
{projectsToDisplay.map(project => (
|
||||
<li
|
||||
key={`projects-action-list-${project.id}`}
|
||||
className={
|
||||
projectIdsRemainingToProcess.includes(project.id)
|
||||
? ''
|
||||
: 'list-style-check-green'
|
||||
}
|
||||
>
|
||||
<b>{project.name}</b>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{bodyBottom}
|
||||
</Modal.Body>
|
||||
<Modal.Body>{children}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{!isProcessing &&
|
||||
errors.length > 0 &&
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import classnames from 'classnames'
|
||||
import { Project } from '../../../../../../types/project/dashboard/api'
|
||||
|
||||
type ProjectsToDisplayProps = {
|
||||
projects: Project[]
|
||||
projectsToDisplay: Project[]
|
||||
}
|
||||
|
||||
function ProjectsList({ projects, projectsToDisplay }: ProjectsToDisplayProps) {
|
||||
return (
|
||||
<ul className="projects-action-list">
|
||||
{projectsToDisplay.map(project => (
|
||||
<li
|
||||
key={`projects-action-list-${project.id}`}
|
||||
className={classnames({
|
||||
'list-style-check-green': !projects.some(
|
||||
({ id }) => id === project.id
|
||||
),
|
||||
})}
|
||||
>
|
||||
<b>{project.name}</b>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectsList
|
|
@ -1,5 +1,7 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ProjectsActionModal from './projects-action-modal'
|
||||
import ProjectsList from './projects-list'
|
||||
|
||||
type TrashProjectPropsModalProps = Pick<
|
||||
React.ComponentProps<typeof ProjectsActionModal>,
|
||||
|
@ -13,29 +15,42 @@ function TrashProjectModal({
|
|||
handleCloseModal,
|
||||
}: TrashProjectPropsModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const [projectsToDisplay, setProjectsToDisplay] = useState<typeof projects>(
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
setProjectsToDisplay(displayProjects => {
|
||||
return displayProjects.length ? displayProjects : projects
|
||||
})
|
||||
} else {
|
||||
setProjectsToDisplay([])
|
||||
}
|
||||
}, [showModal, projects])
|
||||
|
||||
return (
|
||||
<ProjectsActionModal
|
||||
action="trash"
|
||||
actionHandler={actionHandler}
|
||||
title={t('trash_projects')}
|
||||
bodyTop={<p>{t('about_to_trash_projects')}</p>}
|
||||
bodyBottom={
|
||||
<p>
|
||||
{t('trashing_projects_wont_affect_collaborators')}{' '}
|
||||
<a
|
||||
href="https://www.overleaf.com/blog/new-feature-using-archive-and-trash-to-keep-your-projects-organized"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('find_out_more_nt')}
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
projects={projects}
|
||||
/>
|
||||
>
|
||||
<p>{t('about_to_trash_projects')}</p>
|
||||
<ProjectsList projects={projects} projectsToDisplay={projectsToDisplay} />
|
||||
<p>
|
||||
{t('trashing_projects_wont_affect_collaborators')}{' '}
|
||||
<a
|
||||
href="https://www.overleaf.com/blog/new-feature-using-archive-and-trash-to-keep-your-projects-organized"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('find_out_more_nt')}
|
||||
</a>
|
||||
</p>
|
||||
</ProjectsActionModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import ArchiveProjectModal from '../../../modals/archive-project-modal'
|
|||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||
import { archiveProject } from '../../../../util/api'
|
||||
import { Project } from '../../../../../../../../types/project/dashboard/api'
|
||||
|
||||
function ArchiveProjectsButton() {
|
||||
const { selectedProjects, updateProjectViewData } = useProjectListContext()
|
||||
|
@ -25,17 +26,16 @@ function ArchiveProjectsButton() {
|
|||
}
|
||||
}, [isMounted])
|
||||
|
||||
const handleArchiveProjects = useCallback(async () => {
|
||||
for (const project of selectedProjects) {
|
||||
await archiveProject(project.id)
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
archived: true,
|
||||
selected: false,
|
||||
trashed: false,
|
||||
})
|
||||
}
|
||||
}, [selectedProjects, updateProjectViewData])
|
||||
const handleArchiveProject = async (project: Project) => {
|
||||
await archiveProject(project.id)
|
||||
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
archived: true,
|
||||
selected: false,
|
||||
trashed: false,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -54,7 +54,7 @@ function ArchiveProjectsButton() {
|
|||
</Tooltip>
|
||||
<ArchiveProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleArchiveProjects}
|
||||
actionHandler={handleArchiveProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DeleteLeaveProjectModal from '../../../modals/delete-leave-project-modal'
|
||||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||
import { deleteProject, leaveProject } from '../../../../util/api'
|
||||
import { Project } from '../../../../../../../../types/project/dashboard/api'
|
||||
|
||||
function DeleteLeaveProjectsButton() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
selectedProjects,
|
||||
removeProjectFromView,
|
||||
hasLeavableProjectsSelected,
|
||||
hasDeletableProjectsSelected,
|
||||
} = useProjectListContext()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (isMounted.current) {
|
||||
setShowModal(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAndLeaveProject = async (project: Project) => {
|
||||
if (project.accessLevel === 'owner') {
|
||||
await deleteProject(project.id)
|
||||
} else {
|
||||
await leaveProject(project.id)
|
||||
}
|
||||
|
||||
removeProjectFromView(project)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasDeletableProjectsSelected && hasLeavableProjectsSelected && (
|
||||
<Button bsStyle={null} className="btn-danger" onClick={handleOpenModal}>
|
||||
{t('delete_and_leave')}
|
||||
</Button>
|
||||
)}
|
||||
<DeleteLeaveProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleDeleteAndLeaveProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteLeaveProjectsButton
|
|
@ -0,0 +1,54 @@
|
|||
import { useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DeleteProjectModal from '../../../modals/delete-project-modal'
|
||||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||
import { deleteProject } from '../../../../util/api'
|
||||
import { Project } from '../../../../../../../../types/project/dashboard/api'
|
||||
|
||||
function DeleteProjectsButton() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
selectedProjects,
|
||||
removeProjectFromView,
|
||||
hasLeavableProjectsSelected,
|
||||
hasDeletableProjectsSelected,
|
||||
} = useProjectListContext()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (isMounted.current) {
|
||||
setShowModal(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteProject = async (project: Project) => {
|
||||
await deleteProject(project.id)
|
||||
|
||||
removeProjectFromView(project)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasDeletableProjectsSelected && !hasLeavableProjectsSelected && (
|
||||
<Button bsStyle={null} className="btn-danger" onClick={handleOpenModal}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
)}
|
||||
<DeleteProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleDeleteProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteProjectsButton
|
|
@ -0,0 +1,54 @@
|
|||
import { useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import LeaveProjectModal from '../../../modals/leave-project-modal'
|
||||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||
import { leaveProject } from '../../../../util/api'
|
||||
import { Project } from '../../../../../../../../types/project/dashboard/api'
|
||||
|
||||
function LeaveProjectsButton() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
selectedProjects,
|
||||
removeProjectFromView,
|
||||
hasLeavableProjectsSelected,
|
||||
hasDeletableProjectsSelected,
|
||||
} = useProjectListContext()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (isMounted.current) {
|
||||
setShowModal(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLeaveProject = async (project: Project) => {
|
||||
await leaveProject(project.id)
|
||||
|
||||
removeProjectFromView(project)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hasDeletableProjectsSelected && hasLeavableProjectsSelected && (
|
||||
<Button bsStyle={null} className="btn-danger" onClick={handleOpenModal}>
|
||||
{t('leave')}
|
||||
</Button>
|
||||
)}
|
||||
<LeaveProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleLeaveProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeaveProjectsButton
|
|
@ -6,6 +6,7 @@ import TrashProjectModal from '../../../modals/trash-project-modal'
|
|||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||
import { trashProject } from '../../../../util/api'
|
||||
import { Project } from '../../../../../../../../types/project/dashboard/api'
|
||||
|
||||
function TrashProjectsButton() {
|
||||
const { selectedProjects, updateProjectViewData } = useProjectListContext()
|
||||
|
@ -25,17 +26,16 @@ function TrashProjectsButton() {
|
|||
}
|
||||
}, [isMounted])
|
||||
|
||||
const handleTrashProjects = useCallback(async () => {
|
||||
for (const project of selectedProjects) {
|
||||
await trashProject(project.id)
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
trashed: true,
|
||||
archived: false,
|
||||
selected: false,
|
||||
})
|
||||
}
|
||||
}, [selectedProjects, updateProjectViewData])
|
||||
const handleTrashProject = async (project: Project) => {
|
||||
await trashProject(project.id)
|
||||
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
trashed: true,
|
||||
archived: false,
|
||||
selected: false,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -54,7 +54,7 @@ function TrashProjectsButton() {
|
|||
</Tooltip>
|
||||
<TrashProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleTrashProjects}
|
||||
actionHandler={handleTrashProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
|
|
|
@ -8,6 +8,9 @@ import TagsDropdown from './buttons/tags-dropdown'
|
|||
import TrashProjectsButton from './buttons/trash-projects-button'
|
||||
import UnarchiveProjectsButton from './buttons/unarchive-projects-button'
|
||||
import UntrashProjectsButton from './buttons/untrash-projects-button'
|
||||
import DeleteLeaveProjectsButton from './buttons/delete-leave-projects-button'
|
||||
import LeaveProjectsButton from './buttons/leave-projects-button'
|
||||
import DeleteProjectsButton from './buttons/delete-projects-button'
|
||||
|
||||
function ProjectTools() {
|
||||
const { filter, selectedProjects } = useProjectListContext()
|
||||
|
@ -24,6 +27,16 @@ function ProjectTools() {
|
|||
{filter === 'archived' && <UnarchiveProjectsButton />}
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup>
|
||||
{filter === 'trashed' && (
|
||||
<>
|
||||
<LeaveProjectsButton />
|
||||
<DeleteProjectsButton />
|
||||
<DeleteLeaveProjectsButton />
|
||||
</>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
{!['archived', 'trashed'].includes(filter) && <TagsDropdown />}
|
||||
|
||||
{selectedProjects.length === 1 &&
|
||||
|
|
|
@ -29,6 +29,7 @@ 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'
|
||||
|
||||
const MAX_PROJECT_PER_PAGE = 20
|
||||
|
||||
|
@ -62,7 +63,7 @@ const filters: FilterMap = {
|
|||
|
||||
export const UNCATEGORIZED_KEY = 'uncategorized'
|
||||
|
||||
type ProjectListContextValue = {
|
||||
export type ProjectListContextValue = {
|
||||
addClonedProjectToViewData: (project: Project) => void
|
||||
selectOrUnselectAllProjects: React.Dispatch<React.SetStateAction<boolean>>
|
||||
visibleProjects: Project[]
|
||||
|
@ -92,6 +93,8 @@ type ProjectListContextValue = {
|
|||
loadMoreCount: number
|
||||
showAllProjects: () => void
|
||||
loadMoreProjects: () => void
|
||||
hasLeavableProjectsSelected: boolean
|
||||
hasDeletableProjectsSelected: boolean
|
||||
}
|
||||
|
||||
export const ProjectListContext = createContext<
|
||||
|
@ -375,20 +378,26 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
|
|||
|
||||
const updateProjectViewData = useCallback((newProjectData: Project) => {
|
||||
setLoadedProjects(loadedProjects => {
|
||||
return loadedProjects.map((p: Project) =>
|
||||
return loadedProjects.map(p =>
|
||||
p.id === newProjectData.id ? { ...newProjectData } : p
|
||||
)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const removeProjectFromView = useCallback(
|
||||
(project: Project) => {
|
||||
const projects = loadedProjects.filter(
|
||||
(p: Project) => p.id !== project.id
|
||||
)
|
||||
setLoadedProjects(projects)
|
||||
},
|
||||
[loadedProjects]
|
||||
const removeProjectFromView = useCallback((project: Project) => {
|
||||
setLoadedProjects(loadedProjects => {
|
||||
return loadedProjects.filter(p => p.id !== project.id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const hasLeavableProjectsSelected = useMemo(
|
||||
() => selectedProjects.some(isLeavableProject),
|
||||
[selectedProjects]
|
||||
)
|
||||
|
||||
const hasDeletableProjectsSelected = useMemo(
|
||||
() => selectedProjects.some(isDeletableProject),
|
||||
[selectedProjects]
|
||||
)
|
||||
|
||||
const value = useMemo<ProjectListContextValue>(
|
||||
|
@ -399,6 +408,8 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
|
|||
deleteTag,
|
||||
error,
|
||||
filter,
|
||||
hasLeavableProjectsSelected,
|
||||
hasDeletableProjectsSelected,
|
||||
hiddenProjectsCount,
|
||||
isLoading,
|
||||
loadMoreCount,
|
||||
|
@ -430,6 +441,8 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
|
|||
deleteTag,
|
||||
error,
|
||||
filter,
|
||||
hasLeavableProjectsSelected,
|
||||
hasDeletableProjectsSelected,
|
||||
hiddenProjectsCount,
|
||||
isLoading,
|
||||
loadMoreCount,
|
||||
|
|
|
@ -12,3 +12,11 @@ export function getOwnerName(project: Project) {
|
|||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function isDeletableProject(project: Project) {
|
||||
return project.accessLevel === 'owner' && project.trashed
|
||||
}
|
||||
|
||||
export function isLeavableProject(project: Project) {
|
||||
return project.accessLevel !== 'owner' && project.trashed
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
archivedProjects,
|
||||
makeLongProjectList,
|
||||
} from '../fixtures/projects-data'
|
||||
const { fullList, currentList, trashedList } = makeLongProjectList(40)
|
||||
|
||||
const { fullList, currentList, trashedList, leavableList, deletableList } =
|
||||
makeLongProjectList(40)
|
||||
|
||||
const userId = owner.id
|
||||
|
||||
|
@ -78,9 +80,10 @@ describe('<ProjectListRoot />', function () {
|
|||
|
||||
describe('project table', function () {
|
||||
beforeEach(async function () {
|
||||
renderWithProjectListContext(<ProjectListRoot />, {
|
||||
const { unmount } = renderWithProjectListContext(<ProjectListRoot />, {
|
||||
projects: fullList,
|
||||
})
|
||||
this.unmount = unmount
|
||||
await fetchMock.flush(true)
|
||||
await screen.findByRole('table')
|
||||
})
|
||||
|
@ -240,7 +243,7 @@ describe('<ProjectListRoot />', function () {
|
|||
})
|
||||
|
||||
const unarchiveButton =
|
||||
within(actionsToolbar).getByText<HTMLInputElement>('Restore')
|
||||
within(actionsToolbar).getByText<HTMLButtonElement>('Restore')
|
||||
fireEvent.click(unarchiveButton)
|
||||
|
||||
await fetchMock.flush(true)
|
||||
|
@ -284,7 +287,7 @@ describe('<ProjectListRoot />', function () {
|
|||
actionsToolbar = screen.getAllByRole('toolbar')[0]
|
||||
})
|
||||
|
||||
it('only shows the download, archive, and restore buttons in top toolbar', function () {
|
||||
it('shows the download, archive, and restore buttons in top toolbar', function () {
|
||||
expect(screen.queryByLabelText('Trash')).to.be.null
|
||||
within(actionsToolbar).queryByLabelText('Download')
|
||||
within(actionsToolbar).queryByLabelText('Archive')
|
||||
|
@ -307,7 +310,7 @@ describe('<ProjectListRoot />', function () {
|
|||
})
|
||||
|
||||
const untrashButton =
|
||||
within(actionsToolbar).getByText<HTMLInputElement>('Restore')
|
||||
within(actionsToolbar).getByText<HTMLButtonElement>('Restore')
|
||||
fireEvent.click(untrashButton)
|
||||
|
||||
await fetchMock.flush(true)
|
||||
|
@ -330,22 +333,213 @@ describe('<ProjectListRoot />', function () {
|
|||
})
|
||||
|
||||
it('removes project from view when archiving', async function () {
|
||||
fetchMock.post(`express:/project/:id/archive`, {
|
||||
status: 200,
|
||||
})
|
||||
fetchMock.post(
|
||||
`express:/project/:id/archive`,
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
{ repeat: trashedList.length }
|
||||
)
|
||||
|
||||
const untrashButton =
|
||||
within(actionsToolbar).getByLabelText<HTMLInputElement>('Archive')
|
||||
fireEvent.click(untrashButton)
|
||||
const archiveButton =
|
||||
within(actionsToolbar).getByLabelText<HTMLButtonElement>('Archive')
|
||||
fireEvent.click(archiveButton)
|
||||
|
||||
const confirmButton = screen.getByText<HTMLInputElement>('Confirm')
|
||||
const confirmButton = screen.getByText<HTMLButtonElement>('Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
expect(confirmButton.disabled).to.be.true
|
||||
|
||||
await fetchMock.flush(true)
|
||||
expect(fetchMock.done()).to.be.true
|
||||
|
||||
screen.getByText('No projects')
|
||||
const calls = fetchMock.calls().map(([url]) => url)
|
||||
|
||||
trashedList.forEach(project => {
|
||||
expect(calls).to.contain(`/project/${project.id}/archive`)
|
||||
})
|
||||
})
|
||||
|
||||
it('removes only selected projects from view when leaving', async function () {
|
||||
// rerender content with different projects
|
||||
this.unmount()
|
||||
fetchMock.restore()
|
||||
|
||||
renderWithProjectListContext(<ProjectListRoot />, {
|
||||
projects: leavableList,
|
||||
})
|
||||
|
||||
await fetchMock.flush(true)
|
||||
await screen.findByRole('table')
|
||||
|
||||
expect(leavableList.length).to.be.greaterThan(0)
|
||||
|
||||
fetchMock.post(
|
||||
`express:/project/:id/leave`,
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
{ repeat: leavableList.length }
|
||||
)
|
||||
|
||||
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
|
||||
// + 1 because of select all
|
||||
expect(allCheckboxes.length).to.equal(leavableList.length + 1)
|
||||
|
||||
// first one is the select all checkbox
|
||||
fireEvent.click(allCheckboxes[0])
|
||||
|
||||
actionsToolbar = screen.getAllByRole('toolbar')[0]
|
||||
|
||||
const toolbar = within(actionsToolbar)
|
||||
expect(toolbar.queryByRole('button', { name: /delete/i })).to.be.null
|
||||
expect(
|
||||
toolbar.queryByRole('button', {
|
||||
name: /delete \/ leave/i,
|
||||
})
|
||||
).to.be.null
|
||||
|
||||
const leaveButton = toolbar.getByRole('button', {
|
||||
name: /leave/i,
|
||||
})
|
||||
fireEvent.click(leaveButton)
|
||||
|
||||
const confirmButton = screen.getByText<HTMLButtonElement>('Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
expect(confirmButton.disabled).to.be.true
|
||||
|
||||
await fetchMock.flush(true)
|
||||
expect(fetchMock.done()).to.be.true
|
||||
|
||||
const calls = fetchMock.calls().map(([url]) => url)
|
||||
leavableList.forEach(project => {
|
||||
expect(calls).to.contain(`/project/${project.id}/leave`)
|
||||
})
|
||||
})
|
||||
|
||||
it('removes only selected projects from view when deleting', async function () {
|
||||
// rerender content with different projects
|
||||
this.unmount()
|
||||
fetchMock.restore()
|
||||
|
||||
renderWithProjectListContext(<ProjectListRoot />, {
|
||||
projects: deletableList,
|
||||
})
|
||||
|
||||
await fetchMock.flush(true)
|
||||
await screen.findByRole('table')
|
||||
|
||||
expect(deletableList.length).to.be.greaterThan(0)
|
||||
|
||||
fetchMock.delete(
|
||||
`express:/project/:id`,
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
{ repeat: deletableList.length }
|
||||
)
|
||||
|
||||
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
|
||||
// + 1 because of select all
|
||||
expect(allCheckboxes.length).to.equal(deletableList.length + 1)
|
||||
|
||||
// first one is the select all checkbox
|
||||
fireEvent.click(allCheckboxes[0])
|
||||
|
||||
actionsToolbar = screen.getAllByRole('toolbar')[0]
|
||||
|
||||
const toolbar = within(actionsToolbar)
|
||||
expect(toolbar.queryByRole('button', { name: /leave/i })).to.be.null
|
||||
expect(
|
||||
toolbar.queryByRole('button', {
|
||||
name: /delete \/ leave/i,
|
||||
})
|
||||
).to.be.null
|
||||
|
||||
const deleteButton = toolbar.getByRole('button', {
|
||||
name: /delete/i,
|
||||
})
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
const confirmButton = screen.getByText<HTMLButtonElement>('Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
expect(confirmButton.disabled).to.be.true
|
||||
|
||||
await fetchMock.flush(true)
|
||||
expect(fetchMock.done()).to.be.true
|
||||
|
||||
const calls = fetchMock.calls().map(([url]) => url)
|
||||
deletableList.forEach(project => {
|
||||
expect(calls).to.contain(`/project/${project.id}`)
|
||||
})
|
||||
})
|
||||
|
||||
it('removes only selected projects from view when deleting and leaving', async function () {
|
||||
// rerender content with different projects
|
||||
this.unmount()
|
||||
fetchMock.restore()
|
||||
|
||||
const deletableAndLeavableList = [...deletableList, ...leavableList]
|
||||
|
||||
renderWithProjectListContext(<ProjectListRoot />, {
|
||||
projects: deletableAndLeavableList,
|
||||
})
|
||||
|
||||
await fetchMock.flush(true)
|
||||
await screen.findByRole('table')
|
||||
|
||||
expect(deletableList.length).to.be.greaterThan(0)
|
||||
expect(leavableList.length).to.be.greaterThan(0)
|
||||
|
||||
fetchMock
|
||||
.delete(
|
||||
`express:/project/:id`,
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
{ repeat: deletableList.length }
|
||||
)
|
||||
.post(
|
||||
`express:/project/:id/leave`,
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
{ repeat: leavableList.length }
|
||||
)
|
||||
|
||||
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
|
||||
// + 1 because of select all
|
||||
expect(allCheckboxes.length).to.equal(
|
||||
deletableAndLeavableList.length + 1
|
||||
)
|
||||
|
||||
// first one is the select all checkbox
|
||||
fireEvent.click(allCheckboxes[0])
|
||||
|
||||
actionsToolbar = screen.getAllByRole('toolbar')[0]
|
||||
|
||||
const toolbar = within(actionsToolbar)
|
||||
expect(toolbar.queryByRole('button', { name: 'Leave' })).to.be.null
|
||||
expect(toolbar.queryByRole('button', { name: 'Delete' })).to.be.null
|
||||
|
||||
const deleteLeaveButton = toolbar.getByRole('button', {
|
||||
name: /delete \/ leave/i,
|
||||
})
|
||||
fireEvent.click(deleteLeaveButton)
|
||||
|
||||
const confirmButton = screen.getByText<HTMLButtonElement>('Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
expect(confirmButton.disabled).to.be.true
|
||||
|
||||
await fetchMock.flush(true)
|
||||
expect(fetchMock.done()).to.be.true
|
||||
|
||||
const calls = fetchMock.calls().map(([url]) => url)
|
||||
deletableAndLeavableList.forEach(project => {
|
||||
expect(calls).to.contain.oneOf([
|
||||
`/project/${project.id}`,
|
||||
`/project/${project.id}/leave`,
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -497,7 +691,7 @@ describe('<ProjectListRoot />', function () {
|
|||
fireEvent.click(moreDropdown)
|
||||
|
||||
const renameButton =
|
||||
screen.getAllByText<HTMLInputElement>('Rename')[1] // first one is for the tag in the sidebar
|
||||
screen.getAllByText<HTMLButtonElement>('Rename')[1] // first one is for the tag in the sidebar
|
||||
fireEvent.click(renameButton)
|
||||
|
||||
const modals = await screen.findAllByRole('dialog')
|
||||
|
@ -508,7 +702,7 @@ describe('<ProjectListRoot />', function () {
|
|||
|
||||
// same name
|
||||
let confirmButton =
|
||||
within(modal).getByText<HTMLInputElement>('Rename')
|
||||
within(modal).getByText<HTMLButtonElement>('Rename')
|
||||
expect(confirmButton.disabled).to.be.true
|
||||
let input = screen.getByLabelText('New Name') as HTMLButtonElement
|
||||
|
||||
|
@ -517,7 +711,7 @@ describe('<ProjectListRoot />', function () {
|
|||
fireEvent.change(input, {
|
||||
target: { value: '' },
|
||||
})
|
||||
confirmButton = within(modal).getByText<HTMLInputElement>('Rename')
|
||||
confirmButton = within(modal).getByText<HTMLButtonElement>('Rename')
|
||||
expect(confirmButton.disabled).to.be.true
|
||||
})
|
||||
|
||||
|
@ -534,7 +728,7 @@ describe('<ProjectListRoot />', function () {
|
|||
fireEvent.click(moreDropdown)
|
||||
|
||||
const renameButton =
|
||||
within(actionsToolbar).getByText<HTMLInputElement>('Rename') // first one is for the tag in the sidebar
|
||||
within(actionsToolbar).getByText<HTMLButtonElement>('Rename') // first one is for the tag in the sidebar
|
||||
fireEvent.click(renameButton)
|
||||
|
||||
const modals = await screen.findAllByRole('dialog')
|
||||
|
@ -551,7 +745,7 @@ describe('<ProjectListRoot />', function () {
|
|||
})
|
||||
|
||||
const confirmButton =
|
||||
within(modal).getByText<HTMLInputElement>('Rename')
|
||||
within(modal).getByText<HTMLButtonElement>('Rename')
|
||||
expect(confirmButton.disabled).to.be.false
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
|
@ -605,7 +799,7 @@ describe('<ProjectListRoot />', function () {
|
|||
})
|
||||
|
||||
const copyButton =
|
||||
within(actionsToolbar).getByText<HTMLInputElement>('Make a copy')
|
||||
within(actionsToolbar).getByText<HTMLButtonElement>('Make a copy')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
// confirm in modal
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { fireEvent, screen, render } from '@testing-library/react'
|
||||
import DeleteLeaveProjectsButton from '../../../../../../../../frontend/js/features/project-list/components/table/project-tools/buttons/delete-leave-projects-button'
|
||||
import { makeLongProjectList } from '../../../../fixtures/projects-data'
|
||||
import {
|
||||
ProjectListContext,
|
||||
ProjectListContextValue,
|
||||
} from '../../../../../../../../frontend/js/features/project-list/context/project-list-context'
|
||||
|
||||
const { deletableList, leavableList } = makeLongProjectList(40)
|
||||
|
||||
describe('<DeleteLeaveProjectsButton />', function () {
|
||||
it('opens the modal when clicked', function () {
|
||||
const value = {
|
||||
selectedProjects: [...deletableList, ...leavableList],
|
||||
hasDeletableProjectsSelected: true,
|
||||
hasLeavableProjectsSelected: true,
|
||||
} as ProjectListContextValue
|
||||
|
||||
render(
|
||||
<ProjectListContext.Provider value={value}>
|
||||
<DeleteLeaveProjectsButton />
|
||||
</ProjectListContext.Provider>
|
||||
)
|
||||
|
||||
const btn = screen.getByRole('button', { name: /delete \/ leave/i })
|
||||
fireEvent.click(btn)
|
||||
screen.getByRole('heading', { name: /delete and leave projects/i })
|
||||
})
|
||||
})
|
|
@ -0,0 +1,29 @@
|
|||
import { fireEvent, screen, render } from '@testing-library/react'
|
||||
import DeleteProjectsButton from '../../../../../../../../frontend/js/features/project-list/components/table/project-tools/buttons/delete-projects-button'
|
||||
import { makeLongProjectList } from '../../../../fixtures/projects-data'
|
||||
import {
|
||||
ProjectListContext,
|
||||
ProjectListContextValue,
|
||||
} from '../../../../../../../../frontend/js/features/project-list/context/project-list-context'
|
||||
|
||||
const { deletableList } = makeLongProjectList(40)
|
||||
|
||||
describe('<DeleteProjectsButton />', function () {
|
||||
it('opens the modal when clicked', function () {
|
||||
const value = {
|
||||
selectedProjects: deletableList,
|
||||
hasDeletableProjectsSelected: true,
|
||||
hasLeavableProjectsSelected: false,
|
||||
} as ProjectListContextValue
|
||||
|
||||
render(
|
||||
<ProjectListContext.Provider value={value}>
|
||||
<DeleteProjectsButton />
|
||||
</ProjectListContext.Provider>
|
||||
)
|
||||
|
||||
const btn = screen.getByRole('button', { name: /delete/i })
|
||||
fireEvent.click(btn)
|
||||
screen.getByRole('heading', { name: /delete projects/i })
|
||||
})
|
||||
})
|
|
@ -0,0 +1,28 @@
|
|||
import { fireEvent, screen, render } from '@testing-library/react'
|
||||
import LeaveProjectsButton from '../../../../../../../../frontend/js/features/project-list/components/table/project-tools/buttons/leave-projects-button'
|
||||
import { makeLongProjectList } from '../../../../fixtures/projects-data'
|
||||
import {
|
||||
ProjectListContext,
|
||||
ProjectListContextValue,
|
||||
} from '../../../../../../../../frontend/js/features/project-list/context/project-list-context'
|
||||
|
||||
const { leavableList } = makeLongProjectList(40)
|
||||
|
||||
describe('<LeaveProjectsButton />', function () {
|
||||
it('opens the modal when clicked', function () {
|
||||
const value = {
|
||||
selectedProjects: leavableList,
|
||||
hasDeletableProjectsSelected: false,
|
||||
hasLeavableProjectsSelected: true,
|
||||
} as ProjectListContextValue
|
||||
|
||||
render(
|
||||
<ProjectListContext.Provider value={value}>
|
||||
<LeaveProjectsButton />
|
||||
</ProjectListContext.Provider>
|
||||
)
|
||||
const btn = screen.getByRole('button', { name: /leave/i })
|
||||
fireEvent.click(btn)
|
||||
screen.getByRole('heading', { name: /leave projects/i })
|
||||
})
|
||||
})
|
|
@ -76,7 +76,7 @@ describe('<ProjectsActionModal />', function () {
|
|||
})
|
||||
})
|
||||
|
||||
it('should send an analytics even when opened', function () {
|
||||
it('should send an analytics event when opened', function () {
|
||||
renderWithProjectListContext(
|
||||
<ProjectsActionModal
|
||||
action="archive"
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { Project } from '../../../../../types/project/dashboard/api'
|
||||
import {
|
||||
isDeletableProject,
|
||||
isLeavableProject,
|
||||
} from '../../../../../frontend/js/features/project-list/util/project'
|
||||
|
||||
const moment = require('moment')
|
||||
|
||||
export const owner = {
|
||||
|
@ -157,5 +162,7 @@ export const makeLongProjectList = (listLength: number) => {
|
|||
({ archived, trashed }) => !archived && !trashed
|
||||
),
|
||||
trashedList: longList.filter(({ trashed }) => trashed),
|
||||
leavableList: longList.filter(isLeavableProject),
|
||||
deletableList: longList.filter(isDeletableProject),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue