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..6b2a752fa8de52
--- /dev/null
+++ b/packages/dataviews/src/components/dataform-combined-edit/index.tsx
@@ -0,0 +1,66 @@
+/**
+ * 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 ) => {
+ 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
new file mode 100644
index 00000000000000..0b59cbc9a47768
--- /dev/null
+++ b/packages/dataviews/src/components/dataform-combined-edit/style.scss
@@ -0,0 +1,12 @@
+.dataforms-layouts-panel__field-dropdown {
+ .dataforms-combined-edit {
+ border: none;
+ padding: 0;
+ }
+}
+
+.dataforms-combined-edit {
+ &__field {
+ flex: 1 1 auto;
+ }
+}
diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx
index 7147b9c2342638..c929c21f1c21a9 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,62 @@ export const Default = ( { type }: { type: 'panel' | 'regular' } ) => {
/>
);
};
+
+const CombinedFieldsComponent = ( {
+ type = 'regular',
+ combinedFieldDirection = 'vertical',
+}: {
+ type: 'panel' | 'regular';
+ combinedFieldDirection: 'vertical' | 'horizontal';
+} ) => {
+ const [ post, setPost ] = useState( {
+ title: 'Hello, World!',
+ order: 2,
+ author: 1,
+ status: 'draft',
+ } );
+
+ 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 >[],
+ };
+
+ return (
+
+ setPost( ( prev ) => ( {
+ ...prev,
+ ...edits,
+ } ) )
+ }
+ />
+ );
+};
+
+export const CombinedFields = {
+ title: 'DataViews/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' ],
+ },
+ },
+};
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..d95d59a88394e4
--- /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< 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/panel/index.tsx b/packages/dataviews/src/dataforms-layouts/panel/index.tsx
index 94e107dc20665a..5d3bbc532ad457 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;
@@ -142,13 +143,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< Item >(
+ fields,
+ form.fields,
+ form.combinedFields
+ )
),
- [ fields, form.fields ]
+ [ fields, form.fields, form.combinedFields ]
);
return (
diff --git a/packages/dataviews/src/dataforms-layouts/regular/index.tsx b/packages/dataviews/src/dataforms-layouts/regular/index.tsx
index 0ec427ae010032..57aa163b890e5f 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< Item >(
+ fields,
+ form.fields,
+ form.combinedFields
+ )
),
- [ fields, form.fields ]
+ [ fields, form.fields, form.combinedFields ]
);
return (
diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts
index 2d1cc0402bc206..5ef219e45a4787 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< Item > => !! 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..bc44b57eaaecc6 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,37 @@ 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[];
+ /**
+ * 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 ) )