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:
@@ -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) {
|
||||
|
@@ -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,
|
||||
|
45
src/frontend/src/components/forms/fields/BooleanField.tsx
Normal file
45
src/frontend/src/components/forms/fields/BooleanField.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -68,8 +68,6 @@ export function RenderStockItem(
|
||||
quantity_string = `${t`Quantity`}: ${instance.quantity}`;
|
||||
}
|
||||
|
||||
console.log('item:', instance);
|
||||
|
||||
let batch_string = '';
|
||||
|
||||
if (!!instance.batch) {
|
||||
|
@@ -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: {}
|
||||
|
@@ -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 }) => {
|
||||
|
Reference in New Issue
Block a user