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:
@@ -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();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
Reference in New Issue
Block a user