From c38610ae21d5aad266aff9a08fc49e80cd38f0e6 Mon Sep 17 00:00:00 2001 From: louwie17 Date: Wed, 20 Nov 2024 11:42:10 -0400 Subject: [PATCH] DataForm: enable fields to declare a different layout (#66531) Co-authored-by: louwie17 Co-authored-by: oandregal Co-authored-by: gigitux Co-authored-by: youknowriad --- .../dataform-combined-edit/index.tsx | 69 ----- .../dataform-combined-edit/style.scss | 16 - .../src/components/dataform-context/index.tsx | 30 ++ .../src/components/dataform/index.tsx | 27 +- .../dataform/stories/index.story.tsx | 137 +++++--- .../dataforms-layouts/data-form-layout.tsx | 87 ++++++ .../dataforms-layouts/get-visible-fields.ts | 29 -- .../dataviews/src/dataforms-layouts/index.tsx | 14 +- .../dataforms-layouts/is-combined-field.ts | 10 + .../src/dataforms-layouts/panel/index.tsx | 293 ++++++++++++------ .../src/dataforms-layouts/regular/index.tsx | 138 ++++++--- .../src/dataforms-layouts/regular/style.scss | 30 ++ packages/dataviews/src/normalize-fields.ts | 34 +- .../dataviews/src/normalize-form-fields.ts | 42 +++ packages/dataviews/src/style.scss | 2 +- packages/dataviews/src/types.ts | 44 +-- packages/dataviews/src/validation.ts | 2 +- .../src/components/post-edit/index.js | 20 +- 18 files changed, 645 insertions(+), 379 deletions(-) delete mode 100644 packages/dataviews/src/components/dataform-combined-edit/index.tsx delete mode 100644 packages/dataviews/src/components/dataform-combined-edit/style.scss create mode 100644 packages/dataviews/src/components/dataform-context/index.tsx create mode 100644 packages/dataviews/src/dataforms-layouts/data-form-layout.tsx delete mode 100644 packages/dataviews/src/dataforms-layouts/get-visible-fields.ts create mode 100644 packages/dataviews/src/dataforms-layouts/is-combined-field.ts create mode 100644 packages/dataviews/src/dataforms-layouts/regular/style.scss create mode 100644 packages/dataviews/src/normalize-form-fields.ts diff --git a/packages/dataviews/src/components/dataform-combined-edit/index.tsx b/packages/dataviews/src/components/dataform-combined-edit/index.tsx deleted file mode 100644 index 90a92ac861bdd..0000000000000 --- a/packages/dataviews/src/components/dataform-combined-edit/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalHStack as HStack, - __experimentalVStack as VStack, - __experimentalHeading as Heading, - __experimentalSpacer as Spacer, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import type { DataFormCombinedEditProps, NormalizedField } from '../../types'; -import FormFieldVisibility from '../form-field-visibility'; - -function Header( { title }: { title: string } ) { - return ( - - - - { title } - - - - - ); -} - -function DataFormCombinedEdit< Item >( { - field, - data, - onChange, - hideLabelFromVision, -}: DataFormCombinedEditProps< Item > ) { - const className = 'dataforms-combined-edit'; - const visibleChildren = ( field.children ?? [] ) - .map( ( fieldId ) => field.fields.find( ( { id } ) => id === fieldId ) ) - .filter( - ( childField ): childField is NormalizedField< Item > => - !! childField - ); - const children = visibleChildren.map( ( child ) => { - return ( - -
- -
-
- ); - } ); - - const Stack = field.direction === 'horizontal' ? HStack : VStack; - - return ( - <> - { ! hideLabelFromVision &&
} - - { children } - - - ); -} - -export default DataFormCombinedEdit; diff --git a/packages/dataviews/src/components/dataform-combined-edit/style.scss b/packages/dataviews/src/components/dataform-combined-edit/style.scss deleted file mode 100644 index 97e052ed89798..0000000000000 --- a/packages/dataviews/src/components/dataform-combined-edit/style.scss +++ /dev/null @@ -1,16 +0,0 @@ -.dataforms-layouts-panel__field-dropdown { - .dataforms-combined-edit { - border: none; - padding: 0; - } -} - -.dataforms-combined-edit { - &__field { - flex: 1 1 auto; - } - - p.components-base-control__help:has(.components-checkbox-control__help) { - margin-top: $grid-unit-05; - } -} diff --git a/packages/dataviews/src/components/dataform-context/index.tsx b/packages/dataviews/src/components/dataform-context/index.tsx new file mode 100644 index 0000000000000..72fbf7e0f42ab --- /dev/null +++ b/packages/dataviews/src/components/dataform-context/index.tsx @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { NormalizedField } from '../../types'; + +type DataFormContextType< Item > = { + fields: NormalizedField< Item >[]; +}; + +const DataFormContext = createContext< DataFormContextType< any > >( { + fields: [], +} ); + +export function DataFormProvider< Item >( { + fields, + children, +}: React.PropsWithChildren< { fields: NormalizedField< Item >[] } > ) { + return ( + + { children } + + ); +} + +export default DataFormContext; diff --git a/packages/dataviews/src/components/dataform/index.tsx b/packages/dataviews/src/components/dataform/index.tsx index 58f0bf06afb41..b359ddba74381 100644 --- a/packages/dataviews/src/components/dataform/index.tsx +++ b/packages/dataviews/src/components/dataform/index.tsx @@ -1,17 +1,34 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; + /** * Internal dependencies */ import type { DataFormProps } from '../../types'; -import { getFormLayout } from '../../dataforms-layouts'; +import { DataFormProvider } from '../dataform-context'; +import { normalizeFields } from '../../normalize-fields'; +import { DataFormLayout } from '../../dataforms-layouts/data-form-layout'; export default function DataForm< Item >( { + data, form, - ...props + fields, + onChange, }: DataFormProps< Item > ) { - const layout = getFormLayout( form.type ?? 'regular' ); - if ( ! layout ) { + const normalizedFields = useMemo( + () => normalizeFields( fields ), + [ fields ] + ); + + if ( ! form.fields ) { return null; } - return ; + return ( + + + + ); } diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index b59d79063200b..ecad2af43fb84 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -1,13 +1,14 @@ /** * WordPress dependencies */ -import { useState } from '@wordpress/element'; +import { useMemo, useState } from '@wordpress/element'; +import { ToggleControl } from '@wordpress/components'; /** * Internal dependencies */ import DataForm from '../index'; -import type { CombinedFormField, Field } from '../../../types'; +import type { Field, Form } from '../../../types'; type SamplePost = { title: string; @@ -27,8 +28,13 @@ const meta = { type: { control: { type: 'select' }, description: - 'Chooses the layout of the form. "regular" is the default layout.', - options: [ 'regular', 'panel' ], + 'Chooses the default layout of each field. "regular" is the default layout.', + options: [ 'default', 'regular', 'panel' ], + }, + labelPosition: { + control: { type: 'select' }, + description: 'Chooses the label position of the layout.', + options: [ 'default', 'top', 'side', 'none' ], }, }, }; @@ -97,9 +103,33 @@ const fields = [ return item.status !== 'private'; }, }, + { + id: 'sticky', + label: 'Sticky', + type: 'integer', + Edit: ( { field, onChange, data, hideLabelFromVision } ) => { + const { id, getValue } = field; + return ( + + onChange( { [ id ]: ! getValue( { item: data } ) } ) + } + /> + ); + }, + }, ] as Field< SamplePost >[]; -export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { +export const Default = ( { + type, + labelPosition, +}: { + type: 'default' | 'regular' | 'panel'; + labelPosition: 'default' | 'top' | 'side' | 'none'; +} ) => { const [ post, setPost ] = useState( { title: 'Hello, World!', order: 2, @@ -108,29 +138,36 @@ export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { reviewer: 'fulano', date: '2021-01-01T12:00:00', birthdate: '1950-02-23T12:00:00', + sticky: false, } ); - const form = { - fields: [ - 'title', - 'order', - 'author', - 'reviewer', - 'status', - 'password', - 'date', - 'birthdate', - ], - }; + const form = useMemo( + () => ( { + type, + labelPosition, + fields: [ + 'title', + 'order', + { + id: 'sticky', + layout: 'regular', + labelPosition: 'side', + }, + 'author', + 'reviewer', + 'password', + 'date', + 'birthdate', + ], + } ), + [ type, labelPosition ] + ) as Form; return ( data={ post } fields={ fields } - form={ { - ...form, - type, - } } + form={ form } onChange={ ( edits ) => setPost( ( prev ) => ( { ...prev, @@ -142,40 +179,45 @@ export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { }; const CombinedFieldsComponent = ( { - type = 'regular', - combinedFieldDirection = 'vertical', + type, + labelPosition, }: { - type: 'panel' | 'regular'; - combinedFieldDirection: 'vertical' | 'horizontal'; + type: 'default' | 'regular' | 'panel'; + labelPosition: 'default' | 'top' | 'side' | 'none'; } ) => { - const [ post, setPost ] = useState( { + const [ post, setPost ] = useState< SamplePost >( { title: 'Hello, World!', order: 2, author: 1, status: 'draft', + reviewer: 'fulano', + date: '2021-01-01T12:00:00', + birthdate: '1950-02-23T12:00:00', } ); - const form = { - fields: [ 'title', 'status_and_visibility', 'order', 'author' ], - combinedFields: [ - { - id: 'status_and_visibility', - label: 'Status & Visibility', - children: [ 'status', 'password' ], - direction: combinedFieldDirection, - render: ( { item } ) => item.status, - }, - ] as CombinedFormField< any >[], - }; + const form = useMemo( + () => ( { + type, + labelPosition, + fields: [ + 'title', + { + id: 'status', + label: 'Status & Visibility', + children: [ 'status', 'password' ], + }, + 'order', + 'author', + ], + } ), + [ type, labelPosition ] + ) as Form; return ( - data={ post } fields={ fields } - form={ { - ...form, - type, - } } + form={ form } onChange={ ( edits ) => setPost( ( prev ) => ( { ...prev, @@ -191,11 +233,8 @@ export const CombinedFields = { render: CombinedFieldsComponent, argTypes: { ...meta.argTypes, - combinedFieldDirection: { - control: { type: 'select' }, - description: - 'Chooses the direction of the combined field. "vertical" is the default layout.', - options: [ 'vertical', 'horizontal' ], - }, + }, + args: { + type: 'panel', }, }; diff --git a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx new file mode 100644 index 0000000000000..08cc47f569eaf --- /dev/null +++ b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx @@ -0,0 +1,87 @@ +/** + * WordPress dependencies + */ +import { __experimentalVStack as VStack } from '@wordpress/components'; +import { useContext, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { Form, FormField, SimpleFormField } from '../types'; +import { getFormFieldLayout } from './index'; +import DataFormContext from '../components/dataform-context'; +import { isCombinedField } from './is-combined-field'; +import normalizeFormFields from '../normalize-form-fields'; + +export function DataFormLayout< Item >( { + data, + form, + onChange, + children, +}: { + data: Item; + form: Form; + onChange: ( value: any ) => void; + children?: ( + FieldLayout: ( props: { + data: Item; + field: FormField; + onChange: ( value: any ) => void; + hideLabelFromVision?: boolean; + } ) => React.JSX.Element | null, + field: FormField + ) => React.JSX.Element; +} ) { + const { fields: fieldDefinitions } = useContext( DataFormContext ); + + function getFieldDefinition( field: SimpleFormField | string ) { + const fieldId = typeof field === 'string' ? field : field.id; + + return fieldDefinitions.find( + ( fieldDefinition ) => fieldDefinition.id === fieldId + ); + } + + const normalizedFormFields = useMemo( + () => normalizeFormFields( form ), + [ form ] + ); + + return ( + + { normalizedFormFields.map( ( formField ) => { + const FieldLayout = getFormFieldLayout( formField.layout ) + ?.component; + + if ( ! FieldLayout ) { + return null; + } + + const fieldDefinition = ! isCombinedField( formField ) + ? getFieldDefinition( formField ) + : undefined; + + if ( + fieldDefinition && + fieldDefinition.isVisible && + ! fieldDefinition.isVisible( data ) + ) { + return null; + } + + if ( children ) { + return children( FieldLayout, formField ); + } + + return ( + + ); + } ) } + + ); +} diff --git a/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts b/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts deleted file mode 100644 index d95d59a88394e..0000000000000 --- a/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Internal dependencies - */ -import { normalizeCombinedFields } from '../normalize-fields'; -import type { - Field, - CombinedFormField, - NormalizedCombinedFormField, -} from '../types'; - -export function getVisibleFields< Item >( - fields: Field< Item >[], - formFields: string[] = [], - combinedFields?: CombinedFormField< Item >[] -): Field< Item >[] { - const visibleFields: Array< - Field< Item > | NormalizedCombinedFormField< Item > - > = [ ...fields ]; - if ( combinedFields ) { - visibleFields.push( - ...normalizeCombinedFields( combinedFields, fields ) - ); - } - return formFields - .map( ( fieldId ) => - visibleFields.find( ( { id } ) => id === fieldId ) - ) - .filter( ( field ): field is Field< Item > => !! field ); -} diff --git a/packages/dataviews/src/dataforms-layouts/index.tsx b/packages/dataviews/src/dataforms-layouts/index.tsx index 9434ea724ed4c..5e4f3617d9c7d 100644 --- a/packages/dataviews/src/dataforms-layouts/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/index.tsx @@ -1,20 +1,20 @@ /** * Internal dependencies */ -import FormRegular from './regular'; -import FormPanel from './panel'; +import FormRegularField from './regular'; +import FormPanelField from './panel'; -const FORM_LAYOUTS = [ +const FORM_FIELD_LAYOUTS = [ { type: 'regular', - component: FormRegular, + component: FormRegularField, }, { type: 'panel', - component: FormPanel, + component: FormPanelField, }, ]; -export function getFormLayout( type: string ) { - return FORM_LAYOUTS.find( ( layout ) => layout.type === type ); +export function getFormFieldLayout( type: string ) { + return FORM_FIELD_LAYOUTS.find( ( layout ) => layout.type === type ); } diff --git a/packages/dataviews/src/dataforms-layouts/is-combined-field.ts b/packages/dataviews/src/dataforms-layouts/is-combined-field.ts new file mode 100644 index 0000000000000..3df6fdc60f906 --- /dev/null +++ b/packages/dataviews/src/dataforms-layouts/is-combined-field.ts @@ -0,0 +1,10 @@ +/** + * Internal dependencies + */ +import type { FormField, CombinedFormField } from '../types'; + +export function isCombinedField( + field: FormField +): field is CombinedFormField { + return ( field as CombinedFormField ).children !== undefined; +} diff --git a/packages/dataviews/src/dataforms-layouts/panel/index.tsx b/packages/dataviews/src/dataforms-layouts/panel/index.tsx index b74e5e4667d4b..269b2bb418a85 100644 --- a/packages/dataviews/src/dataforms-layouts/panel/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/panel/index.tsx @@ -9,29 +9,29 @@ import { Dropdown, Button, } from '@wordpress/components'; -import { useState, useMemo } from '@wordpress/element'; import { sprintf, __, _x } from '@wordpress/i18n'; +import { useState, useMemo, useContext } from '@wordpress/element'; import { closeSmall } from '@wordpress/icons'; /** * Internal dependencies */ -import { normalizeFields } from '../../normalize-fields'; -import { getVisibleFields } from '../get-visible-fields'; -import type { DataFormProps, NormalizedField } from '../../types'; -import FormFieldVisibility from '../../components/form-field-visibility'; - -interface FormFieldProps< Item > { - data: Item; - field: NormalizedField< Item >; - onChange: ( value: any ) => void; -} +import type { + Form, + FormField, + FieldLayoutProps, + NormalizedField, + SimpleFormField, +} from '../../types'; +import DataFormContext from '../../components/dataform-context'; +import { DataFormLayout } from '../data-form-layout'; +import { isCombinedField } from '../is-combined-field'; function DropdownHeader( { title, onClose, }: { - title: string; + title?: string; onClose: () => void; } ) { return ( @@ -40,9 +40,11 @@ function DropdownHeader( { spacing={ 4 } > - - { title } - + { title && ( + + { title } + + ) } { onClose && ( + ( + + ) } + renderContent={ ( { onClose } ) => ( + <> + + + { ( FieldLayout, nestedField ) => ( + - - ) } - /> - - + ) } + + + ) } + /> ); } -export default function FormPanel< Item >( { +export default function FormPanelField< Item >( { data, - fields, - form, + field, onChange, -}: DataFormProps< Item > ) { - const visibleFields = useMemo( - () => - normalizeFields( - getVisibleFields< Item >( - fields, - form.fields, - form.combinedFields - ) - ), - [ fields, form.fields, form.combinedFields ] +}: FieldLayoutProps< Item > ) { + const { fields } = useContext( DataFormContext ); + const fieldDefinition = fields.find( ( fieldDef ) => { + // Default to the first child if it is a combined field. + if ( isCombinedField( field ) ) { + const children = field.children.filter( + ( child ): child is string | SimpleFormField => + typeof child === 'string' || ! isCombinedField( child ) + ); + const firstChildFieldId = + typeof children[ 0 ] === 'string' + ? children[ 0 ] + : children[ 0 ].id; + return fieldDef.id === firstChildFieldId; + } + return fieldDef.id === field.id; + } ); + const labelPosition = field.labelPosition ?? 'side'; + + // Use internal state instead of a ref to make sure that the component + // re-renders when the popover's anchor updates. + const [ popoverAnchor, setPopoverAnchor ] = useState< HTMLElement | null >( + null ); - return ( - - { visibleFields.map( ( field ) => { - return ( - +
+ { fieldLabel } +
+
+ - - - ); - } ) } - + popoverAnchor={ popoverAnchor } + fieldDefinition={ fieldDefinition } + data={ data } + onChange={ onChange } + labelPosition={ labelPosition } + /> +
+
+ ); + } + + if ( labelPosition === 'none' ) { + return ( +
+ +
+ ); + } + + // Defaults to label position side. + return ( + +
+ { fieldLabel } +
+
+ +
+
); } diff --git a/packages/dataviews/src/dataforms-layouts/regular/index.tsx b/packages/dataviews/src/dataforms-layouts/regular/index.tsx index 6a340a50584df..a3d90b807b5cd 100644 --- a/packages/dataviews/src/dataforms-layouts/regular/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/regular/index.tsx @@ -1,52 +1,116 @@ /** * WordPress dependencies */ -import { __experimentalVStack as VStack } from '@wordpress/components'; -import { useMemo } from '@wordpress/element'; +import { useContext, useMemo } from '@wordpress/element'; +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalHeading as Heading, + __experimentalSpacer as Spacer, +} from '@wordpress/components'; /** * Internal dependencies */ -import { normalizeFields } from '../../normalize-fields'; -import { getVisibleFields } from '../get-visible-fields'; -import type { DataFormProps } from '../../types'; -import FormFieldVisibility from '../../components/form-field-visibility'; +import type { Form, FieldLayoutProps } from '../../types'; +import DataFormContext from '../../components/dataform-context'; +import { DataFormLayout } from '../data-form-layout'; +import { isCombinedField } from '../is-combined-field'; -export default function FormRegular< Item >( { +function Header( { title }: { title: string } ) { + return ( + + + + { title } + + + + + ); +} + +export default function FormRegularField< Item >( { data, - fields, - form, + field, onChange, -}: DataFormProps< Item > ) { - const visibleFields = useMemo( - () => - normalizeFields( - getVisibleFields< Item >( - fields, - form.fields, - form.combinedFields - ) - ), - [ fields, form.fields, form.combinedFields ] + hideLabelFromVision, +}: FieldLayoutProps< Item > ) { + const { fields } = useContext( DataFormContext ); + + const form = useMemo( () => { + if ( isCombinedField( field ) ) { + return { + fields: field.children.map( ( child ) => { + if ( typeof child === 'string' ) { + return { + id: child, + }; + } + return child; + } ), + type: 'regular' as const, + }; + } + + return { + type: 'regular' as const, + fields: [], + }; + }, [ field ] ); + + if ( isCombinedField( field ) ) { + return ( + <> + { ! hideLabelFromVision && field.label && ( +
+ ) } + + + ); + } + + const labelPosition = field.labelPosition ?? 'top'; + const fieldDefinition = fields.find( + ( fieldDef ) => fieldDef.id === field.id ); - return ( - - { visibleFields.map( ( field ) => { - return ( - +
+ { fieldDefinition.label } +
+
+ - - - ); - } ) } - + field={ fieldDefinition } + onChange={ onChange } + hideLabelFromVision + /> +
+ + ); + } + + return ( +
+ +
); } diff --git a/packages/dataviews/src/dataforms-layouts/regular/style.scss b/packages/dataviews/src/dataforms-layouts/regular/style.scss new file mode 100644 index 0000000000000..d94b804fdf1fe --- /dev/null +++ b/packages/dataviews/src/dataforms-layouts/regular/style.scss @@ -0,0 +1,30 @@ +.dataforms-layouts-regular__field { + width: 100%; + min-height: $grid-unit-40; + justify-content: flex-start !important; + align-items: flex-start !important; +} + +.dataforms-layouts-regular__field .components-base-control__label { + font-size: inherit; + font-weight: normal; + text-transform: none; +} + +.dataforms-layouts-regular__field-label { + width: 38%; + flex-shrink: 0; + min-height: $grid-unit-40; + display: flex; + align-items: center; + padding: 6px 0; // Matches button to ensure alignment + line-height: $grid-unit-05 * 5; + hyphens: auto; +} + +.dataforms-layouts-regular__field-control { + flex-grow: 1; + min-height: $grid-unit-40; + display: flex; + align-items: center; +} diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 562f29fcce84f..2ed87cbe11222 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -2,14 +2,8 @@ * Internal dependencies */ import getFieldTypeDefinition from './field-types'; -import type { - CombinedFormField, - Field, - NormalizedField, - NormalizedCombinedFormField, -} from './types'; +import type { Field, NormalizedField } from './types'; import { getControl } from './dataform-controls'; -import DataFormCombinedEdit from './components/dataform-combined-edit'; const getValueFromId = ( id: string ) => @@ -87,29 +81,3 @@ export function normalizeFields< Item >( }; } ); } - -/** - * Apply default values and normalize the fields config. - * - * @param combinedFields combined field list. - * @param fields Fields config. - * @return Normalized fields config. - */ -export function normalizeCombinedFields< Item >( - combinedFields: CombinedFormField< Item >[], - fields: Field< Item >[] -): NormalizedCombinedFormField< Item >[] { - return combinedFields.map( ( combinedField ) => { - return { - ...combinedField, - Edit: DataFormCombinedEdit, - fields: normalizeFields( - combinedField.children - .map( ( fieldId ) => - fields.find( ( { id } ) => id === fieldId ) - ) - .filter( ( field ): field is Field< Item > => !! field ) - ), - }; - } ); -} diff --git a/packages/dataviews/src/normalize-form-fields.ts b/packages/dataviews/src/normalize-form-fields.ts new file mode 100644 index 0000000000000..3cd5f67564d7c --- /dev/null +++ b/packages/dataviews/src/normalize-form-fields.ts @@ -0,0 +1,42 @@ +/** + * Internal dependencies + */ +import type { Form } from './types'; + +interface NormalizedFormField { + id: string; + layout: 'regular' | 'panel'; + labelPosition: 'side' | 'top' | 'none'; +} + +export default function normalizeFormFields( + form: Form +): NormalizedFormField[] { + let layout: 'regular' | 'panel' = 'regular'; + if ( [ 'regular', 'panel' ].includes( form.type ?? '' ) ) { + layout = form.type as 'regular' | 'panel'; + } + + const labelPosition = + form.labelPosition ?? ( layout === 'regular' ? 'top' : 'side' ); + + return ( form.fields ?? [] ).map( ( field ) => { + if ( typeof field === 'string' ) { + return { + id: field, + layout, + labelPosition, + }; + } + + const fieldLayout = field.layout ?? layout; + const fieldLabelPosition = + field.labelPosition ?? + ( fieldLayout === 'regular' ? 'top' : 'side' ); + return { + ...field, + layout: fieldLayout, + labelPosition: fieldLabelPosition, + }; + } ); +} diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 26c6ecea645f4..5639f3cac0da5 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -6,7 +6,6 @@ @import "./components/dataviews-item-actions/style.scss"; @import "./components/dataviews-selection-checkbox/style.scss"; @import "./components/dataviews-view-config/style.scss"; -@import "./components/dataform-combined-edit/style.scss"; @import "./dataviews-layouts/grid/style.scss"; @import "./dataviews-layouts/list/style.scss"; @@ -14,3 +13,4 @@ @import "./dataform-controls/style.scss"; @import "./dataforms-layouts/panel/style.scss"; +@import "./dataforms-layouts/regular/style.scss"; diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 71990f72d4eec..8c4276f2541ec 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -527,37 +527,41 @@ export interface SupportedLayouts { table?: Omit< ViewTable, 'type' >; } -export interface CombinedFormField< Item > extends CombinedField { - render?: ComponentType< { item: Item } >; -} - -export interface DataFormCombinedEditProps< Item > { - field: NormalizedCombinedFormField< Item >; - data: Item; - onChange: ( value: Record< string, any > ) => void; - hideLabelFromVision?: boolean; -} +export type SimpleFormField = { + id: string; + layout?: 'regular' | 'panel'; + labelPosition?: 'side' | 'top' | 'none'; +}; -export type NormalizedCombinedFormField< Item > = CombinedFormField< Item > & { - fields: NormalizedField< Item >[]; - Edit?: ComponentType< DataFormCombinedEditProps< Item > >; +export type CombinedFormField = { + id: string; + label?: string; + layout?: 'regular' | 'panel'; + labelPosition?: 'side' | 'top' | 'none'; + children: Array< FormField | string >; }; +export type FormField = SimpleFormField | CombinedFormField; + /** * The form configuration. */ -export type Form< Item > = { +export type Form = { type?: 'regular' | 'panel'; - fields?: string[]; - /** - * The fields to combine. - */ - combinedFields?: CombinedFormField< Item >[]; + fields?: Array< FormField | string >; + labelPosition?: 'side' | 'top' | 'none'; }; export interface DataFormProps< Item > { data: Item; fields: Field< Item >[]; - form: Form< Item >; + form: Form; onChange: ( value: Record< string, any > ) => void; } + +export interface FieldLayoutProps< Item > { + data: Item; + field: FormField; + onChange: ( value: any ) => void; + hideLabelFromVision?: boolean; +} diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts index 0a6542da4e8d4..bcc9a15908ff5 100644 --- a/packages/dataviews/src/validation.ts +++ b/packages/dataviews/src/validation.ts @@ -16,7 +16,7 @@ import type { Field, Form } from './types'; export function isItemValid< Item >( item: Item, fields: Field< Item >[], - form: Form< Item > + form: Form ): boolean { const _fields = normalizeFields( fields.filter( ( { id } ) => !! form.fields?.includes( id ) ) diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index a535eef4ce787..a7842f0feb3c2 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -70,9 +70,16 @@ function PostEditForm( { postType, postId } ) { () => ( { type: 'panel', fields: [ - 'featured_media', + { + id: 'featured_media', + layout: 'regular', + }, 'title', - 'status_and_visibility', + { + id: 'status', + label: __( 'Status & Visibility' ), + children: [ 'status', 'password' ], + }, 'author', 'date', 'slug', @@ -83,15 +90,6 @@ function PostEditForm( { postType, postId } ) { ids.length === 1 || fieldsWithBulkEditSupport.includes( field ) ), - combinedFields: [ - { - id: 'status_and_visibility', - label: __( 'Status & Visibility' ), - children: [ 'status', 'password' ], - direction: 'vertical', - render: ( { item } ) => item.status, - }, - ], } ), [ ids ] );