From f68d0d1e5f025f2163ba0fcfea444831a3f9626f Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 3 Oct 2022 08:20:42 -0500 Subject: [PATCH] Merge pull request #9784 from overleaf/jel-project-tools-copy [web] Add copy option to project tools dropdown GitOrigin-RevId: 028ba62ed858376e472a5e4e5520079cd5f60ec5 --- .../web/frontend/extracted-translations.json | 1 + .../project-tools-more-dropdown-button.tsx | 2 + .../menu-items/copy-project-menu-item.tsx | 61 +++++++++++++++++++ services/web/locales/en.json | 3 +- .../components/project-list-root.test.tsx | 54 +++++++++++++++- 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 8c258af5af..55b9e86e77 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/project-tools-more-dropdown-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/project-tools-more-dropdown-button.tsx index af0270baf0..4f1c558b31 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/project-tools-more-dropdown-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/project-tools-more-dropdown-button.tsx @@ -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() { {t('more')} + ) diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx new file mode 100644 index 0000000000..bd4b5239c0 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx @@ -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 ( + <> + + {t('make_a_copy')} + + ) +} + +export default memo(CopyProjectMenuItem) diff --git a/services/web/locales/en.json b/services/web/locales/en.json index e5b4a8aa3b..c498de0ad0 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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" } diff --git a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx index e9432793fe..0ee1532531 100644 --- a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx +++ b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx @@ -341,7 +341,7 @@ describe('', 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('', 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('More') + fireEvent.click(moreDropdown) + }) + + const copyButton = + within(actionsToolbar).getByText('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('', 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('', 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]