From afa4bb5b9656444a48e93be0208f19d2d15d1c6e Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 22 May 2024 15:24:35 +1000 Subject: [PATCH] [PUI] Dynamic PartParameter field (#7298) * Add 'adjustValue' callback for form field * Cast checkbox values to boolean * Call "onChange" callbacks * Implement dynamic "data" field for PartParameter dialog - Type of field changes based on selected template * Add playwright unit tests * Add labels to table row actions * linting fixes * Adjust playwright tests --- .../src/components/buttons/ActionButton.tsx | 8 +- src/frontend/src/components/forms/ApiForm.tsx | 4 + .../components/forms/fields/ApiFormField.tsx | 14 ++++ .../forms/fields/RelatedModelField.tsx | 5 ++ src/frontend/src/forms/PartForms.tsx | 58 ++++++++++++++- src/frontend/src/tables/InvenTreeTable.tsx | 3 +- src/frontend/src/tables/RowActions.tsx | 6 +- .../src/tables/general/AttachmentTable.tsx | 73 +++++++------------ .../src/tables/part/PartParameterTable.tsx | 12 +-- src/frontend/tests/pages/pui_part.spec.ts | 39 +++++++++- 10 files changed, 160 insertions(+), 62 deletions(-) diff --git a/src/frontend/src/components/buttons/ActionButton.tsx b/src/frontend/src/components/buttons/ActionButton.tsx index ba541b6ad3..089cb98995 100644 --- a/src/frontend/src/components/buttons/ActionButton.tsx +++ b/src/frontend/src/components/buttons/ActionButton.tsx @@ -1,6 +1,7 @@ import { ActionIcon, FloatingPosition, Group, Tooltip } from '@mantine/core'; import { ReactNode } from 'react'; +import { identifierString } from '../../functions/conversion'; import { notYetImplemented } from '../../functions/notifications'; export type ActionButtonProps = { @@ -26,18 +27,21 @@ export function ActionButton(props: ActionButtonProps) { return ( !hidden && ( diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 2ef68b4281..2317f3e8e0 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -277,6 +277,10 @@ export function ApiForm({ res[k] = processFields(field.children, dataValue); } else { res[k] = dataValue; + + if (field.onValueChange) { + field.onValueChange(dataValue, data); + } } } diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index be86befa10..f6a71b094d 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -52,6 +52,7 @@ export type ApiFormAdjustFilterType = { * @param postFieldContent : Content to render after the field * @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 */ export type ApiFormFieldType = { label?: string; @@ -89,6 +90,7 @@ export type ApiFormFieldType = { description?: string; preFieldContent?: JSX.Element; postFieldContent?: JSX.Element; + adjustValue?: (value: any) => any; onValueChange?: (value: any, record?: any) => void; adjustFilters?: (value: ApiFormAdjustFilterType) => any; headers?: string[]; @@ -133,6 +135,7 @@ export function ApiFormField({ ...definition, onValueChange: undefined, adjustFilters: undefined, + adjustValue: undefined, read_only: undefined, children: undefined }; @@ -141,6 +144,11 @@ export function ApiFormField({ // Callback helper when form value changes const onChange = useCallback( (value: any) => { + // Allow for custom value adjustments (per field) + if (definition.adjustValue) { + value = definition.adjustValue(value); + } + field.onChange(value); // Run custom callback for this field @@ -173,6 +181,11 @@ export function ApiFormField({ return val; }, [value]); + // Coerce the value to a (stringified) boolean value + const booleanValue: string = useMemo(() => { + return isTrue(value).toString(); + }, [value]); + // Construct the individual field function buildField() { switch (definition.field_type) { @@ -209,6 +222,7 @@ export function ApiFormField({ return ( ([]); + + // Field type for "data" input + const [fieldType, setFieldType] = useState<'string' | 'boolean' | 'choice'>( + 'string' + ); + + return useMemo(() => { + return { + part: { + disabled: true + }, + template: { + onValueChange: (value: any, record: any) => { + // Adjust the type of the "data" field based on the selected template + if (record?.checkbox) { + // This is a "checkbox" field + setChoices([]); + setFieldType('boolean'); + } else if (record?.choices) { + let _choices: string[] = record.choices.split(','); + + if (_choices.length > 0) { + setChoices( + _choices.map((choice) => { + return { + label: choice.trim(), + value: choice.trim() + }; + }) + ); + setFieldType('choice'); + } else { + setChoices([]); + setFieldType('string'); + } + } else { + setChoices([]); + setFieldType('string'); + } + } + }, + data: { + field_type: fieldType, + choices: fieldType === 'choice' ? choices : undefined, + adjustValue: (value: any) => { + // Coerce boolean value into a string (required by backend) + return value.toString(); + } + } + }; + }, [fieldType, choices]); +} diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index dbf07b7269..47d26be023 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -263,10 +263,11 @@ export function InvenTreeTable({ hidden: false, switchable: false, width: 50, - render: (record: any) => ( + render: (record: any, index?: number | undefined) => ( 0} + index={index} /> ) }); diff --git a/src/frontend/src/tables/RowActions.tsx b/src/frontend/src/tables/RowActions.tsx index a30eb5bedd..dbd3beb144 100644 --- a/src/frontend/src/tables/RowActions.tsx +++ b/src/frontend/src/tables/RowActions.tsx @@ -84,11 +84,13 @@ export function RowDeleteAction({ export function RowActions({ title, actions, - disabled = false + disabled = false, + index }: { title?: string; disabled?: boolean; actions: RowAction[]; + index?: number; }): ReactNode { // Prevent default event handling // Ref: https://icflorescu.github.io/mantine-datatable/examples/links-or-buttons-inside-clickable-rows-or-cells @@ -146,6 +148,8 @@ export function RowActions({ { - let actions = []; - - if (allowEdit) { - actions.push( - - { - setAttachmentType('attachment'); - setSelectedAttachment(undefined); - uploadAttachment.open(); - }} - variant="transparent" - > - - - - ); - - actions.push( - - { - setAttachmentType('link'); - setSelectedAttachment(undefined); - uploadAttachment.open(); - }} - variant="transparent" - > - - - - ); - } - - return actions; + return [ +