2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-30 00:21:34 +00:00

[bug] Part param edit (#10059)

* Fix for BooleanField

- Ensure that an "undefined" value reads "false" by default

* Tweak part parameter form

* Enhanced playwright tests

* Better boolean field management

* Update src/frontend/src/forms/PartForms.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/frontend/src/components/forms/ApiForm.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Oliver
2025-07-23 18:31:39 +10:00
committed by GitHub
parent 283d5d6050
commit 89279ef091
6 changed files with 99 additions and 23 deletions

View File

@@ -22,6 +22,7 @@ import {
} from 'react-hook-form';
import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { isTrue } from '@lib/functions/Conversion';
import { getDetailUrl } from '@lib/functions/Navigation';
import type {
ApiFormFieldSet,
@@ -372,6 +373,11 @@ export function ApiForm({
hasFiles = true;
}
// Ensure any boolean values are actually boolean
if (field_type === 'boolean') {
value = isTrue(value) || false;
}
// Stringify any JSON objects
if (typeof value === 'object') {
switch (field_type) {

View File

@@ -1,11 +1,11 @@
import { t } from '@lingui/core/macro';
import { Alert, FileInput, NumberInput, Stack, Switch } from '@mantine/core';
import { Alert, FileInput, NumberInput, Stack } from '@mantine/core';
import { useId } from '@mantine/hooks';
import { useCallback, useEffect, useMemo } from 'react';
import { type Control, type FieldValues, useController } from 'react-hook-form';
import { isTrue } from '@lib/functions/Conversion';
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
import { BooleanField } from './BooleanField';
import { ChoiceField } from './ChoiceField';
import DateField from './DateField';
import { DependentField } from './DependentField';
@@ -126,11 +126,6 @@ export function ApiFormField({
return val;
}, [definition.field_type, value]);
// Coerce the value to a (stringified) boolean value
const booleanValue: boolean = useMemo(() => {
return isTrue(value);
}, [value]);
// Construct the individual field
const fieldInstance = useMemo(() => {
switch (fieldDefinition.field_type) {
@@ -174,16 +169,13 @@ export function ApiFormField({
);
case 'boolean':
return (
<Switch
{...reducedDefinition}
checked={booleanValue}
ref={ref}
id={fieldId}
aria-label={`boolean-field-${fieldName}`}
radius='lg'
size='sm'
error={definition.error ?? error?.message}
onChange={(event: any) => onChange(event.currentTarget.checked)}
<BooleanField
controller={controller}
definition={reducedDefinition}
fieldName={fieldName}
onChange={(value: boolean) => {
onChange(value);
}}
/>
);
case 'date':
@@ -273,7 +265,6 @@ export function ApiFormField({
);
}
}, [
booleanValue,
control,
controller,
definition,

View File

@@ -0,0 +1,45 @@
import { isTrue } from '@lib/functions/Conversion';
import type { ApiFormFieldType } from '@lib/types/Forms';
import { Switch } from '@mantine/core';
import { useId } from '@mantine/hooks';
import { useMemo } from 'react';
import type { FieldValues, UseControllerReturn } from 'react-hook-form';
export function BooleanField({
controller,
definition,
fieldName,
onChange
}: Readonly<{
controller: UseControllerReturn<FieldValues, any>;
definition: ApiFormFieldType;
fieldName: string;
onChange: (value: boolean) => void;
}>) {
const fieldId = useId();
const {
field,
fieldState: { error }
} = controller;
const { value } = field;
// Coerce the value to a (stringified) boolean value
const booleanValue: boolean = useMemo(() => {
return isTrue(value);
}, [value]);
return (
<Switch
{...definition}
checked={booleanValue}
id={fieldId}
aria-label={`boolean-field-${fieldName}`}
radius='lg'
size='sm'
error={definition.error ?? error?.message}
onChange={(event: any) => onChange(event.currentTarget.checked || false)}
/>
);
}

View File

@@ -68,8 +68,6 @@ export function RenderStockItem(
quantity_string = `${t`Quantity`}: ${instance.quantity}`;
}
console.log('item:', instance);
let batch_string = '';
if (!!instance.batch) {

View File

@@ -245,10 +245,19 @@ export function usePartParameterFields({
type: fieldType,
field_type: fieldType,
choices: fieldType === 'choice' ? choices : undefined,
default: fieldType === 'boolean' ? 'false' : undefined,
default: fieldType === 'boolean' ? false : undefined,
adjustValue: (value: any) => {
// Coerce boolean value into a string (required by backend)
return value.toString();
let v: string = value.toString().trim();
if (fieldType === 'boolean') {
if (v.toLowerCase() !== 'true') {
v = 'false';
}
}
return v;
}
},
note: {}

View File

@@ -463,14 +463,41 @@ test('Parts - Parameters', async ({ browser }) => {
// Select the "polarized" parameter template (should create a "checkbox" field)
await page.getByLabel('related-field-template').fill('Polarized');
await page.getByText('Is this part polarized?').click();
// Submit with "false" value
await page.getByRole('button', { name: 'Submit' }).click();
// Check for the expected values in the table
let row = await getRowFromCell(
await page.getByRole('cell', { name: 'Polarized', exact: true })
);
await row.getByRole('cell', { name: 'No', exact: true }).waitFor();
await row.getByRole('cell', { name: 'allaccess' }).waitFor();
await row.getByLabel(/row-action-menu-/i).click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
// Toggle false to true
await page
.locator('label')
.filter({ hasText: 'DataParameter Value' })
.locator('div')
.first()
.click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('button', { name: 'Cancel' }).click();
row = await getRowFromCell(
await page.getByRole('cell', { name: 'Polarized', exact: true })
);
await row.getByRole('cell', { name: 'Yes', exact: true }).waitFor();
await page.getByText('1 - 1 / 1').waitFor();
// Finally, delete the parameter
await row.getByLabel(/row-action-menu-/i).click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByText('No records found').first().waitFor();
});
test('Parts - Parameter Filtering', async ({ browser }) => {