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

Merge pull request #18981 from overleaf/mj-mobile-dropdown

[web] Add rename button to project list mobile view

GitOrigin-RevId: 4ca3c68dcaf4e0d5e97f501084b9f850f9a4e867
Mathias Jakobsen 2 лет назад
Родитель
Сommit
46d687855e

+ 22 - 0
services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx

@@ -12,6 +12,7 @@ import LeaveProjectButton from '../table/cells/action-buttons/leave-project-butt
 import DeleteProjectButton from '../table/cells/action-buttons/delete-project-button'
 import { Project } from '../../../../../../types/project/dashboard/api'
 import CompileAndDownloadProjectPDFButton from '../table/cells/action-buttons/compile-and-download-project-pdf-button'
+import RenameProjectButton from '../table/cells/action-buttons/rename-project-button'
 
 type ActionButtonProps = {
   project: Project
@@ -194,6 +195,26 @@ function DeleteProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
   )
 }
 
+function RenameProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
+  const handleClick = (handleOpenModal: () => void) => {
+    handleOpenModal()
+    onClick()
+  }
+  return (
+    <RenameProjectButton project={project}>
+      {(text, handleOpenModal) => (
+        <MenuItemButton
+          onClick={() => handleClick(handleOpenModal)}
+          className="projects-action-menu-item"
+        >
+          <Icon type="pencil" className="menu-item-button-icon" />{' '}
+          <span className="menu-item-button-text">{text}</span>
+        </MenuItemButton>
+      )}
+    </RenameProjectButton>
+  )
+}
+
 type ActionDropdownProps = {
   project: Project
 }
