2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-05 21:20:56 +00:00

[UI] Reduce flicker when reloading detail instance (#9926)

* UI improvement for column selection in tables

- Limit max height of dropdown

* Allow retry for instance query

* Prevent flickering when reloading instance

- Don't hide the children
- Just put a loading overlay on top

* Enhanced rendering for <InstanceDetail>

* Refactor other pages

* remove unused attributes
This commit is contained in:
Oliver
2025-07-01 23:03:45 +10:00
committed by GitHub
parent 0683140278
commit e693c93c08
19 changed files with 69 additions and 93 deletions

View File

@ -1,37 +1,44 @@
import { LoadingOverlay } from '@mantine/core'; import { Center, Container, Loader } from '@mantine/core';
import type { ModelType } from '@lib/enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
import type { UserRoles } from '@lib/enums/Roles'; import type { UserRoles } from '@lib/enums/Roles';
import type { UseQueryResult } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import ClientError from '../errors/ClientError'; import ClientError from '../errors/ClientError';
import PermissionDenied from '../errors/PermissionDenied'; import PermissionDenied from '../errors/PermissionDenied';
import ServerError from '../errors/ServerError'; import ServerError from '../errors/ServerError';
export default function InstanceDetail({ export default function InstanceDetail({
status, query,
loading,
children, children,
requiredRole, requiredRole,
requiredPermission requiredPermission
}: Readonly<{ }: Readonly<{
status: number; query: UseQueryResult;
loading: boolean;
children: React.ReactNode; children: React.ReactNode;
requiredRole?: UserRoles; requiredRole?: UserRoles;
requiredPermission?: ModelType; requiredPermission?: ModelType;
}>) { }>) {
const user = useUserState(); const user = useUserState();
if (loading || !user.isLoggedIn()) { const [loaded, setLoaded] = useState<boolean>(false);
return <LoadingOverlay />;
}
if (status >= 500) { useEffect(() => {
return <ServerError status={status} />; if (query.isSuccess) {
} setLoaded(true);
}
}, [query.isSuccess]);
if (status >= 400) { if (query.isError) {
return <ClientError status={status} />; const reason = query.failureReason as any;
const statusCode = reason?.response?.status ?? reason?.status ?? 0;
if (statusCode >= 500) {
return <ServerError status={statusCode} />;
}
return <ClientError status={statusCode} />;
} }
if (requiredRole && !user.hasViewRole(requiredRole)) { if (requiredRole && !user.hasViewRole(requiredRole)) {
@ -42,5 +49,16 @@ export default function InstanceDetail({
return <PermissionDenied />; return <PermissionDenied />;
} }
if (!loaded || !user.isLoggedIn()) {
// Return a loader for the first page load
return (
<Center>
<Container>
<Loader />
</Container>
</Center>
);
}
return <>{children}</>; return <>{children}</>;
} }

View File

@ -12,7 +12,6 @@ export interface UseInstanceResult {
refreshInstance: () => void; refreshInstance: () => void;
refreshInstancePromise: () => Promise<QueryObserverResult<any, any>>; refreshInstancePromise: () => Promise<QueryObserverResult<any, any>>;
instanceQuery: any; instanceQuery: any;
requestStatus: number;
isLoaded: boolean; isLoaded: boolean;
} }
@ -35,7 +34,6 @@ export function useInstance<T = any>({
hasPrimaryKey = true, hasPrimaryKey = true,
refetchOnMount = true, refetchOnMount = true,
refetchOnWindowFocus = false, refetchOnWindowFocus = false,
throwError = false,
updateInterval updateInterval
}: { }: {
endpoint: ApiEndpoints; endpoint: ApiEndpoints;
@ -46,15 +44,12 @@ export function useInstance<T = any>({
defaultValue?: any; defaultValue?: any;
refetchOnMount?: boolean; refetchOnMount?: boolean;
refetchOnWindowFocus?: boolean; refetchOnWindowFocus?: boolean;
throwError?: boolean;
updateInterval?: number; updateInterval?: number;
}): UseInstanceResult { }): UseInstanceResult {
const api = useApi(); const api = useApi();
const [instance, setInstance] = useState<T | undefined>(defaultValue); const [instance, setInstance] = useState<T | undefined>(defaultValue);
const [requestStatus, setRequestStatus] = useState<number>(0);
const instanceQuery = useQuery<T>({ const instanceQuery = useQuery<T>({
queryKey: [ queryKey: [
'instance', 'instance',
@ -84,7 +79,6 @@ export function useInstance<T = any>({
params: params params: params
}) })
.then((response) => { .then((response) => {
setRequestStatus(response.status);
switch (response.status) { switch (response.status) {
case 200: case 200:
setInstance(response.data); setInstance(response.data);
@ -93,15 +87,6 @@ export function useInstance<T = any>({
setInstance(defaultValue); setInstance(defaultValue);
return defaultValue; return defaultValue;
} }
})
.catch((error) => {
setRequestStatus(error.response?.status || 0);
setInstance(defaultValue);
console.error(`ERR: Error fetching instance ${url}:`, error);
if (throwError) throw error;
return defaultValue;
}); });
}, },
refetchOnMount: refetchOnMount, refetchOnMount: refetchOnMount,
@ -131,7 +116,6 @@ export function useInstance<T = any>({
refreshInstance, refreshInstance,
refreshInstancePromise, refreshInstancePromise,
instanceQuery, instanceQuery,
requestStatus,
isLoaded isLoaded
}; };
} }

View File

@ -75,8 +75,7 @@ export default function BuildDetail() {
const { const {
instance: build, instance: build,
refreshInstance, refreshInstance,
instanceQuery, instanceQuery
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.build_order_list, endpoint: ApiEndpoints.build_order_list,
pk: id, pk: id,
@ -621,11 +620,7 @@ export default function BuildDetail() {
{holdOrder.modal} {holdOrder.modal}
{issueOrder.modal} {issueOrder.modal}
{completeOrder.modal} {completeOrder.modal}
<InstanceDetail <InstanceDetail query={instanceQuery} requiredRole={UserRoles.build}>
status={requestStatus}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.build}
>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={`${t`Build Order`}: ${build.reference}`} title={`${t`Build Order`}: ${build.reference}`}

View File

@ -73,8 +73,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
const { const {
instance: company, instance: company,
refreshInstance, refreshInstance,
instanceQuery, instanceQuery
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.company_list, endpoint: ApiEndpoints.company_list,
pk: id, pk: id,
@ -326,7 +325,10 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
<> <>
{editCompany.modal} {editCompany.modal}
{deleteCompany.modal} {deleteCompany.modal}
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
query={instanceQuery}
requiredPermission={ModelType.company}
>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={`${t`Company`}: ${company.name}`} title={`${t`Company`}: ${company.name}`}

View File

@ -51,8 +51,7 @@ export default function ManufacturerPartDetail() {
const { const {
instance: manufacturerPart, instance: manufacturerPart,
instanceQuery, instanceQuery,
refreshInstance, refreshInstance
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.manufacturer_part_list, endpoint: ApiEndpoints.manufacturer_part_list,
pk: id, pk: id,
@ -273,7 +272,10 @@ export default function ManufacturerPartDetail() {
{deleteManufacturerPart.modal} {deleteManufacturerPart.modal}
{duplicateManufacturerPart.modal} {duplicateManufacturerPart.modal}
{editManufacturerPart.modal} {editManufacturerPart.modal}
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
query={instanceQuery}
requiredPermission={ModelType.manufacturerpart}
>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={t`ManufacturerPart`} title={t`ManufacturerPart`}

View File

@ -56,8 +56,7 @@ export default function SupplierPartDetail() {
const { const {
instance: supplierPart, instance: supplierPart,
instanceQuery, instanceQuery,
refreshInstance, refreshInstance
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.supplier_part_list, endpoint: ApiEndpoints.supplier_part_list,
pk: id, pk: id,
@ -401,8 +400,7 @@ export default function SupplierPartDetail() {
{duplicateSupplierPart.modal} {duplicateSupplierPart.modal}
{editSupplierPart.modal} {editSupplierPart.modal}
<InstanceDetail <InstanceDetail
status={requestStatus} query={instanceQuery}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.purchase_order} requiredRole={UserRoles.purchase_order}
> >
<Stack gap='xs'> <Stack gap='xs'>

View File

@ -26,7 +26,7 @@ import { useInstance } from '../../hooks/UseInstance';
export default function GroupDetail() { export default function GroupDetail() {
const { id } = useParams(); const { id } = useParams();
const { instance, instanceQuery, requestStatus } = useInstance({ const { instance, instanceQuery } = useInstance({
endpoint: ApiEndpoints.group_list, endpoint: ApiEndpoints.group_list,
pk: id pk: id
}); });
@ -72,7 +72,7 @@ export default function GroupDetail() {
}, [instance, id]); }, [instance, id]);
return ( return (
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail query={instanceQuery}>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={`${t`Group`}: ${instance.name}`} title={`${t`Group`}: ${instance.name}`}

View File

@ -29,7 +29,7 @@ export default function UserDetail() {
const user = useUserState(); const user = useUserState();
const settings = useGlobalSettingsState(); const settings = useGlobalSettingsState();
const { instance, instanceQuery, requestStatus } = useInstance({ const { instance, instanceQuery } = useInstance({
endpoint: ApiEndpoints.user_list, endpoint: ApiEndpoints.user_list,
pk: id pk: id
}); });
@ -214,7 +214,7 @@ export default function UserDetail() {
}, [instance, instanceQuery]); }, [instance, instanceQuery]);
return ( return (
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail query={instanceQuery}>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={`${t`User`}: ${instance.username}`} title={`${t`User`}: ${instance.username}`}

View File

@ -64,8 +64,7 @@ export default function CategoryDetail() {
const { const {
instance: category, instance: category,
refreshInstance, refreshInstance,
instanceQuery, instanceQuery
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.category_list, endpoint: ApiEndpoints.category_list,
hasPrimaryKey: true, hasPrimaryKey: true,
@ -325,8 +324,7 @@ export default function CategoryDetail() {
{editCategory.modal} {editCategory.modal}
{deleteCategory.modal} {deleteCategory.modal}
<InstanceDetail <InstanceDetail
status={requestStatus} query={instanceQuery}
loading={id ? instanceQuery.isFetching : false}
requiredRole={UserRoles.part_category} requiredRole={UserRoles.part_category}
> >
<Stack gap='xs'> <Stack gap='xs'>

View File

@ -130,8 +130,7 @@ export default function PartDetail() {
const { const {
instance: part, instance: part,
refreshInstance, refreshInstance,
instanceQuery, instanceQuery
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.part_list, endpoint: ApiEndpoints.part_list,
pk: id, pk: id,
@ -1028,11 +1027,7 @@ export default function PartDetail() {
{orderPartsWizard.wizard} {orderPartsWizard.wizard}
{findBySerialNumber.modal} {findBySerialNumber.modal}
{transferStockItems.modal} {transferStockItems.modal}
<InstanceDetail <InstanceDetail query={instanceQuery} requiredRole={UserRoles.part}>
status={requestStatus}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.part}
>
<Stack gap='xs'> <Stack gap='xs'>
{user.hasViewRole(UserRoles.part_category) && ( {user.hasViewRole(UserRoles.part_category) && (
<NavigationTree <NavigationTree

View File

@ -59,8 +59,7 @@ export default function PurchaseOrderDetail() {
const { const {
instance: order, instance: order,
instanceQuery, instanceQuery,
refreshInstance, refreshInstance
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.purchase_order_list, endpoint: ApiEndpoints.purchase_order_list,
pk: id, pk: id,
@ -514,8 +513,7 @@ export default function PurchaseOrderDetail() {
{editPurchaseOrder.modal} {editPurchaseOrder.modal}
{duplicatePurchaseOrder.modal} {duplicatePurchaseOrder.modal}
<InstanceDetail <InstanceDetail
status={requestStatus} query={instanceQuery}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.purchase_order} requiredRole={UserRoles.purchase_order}
> >
<Stack gap='xs'> <Stack gap='xs'>

View File

@ -59,8 +59,7 @@ export default function ReturnOrderDetail() {
const { const {
instance: order, instance: order,
instanceQuery, instanceQuery,
refreshInstance, refreshInstance
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.return_order_list, endpoint: ApiEndpoints.return_order_list,
pk: id, pk: id,
@ -499,8 +498,7 @@ export default function ReturnOrderDetail() {
{completeOrder.modal} {completeOrder.modal}
{duplicateReturnOrder.modal} {duplicateReturnOrder.modal}
<InstanceDetail <InstanceDetail
status={requestStatus} query={instanceQuery}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.return_order} requiredRole={UserRoles.return_order}
> >
<Stack gap='xs'> <Stack gap='xs'>

View File

@ -68,8 +68,7 @@ export default function SalesOrderDetail() {
const { const {
instance: order, instance: order,
instanceQuery, instanceQuery,
refreshInstance, refreshInstance
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.sales_order_list, endpoint: ApiEndpoints.sales_order_list,
pk: id, pk: id,
@ -563,8 +562,7 @@ export default function SalesOrderDetail() {
{editSalesOrder.modal} {editSalesOrder.modal}
{duplicateSalesOrder.modal} {duplicateSalesOrder.modal}
<InstanceDetail <InstanceDetail
status={requestStatus} query={instanceQuery}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.sales_order} requiredRole={UserRoles.sales_order}
> >
<Stack gap='xs'> <Stack gap='xs'>

View File

@ -52,8 +52,7 @@ export default function SalesOrderShipmentDetail() {
const { const {
instance: shipment, instance: shipment,
instanceQuery: shipmentQuery, instanceQuery: shipmentQuery,
refreshInstance: refreshShipment, refreshInstance: refreshShipment
requestStatus: shipmentStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.sales_order_shipment_list, endpoint: ApiEndpoints.sales_order_shipment_list,
pk: id, pk: id,
@ -65,8 +64,7 @@ export default function SalesOrderShipmentDetail() {
const { const {
instance: customer, instance: customer,
instanceQuery: customerQuery, instanceQuery: customerQuery,
refreshInstance: refreshCustomer, refreshInstance: refreshCustomer
requestStatus: customerStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.company_list, endpoint: ApiEndpoints.company_list,
pk: shipment.order_detail?.customer, pk: shipment.order_detail?.customer,
@ -351,8 +349,7 @@ export default function SalesOrderShipmentDetail() {
{editShipment.modal} {editShipment.modal}
{deleteShipment.modal} {deleteShipment.modal}
<InstanceDetail <InstanceDetail
status={shipmentStatus} query={shipmentQuery}
loading={shipmentQuery.isFetching || customerQuery.isFetching}
requiredRole={UserRoles.sales_order} requiredRole={UserRoles.sales_order}
> >
<Stack gap='xs'> <Stack gap='xs'>

View File

@ -64,8 +64,7 @@ export default function Stock() {
const { const {
instance: location, instance: location,
refreshInstance, refreshInstance,
instanceQuery, instanceQuery
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.stock_location_list, endpoint: ApiEndpoints.stock_location_list,
hasPrimaryKey: true, hasPrimaryKey: true,
@ -423,8 +422,7 @@ export default function Stock() {
{scanInStockItem.dialog} {scanInStockItem.dialog}
{scanInStockLocation.dialog} {scanInStockLocation.dialog}
<InstanceDetail <InstanceDetail
status={requestStatus} query={instanceQuery}
loading={id ? instanceQuery.isFetching : false}
requiredRole={UserRoles.stock_location} requiredRole={UserRoles.stock_location}
> >
<Stack> <Stack>

View File

@ -114,8 +114,7 @@ export default function StockDetail() {
instance: stockitem, instance: stockitem,
refreshInstance, refreshInstance,
refreshInstancePromise, refreshInstancePromise,
instanceQuery, instanceQuery
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.stock_item_list, endpoint: ApiEndpoints.stock_item_list,
pk: id, pk: id,
@ -1053,9 +1052,8 @@ export default function StockDetail() {
{findBySerialNumber.modal} {findBySerialNumber.modal}
{scanIntoLocation.dialog} {scanIntoLocation.dialog}
<InstanceDetail <InstanceDetail
requiredRole={UserRoles.stock} query={instanceQuery}
status={requestStatus} requiredPermission={ModelType.stockitem}
loading={instanceQuery.isFetching}
> >
<Stack> <Stack>
{user.hasViewRole(UserRoles.stock_location) && ( {user.hasViewRole(UserRoles.stock_location) && (

View File

@ -45,7 +45,6 @@ export function GroupDrawer({
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.group_list, endpoint: ApiEndpoints.group_list,
pk: id, pk: id,
throwError: true,
params: { params: {
permission_detail: true, permission_detail: true,
role_detail: true, role_detail: true,

View File

@ -80,8 +80,7 @@ export function TemplateDrawer({
} = useInstance<TemplateI>({ } = useInstance<TemplateI>({
endpoint: templateEndpoint, endpoint: templateEndpoint,
hasPrimaryKey: true, hasPrimaryKey: true,
pk: id, pk: id
throwError: true
}); });
// Editors // Editors

View File

@ -68,8 +68,7 @@ export function UserDrawer({
instanceQuery: { isFetching, error } instanceQuery: { isFetching, error }
} = useInstance<UserDetailI>({ } = useInstance<UserDetailI>({
endpoint: ApiEndpoints.user_list, endpoint: ApiEndpoints.user_list,
pk: id, pk: id
throwError: true
}); });
const currentUserPk = useUserState(useShallow((s) => s.user?.pk)); const currentUserPk = useUserState(useShallow((s) => s.user?.pk));