From a8b805cdec353c1100f57b29b2fb1a7ed4fbb8c7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 29 Jun 2025 22:07:06 +1000 Subject: [PATCH] [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 --- .../components/buttons/PrintingActions.tsx | 3 - .../components/dashboard/DashboardLayout.tsx | 7 +- .../widgets/QueryCountDashboardWidget.tsx | 3 +- .../src/components/details/Details.tsx | 24 ++--- .../src/components/editors/NotesEditor.tsx | 6 +- src/frontend/src/components/forms/ApiForm.tsx | 19 ++-- .../forms/fields/RelatedModelField.tsx | 4 - .../src/components/modals/LicenseModal.tsx | 5 +- src/frontend/src/components/nav/Header.tsx | 20 ++-- .../src/components/nav/NavigationTree.tsx | 17 ++-- .../src/components/nav/NotificationDrawer.tsx | 5 +- .../src/components/nav/SearchDrawer.tsx | 6 +- .../src/components/render/Instance.tsx | 5 +- src/frontend/src/forms/StockForms.tsx | 7 +- src/frontend/src/functions/api.tsx | 7 +- src/frontend/src/functions/notifications.tsx | 7 +- src/frontend/src/hooks/UseCalendar.tsx | 16 +-- src/frontend/src/hooks/UseDashboardItems.tsx | 14 +-- src/frontend/src/hooks/UseDataExport.tsx | 3 - src/frontend/src/hooks/UseFilter.tsx | 3 +- src/frontend/src/hooks/UseGenerator.tsx | 15 ++- src/frontend/src/hooks/UsePluginPanels.tsx | 10 +- src/frontend/src/pages/part/PartDetail.tsx | 3 +- src/frontend/src/pages/stock/StockDetail.tsx | 5 +- src/frontend/src/tables/InvenTreeTable.tsx | 97 +++++++------------ .../src/tables/build/BuildOrderTestTable.tsx | 3 +- .../src/tables/build/BuildOutputTable.tsx | 6 +- .../src/tables/part/ParametricPartTable.tsx | 5 +- .../src/tables/part/PartThumbTable.tsx | 12 +-- .../tables/stock/StockItemTestResultTable.tsx | 18 ++-- 30 files changed, 144 insertions(+), 211 deletions(-) diff --git a/src/frontend/src/components/buttons/PrintingActions.tsx b/src/frontend/src/components/buttons/PrintingActions.tsx index 1dede788a4..68e0e432c6 100644 --- a/src/frontend/src/components/buttons/PrintingActions.tsx +++ b/src/frontend/src/components/buttons/PrintingActions.tsx @@ -72,9 +72,6 @@ export function PrintingActions({ .then((response: any) => { return extractAvailableFields(response, 'POST') || {}; }) - .catch(() => { - return {}; - }) }); const labelFields: ApiFormFieldSet = useMemo(() => { diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index aa031fa081..0a0d02aa3c 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -1,7 +1,7 @@ import { t } from '@lingui/core/macro'; import { Alert, Card, Center, Divider, Loader, Text } from '@mantine/core'; 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 { type Layout, Responsive, WidthProvider } from 'react-grid-layout'; @@ -220,6 +220,11 @@ export default function DashboardLayout() { removing={removing} /> + {availableWidgets.error && ( + }> + {t`Failed to load dashboard widgets.`} + + )} {layouts && loaded && availableWidgets.loaded ? ( <> {widgetLabels.length == 0 ? ( diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index 839ecc3a6d..3db70f530b 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -47,8 +47,7 @@ function QueryCountWidget({ limit: 1 } }) - .then((res) => res.data) - .catch(() => {}); + .then((res) => res.data); } }); diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index 546d4b87a0..4866f8c773 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -196,19 +196,14 @@ function NameBadge({ const url = apiUrl(path, pk); - return api - .get(url) - .then((response) => { - switch (response.status) { - case 200: - return response.data; - default: - return {}; - } - }) - .catch(() => { - return {}; - }); + return api.get(url).then((response) => { + switch (response.status) { + case 200: + return response.data; + default: + return {}; + } + }); } }); @@ -356,9 +351,6 @@ function TableAnchorValue(props: Readonly) { default: return {}; } - }) - .catch(() => { - return {}; }); } }); diff --git a/src/frontend/src/components/editors/NotesEditor.tsx b/src/frontend/src/components/editors/NotesEditor.tsx index 418ca065d7..667cdd1ae5 100644 --- a/src/frontend/src/components/editors/NotesEditor.tsx +++ b/src/frontend/src/components/editors/NotesEditor.tsx @@ -92,11 +92,9 @@ export default function NotesEditor({ const dataQuery = useQuery({ queryKey: ['notes-editor', noteUrl, modelType, modelId], + retry: 5, queryFn: () => - api - .get(noteUrl) - .then((response) => response.data?.notes ?? '') - .catch(() => ''), + api.get(noteUrl).then((response) => response.data?.notes ?? ''), enabled: true }); diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 01d409a2cf..3efe39057e 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -254,19 +254,14 @@ export function ApiForm({ props.pathParams ], queryFn: async () => { - return await api - .get(url) - .then((response: any) => { - // Process API response - const fetchedData: any = processFields(fields, response.data); + return await api.get(url).then((response: any) => { + // Process API response + const fetchedData: any = processFields(fields, response.data); - // Update form values, but only for the fields specified for this form - form.reset(fetchedData); - return fetchedData; - }) - .catch(() => { - return {}; - }); + // Update form values, but only for the fields specified for this form + form.reset(fetchedData); + return fetchedData; + }); } }); diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx index 1b77da0d89..1fb18c0b31 100644 --- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx +++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx @@ -179,10 +179,6 @@ export function RelatedModelField({ setData(values); dataRef.current = values; return response; - }) - .catch((error) => { - setData([]); - return error; }); } }); diff --git a/src/frontend/src/components/modals/LicenseModal.tsx b/src/frontend/src/components/modals/LicenseModal.tsx index cdd1d86a62..1625178138 100644 --- a/src/frontend/src/components/modals/LicenseModal.tsx +++ b/src/frontend/src/components/modals/LicenseModal.tsx @@ -57,10 +57,7 @@ export function LicenseModal() { queryKey: ['license'], refetchOnMount: true, queryFn: () => - api - .get(apiUrl(ApiEndpoints.license)) - .then((res) => res.data ?? {}) - .catch(() => {}) + api.get(apiUrl(ApiEndpoints.license)).then((res) => res.data ?? {}) }); const packageKeys = useMemo(() => { diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index d843b3ffc5..4e08ca07a6 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -78,23 +78,17 @@ export function Header() { return null; } - try { - const params = { + return api + .get(apiUrl(ApiEndpoints.notifications_list), { params: { read: false, limit: 1 } - }; - const response = await api - .get(apiUrl(ApiEndpoints.notifications_list), params) - .catch(() => { - return null; - }); - setNotificationCount(response?.data?.count ?? 0); - return response?.data ?? null; - } catch (error) { - return null; - } + }) + .then((response: any) => { + setNotificationCount(response?.data?.count ?? 0); + return response.data ?? null; + }); }, // Refetch every minute, *if* the tab is visible refetchInterval: 60 * 1000, diff --git a/src/frontend/src/components/nav/NavigationTree.tsx b/src/frontend/src/components/nav/NavigationTree.tsx index 1840d8ca6a..b3ff275197 100644 --- a/src/frontend/src/components/nav/NavigationTree.tsx +++ b/src/frontend/src/components/nav/NavigationTree.tsx @@ -1,5 +1,6 @@ import { ActionIcon, + Alert, Anchor, Divider, Drawer, @@ -15,6 +16,7 @@ import { import { IconChevronDown, IconChevronRight, + IconExclamationCircle, IconSitemap } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; @@ -29,6 +31,7 @@ import { getDetailUrl, navigateToLink } from '@lib/functions/Navigation'; +import { t } from '@lingui/core/macro'; import { useApi } from '../../contexts/ApiContext'; import { ApiIcon } from '../items/ApiIcon'; import { StylishText } from '../items/StylishText'; @@ -67,10 +70,6 @@ export default function NavigationTree({ } }) .then((response) => response.data ?? []) - .catch((error) => { - console.error(`Error fetching ${modelType} tree`); - return []; - }) }); const follow = useCallback( @@ -96,7 +95,7 @@ export default function NavigationTree({ const nodes: Record = {}; const tree: TreeNodeData[] = []; - if (!query?.data?.length) { + if (!query || !query?.data?.length) { return []; } @@ -207,7 +206,13 @@ export default function NavigationTree({ - + {query.isError ? ( + }> + {t`Error loading navigation tree.`} + + ) : ( + + )} ); diff --git a/src/frontend/src/components/nav/NotificationDrawer.tsx b/src/frontend/src/components/nav/NotificationDrawer.tsx index 33b022c933..ee5901c428 100644 --- a/src/frontend/src/components/nav/NotificationDrawer.tsx +++ b/src/frontend/src/components/nav/NotificationDrawer.tsx @@ -122,10 +122,7 @@ export function NotificationDrawer({ ordering: '-creation' } }) - .then((response) => response.data) - .catch((error) => { - return error; - }), + .then((response) => response.data), refetchOnMount: false }); diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index 3c4198b93e..c3ecf0f5d8 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -397,11 +397,7 @@ export function SearchDrawer({ return api .post(apiUrl(ApiEndpoints.api_search), params) - .then((response) => response.data) - .catch((error) => { - console.error(error); - return []; - }); + .then((response) => response.data); }; // Search query manager diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 006256fc6f..50b4e2f2e9 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -137,10 +137,7 @@ export function RenderRemoteInstance({ queryFn: async () => { const url = apiUrl(ModelInformationDict[model].api_endpoint, pk); - return api - .get(url) - .then((response) => response.data) - .catch(() => null); + return api.get(url).then((response) => response.data); } }); diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index dad409597d..cd393e61af 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -1100,10 +1100,7 @@ function useStockOperationModal({ .get(url, { params: params }) - .then((response) => response.data ?? []) - .catch(() => { - return []; - }); + .then((response) => response.data ?? []); } }); @@ -1368,7 +1365,7 @@ export function useFindSerialNumberForm({ } }, checkClose: (data, form) => { - if (data.length == 0) { + if (!data || data?.length == 0) { form.setError('serial', { message: t`No matching items` }); return false; } diff --git a/src/frontend/src/functions/api.tsx b/src/frontend/src/functions/api.tsx index 7aea6e862d..7fb2a14da1 100644 --- a/src/frontend/src/functions/api.tsx +++ b/src/frontend/src/functions/api.tsx @@ -15,7 +15,7 @@ export function extractErrorMessage({ field?: string; defaultMessage?: string; }): string { - const error_data = error.response?.data ?? null; + const error_data = error.response?.data ?? error.data ?? null; let message = ''; @@ -25,7 +25,7 @@ export function extractErrorMessage({ // No message? Look at the response status codes if (!message) { - const status = error.response?.status ?? null; + const status = error.status ?? error.response?.status ?? null; if (status) { switch (status) { @@ -48,8 +48,11 @@ export function extractErrorMessage({ message = t`Internal server error`; break; default: + message = t`Unknown error`; break; } + + message = `${status} - ${message}`; } } diff --git a/src/frontend/src/functions/notifications.tsx b/src/frontend/src/functions/notifications.tsx index 4e471e61cc..90fc97ba68 100644 --- a/src/frontend/src/functions/notifications.tsx +++ b/src/frontend/src/functions/notifications.tsx @@ -80,12 +80,14 @@ export function showApiErrorMessage({ error, title, message, - field + field, + id }: { error: any; title: string; message?: string; field?: string; + id?: string; }) { const errorMessage = extractErrorMessage({ error: error, @@ -93,7 +95,10 @@ export function showApiErrorMessage({ defaultMessage: message }); + notifications.hide(id ?? 'api-error'); + notifications.show({ + id: id ?? 'api-error', title: title, message: errorMessage, color: 'red' diff --git a/src/frontend/src/hooks/UseCalendar.tsx b/src/frontend/src/hooks/UseCalendar.tsx index 7335ea28cc..f7867eb0e3 100644 --- a/src/frontend/src/hooks/UseCalendar.tsx +++ b/src/frontend/src/hooks/UseCalendar.tsx @@ -103,6 +103,14 @@ export default function useCalendar({ const query = useQuery({ enabled: !!startDate && !!endDate, queryKey: ['calendar', name, endpoint, queryFilters], + throwOnError: (error: any) => { + showApiErrorMessage({ + error: error, + title: 'Error fetching calendar data' + }); + + return true; + }, queryFn: async () => { // Fetch data from the API return api @@ -111,12 +119,6 @@ export default function useCalendar({ }) .then((response) => { return response.data ?? []; - }) - .catch((error) => { - showApiErrorMessage({ - error: error, - title: 'Error fetching calendar data' - }); }); } }); @@ -173,6 +175,6 @@ export default function useCalendar({ setEndDate, exportModal, query: query, - data: query.data + data: query.data ?? [] }; } diff --git a/src/frontend/src/hooks/UseDashboardItems.tsx b/src/frontend/src/hooks/UseDashboardItems.tsx index dae1fcb9e0..f7f7e34c94 100644 --- a/src/frontend/src/hooks/UseDashboardItems.tsx +++ b/src/frontend/src/hooks/UseDashboardItems.tsx @@ -19,6 +19,7 @@ import { useUserState } from '../states/UserState'; interface DashboardLibraryProps { items: DashboardWidgetProps[]; loaded: boolean; + error: any; } /** @@ -51,13 +52,7 @@ export function useDashboardItems(): DashboardLibraryProps { feature_type: PluginUIFeatureType.dashboard }); - return api - .get(url) - .then((response: any) => response.data) - .catch((_error: any) => { - console.error('ERR: Failed to fetch plugin dashboard items'); - return []; - }); + return api.get(url).then((response: any) => response.data); } }); @@ -90,7 +85,7 @@ export function useDashboardItems(): DashboardLibraryProps { }; }) ?? [] ); - }, [pluginQuery, inventreeContext]); + }, [pluginQuery.data, inventreeContext]); const items: DashboardWidgetProps[] = useMemo(() => { const widgets = [...builtin, ...pluginDashboardItems]; @@ -113,6 +108,7 @@ export function useDashboardItems(): DashboardLibraryProps { return { items: items, - loaded: loaded + loaded: loaded, + error: pluginQuery.error }; } diff --git a/src/frontend/src/hooks/UseDataExport.tsx b/src/frontend/src/hooks/UseDataExport.tsx index 18465b5215..653a79beb3 100644 --- a/src/frontend/src/hooks/UseDataExport.tsx +++ b/src/frontend/src/hooks/UseDataExport.tsx @@ -72,9 +72,6 @@ export default function useDataExport({ .then((response: any) => { return extractAvailableFields(response, 'GET') || {}; }) - .catch(() => { - return {}; - }) }); // Construct a field set for the export form diff --git a/src/frontend/src/hooks/UseFilter.tsx b/src/frontend/src/hooks/UseFilter.tsx index 31ad5ed244..b4b5693f74 100644 --- a/src/frontend/src/hooks/UseFilter.tsx +++ b/src/frontend/src/hooks/UseFilter.tsx @@ -39,8 +39,7 @@ export function useFilters(props: UseFilterProps) { } return data; - }) - .catch((error) => []); + }); } }); diff --git a/src/frontend/src/hooks/UseGenerator.tsx b/src/frontend/src/hooks/UseGenerator.tsx index c22da48931..3bfa6451f7 100644 --- a/src/frontend/src/hooks/UseGenerator.tsx +++ b/src/frontend/src/hooks/UseGenerator.tsx @@ -85,6 +85,13 @@ export function useGenerator(props: GeneratorProps): GeneratorState { ], refetchOnMount: false, refetchOnWindowFocus: false, + throwOnError: (error: any) => { + console.error( + `Error generating ${props.key} @ ${props.endpoint}:`, + error + ); + return false; + }, queryFn: async () => { const generatorQuery = { ...(props.initialQuery ?? {}), @@ -105,14 +112,6 @@ export function useGenerator(props: GeneratorProps): GeneratorState { props.onGenerate?.(value); return response; - }) - .catch((error) => { - console.error( - `Error generating ${props.key} @ ${props.endpoint}:`, - error - ); - - return null; }); } }); diff --git a/src/frontend/src/hooks/UsePluginPanels.tsx b/src/frontend/src/hooks/UsePluginPanels.tsx index 6418fcd1aa..2dcc367e10 100644 --- a/src/frontend/src/hooks/UsePluginPanels.tsx +++ b/src/frontend/src/hooks/UsePluginPanels.tsx @@ -53,6 +53,10 @@ export function usePluginPanels({ const pluginQuery = useQuery({ enabled: pluginPanelsEnabled && !!model && id !== undefined, queryKey: ['custom-plugin-panels', model, id], + throwOnError: (error: any) => { + console.error('ERR: Failed to fetch plugin panels'); + return false; + }, queryFn: async () => { if (!pluginPanelsEnabled || !model) { return Promise.resolve([]); @@ -69,11 +73,7 @@ export function usePluginPanels({ target_id: id } }) - .then((response: any) => response.data) - .catch((_error: any) => { - console.error('ERR: Failed to fetch plugin panels'); - return []; - }); + .then((response: any) => response.data); } }); diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index b575278130..4b4d7363bd 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -723,8 +723,7 @@ export default function PartDetail() { default: break; } - }) - .catch(() => {}); + }); return revisions; } diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 49bab78ad9..56274de468 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -491,9 +491,6 @@ export default function StockDetail() { } else { return null; } - }) - .catch(() => { - return null; }); } }); @@ -504,7 +501,7 @@ export default function StockDetail() { return true; } - if (trackedBomItemQuery.data != null) { + if (!!trackedBomItemQuery.data) { return trackedBomItemQuery.data; } diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 10fc744344..a89259c45e 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -28,12 +28,12 @@ import { navigateToLink } from '@lib/functions/Navigation'; import type { TableFilter } from '@lib/types/Filters'; import type { ApiFormFieldSet } from '@lib/types/Forms'; import type { TableState } from '@lib/types/Tables'; -import { hideNotification, showNotification } from '@mantine/notifications'; import { IconArrowRight } from '@tabler/icons-react'; import { Boundary } from '../components/Boundary'; import { useApi } from '../contexts/ApiContext'; import { resolveItem } from '../functions/conversion'; import { extractAvailableFields, mapFields } from '../functions/forms'; +import { showApiErrorMessage } from '../functions/notifications'; import { useLocalState } from '../states/LocalState'; import type { TableColumn } from './Column'; import InvenTreeTableHeader from './InvenTreeTableHeader'; @@ -199,7 +199,16 @@ export function InvenTreeTable>({ tableProps.params, 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, gcTime: 5000, queryFn: async () => { @@ -240,17 +249,6 @@ export function InvenTreeTable>({ 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; }); } @@ -270,9 +268,7 @@ export function InvenTreeTable>({ setFieldNames(cachedNames); return; } - - tableOptionQuery.refetch(); - }, [cacheKey, url, props.params, props.enableColumnCaching]); + }, []); const enableSelection: boolean = useMemo(() => { return tableProps.enableSelection || tableProps.enableBulkDelete || false; @@ -555,54 +551,24 @@ export function InvenTreeTable>({ return api .get(url, { params: queryParams, - timeout: 5 * 1000 + timeout: 10 * 1000 }) .then((response) => { - switch (response.status) { - case 200: - setMissingRecordsText( - tableProps.noRecordsText ?? t`No records found` - ); + let results = response.data?.results ?? response.data ?? []; - let results = response.data?.results ?? response.data ?? []; - - if (props.dataFormatter) { - // 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; + if (props.dataFormatter) { + // Custom data formatter provided + results = props.dataFormatter(results); } - return []; - }) - .catch((error) => { - setMissingRecordsText(`${t`Error`}: ${error.message}`); - return []; + if (!Array.isArray(results)) { + setMissingRecordsText(t`Server returned incorrect data type`); + results = []; + } + + tableState.setRecordCount(response.data?.count ?? results.length); + + return results; }); }; @@ -626,9 +592,18 @@ export function InvenTreeTable>({ tableState.storedDataLoaded, 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, - queryFn: fetchTableData, - refetchOnMount: true + queryFn: fetchTableData }); // Refetch data when the query parameters change diff --git a/src/frontend/src/tables/build/BuildOrderTestTable.tsx b/src/frontend/src/tables/build/BuildOrderTestTable.tsx index dbfa6ac688..626e713f2c 100644 --- a/src/frontend/src/tables/build/BuildOrderTestTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTestTable.tsx @@ -53,8 +53,7 @@ export default function BuildOrderTestTable({ required: true } }) - .then((res) => res.data) - .catch((err) => []); + .then((res) => res.data); } }); diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index 59aadfe8e4..8b4fd05a84 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -160,8 +160,7 @@ export default function BuildOutputTable({ required: true } }) - .then((response) => response.data) - .catch(() => []); + .then((response) => response.data); } }); @@ -184,8 +183,7 @@ export default function BuildOutputTable({ tracked: true } }) - .then((response) => response.data) - .catch(() => []); + .then((response) => response.data); } }); diff --git a/src/frontend/src/tables/part/ParametricPartTable.tsx b/src/frontend/src/tables/part/ParametricPartTable.tsx index 6269010352..01e0f1321f 100644 --- a/src/frontend/src/tables/part/ParametricPartTable.tsx +++ b/src/frontend/src/tables/part/ParametricPartTable.tsx @@ -113,8 +113,7 @@ export default function ParametricPartTable({ category: categoryId } }) - .then((response) => response.data) - .catch((_error) => []); + .then((response) => response.data); }, refetchOnMount: true }); @@ -290,7 +289,7 @@ export default function ParametricPartTable({ ); const parameterColumns: TableColumn[] = useMemo(() => { - const data = categoryParameters.data ?? []; + const data = categoryParameters?.data || []; return data.map((template: any) => { let title = template.name; diff --git a/src/frontend/src/tables/part/PartThumbTable.tsx b/src/frontend/src/tables/part/PartThumbTable.tsx index 78bdfc657b..f69d8ac8a5 100644 --- a/src/frontend/src/tables/part/PartThumbTable.tsx +++ b/src/frontend/src/tables/part/PartThumbTable.tsx @@ -137,6 +137,11 @@ export function PartThumbTable({ pk, setImage }: Readonly) { // Fetch thumbnails from API const thumbQuery = useQuery({ queryKey: [ApiEndpoints.part_thumbs_list, page, searchText], + throwOnError: (error: any) => { + setTotalPages(1); + setPage(1); + return true; + }, queryFn: async () => { const offset = Math.max(0, page - 1) * limit; @@ -152,11 +157,6 @@ export function PartThumbTable({ pk, setImage }: Readonly) { const records = response?.data?.count ?? 1; setTotalPages(Math.ceil(records / limit)); return response.data?.results ?? response.data; - }) - .catch((error) => { - setTotalPages(1); - setPage(1); - return []; }); } }); @@ -172,7 +172,7 @@ export function PartThumbTable({ pk, setImage }: Readonly) { spacing='xs' > {!thumbQuery.isFetching - ? thumbQuery?.data.map((data: ImageElement, index: number) => ( + ? thumbQuery?.data?.map((data: ImageElement, index: number) => ( response.data) - .catch((_error) => []); + .then((response) => response.data); } }); @@ -85,13 +84,14 @@ export default function StockItemTestResultTable({ const formatRecords = useCallback( (records: any[]): any[] => { // Construct a list of test templates - const results = testTemplates.map((template: any) => { - return { - ...template, - templateId: template.pk, - results: [] - }; - }); + const results = + testTemplates?.map((template: any) => { + return { + ...template, + templateId: template.pk, + results: [] + }; + }) ?? []; // If any of the tests results point to templates which we do not have, add them in records.forEach((record) => {