Merge pull request #9784 from overleaf/jel-project-tools-copy

[web] Add copy option to project tools dropdown

GitOrigin-RevId: 028ba62ed858376e472a5e4e5520079cd5f60ec5
This commit is contained in:
Jessica Lawshe 2022-10-03 08:20:42 -05:00 committed by Copybot
parent f2748c13d7
commit f68d0d1e5f
5 changed files with 118 additions and 3 deletions

View file

@ -354,6 +354,7 @@
"logs_and_output_files": "",
"looks_like_youre_at": "",
"main_file_not_found": "",
"make_a_copy": "",
"make_email_primary_description": "",
"make_primary": "",
"make_private": "",

View file

@ -2,6 +2,7 @@ import { memo } from 'react'
import { Dropdown } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import ControlledDropdown from '../../../../../../shared/components/controlled-dropdown'
import CopyProjectMenuItem from '../menu-items/copy-project-menu-item'
import RenameProjectMenuItem from '../menu-items/rename-project-menu-item'
function ProjectToolsMoreDropdownButton() {
@ -11,6 +12,7 @@ function ProjectToolsMoreDropdownButton() {
<Dropdown.Toggle>{t('more')}</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
<RenameProjectMenuItem />
<CopyProjectMenuItem />
</Dropdown.Menu>
</ControlledDropdown>
)

View file

@ -0,0 +1,61 @@
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { MenuItem } from 'react-bootstrap'
import CloneProjectModal from '../../../../../clone-project-modal/components/clone-project-modal'
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
import { useProjectListContext } from '../../../../context/project-list-context'
import * as eventTracking from '../../../../../../infrastructure/event-tracking'
import { Project } from '../../../../../../../../types/project/dashboard/api'
function CopyProjectMenuItem() {
const {
addClonedProjectToViewData,
updateProjectViewData,
selectedProjects,
} = useProjectListContext()
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const isMounted = useIsMounted()
const handleOpenModal = useCallback(() => {
setShowModal(true)
}, [])
const handleCloseModal = useCallback(() => {
if (isMounted.current) {
setShowModal(false)
}
}, [isMounted])
const handleAfterCloned = (clonedProject: Project) => {
const project = selectedProjects[0]
eventTracking.send(
'project-list-page-interaction',
'project action',
'Clone'
)
addClonedProjectToViewData(clonedProject)
updateProjectViewData({ ...project, selected: false })
setShowModal(false)
}
if (selectedProjects.length !== 1) return null
if (selectedProjects[0].archived || selectedProjects[0].trashed) return null
return (
<>
<CloneProjectModal
show={showModal}
handleHide={handleCloseModal}
handleAfterCloned={handleAfterCloned}
projectId={selectedProjects[0].id}
projectName={selectedProjects[0].name}
/>
<MenuItem onClick={handleOpenModal}>{t('make_a_copy')}</MenuItem>
</>
)
}
export default memo(CopyProjectMenuItem)

View file

@ -1904,5 +1904,6 @@
"overleaf_labs": "Overleaf Labs",
"show_x_more": "Show __x__ more",
"show_x_more_projects": "Show __x__ more projects",
"showing_x_out_of_n_projects": "Showing __x__ out of __n__ projects."
"showing_x_out_of_n_projects": "Showing __x__ out of __n__ projects.",
"make_a_copy": "Make a copy"
}

View file

@ -341,7 +341,7 @@ describe('<ProjectListRoot />', function () {
.null
})
it('opens the rename modal, validates name, and can rename the project', async function () {
it('opens the rename modal, and can rename the project, and view updated', async function () {
fetchMock.post(`express:/project/:id/rename`, {
status: 200,
})
@ -396,6 +396,54 @@ describe('<ProjectListRoot />', function () {
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
expect(allCheckboxesChecked.length).to.equal(0)
})
it('opens the copy modal, can copy the project, and view updated', async function () {
const tableRows = screen.getAllByRole('row')
const linkForProjectToCopy = within(tableRows[1]).getByRole('link')
const projectNameToCopy = linkForProjectToCopy.textContent || '' // needed for type checking
screen.findByText(projectNameToCopy) // make sure not just empty string
const copiedProjectName = `${projectNameToCopy} (Copy)`
fetchMock.post(`express:/project/:id/clone`, {
status: 200,
body: {
name: copiedProjectName,
lastUpdated: new Date(),
project_id: userId,
owner_ref: userId,
owner,
id: '6328e14abec0df019fce0be5',
lastUpdatedBy: owner,
accessLevel: 'owner',
source: 'owner',
trashed: false,
archived: false,
},
})
await waitFor(() => {
const moreDropdown =
within(actionsToolbar).getByText<HTMLElement>('More')
fireEvent.click(moreDropdown)
})
const copyButton =
within(actionsToolbar).getByText<HTMLInputElement>('Make a copy')
fireEvent.click(copyButton)
// confirm in modal
const copyConfirmButton = document.querySelector(
'button[type="submit"]'
) as HTMLElement
fireEvent.click(copyConfirmButton)
await fetchMock.flush(true)
expect(fetchMock.done()).to.be.true
expect(sendSpy).to.be.calledOnce
expect(sendSpy).calledWith('project-list-page-interaction')
screen.findByText(copiedProjectName)
})
})
})
@ -443,7 +491,6 @@ describe('<ProjectListRoot />', function () {
fireEvent.click(copyButton)
// confirm in modal
// const copyConfirmButton = screen.getByText('Copy')
const copyConfirmButton = document.querySelector(
'button[type="submit"]'
) as HTMLElement
@ -452,6 +499,9 @@ describe('<ProjectListRoot />', function () {
await fetchMock.flush(true)
expect(fetchMock.done()).to.be.true
expect(sendSpy).to.be.calledOnce
expect(sendSpy).calledWith('project-list-page-interaction')
expect(screen.queryByText(copiedProjectName)).to.be.null
const yourProjectFilter = screen.getAllByText('Your Projects')[0]