2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-25 04:23: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:
Matthias Mair
2026-04-22 08:53:31 +02:00
committed by GitHub
parent 6cb0cfbfcc
commit d8cd1849ba
4 changed files with 111 additions and 11 deletions
+1
View File
@@ -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
+1
View File
@@ -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: {},