mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-30 16:41:35 +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';
|
} from 'react-hook-form';
|
||||||
import { type NavigateFunction, useNavigate } from 'react-router-dom';
|
import { type NavigateFunction, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { isTrue } from '@lib/functions/Conversion';
|
||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
import type {
|
import type {
|
||||||
ApiFormFieldSet,
|
ApiFormFieldSet,
|
||||||
@@ -372,6 +373,11 @@ export function ApiForm({
|
|||||||
hasFiles = true;
|
hasFiles = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure any boolean values are actually boolean
|
||||||
|
if (field_type === 'boolean') {
|
||||||
|
value = isTrue(value) || false;
|
||||||
|
}
|
||||||
|
|
||||||
// Stringify any JSON objects
|
// Stringify any JSON objects
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
switch (field_type) {
|
switch (field_type) {
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
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 { useId } from '@mantine/hooks';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { type Control, type FieldValues, useController } from 'react-hook-form';
|
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 type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
|
||||||
|
import { BooleanField } from './BooleanField';
|
||||||
import { ChoiceField } from './ChoiceField';
|
import { ChoiceField } from './ChoiceField';
|
||||||
import DateField from './DateField';
|
import DateField from './DateField';
|
||||||
import { DependentField } from './DependentField';
|
import { DependentField } from './DependentField';
|
||||||
@@ -126,11 +126,6 @@ export function ApiFormField({
|
|||||||
return val;
|
return val;
|
||||||
}, [definition.field_type, value]);
|
}, [definition.field_type, value]);
|
||||||
|
|
||||||
// Coerce the value to a (stringified) boolean value
|
|
||||||
const booleanValue: boolean = useMemo(() => {
|
|
||||||
return isTrue(value);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
// Construct the individual field
|
// Construct the individual field
|
||||||
const fieldInstance = useMemo(() => {
|
const fieldInstance = useMemo(() => {
|
||||||
switch (fieldDefinition.field_type) {
|
switch (fieldDefinition.field_type) {
|
||||||
@@ -174,16 +169,13 @@ export function ApiFormField({
|
|||||||
);
|
);
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return (
|
return (
|
||||||
<Switch
|
<BooleanField
|
||||||
{...reducedDefinition}
|
controller={controller}
|
||||||
checked={booleanValue}
|
definition={reducedDefinition}
|
||||||
ref={ref}
|
fieldName={fieldName}
|
||||||
id={fieldId}
|
onChange={(value: boolean) => {
|
||||||
aria-label={`boolean-field-${fieldName}`}
|
onChange(value);
|
||||||
radius='lg'
|
}}
|
||||||
size='sm'
|
|
||||||
error={definition.error ?? error?.message}
|
|
||||||
onChange={(event: any) => onChange(event.currentTarget.checked)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'date':
|
case 'date':
|
||||||
@@ -273,7 +265,6 @@ export function ApiFormField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
booleanValue,
|
|
||||||
control,
|
control,
|
||||||
controller,
|
controller,
|
||||||
definition,
|
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}`;
|
quantity_string = `${t`Quantity`}: ${instance.quantity}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('item:', instance);
|
|
||||||
|
|
||||||
let batch_string = '';
|
let batch_string = '';
|
||||||
|
|
||||||
if (!!instance.batch) {
|
if (!!instance.batch) {
|
||||||
|
@@ -245,10 +245,19 @@ export function usePartParameterFields({
|
|||||||
type: fieldType,
|
type: fieldType,
|
||||||
field_type: fieldType,
|
field_type: fieldType,
|
||||||
choices: fieldType === 'choice' ? choices : undefined,
|
choices: fieldType === 'choice' ? choices : undefined,
|
||||||
default: fieldType === 'boolean' ? 'false' : undefined,
|
default: fieldType === 'boolean' ? false : undefined,
|
||||||
adjustValue: (value: any) => {
|
adjustValue: (value: any) => {
|
||||||
// Coerce boolean value into a string (required by backend)
|
// 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: {}
|
note: {}
|
||||||
|
@@ -463,14 +463,41 @@ test('Parts - Parameters', async ({ browser }) => {
|
|||||||
// Select the "polarized" parameter template (should create a "checkbox" field)
|
// Select the "polarized" parameter template (should create a "checkbox" field)
|
||||||
await page.getByLabel('related-field-template').fill('Polarized');
|
await page.getByLabel('related-field-template').fill('Polarized');
|
||||||
await page.getByText('Is this part polarized?').click();
|
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
|
await page
|
||||||
.locator('label')
|
.locator('label')
|
||||||
.filter({ hasText: 'DataParameter Value' })
|
.filter({ hasText: 'DataParameter Value' })
|
||||||
.locator('div')
|
.locator('div')
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.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 }) => {
|
test('Parts - Parameter Filtering', async ({ browser }) => {
|
||||||
|
Reference in New Issue
Block a user