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

Merge pull request #9744 from overleaf/jel-refactor-hidden-projects

[web] Refactor hidden projects

GitOrigin-RevId: e9d685842959d34ffa8dfe403b1afffddfe4ab1d
Jessica Lawshe 3 лет назад
Родитель
Сommit
08596f1fb3

+ 4 - 4
services/web/frontend/js/features/project-list/components/load-more.tsx

@@ -5,7 +5,7 @@ import { useProjectListContext } from '../context/project-list-context'
 export default function LoadMore() {
 export default function LoadMore() {
   const {
   const {
     visibleProjects,
     visibleProjects,
-    hiddenProjects,
+    hiddenProjectsCount,
     loadMoreCount,
     loadMoreCount,
     showAllProjects,
     showAllProjects,
     loadMoreProjects,
     loadMoreProjects,
@@ -14,7 +14,7 @@ export default function LoadMore() {
 
 
   return (
   return (
     <div className="text-centered">
     <div className="text-centered">
-      {hiddenProjects.length > 0 ? (
+      {hiddenProjectsCount > 0 ? (
         <Button
         <Button
           bsStyle="info"
           bsStyle="info"
           className="project-list-load-more-button"
           className="project-list-load-more-button"
@@ -25,12 +25,12 @@ export default function LoadMore() {
         </Button>
         </Button>
       ) : null}
       ) : null}
       <p>
       <p>
-        {hiddenProjects.length > 0 ? (
+        {hiddenProjectsCount > 0 ? (
           <>
           <>
             <span aria-live="polite">
             <span aria-live="polite">
               {t('showing_x_out_of_n_projects', {
               {t('showing_x_out_of_n_projects', {
                 x: visibleProjects.length,
                 x: visibleProjects.length,
-                n: visibleProjects.length + hiddenProjects.length,
+                n: visibleProjects.length + hiddenProjectsCount,
               })}
               })}
             </span>{' '}
             </span>{' '}
             <button
             <button

+ 22 - 28
services/web/frontend/js/features/project-list/context/project-list-context.tsx

@@ -87,7 +87,7 @@ type ProjectListContextValue = {
   searchText: string
   searchText: string
   setSearchText: React.Dispatch<React.SetStateAction<string>>
   setSearchText: React.Dispatch<React.SetStateAction<string>>
   selectedProjects: Project[]
   selectedProjects: Project[]
-  hiddenProjects: Project[]
+  hiddenProjectsCount: number
   loadMoreCount: number
   loadMoreCount: number
   showAllProjects: () => void
   showAllProjects: () => void
   loadMoreProjects: () => void
   loadMoreProjects: () => void
@@ -104,7 +104,9 @@ type ProjectListProviderProps = {
 export function ProjectListProvider({ children }: ProjectListProviderProps) {
 export function ProjectListProvider({ children }: ProjectListProviderProps) {
   const [loadedProjects, setLoadedProjects] = useState<Project[]>([])
   const [loadedProjects, setLoadedProjects] = useState<Project[]>([])
   const [visibleProjects, setVisibleProjects] = useState<Project[]>([])
   const [visibleProjects, setVisibleProjects] = useState<Project[]>([])
-  const [hiddenProjects, setHiddenProjects] = useState<Project[]>([])
+  const [maxVisibleProjects, setMaxVisibleProjects] =
+    useState(MAX_PROJECT_PER_PAGE)
+  const [hiddenProjectsCount, setHiddenProjectsCount] = useState(0)
   const [loadMoreCount, setLoadMoreCount] = useState<number>(0)
   const [loadMoreCount, setLoadMoreCount] = useState<number>(0)
   const [loadProgress, setLoadProgress] = useState(20)
   const [loadProgress, setLoadProgress] = useState(20)
   const [totalProjectsCount, setTotalProjectsCount] = useState<number>(0)
   const [totalProjectsCount, setTotalProjectsCount] = useState<number>(0)
@@ -183,29 +185,31 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
       setLoadedProjects(loadedProjectsSorted)
       setLoadedProjects(loadedProjectsSorted)
     }
     }
 
 
-    if (filteredProjects.length > MAX_PROJECT_PER_PAGE) {
+    if (filteredProjects.length > maxVisibleProjects) {
       const visibleFilteredProjects = filteredProjects.slice(
       const visibleFilteredProjects = filteredProjects.slice(
         0,
         0,
-        MAX_PROJECT_PER_PAGE
+        maxVisibleProjects
       )
       )
-      setVisibleProjects(visibleFilteredProjects)
 
 
-      const hiddenFilteredProjects =
-        filteredProjects.slice(MAX_PROJECT_PER_PAGE)
-      setHiddenProjects(hiddenFilteredProjects)
+      const hiddenFilteredProjectsCount =
+        filteredProjects.slice(maxVisibleProjects).length
+
+      setVisibleProjects(visibleFilteredProjects)
+      setHiddenProjectsCount(hiddenFilteredProjectsCount)
 
 
-      if (hiddenFilteredProjects.length > MAX_PROJECT_PER_PAGE) {
+      if (hiddenFilteredProjectsCount > MAX_PROJECT_PER_PAGE) {
         setLoadMoreCount(MAX_PROJECT_PER_PAGE)
         setLoadMoreCount(MAX_PROJECT_PER_PAGE)
       } else {
       } else {
-        setLoadMoreCount(hiddenFilteredProjects.length)
+        setLoadMoreCount(hiddenFilteredProjectsCount)
       }
       }
     } else {
     } else {
-      setHiddenProjects([])
       setVisibleProjects(filteredProjects)
       setVisibleProjects(filteredProjects)
       setLoadMoreCount(0)
       setLoadMoreCount(0)
+      setHiddenProjectsCount(0)
     }
     }
   }, [
   }, [
     loadedProjects,
     loadedProjects,
+    maxVisibleProjects,
     tags,
     tags,
     filter,
     filter,
     setFilter,
     setFilter,
@@ -221,23 +225,13 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
 
 
   const showAllProjects = useCallback(() => {
   const showAllProjects = useCallback(() => {
     setLoadMoreCount(0)
     setLoadMoreCount(0)
-    setVisibleProjects([...visibleProjects, ...hiddenProjects])
-    setHiddenProjects([])
-  }, [hiddenProjects, visibleProjects])
+    setHiddenProjectsCount(0)
+    setMaxVisibleProjects(maxVisibleProjects + hiddenProjectsCount)
+  }, [hiddenProjectsCount, maxVisibleProjects])
 
 
   const loadMoreProjects = useCallback(() => {
   const loadMoreProjects = useCallback(() => {
-    const newVisibleProjects = [
-      ...visibleProjects,
-      ...hiddenProjects.slice(0, loadMoreCount),
-    ]
-    const newHiddenProjects = hiddenProjects.slice(loadMoreCount)
-
-    setVisibleProjects(newVisibleProjects)
-    setHiddenProjects(newHiddenProjects)
-    if (newHiddenProjects.length < MAX_PROJECT_PER_PAGE) {
-      setLoadMoreCount(newHiddenProjects.length)
-    }
-  }, [visibleProjects, hiddenProjects, loadMoreCount])
+    setMaxVisibleProjects(maxVisibleProjects + loadMoreCount)
+  }, [maxVisibleProjects, loadMoreCount])
 
 
   const selectedProjects = useMemo(() => {
   const selectedProjects = useMemo(() => {
     return visibleProjects.filter(project => project.selected)
     return visibleProjects.filter(project => project.selected)
@@ -378,7 +372,7 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
       deleteTag,
       deleteTag,
       error,
       error,
       filter,
       filter,
-      hiddenProjects,
+      hiddenProjectsCount,
       isLoading,
       isLoading,
       loadMoreCount,
       loadMoreCount,
       loadMoreProjects,
       loadMoreProjects,
@@ -408,7 +402,7 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
       deleteTag,
       deleteTag,
       error,
       error,
       filter,
       filter,
-      hiddenProjects,
+      hiddenProjectsCount,
       isLoading,
       isLoading,
       loadMoreCount,
       loadMoreCount,
       loadMoreProjects,
       loadMoreProjects,

+ 34 - 74
services/web/test/frontend/features/project-list/components/load-more.test.tsx

@@ -2,8 +2,11 @@ import { fireEvent, screen, waitFor } from '@testing-library/dom'
 import { expect } from 'chai'
 import { expect } from 'chai'
 import fetchMock from 'fetch-mock'
 import fetchMock from 'fetch-mock'
 import LoadMore from '../../../../../frontend/js/features/project-list/components/load-more'
 import LoadMore from '../../../../../frontend/js/features/project-list/components/load-more'
-import { Project } from '../../../../../types/project/dashboard/api'
-import { projectsData } from '../fixtures/projects-data'
+import {
+  projectsData,
+  makeLongProjectList,
+  currentProjects,
+} from '../fixtures/projects-data'
 import { renderWithProjectListContext } from '../helpers/render-with-context'
 import { renderWithProjectListContext } from '../helpers/render-with-context'
 
 
 describe('<LoadMore />', function () {
 describe('<LoadMore />', function () {
@@ -12,26 +15,17 @@ describe('<LoadMore />', function () {
   })
   })
 
 
   it('renders on a project list longer than 40', async function () {
   it('renders on a project list longer than 40', async function () {
-    // archived and trashed projects are currently not shown
-    const filteredProjects = projectsData.filter(
-      ({ archived, trashed }) => !archived && !trashed
-    )
-    let longProjectsData: Project[] = filteredProjects
-    const MULTIPLY_FACTOR = 10
-    // longProjectsData.length = 55
-    for (let i = 0; i < MULTIPLY_FACTOR; i++) {
-      longProjectsData = [...longProjectsData, ...filteredProjects]
-    }
-
-    renderWithProjectListContext(<LoadMore />, { projects: longProjectsData })
+    const { fullList, currentList } = makeLongProjectList(55)
+
+    renderWithProjectListContext(<LoadMore />, {
+      projects: fullList,
+    })
 
 
     await screen.findByRole('button', {
     await screen.findByRole('button', {
       name: /Show 20 more projects/i,
       name: /Show 20 more projects/i,
     })
     })
 
 
-    await screen.findByText(
-      `Showing 20 out of ${longProjectsData.length} projects.`
-    )
+    await screen.findByText(`Showing 20 out of ${currentList.length} projects.`)
 
 
     await screen.findByRole('button', {
     await screen.findByRole('button', {
       name: /Show all projects/i,
       name: /Show all projects/i,
@@ -39,29 +33,15 @@ describe('<LoadMore />', function () {
   })
   })
 
 
   it('renders on a project list longer than 20 and shorter than 40', async function () {
   it('renders on a project list longer than 20 and shorter than 40', async function () {
-    // archived and trashed projects are currently not shown
-    const filteredProjects = projectsData.filter(
-      ({ archived, trashed }) => !archived && !trashed
-    )
-    let longProjectsData: Project[] = filteredProjects
-    const MULTIPLY_FACTOR = 5
-    // longProjectsData.length = 30
-    for (let i = 0; i < MULTIPLY_FACTOR; i++) {
-      longProjectsData = [...longProjectsData, ...filteredProjects]
-    }
-
-    renderWithProjectListContext(<LoadMore />, { projects: longProjectsData })
+    const { fullList, currentList } = makeLongProjectList(30)
+
+    renderWithProjectListContext(<LoadMore />, { projects: fullList })
 
 
     await screen.findByRole('button', {
     await screen.findByRole('button', {
-      name: new RegExp(
-        `Show ${longProjectsData.length - 20} more projects`,
-        'i'
-      ),
+      name: new RegExp(`Show ${currentList.length - 20} more projects`, 'i'),
     })
     })
 
 
-    await screen.findByText(
-      `Showing 20 out of ${longProjectsData.length} projects.`
-    )
+    await screen.findByText(`Showing 20 out of ${currentList.length} projects.`)
 
 
     await screen.findByRole('button', {
     await screen.findByRole('button', {
       name: /Show all projects/i,
       name: /Show all projects/i,
@@ -69,72 +49,52 @@ describe('<LoadMore />', function () {
   })
   })
 
 
   it('renders on a project list shorter than 20', async function () {
   it('renders on a project list shorter than 20', async function () {
-    // archived and trashed projects are currently not shown
-    // filteredProjects.length = 5
-    const filteredProjects = projectsData.filter(
-      ({ archived, trashed }) => !archived && !trashed
-    )
-
-    renderWithProjectListContext(<LoadMore />, { projects: filteredProjects })
+    renderWithProjectListContext(<LoadMore />, { projects: projectsData })
 
 
     await waitFor(() => {
     await waitFor(() => {
       expect(screen.queryByRole('button', { name: 'Show all' })).to.not.exist
       expect(screen.queryByRole('button', { name: 'Show all' })).to.not.exist
       screen.getByText(
       screen.getByText(
-        `Showing ${filteredProjects.length} out of ${filteredProjects.length} projects.`
+        `Showing ${currentProjects.length} out of ${currentProjects.length} projects.`
       )
       )
     })
     })
   })
   })
 
 
   it('change text when pressing the "Show 20 more" once for project list longer than 40', async function () {
   it('change text when pressing the "Show 20 more" once for project list longer than 40', async function () {
-    // archived and trashed projects are currently not shown
-    const filteredProjects = projectsData.filter(
-      ({ archived, trashed }) => !archived && !trashed
-    )
-    let longProjectsData: Project[] = filteredProjects
-    const MULTIPLY_FACTOR = 10
-    // longProjectsData.length = 55
-    for (let i = 0; i < MULTIPLY_FACTOR; i++) {
-      longProjectsData = [...longProjectsData, ...filteredProjects]
-    }
-
-    renderWithProjectListContext(<LoadMore />, { projects: longProjectsData })
+    const { fullList, currentList } = makeLongProjectList(55)
+
+    renderWithProjectListContext(<LoadMore />, { projects: fullList })
 
 
     await waitFor(() => {
     await waitFor(() => {
       const showMoreBtn = screen.getByRole('button', {
       const showMoreBtn = screen.getByRole('button', {
         name: /Show 20 more projects/i,
         name: /Show 20 more projects/i,
       })
       })
       fireEvent.click(showMoreBtn)
       fireEvent.click(showMoreBtn)
+    })
 
 
-      screen.getByRole('button', {
-        name: /Show 15 more/i,
-      })
-      screen.getByText(`Showing 40 out of ${longProjectsData.length} projects.`)
+    await waitFor(() => {
+      screen.getByLabelText(
+        `Show ${currentList.length - 20 - 20} more projects`
+      )
+      screen.getByText(`Showing 40 out of ${currentList.length} projects.`)
     })
     })
   })
   })
 
 
   it('change text when pressing the "Show 20 more" once for project list longer than 20 and shorter than 40', async function () {
   it('change text when pressing the "Show 20 more" once for project list longer than 20 and shorter than 40', async function () {
-    // archived and trashed projects are currently not shown
-    const filteredProjects = projectsData.filter(
-      ({ archived, trashed }) => !archived && !trashed
-    )
-    let longProjectsData: Project[] = filteredProjects
-    const MULTIPLY_FACTOR = 5
-    // longProjectsData.length = 30
-    for (let i = 0; i < MULTIPLY_FACTOR; i++) {
-      longProjectsData = [...longProjectsData, ...filteredProjects]
-    }
-
-    renderWithProjectListContext(<LoadMore />, { projects: longProjectsData })
+    const { fullList, currentList } = makeLongProjectList(30)
+
+    renderWithProjectListContext(<LoadMore />, { projects: fullList })
 
 
     await waitFor(() => {
     await waitFor(() => {
       const showMoreBtn = screen.getByRole('button', {
       const showMoreBtn = screen.getByRole('button', {
-        name: /Show 10 more projects/i,
+        name: /Show 7 more projects/i,
       })
       })
       fireEvent.click(showMoreBtn)
       fireEvent.click(showMoreBtn)
+    })
 
 
+    await waitFor(() => {
       expect(screen.queryByRole('button', { name: /Show/ })).to.not.exist
       expect(screen.queryByRole('button', { name: /Show/ })).to.not.exist
       screen.getByText(
       screen.getByText(
-        `Showing ${longProjectsData.length} out of ${longProjectsData.length} projects.`
+        `Showing ${currentList.length} out of ${currentList.length} projects.`
       )
       )
     })
     })
   })
   })

+ 44 - 10
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

@@ -4,11 +4,8 @@ import fetchMock from 'fetch-mock'
 import sinon from 'sinon'
 import sinon from 'sinon'
 import ProjectListRoot from '../../../../../frontend/js/features/project-list/components/project-list-root'
 import ProjectListRoot from '../../../../../frontend/js/features/project-list/components/project-list-root'
 import { renderWithProjectListContext } from '../helpers/render-with-context'
 import { renderWithProjectListContext } from '../helpers/render-with-context'
-import {
-  currentProjects,
-  owner,
-  trashedProjects,
-} from '../fixtures/projects-data'
+import { owner, makeLongProjectList } from '../fixtures/projects-data'
+const { fullList, currentList, trashedList } = makeLongProjectList(40)
 
 
 const userId = owner.id
 const userId = owner.id
 
 
@@ -26,7 +23,9 @@ describe('<ProjectListRoot />', function () {
       value: { assign: locationStub },
       value: { assign: locationStub },
     })
     })
 
 
-    renderWithProjectListContext(<ProjectListRoot />)
+    renderWithProjectListContext(<ProjectListRoot />, {
+      projects: fullList,
+    })
     await fetchMock.flush(true)
     await fetchMock.flush(true)
     await waitFor(() => {
     await waitFor(() => {
       screen.findByRole('table')
       screen.findByRole('table')
@@ -145,6 +144,41 @@ describe('<ProjectListRoot />', function () {
         expect(projectRequest2Url).to.equal(`/project/${project2Id}/trash`)
         expect(projectRequest2Url).to.equal(`/project/${project2Id}/trash`)
         expect(projectRequest2Headers?.method).to.equal('POST')
         expect(projectRequest2Headers?.method).to.equal('POST')
       })
       })
+
+      it('only checks the projects that are viewable when there is a load more button', async function () {
+        // first one is the select all checkbox
+        fireEvent.click(allCheckboxes[0])
+
+        allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
+        let checked = allCheckboxes.filter(c => c.checked)
+        expect(checked.length).to.equal(21) // max projects viewable by default is 20, and plus one for check all
+
+        const loadMoreButton = screen.getByLabelText('Show 17 more projects')
+        fireEvent.click(loadMoreButton)
+
+        allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
+        expect(allCheckboxes.length).to.equal(currentList.length + 1)
+        checked = allCheckboxes.filter(c => c.checked)
+        expect(checked.length).to.equal(20) // remains same even after showing more
+      })
+
+      it('maintains viewable and selected projects after loading more and then selecting all', async function () {
+        const loadMoreButton = screen.getByLabelText('Show 17 more projects')
+        fireEvent.click(loadMoreButton)
+        // verify button gone
+        screen.getByText(
+          `Showing ${currentList.length} out of ${currentList.length} projects.`
+        )
+        // first one is the select all checkbox
+        fireEvent.click(allCheckboxes[0])
+        // verify button still gone
+        screen.getByText(
+          `Showing ${currentList.length} out of ${currentList.length} projects.`
+        )
+
+        allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
+        expect(allCheckboxes.length).to.equal(currentList.length + 1)
+      })
     })
     })
 
 
     describe('archived projects', function () {
     describe('archived projects', function () {
@@ -170,14 +204,14 @@ describe('<ProjectListRoot />', function () {
 
 
         allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
         allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
         // + 1 because of select all
         // + 1 because of select all
-        expect(allCheckboxes.length).to.equal(trashedProjects.length + 1)
+        expect(allCheckboxes.length).to.equal(trashedList.length + 1)
 
 
         // first one is the select all checkbox
         // first one is the select all checkbox
         fireEvent.click(allCheckboxes[0])
         fireEvent.click(allCheckboxes[0])
 
 
         const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
         const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
         // + 1 because of select all
         // + 1 because of select all
-        expect(allCheckboxesChecked.length).to.equal(trashedProjects.length + 1)
+        expect(allCheckboxesChecked.length).to.equal(trashedList.length + 1)
 
 
         actionsToolbar = screen.getAllByRole('toolbar')[0]
         actionsToolbar = screen.getAllByRole('toolbar')[0]
       })
       })
@@ -218,7 +252,7 @@ describe('<ProjectListRoot />', function () {
         fireEvent.click(allCheckboxes[1])
         fireEvent.click(allCheckboxes[1])
 
 
         const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
         const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
-        expect(allCheckboxesChecked.length).to.equal(trashedProjects.length - 1)
+        expect(allCheckboxesChecked.length).to.equal(trashedList.length - 1)
 
 
         await fetchMock.flush(true)
         await fetchMock.flush(true)
         expect(fetchMock.done()).to.be.true
         expect(fetchMock.done()).to.be.true
@@ -257,7 +291,7 @@ describe('<ProjectListRoot />', function () {
       const input = screen.getAllByRole('textbox', {
       const input = screen.getAllByRole('textbox', {
         name: /search projects/i,
         name: /search projects/i,
       })[0]
       })[0]
-      const value = currentProjects[0].name
+      const value = currentList[0].name
 
 
       fireEvent.change(input, { target: { value } })
       fireEvent.change(input, { target: { value } })
 
 

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

@@ -139,3 +139,22 @@ export const currentProjects: Array<Project> = projectsData.filter(
 export const trashedProjects: Array<Project> = projectsData.filter(
 export const trashedProjects: Array<Project> = projectsData.filter(
   ({ trashed }) => trashed
   ({ trashed }) => trashed
 )
 )
+
+export const makeLongProjectList = (listLength: number) => {
+  const longList = [...projectsData]
+  while (longList.length < listLength) {
+    longList.push(
+      Object.assign({}, copyableProject, {
+        id: `newProjectId${longList.length}`,
+      })
+    )
+  }
+
+  return {
+    fullList: longList,
+    currentList: longList.filter(
+      ({ archived, trashed }) => !archived && !trashed
+    ),
+    trashedList: longList.filter(({ trashed }) => trashed),
+  }
+}