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..98b73ec77d32d8
--- /dev/null
+++ b/packages/dataviews/src/components/dataform-combined-edit/index.tsx
@@ -0,0 +1,72 @@
+/**
+ * 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 FieldHeader( { title }: { title: string } ) {
+ return (
+
+
+
+ { title }
+
+
+
+
+ );
+}
+
+function DataFormCombinedEdit< Item >( {
+ field,
+ data,
+ onChange,
+}: 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 && }
+
+
+ );
+ } );
+
+ if ( field.direction === 'horizontal' ) {
+ return (
+
+ { children }
+
+ );
+ }
+ return (
+
+ { 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..74aebf9f797a4d
--- /dev/null
+++ b/packages/dataviews/src/components/dataform-combined-edit/style.scss
@@ -0,0 +1,6 @@
+.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/dataforms-layouts/panel/index.tsx b/packages/dataviews/src/dataforms-layouts/panel/index.tsx
index 9f118584998bd3..acc0c59b853825 100644
--- a/packages/dataviews/src/dataforms-layouts/panel/index.tsx
+++ b/packages/dataviews/src/dataforms-layouts/panel/index.tsx
@@ -16,8 +16,17 @@ import { closeSmall } from '@wordpress/icons';
/**
* Internal dependencies
*/
-import { normalizeFields } from '../../normalize-fields';
-import type { DataFormProps, NormalizedField, Field } from '../../types';
+import {
+ normalizeFields,
+ normalizeCombinedFields,
+} from '../../normalize-fields';
+import type {
+ DataFormProps,
+ NormalizedField,
+ Field,
+ CombinedFormField,
+ NormalizedCombinedFormField,
+} from '../../types';
interface FormFieldProps< Item > {
data: Item;
@@ -135,6 +144,26 @@ function FormField< Item >( {
);
}
+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 );
+}
+
export default function FormPanel< Item >( {
data,
fields,
@@ -144,13 +173,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/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..d8da33c546bd10 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,38 @@ 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;
+}
+
+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 ) )