diff --git a/packages/dataviews/src/components/dataform/index.tsx b/packages/dataviews/src/components/dataform/index.tsx
index b359ddba74381..b96df5f186c75 100644
--- a/packages/dataviews/src/components/dataform/index.tsx
+++ b/packages/dataviews/src/components/dataform/index.tsx
@@ -10,8 +10,44 @@ import type { DataFormProps } from '../../types';
import { DataFormProvider } from '../dataform-context';
import { normalizeFields } from '../../normalize-fields';
import { DataFormLayout } from '../../dataforms-layouts/data-form-layout';
+import { MIXED_VALUE } from '../../constants';
-export default function DataForm< Item >( {
+/**
+ * Loops through the list of data items and returns an object with the intersecting ( same ) key and values.
+ * Skips keys that start with an underscore.
+ *
+ * @param data list of items.
+ */
+function getIntersectingValues< Item extends object >( data: Item[] ): Item {
+ const intersectingValues = {} as Item;
+ const keys = Object.keys( data[ 0 ] ) as Array< keyof Item >;
+ for ( const key of keys ) {
+ // Skip keys that start with underscore.
+ if ( key.toString().startsWith( '_' ) ) {
+ continue;
+ }
+ const [ firstRecord, ...remainingRecords ] = data;
+
+ if ( typeof firstRecord[ key ] === 'object' ) {
+ // Traverse through nested objects.
+ intersectingValues[ key ] = getIntersectingValues(
+ data.map( ( item ) => item[ key ] as object )
+ ) as Item[ keyof Item ];
+ } else {
+ const intersects = remainingRecords.every( ( item ) => {
+ return item[ key ] === firstRecord[ key ];
+ } );
+ if ( intersects ) {
+ intersectingValues[ key ] = firstRecord[ key ];
+ } else {
+ intersectingValues[ key ] = MIXED_VALUE as Item[ keyof Item ];
+ }
+ }
+ }
+ return intersectingValues;
+}
+
+export default function DataForm< Item extends object >( {
data,
form,
fields,
@@ -22,13 +58,27 @@ export default function DataForm< Item >( {
[ fields ]
);
+ const flattenedData = useMemo( () => {
+ if ( Array.isArray( data ) ) {
+ return getIntersectingValues< Item >( data );
+ }
+ return data;
+ }, [ data ] );
+
if ( ! form.fields ) {
return null;
}
+ const isBulkEditing = Array.isArray( data );
+
return (
-
+
);
}
diff --git a/packages/dataviews/src/constants.ts b/packages/dataviews/src/constants.ts
index 5ae94c7eb4a13..fa5a9cd179de9 100644
--- a/packages/dataviews/src/constants.ts
+++ b/packages/dataviews/src/constants.ts
@@ -68,3 +68,6 @@ export const sortIcons = {
export const LAYOUT_TABLE = 'table';
export const LAYOUT_GRID = 'grid';
export const LAYOUT_LIST = 'list';
+
+// Dataform mixed value.
+export const MIXED_VALUE = Symbol.for( 'DATAFORM_MIXED_VALUE' );
diff --git a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx
index 08cc47f569eaf..55542d434cb7a 100644
--- a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx
+++ b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx
@@ -7,17 +7,37 @@ import { useContext, useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { Form, FormField, SimpleFormField } from '../types';
+import type {
+ CombinedFormField,
+ Form,
+ FormField,
+ NormalizedField,
+ 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 >( {
+function doesCombinedFieldSupportBulkEdits< Item >(
+ combinedField: CombinedFormField,
+ fieldDefinitions: NormalizedField< Item >[]
+): boolean {
+ return combinedField.children.some( ( child ) => {
+ const fieldId = typeof child === 'string' ? child : child.id;
+
+ return fieldDefinitions.find(
+ ( fieldDefinition ) => fieldDefinition.id === fieldId
+ )?.supportsBulkEditing;
+ } );
+}
+
+export function DataFormLayout< Item extends object >( {
data,
form,
onChange,
children,
+ isBulkEditing,
}: {
data: Item;
form: Form;
@@ -31,6 +51,7 @@ export function DataFormLayout< Item >( {
} ) => React.JSX.Element | null,
field: FormField
) => React.JSX.Element;
+ isBulkEditing?: boolean;
} ) {
const { fields: fieldDefinitions } = useContext( DataFormContext );
@@ -69,6 +90,19 @@ export function DataFormLayout< Item >( {
return null;
}
+ if (
+ isBulkEditing &&
+ ( ( isCombinedField( formField ) &&
+ ! doesCombinedFieldSupportBulkEdits(
+ formField,
+ fieldDefinitions
+ ) ) ||
+ ( fieldDefinition &&
+ ! fieldDefinition.supportsBulkEditing ) )
+ ) {
+ return null;
+ }
+
if ( children ) {
return children( FieldLayout, formField );
}
@@ -79,6 +113,7 @@ export function DataFormLayout< Item >( {
data={ data }
field={ formField }
onChange={ onChange }
+ isBulkEditing={ isBulkEditing }
/>
);
} ) }
diff --git a/packages/dataviews/src/dataforms-layouts/panel/index.tsx b/packages/dataviews/src/dataforms-layouts/panel/index.tsx
index 269b2bb418a85..f7f70d8571c61 100644
--- a/packages/dataviews/src/dataforms-layouts/panel/index.tsx
+++ b/packages/dataviews/src/dataforms-layouts/panel/index.tsx
@@ -26,6 +26,7 @@ import type {
import DataFormContext from '../../components/dataform-context';
import { DataFormLayout } from '../data-form-layout';
import { isCombinedField } from '../is-combined-field';
+import { MIXED_VALUE } from '../../constants';
function DropdownHeader( {
title,
@@ -59,13 +60,14 @@ function DropdownHeader( {
);
}
-function PanelDropdown< Item >( {
+function PanelDropdown< Item extends object >( {
fieldDefinition,
popoverAnchor,
labelPosition = 'side',
data,
onChange,
field,
+ isBulkEditing,
}: {
fieldDefinition: NormalizedField< Item >;
popoverAnchor: HTMLElement | null;
@@ -73,6 +75,7 @@ function PanelDropdown< Item >( {
data: Item;
onChange: ( value: any ) => void;
field: FormField;
+ isBulkEditing?: boolean;
} ) {
const fieldLabel = isCombinedField( field )
? field.label
@@ -111,6 +114,9 @@ function PanelDropdown< Item >( {
[ popoverAnchor ]
);
+ const fieldValue = fieldDefinition.getValue( { item: data } );
+ const showMixedValue = isBulkEditing && fieldValue === MIXED_VALUE;
+
return (
( {
) }
onClick={ onToggle }
>
-
+ { showMixedValue ? (
+ __( 'Mixed' )
+ ) : (
+
+ ) }
) }
renderContent={ ( { onClose } ) => (
@@ -148,6 +158,7 @@ function PanelDropdown< Item >( {
data={ data }
form={ form as Form }
onChange={ onChange }
+ isBulkEditing={ isBulkEditing }
>
{ ( FieldLayout, nestedField ) => (
( {
data,
field,
onChange,
+ isBulkEditing,
}: FieldLayoutProps< Item > ) {
const { fields } = useContext( DataFormContext );
const fieldDefinition = fields.find( ( fieldDef ) => {
@@ -221,6 +233,7 @@ export default function FormPanelField< Item >( {
data={ data }
onChange={ onChange }
labelPosition={ labelPosition }
+ isBulkEditing={ isBulkEditing }
/>
@@ -237,6 +250,7 @@ export default function FormPanelField< Item >( {
data={ data }
onChange={ onChange }
labelPosition={ labelPosition }
+ isBulkEditing={ isBulkEditing }
/>
);
@@ -259,6 +273,7 @@ export default function FormPanelField< Item >( {
data={ data }
onChange={ onChange }
labelPosition={ labelPosition }
+ isBulkEditing={ isBulkEditing }
/>
diff --git a/packages/dataviews/src/dataforms-layouts/regular/index.tsx b/packages/dataviews/src/dataforms-layouts/regular/index.tsx
index a3d90b807b5cd..aa47d36e56339 100644
--- a/packages/dataviews/src/dataforms-layouts/regular/index.tsx
+++ b/packages/dataviews/src/dataforms-layouts/regular/index.tsx
@@ -30,7 +30,7 @@ function Header( { title }: { title: string } ) {
);
}
-export default function FormRegularField< Item >( {
+export default function FormRegularField< Item extends object >( {
data,
field,
onChange,
diff --git a/packages/dataviews/src/index.ts b/packages/dataviews/src/index.ts
index 4ca0cf3db0bd3..bb1a8c21c98ed 100644
--- a/packages/dataviews/src/index.ts
+++ b/packages/dataviews/src/index.ts
@@ -4,3 +4,4 @@ export { VIEW_LAYOUTS } from './dataviews-layouts';
export { filterSortAndPaginate } from './filter-and-sort-data-view';
export type * from './types';
export { isItemValid } from './validation';
+export { MIXED_VALUE as DATAFORM_MIXED_VALUE } from './constants';
diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts
index 2ed87cbe11222..c32fcf81e9ae7 100644
--- a/packages/dataviews/src/normalize-fields.ts
+++ b/packages/dataviews/src/normalize-fields.ts
@@ -64,6 +64,12 @@ export function normalizeFields< Item >(
);
};
+ let supportsBulkEditing = true;
+ // If custom Edit component is passed in we default to false for bulk edit support.
+ if ( typeof field.Edit === 'function' || field.supportsBulkEditing ) {
+ supportsBulkEditing = field.supportsBulkEditing ?? false;
+ }
+
const render =
field.render || ( field.elements ? renderFromElements : getValue );
@@ -76,6 +82,7 @@ export function normalizeFields< Item >(
sort,
isValid,
Edit,
+ supportsBulkEditing,
enableHiding: field.enableHiding ?? true,
enableSorting: field.enableSorting ?? true,
};
diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts
index 0bce8b8cf2c08..b05e13ae5dffc 100644
--- a/packages/dataviews/src/types.ts
+++ b/packages/dataviews/src/types.ts
@@ -158,6 +158,11 @@ export type Field< Item > = {
* Defaults to `item[ field.id ]`.
*/
getValue?: ( args: { item: Item } ) => any;
+
+ /**
+ * Whether the field supports bulk editing.
+ */
+ supportsBulkEditing?: boolean;
};
export type NormalizedField< Item > = Field< Item > & {
@@ -564,7 +569,7 @@ export type Form = {
};
export interface DataFormProps< Item > {
- data: Item;
+ data: Item | Item[];
fields: Field< Item >[];
form: Form;
onChange: ( value: Record< string, any > ) => void;
@@ -575,4 +580,5 @@ export interface FieldLayoutProps< Item > {
field: FormField;
onChange: ( value: any ) => void;
hideLabelFromVision?: boolean;
+ isBulkEditing?: boolean;
}
diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js
index 3e75ef71d1ac9..70ba9d230548e 100644
--- a/packages/edit-site/src/components/post-edit/index.js
+++ b/packages/edit-site/src/components/post-edit/index.js
@@ -11,7 +11,7 @@ import { DataForm } from '@wordpress/dataviews';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { __experimentalVStack as VStack } from '@wordpress/components';
-import { useState, useMemo, useEffect } from '@wordpress/element';
+import { useMemo } from '@wordpress/element';
import { privateApis as editorPrivateApis } from '@wordpress/editor';
/**
@@ -22,17 +22,30 @@ import { unlock } from '../../lock-unlock';
const { PostCardPanel, usePostFields } = unlock( editorPrivateApis );
-const fieldsWithBulkEditSupport = [
- 'title',
- 'status',
- 'date',
- 'author',
- 'comment_status',
-];
+const DATAFORM_CONFIG = {
+ type: 'panel',
+ fields: [
+ {
+ id: 'featured_media',
+ layout: 'regular',
+ },
+ 'title',
+ {
+ id: 'status',
+ label: __( 'Status & Visibility' ),
+ children: [ 'status', 'password' ],
+ },
+ 'author',
+ 'date',
+ 'slug',
+ 'parent',
+ 'comment_status',
+ ],
+};
function PostEditForm( { postType, postId } ) {
const ids = useMemo( () => postId.split( ',' ), [ postId ] );
- const { record } = useSelect(
+ const { record, records } = useSelect(
( select ) => {
return {
record:
@@ -43,11 +56,20 @@ function PostEditForm( { postType, postId } ) {
ids[ 0 ]
)
: null,
+ records:
+ ids.length > 1
+ ? ids.map( ( id ) =>
+ select( coreDataStore ).getEditedEntityRecord(
+ 'postType',
+ postType,
+ id
+ )
+ )
+ : null,
};
},
[ postType, ids ]
);
- const [ multiEdits, setMultiEdits ] = useState( {} );
const { editEntityRecord } = useDispatch( coreDataStore );
const { fields: _fields } = usePostFields( { postType } );
const fields = useMemo(
@@ -66,33 +88,6 @@ function PostEditForm( { postType, postId } ) {
[ _fields ]
);
- const form = useMemo(
- () => ( {
- type: 'panel',
- fields: [
- {
- id: 'featured_media',
- layout: 'regular',
- },
- 'title',
- {
- id: 'status',
- label: __( 'Status & Visibility' ),
- children: [ 'status', 'password' ],
- },
- 'author',
- 'date',
- 'slug',
- 'parent',
- 'comment_status',
- ].filter(
- ( field ) =>
- ids.length === 1 ||
- fieldsWithBulkEditSupport.includes( field )
- ),
- } ),
- [ ids ]
- );
const onChange = ( edits ) => {
for ( const id of ids ) {
if (
@@ -111,17 +106,8 @@ function PostEditForm( { postType, postId } ) {
edits.password = '';
}
editEntityRecord( 'postType', postType, id, edits );
- if ( ids.length > 1 ) {
- setMultiEdits( ( prev ) => ( {
- ...prev,
- ...edits,
- } ) );
- }
}
};
- useEffect( () => {
- setMultiEdits( {} );
- }, [ ids ] );
return (
@@ -129,9 +115,9 @@ function PostEditForm( { postType, postId } ) {
) }
diff --git a/packages/fields/src/fields/title/index.ts b/packages/fields/src/fields/title/index.ts
index d8e6f25276d6b..7666710012ac3 100644
--- a/packages/fields/src/fields/title/index.ts
+++ b/packages/fields/src/fields/title/index.ts
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import type { Field } from '@wordpress/dataviews';
+import { type Field } from '@wordpress/dataviews';
import { __ } from '@wordpress/i18n';
/**
@@ -16,7 +16,12 @@ const titleField: Field< BasePost > = {
id: 'title',
label: __( 'Title' ),
placeholder: __( 'No title' ),
- getValue: ( { item } ) => getItemTitle( item ),
+ getValue: ( { item } ) => {
+ if ( typeof item.title === 'symbol' ) {
+ return item.title;
+ }
+ return getItemTitle( item );
+ },
render: TitleView,
enableHiding: false,
};