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

[web] Add rename button to project list mobile view

GitOrigin-RevId: 4ca3c68dcaf4e0d5e97f501084b9f850f9a4e867
This commit is contained in:
Mathias Jakobsen 2024-06-20 13:20:22 +01:00 committed by Copybot
parent 7b47acc486
commit 46d687855e
6 changed files with 147 additions and 3 deletions

View file

@ -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}

View file

@ -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)

View file

@ -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>

View file

@ -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
)
})
})

View file

@ -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')
})
})

View file

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