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

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

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

GitOrigin-RevId: c671b97646918e185d7989f539b44562a87873ee
Jessica Lawshe 4 лет назад
Родитель
Сommit
8323cb37e8

+ 28 - 5
services/web/frontend/js/features/project-list/components/modals/projects-action-modal.tsx

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

+ 20 - 0
services/web/frontend/stylesheets/app/project-list-react.less

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

+ 85 - 0
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

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