mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-21 00:13:37 +00:00
Merge pull request #9565 from overleaf/jel-dash-btn-delete
[web] Add delete project modal GitOrigin-RevId: 4ff408aeeac5c96dc8982ded699f3ef355999a9d
This commit is contained in:
parent
8438de1167
commit
4c02db777b
5 changed files with 122 additions and 16 deletions
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
"about_to_archive_projects": "",
|
||||
"about_to_delete_folder": "",
|
||||
"about_to_delete_projects": "",
|
||||
"about_to_delete_the_following": "",
|
||||
"about_to_leave_projects": "",
|
||||
"about_to_trash_projects": "",
|
||||
"access_denied": "",
|
||||
|
@ -60,6 +62,7 @@
|
|||
"change_or_cancel-cancel": "",
|
||||
"change_or_cancel-change": "",
|
||||
"change_or_cancel-or": "",
|
||||
"change_owner": "",
|
||||
"change_password": "",
|
||||
"change_primary_email_address_instructions": "<0></0><2></2>",
|
||||
"change_project_owner": "",
|
||||
|
@ -114,6 +117,7 @@
|
|||
"delete_account_confirmation_label": "",
|
||||
"delete_account_warning_message_3": "",
|
||||
"delete_acct_no_existing_pw": "",
|
||||
"delete_folder": "",
|
||||
"delete_projects": "",
|
||||
"delete_your_account": "",
|
||||
"deleting": "",
|
||||
|
@ -439,6 +443,8 @@
|
|||
"project_layout_sharing_submission": "",
|
||||
"project_name": "",
|
||||
"project_not_linked_to_github": "",
|
||||
"project_ownership_transfer_confirmation_1": "",
|
||||
"project_ownership_transfer_confirmation_2": "",
|
||||
"project_synced_with_git_repo_at": "",
|
||||
"project_synchronisation": "",
|
||||
"project_timed_out_enable_stop_on_first_error": "",
|
||||
|
|
|
@ -1,33 +1,80 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { Project } from '../../../../../../../../types/project/dashboard/api'
|
||||
import Icon from '../../../../../../shared/components/icon'
|
||||
import Tooltip from '../../../../../../shared/components/tooltip'
|
||||
import ProjectsActionModal from '../../projects-action-modal'
|
||||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||
import { deleteProject } from '../../../../util/api'
|
||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||
|
||||
type DeleteProjectButtonProps = {
|
||||
project: Project
|
||||
}
|
||||
|
||||
function DeleteProjectButton({ project }: DeleteProjectButtonProps) {
|
||||
const { removeProjectFromView } = useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
const text = t('delete')
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const handleOpenModal = useCallback(() => {
|
||||
setShowModal(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
if (isMounted.current) {
|
||||
setShowModal(false)
|
||||
}
|
||||
}, [isMounted])
|
||||
|
||||
const isOwner = useMemo(() => {
|
||||
return project.owner && window.user_id === project.owner.id
|
||||
}, [project])
|
||||
|
||||
const handleDeleteProject = useCallback(async () => {
|
||||
await deleteProject(project.id)
|
||||
|
||||
// update view
|
||||
removeProjectFromView(project)
|
||||
}, [project, removeProjectFromView])
|
||||
|
||||
if (!project.trashed || !isOwner) return null
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={`tooltip-delete-project-${project.id}`}
|
||||
id={`tooltip-delete-project-${project.id}`}
|
||||
description={text}
|
||||
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
|
||||
>
|
||||
<button className="btn btn-link action-btn" aria-label={text}>
|
||||
<Icon type="ban" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<>
|
||||
<Tooltip
|
||||
key={`tooltip-delete-project-${project.id}`}
|
||||
id={`tooltip-delete-project-${project.id}`}
|
||||
description={text}
|
||||
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
|
||||
>
|
||||
<button
|
||||
className="btn btn-link action-btn"
|
||||
aria-label={text}
|
||||
onClick={handleOpenModal}
|
||||
>
|
||||
<Icon type="ban" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<ProjectsActionModal
|
||||
title={t('delete_projects')}
|
||||
action="delete"
|
||||
actionHandler={handleDeleteProject}
|
||||
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={[project]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import * as eventTracking from '../../../../infrastructure/event-tracking'
|
|||
|
||||
type ProjectsActionModalProps = {
|
||||
title: string
|
||||
action: 'archive' | 'trash'
|
||||
action: 'archive' | 'trash' | 'delete'
|
||||
actionHandler: (project: Project) => Promise<void>
|
||||
handleCloseModal: () => void
|
||||
bodyTop: React.ReactNode
|
||||
|
|
|
@ -33,6 +33,14 @@ export function archiveProject(projectId: string) {
|
|||
})
|
||||
}
|
||||
|
||||
export function deleteProject(projectId: string) {
|
||||
return deleteJSON(`/project/${projectId}`, {
|
||||
body: {
|
||||
_csrf: window.csrfToken,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function leaveProject(projectId: string) {
|
||||
return postJSON(`/project/${projectId}/leave`, {
|
||||
body: {
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
import { expect } from 'chai'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import DeleteProjectButton from '../../../../../../../../frontend/js/features/project-list/components/table/cells/action-buttons/delete-project-button'
|
||||
import {
|
||||
archiveableProject,
|
||||
trashedAndNotOwnedProject,
|
||||
trashedProject,
|
||||
} from '../../../../fixtures/projects-data'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {
|
||||
renderWithProjectListContext,
|
||||
resetProjectListContextFetch,
|
||||
} from '../../../../helpers/render-with-context'
|
||||
|
||||
describe('<DeleteProjectButton />', function () {
|
||||
afterEach(function () {
|
||||
resetProjectListContextFetch()
|
||||
})
|
||||
|
||||
it('renders tooltip for button', function () {
|
||||
window.user_id = trashedProject?.owner?.id
|
||||
render(<DeleteProjectButton project={trashedProject} />)
|
||||
renderWithProjectListContext(
|
||||
<DeleteProjectButton project={trashedProject} />
|
||||
)
|
||||
const btn = screen.getByLabelText('Delete')
|
||||
fireEvent.mouseOver(btn)
|
||||
screen.getByRole('tooltip', { name: 'Delete' })
|
||||
|
@ -18,13 +29,47 @@ describe('<DeleteProjectButton />', function () {
|
|||
|
||||
it('does not render button when trashed and not owner', function () {
|
||||
window.user_id = '123abc'
|
||||
render(<DeleteProjectButton project={trashedAndNotOwnedProject} />)
|
||||
renderWithProjectListContext(
|
||||
<DeleteProjectButton project={trashedAndNotOwnedProject} />
|
||||
)
|
||||
const btn = screen.queryByLabelText('Delete')
|
||||
expect(btn).to.be.null
|
||||
})
|
||||
|
||||
it('does not render the button when project is current', function () {
|
||||
render(<DeleteProjectButton project={archiveableProject} />)
|
||||
renderWithProjectListContext(
|
||||
<DeleteProjectButton project={archiveableProject} />
|
||||
)
|
||||
expect(screen.queryByLabelText('Delete')).to.be.null
|
||||
})
|
||||
|
||||
it('opens the modal and deletes the project', async function () {
|
||||
window.user_id = trashedProject?.owner?.id
|
||||
const project = Object.assign({}, trashedProject)
|
||||
fetchMock.delete(
|
||||
`express:/project/${project.id}`,
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
{ delay: 0 }
|
||||
)
|
||||
renderWithProjectListContext(<DeleteProjectButton project={project} />)
|
||||
const btn = screen.getByLabelText('Delete')
|
||||
fireEvent.click(btn)
|
||||
screen.getByText('Delete Projects')
|
||||
screen.getByText('You are about to delete the following projects:')
|
||||
screen.getByText('This action cannot be undone.')
|
||||
const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
|
||||
fireEvent.click(confirmBtn)
|
||||
expect(confirmBtn.disabled).to.be.true
|
||||
// verify trashed
|
||||
await fetchMock.flush(true)
|
||||
expect(fetchMock.done()).to.be.true
|
||||
const requests = fetchMock.calls()
|
||||
// first request is project list api in projectlistcontext
|
||||
const [requestUrl, requestHeaders] = requests[1]
|
||||
expect(requestUrl).to.equal(`/project/${project.id}`)
|
||||
expect(requestHeaders?.method).to.equal('DELETE')
|
||||
fetchMock.reset()
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue