diff --git a/src/api/systems.test.tsx b/src/api/systems.test.tsx index b69589cec..713448c44 100644 --- a/src/api/systems.test.tsx +++ b/src/api/systems.test.tsx @@ -17,6 +17,7 @@ import { useEditSystem, useMoveToSystem, useSystem, + useSystemIds, useSystems, useSystemsBreadcrumbs, } from './systems'; @@ -106,6 +107,36 @@ describe('System api functions', () => { }); }); + describe('useSystemIds', () => { + it('sends a request to fetch system data and returns a successful response', async () => { + const { result } = renderHook( + () => + useSystemIds([ + '65328f34a40ff5301575a4e3', + '656ef565ed0773f82e44bc6d', + ]), + { + wrapper: hooksWrapperWithProviders(), + } + ); + + await waitFor(() => { + result.current.forEach((query) => expect(query.isSuccess).toBeTruthy()); + }); + + expect(result.current[0].data).toEqual( + SystemsJSON.filter( + (system) => system.id === '65328f34a40ff5301575a4e3' + )[0] + ); + expect(result.current[1].data).toEqual( + SystemsJSON.filter( + (system) => system.id === '656ef565ed0773f82e44bc6d' + )[0] + ); + }); + }); + describe('useSystemsBreadcrumbs', () => { it('does not send a request to fetch breadcrumbs data for a system when its id is null', async () => { const { result } = renderHook(() => useSystemsBreadcrumbs(null), { diff --git a/src/api/systems.tsx b/src/api/systems.tsx index 1c288c048..f66e7e856 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -2,6 +2,7 @@ import { UseMutationResult, UseQueryResult, useMutation, + useQueries, useQuery, useQueryClient, } from '@tanstack/react-query'; @@ -44,6 +45,15 @@ const fetchSystems = async (parent_id?: string): Promise => { }); }; +export const useSystemIds = (ids: string[]): UseQueryResult[] => { + return useQueries({ + queries: ids.map((id) => ({ + queryKey: ['System', id], + queryFn: () => fetchSystem(id), + })), + }); +}; + export const useSystems = ( parent_id?: string ): UseQueryResult => { diff --git a/src/items/__snapshots__/itemsDetailsPanel.component.test.tsx.snap b/src/items/__snapshots__/itemsDetailsPanel.component.test.tsx.snap index 9248fb34c..ca233fd06 100644 --- a/src/items/__snapshots__/itemsDetailsPanel.component.test.tsx.snap +++ b/src/items/__snapshots__/itemsDetailsPanel.component.test.tsx.snap @@ -219,6 +219,24 @@ exports[`Catalogue Items details panel > renders details panel correctly (None v scrapped

+
+

+ System +

+

+ +

+
@@ -739,6 +757,24 @@ exports[`Catalogue Items details panel > renders details panel correctly (no dat inUse

+
@@ -1259,6 +1295,24 @@ exports[`Catalogue Items details panel > renders details panel correctly (when t new

+
@@ -1779,6 +1833,24 @@ exports[`Catalogue Items details panel > renders details panel correctly 1`] = ` inUse

+
@@ -2309,6 +2381,26 @@ exports[`Catalogue Items details panel > renders manufacturer panel correctly 1` inUse

+
@@ -2856,6 +2948,26 @@ exports[`Catalogue Items details panel > renders notes panel correctly 1`] = ` inUse

+
+

+ System +

+

+ + Giant laser + +

+
@@ -3403,6 +3515,26 @@ exports[`Catalogue Items details panel > renders properties panel correctly 1`] inUse

+
+

+ System +

+

+ + Giant laser + +

+
diff --git a/src/items/__snapshots__/itemsTable.component.test.tsx.snap b/src/items/__snapshots__/itemsTable.component.test.tsx.snap index 34f11531f..45b623b9c 100644 --- a/src/items/__snapshots__/itemsTable.component.test.tsx.snap +++ b/src/items/__snapshots__/itemsTable.component.test.tsx.snap @@ -246,7 +246,7 @@ exports[`Items Table > renders correctly part 1 due column virtualisation 1`] = > { setTabValue(newValue); }; @@ -133,13 +136,27 @@ function ItemsDetailsPanel(props: ItemsDetailsPanelProps) { + System + + + {systemData?.name} + + + + + Last Modified {formatDateTimeStrings(itemData.modified_time, true)} - + Created {formatDateTimeStrings(itemData.created_time, true)} diff --git a/src/items/itemsLandingPage.component.test.tsx b/src/items/itemsLandingPage.component.test.tsx index 9d7e5180b..510bea048 100644 --- a/src/items/itemsLandingPage.component.test.tsx +++ b/src/items/itemsLandingPage.component.test.tsx @@ -52,7 +52,7 @@ describe('Items Landing Page', () => { expect(screen.getByText('Asset Number')).toBeInTheDocument(); - expect(screen.getByText('System ID')).toBeInTheDocument(); + expect(screen.getByText('System')).toBeInTheDocument(); }); it('navigates to the system when the system id is clicked', async () => { @@ -61,8 +61,8 @@ describe('Items Landing Page', () => { expect(screen.getByText('Cameras 1')).toBeInTheDocument(); }); - const systemID = screen.getByText('65328f34a40ff5301575a4e3'); - expect(systemID).toHaveAttribute( + const systemName = screen.getByText('Giant laser'); + expect(systemName).toHaveAttribute( 'href', '/systems/65328f34a40ff5301575a4e3' ); diff --git a/src/items/itemsLandingPage.component.tsx b/src/items/itemsLandingPage.component.tsx index 46b95756a..1442a4dcc 100644 --- a/src/items/itemsLandingPage.component.tsx +++ b/src/items/itemsLandingPage.component.tsx @@ -26,6 +26,7 @@ import Breadcrumbs from '../view/breadcrumbs.component'; import ItemDialog from './itemDialog.component'; import { useNavigateToCatalogue } from '../catalogue/catalogue.component'; import { formatDateTimeStrings } from '../utils'; +import { useSystem } from '../api/systems'; function ItemsLandingPage() { // Navigation @@ -47,6 +48,8 @@ function ItemsLandingPage() { catalogueItemData?.catalogue_category_id ); + const { data: systemData } = useSystem(itemData?.system_id); + const [itemLandingBreadcrumbs, setItemLandingBreadcrumbs] = React.useState< BreadcrumbsInfo | undefined >(catalogueBreadcrumbs); @@ -261,15 +264,15 @@ function ItemsLandingPage() { - System ID + System - {itemData.system_id} + {systemData?.name} diff --git a/src/items/itemsTable.component.test.tsx b/src/items/itemsTable.component.test.tsx index a99fe7849..505ed9bfa 100644 --- a/src/items/itemsTable.component.test.tsx +++ b/src/items/itemsTable.component.test.tsx @@ -56,7 +56,7 @@ describe('Items Table', () => { const view = createView(); await waitFor(() => { - expect(screen.getByText('Serial Number')).toBeInTheDocument(); + expect(screen.getByText('5YUQDDjKpz2z')).toBeInTheDocument(); }); expect(view.asFragment()).toMatchSnapshot(); }); @@ -74,9 +74,9 @@ describe('Items Table', () => { it('renders correctly part 3 due column virtualisation and checks that the href is correct for the system ID', async () => { createView(); - await ensureColumnsVisible(['Warranty End Date', 'System ID', 'Created']); + await ensureColumnsVisible(['Warranty End Date', 'System', 'Created']); - const systemID = screen.getAllByText('65328f34a40ff5301575a4e3'); + const systemID = screen.getAllByText('Giant laser'); expect(systemID[0]).toHaveAttribute( 'href', '/systems/65328f34a40ff5301575a4e3' diff --git a/src/items/itemsTable.component.tsx b/src/items/itemsTable.component.tsx index 1472df8d2..066d12a0b 100644 --- a/src/items/itemsTable.component.tsx +++ b/src/items/itemsTable.component.tsx @@ -28,6 +28,7 @@ import { CatalogueCategory, CatalogueItem, Item, + System, UsageStatusType, } from '../app.types'; import { @@ -39,6 +40,7 @@ import { formatDateTimeStrings, getPageHeightCalc } from '../utils'; import DeleteItemDialog from './deleteItemDialog.component'; import ItemDialog from './itemDialog.component'; import ItemsDetailsPanel from './itemsDetailsPanel.component'; +import { useSystemIds } from '../api/systems'; export interface ItemTableProps { catalogueCategory: CatalogueCategory; @@ -46,12 +48,22 @@ export interface ItemTableProps { dense: boolean; } +interface TableRowData { + item: Item; + system?: System; +} + export function ItemsTable(props: ItemTableProps) { const { catalogueCategory, catalogueItem, dense } = props; + const [tableRows, setTableRows] = React.useState([]); + const noResultsTxt = 'No results found: Try adding an item by using the Add Item button on the top left of your screen'; - const { data, isLoading } = useItems(undefined, catalogueItem.id); + const { data: itemsData, isLoading: isLoadingItems } = useItems( + undefined, + catalogueItem.id + ); const [deleteItemDialogOpen, setDeleteItemDialogOpen] = React.useState(false); @@ -60,13 +72,41 @@ export function ItemsTable(props: ItemTableProps) { undefined ); + const systemIdSet = new Set( + itemsData?.map((item) => item.system_id) ?? [] + ); + + let isLoading = isLoadingItems; + const systemList: (System | undefined)[] = useSystemIds( + Array.from(systemIdSet.values()) + ).map((query) => { + isLoading = isLoading || query.isLoading; + return query.data; + }); + + //Once loading finished - use same logic as catalogueItemsTable to pair up data + React.useEffect(() => { + if (!isLoading && itemsData) { + setTableRows( + itemsData.map((itemData) => ({ + item: itemData, + system: systemList?.find( + (system) => system?.id === itemData.system_id + ), + })) + ); + } + //Purposefully leave out systemList from dependencies for same reasons as catalogueItemsTable + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [itemsData, isLoading]); + const [itemDialogType, setItemsDialogType] = React.useState< 'create' | 'save as' | 'edit' >('create'); // Breadcrumbs + Mui table V2 + extra const tableHeight = getPageHeightCalc('50px + 110px + 32px'); - const columns = React.useMemo[]>(() => { + const columns = React.useMemo[]>(() => { const viewCatalogueItemProperties = catalogueCategory?.catalogue_item_properties ?? []; const propertyFilters: PropertyFiltersType = { @@ -78,52 +118,52 @@ export function ItemsTable(props: ItemTableProps) { return [ { header: 'Serial Number', - accessorFn: (row) => row.serial_number ?? 'No serial number', + accessorFn: (row) => row.item.serial_number ?? 'No serial number', id: 'serial_number', size: 250, Cell: ({ row }) => ( - - {row.original.serial_number ?? 'No serial number'} + + {row.original.item.serial_number ?? 'No serial number'} ), enableGrouping: false, }, { header: 'Last modified', - accessorFn: (row) => new Date(row.modified_time), + accessorFn: (row) => new Date(row.item.modified_time), id: 'modified_time', filterVariant: 'datetime-range', size: 350, Cell: ({ row }) => - row.original.modified_time && - formatDateTimeStrings(row.original.modified_time, true), + row.original.item.modified_time && + formatDateTimeStrings(row.original.item.modified_time, true), enableGrouping: false, }, { header: 'Created', - accessorFn: (row) => new Date(row.created_time), + accessorFn: (row) => new Date(row.item.created_time), id: 'created_time', filterVariant: 'datetime-range', size: 350, Cell: ({ row }) => - formatDateTimeStrings(row.original.created_time, true), + formatDateTimeStrings(row.original.item.created_time, true), enableGrouping: false, }, { header: 'Asset Number', - accessorFn: (row) => row.asset_number, + accessorFn: (row) => row.item.asset_number, id: 'asset_number', size: 250, }, { header: 'Purchase Order Number', - accessorFn: (row) => row.purchase_order_number, + accessorFn: (row) => row.item.purchase_order_number, id: 'purchase_order_number', size: 350, }, { header: 'Warranty End Date', - accessorFn: (row) => new Date(row.warranty_end_date ?? ''), + accessorFn: (row) => new Date(row.item.warranty_end_date ?? ''), id: 'warranty_end_date', filterVariant: 'date-range', size: 350, @@ -132,14 +172,14 @@ export function ItemsTable(props: ItemTableProps) { // For ensuring space when grouping sx={{ marginRight: 0.5, fontSize: 'inherit' }} > - {row.original.warranty_end_date && - formatDateTimeStrings(row.original.warranty_end_date, false)} + {row.original.item.warranty_end_date && + formatDateTimeStrings(row.original.item.warranty_end_date, false)} ), }, { header: 'Delivered Date', - accessorFn: (row) => new Date(row.delivered_date ?? ''), + accessorFn: (row) => new Date(row.item.delivered_date ?? ''), id: 'delivered_date', filterVariant: 'date-range', size: 350, @@ -148,14 +188,14 @@ export function ItemsTable(props: ItemTableProps) { // For ensuring space when grouping sx={{ marginRight: 0.5, fontSize: 'inherit' }} > - {row.original.delivered_date && - formatDateTimeStrings(row.original.delivered_date, false)} + {row.original.item.delivered_date && + formatDateTimeStrings(row.original.item.delivered_date, false)} ), }, { header: 'Is Defective', - accessorFn: (row) => (row.is_defective === true ? 'Yes' : 'No'), + accessorFn: (row) => (row.item.is_defective === true ? 'Yes' : 'No'), id: 'is_defective', size: 200, filterVariant: 'select', @@ -167,7 +207,7 @@ export function ItemsTable(props: ItemTableProps) { const status = Object.values(UsageStatusType).find( (value) => UsageStatusType[value as keyof typeof UsageStatusType] === - row.usage_status + row.item.usage_status ); return status || 'Unknown'; }, @@ -177,17 +217,17 @@ export function ItemsTable(props: ItemTableProps) { }, { header: 'Notes', - accessorFn: (row) => row.notes ?? '', + accessorFn: (row) => row.item.notes ?? '', id: 'notes', size: 250, Cell: ({ row }) => - row.original.notes && ( + row.original.item.notes && ( @@ -195,43 +235,46 @@ export function ItemsTable(props: ItemTableProps) { enableGrouping: false, }, { - header: 'System ID', - accessorFn: (row) => row.system_id, - id: 'system_id', + header: 'System', + accessorFn: (row) => row.system?.name ?? '', + getGroupingValue: (row) => row.system?.id ?? '', + id: 'system.name', size: 250, Cell: ({ row }) => ( - {row.original.system_id} + {row.original.system?.name} ), }, ...viewCatalogueItemProperties.map((property) => ({ header: `${property.name} ${property.unit ? `(${property.unit})` : ''}`, id: `row.catalogueItem.properties.${property.id}`, - accessorFn: (row: Item) => { + accessorFn: (row: TableRowData) => { if (property.type === 'boolean') { return (findPropertyValue( - row.properties, + row.item.properties, property.id ) as boolean) === true ? 'Yes' : 'No'; } else if (property.type === 'number') { - return typeof findPropertyValue(row.properties, property.id) === - 'number' - ? findPropertyValue(row.properties, property.id) + return typeof findPropertyValue( + row.item.properties, + property.id + ) === 'number' + ? findPropertyValue(row.item.properties, property.id) : 0; } else { // if the value doesn't exist it return type "true" we need to change this // to '' to allow this column to be filterable - return findPropertyValue(row.properties, property.id); + return findPropertyValue(row.item.properties, property.id); } }, size: 250, @@ -240,25 +283,33 @@ export function ItemsTable(props: ItemTableProps) { property.type as 'string' | 'boolean' | 'number' | 'null' ], - Cell: ({ row }: { row: MRT_Row }) => { + Cell: ({ row }: { row: MRT_Row }) => { if ( - typeof findPropertyValue(row.original.properties, property.id) === - 'number' + typeof findPropertyValue( + row.original.item.properties, + property.id + ) === 'number' ) { - return findPropertyValue(row.original.properties, property.id) === 0 + return findPropertyValue( + row.original.item.properties, + property.id + ) === 0 ? 0 - : findPropertyValue(row.original.properties, property.id) !== null - ? findPropertyValue(row.original.properties, property.id) + : findPropertyValue(row.original.item.properties, property.id) !== + null + ? findPropertyValue(row.original.item.properties, property.id) : ''; } else if ( - typeof findPropertyValue(row.original.properties, property.id) === - 'boolean' + typeof findPropertyValue( + row.original.item.properties, + property.id + ) === 'boolean' ) { - return findPropertyValue(row.original.properties, property.id) + return findPropertyValue(row.original.item.properties, property.id) ? 'Yes' : 'No'; } else { - return findPropertyValue(row.original.properties, property.id); + return findPropertyValue(row.original.item.properties, property.id); } }, })), @@ -285,7 +336,7 @@ export function ItemsTable(props: ItemTableProps) { { ...columns[7], size: 400 }, ] : columns, - data: data ?? [], //data must be memoized or stable (useState, useMemo, defined outside of this component, etc.) + data: tableRows, //data must be memoized or stable (useState, useMemo, defined outside of this component, etc.) // Features enableColumnOrdering: dense ? false : true, enableFacetedValues: true, @@ -349,7 +400,7 @@ export function ItemsTable(props: ItemTableProps) { }, // Functions ...onPreservedStatesChange, - getRowId: (row) => row.id, + getRowId: (row) => row.item.id, renderCreateRowDialogContent: ({ table, row }) => { return ( <> @@ -365,11 +416,11 @@ export function ItemsTable(props: ItemTableProps) { itemDialogType === 'create' ? undefined : { - ...row.original, + ...row.original.item, notes: itemDialogType === 'save as' - ? `${row.original.notes || ''}\n\nThis is a copy of the item with this ID: ${row.original.id}` - : row.original.notes, + ? `${row.original.item.notes || ''}\n\nThis is a copy of the item with this ID: ${row.original.item.id}` + : row.original.item.notes, } } /> @@ -407,7 +458,7 @@ export function ItemsTable(props: ItemTableProps) { return [ { setItemsDialogType('edit'); table.setCreatingRow(row); @@ -422,7 +473,7 @@ export function ItemsTable(props: ItemTableProps) { , { setItemsDialogType('save as'); table.setCreatingRow(row); @@ -437,10 +488,10 @@ export function ItemsTable(props: ItemTableProps) { , { setDeleteItemDialogOpen(true); - setSelectedItem(row.original); + setSelectedItem(row.original.item); closeMenu(); }} sx={{ m: 0 }} @@ -455,7 +506,7 @@ export function ItemsTable(props: ItemTableProps) { renderDetailPanel: dense ? ({ row }) => ( )