mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-02 03:30:54 +00:00
[UI] Query Improvements (#9791)
* Fix for stockOperationModal * Re-check when opened status changes * rename stockOperationModal -> useStockOperationModal * Fix enabled status of query * Add option to specify modalId * Track modal state when open / close * Prevent generators from running until forms are open * Prevent double loading of tables * Fix useQuery * Fix queryKey * Revert API change
This commit is contained in:
@ -174,6 +174,7 @@ export interface ApiFormProps {
|
|||||||
*/
|
*/
|
||||||
export interface ApiFormModalProps extends ApiFormProps {
|
export interface ApiFormModalProps extends ApiFormProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
modalId?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
cancelColor?: string;
|
cancelColor?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { UiSizeType } from './Core';
|
import type { UiSizeType } from './Core';
|
||||||
|
|
||||||
export interface UseModalProps {
|
export interface UseModalProps {
|
||||||
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
children: React.ReactElement;
|
children: React.ReactElement;
|
||||||
size?: UiSizeType;
|
size?: UiSizeType;
|
||||||
|
@ -155,7 +155,7 @@ export function TableField({
|
|||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{value.length > 0 ? (
|
{(value?.length ?? 0) > 0 ? (
|
||||||
value.map((item: any, idx: number) => {
|
value.map((item: any, idx: number) => {
|
||||||
return (
|
return (
|
||||||
<TableFieldRow
|
<TableFieldRow
|
||||||
|
@ -36,9 +36,11 @@ import { PartColumn } from '../tables/ColumnRenderers';
|
|||||||
* Field set for BuildOrder forms
|
* Field set for BuildOrder forms
|
||||||
*/
|
*/
|
||||||
export function useBuildOrderFields({
|
export function useBuildOrderFields({
|
||||||
create
|
create,
|
||||||
|
modalId
|
||||||
}: {
|
}: {
|
||||||
create: boolean;
|
create: boolean;
|
||||||
|
modalId: string;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
const [destination, setDestination] = useState<number | null | undefined>(
|
const [destination, setDestination] = useState<number | null | undefined>(
|
||||||
null
|
null
|
||||||
@ -47,6 +49,7 @@ export function useBuildOrderFields({
|
|||||||
const [batchCode, setBatchCode] = useState<string>('');
|
const [batchCode, setBatchCode] = useState<string>('');
|
||||||
|
|
||||||
const batchGenerator = useBatchCodeGenerator({
|
const batchGenerator = useBatchCodeGenerator({
|
||||||
|
modalId: modalId,
|
||||||
onGenerate: (value: any) => {
|
onGenerate: (value: any) => {
|
||||||
setBatchCode((batch: any) => batch || value);
|
setBatchCode((batch: any) => batch || value);
|
||||||
}
|
}
|
||||||
@ -152,9 +155,11 @@ export function useBuildOrderFields({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useBuildOrderOutputFields({
|
export function useBuildOrderOutputFields({
|
||||||
build
|
build,
|
||||||
|
modalId
|
||||||
}: {
|
}: {
|
||||||
build: any;
|
build: any;
|
||||||
|
modalId: string;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
const trackable: boolean = useMemo(() => {
|
const trackable: boolean = useMemo(() => {
|
||||||
return build.part_detail?.trackable ?? false;
|
return build.part_detail?.trackable ?? false;
|
||||||
@ -176,12 +181,14 @@ export function useBuildOrderOutputFields({
|
|||||||
}, [build]);
|
}, [build]);
|
||||||
|
|
||||||
const serialGenerator = useSerialNumberGenerator({
|
const serialGenerator = useSerialNumberGenerator({
|
||||||
|
modalId: modalId,
|
||||||
initialQuery: {
|
initialQuery: {
|
||||||
part: build.part || build.part_detail?.pk
|
part: build.part || build.part_detail?.pk
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const batchGenerator = useBatchCodeGenerator({
|
const batchGenerator = useBatchCodeGenerator({
|
||||||
|
modalId: modalId,
|
||||||
initialQuery: {
|
initialQuery: {
|
||||||
part: build.part || build.part_detail?.pk,
|
part: build.part || build.part_detail?.pk,
|
||||||
quantity: build.quantity
|
quantity: build.quantity
|
||||||
|
@ -296,6 +296,7 @@ function LineItemFormRow({
|
|||||||
|
|
||||||
// Batch code generator
|
// Batch code generator
|
||||||
const batchCodeGenerator = useBatchCodeGenerator({
|
const batchCodeGenerator = useBatchCodeGenerator({
|
||||||
|
isEnabled: () => batchOpen,
|
||||||
onGenerate: (value: any) => {
|
onGenerate: (value: any) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
props.changeFn(props.idx, 'batch_code', value);
|
props.changeFn(props.idx, 'batch_code', value);
|
||||||
@ -305,6 +306,7 @@ function LineItemFormRow({
|
|||||||
|
|
||||||
// Serial number generator
|
// Serial number generator
|
||||||
const serialNumberGenerator = useSerialNumberGenerator({
|
const serialNumberGenerator = useSerialNumberGenerator({
|
||||||
|
isEnabled: () => batchOpen && trackable,
|
||||||
onGenerate: (value: any) => {
|
onGenerate: (value: any) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
props.changeFn(props.idx, 'serial_numbers', value);
|
props.changeFn(props.idx, 'serial_numbers', value);
|
||||||
|
@ -65,10 +65,12 @@ import { StatusFilterOptions } from '../tables/Filter';
|
|||||||
export function useStockFields({
|
export function useStockFields({
|
||||||
partId,
|
partId,
|
||||||
stockItem,
|
stockItem,
|
||||||
|
modalId,
|
||||||
create = false
|
create = false
|
||||||
}: {
|
}: {
|
||||||
partId?: number;
|
partId?: number;
|
||||||
stockItem?: any;
|
stockItem?: any;
|
||||||
|
modalId: string;
|
||||||
create: boolean;
|
create: boolean;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
const globalSettings = useGlobalSettingsState();
|
const globalSettings = useGlobalSettingsState();
|
||||||
@ -81,12 +83,14 @@ export function useStockFields({
|
|||||||
const [expiryDate, setExpiryDate] = useState<string | null>(null);
|
const [expiryDate, setExpiryDate] = useState<string | null>(null);
|
||||||
|
|
||||||
const batchGenerator = useBatchCodeGenerator({
|
const batchGenerator = useBatchCodeGenerator({
|
||||||
|
modalId: modalId,
|
||||||
initialQuery: {
|
initialQuery: {
|
||||||
part: partInstance?.pk || partId
|
part: partInstance?.pk || partId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const serialGenerator = useSerialNumberGenerator({
|
const serialGenerator = useSerialNumberGenerator({
|
||||||
|
modalId: modalId,
|
||||||
initialQuery: {
|
initialQuery: {
|
||||||
part: partInstance?.pk || partId
|
part: partInstance?.pk || partId
|
||||||
}
|
}
|
||||||
@ -235,11 +239,15 @@ export function useStockFields({
|
|||||||
* Launch a form to create a new StockItem instance
|
* Launch a form to create a new StockItem instance
|
||||||
*/
|
*/
|
||||||
export function useCreateStockItem() {
|
export function useCreateStockItem() {
|
||||||
const fields = useStockFields({ create: true });
|
const fields = useStockFields({
|
||||||
|
create: true,
|
||||||
|
modalId: 'create-stock-item'
|
||||||
|
});
|
||||||
|
|
||||||
return useCreateApiFormModal({
|
return useCreateApiFormModal({
|
||||||
url: ApiEndpoints.stock_item_list,
|
url: ApiEndpoints.stock_item_list,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
|
modalId: 'create-stock-item',
|
||||||
title: t`Add Stock Item`
|
title: t`Add Stock Item`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -318,12 +326,15 @@ export function useStockItemInstallFields({
|
|||||||
*/
|
*/
|
||||||
export function useStockItemSerializeFields({
|
export function useStockItemSerializeFields({
|
||||||
partId,
|
partId,
|
||||||
trackable
|
trackable,
|
||||||
|
modalId
|
||||||
}: {
|
}: {
|
||||||
partId: number;
|
partId: number;
|
||||||
trackable: boolean;
|
trackable: boolean;
|
||||||
|
modalId: string;
|
||||||
}) {
|
}) {
|
||||||
const serialGenerator = useSerialNumberGenerator({
|
const serialGenerator = useSerialNumberGenerator({
|
||||||
|
modalId: modalId,
|
||||||
initialQuery: {
|
initialQuery: {
|
||||||
part: partId
|
part: partId
|
||||||
}
|
}
|
||||||
@ -1018,7 +1029,7 @@ type apiModalFunc = (props: ApiFormModalProps) => {
|
|||||||
modal: JSX.Element;
|
modal: JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
function stockOperationModal({
|
function useStockOperationModal({
|
||||||
items,
|
items,
|
||||||
pk,
|
pk,
|
||||||
model,
|
model,
|
||||||
@ -1061,25 +1072,29 @@ function stockOperationModal({
|
|||||||
return query_params;
|
return query_params;
|
||||||
}, [baseParams, filters, model, pk]);
|
}, [baseParams, filters, model, pk]);
|
||||||
|
|
||||||
|
const [opened, setOpened] = useState<boolean>(false);
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ['stockitems', model, pk, items, params],
|
queryKey: ['stockitems', opened, model, pk, items, params],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (items) {
|
if (items) {
|
||||||
|
// If a list of items is provided, use that directly
|
||||||
return Array.isArray(items) ? items : [items];
|
return Array.isArray(items) ? items : [items];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pk || !opened) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const url = apiUrl(ApiEndpoints.stock_item_list);
|
const url = apiUrl(ApiEndpoints.stock_item_list);
|
||||||
|
|
||||||
return api
|
return api
|
||||||
.get(url, {
|
.get(url, {
|
||||||
params: params
|
params: params
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => response.data ?? [])
|
||||||
if (response.status === 200) {
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return null;
|
return [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1095,7 +1110,9 @@ function stockOperationModal({
|
|||||||
title: title,
|
title: title,
|
||||||
size: '80%',
|
size: '80%',
|
||||||
successMessage: successMessage,
|
successMessage: successMessage,
|
||||||
onFormSuccess: () => refresh()
|
onFormSuccess: () => refresh(),
|
||||||
|
onClose: () => setOpened(false),
|
||||||
|
onOpen: () => setOpened(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1108,7 +1125,7 @@ export type StockOperationProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useAddStockItem(props: StockOperationProps) {
|
export function useAddStockItem(props: StockOperationProps) {
|
||||||
return stockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
fieldGenerator: stockAddFields,
|
fieldGenerator: stockAddFields,
|
||||||
endpoint: ApiEndpoints.stock_add,
|
endpoint: ApiEndpoints.stock_add,
|
||||||
@ -1118,7 +1135,7 @@ export function useAddStockItem(props: StockOperationProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useRemoveStockItem(props: StockOperationProps) {
|
export function useRemoveStockItem(props: StockOperationProps) {
|
||||||
return stockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
fieldGenerator: stockRemoveFields,
|
fieldGenerator: stockRemoveFields,
|
||||||
endpoint: ApiEndpoints.stock_remove,
|
endpoint: ApiEndpoints.stock_remove,
|
||||||
@ -1128,7 +1145,7 @@ export function useRemoveStockItem(props: StockOperationProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useTransferStockItem(props: StockOperationProps) {
|
export function useTransferStockItem(props: StockOperationProps) {
|
||||||
return stockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
fieldGenerator: stockTransferFields,
|
fieldGenerator: stockTransferFields,
|
||||||
endpoint: ApiEndpoints.stock_transfer,
|
endpoint: ApiEndpoints.stock_transfer,
|
||||||
@ -1138,7 +1155,7 @@ export function useTransferStockItem(props: StockOperationProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useCountStockItem(props: StockOperationProps) {
|
export function useCountStockItem(props: StockOperationProps) {
|
||||||
return stockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
fieldGenerator: stockCountFields,
|
fieldGenerator: stockCountFields,
|
||||||
endpoint: ApiEndpoints.stock_count,
|
endpoint: ApiEndpoints.stock_count,
|
||||||
@ -1148,7 +1165,7 @@ export function useCountStockItem(props: StockOperationProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useChangeStockStatus(props: StockOperationProps) {
|
export function useChangeStockStatus(props: StockOperationProps) {
|
||||||
return stockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
fieldGenerator: stockChangeStatusFields,
|
fieldGenerator: stockChangeStatusFields,
|
||||||
endpoint: ApiEndpoints.stock_change_status,
|
endpoint: ApiEndpoints.stock_change_status,
|
||||||
@ -1158,7 +1175,7 @@ export function useChangeStockStatus(props: StockOperationProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useMergeStockItem(props: StockOperationProps) {
|
export function useMergeStockItem(props: StockOperationProps) {
|
||||||
return stockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
fieldGenerator: stockMergeFields,
|
fieldGenerator: stockMergeFields,
|
||||||
endpoint: ApiEndpoints.stock_merge,
|
endpoint: ApiEndpoints.stock_merge,
|
||||||
@ -1182,7 +1199,7 @@ export function useAssignStockItem(props: StockOperationProps) {
|
|||||||
return props.items?.filter((item) => item?.part_detail?.salable);
|
return props.items?.filter((item) => item?.part_detail?.salable);
|
||||||
}, [props.items]);
|
}, [props.items]);
|
||||||
|
|
||||||
return stockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
items: items,
|
items: items,
|
||||||
fieldGenerator: stockAssignFields,
|
fieldGenerator: stockAssignFields,
|
||||||
@ -1193,7 +1210,7 @@ export function useAssignStockItem(props: StockOperationProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useDeleteStockItem(props: StockOperationProps) {
|
export function useDeleteStockItem(props: StockOperationProps) {
|
||||||
return stockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
fieldGenerator: stockDeleteFields,
|
fieldGenerator: stockDeleteFields,
|
||||||
endpoint: ApiEndpoints.stock_item_list,
|
endpoint: ApiEndpoints.stock_item_list,
|
||||||
|
@ -8,6 +8,7 @@ import type {
|
|||||||
BulkEditApiFormModalProps
|
BulkEditApiFormModalProps
|
||||||
} from '@lib/types/Forms';
|
} from '@lib/types/Forms';
|
||||||
import { OptionsApiForm } from '../components/forms/ApiForm';
|
import { OptionsApiForm } from '../components/forms/ApiForm';
|
||||||
|
import { useModalState } from '../states/ModalState';
|
||||||
import { useModal } from './UseModal';
|
import { useModal } from './UseModal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,6 +18,12 @@ export function useApiFormModal(props: ApiFormModalProps) {
|
|||||||
const id = useId();
|
const id = useId();
|
||||||
const modalClose = useRef(() => {});
|
const modalClose = useRef(() => {});
|
||||||
|
|
||||||
|
const modalState = useModalState();
|
||||||
|
|
||||||
|
const modalId = useMemo(() => {
|
||||||
|
return props.modalId ?? id;
|
||||||
|
}, [props.modalId, id]);
|
||||||
|
|
||||||
const formProps = useMemo<ApiFormModalProps>(
|
const formProps = useMemo<ApiFormModalProps>(
|
||||||
() => ({
|
() => ({
|
||||||
...props,
|
...props,
|
||||||
@ -44,15 +51,22 @@ export function useApiFormModal(props: ApiFormModalProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const modal = useModal({
|
const modal = useModal({
|
||||||
|
id: modalId,
|
||||||
title: formProps.title,
|
title: formProps.title,
|
||||||
onOpen: formProps.onOpen,
|
onOpen: () => {
|
||||||
onClose: formProps.onClose,
|
modalState.setModalOpen(modalId, true);
|
||||||
|
formProps.onOpen?.();
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
modalState.setModalOpen(modalId, false);
|
||||||
|
formProps.onClose?.();
|
||||||
|
},
|
||||||
closeOnClickOutside: formProps.closeOnClickOutside,
|
closeOnClickOutside: formProps.closeOnClickOutside,
|
||||||
size: props.size ?? 'xl',
|
size: props.size ?? 'xl',
|
||||||
children: (
|
children: (
|
||||||
<Stack gap={'xs'}>
|
<Stack gap={'xs'}>
|
||||||
<Divider />
|
<Divider />
|
||||||
<OptionsApiForm props={formProps} id={id} />
|
<OptionsApiForm props={formProps} id={modalId} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -5,12 +5,15 @@ import { useCallback, useState } from 'react';
|
|||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
|
import { useModalState } from '../states/ModalState';
|
||||||
|
|
||||||
export type GeneratorProps = {
|
export type GeneratorProps = {
|
||||||
endpoint: ApiEndpoints;
|
endpoint: ApiEndpoints;
|
||||||
key: string;
|
key: string;
|
||||||
initialQuery?: Record<string, any>;
|
initialQuery?: Record<string, any>;
|
||||||
onGenerate?: (value: any) => void;
|
onGenerate?: (value: any) => void;
|
||||||
|
isEnabled?: () => boolean;
|
||||||
|
modalId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GeneratorState = {
|
export type GeneratorState = {
|
||||||
@ -25,6 +28,8 @@ export type GeneratorState = {
|
|||||||
* Each update calls a new query to the API, and the result is stored in the state.
|
* Each update calls a new query to the API, and the result is stored in the state.
|
||||||
*/
|
*/
|
||||||
export function useGenerator(props: GeneratorProps): GeneratorState {
|
export function useGenerator(props: GeneratorProps): GeneratorState {
|
||||||
|
const modalState = useModalState();
|
||||||
|
|
||||||
// Track the result
|
// Track the result
|
||||||
const [result, setResult] = useState<any>(null);
|
const [result, setResult] = useState<any>(null);
|
||||||
|
|
||||||
@ -34,6 +39,24 @@ export function useGenerator(props: GeneratorProps): GeneratorState {
|
|||||||
// Prevent rapid updates
|
// Prevent rapid updates
|
||||||
const [debouncedQuery] = useDebouncedValue<Record<string, any>>(query, 100);
|
const [debouncedQuery] = useDebouncedValue<Record<string, any>>(query, 100);
|
||||||
|
|
||||||
|
// Callback to determine if the function is enabled
|
||||||
|
const isEnabled = useCallback(() => {
|
||||||
|
if (props.isEnabled?.() == false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.modalId && !modalState.isModalOpen(props.modalId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [
|
||||||
|
modalState.isModalOpen,
|
||||||
|
modalState.openModals,
|
||||||
|
props.isEnabled,
|
||||||
|
props.modalId
|
||||||
|
]);
|
||||||
|
|
||||||
// Callback to update the generator query
|
// Callback to update the generator query
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(params: Record<string, any>, overwrite?: boolean) => {
|
(params: Record<string, any>, overwrite?: boolean) => {
|
||||||
@ -57,6 +80,7 @@ export function useGenerator(props: GeneratorProps): GeneratorState {
|
|||||||
props.key,
|
props.key,
|
||||||
props.endpoint,
|
props.endpoint,
|
||||||
props.initialQuery,
|
props.initialQuery,
|
||||||
|
modalState.openModals,
|
||||||
debouncedQuery
|
debouncedQuery
|
||||||
],
|
],
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
@ -67,6 +91,11 @@ export function useGenerator(props: GeneratorProps): GeneratorState {
|
|||||||
...(props.initialQuery ?? {})
|
...(props.initialQuery ?? {})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isEnabled()) {
|
||||||
|
setResult(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return api
|
return api
|
||||||
.post(apiUrl(props.endpoint), generatorQuery)
|
.post(apiUrl(props.endpoint), generatorQuery)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -96,31 +125,43 @@ export function useGenerator(props: GeneratorProps): GeneratorState {
|
|||||||
// Generate a batch code with provided data
|
// Generate a batch code with provided data
|
||||||
export function useBatchCodeGenerator({
|
export function useBatchCodeGenerator({
|
||||||
initialQuery,
|
initialQuery,
|
||||||
onGenerate
|
onGenerate,
|
||||||
|
isEnabled,
|
||||||
|
modalId
|
||||||
}: {
|
}: {
|
||||||
initialQuery?: Record<string, any>;
|
initialQuery?: Record<string, any>;
|
||||||
onGenerate?: (value: any) => void;
|
onGenerate?: (value: any) => void;
|
||||||
|
isEnabled?: () => boolean;
|
||||||
|
modalId?: string;
|
||||||
}): GeneratorState {
|
}): GeneratorState {
|
||||||
return useGenerator({
|
return useGenerator({
|
||||||
endpoint: ApiEndpoints.generate_batch_code,
|
endpoint: ApiEndpoints.generate_batch_code,
|
||||||
key: 'batch_code',
|
key: 'batch_code',
|
||||||
initialQuery: initialQuery,
|
initialQuery: initialQuery,
|
||||||
onGenerate: onGenerate
|
onGenerate: onGenerate,
|
||||||
|
isEnabled: isEnabled,
|
||||||
|
modalId: modalId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a serial number with provided data
|
// Generate a serial number with provided data
|
||||||
export function useSerialNumberGenerator({
|
export function useSerialNumberGenerator({
|
||||||
initialQuery,
|
initialQuery,
|
||||||
onGenerate
|
onGenerate,
|
||||||
|
isEnabled,
|
||||||
|
modalId
|
||||||
}: {
|
}: {
|
||||||
initialQuery?: Record<string, any>;
|
initialQuery?: Record<string, any>;
|
||||||
onGenerate?: (value: any) => void;
|
onGenerate?: (value: any) => void;
|
||||||
|
isEnabled?: () => boolean;
|
||||||
|
modalId?: string;
|
||||||
}): GeneratorState {
|
}): GeneratorState {
|
||||||
return useGenerator({
|
return useGenerator({
|
||||||
endpoint: ApiEndpoints.generate_serial_number,
|
endpoint: ApiEndpoints.generate_serial_number,
|
||||||
key: 'serial_number',
|
key: 'serial_number',
|
||||||
initialQuery: initialQuery,
|
initialQuery: initialQuery,
|
||||||
onGenerate: onGenerate
|
onGenerate: onGenerate,
|
||||||
|
isEnabled: isEnabled,
|
||||||
|
modalId: modalId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ export function useModal(props: UseModalProps): UseModalReturn {
|
|||||||
toggle,
|
toggle,
|
||||||
modal: (
|
modal: (
|
||||||
<Modal
|
<Modal
|
||||||
|
key={props.id}
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={close}
|
onClose={close}
|
||||||
closeOnClickOutside={props.closeOnClickOutside}
|
closeOnClickOutside={props.closeOnClickOutside}
|
||||||
|
@ -416,13 +416,17 @@ export default function BuildDetail() {
|
|||||||
];
|
];
|
||||||
}, [build, id, user, buildStatus, globalSettings]);
|
}, [build, id, user, buildStatus, globalSettings]);
|
||||||
|
|
||||||
const buildOrderFields = useBuildOrderFields({ create: false });
|
const editBuildOrderFields = useBuildOrderFields({
|
||||||
|
create: false,
|
||||||
|
modalId: 'edit-build-order'
|
||||||
|
});
|
||||||
|
|
||||||
const editBuild = useEditApiFormModal({
|
const editBuild = useEditApiFormModal({
|
||||||
url: ApiEndpoints.build_order_list,
|
url: ApiEndpoints.build_order_list,
|
||||||
pk: build.pk,
|
pk: build.pk,
|
||||||
title: t`Edit Build Order`,
|
title: t`Edit Build Order`,
|
||||||
fields: buildOrderFields,
|
modalId: 'edit-build-order',
|
||||||
|
fields: editBuildOrderFields,
|
||||||
onFormSuccess: refreshInstance
|
onFormSuccess: refreshInstance
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -435,10 +439,16 @@ export default function BuildDetail() {
|
|||||||
return data;
|
return data;
|
||||||
}, [build]);
|
}, [build]);
|
||||||
|
|
||||||
|
const duplicateBuildOrderFields = useBuildOrderFields({
|
||||||
|
create: false,
|
||||||
|
modalId: 'duplicate-build-order'
|
||||||
|
});
|
||||||
|
|
||||||
const duplicateBuild = useCreateApiFormModal({
|
const duplicateBuild = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.build_order_list,
|
url: ApiEndpoints.build_order_list,
|
||||||
title: t`Add Build Order`,
|
title: t`Add Build Order`,
|
||||||
fields: buildOrderFields,
|
modalId: 'duplicate-build-order',
|
||||||
|
fields: duplicateBuildOrderFields,
|
||||||
initialData: duplicateBuildOrderInitialData,
|
initialData: duplicateBuildOrderInitialData,
|
||||||
follow: true,
|
follow: true,
|
||||||
modelType: ModelType.build
|
modelType: ModelType.build
|
||||||
|
@ -1005,11 +1005,13 @@ export default function PartDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{duplicatePart.modal}
|
|
||||||
{editPart.modal}
|
{editPart.modal}
|
||||||
{deletePart.modal}
|
{deletePart.modal}
|
||||||
{findBySerialNumber.modal}
|
{duplicatePart.modal}
|
||||||
|
{countStockItems.modal}
|
||||||
{orderPartsWizard.wizard}
|
{orderPartsWizard.wizard}
|
||||||
|
{findBySerialNumber.modal}
|
||||||
|
{transferStockItems.modal}
|
||||||
<InstanceDetail
|
<InstanceDetail
|
||||||
status={requestStatus}
|
status={requestStatus}
|
||||||
loading={instanceQuery.isFetching}
|
loading={instanceQuery.isFetching}
|
||||||
@ -1096,8 +1098,6 @@ export default function PartDetail() {
|
|||||||
model={ModelType.part}
|
model={ModelType.part}
|
||||||
id={part.pk}
|
id={part.pk}
|
||||||
/>
|
/>
|
||||||
{transferStockItems.modal}
|
|
||||||
{countStockItems.modal}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
</>
|
</>
|
||||||
|
@ -640,22 +640,28 @@ export default function StockDetail() {
|
|||||||
const editStockItemFields = useStockFields({
|
const editStockItemFields = useStockFields({
|
||||||
create: false,
|
create: false,
|
||||||
stockItem: stockitem,
|
stockItem: stockitem,
|
||||||
partId: stockitem.part
|
partId: stockitem.part,
|
||||||
|
modalId: 'edit-stock-item'
|
||||||
});
|
});
|
||||||
|
|
||||||
const editStockItem = useEditApiFormModal({
|
const editStockItem = useEditApiFormModal({
|
||||||
url: ApiEndpoints.stock_item_list,
|
url: ApiEndpoints.stock_item_list,
|
||||||
pk: stockitem.pk,
|
pk: stockitem.pk,
|
||||||
title: t`Edit Stock Item`,
|
title: t`Edit Stock Item`,
|
||||||
|
modalId: 'edit-stock-item',
|
||||||
fields: editStockItemFields,
|
fields: editStockItemFields,
|
||||||
onFormSuccess: refreshInstance
|
onFormSuccess: refreshInstance
|
||||||
});
|
});
|
||||||
|
|
||||||
const duplicateStockItemFields = useStockFields({ create: true });
|
const duplicateStockItemFields = useStockFields({
|
||||||
|
create: true,
|
||||||
|
modalId: 'duplicate-stock-item'
|
||||||
|
});
|
||||||
|
|
||||||
const duplicateStockItem = useCreateApiFormModal({
|
const duplicateStockItem = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.stock_item_list,
|
url: ApiEndpoints.stock_item_list,
|
||||||
title: t`Add Stock Item`,
|
title: t`Add Stock Item`,
|
||||||
|
modalId: 'duplicate-stock-item',
|
||||||
fields: duplicateStockItemFields,
|
fields: duplicateStockItemFields,
|
||||||
initialData: {
|
initialData: {
|
||||||
...stockitem
|
...stockitem
|
||||||
@ -700,13 +706,15 @@ export default function StockDetail() {
|
|||||||
|
|
||||||
const serializeStockFields = useStockItemSerializeFields({
|
const serializeStockFields = useStockItemSerializeFields({
|
||||||
partId: stockitem.part,
|
partId: stockitem.part,
|
||||||
trackable: stockitem.part_detail?.trackable
|
trackable: stockitem.part_detail?.trackable,
|
||||||
|
modalId: 'stock-item-serialize'
|
||||||
});
|
});
|
||||||
|
|
||||||
const serializeStockItem = useCreateApiFormModal({
|
const serializeStockItem = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.stock_serialize,
|
url: ApiEndpoints.stock_serialize,
|
||||||
pk: stockitem.pk,
|
pk: stockitem.pk,
|
||||||
title: t`Serialize Stock Item`,
|
title: t`Serialize Stock Item`,
|
||||||
|
modalId: 'stock-item-serialize',
|
||||||
fields: serializeStockFields,
|
fields: serializeStockFields,
|
||||||
initialData: {
|
initialData: {
|
||||||
quantity: stockitem.quantity,
|
quantity: stockitem.quantity,
|
||||||
|
28
src/frontend/src/states/ModalState.tsx
Normal file
28
src/frontend/src/states/ModalState.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface ModalStateProps {
|
||||||
|
openModals: Record<string, boolean>;
|
||||||
|
isModalOpen: (modalKey: string) => boolean;
|
||||||
|
setModalOpen: (modalKey: string, isOpen: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global state manager for determining modal visibility.
|
||||||
|
* Useful to share modal state (open / closed) between components.
|
||||||
|
*/
|
||||||
|
export const useModalState = create<ModalStateProps>()((set, get) => ({
|
||||||
|
openModals: {},
|
||||||
|
|
||||||
|
isModalOpen: (modalKey: string) => {
|
||||||
|
return get().openModals[modalKey] ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
setModalOpen: (modalKey: string, isOpen: boolean) => {
|
||||||
|
set((state) => ({
|
||||||
|
openModals: {
|
||||||
|
...state.openModals,
|
||||||
|
[modalKey]: isOpen
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
@ -176,10 +176,24 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
);
|
);
|
||||||
}, [props.tableFilters, fieldNames]);
|
}, [props.tableFilters, fieldNames]);
|
||||||
|
|
||||||
|
// Build table properties based on provided props (and default props)
|
||||||
|
const tableProps: InvenTreeTableProps<T> = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...defaultInvenTreeTableProps,
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
// Request OPTIONS data from the API, before we load the table
|
// Request OPTIONS data from the API, before we load the table
|
||||||
const tableOptionQuery = useQuery({
|
const tableOptionQuery = useQuery({
|
||||||
enabled: !!url && !tableData,
|
enabled: !!url && !tableData,
|
||||||
queryKey: ['options', url, cacheKey, props.enableColumnCaching],
|
queryKey: [
|
||||||
|
'options',
|
||||||
|
url,
|
||||||
|
cacheKey,
|
||||||
|
tableProps.params,
|
||||||
|
props.enableColumnCaching
|
||||||
|
],
|
||||||
retry: 3,
|
retry: 3,
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
gcTime: 5000,
|
gcTime: 5000,
|
||||||
@ -255,14 +269,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
tableOptionQuery.refetch();
|
tableOptionQuery.refetch();
|
||||||
}, [cacheKey, url, props.params, props.enableColumnCaching]);
|
}, [cacheKey, url, props.params, props.enableColumnCaching]);
|
||||||
|
|
||||||
// Build table properties based on provided props (and default props)
|
|
||||||
const tableProps: InvenTreeTableProps<T> = useMemo(() => {
|
|
||||||
return {
|
|
||||||
...defaultInvenTreeTableProps,
|
|
||||||
...props
|
|
||||||
};
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
const enableSelection: boolean = useMemo(() => {
|
const enableSelection: boolean = useMemo(() => {
|
||||||
return tableProps.enableSelection || tableProps.enableBulkDelete || false;
|
return tableProps.enableSelection || tableProps.enableBulkDelete || false;
|
||||||
}, [tableProps]);
|
}, [tableProps]);
|
||||||
@ -450,13 +456,17 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [sortingLoaded, setSortingLoaded] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tableKey: string = tableState.tableKey.split('-')[0];
|
const tableKey: string = tableState.tableKey.split('-')[0];
|
||||||
const sorting: DataTableSortStatus = getTableSorting(tableKey);
|
const sorting: DataTableSortStatus = getTableSorting(tableKey);
|
||||||
|
|
||||||
if (sorting) {
|
if (sorting && !!sorting.columnAccessor && !!sorting.direction) {
|
||||||
setSortStatus(sorting);
|
setSortStatus(sorting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSortingLoaded(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Return the ordering parameter
|
// Return the ordering parameter
|
||||||
@ -492,6 +502,12 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
const queryParams = getTableFilters(true);
|
const queryParams = getTableFilters(true);
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
|
// No URL supplied - do not load!
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sortingLoaded) {
|
||||||
|
// Sorting not yet loaded - do not load!
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,6 +576,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
url,
|
url,
|
||||||
tableState.page,
|
tableState.page,
|
||||||
props.params,
|
props.params,
|
||||||
|
sortingLoaded,
|
||||||
sortStatus.columnAccessor,
|
sortStatus.columnAccessor,
|
||||||
sortStatus.direction,
|
sortStatus.direction,
|
||||||
tableState.tableKey,
|
tableState.tableKey,
|
||||||
|
@ -419,7 +419,10 @@ export default function BuildLineTable({
|
|||||||
];
|
];
|
||||||
}, [hasOutput, isActive, table, output]);
|
}, [hasOutput, isActive, table, output]);
|
||||||
|
|
||||||
const buildOrderFields = useBuildOrderFields({ create: true });
|
const buildOrderFields = useBuildOrderFields({
|
||||||
|
create: true,
|
||||||
|
modalId: 'new-build-order'
|
||||||
|
});
|
||||||
|
|
||||||
const [initialData, setInitialData] = useState<any>({});
|
const [initialData, setInitialData] = useState<any>({});
|
||||||
|
|
||||||
@ -431,6 +434,7 @@ export default function BuildLineTable({
|
|||||||
url: ApiEndpoints.build_order_list,
|
url: ApiEndpoints.build_order_list,
|
||||||
title: t`Create Build Order`,
|
title: t`Create Build Order`,
|
||||||
fields: buildOrderFields,
|
fields: buildOrderFields,
|
||||||
|
modalId: 'new-build-order',
|
||||||
initialData: initialData,
|
initialData: initialData,
|
||||||
follow: true,
|
follow: true,
|
||||||
modelType: ModelType.build
|
modelType: ModelType.build
|
||||||
|
@ -194,11 +194,15 @@ export function BuildOrderTable({
|
|||||||
|
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
|
||||||
const buildOrderFields = useBuildOrderFields({ create: true });
|
const buildOrderFields = useBuildOrderFields({
|
||||||
|
create: true,
|
||||||
|
modalId: 'create-build-order'
|
||||||
|
});
|
||||||
|
|
||||||
const newBuild = useCreateApiFormModal({
|
const newBuild = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.build_order_list,
|
url: ApiEndpoints.build_order_list,
|
||||||
title: t`Add Build Order`,
|
title: t`Add Build Order`,
|
||||||
|
modalId: 'create-build-order',
|
||||||
fields: buildOrderFields,
|
fields: buildOrderFields,
|
||||||
initialData: {
|
initialData: {
|
||||||
part: partId,
|
part: partId,
|
||||||
|
@ -256,11 +256,15 @@ export default function BuildOutputTable({
|
|||||||
[partId, buildId, testTemplates, trackedItems]
|
[partId, buildId, testTemplates, trackedItems]
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildOutputFields = useBuildOrderOutputFields({ build: build });
|
const buildOutputFields = useBuildOrderOutputFields({
|
||||||
|
build: build,
|
||||||
|
modalId: 'add-build-output'
|
||||||
|
});
|
||||||
|
|
||||||
const addBuildOutput = useCreateApiFormModal({
|
const addBuildOutput = useCreateApiFormModal({
|
||||||
url: apiUrl(ApiEndpoints.build_output_create, buildId),
|
url: apiUrl(ApiEndpoints.build_output_create, buildId),
|
||||||
title: t`Add Build Output`,
|
title: t`Add Build Output`,
|
||||||
|
modalId: 'add-build-output',
|
||||||
fields: buildOutputFields,
|
fields: buildOutputFields,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
initialData: {
|
initialData: {
|
||||||
@ -302,13 +306,15 @@ export default function BuildOutputTable({
|
|||||||
const editStockItemFields = useStockFields({
|
const editStockItemFields = useStockFields({
|
||||||
create: false,
|
create: false,
|
||||||
partId: partId,
|
partId: partId,
|
||||||
stockItem: selectedOutputs[0]
|
stockItem: selectedOutputs[0],
|
||||||
|
modalId: 'edit-build-output'
|
||||||
});
|
});
|
||||||
|
|
||||||
const editBuildOutput = useEditApiFormModal({
|
const editBuildOutput = useEditApiFormModal({
|
||||||
url: ApiEndpoints.stock_item_list,
|
url: ApiEndpoints.stock_item_list,
|
||||||
pk: selectedOutputs[0]?.pk,
|
pk: selectedOutputs[0]?.pk,
|
||||||
title: t`Edit Build Output`,
|
title: t`Edit Build Output`,
|
||||||
|
modalId: 'edit-build-output',
|
||||||
fields: editStockItemFields,
|
fields: editStockItemFields,
|
||||||
table: table
|
table: table
|
||||||
});
|
});
|
||||||
|
@ -276,11 +276,15 @@ export default function SalesOrderLineItemTable({
|
|||||||
table: table
|
table: table
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildOrderFields = useBuildOrderFields({ create: true });
|
const buildOrderFields = useBuildOrderFields({
|
||||||
|
create: true,
|
||||||
|
modalId: 'build-order-create-from-sales-order'
|
||||||
|
});
|
||||||
|
|
||||||
const newBuildOrder = useCreateApiFormModal({
|
const newBuildOrder = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.build_order_list,
|
url: ApiEndpoints.build_order_list,
|
||||||
title: t`Create Build Order`,
|
title: t`Create Build Order`,
|
||||||
|
modalId: 'build-order-create-from-sales-order',
|
||||||
fields: buildOrderFields,
|
fields: buildOrderFields,
|
||||||
initialData: initialData,
|
initialData: initialData,
|
||||||
follow: true,
|
follow: true,
|
||||||
|
@ -526,12 +526,14 @@ export function StockItemTable({
|
|||||||
|
|
||||||
const newStockItemFields = useStockFields({
|
const newStockItemFields = useStockFields({
|
||||||
create: true,
|
create: true,
|
||||||
partId: params.part
|
partId: params.part,
|
||||||
|
modalId: 'add-stock-item'
|
||||||
});
|
});
|
||||||
|
|
||||||
const newStockItem = useCreateApiFormModal({
|
const newStockItem = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.stock_item_list,
|
url: ApiEndpoints.stock_item_list,
|
||||||
title: t`Add Stock Item`,
|
title: t`Add Stock Item`,
|
||||||
|
modalId: 'add-stock-item',
|
||||||
fields: newStockItemFields,
|
fields: newStockItemFields,
|
||||||
initialData: {
|
initialData: {
|
||||||
part: params.part,
|
part: params.part,
|
||||||
|
Reference in New Issue
Block a user