Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dataviews: Add: Bulk actions toolbar. #59714

Merged
merged 9 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/base-styles/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ $z-layers: (

// Ensure checkbox + actions don't overlap table header
".dataviews-view-table thead": 1,

// Ensure quick actions toolbar appear above pagination
".dataviews-bulk-actions": 2,
);

@function z-index( $key ) {
Expand Down
244 changes: 244 additions & 0 deletions packages/dataviews/src/bulk-actions-toolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/**
* WordPress dependencies
*/
import {
ToolbarButton,
Toolbar,
ToolbarGroup,
__unstableMotion as motion,
__unstableAnimatePresence as AnimatePresence,
} from '@wordpress/components';
import { useMemo, useState, useRef } from '@wordpress/element';
import { _n, sprintf, __ } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';
import { useReducedMotion } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { ActionWithModal } from './item-actions';

const SNACKBAR_VARIANTS = {
init: {
bottom: -48,
},
open: {
bottom: 24,
transition: {
bottom: { type: 'tween', duration: 0.2, ease: [ 0, 0, 0.2, 1 ] },
},
},
exit: {
opacity: 0,
bottom: 24,
transition: {
opacity: { type: 'tween', duration: 0.2, ease: [ 0, 0, 0.2, 1 ] },
},
},
};

function ActionTrigger( { action, onClick, isBusy } ) {
return (
<ToolbarButton
disabled={ isBusy }
label={ action.label }
icon={ action.icon }
isDestructive={ action.isDestructive }
size="compact"
onClick={ onClick }
isBusy={ isBusy }
__experimentalIsFocusable
tooltipPosition="top"
/>
);
}

const EMPTY_ARRAY = [];

function ActionButton( {
action,
selectedItems,
actionInProgress,
setActionInProgress,
} ) {
const selectedEligibleItems = useMemo( () => {
return selectedItems.filter( ( item ) => {
return action.isEligible( item );
} );
}, [ action, selectedItems ] );
if ( !! action.RenderModal ) {
return (
<ActionWithModal
key={ action.id }
action={ action }
items={ selectedEligibleItems }
ActionTrigger={ ActionTrigger }
onActionStart={ () => {
setActionInProgress( action.id );
} }
onActionPerformed={ () => {
setActionInProgress( null );
} }
/>
);
}
return (
<ActionTrigger
key={ action.id }
action={ action }
items={ selectedItems }
onClick={ () => {
setActionInProgress( action.id );
action.callback( selectedItems, () => {
setActionInProgress( action.id );
} );
} }
isBusy={ actionInProgress === action.id }
/>
);
}

function renderToolbarContent(
selection,
actionsToShow,
selectedItems,
actionInProgress,
setActionInProgress,
setSelection
) {
return (
<>
<ToolbarGroup>
<div className="dataviews-bulk-actions__selection-count">
{ selection.length === 1
? __( '1 item selected' )
: sprintf(
// translators: %s: Total number of selected items.
_n(
'%s item selected',
'%s items selected',
selection.length
),
selection.length
) }
</div>
</ToolbarGroup>
<ToolbarGroup>
{ actionsToShow.map( ( action ) => {
return (
<ActionButton
key={ action.id }
action={ action }
selectedItems={ selectedItems }
actionInProgress={ actionInProgress }
setActionInProgress={ setActionInProgress }
/>
);
} ) }
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton
icon={ closeSmall }
showTooltip
tooltipPosition="top"
label={ __( 'Cancel' ) }
disabled={ !! actionInProgress }
onClick={ () => {
setSelection( EMPTY_ARRAY );
} }
/>
</ToolbarGroup>
</>
);
}

function ToolbarContent( {
selection,
actionsToShow,
selectedItems,
setSelection,
} ) {
const [ actionInProgress, setActionInProgress ] = useState( null );
const buttons = useRef( null );
if ( ! actionInProgress ) {
if ( buttons.current ) {
buttons.current = null;
}
return renderToolbarContent(
selection,
actionsToShow,
selectedItems,
actionInProgress,
setActionInProgress,
setSelection
);
} else if ( ! buttons.current ) {
buttons.current = renderToolbarContent(
selection,
actionsToShow,
selectedItems,
actionInProgress,
setActionInProgress,
setSelection
);
}
return buttons.current;
}

export default function BulkActionsToolbar( {
data,
selection,
actions = EMPTY_ARRAY,
setSelection,
getItemId,
} ) {
const isReducedMotion = useReducedMotion();
const selectedItems = useMemo( () => {
return data.filter( ( item ) =>
selection.includes( getItemId( item ) )
);
}, [ selection, data, getItemId ] );

const actionsToShow = useMemo(
() =>
actions.filter( ( action ) => {
return (
action.supportsBulk &&
action.icon &&
selectedItems.some( ( item ) => action.isEligible( item ) )
);
} ),
[ actions, selectedItems ]
);

if (
( selection && selection.length === 0 ) ||
actionsToShow.length === 0
) {
return null;
}

return (
<AnimatePresence>
<motion.div
layout={ ! isReducedMotion } // See https://www.framer.com/docs/animation/#layout-animations
initial={ 'init' }
animate={ 'open' }
exit={ 'exit' }
variants={ isReducedMotion ? undefined : SNACKBAR_VARIANTS }
className="dataviews-bulk-actions"
>
<Toolbar label={ __( 'Bulk actions' ) }>
<div className="dataviews-bulk-actions-toolbar-wrapper">
<ToolbarContent
selection={ selection }
actionsToShow={ actionsToShow }
selectedItems={ selectedItems }
setSelection={ setSelection }
/>
</div>
</Toolbar>
</motion.div>
</AnimatePresence>
);
}
11 changes: 11 additions & 0 deletions packages/dataviews/src/dataviews.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { LAYOUT_TABLE, LAYOUT_GRID } from './constants';
import { VIEW_LAYOUTS } from './layouts';
import BulkActions from './bulk-actions';
import { normalizeFields } from './normalize-fields';
import BulkActionsToolbar from './bulk-actions-toolbar';

const defaultGetItemId = ( item ) => item.id;
const defaultOnSelectionChange = () => {};
Expand Down Expand Up @@ -143,6 +144,16 @@ export default function DataViews( {
onChangeView={ onChangeView }
paginationInfo={ paginationInfo }
/>
{ [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) &&
hasPossibleBulkAction && (
<BulkActionsToolbar
data={ data }
actions={ actions }
selection={ selection }
setSelection={ setSelection }
getItemId={ getItemId }
/>
) }
</div>
);
}
23 changes: 18 additions & 5 deletions packages/dataviews/src/item-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,22 @@ function DropdownMenuItemTrigger( { action, onClick } ) {
);
}

