From 1966da03df6e660ecb907fa1f60a145e6690ad4f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Maneiro?=
<583546+oandregal@users.noreply.github.com>
Date: Mon, 8 Sep 2025 16:59:10 +0200
Subject: [PATCH 1/3] Implement groupBy for list layout
---
.../src/dataviews-layouts/list/index.tsx | 78 ++++++++++++++++++-
.../src/dataviews-layouts/list/style.scss | 8 ++
2 files changed, 85 insertions(+), 1 deletion(-)
diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx
index 94db0c120e28e7..e55f5abe4ad5b8 100644
--- a/packages/dataviews/src/dataviews-layouts/list/index.tsx
+++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx
@@ -24,7 +24,7 @@ import {
useState,
useContext,
} from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import { moreVertical } from '@wordpress/icons';
import { useRegistry } from '@wordpress/data';
@@ -517,6 +517,82 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) {
);
}
+ // Get the group field if specified
+ const groupField = view.groupByField
+ ? fields.find( ( field ) => field.id === view.groupByField )
+ : null;
+
+ // Group data by groupByField if specified
+ const dataByGroup = groupField
+ ? data.reduce( ( groups: Map< string, typeof data >, item ) => {
+ const groupName = groupField.getValue( { item } );
+ if ( ! groups.has( groupName ) ) {
+ groups.set( groupName, [] );
+ }
+ groups.get( groupName )?.push( item );
+ return groups;
+ }, new Map< string, typeof data >() )
+ : null;
+
+ // Render data grouped by field
+ if ( hasData && groupField && dataByGroup ) {
+ return (
+ }
+ className="dataviews-view-list__group"
+ role="grid"
+ activeId={ activeCompositeId }
+ setActiveId={ setActiveCompositeId }
+ >
+
+ { Array.from( dataByGroup.entries() ).map(
+ ( [ groupName, groupItems ] ) => (
+
+
+ { sprintf(
+ // translators: 1: The label of the field e.g. "Date". 2: The value of the field, e.g.: "May 2022".
+ __( '%1$s: %2$s' ),
+ groupField.label,
+ groupName
+ ) }
+
+ { groupItems.map( ( item ) => {
+ const id =
+ generateCompositeItemIdPrefix( item );
+ return (
+
+ );
+ } ) }
+
+ )
+ ) }
+
+
+ );
+ }
+
+ // Render ungrouped data
return (
<>
Date: Mon, 8 Sep 2025 17:07:13 +0200
Subject: [PATCH 2/3] Update changelog
---
packages/dataviews/CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md
index 0d3ad10ecc801d..b44bcc945de345 100644
--- a/packages/dataviews/CHANGELOG.md
+++ b/packages/dataviews/CHANGELOG.md
@@ -14,6 +14,7 @@
- Dataform: Add new `url` field type and field control. [#71518](https://github.com/WordPress/gutenberg/pull/71518)
- Dataform: Add new `password` field type and field control. [#71545](https://github.com/WordPress/gutenberg/pull/71545)
- DataForm: Add a textarea control for use with the `text` field type ([#71495](https://github.com/WordPress/gutenberg/pull/71495))
+- DataViews: support groupBy in the list layout. [#71548](https://github.com/WordPress/gutenberg/pull/71548)
### Bug Fixes
From 2a948eddb031850b04e5fcd6c7ef173cd9ba9f48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Maneiro?=
<583546+oandregal@users.noreply.github.com>
Date: Tue, 9 Sep 2025 18:06:00 +0200
Subject: [PATCH 3/3] Extract to getDataByGroup
---
.../src/dataviews-layouts/grid/index.tsx | 15 ++-------------
.../src/dataviews-layouts/list/index.tsx | 15 ++-------------
.../dataviews-layouts/picker-grid/index.tsx | 14 ++------------
.../src/dataviews-layouts/table/index.tsx | 15 ++-------------
.../utils/get-data-by-group.ts | 18 ++++++++++++++++++
5 files changed, 26 insertions(+), 51 deletions(-)
create mode 100644 packages/dataviews/src/dataviews-layouts/utils/get-data-by-group.ts
diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx
index 23f4265fd58c21..6970146e145dcb 100644
--- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx
+++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx
@@ -42,6 +42,7 @@ import type { SetSelection } from '../../private-types';
import { ItemClickWrapper } from '../utils/item-click-wrapper';
import { GridItems } from '../utils/grid-items';
const { Badge } = unlock( componentsPrivateApis );
+import getDataByGroup from '../utils/get-data-by-group';
interface GridItemProps< Item > {
view: ViewGridType;
@@ -338,19 +339,7 @@ function ViewGrid< Item >( {
const groupField = view.groupByField
? fields.find( ( f ) => f.id === view.groupByField )
: null;
-
- // Group data by groupByField if specified
- const dataByGroup = groupField
- ? data.reduce( ( groups: Map< string, typeof data >, item ) => {
- const groupName = groupField.getValue( { item } );
- if ( ! groups.has( groupName ) ) {
- groups.set( groupName, [] );
- }
- groups.get( groupName )?.push( item );
- return groups;
- }, new Map< string, typeof data >() )
- : null;
-
+ const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null;
const isInfiniteScroll = view.infiniteScrollEnabled && ! dataByGroup;
return (
diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx
index e55f5abe4ad5b8..6951cf5b6f1b59 100644
--- a/packages/dataviews/src/dataviews-layouts/list/index.tsx
+++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx
@@ -44,6 +44,7 @@ import type {
ViewListProps,
ActionModal as ActionModalType,
} from '../../types';
+import getDataByGroup from '../utils/get-data-by-group';
interface ListViewItemProps< Item > {
view: ViewListType;
@@ -517,22 +518,10 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) {
);
}
- // Get the group field if specified
const groupField = view.groupByField
? fields.find( ( field ) => field.id === view.groupByField )
: null;
-
- // Group data by groupByField if specified
- const dataByGroup = groupField
- ? data.reduce( ( groups: Map< string, typeof data >, item ) => {
- const groupName = groupField.getValue( { item } );
- if ( ! groups.has( groupName ) ) {
- groups.set( groupName, [] );
- }
- groups.get( groupName )?.push( item );
- return groups;
- }, new Map< string, typeof data >() )
- : null;
+ const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null;
// Render data grouped by field
if ( hasData && groupField && dataByGroup ) {
diff --git a/packages/dataviews/src/dataviews-layouts/picker-grid/index.tsx b/packages/dataviews/src/dataviews-layouts/picker-grid/index.tsx
index 1e645822ef7823..823a46c729fcb4 100644
--- a/packages/dataviews/src/dataviews-layouts/picker-grid/index.tsx
+++ b/packages/dataviews/src/dataviews-layouts/picker-grid/index.tsx
@@ -35,6 +35,7 @@ import type {
import type { SetSelection } from '../../private-types';
import { GridItems } from '../utils/grid-items';
const { Badge } = unlock( componentsPrivateApis );
+import getDataByGroup from '../utils/get-data-by-group';
interface GridItemProps< Item > {
view: ViewPickerGridType;
@@ -301,18 +302,7 @@ function ViewPickerGrid< Item >( {
const groupField = view.groupByField
? fields.find( ( f ) => f.id === view.groupByField )
: null;
-
- // Group data by groupByField if specified
- const dataByGroup = groupField
- ? data.reduce( ( groups: Map< string, typeof data >, item ) => {
- const groupName = groupField.getValue( { item } );
- if ( ! groups.has( groupName ) ) {
- groups.set( groupName, [] );
- }
- groups.get( groupName )?.push( item );
- return groups;
- }, new Map< string, typeof data >() )
- : null;
+ const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null;
const isInfiniteScroll = view.infiniteScrollEnabled && ! dataByGroup;
diff --git a/packages/dataviews/src/dataviews-layouts/table/index.tsx b/packages/dataviews/src/dataviews-layouts/table/index.tsx
index 0497737eacf4e8..1e8b7ea9953541 100644
--- a/packages/dataviews/src/dataviews-layouts/table/index.tsx
+++ b/packages/dataviews/src/dataviews-layouts/table/index.tsx
@@ -40,6 +40,7 @@ import type { SetSelection } from '../../private-types';
import ColumnHeaderMenu from './column-header-menu';
import ColumnPrimary from './column-primary';
import { useIsHorizontalScrollEnd } from './use-is-horizontal-scroll-end';
+import getDataByGroup from '../utils/get-data-by-group';
interface TableColumnFieldProps< Item > {
fields: NormalizedField< Item >[];
@@ -336,22 +337,10 @@ function ViewTable< Item >( {
( field ) => field.id === view.descriptionField
);
- // Get group field if groupByField is specified
const groupField = view.groupByField
? fields.find( ( f ) => f.id === view.groupByField )
: null;
-
- // Group data by groupByField if specified
- const dataByGroup = groupField
- ? data.reduce( ( groups: Map< string, typeof data >, item ) => {
- const groupName = groupField.getValue( { item } );
- if ( ! groups.has( groupName ) ) {
- groups.set( groupName, [] );
- }
- groups.get( groupName )?.push( item );
- return groups;
- }, new Map< string, typeof data >() )
- : null;
+ const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null;
const { showTitle = true, showMedia = true, showDescription = true } = view;
const hasPrimaryColumn =
( titleField && showTitle ) ||
diff --git a/packages/dataviews/src/dataviews-layouts/utils/get-data-by-group.ts b/packages/dataviews/src/dataviews-layouts/utils/get-data-by-group.ts
new file mode 100644
index 00000000000000..22c5bf741c261a
--- /dev/null
+++ b/packages/dataviews/src/dataviews-layouts/utils/get-data-by-group.ts
@@ -0,0 +1,18 @@
+/**
+ * Internal dependencies
+ */
+import type { NormalizedField } from '../../types';
+
+export default function getDataByGroup< Item >(
+ data: any[],
+ groupByField: NormalizedField< Item >
+): Map< string, any[] > {
+ return data.reduce( ( groups: Map< string, typeof data >, item ) => {
+ const groupName = groupByField.getValue( { item } );
+ if ( ! groups.has( groupName ) ) {
+ groups.set( groupName, [] );
+ }
+ groups.get( groupName )?.push( item );
+ return groups;
+ }, new Map< string, typeof data >() );
+}