Skip to content
Merged
Next Next commit
DataViews: Add groupByField support to grid layout
Add groupByField property to ViewBase interface and implement grouping functionality in grid layout. When groupByField is specified, data is grouped by the field value and displayed with group headers. Each group maintains the same grid layout structure while being visually separated.

Changes:
- Add groupByField property to ViewBase type
- Implement grouping logic in grid layout component
- Add styling for group headers
- Add GroupedGridLayout story to demonstrate the feature

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
  • Loading branch information
oandregal and claude committed Jul 21, 2025
commit 5339afb53030e6934fc2ed4d0a72727d354537f1
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,38 @@ export const CustomPerPageSizes = () => {
/>
);
};

export const GroupedGridLayout = () => {
const [ view, setView ] = useState< View >( {
type: LAYOUT_GRID,
search: '',
page: 1,
perPage: 20,
filters: [],
fields: [ 'satellites' ],
titleField: 'title',
descriptionField: 'description',
mediaField: 'image',
groupByField: 'type',
layout: {
badgeFields: [ 'satellites' ],
},
} );
const { data: shownData, paginationInfo } = useMemo( () => {
return filterSortAndPaginate( data, view, fields );
}, [ view ] );
Comment on lines +346 to +348
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if just the data changes? Don't we risk showing obsolete memoized data that way?

I know this is in a story, but folks tend to copy code from stories, so we need to be mindful about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, data and fields won't change because they are static arrays.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I was clear enough - my worry is about people copying the code from stories and it potentially leading to the issues I mentioned.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that viewpoint, but I'm reluctant to add unnecessary code to cover misuse — that's not something I've seen us doing.

return (
<DataViews
getItemId={ ( item ) => item.id.toString() }
paginationInfo={ paginationInfo }
data={ shownData }
view={ view }
fields={ fields }
onChangeView={ setView }
actions={ actions }
defaultLayouts={ {
[ LAYOUT_GRID ]: {},
} }
/>
);
};
156 changes: 125 additions & 31 deletions packages/dataviews/src/dataviews-layouts/grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,40 +293,134 @@ function ViewGrid< Item >( {
gridTemplateColumns: `repeat(${ usedPreviewSize }, minmax(0, 1fr))`,
}
: {};

// Group data by groupByField if specified
const groupedData = view.groupByField
? data.reduce( ( groups: { [ key: string ]: typeof data }, item ) => {
const groupField = fields.find(
( f ) => f.id === view.groupByField
);
const groupValue = groupField?.getValue
? groupField.getValue( { item } )
: ( item as any )[ view.groupByField! ];
const key = String( groupValue ?? __( 'No group' ) );
if ( ! groups[ key ] ) {
groups[ key ] = [];
}
groups[ key ].push( item );
return groups;
}, {} )
: null;

return (
<>
{ hasData && (
<Grid
gap={ 8 }
columns={ 2 }
alignment="top"
className={ clsx( 'dataviews-view-grid', className ) }
style={ gridStyle }
aria-busy={ isLoading }
>
{ data.map( ( item ) => {
return (
<GridItem
key={ getItemId( item ) }
view={ view }
selection={ selection }
onChangeSelection={ onChangeSelection }
onClickItem={ onClickItem }
isItemClickable={ isItemClickable }
renderItemLink={ renderItemLink }
getItemId={ getItemId }
item={ item }
actions={ actions }
mediaField={ mediaField }
titleField={ titleField }
descriptionField={ descriptionField }
regularFields={ regularFields }
badgeFields={ badgeFields }
hasBulkActions={ hasBulkActions }
/>
);
} ) }
</Grid>
<>
{ groupedData ? (
<VStack spacing={ 4 }>
{ Object.entries( groupedData ).map(
( [ groupName, groupItems ] ) => (
<VStack key={ groupName } spacing={ 2 }>
<h3 className="dataviews-view-grid__group-header">
{ groupName }
</h3>
<Grid
gap={ 8 }
columns={ 2 }
alignment="top"
className={ clsx(
'dataviews-view-grid',
className
) }
style={ gridStyle }
aria-busy={ isLoading }
>
{ groupItems.map( ( item ) => {
return (
<GridItem
key={ getItemId(
item
) }
view={ view }
selection={ selection }
onChangeSelection={
onChangeSelection
}
onClickItem={
onClickItem
}
isItemClickable={
isItemClickable
}
renderItemLink={
renderItemLink
}
getItemId={ getItemId }
item={ item }
actions={ actions }
mediaField={
mediaField
}
titleField={
titleField
}
descriptionField={
descriptionField
}
regularFields={
regularFields
}
badgeFields={
badgeFields
}
hasBulkActions={
hasBulkActions
}
/>
);
} ) }
</Grid>
</VStack>
)
) }
</VStack>
) : (
<Grid
gap={ 8 }
columns={ 2 }
alignment="top"
className={ clsx(
'dataviews-view-grid',
className
) }
style={ gridStyle }
aria-busy={ isLoading }
>
{ data.map( ( item ) => {
return (
<GridItem
key={ getItemId( item ) }
view={ view }
selection={ selection }
onChangeSelection={ onChangeSelection }
onClickItem={ onClickItem }
isItemClickable={ isItemClickable }
renderItemLink={ renderItemLink }
getItemId={ getItemId }
item={ item }
actions={ actions }
mediaField={ mediaField }
titleField={ titleField }
descriptionField={ descriptionField }
regularFields={ regularFields }
badgeFields={ badgeFields }
hasBulkActions={ hasBulkActions }
/>
);
} ) }
</Grid>
) }
</>
) }
{ ! hasData && (
<div
Expand Down
8 changes: 8 additions & 0 deletions packages/dataviews/src/dataviews-layouts/grid/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,11 @@
.dataviews-view-grid__media--clickable {
cursor: pointer;
}

.dataviews-view-grid__group-header {
font-size: 16px;
font-weight: 600;
color: $gray-900;
margin: 0 0 $grid-unit-10 0;
padding: 0 $grid-unit-60;
}
5 changes: 5 additions & 0 deletions packages/dataviews/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ interface ViewBase {
* Whether to show the hierarchical levels.
*/
showLevels?: boolean;

/**
* The field to group by.
*/
groupByField?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on a UI to configure this? (not necessary for this PR)

Also would be good to document this in the README.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done at d97929e

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on a UI to configure this? (not necessary for this PR)

The obvious way is to add a new select in the view config somewhere. Alternatively, we could explore making it part of the field list (similarly to what we have for the media field).

}

export interface ColumnStyle {
Expand Down