diff --git a/packages/dataviews/src/bulk-actions.js b/packages/dataviews/src/bulk-actions.js
new file mode 100644
index 0000000000000..3de09e6a15ab4
--- /dev/null
+++ b/packages/dataviews/src/bulk-actions.js
@@ -0,0 +1,117 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ ToolbarButton,
+ Toolbar,
+ ToolbarGroup,
+ Popover,
+} from '@wordpress/components';
+import { useMemo } from '@wordpress/element';
+import { __, _n, sprintf } from '@wordpress/i18n';
+/**
+ * Internal dependencies
+ */
+import { ActionWithModal } from './item-actions';
+
+function PrimaryActionTrigger( { action, onClick } ) {
+ return (
+
+ );
+}
+
+const EMPTY_ARRAY = [];
+
+export default function BulkActions( {
+ data,
+ selection,
+ actions = EMPTY_ARRAY,
+ setSelection,
+} ) {
+ const items = useMemo(
+ () =>
+ data?.filter( ( item ) => selection?.includes( item.id ) ) ??
+ EMPTY_ARRAY,
+ [ data, selection ]
+ );
+ const primaryActions = useMemo(
+ () =>
+ actions.filter( ( action ) => {
+ return (
+ action.isBulk &&
+ action.isPrimary &&
+ items.every( ( item ) => action.isEligible( item ) )
+ );
+ } ),
+ [ actions, items ]
+ );
+
+ if (
+ ( selection && selection.length === 0 ) ||
+ primaryActions.length === 0
+ ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {} } disabled={ true }>
+ {
+ // translators: %s: Total number of selected items.
+ sprintf(
+ // translators: %s: Total number of selected items.
+ _n(
+ '%s item selected',
+ '%s items selected',
+ selection.length
+ ),
+ selection.length
+ )
+ }
+
+ {
+ setSelection( EMPTY_ARRAY );
+ } }
+ >
+ { __( 'Deselect' ) }
+
+
+
+ { primaryActions.map( ( action ) => {
+ if ( !! action.RenderModal ) {
+ return (
+
+ );
+ }
+ return (
+ action.callback( items ) }
+ />
+ );
+ } ) }
+
+
+
+
+ );
+}
diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js
index b75155e8fddf0..5c968c394130e 100644
--- a/packages/dataviews/src/dataviews.js
+++ b/packages/dataviews/src/dataviews.js
@@ -15,6 +15,7 @@ import ViewActions from './view-actions';
import Filters from './filters';
import Search from './search';
import { VIEW_LAYOUTS } from './constants';
+import BulkActions from './bulk-actions';
export default function DataViews( {
view,
@@ -28,6 +29,9 @@ export default function DataViews( {
isLoading = false,
paginationInfo,
supportedLayouts,
+ selection,
+ setSelection,
+ labels,
} ) {
const ViewComponent = VIEW_LAYOUTS.find(
( v ) => v.type === view.type
@@ -72,12 +76,24 @@ export default function DataViews( {
data={ data }
getItemId={ getItemId }
isLoading={ isLoading }
+ selection={ selection }
+ setSelection={ setSelection }
+ labels={ labels }
/>
+
+
);
diff --git a/packages/dataviews/src/item-actions.js b/packages/dataviews/src/item-actions.js
index 1b0bd5f213ca8..5b960f51fe68c 100644
--- a/packages/dataviews/src/item-actions.js
+++ b/packages/dataviews/src/item-actions.js
@@ -46,12 +46,18 @@ function DropdownMenuItemTrigger( { action, onClick } ) {
);
}
-function ActionWithModal( { action, item, ActionTrigger } ) {
+export function ActionWithModal( { action, item, items, ActionTrigger } ) {
const [ isModalOpen, setIsModalOpen ] = useState( false );
const actionTriggerProps = {
action,
onClick: () => setIsModalOpen( true ),
};
+ const additionalProps = {};
+ if ( action.isBulk ) {
+ additionalProps.items = items ? items : [ item ];
+ } else {
+ additionalProps.item = item;
+ }
const { RenderModal, hideModalHeader } = action;
return (
<>
@@ -66,7 +72,7 @@ function ActionWithModal( { action, item, ActionTrigger } ) {
overlayClassName="dataviews-action-modal"
>
setIsModalOpen( false ) }
/>
@@ -157,7 +163,11 @@ export default function ItemActions( { item, actions, isCompact } ) {
action.callback( item ) }
+ onClick={
+ action.isBulk
+ ? () => action.callback( [ item ] )
+ : () => action.callback( item )
+ }
/>
);
} ) }
diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss
index 5dd89f4c27970..aa9ee642bc1c2 100644
--- a/packages/dataviews/src/style.scss
+++ b/packages/dataviews/src/style.scss
@@ -130,3 +130,25 @@
.dataviews-action-modal {
z-index: z-index(".dataviews-action-modal");
}
+
+.dataviews-bulk-actions-popover .components-popover__content {
+ min-width: max-content;
+}
+
+.dataviews-bulk-actions-toolbar-wrapper {
+ display: flex;
+ flex-grow: 1;
+ width: 100%;
+}
+
+.dataviews-table-view__selection-column label {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js
index 8b6422b4be11a..663ac1cb2fc2c 100644
--- a/packages/dataviews/src/view-table.js
+++ b/packages/dataviews/src/view-table.js
@@ -29,6 +29,7 @@ import {
Button,
Icon,
privateApis as componentsPrivateApis,
+ CheckboxControl,
} from '@wordpress/components';
import { useMemo, Children, Fragment } from '@wordpress/element';
@@ -340,7 +341,11 @@ function ViewTable( {
getItemId,
isLoading = false,
paginationInfo,
+ selection,
+ setSelection,
+ labels,
} ) {
+ const areAllSelected = selection && selection.length === data.length;
const columns = useMemo( () => {
const _columns = fields.map( ( field ) => {
const { render, getValue, ...column } = field;
@@ -351,6 +356,70 @@ function ViewTable( {
}
return column;
} );
+ if ( selection !== undefined ) {
+ _columns.unshift( {
+ header: (
+ {
+ if ( areAllSelected ) {
+ setSelection( [] );
+ } else {
+ setSelection( data.map( ( { id } ) => id ) );
+ }
+ } }
+ label={
+ areAllSelected
+ ? __( 'Deselect all' )
+ : __( 'Select all' )
+ }
+ />
+ ),
+ id: 'selection',
+ cell: ( props ) => {
+ //console.log({ props });
+ const item = props.row.original;
+ const isSelected = selection.includes( item.id );
+ let selectionLabel;
+ if ( isSelected ) {
+ selectionLabel = labels?.getDeselectLabel
+ ? labels?.getDeselectLabel( item )
+ : __( 'Deselect item' );
+ } else {
+ selectionLabel = labels?.getSelectLabel
+ ? labels?.getSelectLabel( item )
+ : __( 'Select a new item' );
+ }
+ return (
+ {
+ if ( ! isSelected ) {
+ const newSelection = [
+ ...selection,
+ item.id,
+ ];
+ setSelection( newSelection );
+ } else {
+ setSelection(
+ selection.filter(
+ ( id ) => id !== item.id
+ )
+ );
+ }
+ } }
+ />
+ );
+ },
+ enableHiding: false,
+ width: 40,
+ className: 'dataviews-table-view__selection-column',
+ } );
+ }
if ( actions?.length ) {
_columns.push( {
header: __( 'Actions' ),
@@ -368,7 +437,15 @@ function ViewTable( {
}
return _columns;
- }, [ fields, actions, view ] );
+ }, [
+ areAllSelected,
+ fields,
+ actions,
+ view,
+ selection,
+ setSelection,
+ data,
+ ] );
const columnVisibility = useMemo( () => {
if ( ! view.hiddenFields?.length ) {
@@ -566,6 +643,10 @@ function ViewTable( {
header.column.columnDef
.maxWidth || undefined,
} }
+ className={
+ header.column.columnDef.className ||
+ undefined
+ }
data-field-id={ header.id }
>
{ flexRender(
cell.column.columnDef.cell,
diff --git a/packages/edit-site/src/components/actions/index.js b/packages/edit-site/src/components/actions/index.js
index ca673e3867bda..df66fe9957287 100644
--- a/packages/edit-site/src/components/actions/index.js
+++ b/packages/edit-site/src/components/actions/index.js
@@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url';
import { useDispatch } from '@wordpress/data';
import { decodeEntities } from '@wordpress/html-entities';
import { store as coreStore } from '@wordpress/core-data';
-import { __, sprintf } from '@wordpress/i18n';
+import { __, sprintf, _n } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { useMemo } from '@wordpress/element';
import { privateApis as routerPrivateApis } from '@wordpress/router';
@@ -28,23 +28,34 @@ export const trashPostAction = {
id: 'move-to-trash',
label: __( 'Move to Trash' ),
isPrimary: true,
+ isBulk: true,
icon: trash,
isEligible( { status } ) {
return status !== 'trash';
},
hideModalHeader: true,
- RenderModal: ( { item: post, closeModal } ) => {
+ RenderModal: ( { items: posts, closeModal } ) => {
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const { deleteEntityRecord } = useDispatch( coreStore );
return (
- { sprintf(
- // translators: %s: The page's title.
- __( 'Are you sure you want to delete "%s"?' ),
- decodeEntities( post.title.rendered )
- ) }
+ { posts.length > 1
+ ? sprintf(
+ // translators: %s: The number of posts (always plural).
+ __(
+ 'Are you sure you want to delete %s posts?'
+ ),
+ decodeEntities( posts.length )
+ )
+ : sprintf(
+ // translators: %s: The page's title.
+ __( 'Are you sure you want to delete "%s"?' ),
+ decodeEntities(
+ posts && posts[ 0 ]?.title?.rendered
+ )
+ ) }