Просмотр исходного кода

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

[web] Add copy option to project tools dropdown

GitOrigin-RevId: 028ba62ed858376e472a5e4e5520079cd5f60ec5
Jessica Lawshe 4 лет назад
Родитель
Сommit
f68d0d1e5f

+ 1 - 0
services/web/frontend/extracted-translations.json

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

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

+ 61 - 0
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 (
+    <>
+      <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)

+ 2 - 1
services/web/locales/en.json

@@ -1904,5 +1904,6 @@
   "overleaf_labs": "Overleaf Labs",
   "overleaf_labs": "Overleaf Labs",
   "show_x_more": "Show __x__ more",
   "show_x_more": "Show __x__ more",
   "show_x_more_projects": "Show __x__ more projects",
   "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"
 }
 }

+ 52 - 2
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

@@ -341,7 +341,7 @@ describe('<ProjectListRoot />', function () {
           .null
           .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`, {
         fetchMock.post(`express:/project/:id/rename`, {
           status: 200,
           status: 200,
         })
         })
@@ -396,6 +396,54 @@ describe('<ProjectListRoot />', function () {
         const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
         const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
         expect(allCheckboxesChecked.length).to.equal(0)
         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)
       fireEvent.click(copyButton)
 
 
       // confirm in modal
       // confirm in modal
-      // const copyConfirmButton = screen.getByText('Copy')
       const copyConfirmButton = document.querySelector(
       const copyConfirmButton = document.querySelector(
         'button[type="submit"]'
         'button[type="submit"]'
       ) as HTMLElement
       ) as HTMLElement
@@ -452,6 +499,9 @@ describe('<ProjectListRoot />', function () {
       await fetchMock.flush(true)
       await fetchMock.flush(true)
       expect(fetchMock.done()).to.be.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
       expect(screen.queryByText(copiedProjectName)).to.be.null
 
 
       const yourProjectFilter = screen.getAllByText('Your Projects')[0]
       const yourProjectFilter = screen.getAllByText('Your Projects')[0]