mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 02:47:49 +00:00
- Fetch appropriate 'tags' detail in the form request - Additional playwright tests
345 lines
9.7 KiB
TypeScript
345 lines
9.7 KiB
TypeScript
import { t } from '@lingui/core/macro';
|
|
import { Alert, FileInput, 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 { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|
import { ModelType } from '@lib/enums/ModelType';
|
|
import { apiUrl } from '@lib/functions/Api';
|
|
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
|
|
import { IconFileUpload } from '@tabler/icons-react';
|
|
import type { NavigateFunction } from 'react-router-dom';
|
|
import DateTimeField from '../DateTimeField';
|
|
import { BooleanField } from './BooleanField';
|
|
import { ChoiceField } from './ChoiceField';
|
|
import DateField from './DateField';
|
|
import { DependentField } from './DependentField';
|
|
import IconField from './IconField';
|
|
import { JsonField } from './JsonField';
|
|
import { NestedObjectField } from './NestedObjectField';
|
|
import NumberField from './NumberField';
|
|
import { RelatedModelField } from './RelatedModelField';
|
|
import { TableField } from './TableField';
|
|
import TagsField from './TagsField';
|
|
import TextField from './TextField';
|
|
import { TreeField } from './TreeField';
|
|
|
|
/**
|
|
* Render an individual form field
|
|
*/
|
|
export function ApiFormField({
|
|
fieldName,
|
|
definition,
|
|
control,
|
|
hideLabels,
|
|
navigate,
|
|
url,
|
|
setFields,
|
|
onKeyDown
|
|
}: Readonly<{
|
|
fieldName: string;
|
|
definition: ApiFormFieldType;
|
|
control: Control<FieldValues, any>;
|
|
hideLabels?: boolean;
|
|
navigate?: NavigateFunction | null;
|
|
url?: string;
|
|
setFields?: React.Dispatch<React.SetStateAction<ApiFormFieldSet>>;
|
|
onKeyDown?: (value: any) => void;
|
|
}>) {
|
|
const fieldId = useId();
|
|
const controller = useController({
|
|
name: fieldName,
|
|
control
|
|
});
|
|
const {
|
|
field,
|
|
fieldState: { error }
|
|
} = controller;
|
|
const { value, ref } = field;
|
|
|
|
useEffect(() => {
|
|
if (
|
|
definition.field_type === 'nested object' ||
|
|
definition.field_type === 'dependent field'
|
|
)
|
|
return;
|
|
|
|
// hook up the value state to the input field
|
|
if (definition.value !== undefined) {
|
|
field.onChange(
|
|
definition.adjustValue
|
|
? definition.adjustValue(definition.value)
|
|
: definition.value
|
|
);
|
|
}
|
|
}, [definition.value, definition.field_type]);
|
|
|
|
const fieldDefinition: ApiFormFieldType = useMemo(() => {
|
|
return {
|
|
...definition,
|
|
label: hideLabels ? undefined : definition.label,
|
|
description: hideLabels ? undefined : definition.description
|
|
};
|
|
}, [hideLabels, definition]);
|
|
|
|
// pull out onValueChange as this can cause strange errors when passing the
|
|
// definition to the input components via spread syntax
|
|
const reducedDefinition = useMemo(() => {
|
|
return {
|
|
...fieldDefinition,
|
|
autoFill: undefined,
|
|
placeholderAutofill: undefined,
|
|
placeholderWarning: undefined,
|
|
placeholderWarningCompare: undefined,
|
|
singleFetchFunction: undefined,
|
|
fetchParams: undefined,
|
|
autoFillFilters: undefined,
|
|
onValueChange: undefined,
|
|
adjustFilters: undefined,
|
|
adjustValue: undefined,
|
|
allow_blank: undefined,
|
|
allow_null: undefined,
|
|
read_only: undefined,
|
|
children: undefined,
|
|
exclude: undefined
|
|
};
|
|
}, [fieldDefinition]);
|
|
|
|
// Callback helper when form value changes
|
|
const onChange = useCallback(
|
|
(value: any) => {
|
|
let rtnValue: any = value;
|
|
// Allow for custom value adjustments (per field)
|
|
if (definition.adjustValue) {
|
|
rtnValue = definition.adjustValue(value);
|
|
}
|
|
|
|
field.onChange(rtnValue);
|
|
|
|
// Run custom callback for this field
|
|
definition.onValueChange?.(rtnValue);
|
|
},
|
|
[fieldName, definition]
|
|
);
|
|
|
|
// Stable wrapper so the identity passed to leaf field components does not
|
|
// change unless onKeyDown itself changes (onKeyDown may be undefined)
|
|
const safeOnKeyDown = useCallback(
|
|
(value: any) => {
|
|
onKeyDown?.(value);
|
|
},
|
|
[onKeyDown]
|
|
);
|
|
|
|
// Construct the individual field
|
|
const fieldInstance = useMemo(() => {
|
|
switch (fieldDefinition.field_type) {
|
|
case 'related field':
|
|
if (
|
|
fieldDefinition.api_url === apiUrl(ApiEndpoints.stock_location_list)
|
|
) {
|
|
// Redirect location fields to the appropriate tree field
|
|
return (
|
|
<TreeField
|
|
controller={controller}
|
|
definition={fieldDefinition}
|
|
fieldName={fieldName}
|
|
endpoint={ApiEndpoints.stock_location_tree}
|
|
childIdentifier='sublocations'
|
|
genericPlaceholder={t`Select location`}
|
|
model={ModelType.stocklocation}
|
|
navigate={navigate}
|
|
/>
|
|
);
|
|
} else if (
|
|
fieldDefinition.api_url === apiUrl(ApiEndpoints.category_list)
|
|
) {
|
|
// Redirect category fields to the appropriate tree field
|
|
return (
|
|
<TreeField
|
|
controller={controller}
|
|
definition={fieldDefinition}
|
|
fieldName={fieldName}
|
|
endpoint={ApiEndpoints.category_tree}
|
|
childIdentifier='subcategories'
|
|
genericPlaceholder={t`Select category`}
|
|
model={ModelType.partcategory}
|
|
navigate={navigate}
|
|
/>
|
|
);
|
|
} else {
|
|
return (
|
|
<RelatedModelField
|
|
definition={fieldDefinition}
|
|
controller={controller}
|
|
fieldName={fieldName}
|
|
navigate={navigate}
|
|
/>
|
|
);
|
|
}
|
|
case 'email':
|
|
case 'url':
|
|
case 'string':
|
|
return (
|
|
<TextField
|
|
definition={reducedDefinition}
|
|
placeholderAutofill={fieldDefinition.placeholderAutofill ?? false}
|
|
controller={controller}
|
|
fieldName={fieldName}
|
|
onChange={onChange}
|
|
onKeyDown={safeOnKeyDown}
|
|
/>
|
|
);
|
|
case 'password':
|
|
return (
|
|
<TextField
|
|
definition={{ ...reducedDefinition, type: 'password' }}
|
|
controller={controller}
|
|
fieldName={fieldName}
|
|
onChange={onChange}
|
|
onKeyDown={safeOnKeyDown}
|
|
/>
|
|
);
|
|
case 'icon':
|
|
return (
|
|
<IconField definition={fieldDefinition} controller={controller} />
|
|
);
|
|
case 'boolean':
|
|
return (
|
|
<BooleanField
|
|
controller={controller}
|
|
definition={reducedDefinition}
|
|
fieldName={fieldName}
|
|
onChange={onChange}
|
|
/>
|
|
);
|
|
case 'date':
|
|
return (
|
|
<DateField controller={controller} definition={fieldDefinition} />
|
|
);
|
|
case 'datetime':
|
|
return (
|
|
<DateTimeField controller={controller} definition={fieldDefinition} />
|
|
);
|
|
case 'integer':
|
|
case 'decimal':
|
|
case 'float':
|
|
case 'number':
|
|
return (
|
|
<NumberField
|
|
controller={controller}
|
|
fieldName={fieldName}
|
|
definition={reducedDefinition}
|
|
placeholderAutofill={fieldDefinition.placeholderAutofill ?? false}
|
|
placeholderWarningCompare={
|
|
fieldDefinition.placeholderWarningCompare ?? undefined
|
|
}
|
|
placeholderWarning={fieldDefinition.placeholderWarning ?? undefined}
|
|
onChange={onChange}
|
|
/>
|
|
);
|
|
case 'choice':
|
|
return (
|
|
<ChoiceField
|
|
controller={controller}
|
|
fieldName={fieldName}
|
|
definition={fieldDefinition}
|
|
/>
|
|
);
|
|
case 'file upload':
|
|
return (
|
|
<FileInput
|
|
{...reducedDefinition}
|
|
clearable={!definition.required}
|
|
aria-label={`file-field-${fieldName}`}
|
|
placeholder={definition.placeholder ?? t`Select file to upload`}
|
|
leftSection={<IconFileUpload />}
|
|
id={fieldId}
|
|
ref={field.ref}
|
|
radius='sm'
|
|
value={value}
|
|
error={definition.error ?? error?.message}
|
|
onChange={(payload: File | null) => onChange(payload)}
|
|
/>
|
|
);
|
|
case 'nested object':
|
|
return (
|
|
<NestedObjectField
|
|
definition={fieldDefinition}
|
|
fieldName={fieldName}
|
|
control={control}
|
|
url={url}
|
|
setFields={setFields}
|
|
/>
|
|
);
|
|
case 'dependent field':
|
|
return (
|
|
<DependentField
|
|
definition={fieldDefinition}
|
|
fieldName={fieldName}
|
|
control={control}
|
|
url={url}
|
|
setFields={setFields}
|
|
/>
|
|
);
|
|
case 'table':
|
|
return (
|
|
<TableField
|
|
definition={fieldDefinition}
|
|
fieldName={fieldName}
|
|
value={value}
|
|
onChange={field.onChange}
|
|
error={error}
|
|
/>
|
|
);
|
|
case 'tags':
|
|
return (
|
|
<TagsField controller={controller} definition={fieldDefinition} />
|
|
);
|
|
case 'json':
|
|
return (
|
|
<JsonField
|
|
controller={controller}
|
|
definition={fieldDefinition}
|
|
fieldName={fieldName}
|
|
onChange={onChange}
|
|
/>
|
|
);
|
|
default:
|
|
return (
|
|
<Alert color='red' title={t`Error`}>
|
|
Invalid field type for field '{fieldName}': '
|
|
{fieldDefinition.field_type}'
|
|
</Alert>
|
|
);
|
|
}
|
|
}, [
|
|
control,
|
|
controller,
|
|
definition,
|
|
field,
|
|
fieldId,
|
|
fieldName,
|
|
fieldDefinition,
|
|
onChange,
|
|
safeOnKeyDown,
|
|
reducedDefinition,
|
|
ref,
|
|
setFields,
|
|
value
|
|
]);
|
|
|
|
if (fieldDefinition.hidden) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Stack>
|
|
{definition.preFieldContent}
|
|
{fieldInstance}
|
|
{definition.postFieldContent}
|
|
</Stack>
|
|
);
|
|
}
|