From 2b2140c57bf65d6fcf948fd0aa8300e7aa5ec835 Mon Sep 17 00:00:00 2001 From: Flaminia Cavallo Date: Tue, 19 Nov 2024 16:12:23 +0100 Subject: [PATCH] fix: fix load more org unit row errors --- i18n/en.pot | 9 +- src/pages/organisationUnits/List.spec.tsx | 167 +++++++++++++++--- .../list/OrganisationUnitRow.tsx | 1 + .../list/useOrganisationUnits.tsx | 67 ++++--- 4 files changed, 187 insertions(+), 57 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 76676005..6d937319 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,10 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-11-13T12:57:12.003Z\n" -"PO-Revision-Date: 2024-11-13T12:57:12.004Z\n" -"POT-Creation-Date: 2024-11-13T08:29:01.348Z\n" -"PO-Revision-Date: 2024-11-13T08:29:01.348Z\n" +"POT-Creation-Date: 2024-11-19T08:16:32.043Z\n" +"PO-Revision-Date: 2024-11-19T08:16:32.043Z\n" msgid "schemas" msgstr "schemas" @@ -1042,6 +1040,9 @@ msgstr "" "Choose when, and for which organisation units this category option will be " "available." +msgid "Form name should not exceed 230 characters" +msgstr "Form name should not exceed 230 characters" + msgid "End date should be after start date" msgstr "End date should be after start date" diff --git a/src/pages/organisationUnits/List.spec.tsx b/src/pages/organisationUnits/List.spec.tsx index 3581086b..71c74f01 100644 --- a/src/pages/organisationUnits/List.spec.tsx +++ b/src/pages/organisationUnits/List.spec.tsx @@ -32,6 +32,7 @@ const renderList = async ({ rootOrgUnits = [testOrgUnit()] as Partial[], otherOrgUnits = [] as Partial[], userDataStore = defaultUserDataStoreData, + customData = undefined, }) => { const routeOptions = { handle: { section: SECTIONS_MAP.organisationUnit }, @@ -54,31 +55,33 @@ const renderList = async ({ const result = render( { - if (type === 'read' && params.id !== undefined) { - const foundOrgUnit = organisationUnits.find( - (ou) => ou.id === params.id - ) - return foundOrgUnit - } - if (type === 'read') { - const regex = /:(\w+)$/ - const orgUnitFilter = - params.params.filter[0].match(regex)[1] - const filteredOrgUnits = organisationUnits.filter( - (ou) => - ou.id === orgUnitFilter || - ou.parent?.id === orgUnitFilter - ) - return { organisationUnits: filteredOrgUnits } - } - if (type === 'delete') { - deleteOrgUnitMock(params) - } - }, - userDataStore, - }} + customData={ + customData || { + organisationUnits: (type: any, params: any) => { + if (type === 'read' && params.id !== undefined) { + const foundOrgUnit = organisationUnits.find( + (ou) => ou.id === params.id + ) + return foundOrgUnit + } + if (type === 'read') { + const regex = /:(\w+)$/ + const orgUnitFilter = + params.params.filter[0].match(regex)[1] + const filteredOrgUnits = organisationUnits.filter( + (ou) => + ou.id === orgUnitFilter || + ou.parent?.id === orgUnitFilter + ) + return { organisationUnits: filteredOrgUnits } + } + if (type === 'delete') { + deleteOrgUnitMock(params) + } + }, + userDataStore, + } + } routeOptions={routeOptions} > @@ -312,6 +315,120 @@ describe('Org Unit List', () => { ) }) + it('should load more rows on load more', async () => { + const rootOrg1 = testOrgUnit({ level: 1, childCount: 3 }) + const root1Level2Child1 = testOrgUnit({ + level: 2, + ancestors: [rootOrg1], + parentId: rootOrg1.id, + childCount: 0, + }) + const root1Level2Child2 = testOrgUnit({ + level: 2, + ancestors: [rootOrg1], + parentId: rootOrg1.id, + childCount: 0, + }) + const root1Level2Child3 = testOrgUnit({ + level: 2, + ancestors: [rootOrg1], + parentId: rootOrg1.id, + childCount: 0, + }) + + const customData = { + organisationUnits: (type: any, params: any) => { + if (type === 'read' && params.params.page === 1) { + return { organisationUnits: [rootOrg1, root1Level2Child1] } + } + if (type === 'read' && params.params.page === 2) { + return { + organisationUnits: [ + rootOrg1, + root1Level2Child2, + root1Level2Child3, + ], + } + } + }, + userDataStore: defaultUserDataStoreData, + } + + const screen = await renderList({ + rootOrgUnits: [rootOrg1], + customData, + }) + + const tableRows = screen.getAllByTestId('dhis2-uicore-datatablerow') + expect(tableRows.length).toBe(4) + expect(tableRows[3]).toHaveTextContent( + `Load more for ${rootOrg1.displayName}` + ) + fireEvent.click(within(tableRows[3]).getByTestId('load-more')) + let tableRowsRefreshed: any = [] + await waitFor(() => { + tableRowsRefreshed = screen.getAllByTestId( + 'dhis2-uicore-datatablerow' + ) + expect(tableRowsRefreshed.length).toBe(5) + }) + }) + + it('should load more the right rows when first load more fails', async () => { + const rootOrg1 = testOrgUnit({ level: 1, childCount: 3 }) + const root1Level2Child1 = testOrgUnit({ + level: 2, + ancestors: [rootOrg1], + parentId: rootOrg1.id, + childCount: 0, + }) + const root1Level2Child2 = testOrgUnit({ + level: 2, + ancestors: [rootOrg1], + parentId: rootOrg1.id, + childCount: 0, + }) + const root1Level2Child3 = testOrgUnit({ + level: 2, + ancestors: [rootOrg1], + parentId: rootOrg1.id, + childCount: 0, + }) + const loadPageThreeMock = jest.fn() + + const customData = { + organisationUnits: (type: any, params: any) => { + if (type === 'read' && params.params.page === 1) { + return { organisationUnits: [rootOrg1, root1Level2Child1] } + } + if (type === 'read' && params.params.page === 2) { + throw 'ERROR' + } + if (type === 'read' && params.params.page === 3) { + loadPageThreeMock() + } + }, + userDataStore: defaultUserDataStoreData, + } + + const screen = await renderList({ + rootOrgUnits: [rootOrg1], + customData, + }) + + const tableRows = screen.getAllByTestId('dhis2-uicore-datatablerow') + expect(tableRows.length).toBe(4) + expect(tableRows[3]).toHaveTextContent( + `Load more for ${rootOrg1.displayName}` + ) + fireEvent.click(within(tableRows[3]).getByTestId('load-more')) + await waitFor(() => { + expect(screen.getByTestId('dhis2-uicore-noticebox')).toBeVisible() + }) + fireEvent.click(within(tableRows[3]).getByTestId('load-more')) + expect(loadPageThreeMock).not.toHaveBeenCalled() + }) + it('should show a clickable delete action when org unit can be deleted', async () => { const rootOrg = testOrgUnit({ level: 1, diff --git a/src/pages/organisationUnits/list/OrganisationUnitRow.tsx b/src/pages/organisationUnits/list/OrganisationUnitRow.tsx index 49b740bc..12cf6cd8 100644 --- a/src/pages/organisationUnits/list/OrganisationUnitRow.tsx +++ b/src/pages/organisationUnits/list/OrganisationUnitRow.tsx @@ -124,6 +124,7 @@ export const OrganisationUnitRow = ({ colSpan="100" style={{ textAlign: 'center' }} onClick={() => fetchNextPage(parentRow.original.id)} + dataTest="load-more" > {i18n.t('Load more for {{orgUnitName}}', { orgUnitName: parentRow.original.displayName, diff --git a/src/pages/organisationUnits/list/useOrganisationUnits.tsx b/src/pages/organisationUnits/list/useOrganisationUnits.tsx index b1f1faf6..ce704730 100644 --- a/src/pages/organisationUnits/list/useOrganisationUnits.tsx +++ b/src/pages/organisationUnits/list/useOrganisationUnits.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useQuery, useQueries } from 'react-query' import { useBoundResourceQueryFn } from '../../../lib/query/useBoundQueryFn' import { OrganisationUnit, PagedResponse } from '../../../types/generated' @@ -83,7 +83,7 @@ export const usePaginatedChildrenOrgUnitsController = ( const [parentIdPages, setFetchPages] = useState( Object.fromEntries(Object.keys(parentIds).map((id) => [id, [1]])) ) - + const [hasErrored, setHasErrored] = useState(false) // this will create a query for each parent id and each page // eg if parentIds = ['a', 'b'] and fetchPages = {a: [1, 2], b: [1]} // then queries will be [['a', 1], ['a', 2], ['b', 1]] @@ -98,40 +98,51 @@ export const usePaginatedChildrenOrgUnitsController = ( (id: string) => { setFetchPages((prev) => { const pages = prev[id] || [1] + const nextPaged = hasErrored + ? [...pages] + : [...pages, pages[pages.length - 1] + 1] return { ...prev, - [id]: [...pages, pages[pages.length - 1] + 1], + [id]: nextPaged, } }) }, - [setFetchPages] + [setFetchPages, hasErrored] + ) + const queryObjects = useMemo( + () => + flatParentIdPages.map(([id, page]) => { + const resourceQuery = { + resource: 'organisationUnits', + params: { + fields: getOrgUnitFieldFilters(options.fieldFilters), + // `id:eq:id` is for an edge-case where a root-unit is a leaf-node + // and `parent.id`-filter would return empty results + filter: [`parent.id:eq:${id}`, `id:eq:${id}`], + rootJunction: 'OR', + order: 'displayName:asc', + page: page, + }, + } + const queryOptions = { + enabled: options.enabled, + queryKey: [resourceQuery], + queryFn: boundQueryFn, + staleTime: 60000, + cacheTime: 60000, + meta: { parent: id }, + } as const + return queryOptions + }), + [flatParentIdPages] ) - - const queryObjects = flatParentIdPages.map(([id, page]) => { - const resourceQuery = { - resource: 'organisationUnits', - params: { - fields: getOrgUnitFieldFilters(options.fieldFilters), - // `id:eq:id` is for an edge-case where a root-unit is a leaf-node - // and `parent.id`-filter would return empty results - filter: [`parent.id:eq:${id}`, `id:eq:${id}`], - rootJunction: 'OR', - order: 'displayName:asc', - page: page, - }, - } - const queryOptions = { - enabled: options.enabled, - queryKey: [resourceQuery], - queryFn: boundQueryFn, - staleTime: 60000, - cacheTime: 60000, - meta: { parent: id }, - } as const - return queryOptions - }) const queries = useQueries(queryObjects) + + useEffect(() => { + setHasErrored(queries.some((query) => query.isError)) + }, [queries.some((query) => query.isError)]) + return { queries, fetchNextPage,