Skip to content

Commit cb0b8fd

Browse files
authored
SW-7564 Convert Nusery Withdrawal History table to backend pagination (#4677)
Change Nursery Withdrawal History table to backend pagination using updated table component from `web-components`. Eventually will build a backend row model table in `terraware-web`, this is just a first round to ensure we have everything we need. Will update package.json to correct version number once terraware/web-components#707 is merged.
1 parent 97325f0 commit cb0b8fd

File tree

11 files changed

+176
-56
lines changed

11 files changed

+176
-56
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@mux/mux-player": "^3.6.1",
5757
"@mux/mux-player-react": "^3.6.1",
5858
"@reduxjs/toolkit": "^1.9.3",
59-
"@terraware/web-components": "^3.9.5",
59+
"@terraware/web-components": "^3.9.6",
6060
"@testing-library/jest-dom": "^6.0.0",
6161
"@testing-library/react": "^16.0.0",
6262
"@testing-library/user-event": "^14.4.3",

playwright/e2e/suites/inventory.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,17 +201,17 @@ export default function InventoryTests() {
201201
await page.getByText('Withdrawal DetailsSelect a').click();
202202
await expect(page.getByText('Withdraw Quantity150')).toBeVisible();
203203
await page.locator('textarea').click();
204-
await page.locator('textarea').fill('Transfering some banana to the other nursery');
204+
await page.locator('textarea').fill('Transferring some banana to the other nursery');
205205
await page.getByRole('button', { name: 'Next' }).click();
206206
await page.getByRole('button', { name: 'Withdraw', exact: true }).click();
207207
await expect(page.getByText('1 batch for a total of 150')).toBeVisible();
208208
await page.getByRole('button', { name: 'Withdrawals' }).click();
209209
await page.getByRole('tab', { name: 'Withdrawal History' }).click();
210-
await page.locator('#row1-withdrawnDate').click();
210+
await page.getByRole('row', { name: 'Nursery Transfer' }).locator('[id$="-withdrawnDate"]').click();
211211
await expect(page.getByText('Purpose Nursery Transfer')).toBeVisible();
212212
await expect(page.getByText('Quantity 150')).toBeVisible();
213213
await expect(page.getByText('Destination Nursery')).toBeVisible();
214-
await expect(page.getByText('Notes Transfering some banana')).toBeVisible();
214+
await expect(page.getByText('Notes Transferring some banana')).toBeVisible();
215215
await expect(page.getByRole('cell', { name: '-2-2-003' })).toBeVisible();
216216
await expect(page.getByRole('cell', { name: 'Banana' })).toBeVisible();
217217
await expect(page.locator('#row1-germinating')).toBeVisible();

src/api/types/generated-schema.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3075,6 +3075,26 @@ export interface paths {
30753075
patch?: never;
30763076
trace?: never;
30773077
};
3078+
"/api/v1/search/count": {
3079+
parameters: {
3080+
query?: never;
3081+
header?: never;
3082+
path?: never;
3083+
cookie?: never;
3084+
};
3085+
get?: never;
3086+
put?: never;
3087+
/**
3088+
* Get the total count of values matching a set of search criteria.
3089+
* @description Note that fields, sortOrder, cursor, and count in the payload are unused in the count query and thus can be included or left out.
3090+
*/
3091+
post: operations["searchCount"];
3092+
delete?: never;
3093+
options?: never;
3094+
head?: never;
3095+
patch?: never;
3096+
trace?: never;
3097+
};
30783098
"/api/v1/search/values": {
30793099
parameters: {
30803100
query?: never;
@@ -9533,6 +9553,10 @@ export interface components {
95339553
*/
95349554
value?: number;
95359555
};
9556+
SearchCountResponsePayload: {
9557+
/** Format: int64 */
9558+
count: number;
9559+
};
95369560
/** @description A search criterion. The search will return results that match this criterion. The criterion can be composed of other search criteria to form arbitrary Boolean search expressions. TYPESCRIPT-OVERRIDE-TYPE-WITH-ANY */
95379561
SearchNodePayload: {
95389562
operation: "and" | "field" | "not" | "or";
@@ -17748,6 +17772,30 @@ export interface operations {
1774817772
};
1774917773
};
1775017774
};
17775+
searchCount: {
17776+
parameters: {
17777+
query?: never;
17778+
header?: never;
17779+
path?: never;
17780+
cookie?: never;
17781+
};
17782+
requestBody: {
17783+
content: {
17784+
"application/json": components["schemas"]["SearchRequestPayload"];
17785+
};
17786+
};
17787+
responses: {
17788+
/** @description OK */
17789+
200: {
17790+
headers: {
17791+
[name: string]: unknown;
17792+
};
17793+
content: {
17794+
"application/json": components["schemas"]["SearchCountResponsePayload"];
17795+
};
17796+
};
17797+
};
17798+
};
1775117799
searchDistinctValues: {
1775217800
parameters: {
1775317801
query?: never;

src/scenes/NurseryRouter/NurseryWithdrawalsTabContent.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22

33
import { Typography, useTheme } from '@mui/material';
4-
import { IconTooltip } from '@terraware/web-components';
54

65
import Card from 'src/components/common/Card';
76
import strings from 'src/strings';
@@ -22,7 +21,6 @@ export default function NurseryWithdrawalsTabContent(): JSX.Element {
2221
}}
2322
>
2423
{strings.WITHDRAWAL_HISTORY}
25-
<IconTooltip placement='top' title={strings.WITHDRAWAL_HISTORY_ONLY_1000} />
2624
</Typography>
2725
<NurseryWithdrawalsTable />
2826
</Card>

src/scenes/NurseryRouter/NurseryWithdrawalsTable.tsx

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ import useDebounce from 'src/utils/useDebounce';
3838
import useQuery from 'src/utils/useQuery';
3939
import useStateLocation, { getLocation } from 'src/utils/useStateLocation';
4040

41+
const ITEMS_PER_PAGE = 100;
42+
43+
const DEFAULT_SORT_ORDER: SearchSortOrder = {
44+
field: 'withdrawnDate',
45+
direction: 'Descending',
46+
};
47+
4148
export default function NurseryWithdrawalsTable(): JSX.Element {
4249
const { selectedOrganization } = useOrganization();
4350
const { strings } = useLocalization();
@@ -52,11 +59,10 @@ export default function NurseryWithdrawalsTable(): JSX.Element {
5259
const [filters, setFilters] = useState<Record<string, SearchNodePayload>>({});
5360
const [rows, setRows] = useState<SearchResponseElement[] | null>();
5461
const [searchValue, setSearchValue] = useState('');
62+
const [currentPage, setCurrentPage] = useState<number>();
63+
const [totalRowCount, setTotalRowCount] = useState<number>();
5564
const debouncedSearchTerm = useDebounce(searchValue, DEFAULT_SEARCH_DEBOUNCE_MS);
56-
const [searchSortOrder, setSearchSortOrder] = useState<SearchSortOrder | undefined>({
57-
field: 'withdrawnDate',
58-
direction: 'Descending',
59-
} as SearchSortOrder);
65+
const [searchSortOrder, setSearchSortOrder] = useState<SearchSortOrder>(DEFAULT_SORT_ORDER);
6066
const [filterOptions, setFilterOptions] = useState<FieldOptionsMap>({});
6167

6268
const getProjectName = useCallback(
@@ -266,16 +272,33 @@ export default function NurseryWithdrawalsTable(): JSX.Element {
266272
return finalSearchValueChildren;
267273
}, [filters, debouncedSearchTerm]);
268274

269-
const retrieveWithdrawals: (limit: number) => Promise<PlantingProgress[]> = useCallback(
270-
async (limit: number) => {
275+
const retrieveTotalRowCount = useCallback(
276+
async (orgId: number) => {
277+
const count = await NurseryWithdrawalService.countNurseryWithdrawals(orgId, searchChildren);
278+
if (count) {
279+
setTotalRowCount(count);
280+
}
281+
},
282+
[searchChildren]
283+
);
284+
285+
useEffect(() => {
286+
if (selectedOrganization) {
287+
void retrieveTotalRowCount(selectedOrganization.id);
288+
}
289+
}, [retrieveTotalRowCount, selectedOrganization]);
290+
291+
const retrieveWithdrawals: (limit: number, pageNumber: number) => Promise<PlantingProgress[]> = useCallback(
292+
async (limit: number, pageNumber: number) => {
271293
if (selectedOrganization) {
272294
const requestId = Math.random().toString();
273295
setRequestId('searchWithdrawals', requestId);
274296
const apiSearchResults = await NurseryWithdrawalService.listNurseryWithdrawals(
275297
selectedOrganization.id,
276298
searchChildren,
277299
searchSortOrder,
278-
limit
300+
limit,
301+
(pageNumber - 1) * ITEMS_PER_PAGE
279302
);
280303
if (apiSearchResults) {
281304
if (getRequestId('searchWithdrawals') === requestId) {
@@ -295,14 +318,26 @@ export default function NurseryWithdrawalsTable(): JSX.Element {
295318
[filters.destinationName?.values, searchChildren, searchSortOrder, selectedOrganization]
296319
);
297320

298-
const onApplyFilters = useCallback(async () => {
299-
setRows(await retrieveWithdrawals(1000));
300-
}, [retrieveWithdrawals]);
321+
const onApplyFilters = useCallback(
322+
async (pageNumber?: number) => {
323+
const newPageNumber = pageNumber || 1;
324+
if (!pageNumber) {
325+
setCurrentPage(newPageNumber);
326+
}
327+
const newRows = await retrieveWithdrawals(ITEMS_PER_PAGE, newPageNumber);
328+
setRows(newRows);
329+
},
330+
[retrieveWithdrawals]
331+
);
301332

302333
const reload = useCallback(() => {
303334
void onApplyFilters();
304335
}, [onApplyFilters]);
305336

337+
useEffect(() => {
338+
reload();
339+
}, [searchChildren, reload]);
340+
306341
useEffect(() => {
307342
if (siteParam) {
308343
query.delete('siteName');
@@ -335,29 +370,22 @@ export default function NurseryWithdrawalsTable(): JSX.Element {
335370
}
336371
}, [subzoneParam, query, navigate, location]);
337372

338-
useEffect(() => {
339-
void onApplyFilters();
340-
}, [filters, onApplyFilters]);
341-
342-
const onSortChange = useCallback(
343-
(order: SortOrder, orderBy: string) => {
344-
const orderByStr =
345-
orderBy === 'speciesScientificNames'
346-
? 'batchWithdrawals.batch_species_scientificName'
347-
: orderBy === 'project_names'
348-
? null
349-
: orderBy;
350-
setSearchSortOrder(
351-
orderByStr
352-
? {
353-
field: orderByStr,
354-
direction: order === 'asc' ? 'Ascending' : 'Descending',
355-
}
356-
: undefined
357-
);
358-
},
359-
[setSearchSortOrder]
360-
);
373+
const onSortChange = useCallback((order: SortOrder, orderBy: string) => {
374+
const orderByStr =
375+
orderBy === 'speciesScientificNames'
376+
? 'batchWithdrawals.batch_species_scientificName'
377+
: orderBy === 'project_names'
378+
? 'batchWithdrawals.batch_project_name'
379+
: orderBy;
380+
setSearchSortOrder(
381+
orderByStr
382+
? {
383+
field: orderByStr,
384+
direction: order === 'asc' ? 'Ascending' : 'Descending',
385+
}
386+
: DEFAULT_SORT_ORDER
387+
);
388+
}, []);
361389

362390
const isClickable = useCallback(() => false, []);
363391

@@ -369,7 +397,7 @@ export default function NurseryWithdrawalsTable(): JSX.Element {
369397
return {
370398
filename: `${nurseryName}-${strings.NURSERY_WITHDRAWALS}`,
371399
columnHeaders: exportColumnHeaders,
372-
retrieveResults: () => retrieveWithdrawals(0),
400+
retrieveResults: () => retrieveWithdrawals(0, 1),
373401
convertRow: (withdrawal: SearchResponseElement) =>
374402
({
375403
...withdrawal,
@@ -381,6 +409,14 @@ export default function NurseryWithdrawalsTable(): JSX.Element {
381409
};
382410
}, [exportColumnHeaders, retrieveWithdrawals, rows, strings.NURSERY_WITHDRAWALS, strings.UNKNOWN]);
383411

412+
const onPageChange = useCallback(
413+
(newPage: number) => {
414+
setCurrentPage(newPage);
415+
void onApplyFilters(newPage);
416+
},
417+
[onApplyFilters]
418+
);
419+
384420
return (
385421
<Grid container>
386422
<Grid item xs={12} sx={{ display: 'flex', marginBottom: '16px', alignItems: 'center' }}>
@@ -399,16 +435,20 @@ export default function NurseryWithdrawalsTable(): JSX.Element {
399435
columns={columns}
400436
rows={rows || []}
401437
Renderer={WithdrawalLogRenderer}
402-
orderBy={searchSortOrder?.field || 'project_names'}
403-
order={searchSortOrder?.direction === 'Ascending' ? 'asc' : 'desc'}
404-
isPresorted={
405-
searchSortOrder !== undefined && searchSortOrder.field !== 'batchWithdrawals.batch_species_scientificName'
438+
orderBy={
439+
searchSortOrder.field === 'batchWithdrawals.batch_project_name' ? 'project_names' : searchSortOrder.field
406440
}
441+
order={searchSortOrder.direction === 'Ascending' ? 'asc' : 'desc'}
442+
isPresorted={true}
407443
onSelect={onWithdrawalClicked}
408444
controlledOnSelect={true}
409445
sortHandler={onSortChange}
410446
isClickable={isClickable}
411447
reloadData={reload}
448+
onPageChange={onPageChange}
449+
totalRowCount={totalRowCount}
450+
maxItemsPerPage={ITEMS_PER_PAGE}
451+
currentPage={currentPage}
412452
/>
413453
</Grid>
414454
</Grid>

src/services/NurseryWithdrawalService.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,21 @@ export type NurseryWithdrawalsSearchResponseElement = {
9797
}[];
9898
};
9999

100+
const NURSERY_WITHDRAWALS_PREFIX = 'nurseryWithdrawals';
101+
100102
/**
101103
* List nursery withdrawals
102104
*/
103105
const listNurseryWithdrawals = async (
104106
organizationId: number,
105107
searchCriteria: SearchNodePayload[],
106108
sortOrder?: SearchSortOrder,
107-
limit: number = 1000
109+
limit: number = 1000,
110+
offset: number = 0
108111
): Promise<SearchResponseElement[] | null> => {
109-
const createdTimeOrder = { direction: sortOrder?.direction, field: 'createdTime' };
112+
const createdTimeOrder = { direction: 'Ascending', field: 'createdTime' } as SearchSortOrder;
110113
const searchParams: SearchRequestPayload = {
111-
prefix: 'nurseryWithdrawals',
114+
prefix: NURSERY_WITHDRAWALS_PREFIX,
112115
fields: [
113116
'id',
114117
'createdTime',
@@ -130,6 +133,7 @@ const listNurseryWithdrawals = async (
130133
search: SearchService.convertToSearchNodePayload(searchCriteria, organizationId),
131134
sortOrder: sortOrder ? [sortOrder, createdTimeOrder] : [{ field: 'id', direction: 'Ascending' }],
132135
count: limit,
136+
cursor: offset.toString(),
133137
};
134138

135139
const data: NurseryWithdrawalsSearchResponseElement[] | null = await SearchService.search(searchParams);
@@ -157,12 +161,26 @@ const listNurseryWithdrawals = async (
157161
return null;
158162
};
159163

164+
const countNurseryWithdrawals = async (
165+
organizationId: number,
166+
searchCriteria: SearchNodePayload[]
167+
): Promise<number | null> => {
168+
const searchParams: SearchRequestPayload = {
169+
prefix: NURSERY_WITHDRAWALS_PREFIX,
170+
fields: [],
171+
search: SearchService.convertToSearchNodePayload(searchCriteria, organizationId),
172+
count: 0,
173+
};
174+
175+
return await SearchService.searchCount(searchParams);
176+
};
177+
160178
/**
161179
* Check if an org has nursery withdrawals
162180
*/
163181
const hasNurseryWithdrawals = async (organizationId: number): Promise<boolean> => {
164182
const searchParams: SearchRequestPayload = {
165-
prefix: 'nurseryWithdrawals',
183+
prefix: NURSERY_WITHDRAWALS_PREFIX,
166184
fields: ['id'],
167185
search: SearchService.convertToSearchNodePayload({}, organizationId),
168186
count: 1,
@@ -218,7 +236,7 @@ const getWithdrawalPhotosList = async (withdrawalId: number): Promise<Response &
218236
*/
219237
const getFilterOptions = async (organizationId: number): Promise<FieldOptionsMap> => {
220238
const searchParams: SearchRequestPayload = {
221-
prefix: 'nurseryWithdrawals',
239+
prefix: NURSERY_WITHDRAWALS_PREFIX,
222240
fields: [
223241
'id',
224242
'purpose',
@@ -305,6 +323,7 @@ const getPlantingSiteWithdrawnSpecies = async (
305323
*/
306324
const NurseryWithdrawalService = {
307325
createBatchWithdrawal,
326+
countNurseryWithdrawals,
308327
uploadWithdrawalPhotos,
309328
listNurseryWithdrawals,
310329
hasNurseryWithdrawals,

0 commit comments

Comments
 (0)