mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-25 12:33:33 +00:00
feat(frontend): add inline create modal to PurchaseOrderLineItem dialog (#11778)
* feat(frontend): add inline create modal to PurchaseOrderLineItem dialog fixes https://github.com/invenhost/InvenTree/issues/299 * add changelog * implement suggested fix Co-authored-by: Oliver <oliver.henry.walters@gmail.com> --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- [#11778](https://github.com/inventree/InvenTree/pull/11778) adds inline supplier part creation to po line item addition dialog.
|
||||||
- [#11772](https://github.com/inventree/InvenTree/pull/11772) the UI now warns if you navigate away from a note panel with unsaved changes
|
- [#11772](https://github.com/inventree/InvenTree/pull/11772) the UI now warns if you navigate away from a note panel with unsaved changes
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export type ApiFormFieldType = {
|
|||||||
placeholderAutofill?: boolean;
|
placeholderAutofill?: boolean;
|
||||||
placeholderWarningCompare?: string | number;
|
placeholderWarningCompare?: string | number;
|
||||||
placeholderWarning?: string;
|
placeholderWarning?: string;
|
||||||
|
addCreateFields?: ApiFormFieldSet;
|
||||||
description?: string;
|
description?: string;
|
||||||
preFieldContent?: JSX.Element;
|
preFieldContent?: JSX.Element;
|
||||||
postFieldContent?: JSX.Element;
|
postFieldContent?: JSX.Element;
|
||||||
|
|||||||
@@ -8,17 +8,32 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDebouncedValue, useId } from '@mantine/hooks';
|
import { useDebouncedValue, useId } from '@mantine/hooks';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
type FieldValues,
|
type FieldValues,
|
||||||
type UseControllerReturn,
|
type UseControllerReturn,
|
||||||
|
type UseFormReturn,
|
||||||
useFormContext
|
useFormContext
|
||||||
} from 'react-hook-form';
|
} from 'react-hook-form';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
|
|
||||||
import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
import { ActionButton } from '@lib/components/ActionButton';
|
||||||
|
import {
|
||||||
|
ModelInformationDict,
|
||||||
|
type TranslatableModelInformationInterface
|
||||||
|
} from '@lib/enums/ModelInformation';
|
||||||
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import type { ApiFormFieldType } from '@lib/types/Forms';
|
import type { ApiFormFieldType } from '@lib/types/Forms';
|
||||||
|
import { IconPlus } from '@tabler/icons-react';
|
||||||
import { useApi } from '../../../contexts/ApiContext';
|
import { useApi } from '../../../contexts/ApiContext';
|
||||||
|
import { useCreateApiFormModal } from '../../../hooks/UseForm';
|
||||||
import {
|
import {
|
||||||
useGlobalSettingsState,
|
useGlobalSettingsState,
|
||||||
useUserSettingsState
|
useUserSettingsState
|
||||||
@@ -54,6 +69,10 @@ export function RelatedModelField({
|
|||||||
// Keep track of the primary key value for this field
|
// Keep track of the primary key value for this field
|
||||||
const [pk, setPk] = useState<number | null>(null);
|
const [pk, setPk] = useState<number | null>(null);
|
||||||
|
|
||||||
|
function setValuefromPK(pk: number) {
|
||||||
|
fetchSingleField(pk);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle condition where the form is rebuilt dynamically
|
// Handle condition where the form is rebuilt dynamically
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const value = field.value || pk;
|
const value = field.value || pk;
|
||||||
@@ -139,6 +158,17 @@ export function RelatedModelField({
|
|||||||
return ModelInformationDict[definition.model];
|
return ModelInformationDict[definition.model];
|
||||||
}, [definition.model]);
|
}, [definition.model]);
|
||||||
|
|
||||||
|
// Determine whether an add button should be added for this field
|
||||||
|
const addButton = useMemo(() => {
|
||||||
|
if (!modelInfo) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (definition.addCreateFields) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [definition.addCreateFields, modelInfo]);
|
||||||
|
|
||||||
// Determine whether a barcode field should be added
|
// Determine whether a barcode field should be added
|
||||||
const addBarcodeField: boolean = useMemo(() => {
|
const addBarcodeField: boolean = useMemo(() => {
|
||||||
if (!modelInfo || !modelInfo.supports_barcode) {
|
if (!modelInfo || !modelInfo.supports_barcode) {
|
||||||
@@ -296,15 +326,7 @@ export function RelatedModelField({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _filters = definition.filters ?? {};
|
const _filters = retrieveFilters(definition, form);
|
||||||
|
|
||||||
if (definition.adjustFilters) {
|
|
||||||
_filters =
|
|
||||||
definition.adjustFilters({
|
|
||||||
filters: _filters,
|
|
||||||
data: form.getValues()
|
|
||||||
}) ?? _filters;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the filters have changed, clear the data
|
// If the filters have changed, clear the data
|
||||||
if (JSON.stringify(_filters) !== JSON.stringify(filters)) {
|
if (JSON.stringify(_filters) !== JSON.stringify(filters)) {
|
||||||
@@ -461,6 +483,9 @@ export function RelatedModelField({
|
|||||||
styles={{ description: { paddingBottom: '5px' } }}
|
styles={{ description: { paddingBottom: '5px' } }}
|
||||||
>
|
>
|
||||||
<Group justify='space-between' wrap='nowrap' gap={3}>
|
<Group justify='space-between' wrap='nowrap' gap={3}>
|
||||||
|
{addButton &&
|
||||||
|
modelInfo &&
|
||||||
|
InlineCreateButton(definition, modelInfo, form, setValuefromPK)}
|
||||||
<Expand>
|
<Expand>
|
||||||
<Select
|
<Select
|
||||||
id={fieldId}
|
id={fieldId}
|
||||||
@@ -525,3 +550,70 @@ export function RelatedModelField({
|
|||||||
</Input.Wrapper>
|
</Input.Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function InlineCreateButton(
|
||||||
|
definition: ApiFormFieldType,
|
||||||
|
modelInfo: TranslatableModelInformationInterface,
|
||||||
|
form: UseFormReturn<FieldValues, any, FieldValues>,
|
||||||
|
setValue: (value: number) => void
|
||||||
|
): ReactNode {
|
||||||
|
const relatedInitialData = useMemo(
|
||||||
|
() => calculateModalData(definition, form),
|
||||||
|
[definition.filters, definition.addCreateFields, form]
|
||||||
|
);
|
||||||
|
const model = useMemo(() => modelInfo?.label() ?? '', [modelInfo]);
|
||||||
|
|
||||||
|
const create_modal = useCreateApiFormModal({
|
||||||
|
title: t`Create New ${model}`,
|
||||||
|
url: apiUrl(modelInfo.api_endpoint),
|
||||||
|
modelType: definition.model,
|
||||||
|
initialData: relatedInitialData,
|
||||||
|
fields: definition.addCreateFields,
|
||||||
|
onFormSuccess: (response: any) => {
|
||||||
|
setValue(response.pk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{create_modal.modal}
|
||||||
|
<ActionButton
|
||||||
|
onClick={() => {
|
||||||
|
create_modal.open();
|
||||||
|
}}
|
||||||
|
color='green'
|
||||||
|
icon={<IconPlus />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function retrieveFilters(
|
||||||
|
definition: ApiFormFieldType,
|
||||||
|
form: UseFormReturn<FieldValues, any, FieldValues>
|
||||||
|
) {
|
||||||
|
let _filters = definition.filters ?? {};
|
||||||
|
|
||||||
|
if (definition.adjustFilters) {
|
||||||
|
_filters =
|
||||||
|
definition.adjustFilters({
|
||||||
|
filters: _filters,
|
||||||
|
data: form.getValues()
|
||||||
|
}) ?? _filters;
|
||||||
|
}
|
||||||
|
return _filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateModalData(
|
||||||
|
definition: ApiFormFieldType,
|
||||||
|
form: UseFormReturn<FieldValues, any, FieldValues>
|
||||||
|
) {
|
||||||
|
if (!definition.addCreateFields) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const fields = new Set(Object.keys(definition.addCreateFields));
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(retrieveFilters(definition, form)).filter(([key]) =>
|
||||||
|
fields.has(key)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ export function usePurchaseOrderLineItemFields({
|
|||||||
...adjust.filters,
|
...adjust.filters,
|
||||||
supplier: supplierId
|
supplier: supplierId
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
addCreateFields: {
|
||||||
|
part: {},
|
||||||
|
SKU: {},
|
||||||
|
description: {},
|
||||||
|
supplier: { hidden: true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
line: {},
|
line: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user