@@ -216,6 +237,7 @@ function ActionsDropdown({ project }: ActionDropdownProps) {
         <Icon type="ellipsis-h" fw />
       </Dropdown.Toggle>
       <Dropdown.Menu className="projects-dropdown-menu text-left">
+        <RenameProjectButtonMenuItem project={project} onClick={handleClose} />
         <CopyProjectButtonMenuItem project={project} onClick={handleClose} />
         <DownloadProjectButtonMenuItem
           project={project}

+ 43 - 0
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/rename-project-button.tsx

@@ -0,0 +1,43 @@
+import { Project } from '../../../../../../../../types/project/dashboard/api'
+import { useTranslation } from 'react-i18next'
+import { memo, useCallback, useState } from 'react'
+import useIsMounted from '@/shared/hooks/use-is-mounted'
+import RenameProjectModal from '../../../modals/rename-project-modal'
+
+type RenameProjectButtonProps = {
+  project: Project
+  children: (text: string, handleOpenModal: () => void) => React.ReactElement
+}
+
+function RenameProjectButton({ project, children }: RenameProjectButtonProps) {
+  const { t } = useTranslation()
+  const text = t('rename')
+  const [showModal, setShowModal] = useState(false)
+  const isMounted = useIsMounted()
+
+  const handleOpenModal = useCallback(() => {
+    setShowModal(true)
+  }, [])
+
+  const handleCloseModal = useCallback(() => {
+    if (isMounted.current) {
+      setShowModal(false)
+    }
+  }, [isMounted])
+
+  if (project.accessLevel !== 'owner') {
+    return null
+  }
+  return (
+    <>
+      {children(text, handleOpenModal)}
+      <RenameProjectModal
+        handleCloseModal={handleCloseModal}
+        project={project}
+        showModal={showModal}
+      />
+    </>
+  )
+}
+
+export default memo(RenameProjectButton)

+ 4 - 1
services/web/frontend/js/features/project-list/components/table/project-tools/buttons/project-tools-more-dropdown-button.tsx

@@ -12,7 +12,10 @@ function ProjectToolsMoreDropdownButton() {
       <Dropdown.Toggle bsStyle={null} className="btn-secondary">
         {t('more')}
       </Dropdown.Toggle>
-      <Dropdown.Menu className="dropdown-menu-right">
+      <Dropdown.Menu
+        className="dropdown-menu-right"
+        data-testid="project-tools-more-dropdown-menu"
+      >
         <RenameProjectMenuItem />
         <CopyProjectMenuItem />
       </Dropdown.Menu>

+ 68 - 0
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/rename-project-button.test.tsx

@@ -0,0 +1,68 @@
+import RenameProjectButton from '@/features/project-list/components/table/cells/action-buttons/rename-project-button'
+import {
+  renderWithProjectListContext,
+  resetProjectListContextFetch,
+} from '../../../../helpers/render-with-context'
+import { ownedProject, sharedProject } from '../../../../fixtures/projects-data'
+import { Project } from '../../../../../../../../types/project/dashboard/api'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
+import fetchMock from 'fetch-mock'
+import { expect } from 'chai'
+
+// Little test jig for rendering the button
+function renderWithProject(project: Project) {
+  renderWithProjectListContext(
+    <RenameProjectButton project={project}>
+      {(text, onClick) => {
+        return <button onClick={onClick}>Rename Project Button</button>
+      }}
+    </RenameProjectButton>
+  )
+}
+
+describe('<RenameProjectButton />', function () {
+  afterEach(function () {
+    resetProjectListContextFetch()
+  })
+
+  it('opens the modal when clicked', function () {
+    renderWithProject(ownedProject)
+    const btn = screen.getByRole('button')
+    fireEvent.click(btn)
+    screen.getByText('Rename Project')
+    screen.getByDisplayValue(ownedProject.name)
+  })
+
+  it('does not render the button when already archived', function () {
+    renderWithProject(sharedProject)
+    expect(screen.queryByRole('button')).to.be.null
+  })
+
+  it('should rename the project', async function () {
+    const project = Object.assign({}, ownedProject)
+    const renameProjectMock = fetchMock.post(
+      `express:/project/:projectId/rename`,
+      {
+        status: 200,
+      },
+      { delay: 0 }
+    )
+    renderWithProject(ownedProject)
+    const btn = screen.getByRole('button')
+    fireEvent.click(btn)
+    screen.getByText('Rename Project')
+    const confirmBtn = screen.getByText('Rename') as HTMLButtonElement
+    expect(confirmBtn.disabled).to.be.true
+    const nameInput = screen.getByDisplayValue(ownedProject.name)
+    fireEvent.change(nameInput, { target: { value: 'new name' } })
+    expect(confirmBtn.disabled).to.be.false
+    fireEvent.click(confirmBtn)
+    expect(confirmBtn.disabled).to.be.true
+
+    await waitFor(
+      () =>
+        expect(renameProjectMock.called(`/project/${project.id}/rename`)).to.be
+          .true
+    )
+  })
+})

+ 8 - 2
services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx

@@ -64,14 +64,20 @@ describe('<ProjectTools />', function () {
     render(<ProjectListRootInner />)
     screen.getByLabelText('Select Starfleet Report (readAndWrite)').click()
     screen.getByRole('button', { name: 'More' }).click()
-    expect(screen.queryByRole('menuitem', { name: 'Rename' })).to.be.null
+    expect(
+      within(
+        screen.getByTestId('project-tools-more-dropdown-menu')
+      ).queryByRole('menuitem', { name: 'Rename' })
+    ).to.be.null
   })
 
   it('displays the Rename option for a project owned by the current user', function () {
     render(<ProjectListRootInner />)
     screen.getByLabelText('Select Starfleet Report (owner)').click()
     screen.getByRole('button', { name: 'More' }).click()
-    screen.getByRole('menuitem', { name: 'Rename' }).click()
+    within(screen.getByTestId('project-tools-more-dropdown-menu'))
+      .getByRole('menuitem', { name: 'Rename' })
+      .click()
     within(screen.getByRole('dialog')).getByText('Rename Project')
   })
 })

+ 2 - 0
services/web/test/frontend/features/project-list/fixtures/projects-data.ts

@@ -97,6 +97,8 @@ export const trashedAndNotOwnedProject = <Project>{
 
 export const sharedProject = archiveableProject
 
+export const ownedProject = copyableProject
+
 export const projectsData: Array<Project> = [
   copyableProject,
   archiveableProject,