2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

Forms fixes (#8722)

* Refactor form fields

- Allow error message to be passed through via field definition
- Return error information to onFormError

* Fix debounce issue for text fields

* Fix for useForm hook

* Badge fix

- Fix badge rendering for SalesOrderShipment

* Cleanup unit test
This commit is contained in:
Oliver 2024-12-20 14:53:39 +11:00 committed by GitHub
parent 68ac4118e9
commit aabcf52cd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 34 additions and 24 deletions

View File

@ -94,7 +94,7 @@ export interface ApiFormProps {
postFormContent?: JSX.Element; postFormContent?: JSX.Element;
successMessage?: string; successMessage?: string;
onFormSuccess?: (data: any) => void; onFormSuccess?: (data: any) => void;
onFormError?: () => void; onFormError?: (response: any) => void;
processFormData?: (data: any) => any; processFormData?: (data: any) => any;
table?: TableState; table?: TableState;
modelType?: ModelType; modelType?: ModelType;
@ -482,7 +482,7 @@ export function ApiForm({
default: default:
// Unexpected state on form success // Unexpected state on form success
invalidResponse(response.status); invalidResponse(response.status);
props.onFormError?.(); props.onFormError?.(response);
break; break;
} }
@ -534,26 +534,30 @@ export function ApiForm({
processErrors(error.response.data); processErrors(error.response.data);
setNonFieldErrors(_nonFieldErrors); setNonFieldErrors(_nonFieldErrors);
props.onFormError?.(error);
break; break;
default: default:
// Unexpected state on form error // Unexpected state on form error
invalidResponse(error.response.status); invalidResponse(error.response.status);
props.onFormError?.(); props.onFormError?.(error);
break; break;
} }
} else { } else {
showTimeoutNotification(); showTimeoutNotification();
props.onFormError?.(); props.onFormError?.(error);
} }
return error; return error;
}); });
}; };
const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(() => { const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(
props.onFormError?.(); (error: any) => {
}, [props.onFormError]); props.onFormError?.(error);
},
[props.onFormError]
);
if (optionsLoading || initialDataQuery.isFetching) { if (optionsLoading || initialDataQuery.isFetching) {
return ( return (

View File

@ -46,6 +46,7 @@ export type ApiFormFieldChoice = {
* @param required : Whether the field is required * @param required : Whether the field is required
* @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 exclude : Whether to exclude the field from the submitted data * @param exclude : Whether to exclude the field from the submitted data
* @param placeholder : The placeholder text to display * @param placeholder : The placeholder text to display
* @param description : The description to display for the field * @param description : The description to display for the field
@ -88,6 +89,7 @@ export type ApiFormFieldType = {
child?: ApiFormFieldType; child?: ApiFormFieldType;
children?: { [key: string]: ApiFormFieldType }; children?: { [key: string]: ApiFormFieldType };
required?: boolean; required?: boolean;
error?: string;
choices?: ApiFormFieldChoice[]; choices?: ApiFormFieldChoice[];
hidden?: boolean; hidden?: boolean;
disabled?: boolean; disabled?: boolean;
@ -256,7 +258,7 @@ export function ApiFormField({
aria-label={`boolean-field-${fieldName}`} aria-label={`boolean-field-${fieldName}`}
radius='lg' radius='lg'
size='sm' size='sm'
error={error?.message} error={definition.error ?? error?.message}
onChange={(event) => onChange(event.currentTarget.checked)} onChange={(event) => onChange(event.currentTarget.checked)}
/> />
); );
@ -277,7 +279,7 @@ export function ApiFormField({
id={fieldId} id={fieldId}
aria-label={`number-field-${field.name}`} aria-label={`number-field-${field.name}`}
value={numericalValue} value={numericalValue}
error={error?.message} error={definition.error ?? error?.message}
decimalScale={definition.field_type == 'integer' ? 0 : 10} decimalScale={definition.field_type == 'integer' ? 0 : 10}
onChange={(value: number | string | null) => onChange(value)} onChange={(value: number | string | null) => onChange(value)}
step={1} step={1}
@ -299,7 +301,7 @@ export function ApiFormField({
ref={field.ref} ref={field.ref}
radius='sm' radius='sm'
value={value} value={value}
error={error?.message} error={definition.error ?? error?.message}
onChange={(payload: File | null) => onChange(payload)} onChange={(payload: File | null) => onChange(payload)}
/> />
); );
@ -343,6 +345,7 @@ export function ApiFormField({
booleanValue, booleanValue,
control, control,
controller, controller,
definition,
field, field,
fieldId, fieldId,
fieldName, fieldName,

View File

@ -63,7 +63,7 @@ export function ChoiceField({
<Select <Select
id={fieldId} id={fieldId}
aria-label={`choice-field-${field.name}`} aria-label={`choice-field-${field.name}`}
error={error?.message} error={definition.error ?? error?.message}
radius='sm' radius='sm'
{...field} {...field}
onChange={onChange} onChange={onChange}

View File

@ -61,7 +61,7 @@ export default function DateField({
radius='sm' radius='sm'
ref={field.ref} ref={field.ref}
type={undefined} type={undefined}
error={error?.message} error={definition.error ?? error?.message}
value={dateValue ?? null} value={dateValue ?? null}
clearable={!definition.required} clearable={!definition.required}
onChange={onChange} onChange={onChange}

View File

@ -50,7 +50,7 @@ export default function IconField({
label={definition.label} label={definition.label}
description={definition.description} description={definition.description}
required={definition.required} required={definition.required}
error={error?.message} error={definition.error ?? error?.message}
ref={field.ref} ref={field.ref}
component='button' component='button'
type='button' type='button'

View File

@ -284,7 +284,7 @@ export function RelatedModelField({
return ( return (
<Input.Wrapper <Input.Wrapper
{...fieldDefinition} {...fieldDefinition}
error={error?.message} error={definition.error ?? error?.message}
styles={{ description: { paddingBottom: '5px' } }} styles={{ description: { paddingBottom: '5px' } }}
> >
<Select <Select

View File

@ -258,7 +258,7 @@ export function TableFieldExtraRow({
fieldName={fieldName ?? 'field'} fieldName={fieldName ?? 'field'}
fieldDefinition={field} fieldDefinition={field}
defaultValue={defaultValue} defaultValue={defaultValue}
error={error} error={fieldDefinition.error ?? error}
/> />
</Group> </Group>
</Table.Td> </Table.Td>

View File

@ -56,7 +56,7 @@ export default function TextField({
aria-label={`text-field-${field.name}`} aria-label={`text-field-${field.name}`}
type={definition.field_type} type={definition.field_type}
value={rawText || ''} value={rawText || ''}
error={error?.message} error={definition.error ?? error?.message}
radius='sm' radius='sm'
onChange={(event) => onTextChange(event.currentTarget.value)} onChange={(event) => onTextChange(event.currentTarget.value)}
onBlur={(event) => { onBlur={(event) => {
@ -64,7 +64,13 @@ export default function TextField({
onChange(event.currentTarget.value); onChange(event.currentTarget.value);
} }
}} }}
onKeyDown={(event) => onKeyDown(event.code)} onKeyDown={(event) => {
if (event.code === 'Enter') {
// Bypass debounce on enter key
onChange(event.currentTarget.value);
}
onKeyDown(event.code);
}}
rightSection={ rightSection={
value && !definition.required ? ( value && !definition.required ? (
<IconX size='1rem' color='red' onClick={() => onTextChange('')} /> <IconX size='1rem' color='red' onClick={() => onTextChange('')} />

View File

@ -48,9 +48,8 @@ export function useApiFormModal(props: ApiFormModalProps) {
modalClose.current(); modalClose.current();
props.onFormSuccess?.(data); props.onFormSuccess?.(data);
}, },
onFormError: () => { onFormError: (error: any) => {
modalClose.current(); props.onFormError?.(error);
props.onFormError?.();
} }
}), }),
[props] [props]

View File

@ -90,10 +90,8 @@ test('Build Order - Basic Tests', async ({ page }) => {
test('Build Order - Build Outputs', async ({ page }) => { test('Build Order - Build Outputs', async ({ page }) => {
await doQuickLogin(page); await doQuickLogin(page);
await page.goto(`${baseUrl}/part/`); await page.goto(`${baseUrl}/manufacturing/index/`);
await page.getByRole('tab', { name: 'Build Orders', exact: true }).click();
// Navigate to the correct build order
await page.getByRole('tab', { name: 'Manufacturing', exact: true }).click();
// We have now loaded the "Build Order" table. Check for some expected texts // We have now loaded the "Build Order" table. Check for some expected texts
await page.getByText('On Hold').first().waitFor(); await page.getByText('On Hold').first().waitFor();