2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-25 12:33: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 * - All other attributes are optional, and may be provided by the API
* - However, they can be overridden by the user * - 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 label : The label to display for the field
* @param value : The value of the field * @param value : The value of the field
* @param default : The default 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 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 pk_field : The primary key field for the related field (default = "pk")
* @param model : The model to use for related fields * @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 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 required : Whether the field is required
* @param allow_null: Whether the field allows null values * @param error : Optional error message to display
* @param allow_blank: Whether the field allows blank values
* @param hidden : Whether the field is hidden * @param hidden : Whether the field is hidden
* @param disabled : Whether the field is disabled * @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 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 placeholder : The placeholder text to display
* @param placeholderAutofill: Whether to allow auto-filling of the placeholder value * @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 description : The description to display for the field
* @param preFieldContent : Content to render before the field * @param preFieldContent : Content to render before the field
* @param postFieldContent : Content to render after 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 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 autoFill: Whether to automatically fill the field with data from the API
* @param autoFillFilters: Optional filters to apply when auto-filling the field * @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 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 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 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) * @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 = { export type ApiFormFieldType = {
@@ -69,7 +69,7 @@ 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) { function setValueFromPK(pk: number) {
fetchSingleField(pk); fetchSingleField(pk);
} }
@@ -483,9 +483,14 @@ 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 && {addButton && modelInfo && (
modelInfo && <InlineCreateButton
InlineCreateButton(definition, modelInfo, form, setValuefromPK)} definition={definition}
modelInfo={modelInfo}
form={form}
setValue={setValueFromPK}
/>
)}
<Expand> <Expand>
<Select <Select
id={fieldId} id={fieldId}
@@ -551,20 +556,29 @@ export function RelatedModelField({
); );
} }
function InlineCreateButton( function InlineCreateButton({
definition: ApiFormFieldType, definition,
modelInfo: TranslatableModelInformationInterface, modelInfo,
form: UseFormReturn<FieldValues, any, FieldValues>, form,
setValue: (value: number) => void setValue
): ReactNode { }: {
definition: ApiFormFieldType;
modelInfo: TranslatableModelInformationInterface;
form: UseFormReturn<FieldValues, any, FieldValues>;
setValue: (value: number) => void;
}): ReactNode {
const relatedInitialData = useMemo( const relatedInitialData = useMemo(
() => calculateModalData(definition, form), () => calculateModalData(definition, form),
[definition.filters, definition.addCreateFields, 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({ const create_modal = useCreateApiFormModal({
title: t`Create New ${model}`, title: title,
url: apiUrl(modelInfo.api_endpoint), url: apiUrl(modelInfo.api_endpoint),
modelType: definition.model, modelType: definition.model,
initialData: relatedInitialData, initialData: relatedInitialData,
@@ -577,6 +591,8 @@ function InlineCreateButton(
<> <>
{create_modal.modal} {create_modal.modal}
<ActionButton <ActionButton
tooltip={title}
tooltipAlignment='top-start'
onClick={() => { onClick={() => {
create_modal.open(); create_modal.open();
}} }}
+36 -2
View File
@@ -12,6 +12,7 @@ import type {
} from '../components/render/StatusRenderer'; } from '../components/render/StatusRenderer';
import { useApi } from '../contexts/ApiContext'; import { useApi } from '../contexts/ApiContext';
import { useGlobalStatusState } from '../states/GlobalStatusState'; import { useGlobalStatusState } from '../states/GlobalStatusState';
import { useUserState } from '../states/UserState';
export function projectCodeFields(): ApiFormFieldSet { export function projectCodeFields(): ApiFormFieldSet {
return { 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({ export function useParameterFields({
modelType, modelType,
modelId modelId
@@ -106,6 +126,10 @@ export function useParameterFields({
}): ApiFormFieldSet { }): ApiFormFieldSet {
const api = useApi(); const api = useApi();
const user = useUserState.getState();
const templateCreateFields = useParameterTemplateFields();
const [selectionListId, setSelectionListId] = useState<number | null>(null); const [selectionListId, setSelectionListId] = useState<number | null>(null);
// Valid field choices // Valid field choices
@@ -193,7 +217,8 @@ export function useParameterFields({
} else if (record?.selectionlist) { } else if (record?.selectionlist) {
setFieldType('related field'); setFieldType('related field');
} }
} },
addCreateFields: user.isStaff() ? templateCreateFields : undefined
}, },
data: { data: {
value: data, value: data,
@@ -244,5 +269,14 @@ export function useParameterFields({
}, },
note: {} 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: { filters: {
active: true, active: true,
is_supplier: true is_supplier: true
},
addCreateFields: {
name: {},
description: {},
is_supplier: { value: true, hidden: true }
} }
}, },
SKU: { SKU: {
@@ -88,6 +93,11 @@ export function useManufacturerPartFields() {
filters: { filters: {
active: true, active: true,
is_manufacturer: true is_manufacturer: true
},
addCreateFields: {
name: {},
description: {},
is_manufacturer: { value: true, hidden: true }
} }
}, },
MPN: {}, MPN: {},
@@ -247,6 +247,11 @@ export function usePurchaseOrderFields({
filters: { filters: {
is_supplier: true, is_supplier: true,
active: true active: true
},
addCreateFields: {
name: {},
description: {},
is_supplier: { value: true, hidden: true }
} }
}, },
supplier_reference: {}, supplier_reference: {},
@@ -48,6 +48,11 @@ export function useSalesOrderFields({
filters: { filters: {
is_customer: true, is_customer: true,
active: true active: true
},
addCreateFields: {
name: {},
description: {},
is_customer: { value: true, hidden: true }
} }
}, },
customer_reference: {}, customer_reference: {},
@@ -13,6 +13,7 @@ import type { TableFilter } from '@lib/types/Filters';
import type { RowAction, TableColumn } from '@lib/types/Tables'; import type { RowAction, TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useParameterTemplateFields } from '../../forms/CommonForms';
import { useFilters } from '../../hooks/UseFilter'; import { useFilters } from '../../hooks/UseFilter';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
@@ -30,22 +31,7 @@ export default function ParameterTemplateTable() {
const table = useTable('parameter-templates'); const table = useTable('parameter-templates');
const user = useUserState(); const user = useUserState();
const parameterTemplateFields: ApiFormFieldSet = useMemo(() => { const parameterTemplateFields: ApiFormFieldSet = useParameterTemplateFields();
return {
name: {},
description: {},
units: {},
model_type: {},
choices: {},
checkbox: {},
selectionlist: {
filters: {
active: true
}
},
enabled: {}
};
}, []);
const newTemplate = useCreateApiFormModal({ const newTemplate = useCreateApiFormModal({
url: ApiEndpoints.parameter_template_list, url: ApiEndpoints.parameter_template_list,
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
import { test } from '../baseFixtures.ts'; import { test } from '../baseFixtures.ts';
import { readeruser } from '../defaults.ts'; import { readeruser, stevenuser } from '../defaults.ts';
import { import {
activateCalendarView, activateCalendarView,
activateTableView, activateTableView,
@@ -84,11 +84,11 @@ test('Purchasing - Index', async ({ browser }) => {
test('Purchasing - Parameters', async ({ browser }) => { test('Purchasing - Parameters', async ({ browser }) => {
const page = await doCachedLogin(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 // Create a new parameter against this purchase order
// We will use a "SelectionList" to choose the value here
await page await page
.getByRole('button', { name: 'action-menu-add-parameters' }) .getByRole('button', { name: 'action-menu-add-parameters' })
.click(); .click();
@@ -98,6 +98,25 @@ test('Purchasing - Parameters', async ({ browser }) => {
}) })
.click(); .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 // Select the template
await page await page
.getByRole('combobox', { name: 'related-field-template' }) .getByRole('combobox', { name: 'related-field-template' })