2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00: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) => {
return extractAvailableFields(response, 'POST') || {};
})
.catch(() => {
return {};
})
});
const labelFields: ApiFormFieldSet = useMemo(() => {

View File

@ -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}
/>
<Divider p='xs' />
{availableWidgets.error && (
<Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
{t`Failed to load dashboard widgets.`}
</Alert>
)}
{layouts && loaded && availableWidgets.loaded ? (
<>
{widgetLabels.length == 0 ? (

View File

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

View File

@ -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<FieldProps>) {
default:
return {};
}
})
.catch(() => {
return {};
});
}
});

View File

@ -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
});

View File

@ -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;
});
}
});

View File

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

View File

@ -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(() => {

View File

@ -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,

View File

@ -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<number, any> = {};
const tree: TreeNodeData[] = [];
if (!query?.data?.length) {
if (!query || !query?.data?.length) {
return [];
}
@ -207,7 +206,13 @@ export default function NavigationTree({
<Stack gap='xs'>
<Divider />
<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>
</Drawer>
);

View File

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

View File

@ -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

View File

@ -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);
}
});

View File

@ -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;
}

View File

@ -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}`;
}
}

View File

@ -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'

View File

@ -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 ?? []
};
}

View File

@ -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
};
}

View File

@ -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

View File

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

View File

@ -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;
});
}
});

View File

@ -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);
}
});

View File

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

View File

@ -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;
}

View File

@ -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<T extends Record<string, any>>({
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<T extends Record<string, any>>({
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<T extends Record<string, any>>({
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<T extends Record<string, any>>({
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<T extends Record<string, any>>({
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

View File

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

View File

@ -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);
}
});

View File

@ -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;

View File

@ -137,6 +137,11 @@ export function PartThumbTable({ pk, setImage }: Readonly<ThumbTableProps>) {
// 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<ThumbTableProps>) {
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<ThumbTableProps>) {
spacing='xs'
>
{!thumbQuery.isFetching
? thumbQuery?.data.map((data: ImageElement, index: number) => (
? thumbQuery?.data?.map((data: ImageElement, index: number) => (
<PartThumbComponent
element={data}
key={index}

View File

@ -72,8 +72,7 @@ export default function StockItemTestResultTable({
enabled: true
}
})
.then((response) => 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) => {