diff --git a/frontend/.changeset/shaky-sites-think.md b/frontend/.changeset/shaky-sites-think.md new file mode 100644 index 00000000..3e4279f8 --- /dev/null +++ b/frontend/.changeset/shaky-sites-think.md @@ -0,0 +1,5 @@ +--- +'pydantic-forms': patch +--- + +Adds default value for required array fields. Disables descendants of disabled fields diff --git a/frontend/packages/pydantic-forms/src/components/fields/ArrayField.tsx b/frontend/packages/pydantic-forms/src/components/fields/ArrayField.tsx index bc317a6e..a94d01e8 100644 --- a/frontend/packages/pydantic-forms/src/components/fields/ArrayField.tsx +++ b/frontend/packages/pydantic-forms/src/components/fields/ArrayField.tsx @@ -4,13 +4,14 @@ import { useFieldArray } from 'react-hook-form'; import { usePydanticFormContext } from '@/core'; import { fieldToComponentMatcher } from '@/core/helper'; import { PydanticFormElementProps } from '@/types'; -import { itemizeArrayItem } from '@/utils'; +import { disableField, itemizeArrayItem } from '@/utils'; import { RenderFields } from '../render'; export const ArrayField = ({ pydanticFormField }: PydanticFormElementProps) => { const { rhf, config } = usePydanticFormContext(); + const disabled = pydanticFormField.attributes?.disabled || false; const { control } = rhf; const { id: arrayName, arrayItem } = pydanticFormField; const { fields, append, remove } = useFieldArray({ @@ -28,7 +29,12 @@ export const ArrayField = ({ pydanticFormField }: PydanticFormElementProps) => { ); const renderField = (field: Record<'id', string>, index: number) => { - const arrayItemField = itemizeArrayItem(index, arrayItem, arrayName); + const itemizedField = itemizeArrayItem(index, arrayItem, arrayName); + // We have decided - for now - on the convention that all descendants of disabled fields will be disabled as well + // so we will not displaying any interactive elements inside a disabled element + const arrayItemField = disabled + ? disableField(itemizedField) + : itemizedField; return (
{ ]} extraTriggerFields={[arrayName]} /> - {(!minItems || (minItems && fields.length > minItems)) && ( - { - remove(index); - }} - > - - - - )} + {(!minItems || (minItems && fields.length > minItems)) && + !disabled && ( + { + remove(index); + }} + > + - + + )}
); }; @@ -66,7 +73,7 @@ export const ArrayField = ({ pydanticFormField }: PydanticFormElementProps) => {
{ }} > {fields.map(renderField)} - {(!maxItems || (maxItems && fields.length < maxItems)) && ( -
{ - append({ - [arrayName]: arrayItem.default, - }); - }} - style={{ - cursor: 'pointer', - fontSize: '32px', - display: 'flex', - justifyContent: 'end', - marginTop: '8px', - marginBottom: '8px', - padding: '16px', - }} - > - + -
- )} + {(!maxItems || (maxItems && fields.length < maxItems)) && + !disabled && ( +
{ + append({ + [arrayName]: arrayItem.default, + }); + }} + style={{ + cursor: 'pointer', + fontSize: '32px', + display: 'flex', + justifyContent: 'end', + marginTop: '8px', + marginBottom: '8px', + padding: '16px', + }} + > + + +
+ )}
); }; diff --git a/frontend/packages/pydantic-forms/src/components/fields/ObjectField.tsx b/frontend/packages/pydantic-forms/src/components/fields/ObjectField.tsx index b2faa78d..e826d9ca 100644 --- a/frontend/packages/pydantic-forms/src/components/fields/ObjectField.tsx +++ b/frontend/packages/pydantic-forms/src/components/fields/ObjectField.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { usePydanticFormContext } from '@/core'; import { getPydanticFormComponents } from '@/core/helper'; import { PydanticFormElementProps } from '@/types'; +import { disableField } from '@/utils'; import { RenderFields } from '../render'; @@ -10,11 +11,22 @@ export const ObjectField = ({ pydanticFormField, }: PydanticFormElementProps) => { const { config } = usePydanticFormContext(); + const disabled = pydanticFormField.attributes?.disabled || false; const components = getPydanticFormComponents( pydanticFormField.properties || {}, config?.componentMatcherExtender, ); + // We have decided - for now - on the convention that all descendants of disabled fields will be disabled as well + // so we will not displaying any interactive elements inside a disabled element + if (disabled) { + components.forEach((component) => { + component.pydanticFormField = disableField( + component.pydanticFormField, + ); + }); + } + return (
{ }); }); - it('Returns empty object if arrayItem and array both have no default values', () => { - // When an array fields has no default value the default value should be taken from the arrayItem + it('Returns empty object if arrayItem and array both have no default values and the array is not required', () => { + // When an array field has no default value the default value and the arrayItem doesn't either we assume an empty array const properties: Properties = { test: getMockPydanticFormField({ id: 'test', @@ -724,11 +724,28 @@ describe('getFormValuesFromFieldOrLabels', () => { id: 'nestedField', type: PydanticFormFieldType.STRING, }), + required: false, }), }; expect(getFormValuesFromFieldOrLabels(properties)).toEqual({}); }); - + it('Returns empty array if arrayItem and array both have no default values and the array is required', () => { + // When an array field has no default value the default value and the arrayItem doesn't either we assume an empty array + const properties: Properties = { + test: getMockPydanticFormField({ + id: 'test', + type: PydanticFormFieldType.ARRAY, + arrayItem: getMockPydanticFormField({ + id: 'nestedField', + type: PydanticFormFieldType.STRING, + }), + required: true, + }), + }; + expect(getFormValuesFromFieldOrLabels(properties)).toEqual({ + test: [], + }); + }); it('Returns empty object if object field and properties both have no default values', () => { // When an array fields has no default value the default value should be taken from the arrayItem const properties: Properties = { diff --git a/frontend/packages/pydantic-forms/src/core/helper.ts b/frontend/packages/pydantic-forms/src/core/helper.ts index 0d7402d3..1e149900 100644 --- a/frontend/packages/pydantic-forms/src/core/helper.ts +++ b/frontend/packages/pydantic-forms/src/core/helper.ts @@ -385,10 +385,16 @@ export const getFormValuesFromFieldOrLabels = ( labelData, componentMatcherExtender, ); + if (objectHasProperties(arrayItemDefault)) { fieldValues[pydanticFormField.id] = [ arrayItemDefault[arrayItem.id], ]; + } else if (pydanticFormField.required) { + // This is somewhat of a special case. + // It deals with the situation where an array is marked required but has no default value. + // Not setting the value here would require a user to select and then unselect an item if they want to send an empty array + fieldValues[pydanticFormField.id] = []; } } } else if (hasDefaultValue(defaultFieldValue)) { diff --git a/frontend/packages/pydantic-forms/src/utils.spec.ts b/frontend/packages/pydantic-forms/src/utils.spec.ts index a91d4c02..307c9404 100644 --- a/frontend/packages/pydantic-forms/src/utils.spec.ts +++ b/frontend/packages/pydantic-forms/src/utils.spec.ts @@ -3,6 +3,7 @@ import type { FieldValues } from 'react-hook-form'; import { getMockPydanticFormField } from './core/helper.spec'; import { PydanticFormFieldType } from './types'; import { + disableField, getFormFieldIdWithPath, getFormFieldValue, insertItemAtIndex, @@ -374,3 +375,13 @@ describe('itemizeArrayItem', () => { } }); }); + +describe('disableField', () => { + it('Disables the field by setting the disabled attribute to true', () => { + const field = getMockPydanticFormField({ + attributes: { disabled: false }, + }); + const disabledField = disableField(field); + expect(disabledField.attributes?.disabled).toBe(true); + }); +}); diff --git a/frontend/packages/pydantic-forms/src/utils.ts b/frontend/packages/pydantic-forms/src/utils.ts index 7819f7f2..8f0678f6 100644 --- a/frontend/packages/pydantic-forms/src/utils.ts +++ b/frontend/packages/pydantic-forms/src/utils.ts @@ -43,6 +43,18 @@ export const itemizeArrayItem = ( }; }; +export const disableField = ( + pydanticFormField: PydanticFormField, +): PydanticFormField => { + return { + ...pydanticFormField, + attributes: { + ...pydanticFormField.attributes, + disabled: true, + }, + }; +}; + /** * Determines how many parts to slice from the PydanticFormField's id. * If the last segment is a number we conclude it's an array item and it returns 2 (to slice off the index and the field name).