2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 11:10:54 +00:00

[UI] Refactor useQuery hooks (#9894)

* Improvements for table loading

- Retry table queries on failure
- Properly store failure modes
- Increasing standoff time on query failure

* Add error messages

* Better error extraction

* Simplify error handling

* Update NotesEditor

* Update dashboard items

* Tweak table refetch

* Refactor notifications query

* Fix for calendar querty

* Other fixes

* Allow retry for search query

* Further adjustments

* Improved dashboard

* Upate more useQuery hooks

* Fix broken URL (was used for testing)

* Remove custom delay

* Revert change to noRecordsText
This commit is contained in:
Oliver
2025-06-29 22:07:06 +10:00
committed by GitHub
parent ae653e5649
commit a8b805cdec
30 changed files with 144 additions and 211 deletions

View File

@ -72,9 +72,6 @@ export function PrintingActions({
.then((response: any) => { .then((response: any) => {
return extractAvailableFields(response, 'POST') || {}; return extractAvailableFields(response, 'POST') || {};
}) })
.catch(() => {
return {};
})
}); });
const labelFields: ApiFormFieldSet = useMemo(() => { const labelFields: ApiFormFieldSet = useMemo(() => {

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Alert, Card, Center, Divider, Loader, Text } from '@mantine/core'; import { Alert, Card, Center, Divider, Loader, Text } from '@mantine/core';
import { useDisclosure, useHotkeys } from '@mantine/hooks'; import { useDisclosure, useHotkeys } from '@mantine/hooks';
import { IconInfoCircle } from '@tabler/icons-react'; import { IconExclamationCircle, IconInfoCircle } from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { type Layout, Responsive, WidthProvider } from 'react-grid-layout'; import { type Layout, Responsive, WidthProvider } from 'react-grid-layout';
@ -220,6 +220,11 @@ export default function DashboardLayout() {
removing={removing} removing={removing}
/> />
<Divider p='xs' /> <Divider p='xs' />
{availableWidgets.error && (
<Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
{t`Failed to load dashboard widgets.`}
</Alert>
)}
{layouts && loaded && availableWidgets.loaded ? ( {layouts && loaded && availableWidgets.loaded ? (
<> <>
{widgetLabels.length == 0 ? ( {widgetLabels.length == 0 ? (

View File

@ -47,8 +47,7 @@ function QueryCountWidget({
limit: 1 limit: 1
} }
}) })
.then((res) => res.data) .then((res) => res.data);
.catch(() => {});
} }
}); });

View File

@ -196,19 +196,14 @@ function NameBadge({
const url = apiUrl(path, pk); const url = apiUrl(path, pk);
return api return api.get(url).then((response) => {
.get(url) switch (response.status) {
.then((response) => { case 200:
switch (response.status) { return response.data;
case 200: default:
return response.data; return {};
default: }
return {}; });
}
})
.catch(() => {
return {};
});
} }
}); });
@ -356,9 +351,6 @@ function TableAnchorValue(props: Readonly<FieldProps>) {
default: default:
return {}; return {};
} }
})
.catch(() => {
return {};
}); });
} }
}); });

View File

@ -92,11 +92,9 @@ export default function NotesEditor({
const dataQuery = useQuery({ const dataQuery = useQuery({
queryKey: ['notes-editor', noteUrl, modelType, modelId], queryKey: ['notes-editor', noteUrl, modelType, modelId],
retry: 5,
queryFn: () => queryFn: () =>
api api.get(noteUrl).then((response) => response.data?.notes ?? ''),
.get(noteUrl)
.then((response) => response.data?.notes ?? '')
.catch(() => ''),
enabled: true enabled: true
}); });

View File

@ -254,19 +254,14 @@ export function ApiForm({
props.pathParams props.pathParams
], ],
queryFn: async () => { queryFn: async () => {
return await api return await api.get(url).then((response: any) => {
.get(url) // Process API response
.then((response: any) => { const fetchedData: any = processFields(fields, response.data);
// Process API response
const fetchedData: any = processFields(fields, response.data);
// Update form values, but only for the fields specified for this form // Update form values, but only for the fields specified for this form
form.reset(fetchedData); form.reset(fetchedData);
return fetchedData; return fetchedData;
}) });
.catch(() => {
return {};
});
} }
}); });

