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

Add: Selection and bulk actions to grid view. #58144

Merged
merged 6 commits into from Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/dataviews/src/dataviews.js
Expand Up @@ -14,7 +14,7 @@ import Pagination from './pagination';
import ViewActions from './view-actions';
import Filters from './filters';
import Search from './search';
import { VIEW_LAYOUTS, LAYOUT_TABLE } from './constants';
import { VIEW_LAYOUTS, LAYOUT_TABLE, LAYOUT_GRID } from './constants';
import BulkActions from './bulk-actions';

const defaultGetItemId = ( item ) => item.id;
Expand Down Expand Up @@ -93,7 +93,8 @@ export default function DataViews( {
onChangeView={ onChangeView }
/>
</HStack>
{ view.type === LAYOUT_TABLE && (
{ ( view.type === LAYOUT_TABLE ||
view.type === LAYOUT_GRID ) && (
<BulkActions
actions={ actions }
data={ data }
Expand Down
59 changes: 59 additions & 0 deletions packages/dataviews/src/single-selection-checkbox.js
@@ -0,0 +1,59 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { CheckboxControl } from '@wordpress/components';

export default function SingleSelectionCheckbox( {
selection,
onSelectionChange,
item,
data,
getItemId,
primaryField,
} ) {
const id = getItemId( item );
const isSelected = selection.includes( id );
let selectionLabel;
if ( primaryField?.getValue && item ) {
// eslint-disable-next-line @wordpress/valid-sprintf
selectionLabel = sprintf(
/* translators: %s: item title. */
isSelected ? __( 'Deselect item: %s' ) : __( 'Select item: %s' ),
primaryField.getValue( { item } )
);
} else {
selectionLabel = isSelected
? __( 'Select a new item' )
: __( 'Deselect item' );
}
return (
<CheckboxControl
className="dataviews-view-table-selection-checkbox"
__nextHasNoMarginBottom
checked={ isSelected }
label={ selectionLabel }
onChange={ () => {
if ( ! isSelected ) {
onSelectionChange(
data.filter( ( _item ) => {
const itemId = getItemId?.( _item );
return (
itemId === id || selection.includes( itemId )
);
} )
);
} else {
onSelectionChange(
data.filter( ( _item ) => {
const itemId = getItemId?.( _item );
return (
itemId !== id && selection.includes( itemId )
);
} )
);
}
} }
/>
);
}
20 changes: 16 additions & 4 deletions packages/dataviews/src/style.scss
Expand Up @@ -280,6 +280,14 @@
.dataviews-view-grid__primary-field {
min-height: $grid-unit-50;
}
&.is-selected {
border-color: var(--wp-admin-theme-color);
background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04);

.dataviews-view-grid__fields .dataviews-view-grid__field .dataviews-view-grid__field-value {
color: $gray-900;
}
}
}

.dataviews-view-grid__media {
Expand All @@ -297,10 +305,6 @@
}
}

.dataviews-view-grid__primary-field {
padding: $grid-unit-10;
}

.dataviews-view-grid__fields {
position: relative;
font-size: 12px;
Expand Down Expand Up @@ -495,3 +499,11 @@
.dataviews-bulk-edit-button.components-button {
flex-shrink: 0;
}

.dataviews-view-grid__title-actions .dataviews-view-table-selection-checkbox {
margin-left: $grid-unit-10;
}

.dataviews-view-grid__card.has-no-pointer-events * {
pointer-events: none;
}
180 changes: 127 additions & 53 deletions packages/dataviews/src/view-grid.js
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
Expand All @@ -8,11 +13,115 @@ import {
Tooltip,
} from '@wordpress/components';
import { useAsyncList } from '@wordpress/compose';
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import ItemActions from './item-actions';
import SingleSelectionCheckbox from './single-selection-checkbox';

function GridItem( {
selection,
data,
onSelectionChange,
getItemId,
item,
actions,
mediaField,
primaryField,
visibleFields,
} ) {
const [ hasNoPointerEvents, setHasNoPointerEvents ] = useState( false );
const id = getItemId( item );
const isSelected = selection.includes( id );
return (
<VStack
spacing={ 0 }
key={ id }
className={ classnames( 'dataviews-view-grid__card', {
'is-selected': isSelected,
'has-no-pointer-events': hasNoPointerEvents,
} ) }
onMouseDown={ ( event ) => {
if ( event.ctrlKey || event.metaKey ) {
setHasNoPointerEvents( true );
if ( ! isSelected ) {
onSelectionChange(
data.filter( ( _item ) => {
const itemId = getItemId?.( _item );
return (
itemId === id ||
selection.includes( itemId )
);
} )
);
} else {
onSelectionChange(
data.filter( ( _item ) => {
const itemId = getItemId?.( _item );
return (
itemId !== id &&
selection.includes( itemId )
);
} )
);
}
}
} }
onClick={ () => {
if ( hasNoPointerEvents ) {
setHasNoPointerEvents( false );
}
} }
>
<div className="dataviews-view-grid__media">
{ mediaField?.render( { item } ) }
</div>
<HStack
justify="space-between"
className="dataviews-view-grid__title-actions"
>
<SingleSelectionCheckbox
id={ id }
item={ item }
selection={ selection }
onSelectionChange={ onSelectionChange }
getItemId={ getItemId }
data={ data }
primaryField={ primaryField }
/>
<HStack className="dataviews-view-grid__primary-field">
{ primaryField?.render( { item } ) }
</HStack>
<ItemActions item={ item } actions={ actions } isCompact />
</HStack>
<VStack className="dataviews-view-grid__fields" spacing={ 3 }>
{ visibleFields.map( ( field ) => {
const renderedValue = field.render( {
item,
} );
if ( ! renderedValue ) {
return null;
}
return (
<VStack
className="dataviews-view-grid__field"
key={ field.id }
spacing={ 1 }
>
<Tooltip text={ field.header } placement="left">
<div className="dataviews-view-grid__field-value">
{ renderedValue }
</div>
</Tooltip>
</VStack>
);
} ) }
</VStack>
</VStack>
);
}

export default function ViewGrid( {
data,
Expand All @@ -21,6 +130,8 @@ export default function ViewGrid( {
actions,
getItemId,
deferredRendering,
selection,
onSelectionChange,
} ) {
const mediaField = fields.find(
( field ) => field.id === view.layout.mediaField
Expand All @@ -44,59 +155,22 @@ export default function ViewGrid( {
alignment="top"
className="dataviews-view-grid"
>
{ usedData.map( ( item ) => (
<VStack
spacing={ 0 }
key={ getItemId( item ) }
className="dataviews-view-grid__card"
>
<div className="dataviews-view-grid__media">
{ mediaField?.render( { item } ) }
</div>
<HStack
justify="space-between"
className="dataviews-view-grid__title-actions"
>
<HStack className="dataviews-view-grid__primary-field">
{ primaryField?.render( { item } ) }
</HStack>
<ItemActions
item={ item }
actions={ actions }
isCompact
/>
</HStack>
<VStack
className="dataviews-view-grid__fields"
spacing={ 3 }
>
{ visibleFields.map( ( field ) => {
const renderedValue = field.render( {
item,
} );
if ( ! renderedValue ) {
return null;
}
return (
<VStack
className="dataviews-view-grid__field"
key={ field.id }
spacing={ 1 }
>
<Tooltip
text={ field.header }
placement="left"
>
<div className="dataviews-view-grid__field-value">
{ renderedValue }
</div>
</Tooltip>
</VStack>
);
} ) }
</VStack>
</VStack>
) ) }
{ usedData.map( ( item ) => {
return (
<GridItem
key={ getItemId( item ) }
selection={ selection }
data={ data }
onSelectionChange={ onSelectionChange }
getItemId={ getItemId }
item={ item }
actions={ actions }
mediaField={ mediaField }
primaryField={ primaryField }
visibleFields={ visibleFields }
/>
);
} ) }
</Grid>
);
}