diff --git a/CHANGELOG_v6.md b/CHANGELOG_v6.md index f05f013127..6824d63660 100644 --- a/CHANGELOG_v6.md +++ b/CHANGELOG_v6.md @@ -24,7 +24,7 @@ should change the heading of the (upcoming) version to include a major version b - Implemented the `GridTemplate` component, adding it to the `templates` for the theme - BREAKING CHANGE: Removed support for version 4 of `antd` - Updated `ArrayFieldItemTemplate` to replace `Button.Group` with `Space.Compact` since `Button.Group` is deprecated in `antd` version 5 -- Upgraded to `@ant-design/icon@5`, fixing typing issues in `IconButton` +- Upgraded to `@ant-design/icon@5` ## @rjsf/chakra-ui @@ -39,6 +39,7 @@ should change the heading of the (upcoming) version to include a major version b - Refactored `ArrayFieldItemTemplate` to use the new `ArrayFieldItemButtonsTemplate` - Updated the `ArrayFieldTemplate`, `ObjectFieldTemplate`, and `WrapIfAdditionalTemplate` to a unique id using the `buttonId()` function and adding consistent marker classes - Implemented the `GridTemplate` component, adding it to the `templates` for the theme +- Implemented the new `LayoutGridField`, `LayoutMultiSchemaField` and `LayoutHeaderField` fields, adding them to the `fields` list ## @rjsf/fluent-ui diff --git a/packages/core/src/components/fields/LayoutHeaderField.tsx b/packages/core/src/components/fields/LayoutHeaderField.tsx new file mode 100644 index 0000000000..ae54e61443 --- /dev/null +++ b/packages/core/src/components/fields/LayoutHeaderField.tsx @@ -0,0 +1,49 @@ +import { + getTemplate, + getUiOptions, + titleId, + FieldProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, + TemplatesType, +} from '@rjsf/utils'; + +/** The `LayoutHeaderField` component renders a `TitleFieldTemplate` with an `id` derived from the `idSchema` + * and whether it is `required` from the props. The `title` is derived from the props as follows: + * - If there is a title in the `uiSchema`, it is displayed + * - Else, if there is an explicit `title` passed in the props, it is displayed + * - Otherwise, if there is a title in the `schema`, it is displayed + * - Finally, the `name` prop is displayed as the title + * + * @param props - The `LayoutHeaderField` for the component + */ +export default function LayoutHeaderField< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>(props: FieldProps) { + const { idSchema, title, schema, uiSchema, required, registry, name } = props; + const options = getUiOptions(uiSchema, registry.globalUiOptions); + const { title: uiTitle } = options; + const { title: schemaTitle } = schema; + const fieldTitle = uiTitle || title || schemaTitle || name; + if (!fieldTitle) { + return null; + } + const TitleFieldTemplate: TemplatesType['TitleFieldTemplate'] = getTemplate<'TitleFieldTemplate', T, S, F>( + 'TitleFieldTemplate', + registry, + options + ); + return ( + (idSchema)} + title={fieldTitle} + required={required} + schema={schema} + uiSchema={uiSchema} + registry={registry} + /> + ); +} diff --git a/packages/core/src/components/fields/index.ts b/packages/core/src/components/fields/index.ts index 95eeb8f1c2..c2fac59ed4 100644 --- a/packages/core/src/components/fields/index.ts +++ b/packages/core/src/components/fields/index.ts @@ -3,6 +3,7 @@ import { Field, FormContextType, RegistryFieldsType, RJSFSchema, StrictRJSFSchem import ArrayField from './ArrayField'; import BooleanField from './BooleanField'; import LayoutGridField from './LayoutGridField'; +import LayoutHeaderField from './LayoutHeaderField'; import LayoutMultiSchemaField from './LayoutMultiSchemaField'; import MultiSchemaField from './MultiSchemaField'; import NumberField from './NumberField'; @@ -22,6 +23,7 @@ function fields< // ArrayField falls back to SchemaField if ArraySchemaField is not defined, which it isn't by default BooleanField, LayoutGridField, + LayoutHeaderField, LayoutMultiSchemaField, NumberField, ObjectField, diff --git a/packages/core/test/LayoutHeaderField.test.tsx b/packages/core/test/LayoutHeaderField.test.tsx new file mode 100644 index 0000000000..40a145da82 --- /dev/null +++ b/packages/core/test/LayoutHeaderField.test.tsx @@ -0,0 +1,132 @@ +import { titleId, FieldProps, ID_KEY, IdSchema, Registry, TitleFieldProps } from '@rjsf/utils'; +import { render, screen, within } from '@testing-library/react'; +import noop from 'lodash/noop'; + +import templates from '../src/components/templates'; +import LayoutHeaderField from '../src/components/fields/LayoutHeaderField'; + +const TEST_ID = 'test-id'; +const REQUIRED_ID = 'required-id'; + +const TITLE_BOLD = 'test'; +const TITLE_BOLD_2 = 'test ui'; +const TITLE_NORMAL = 'title'; + +function TestTitleField(props: TitleFieldProps) { + const { id, title, required } = props; + return ( +
+ {title} + {required && } +
+ ); +} + +describe('LayoutHeaderField', () => { + function getProps(overrideProps: Partial = {}): FieldProps { + const { idSchema = {} as IdSchema, schema = {}, name = '', uiSchema = {}, required = false, title } = overrideProps; + return { + // required FieldProps stubbed + autofocus: false, + disabled: false, + errorSchema: {}, + formContext: undefined, + formData: undefined, + onBlur: noop, + onChange: noop, + onFocus: noop, + readonly: false, + title, + required, + // end required FieldProps + idSchema, + schema, + uiSchema, + name, + registry: { + templates: { + ...templates(), + TitleFieldTemplate: TestTitleField, + }, + } as Registry, + }; + } + + test('default render with no title is empty render', () => { + const props = getProps(); + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + test('name is provided, and it is required', () => { + const props = getProps({ name: TITLE_BOLD, required: true }); + render(); + + // renders header field and has expected text and no id + const headerField = screen.getByTestId(TEST_ID); + expect(headerField).toHaveTextContent(TITLE_BOLD); + expect(headerField).toHaveAttribute('id', titleId('undefined')); + + // Is required + const requiredSpan = within(headerField).getByTestId(REQUIRED_ID); + expect(requiredSpan).toBeInTheDocument(); + }); + + test('name is provided, schema has title, idSchema has ID_KEY, not required', () => { + const props = getProps({ + name: TITLE_BOLD, + schema: { title: TITLE_NORMAL }, + idSchema: { [ID_KEY]: 'foo' } as IdSchema, + }); + render(); + + // renders header field and has expected text and id + const headerField = screen.getByTestId(TEST_ID); + expect(headerField).toHaveTextContent(TITLE_NORMAL); + expect(headerField).toHaveAttribute('id', titleId(props.idSchema[ID_KEY])); + + // Is not required + const requiredSpan = within(headerField).queryByTestId(REQUIRED_ID); + expect(requiredSpan).not.toBeInTheDocument(); + }); + + test('title prop is passed, schema has title, idSchema has ID_KEY, required', () => { + const props = getProps({ + title: TITLE_BOLD, + schema: { title: TITLE_NORMAL }, + idSchema: { [ID_KEY]: 'foo' } as IdSchema, + required: true, + }); + render(); + + // renders header field and has expected text and id + const headerField = screen.getByTestId(TEST_ID); + expect(headerField).toHaveTextContent(TITLE_BOLD); + expect(headerField).toHaveAttribute('id', titleId(props.idSchema[ID_KEY])); + + // Is not required + const requiredSpan = within(headerField).getByTestId(REQUIRED_ID); + expect(requiredSpan).toBeInTheDocument(); + }); + + test('uiSchema has ui:title, title prop is passed, no id, not required', () => { + const props = getProps({ + title: TITLE_BOLD, + uiSchema: { + 'ui:title': TITLE_BOLD_2, + }, + idSchema: { [ID_KEY]: 'foo' } as IdSchema, + }); + render(); + + // renders header field and has expected text and no id + const headerField = screen.getByTestId(TEST_ID); + expect(headerField).toHaveTextContent(TITLE_BOLD_2); + expect(headerField).toHaveAttribute('id', titleId(props.idSchema[ID_KEY])); + + // Is not required + const requiredSpan = within(headerField).queryByTestId(REQUIRED_ID); + expect(requiredSpan).not.toBeInTheDocument(); + }); +}); diff --git a/packages/docs/docs/advanced-customization/custom-widgets-fields.md b/packages/docs/docs/advanced-customization/custom-widgets-fields.md index 86240d14b7..4c1983f056 100644 --- a/packages/docs/docs/advanced-customization/custom-widgets-fields.md +++ b/packages/docs/docs/advanced-customization/custom-widgets-fields.md @@ -62,6 +62,9 @@ The default fields you can override are: - `DescriptionField` - `OneOfField` - `AnyOfField` +- `LayoutGridField` +- `LayoutMultiSchemaField` +- `LayoutHeaderField` - `NullField` - `NumberField` - `ObjectField` diff --git a/packages/playground/src/samples/layoutGrid.tsx b/packages/playground/src/samples/layoutGrid.tsx index 5b1aa29474..04a269bf04 100644 --- a/packages/playground/src/samples/layoutGrid.tsx +++ b/packages/playground/src/samples/layoutGrid.tsx @@ -6,7 +6,7 @@ const layoutGrid: Sample = { schema: { type: 'object', properties: { - person: { title: 'Person', $ref: '#/definitions/Person' }, + person: { title: 'Person Info', $ref: '#/definitions/Person' }, employment: { title: 'Employment', discriminator: { @@ -277,6 +277,18 @@ const layoutGrid: Sample = { 'ui:layoutGrid': { 'ui:row': { children: [ + { + 'ui:row': { + children: [ + { + 'ui:col': { + xs: 24, + children: ['person'], + }, + }, + ], + }, + }, { 'ui:row': { gutter: [6, 0], @@ -439,6 +451,7 @@ const layoutGrid: Sample = { }, }, person: { + 'ui:field': 'LayoutHeaderField', race: { 'ui:options': { widget: 'checkboxes', @@ -489,6 +502,17 @@ const layoutGrid: Sample = { 'ui:row': { gap: 2, children: [ + { + 'ui:row': { + gap: 2, + templateColumns: 'repeat(1, 1fr)', + children: [ + { + 'ui:col': ['person'], + }, + ], + }, + }, { 'ui:row': { gap: 2, @@ -634,6 +658,7 @@ const layoutGrid: Sample = { }, }, person: { + 'ui:field': 'LayoutHeaderField', race: { 'ui:options': { widget: 'checkboxes', @@ -683,25 +708,31 @@ const layoutGrid: Sample = { children: [ { 'ui:col': { - style: { gridRow: '1 / auto', gridColumn: '1 / span 4' }, + style: { gridRow: '1 / auto', gridColumn: '1 / span 12' }, + children: ['person'], + }, + }, + { + 'ui:col': { + style: { gridRow: '2 / auto', gridColumn: '1 / span 4' }, children: ['person.name.first'], }, }, { 'ui:col': { - style: { gridRow: '1 / auto', gridColumn: '5 / span 4' }, + style: { gridRow: '2 / auto', gridColumn: '5 / span 4' }, children: ['person.name.middle'], }, }, { 'ui:col': { - style: { gridRow: '1 / auto', gridColumn: '9 / span 4' }, + style: { gridRow: '2 / auto', gridColumn: '9 / span 4' }, children: ['person.name.last'], }, }, { 'ui:col': { - style: { gridRow: '2 / auto', gridColumn: '1 / span 4' }, + style: { gridRow: '3 / auto', gridColumn: '1 / span 4' }, children: [ { name: 'person.birth_date', @@ -712,37 +743,37 @@ const layoutGrid: Sample = { }, { 'ui:col': { - style: { gridRow: '2 / auto', gridColumn: '5 / span 8', marginTop: '3px' }, + style: { gridRow: '3 / auto', gridColumn: '5 / span 8', marginTop: '3px' }, children: ['person.race'], }, }, { 'ui:col': { - style: { gridRow: '3 / auto', gridColumn: '1 / span 5' }, + style: { gridRow: '4 / auto', gridColumn: '1 / span 4' }, children: ['line_1'], }, }, { 'ui:col': { - style: { gridRow: '4 / auto', gridColumn: '1 / span 5' }, + style: { gridRow: '5 / auto', gridColumn: '1 / span 4' }, children: ['line_2'], }, }, { 'ui:col': { - style: { gridRow: '5 / auto', gridColumn: '1 / span 5' }, + style: { gridRow: '6 / auto', gridColumn: '1 / span 4' }, children: ['city'], }, }, { 'ui:col': { - style: { gridRow: '3 / auto', gridColumn: '1 / span 5' }, + style: { gridRow: '4 / auto', gridColumn: '1 / span 4' }, children: ['person.address'], }, }, { 'ui:row': { - style: { gridRow: '3 / auto', gridColumn: '6 / span 7' }, + style: { gridRow: '4 / auto', gridColumn: '6 / span 7' }, children: [ { 'ui:col': { @@ -840,6 +871,7 @@ const layoutGrid: Sample = { }, }, person: { + 'ui:field': 'LayoutHeaderField', race: { 'ui:options': { widget: 'checkboxes', @@ -886,9 +918,22 @@ const layoutGrid: Sample = { 'ui:field': 'LayoutGridField', 'ui:layoutGrid': { 'ui:row': { - mt: 1, spacing: 2, children: [ + { + 'ui:row': { + spacing: 2, + size: 12, + children: [ + { + 'ui:col': { + size: 12, + children: ['person'], + }, + }, + ], + }, + }, { 'ui:row': { spacing: 2, @@ -1051,6 +1096,7 @@ const layoutGrid: Sample = { }, }, person: { + 'ui:field': 'LayoutHeaderField', race: { 'ui:options': { widget: 'checkboxes', @@ -1102,6 +1148,18 @@ const layoutGrid: Sample = { 'ui:layoutGrid': { 'ui:row': { children: [ + { + 'ui:row': { + children: [ + { + 'ui:col': { + xs: 12, + children: ['person'], + }, + }, + ], + }, + }, { 'ui:row': { children: [ @@ -1247,6 +1305,7 @@ const layoutGrid: Sample = { }, }, person: { + 'ui:field': 'LayoutHeaderField', race: { 'ui:options': { widget: 'checkboxes', @@ -1297,6 +1356,20 @@ const layoutGrid: Sample = { 'ui:row': { container: true, children: [ + { + 'ui:row': { + style: { width: '100%' }, + children: [ + { + 'ui:columns': { + width: 18, + style: { paddingBottom: 0 }, + children: ['person'], + }, + }, + ], + }, + }, { 'ui:row': { style: { width: '100%' }, @@ -1476,6 +1549,7 @@ const layoutGrid: Sample = { }, }, person: { + 'ui:field': 'LayoutHeaderField', race: { 'ui:options': { widget: 'checkboxes', @@ -1527,6 +1601,18 @@ const layoutGrid: Sample = { 'ui:field': 'LayoutGridField', 'ui:layoutGrid': { 'ui:row': [ + { + 'ui:row': { + className: 'grid grid-cols-1 gap-4 col-span-12', + children: [ + { + 'ui:col': { + children: ['person'], + }, + }, + ], + }, + }, { 'ui:row': { className: 'grid grid-cols-12 gap-4 col-span-12', @@ -1660,6 +1746,7 @@ const layoutGrid: Sample = { ], }, person: { + 'ui:field': 'LayoutHeaderField', race: { 'ui:options': { widget: 'checkboxes', @@ -1679,6 +1766,7 @@ const layoutGrid: Sample = { }, { 'ui:row': { + className: 'grid-cols-12 col-span-12', children: [ { 'ui:columns': { @@ -1708,6 +1796,19 @@ const layoutGrid: Sample = { 'ui:field': 'LayoutGridField', 'ui:layoutGrid': { 'ui:row': [ + { + 'ui:row': { + className: 'row', + children: [ + { + 'ui:col': { + className: 'col-xs-12', + children: ['person'], + }, + }, + ], + }, + }, { 'ui:row': { className: 'row', @@ -1842,6 +1943,7 @@ const layoutGrid: Sample = { ], }, person: { + 'ui:field': 'LayoutHeaderField', race: { 'ui:options': { widget: 'checkboxes', diff --git a/packages/shadcn/src/GridTemplate/GridTemplate.tsx b/packages/shadcn/src/GridTemplate/GridTemplate.tsx index 193ffe1fe5..2498eaa985 100644 --- a/packages/shadcn/src/GridTemplate/GridTemplate.tsx +++ b/packages/shadcn/src/GridTemplate/GridTemplate.tsx @@ -7,12 +7,9 @@ import { cn } from '../lib/utils'; * @param props - The GridTemplateProps, including the extra props containing the mui grid positioning details */ export default function GridTemplate(props: GridTemplateProps) { - const { children, column, ...rest } = props; + const { children, column, className, ...rest } = props; return ( -
+
{children}
);