View File

@ -179,10 +179,6 @@ export function RelatedModelField({
setData(values); setData(values);
dataRef.current = values; dataRef.current = values;
return response; return response;
})
.catch((error) => {
setData([]);
return error;
}); });
} }
}); });

View File

@ -57,10 +57,7 @@ export function LicenseModal() {
queryKey: ['license'], queryKey: ['license'],
refetchOnMount: true, refetchOnMount: true,
queryFn: () => queryFn: () =>
api api.get(apiUrl(ApiEndpoints.license)).then((res) => res.data ?? {})
.get(apiUrl(ApiEndpoints.license))
.then((res) => res.data ?? {})
.catch(() => {})
}); });
const packageKeys = useMemo(() => { const packageKeys = useMemo(() => {

View File

@ -78,23 +78,17 @@ export function Header() {
return null; return null;
} }
try { return api
const params = { .get(apiUrl(ApiEndpoints.notifications_list), {
params: { params: {
read: false, read: false,
limit: 1 limit: 1
} }
}; })
const response = await api .then((response: any) => {
.get(apiUrl(ApiEndpoints.notifications_list), params) setNotificationCount(response?.data?.count ?? 0);
.catch(() => { return response.data ?? null;
return null; });
});
setNotificationCount(response?.data?.count ?? 0);
return response?.data ?? null;
} catch (error) {
return null;
}
}, },
// Refetch every minute, *if* the tab is visible // Refetch every minute, *if* the tab is visible
refetchInterval: 60 * 1000, refetchInterval: 60 * 1000,

View File

@ -1,5 +1,6 @@
import { import {
ActionIcon, ActionIcon,
Alert,
Anchor, Anchor,
Divider, Divider,
Drawer, Drawer,
@ -15,6 +16,7 @@ import {
import { import {
IconChevronDown, IconChevronDown,
IconChevronRight, IconChevronRight,
IconExclamationCircle,
IconSitemap IconSitemap
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@ -29,6 +31,7 @@ import {
getDetailUrl, getDetailUrl,
navigateToLink navigateToLink
} from '@lib/functions/Navigation'; } from '@lib/functions/Navigation';
import { t } from '@lingui/core/macro';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import { ApiIcon } from '../items/ApiIcon'; import { ApiIcon } from '../items/ApiIcon';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
@ -67,10 +70,6 @@ export default function NavigationTree({
} }
}) })
.then((response) => response.data ?? []) .then((response) => response.data ?? [])
.catch((error) => {
console.error(`Error fetching ${modelType} tree`);
return [];
})
}); });
const follow = useCallback( const follow = useCallback(
@ -96,7 +95,7 @@ export default function NavigationTree({
const nodes: Record<number, any> = {}; const nodes: Record<number, any> = {};
const tree: TreeNodeData[] = []; const tree: TreeNodeData[] = [];
if (!query?.data?.length) { if (!query || !query?.data?.length) {
return []; return [];
} }
@ -207,7 +206,13 @@ export default function NavigationTree({
<Stack gap='xs'> <Stack gap='xs'>
<Divider /> <Divider />
<LoadingOverlay visible={query.isFetching || query.isLoading} /> <LoadingOverlay visible={query.isFetching || query.isLoading} />
<Tree data={data} tree={treeState} renderNode={renderNode} /> {query.isError ? (
<Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
{t`Error loading navigation tree.`}
</Alert>
) : (
<Tree data={data} tree={treeState} renderNode={renderNode} />
)}
</Stack> </Stack>
</Drawer> </Drawer>
); );

View File

@ -122,10 +122,7 @@ export function NotificationDrawer({
ordering: '-creation' ordering: '-creation'
} }
}) })
.then((response) => response.data) .then((response) => response.data),
.catch((error) => {
return error;
}),
refetchOnMount: false refetchOnMount: false
}); });

View File

@ -397,11 +397,7 @@ export function SearchDrawer({
return api return api
.post(apiUrl(ApiEndpoints.api_search), params) .post(apiUrl(ApiEndpoints.api_search), params)
.then((response) => response.data) .then((response) => response.data);
.catch((error) => {
console.error(error);
return [];
});
}; };
// Search query manager // Search query manager

View File