function ActionWithModal( { action, item, ActionTrigger } ) {
export function ActionWithModal( {
action,
items,
ActionTrigger,
onActionStart,
onActionPerformed,
isBusy,
} ) {
const [ isModalOpen, setIsModalOpen ] = useState( false );
const actionTriggerProps = {
action,
onClick: () => setIsModalOpen( true ),
onClick: () => {
setIsModalOpen( true );
},
items,
isBusy,
};
const { RenderModal, hideModalHeader } = action;
return (
Expand All @@ -69,8 +80,10 @@ function ActionWithModal( { action, item, ActionTrigger } ) {
) }` }
>
<RenderModal
items={ [ item ] }
items={ items }
closeModal={ () => setIsModalOpen( false ) }
onActionStart={ onActionStart }
onActionPerformed={ onActionPerformed }
/>
</Modal>
) }
Expand All @@ -87,7 +100,7 @@ function ActionsDropdownMenuGroup( { actions, item } ) {
<ActionWithModal
key={ action.id }
action={ action }
item={ item }
items={ [ item ] }
ActionTrigger={ DropdownMenuItemTrigger }
/>
);
Expand Down Expand Up @@ -139,7 +152,7 @@ export default function ItemActions( { item, actions, isCompact } ) {
<ActionWithModal
key={ action.id }
action={ action }
item={ item }
items={ [ item ] }
ActionTrigger={ ButtonTrigger }
/>
);
Expand Down
45 changes: 45 additions & 0 deletions packages/dataviews/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -783,3 +783,48 @@
}
}
}


.dataviews-bulk-actions-toolbar-wrapper {
display: flex;
flex-grow: 1;
width: 100%;

.components-toolbar-group {
align-items: center;
}

.components-button.is-busy {
max-height: $button-size;
}
}

.dataviews-bulk-actions {
position: absolute;
display: flex;
flex-direction: column;
align-content: center;
flex-wrap: wrap;
width: 100%;
bottom: $grid-unit-30;
z-index: z-index(".dataviews-bulk-actions");

.components-accessible-toolbar {
border-color: $gray-300;
box-shadow: $shadow-popover;

.components-toolbar-group {
border-color: $gray-200;

&:last-child {
border: 0;
}
}
}

.dataviews-bulk-actions__selection-count {
display: flex;
align-items: center;
margin: 0 $grid-unit-10 0 $grid-unit-10;
}
}
Loading
Loading