2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-25 04:23:33 +00:00

[UI] More sub-forms (#11785)

* Fix ApiFormFIeldType docstring

* Case fix

* Add sub-forms for company creation

* Tweak component def

* Add new company for manufacturer-part and supplier-part

* Create new parameter template directly from parameter view

* Add tooltip

* Add playwright tests

* Consolidate translation

* Revert to left side

* Fix title case

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Oliver
2026-04-23 12:54:44 +10:00
committed by GitHub
parent b686cc1327
commit 757597c55d
8 changed files with 118 additions and 39 deletions
+10 -6
View File
@@ -37,7 +37,6 @@ export type ApiFormFieldHeader = {
* - All other attributes are optional, and may be provided by the API
* - However, they can be overridden by the user
*
* @param name : The name of the field
* @param label : The label to display for the field
* @param value : The value of the field
* @param default : The default value of the field
@@ -46,16 +45,21 @@ export type ApiFormFieldHeader = {
* @param api_url : The API endpoint to fetch data from (for related fields)
* @param pk_field : The primary key field for the related field (default = "pk")
* @param model : The model to use for related fields
* @param modelRenderer : Optional function to render the related model instance (for related fields)
* @param filters : Optional API filters to apply to related fields
* @param child: Optional definition of a child field (for nested objects)
* @param children: Optional definitions of child fields (for nested objects with multiple fields)
* @param required : Whether the field is required
* @param allow_null: Whether the field allows null values
* @param allow_blank: Whether the field allows blank values
* @param error : Optional error message to display
* @param hidden : Whether the field is hidden
* @param disabled : Whether the field is disabled
* @param error : Optional error message to display
* @param allow_null: Whether the field allows null values
* @param allow_blank: Whether the field allows blank values
* @param exclude : Whether to exclude the field from the submitted data
* @param read_only : Whether the field is read-only
* @param placeholder : The placeholder text to display
* @param placeholderAutofill: Whether to allow auto-filling of the placeholder value
* @param addCreateFields : Fields to display when creating a new related object (for related fields)
* @param description : The description to display for the field
* @param preFieldContent : Content to render before the field
* @param postFieldContent : Content to render after the field
@@ -63,11 +67,11 @@ export type ApiFormFieldHeader = {
* @param rightSection : Content to render in the right section of the field
* @param autoFill: Whether to automatically fill the field with data from the API
* @param autoFillFilters: Optional filters to apply when auto-filling the field
* @param adjustValue : Callback function to adjust the value of the field before it is sent to the API
* @param onValueChange : Callback function to call when the field value changes
* @param adjustFilters : Callback function to adjust the filters for a related field before a query is made
* @param adjustValue : Callback function to adjust the value of the field before it is sent to the API
* @param addRow : Callback function to add a new row to a table field
* @param onKeyDown : Callback function to get which key was pressed in the form to handle submission on enter
* @param headers : Optional definitions of table headers (for table fields)
* @param singleFetchFunction : Optional function to fetch a single value for this field (used for fetching the initial value when editing an existing object)
*/
export type ApiFormFieldType = {
@@ -69,7 +69,7 @@ export function RelatedModelField({
// Keep track of the primary key value for this field
const [pk, setPk] = useState<number | null>(null);
function setValuefromPK(pk: number) {
function setValueFromPK(pk: number) {
fetchSingleField(pk);
}
@@ -483,9 +483,14 @@ export function RelatedModelField({
styles={{ description: { paddingBottom: '5px' } }}
>
<Group justify='space-between' wrap='nowrap' gap={3}>
{addButton &&
modelInfo &&
InlineCreateButton(definition, modelInfo, form, setValuefromPK)}
{addButton && modelInfo && (
<InlineCreateButton
definition={definition}
modelInfo={modelInfo}
form={form}
setValue={setValueFromPK}
/>
)}
<Expand>
<Select
id={fieldId}
@@ -551,20 +556,29 @@ export function RelatedModelField({
);
}
function InlineCreateButton(
definition: ApiFormFieldType,
modelInfo: TranslatableModelInformationInterface,
form: UseFormReturn<FieldValues, any, FieldValues>,
setValue: (value: number) => void
): ReactNode {
function InlineCreateButton({
definition,
modelInfo,
form,
setValue
}: {
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 title: string = useMemo(() => {
const model = modelInfo?.label() ?? t`Item`;
return t`Create New ${model}`;
}, [modelInfo]);
const create_modal = useCreateApiFormModal({
title: t`Create New ${model}`,
title: title,
url: apiUrl(modelInfo.api_endpoint),
modelType: definition.model,
initialData: relatedInitialData,
@@ -577,6 +591,8 @@ function InlineCreateButton(
<>
{create_modal.modal}
<ActionButton
tooltip={title}
tooltipAlignment='top-start'
onClick={() => {
create_modal.open();
}}
+36 -2
View File
@@ -12,6 +12,7 @@ import type {
} from '../components/render/StatusRenderer';
import { useApi } from '../contexts/ApiContext';
import { useGlobalStatusState } from '../states/GlobalStatusState';
import { useUserState } from '../states/UserState';
export function projectCodeFields(): ApiFormFieldSet {
return {
@@ -97,6 +98,25 @@ export function extraLineItemFields(): ApiFormFieldSet {
};
}
export function useParameterTemplateFields(): ApiFormFieldSet {
return useMemo(() => {
return {
name: {},
description: {},
units: {},
model_type: {},
choices: {},
checkbox: {},
selectionlist: {
filters: {
active: true
}
},
enabled: {}
};
}, []);
}
export function useParameterFields({
modelType,
modelId
@@ -106,6 +126,10 @@ export function useParameterFields({
}): ApiFormFieldSet {
const api = useApi();
const user = useUserState.getState();
const templateCreateFields = useParameterTemplateFields();
const [selectionListId, setSelectionListId] = useState<number | null>(null);
// Valid field choices
@@ -193,7 +217,8 @@ export function useParameterFields({
} else if (record?.selectionlist) {
setFieldType('related field');
}
}
},
addCreateFields: user.isStaff() ? templateCreateFields : undefined
},
data: {
value: data,
@@ -244,5 +269,14 @@ export function useParameterFields({
},
note: {}
};
}, [data, modelType, fieldType, choices, modelId, selectionListId]);
}, [
data,
modelType,
fieldType,
choices,
modelId,
selectionListId,
templateCreateFields,
user
]);
}
+10
View File
@@ -56,6 +56,11 @@ export function useSupplierPartFields({
filters: {
active: true,
is_supplier: true
},
addCreateFields: {
name: {},
description: {},
is_supplier: { value: true, hidden: true }
}
},
SKU: {
@@ -88,6 +93,11 @@ export function useManufacturerPartFields() {
filters: {
active: true,
is_manufacturer: true
},
addCreateFields: {
name: {},
description: {},
is_manufacturer: { value: true, hidden: true }
}
},
MPN: {},
@@ -247,6 +247,11 @@ export function usePurchaseOrderFields({
filters: {
is_supplier: true,
active: true
},
addCreateFields: {
name: {},
description: {},
is_supplier: { value: true, hidden: true }
}
},
supplier_reference: {},
@@ -48,6 +48,11 @@ export function useSalesOrderFields({
filters: {
is_customer: true,
active: true
},
addCreateFields: {
name: {},
description: {},
is_customer: { value: true, hidden: true }
}
},
customer_reference: {},
@@ -13,6 +13,7 @@ import type { TableFilter } from '@lib/types/Filters';
import type { RowAction, TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
import { useCallback, useMemo, useState } from 'react';
import { useParameterTemplateFields } from '../../forms/CommonForms';
import { useFilters } from '../../hooks/UseFilter';
import {
useCreateApiFormModal,
@@ -30,22 +31,7 @@ export default function ParameterTemplateTable() {
const table = useTable('parameter-templates');
const user = useUserState();
const parameterTemplateFields: ApiFormFieldSet = useMemo(() => {
return {
name: {},
description: {},
units: {},
model_type: {},
choices: {},
checkbox: {},
selectionlist: {
filters: {
active: true
}
},
enabled: {}
};
}, []);
const parameterTemplateFields: ApiFormFieldSet = useParameterTemplateFields();
const newTemplate = useCreateApiFormModal({
url: ApiEndpoints.parameter_template_list,
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test';
import { test } from '../baseFixtures.ts';
import { readeruser } from '../defaults.ts';
import { readeruser, stevenuser } from '../defaults.ts';
import {
activateCalendarView,
activateTableView,
@@ -84,11 +84,11 @@ test('Purchasing - Index', async ({ browser }) => {
test('Purchasing - Parameters', async ({ browser }) => {
const page = await doCachedLogin(browser, {
url: 'purchasing/purchase-order/11/parameters'
url: 'purchasing/purchase-order/11/parameters',
user: stevenuser
});
// Create a new parameter against this purchase order
// We will use a "SelectionList" to choose the value here
await page
.getByRole('button', { name: 'action-menu-add-parameters' })
.click();
@@ -98,6 +98,25 @@ test('Purchasing - Parameters', async ({ browser }) => {
})
.click();
// Check for button to add a new template
await page
.getByRole('button', {
name: 'action-button-create-new-parameter-template'
})
.click();
await page
.getByText('Create New Parameter Template', { exact: true })
.first()
.waitFor();
await page.getByRole('textbox', { name: 'text-field-description' }).waitFor();
await page.getByRole('textbox', { name: 'text-field-choices' }).waitFor();
await page
.getByLabel('Create New Parameter Template')
.getByRole('button', { name: 'Cancel' })
.click();
// We will use a "SelectionList" to choose the value here
// Select the template
await page
.getByRole('combobox', { name: 'related-field-template' })