@ -137,10 +137,7 @@ export function RenderRemoteInstance({
queryFn: async () => { queryFn: async () => {
const url = apiUrl(ModelInformationDict[model].api_endpoint, pk); const url = apiUrl(ModelInformationDict[model].api_endpoint, pk);
return api return api.get(url).then((response) => response.data);
.get(url)
.then((response) => response.data)
.catch(() => null);
} }
}); });

View File

@ -1100,10 +1100,7 @@ function useStockOperationModal({
.get(url, { .get(url, {
params: params params: params
}) })
.then((response) => response.data ?? []) .then((response) => response.data ?? []);
.catch(() => {
return [];
});
} }
}); });
@ -1368,7 +1365,7 @@ export function useFindSerialNumberForm({
} }
}, },
checkClose: (data, form) => { checkClose: (data, form) => {
if (data.length == 0) { if (!data || data?.length == 0) {
form.setError('serial', { message: t`No matching items` }); form.setError('serial', { message: t`No matching items` });
return false; return false;
} }

View File

@ -15,7 +15,7 @@ export function extractErrorMessage({
field?: string; field?: string;
defaultMessage?: string; defaultMessage?: string;
}): string { }): string {
const error_data = error.response?.data ?? null; const error_data = error.response?.data ?? error.data ?? null;
let message = ''; let message = '';
@ -25,7 +25,7 @@ export function extractErrorMessage({
// No message? Look at the response status codes // No message? Look at the response status codes
if (!message) { if (!message) {
const status = error.response?.status ?? null; const status = error.status ?? error.response?.status ?? null;
if (status) { if (status) {
switch (status) { switch (status) {
@ -48,8 +48,11 @@ export function extractErrorMessage({
message = t`Internal server error`; message = t`Internal server error`;
break; break;
default: default:
message = t`Unknown error`;
break; break;
} }
message = `${status} - ${message}`;
} }
} }

View File

@ -80,12 +80,14 @@ export function showApiErrorMessage({
error, error,
title, title,
message, message,
field field,
id
}: { }: {
error: any; error: any;
title: string; title: string;
message?: string; message?: string;
field?: string; field?: string;
id?: string;
}) { }) {
const errorMessage = extractErrorMessage({ const errorMessage = extractErrorMessage({
error: error, error: error,
@ -93,7 +95,10 @@ export function showApiErrorMessage({
defaultMessage: message defaultMessage: message
}); });
notifications.hide(id ?? 'api-error');
notifications.show({ notifications.show({
id: id ?? 'api-error',
title: title, title: title,
message: errorMessage, message: errorMessage,
color: 'red' color: 'red'

View File

@ -103,6 +103,14 @@ export default function useCalendar({
const query = useQuery({ const query = useQuery({
enabled: !!startDate && !!endDate, enabled: !!startDate && !!endDate,
queryKey: ['calendar', name, endpoint, queryFilters], queryKey: ['calendar', name, endpoint, queryFilters],
throwOnError: (error: any) => {
showApiErrorMessage({
error: error,
title: 'Error fetching calendar data'
});
return true;
},
queryFn: async () => { queryFn: async () => {
// Fetch data from the API // Fetch data from the API
return api return api
@ -111,12 +119,6 @@ export default function useCalendar({
}) })
.then((response) => { .then((response) => {
return response.data ?? []; return response.data ?? [];
})
.catch((error) => {
showApiErrorMessage({
error: error,
title: 'Error fetching calendar data'
});
}); });
} }
}); });
@ -173,6 +175,6 @@ export default function useCalendar({
setEndDate, setEndDate,
exportModal, exportModal,
query: query, query: query,
data: query.data data: query.data ?? []
}; };
} }

View File

@ -19,6 +19,7 @@ import { useUserState } from '../states/UserState';
interface DashboardLibraryProps { interface DashboardLibraryProps {
items: DashboardWidgetProps[]; items: DashboardWidgetProps[];
loaded: boolean; loaded: boolean;
error: any;
} }
/** /**
@ -51,13 +52,7 @@ export function useDashboardItems(): DashboardLibraryProps {
feature_type: PluginUIFeatureType.dashboard feature_type: PluginUIFeatureType.dashboard
}); });
return api return api.get(url).then((response: any) => response.data);
.get(url)
.then((response: any) => response.data)
.catch((_error: any) => {
console.error('ERR: Failed to fetch plugin dashboard items');
return [];
});
} }
}); });
@ -90,7 +85,7 @@ export function useDashboardItems(): DashboardLibraryProps {
}; };
}) ?? [] }) ?? []
); );
}, [pluginQuery, inventreeContext]); }, [pluginQuery.data, inventreeContext]);
const items: DashboardWidgetProps[] = useMemo(() => { const items: DashboardWidgetProps[] = useMemo(() => {
const widgets = [...builtin, ...pluginDashboardItems]; const widgets = [...builtin, ...pluginDashboardItems];
@ -113,6 +108,7 @@ export function useDashboardItems(): DashboardLibraryProps {
return { return {
items: items, items: items,
loaded: loaded loaded: loaded,
error: pluginQuery.error
}; };
} }

View File

@ -72,9 +72,6 @@ export default function useDataExport({
.then((response: any) => { .then((response: any) => {
return extractAvailableFields(response, 'GET') || {}; return extractAvailableFields(response, 'GET') || {};
}) })
.catch(() => {
return {};
})
}); });
// Construct a field set for the export form // Construct a field set for the export form

View File

@ -39,8 +39,7 @@ export function useFilters(props: UseFilterProps) {
} }
return data; return data;
}) });
.catch((error) => []);
} }
}); });

View File

@ -85,6 +85,13 @@ export function useGenerator(props: GeneratorProps): GeneratorState {
], ],
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
throwOnError: (error: any) => {
console.error(
`Error generating ${props.key} @ ${props.endpoint}:`,
error
);
return false;
},
queryFn: async () => { queryFn: async () => {
const generatorQuery = { const generatorQuery = {
...(props.initialQuery ?? {}), ...(props.initialQuery ?? {}),
@ -105,14 +112,6 @@ export function useGenerator(props: GeneratorProps): GeneratorState {
props.onGenerate?.(value); props.onGenerate?.(value);
return response; return response;
})
.catch((error) => {
console.error(
`Error generating ${props.key} @ ${props.endpoint}:`,
error
);
return null;
}); });
} }
}); });

View File

@ -53,6 +53,10 @@ export function usePluginPanels({
const pluginQuery = useQuery({ const pluginQuery = useQuery({
enabled: pluginPanelsEnabled && !!model && id !== undefined, enabled: pluginPanelsEnabled && !!model && id !== undefined,
queryKey: ['custom-plugin-panels', model, id], queryKey: ['custom-plugin-panels', model, id],
throwOnError: (error: any) => {
console.error('ERR: Failed to fetch plugin panels');
return false;
},
queryFn: async () => { queryFn: async () => {
if (!pluginPanelsEnabled || !model) { if (!pluginPanelsEnabled || !model) {
return Promise.resolve([]); return Promise.resolve([]);
@ -69,11 +73,7 @@ export function usePluginPanels({
target_id: id target_id: id
} }
}) })
.then((response: any) => response.data) .then((response: any) => response.data);
.catch((_error: any) => {
console.error('ERR: Failed to fetch plugin panels');
return [];
});
} }
}); });

View File

@ -723,8 +723,7 @@ export default function PartDetail() {
default: default:
break; break;
} }
}) });
.catch(() => {});
return revisions; return revisions;
} }

View File

@ -491,9 +491,6 @@ export default function StockDetail() {
} else { } else {
return null; return null;
} }
})
.catch(() => {
return null;
}); });
} }
}); });
@ -504,7 +501,7 @@ export default function StockDetail() {
return true; return true;
} }
if (trackedBomItemQuery.data != null) { if (!!trackedBomItemQuery.data) {
return trackedBomItemQuery.data; return trackedBomItemQuery.data;
} }

View File

@ -28,12 +28,12 @@ import { navigateToLink } from '@lib/functions/Navigation';
import type { TableFilter } from '@lib/types/Filters'; import type { TableFilter } from '@lib/types/Filters';
import type { ApiFormFieldSet } from '@lib/types/Forms'; import type { ApiFormFieldSet } from '@lib/types/Forms';
import type { TableState } from '@lib/types/Tables'; import type { TableState } from '@lib/types/Tables';
import { hideNotification, showNotification } from '@mantine/notifications';
import { IconArrowRight } from '@tabler/icons-react'; import { IconArrowRight } from '@tabler/icons-react';
import { Boundary } from '../components/Boundary'; import { Boundary } from '../components/Boundary';
import { useApi } from '../contexts/ApiContext'; import { useApi } from '../contexts/ApiContext';
import { resolveItem } from '../functions/conversion'; import { resolveItem } from '../functions/conversion';
import { extractAvailableFields, mapFields } from '../functions/forms'; import { extractAvailableFields, mapFields } from '../functions/forms';
import { showApiErrorMessage } from '../functions/notifications';
import { useLocalState } from '../states/LocalState'; import { useLocalState } from '../states/LocalState';
import type { TableColumn } from './Column'; import type { TableColumn } from './Column';
import InvenTreeTableHeader from './InvenTreeTableHeader'; import InvenTreeTableHeader from './InvenTreeTableHeader';
@ -199,7 +199,16 @@ export function InvenTreeTable<T extends Record<string, any>>({
tableProps.params, tableProps.params,
props.enableColumnCaching props.enableColumnCaching
], ],
retry: 3, retry: 5,
retryDelay: (attempt: number) => (1 + attempt) * 250,
throwOnError: (error: any) => {
showApiErrorMessage({
error: error,
title: t`Error loading table options`
});
return true;
},
refetchOnMount: true, refetchOnMount: true,
gcTime: 5000, gcTime: 5000,
queryFn: async () => { queryFn: async () => {
@ -240,17 +249,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
setTableColumnNames(cacheKey)(names); setTableColumnNames(cacheKey)(names);
} }
return null;
})
.catch(() => {
hideNotification('table-options-error');
showNotification({
id: 'table-options-error',
title: t`API Error`,
message: t`Failed to load table options`,
color: 'red'
});
return null; return null;
}); });
} }
@ -270,9 +268,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
setFieldNames(cachedNames); setFieldNames(cachedNames);
return; return;
} }
}, []);
tableOptionQuery.refetch();
}, [cacheKey, url, props.params, props.enableColumnCaching]);
const enableSelection: boolean = useMemo(() => { const enableSelection: boolean = useMemo(() => {
return tableProps.enableSelection || tableProps.enableBulkDelete || false; return tableProps.enableSelection || tableProps.enableBulkDelete || false;
@ -555,54 +551,24 @@ export function InvenTreeTable<T extends Record<string, any>>({
return api return api
.get(url, { .get(url, {
params: queryParams, params: queryParams,
timeout: 5 * 1000 timeout: 10 * 1000
}) })
.then((response) => { .then((response) => {
switch (response.status) { let results = response.data?.results ?? response.data ?? [];
case 200:
setMissingRecordsText(
tableProps.noRecordsText ?? t`No records found`
);
let results = response.data?.results ?? response.data ?? []; if (props.dataFormatter) {
// Custom data formatter provided
if (props.dataFormatter) { results = props.dataFormatter(results);
// Custom data formatter provided
results = props.dataFormatter(results);
}
if (!Array.isArray(results)) {
setMissingRecordsText(t`Server returned incorrect data type`);
results = [];
}
tableState.setRecordCount(response.data?.count ?? results.length);
return results;
case 400:
setMissingRecordsText(t`Bad request`);
break;
case 401:
setMissingRecordsText(t`Unauthorized`);
break;
case 403:
setMissingRecordsText(t`Forbidden`);
break;
case 404:
setMissingRecordsText(t`Not found`);
break;
default:
setMissingRecordsText(
`${t`Unknown error`}: ${response.statusText}`
);
break;
} }
return []; if (!Array.isArray(results)) {
}) setMissingRecordsText(t`Server returned incorrect data type`);
.catch((error) => { results = [];
setMissingRecordsText(`${t`Error`}: ${error.message}`); }
return [];
tableState.setRecordCount(response.data?.count ?? results.length);
return results;
}); });
}; };
@ -626,9 +592,18 @@ export function InvenTreeTable<T extends Record<string, any>>({
tableState.storedDataLoaded, tableState.storedDataLoaded,
tableState.searchTerm tableState.searchTerm
], ],
retry: 5,
retryDelay: (attempt: number) => (1 + attempt) * 250,
throwOnError: (error: any) => {
showApiErrorMessage({
error: error,
title: t`Error loading table data`
});
return true;
},
enabled: !!url && !tableData && tableState.storedDataLoaded, enabled: !!url && !tableData && tableState.storedDataLoaded,
queryFn: fetchTableData, queryFn: fetchTableData
refetchOnMount: true
}); });
// Refetch data when the query parameters change // Refetch data when the query parameters change

View File

@ -53,8 +53,7 @@ export default function BuildOrderTestTable({
required: true required: true
} }
}) })
.then((res) => res.data) .then((res) => res.data);
.catch((err) => []);
} }
}); });

View File

@ -160,8 +160,7 @@ export default function BuildOutputTable({
required: true required: true
} }
}) })
.then((response) => response.data) .then((response) => response.data);
.catch(() => []);
} }
}); });
@ -184,8 +183,7 @@ export default function BuildOutputTable({
tracked: true tracked: true
} }
}) })
.then((response) => response.data) .then((response) => response.data);
.catch(() => []);
} }
}); });

View File

@ -113,8 +113,7 @@ export default function ParametricPartTable({
category: categoryId category: categoryId
} }
}) })
.then((response) => response.data) .then((response) => response.data);
.catch((_error) => []);
}, },
refetchOnMount: true refetchOnMount: true
}); });
@ -290,7 +289,7 @@ export default function ParametricPartTable({
); );
const parameterColumns: TableColumn[] = useMemo(() => { const parameterColumns: TableColumn[] = useMemo(() => {
const data = categoryParameters.data ?? []; const data = categoryParameters?.data || [];
return data.map((template: any) => { return data.map((template: any) => {
let title = template.name; let title = template.name;

View File

@ -137,6 +137,11 @@ export function PartThumbTable({ pk, setImage }: Readonly<ThumbTableProps>) {
// Fetch thumbnails from API // Fetch thumbnails from API
const thumbQuery = useQuery({ const thumbQuery = useQuery({
queryKey: [ApiEndpoints.part_thumbs_list, page, searchText], queryKey: [ApiEndpoints.part_thumbs_list, page, searchText],
throwOnError: (error: any) => {
setTotalPages(1);
setPage(1);
return true;
},
queryFn: async () => { queryFn: async () => {
const offset = Math.max(0, page - 1) * limit; const offset = Math.max(0, page - 1) * limit;
@ -152,11 +157,6 @@ export function PartThumbTable({ pk, setImage }: Readonly<ThumbTableProps>) {
const records = response?.data?.count ?? 1; const records = response?.data?.count ?? 1;
setTotalPages(Math.ceil(records / limit)); setTotalPages(Math.ceil(records / limit));
return response.data?.results ?? response.data; return response.data?.results ?? response.data;
})
.catch((error) => {
setTotalPages(1);
setPage(1);
return [];
}); });
} }
}); });
@ -172,7 +172,7 @@ export function PartThumbTable({ pk, setImage }: Readonly<ThumbTableProps>) {
spacing='xs' spacing='xs'
> >
{!thumbQuery.isFetching {!thumbQuery.isFetching
? thumbQuery?.data.map((data: ImageElement, index: number) => ( ? thumbQuery?.data?.map((data: ImageElement, index: number) => (
<PartThumbComponent <PartThumbComponent
element={data} element={data}
key={index} key={index}

View File

@ -72,8 +72,7 @@ export default function StockItemTestResultTable({
enabled: true enabled: true
} }
}) })
.then((response) => response.data) .then((response) => response.data);
.catch((_error) => []);
} }
}); });
@ -85,13 +84,14 @@ export default function StockItemTestResultTable({
const formatRecords = useCallback( const formatRecords = useCallback(
(records: any[]): any[] => { (records: any[]): any[] => {
// Construct a list of test templates // Construct a list of test templates
const results = testTemplates.map((template: any) => { const results =
return { testTemplates?.map((template: any) => {
...template, return {
templateId: template.pk, ...template,
results: [] templateId: template.pk,
}; results: []
}); };
}) ?? [];
// If any of the tests results point to templates which we do not have, add them in // If any of the tests results point to templates which we do not have, add them in
records.forEach((record) => { records.forEach((record) => {