diff --git a/packages/edit-site/src/components/bulk-actions/index.js b/packages/edit-site/src/components/bulk-actions/index.js new file mode 100644 index 00000000000000..390f9600a0810a --- /dev/null +++ b/packages/edit-site/src/components/bulk-actions/index.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { trash } from '@wordpress/icons'; +import { useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +export function useBulkTrashPostAction() { + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const { deleteEntityRecord } = useDispatch( coreStore ); + return useMemo( + () => ( { + id: 'move-to-trash', + label: __( 'Move to Trash' ), + isPrimary: true, + icon: trash, + isEligible( data, selection ) { + console.log( { + p: data.filter( ( post ) => selection.includes( post.id ) ), + } ); + return ! data + .filter( ( post ) => selection.includes( post.id ) ) + .some( ( post ) => post.status === 'trash' ); + }, + async callback( data, selection ) { + const postsToDelete = data.filter( ( post ) => + selection.includes( post.id ) + ); + try { + await Promise.all( + postsToDelete.map( async ( post ) => { + deleteEntityRecord( + 'postType', + post.type, + post.id, + {}, + { throwOnError: true } + ); + } ) + ); + createSuccessNotice( + __( 'The selected posts were moved to the trash.' ), + { + type: 'snackbar', + id: 'edit-site-page-trashed', + } + ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( + 'An error occurred while moving the posts to the trash.' + ); + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + }, + } ), + [ createErrorNotice, createSuccessNotice, deleteEntityRecord ] + ); +} diff --git a/packages/edit-site/src/components/dataviews/bulk-actions.js b/packages/edit-site/src/components/dataviews/bulk-actions.js new file mode 100644 index 00000000000000..cd55aa94c9b2a7 --- /dev/null +++ b/packages/edit-site/src/components/dataviews/bulk-actions.js @@ -0,0 +1,94 @@ +/** + * WordPress dependencies + */ +import { + ToolbarButton, + Toolbar, + ToolbarGroup, + ToolbarItem, + Popover, +} from '@wordpress/components'; +import { useMemo } from '@wordpress/element'; +import { __, _n, sprintf } from '@wordpress/i18n'; + +function PrimaryActionTrigger( { action, onClick } ) { + return ( + + ); +} + +const EMPTY_ARRAY = []; + +export default function BulkActions( { + data, + selection, + bulkActions = EMPTY_ARRAY, + setSelection, +} ) { + const primaryActions = useMemo( + () => + bulkActions.filter( ( action ) => { + return action.isPrimary && action.isEligible( data, selection ); + } ), + [ bulkActions, data, selection ] + ); + 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 ) => { + return ( + + action.callback( data, selection ) + } + /> + ); + } ) } + +
+
+
+ ); +} diff --git a/packages/edit-site/src/components/dataviews/dataviews.js b/packages/edit-site/src/components/dataviews/dataviews.js index 682060203de7c3..b9fbff2b438e0d 100644 --- a/packages/edit-site/src/components/dataviews/dataviews.js +++ b/packages/edit-site/src/components/dataviews/dataviews.js @@ -4,6 +4,7 @@ import { __experimentalVStack as VStack, __experimentalHStack as HStack, + Popover, } from '@wordpress/components'; import { useMemo } from '@wordpress/element'; @@ -17,6 +18,7 @@ import Filters from './filters'; import Search from './search'; import { ViewGrid } from './view-grid'; import { ViewSideBySide } from './view-side-by-side'; +import BulkActions from './bulk-actions'; // To do: convert to view type registry. export const viewTypeSupportsMap = { @@ -45,6 +47,9 @@ export default function DataViews( { isLoading = false, paginationInfo, supportedLayouts, + selection, + setSelection, + bulkActions, } ) { const ViewComponent = viewTypeMap[ view.type ]; const _fields = useMemo( () => { @@ -89,12 +94,23 @@ export default function DataViews( { data={ data } getItemId={ getItemId } isLoading={ isLoading } + selection={ selection } + setSelection={ setSelection } /> - + +
+ + +
); diff --git a/packages/edit-site/src/components/dataviews/style.scss b/packages/edit-site/src/components/dataviews/style.scss index 76e6fca78de67d..4aba17a640f417 100644 --- a/packages/edit-site/src/components/dataviews/style.scss +++ b/packages/edit-site/src/components/dataviews/style.scss @@ -53,3 +53,14 @@ .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%; +} + diff --git a/packages/edit-site/src/components/dataviews/view-list.js b/packages/edit-site/src/components/dataviews/view-list.js index 0939297e374dfa..e1e03b39c01d58 100644 --- a/packages/edit-site/src/components/dataviews/view-list.js +++ b/packages/edit-site/src/components/dataviews/view-list.js @@ -28,6 +28,7 @@ import { Button, Icon, privateApis as componentsPrivateApis, + CheckboxControl, } from '@wordpress/components'; import { useMemo, Children, Fragment } from '@wordpress/element'; @@ -252,9 +253,12 @@ function ViewList( { getItemId, isLoading = false, paginationInfo, + selection, + setSelection, } ) { + const areAllSelected = selection && selection.length === data.length; const columns = useMemo( () => { - const _columns = fields.map( ( field ) => { + const fieldsColumns = fields.map( ( field ) => { const { render, getValue, ...column } = field; column.cell = ( props ) => render( { item: props.row.original, view } ); @@ -263,6 +267,61 @@ function ViewList( { } return column; } ); + const _columns = + selection !== undefined + ? [ + { + header: ( + { + if ( areAllSelected ) { + setSelection( [] ); + } else { + setSelection( + data.map( ( { id } ) => id ) + ); + } + } } + /> + ), + id: 'selection', + cell: ( props ) => { + //console.log({ props }); + const item = props.row.original; + const isSelected = selection.includes( + item.id + ); + //console.log({ item, isSelected }); + return ( + { + if ( ! isSelected ) { + const newSelection = [ + ...selection, + item.id, + ]; + setSelection( newSelection ); + } else { + setSelection( + selection.filter( + ( id ) => id !== item.id + ) + ); + } + } } + /> + ); + }, + enableHiding: false, + width: 40, + }, + ...fieldsColumns, + ] + : fieldsColumns; if ( actions?.length ) { _columns.push( { header: __( 'Actions' ), @@ -280,7 +339,15 @@ function ViewList( { } return _columns; - }, [ fields, actions, view ] ); + }, [ + areAllSelected, + fields, + actions, + view, + selection, + setSelection, + data, + ] ); const columnVisibility = useMemo( () => { if ( ! view.hiddenFields?.length ) { diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index b933f1d82500e4..66d063a6cf4043 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -28,6 +28,7 @@ import { viewPostAction, useEditPostAction, } from '../actions'; +import { useBulkTrashPostAction } from '../bulk-actions'; import SideEditor from './side-editor'; import Media from '../media'; import { unlock } from '../../lock-unlock'; @@ -157,6 +158,20 @@ export default function PagePages() { totalPages, } = useEntityRecords( 'postType', postType, queryArgs ); + useEffect( () => { + if ( + selection.some( + ( id ) => ! pages?.some( ( page ) => page.id === id ) + ) + ) { + setSelection( + selection.filter( ( id ) => + pages?.some( ( page ) => page.id === id ) + ) + ); + } + }, [ pages, selection ] ); + const { records: authors, isResolving: isLoadingAuthors } = useEntityRecords( 'root', 'user' ); @@ -271,6 +286,9 @@ export default function PagePages() { const permanentlyDeletePostAction = usePermanentlyDeletePostAction(); const restorePostAction = useRestorePostAction(); const editPostAction = useEditPostAction(); + + const bulkTrashPostAction = useBulkTrashPostAction(); + const actions = useMemo( () => [ viewPostAction, @@ -287,6 +305,11 @@ export default function PagePages() { editPostAction, ] ); + + const bulkActions = useMemo( + () => [ bulkTrashPostAction ], + [ bulkTrashPostAction ] + ); const onChangeView = useCallback( ( viewUpdater ) => { let updatedView = @@ -315,11 +338,14 @@ export default function PagePages() { paginationInfo={ paginationInfo } fields={ fields } actions={ actions } + bulkActions={ bulkActions } data={ pages || EMPTY_ARRAY } getItemId={ ( item ) => item.id } isLoading={ isLoadingPages || isLoadingAuthors } view={ view } onChangeView={ onChangeView } + selection={ selection } + setSelection={ setSelection } /> { viewTypeSupportsMap[ view.type ].preview && (