From 8e0ab842f5f057e34577ffd8eb05b01c1bec644b Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 23 Nov 2023 12:10:18 +0000 Subject: [PATCH] partial feedback (+7 squashed commits) Squashed commits: [43c3d854d5] Fix rebase [cb954b6583] Fix rebase [0a3723a865] add selection label functionality [3c48c6859a] Add labels [2ae6b76b3c] Lint fixes [77dc74c661] Feedback [d0d1456cc2] Add: Bulk actions API and trash action. --- packages/dataviews/src/bulk-actions.js | 117 ++++++++++++++++++ packages/dataviews/src/dataviews.js | 16 +++ packages/dataviews/src/item-actions.js | 16 ++- packages/dataviews/src/style.scss | 22 ++++ packages/dataviews/src/view-table.js | 87 ++++++++++++- .../edit-site/src/components/actions/index.js | 65 +++++++--- .../src/components/page-pages/index.js | 37 +++++- packages/edit-site/src/store/actions.js | 2 +- 8 files changed, 336 insertions(+), 26 deletions(-) create mode 100644 packages/dataviews/src/bulk-actions.js 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 + ) + ) }