2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-05 13:10:57 +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 { UserRoles } from '@lib/enums/Roles';
import type { UseQueryResult } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useUserState } from '../../states/UserState';
import ClientError from '../errors/ClientError';
import PermissionDenied from '../errors/PermissionDenied';
import ServerError from '../errors/ServerError';
export default function InstanceDetail({
status,
loading,
query,
children,
requiredRole,
requiredPermission
}: Readonly<{
status: number;
loading: boolean;
query: UseQueryResult;
children: React.ReactNode;
requiredRole?: UserRoles;
requiredPermission?: ModelType;
}>) {
const user = useUserState();
if (loading || !user.isLoggedIn()) {
return <LoadingOverlay />;
}
const [loaded, setLoaded] = useState<boolean>(false);
if (status >= 500) {
return <ServerError status={status} />;
}
useEffect(() => {
if (query.isSuccess) {
setLoaded(true);
}
}, [query.isSuccess]);
if (status >= 400) {
return <ClientError status={status} />;
if (query.isError) {
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)) {
@ -42,5 +49,16 @@ export default function InstanceDetail({
return <PermissionDenied />;
}
if (!loaded || !user.isLoggedIn()) {
// Return a loader for the first page load
return (
<Center>
<Container>
<Loader />
</Container>
</Center>
);
}
return <>{children}</>;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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