diff --git a/packages/dataviews/src/components/dataform-combined-edit/index.tsx b/packages/dataviews/src/components/dataform-combined-edit/index.tsx new file mode 100644 index 00000000000000..137db111c9bd30 --- /dev/null +++ b/packages/dataviews/src/components/dataform-combined-edit/index.tsx @@ -0,0 +1,70 @@ +/** + * 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'; + +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, index ) => { + return ( +
+ { index !== 0 && hideLabelFromVision && ( +
+ ) } + +
+ ); + } ); + + 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 new file mode 100644 index 00000000000000..4f5aab5533fb72 --- /dev/null +++ b/packages/dataviews/src/components/dataform-combined-edit/style.scss @@ -0,0 +1,8 @@ +.dataforms-layouts-panel__field-dropdown { + .dataforms-combined-edit { + &__field:not(:first-child) { + border-top: $border-width solid $gray-200; + padding-top: $grid-unit-20; + } + } +} diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 7147b9c2342638..183e0d80c666da 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -7,6 +7,7 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import DataForm from '../index'; +import type { CombinedFormField } from '../../../types'; const meta = { title: 'DataViews/DataForm', @@ -76,6 +77,11 @@ const fields = [ { value: 'published', label: 'Published' }, ], }, + { + id: 'password', + label: 'Password', + type: 'text' as const, + }, ]; export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { @@ -118,3 +124,44 @@ export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { /> ); }; + +export const CombinedFields = ( { type }: { type: 'panel' | 'regular' } ) => { + const [ post, setPost ] = useState( { + title: 'Hello, World!', + order: 2, + author: 1, + status: 'draft', + } ); + + const form = { + fields: [ 'title', 'status_and_visibility', 'order', 'author' ], + layout: { + combinedFields: [ + { + id: 'status_and_visibility', + label: 'Status & Visibility', + children: [ 'status', 'password' ], + direction: 'vertical', + render: ( { item } ) => item.status, + }, + ] as CombinedFormField< any >[], + }, + }; + + return ( + + setPost( ( prev ) => ( { + ...prev, + ...edits, + } ) ) + } + /> + ); +}; diff --git a/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts b/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts new file mode 100644 index 00000000000000..c2c5cfeb8a3785 --- /dev/null +++ b/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import { normalizeCombinedFields } from '../normalize-fields'; +import type { + Field, + CombinedFormField, + NormalizedCombinedFormField, +} from '../types'; + +export function getVisibleFields( + fields: Field< any >[], + formFields: string[] = [], + combinedFields?: CombinedFormField< any >[] +): Field< any >[] { + const visibleFields: Array< + Field< any > | NormalizedCombinedFormField< any > + > = [ ...fields ]; + if ( combinedFields ) { + visibleFields.push( + ...normalizeCombinedFields( combinedFields, fields ) + ); + } + return formFields + .map( ( fieldId ) => + visibleFields.find( ( { id } ) => id === fieldId ) + ) + .filter( ( field ): field is Field< any > => !! field ); +} diff --git a/packages/dataviews/src/dataforms-layouts/panel/index.tsx b/packages/dataviews/src/dataforms-layouts/panel/index.tsx index 9f118584998bd3..e12847133b5a74 100644 --- a/packages/dataviews/src/dataforms-layouts/panel/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/panel/index.tsx @@ -17,7 +17,8 @@ import { closeSmall } from '@wordpress/icons'; * Internal dependencies */ import { normalizeFields } from '../../normalize-fields'; -import type { DataFormProps, NormalizedField, Field } from '../../types'; +import { getVisibleFields } from '../get-visible-fields'; +import type { DataFormProps, NormalizedField } from '../../types'; interface FormFieldProps< Item > { data: Item; @@ -144,13 +145,13 @@ export default function FormPanel< Item >( { const visibleFields = useMemo( () => normalizeFields( - ( form.fields ?? [] ) - .map( ( fieldId ) => - fields.find( ( { id } ) => id === fieldId ) - ) - .filter( ( field ): field is Field< Item > => !! field ) + getVisibleFields( + fields, + form.fields, + form.layout?.combinedFields + ) ), - [ fields, form.fields ] + [ fields, form.fields, form.layout?.combinedFields ] ); return ( diff --git a/packages/dataviews/src/dataforms-layouts/regular/index.tsx b/packages/dataviews/src/dataforms-layouts/regular/index.tsx index 0ec427ae010032..424580444bc4dd 100644 --- a/packages/dataviews/src/dataforms-layouts/regular/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/regular/index.tsx @@ -8,7 +8,8 @@ import { useMemo } from '@wordpress/element'; * Internal dependencies */ import { normalizeFields } from '../../normalize-fields'; -import type { DataFormProps, Field } from '../../types'; +import { getVisibleFields } from '../get-visible-fields'; +import type { DataFormProps } from '../../types'; export default function FormRegular< Item >( { data, @@ -19,13 +20,13 @@ export default function FormRegular< Item >( { const visibleFields = useMemo( () => normalizeFields( - ( form.fields ?? [] ) - .map( ( fieldId ) => - fields.find( ( { id } ) => id === fieldId ) - ) - .filter( ( field ): field is Field< Item > => !! field ) + getVisibleFields( + fields, + form.fields, + form.layout?.combinedFields + ) ), - [ fields, form.fields ] + [ fields, form.fields, form.layout?.combinedFields ] ); return ( diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 2d1cc0402bc206..cfd330934802de 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -2,8 +2,14 @@ * Internal dependencies */ import getFieldTypeDefinition from './field-types'; -import type { Field, NormalizedField } from './types'; +import type { + CombinedFormField, + Field, + NormalizedField, + NormalizedCombinedFormField, +} from './types'; import { getControl } from './dataform-controls'; +import DataFormCombinedEdit from './components/dataform-combined-edit'; /** * Apply default values and normalize the fields config. @@ -66,3 +72,29 @@ 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< any > => !! field ) + ), + }; + } ); +} diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 087e812fffa192..26c6ecea645f43 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -6,6 +6,7 @@ @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"; diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index e95a43994cd63d..da3a9fbeee878a 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -174,14 +174,6 @@ export type Fields< Item > = Field< Item >[]; export type Data< Item > = Item[]; -/** - * The form configuration. - */ -export type Form = { - type?: 'regular' | 'panel'; - fields?: string[]; -}; - export type DataFormControlProps< Item > = { data: Item; field: NormalizedField< Item >; @@ -524,9 +516,39 @@ 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 NormalizedCombinedFormField< Item > = CombinedFormField< Item > & { + fields: NormalizedField< Item >[]; + Edit?: ComponentType< DataFormCombinedEditProps< Item > >; +}; + +/** + * The form configuration. + */ +export type Form< Item > = { + type?: 'regular' | 'panel'; + fields?: string[]; + layout?: { + /** + * The fields to combine. + */ + combinedFields?: CombinedFormField< Item >[]; + }; +}; + export interface DataFormProps< Item > { data: Item; fields: Field< Item >[]; - form: Form; + form: Form< Item >; onChange: ( value: Record< string, any > ) => void; } diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts index cc0b031f6c96c6..41969a7960af65 100644 --- a/packages/dataviews/src/validation.ts +++ b/packages/dataviews/src/validation.ts @@ -7,7 +7,7 @@ import type { Field, Form } from './types'; export function isItemValid< Item >( item: Item, fields: Field< Item >[], - form: Form + form: Form< Item > ): boolean { const _fields = normalizeFields( fields.filter( ( { id } ) => !! form.fields?.includes( id ) )