Merge pull request #9955 from overleaf/jel-actions-list

[web] Maintain project list in actions modal while processing and show processed

GitOrigin-RevId: c671b97646918e185d7989f539b44562a87873ee
This commit is contained in:
Jessica Lawshe 2022-10-17 10:54:12 -05:00 committed by Copybot
parent fe963ba692
commit 8323cb37e8
3 changed files with 133 additions and 5 deletions

View file

@ -1,4 +1,4 @@
import { memo, useEffect, useState } from 'react'
import { memo, useEffect, useRef, useState } from 'react'
import { Alert, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Project } from '../../../../../../types/project/dashboard/api'
@ -31,6 +31,8 @@ function ProjectsActionModal({
const { t } = useTranslation()
const [errors, setErrors] = useState<Array<any>>([])
const [isProcessing, setIsProcessing] = useState(false)
const [projectsToDisplay, setProjectsToDisplay] = useState<Project[]>([])
const projectsRef = useRef<Project[]>([])
const isMounted = useIsMounted()
async function handleActionForProjects(projects: Array<Project>) {
@ -64,8 +66,21 @@ function ProjectsActionModal({
'project action',
action
)
if (projectsRef.current.length > 0) {
// maintain the original list in the display of the modal,
// even after some project actions have completed
setProjectsToDisplay(projectsRef.current)
} else {
projectsRef.current = projects
setProjectsToDisplay(projects)
}
} else {
projectsRef.current = []
}
}, [action, showModal])
}, [action, projects, projectsRef, showModal])
const projectIdsRemainingToProcess = projects.map(p => p.id)
return (
<AccessibleModal
@ -80,13 +95,21 @@ function ProjectsActionModal({
</Modal.Header>
<Modal.Body>
{bodyTop}
<ul>
{projects.map(project => (
<li key={`projects-action-list-${project.id}`}>
<ul className="projects-action-list">
{projectsToDisplay.map(project => (
<li
key={`projects-action-list-${project.id}`}
className={
projectIdsRemainingToProcess.includes(project.id)
? ''
: 'completed-project-action'
}
>
<b>{project.name}</b>
</li>
))}
</ul>
{bodyBottom}
</Modal.Body>
<Modal.Footer>

View file

@ -707,3 +707,23 @@
flex: @project-list-sidebar-wrapper-flex;
}
}
.projects-action-list {
list-style: none;
li:not(.completed-project-action) {
list-style-type: disc;
}
li.completed-project-action {
&:before {
display: inline-block;
content: '\f00c';
font-family: FontAwesome;
color: @ol-green;
width: 1.2em;
margin-left: -1.2em;
height: 0; // prevent slight vertical layout shift
}
}
}

View file

@ -640,6 +640,91 @@ describe('<ProjectListRoot />', function () {
screen.getByText(copiedProjectName)
})
})
describe('projects list in actions modal', function () {
let modal: HTMLElement
let projectsToProcess: any[]
function selectedProjectNames() {
projectsToProcess = []
// needs to be done ahead of opening modal
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
// update list so we know which are checked
const tableRows = screen.getAllByRole('row')
for (const [index, checkbox] of allCheckboxes.entries()) {
if (checkbox.checked) {
const linkForProjectToCopy = within(tableRows[index]).getByRole(
'link'
)
const projectNameToCopy = linkForProjectToCopy.textContent
projectsToProcess.push(projectNameToCopy)
}
}
expect(projectsToProcess.length > 0).to.be.true
}
function selectedMatchesDisplayed(expectedLength: number) {
selectedProjectNames()
// any action will work for check since they all use the same modal
const archiveButton = within(actionsToolbar).getByLabelText('Archive')
fireEvent.click(archiveButton)
modal = screen.getAllByRole('dialog')[0]
const listitems = within(modal).getAllByRole('listitem')
expect(listitems.length).to.equal(projectsToProcess.length)
expect(listitems.length).to.equal(expectedLength)
for (const projectName of projectsToProcess) {
within(modal).getByText(projectName)
}
}
beforeEach(function () {
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
// first one is the select all checkbox, just check 2 at first
fireEvent.click(allCheckboxes[1])
fireEvent.click(allCheckboxes[2])
actionsToolbar = screen.getAllByRole('toolbar')[0]
})
it('opens the modal with the 2 originally selected projects', function () {
selectedMatchesDisplayed(2)
})
it('shows correct list after closing modal, changing selecting, and reopening modal', async function () {
selectedMatchesDisplayed(2)
const cancelButton = screen.getByRole('button', { name: 'Cancel' })
fireEvent.click(cancelButton)
expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
await screen.findAllByRole('checkbox')
fireEvent.click(allCheckboxes[3])
selectedMatchesDisplayed(3)
})
it('maintains original list even after some have been processed', async function () {
const totalProjectsToProcess = 2
selectedMatchesDisplayed(totalProjectsToProcess)
const button = screen.getByRole('button', { name: 'Confirm' })
fireEvent.click(button)
project1Id = allCheckboxes[1].getAttribute('data-project-id')
fetchMock.post('express:/project/:id/archive', {
status: 200,
})
fetchMock.post(`express:/${project2Id}/:id/archive`, {
status: 500,
})
await screen.findByRole('alert') // ensure that error was thrown for the 2nd project
const listitems = within(modal).getAllByRole('listitem')
expect(listitems.length).to.equal(totalProjectsToProcess)
})
})
})
describe('search', function () {