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 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
5 changes: 3 additions & 2 deletions packages/dataviews/src/dataviews.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 )
);
} )
);
}
} }
/>
);
}
22 changes: 17 additions & 5 deletions packages/dataviews/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,20 @@
justify-content: flex-start;

.dataviews-view-grid__title-actions {
padding: 0 $grid-unit-05;
padding: $grid-unit-05 $grid-unit 0 $grid-unit-05;
}

.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
Original file line number Diff line number Diff line change
@@ -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>
);
}
Loading
Loading