diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss
index e6ecaea017..7f862fb259 100644
--- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss
+++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss
@@ -420,7 +420,8 @@ $root: ".widget-datagrid";
align-items: center;
}
- &-exporting {
+ &-exporting,
+ &-selecting-all-pages {
.widget-datagrid-top-bar,
.widget-datagrid-header,
.widget-datagrid-content,
@@ -555,16 +556,32 @@ $root: ".widget-datagrid";
:where(#{$root}-pb-start) {
margin-block: var(--spacing-medium);
padding-inline: var(--spacing-medium);
+ display: flex;
+ align-items: center;
}
-#{$root}-clear-selection {
+#{$root}-btn-invisible {
cursor: pointer;
background: transparent;
border: none;
- text-decoration: underline;
color: var(--link-color);
- padding: 0;
+ padding: 0.3em;
+ border-radius: 6px;
display: inline-block;
+
+ &:hover,
+ &:focus-visible {
+ background-color: #e6e7f2;
+ }
+}
+
+:where(#{$root}-select-all-bar) {
+ grid-column: 1 / -1;
+ background-color: #f0f1f2;
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ padding: var(--spacing-smaller, 8px) var(--spacing-medium, 16px);
}
@keyframes skeleton-loading {
diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md
index 37c9cb6828..6f731c5e52 100644
--- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md
+++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]
+### Added
+
+- We added multi-page select all functionality for Datagrid widget with configurable batch processing, progress tracking, and page restoration to allow users to select all items across multiple pages with a single click.
+
## [3.6.0] - 2025-10-01
### Fixed
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts
index 2139f6870d..8db692871e 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts
@@ -3,8 +3,7 @@ import {
hideNestedPropertiesIn,
hidePropertiesIn,
hidePropertyIn,
- Properties,
- transformGroupsIntoTabs
+ Properties
} from "@mendix/pluggable-widgets-tools";
import {
container,
@@ -22,7 +21,7 @@ import { ColumnsPreviewType, DatagridPreviewProps } from "../typings/DatagridPro
export function getProperties(
values: DatagridPreviewProps,
defaultProperties: Properties,
- platform: "web" | "desktop"
+ _: "web" | "desktop"
): Properties {
values.columns.forEach((column, index) => {
if (column.showContentAs !== "attribute" && !column.sortable && !values.columnsFilterable) {
@@ -65,15 +64,6 @@ export function getProperties(
if (column.minWidth !== "manual") {
hidePropertyIn(defaultProperties, values, "columns", index, "minWidthLimit");
}
- if (!values.advanced && platform === "web") {
- hideNestedPropertiesIn(defaultProperties, values, "columns", index, [
- "columnClass",
- "sortable",
- "resizable",
- "draggable",
- "hidable"
- ]);
- }
});
if (values.pagination === "buttons") {
hidePropertyIn(defaultProperties, values, "showNumberOfRows");
@@ -124,28 +114,6 @@ export function getProperties(
"columns"
);
- if (platform === "web") {
- if (!values.advanced) {
- hidePropertiesIn(defaultProperties, values, [
- "pagination",
- "pagingPosition",
- "showEmptyPlaceholder",
- "rowClass",
- "columnsSortable",
- "columnsDraggable",
- "columnsResizable",
- "columnsHidable",
- "configurationAttribute",
- "onConfigurationChange",
- "filterSectionTitle"
- ]);
- }
-
- transformGroupsIntoTabs(defaultProperties);
- } else {
- hidePropertyIn(defaultProperties, values, "advanced");
- }
-
if (values.configurationStorageType === "localStorage") {
hidePropertiesIn(defaultProperties, values, ["configurationAttribute", "onConfigurationChange"]);
}
@@ -154,7 +122,7 @@ export function getProperties(
}
function hideSelectionProperties(defaultProperties: Properties, values: DatagridPreviewProps): void {
- const { itemSelection, itemSelectionMethod } = values;
+ const { itemSelection, itemSelectionMethod, selectAllPagesEnabled } = values;
if (itemSelection === "None") {
hidePropertiesIn(defaultProperties, values, ["itemSelectionMethod", "itemSelectionMode", "onSelectionChange"]);
@@ -170,6 +138,13 @@ function hideSelectionProperties(defaultProperties: Properties, values: Datagrid
if (itemSelection !== "Multi") {
hidePropertyIn(defaultProperties, values, "keepSelection");
+ hidePropertyIn(defaultProperties, values, "selectAllPagesEnabled");
+ }
+
+ if (!selectAllPagesEnabled) {
+ hidePropertyIn(defaultProperties, values, "selectAllPagesPageSize");
+ hidePropertyIn(defaultProperties, values, "selectingAllLabel");
+ hidePropertyIn(defaultProperties, values, "cancelSelectionLabel");
}
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
index 65b72439ee..c4ac43340d 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
@@ -5,6 +5,11 @@ import { enableStaticRendering } from "mobx-react-lite";
enableStaticRendering(true);
import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController";
+import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController";
+import { SelectAllController } from "@mendix/widget-plugin-grid/selection";
+import { ProgressStore } from "@mendix/widget-plugin-grid/stores/ProgressStore";
+import { SelectionCountStore } from "@mendix/widget-plugin-grid/stores/SelectionCountStore";
+import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost";
import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider";
import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style";
@@ -15,11 +20,11 @@ import { ColumnsPreviewType, DatagridPreviewProps } from "typings/DatagridProps"
import { Cell } from "./components/Cell";
import { Widget } from "./components/Widget";
import { ColumnPreview } from "./helpers/ColumnPreview";
-import { DatagridContext } from "./helpers/root-context";
+import { DatagridContext, DatagridRootScope } from "./helpers/root-context";
import { useSelectActionHelper } from "./helpers/SelectActionHelper";
import { GridBasicData } from "./helpers/state/GridBasicData";
-
-import { SelectionCountStore } from "@mendix/widget-plugin-grid/selection/stores/SelectionCountStore";
+import { SelectAllBarViewModel } from "./helpers/state/SelectAllBarViewModel";
+import { SelectionProgressDialogViewModel } from "./helpers/state/SelectionProgressDialogViewModel";
import "./ui/DatagridPreview.scss";
// Fix type definition for Selectable
@@ -61,6 +66,8 @@ const initColumns: ColumnsPreviewType[] = [
const numberOfItems = 3;
+class Host extends BaseControllerHost {}
+
export function preview(props: DatagridPreviewProps): ReactElement {
const EmptyPlaceholder = props.emptyPlaceholder.renderer;
const data: ObjectItem[] = Array.from({ length: numberOfItems }).map((_, index) => ({
@@ -87,9 +94,13 @@ export function preview(props: DatagridPreviewProps): ReactElement {
const eventsController = { getProps: () => Object.create({}) };
const ctx = useConst(() => {
- const gateProvider = new GateProvider({});
- const basicData = new GridBasicData(gateProvider.gate);
- const selectionCountStore = new SelectionCountStore(gateProvider.gate);
+ const host = new Host();
+ const gateProvider = new GateProvider({ datasource: {} as any, itemSelection: undefined });
+ const basicData = new GridBasicData(gateProvider.gate as any);
+ const query = new DatasourceController(host, { gate: gateProvider.gate });
+ const selectionCountStore = new SelectionCountStore(gateProvider.gate as any);
+ const selectAllController = new SelectAllController(host, { gate: gateProvider.gate, pageSize: 2, query });
+ const selectAllProgressStore = new ProgressStore();
return {
basicData,
selectionHelper: undefined,
@@ -97,8 +108,20 @@ export function preview(props: DatagridPreviewProps): ReactElement {
cellEventsController: eventsController,
checkboxEventsController: eventsController,
focusController,
- selectionCountStore
- };
+ selectionCountStore,
+ selectAllProgressStore,
+ selectAllBarViewModel: new SelectAllBarViewModel(
+ host,
+ gateProvider.gate as any,
+ selectAllController,
+ selectionCountStore
+ ),
+ selectionProgressDialogViewModel: new SelectionProgressDialogViewModel(
+ gateProvider.gate as any,
+ selectAllProgressStore,
+ selectAllController
+ )
+ } satisfies DatagridRootScope;
});
return (
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
index 9ff9f6bb04..5f623e3ca5 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
@@ -9,35 +9,31 @@ import { DatagridContainerProps } from "../typings/DatagridProps";
import { Cell } from "./components/Cell";
import { Widget } from "./components/Widget";
import { WidgetHeaderContext } from "./components/WidgetHeaderContext";
-import { ProgressStore } from "./features/data-export/ProgressStore";
import { useDataExport } from "./features/data-export/useDataExport";
import { useCellEventsController } from "./features/row-interaction/CellEventsController";
import { useCheckboxEventsController } from "./features/row-interaction/CheckboxEventsController";
-import { DatagridContext } from "./helpers/root-context";
+import { DatagridContext, DatagridRootScope } from "./helpers/root-context";
import { useSelectActionHelper } from "./helpers/SelectActionHelper";
-import { IColumnGroupStore } from "./helpers/state/ColumnGroupStore";
import { RootGridStore } from "./helpers/state/RootGridStore";
import { useRootStore } from "./helpers/state/useRootStore";
import { useDataGridJSActions } from "./helpers/useDataGridJSActions";
interface Props extends DatagridContainerProps {
- columnsStore: IColumnGroupStore;
rootStore: RootGridStore;
- progressStore: ProgressStore;
}
const Container = observer((props: Props): ReactElement => {
- const { columnsStore, rootStore } = props;
- const { paginationCtrl } = rootStore;
+ const { rootStore } = props;
+ const { paginationCtrl, gate, query, columnsStore, exportProgressStore } = rootStore;
- const items = props.datasource.items ?? [];
+ const items = query.items ?? [];
- const [exportProgress, abortExport] = useDataExport(props, props.columnsStore, props.progressStore);
+ const [exportProgress, abortExport] = useDataExport(props, columnsStore, exportProgressStore);
const selectionHelper = useSelectionHelper(
- props.itemSelection,
- props.datasource,
- props.onSelectionChange,
+ gate.props.itemSelection,
+ gate.props.datasource,
+ gate.props.onSelectionChange,
props.keepSelection ? "always keep" : "always clear"
);
@@ -65,16 +61,20 @@ const Container = observer((props: Props): ReactElement => {
const checkboxEventsController = useCheckboxEventsController(selectActionHelper, focusController);
const ctx = useConst(() => {
- rootStore.basicData.setSelectionHelper(selectionHelper);
- return {
+ const scope: DatagridRootScope = {
basicData: rootStore.basicData,
selectionHelper,
selectActionHelper,
cellEventsController,
checkboxEventsController,
focusController,
- selectionCountStore: rootStore.selectionCountStore
+ selectionCountStore: rootStore.selectionCountStore,
+ selectAllProgressStore: rootStore.selectAllProgressStore,
+ selectAllBarViewModel: rootStore.selectAllBarViewModel,
+ selectionProgressDialogViewModel: rootStore.selectionProgressDialogViewModel
};
+
+ return scope;
});
return (
@@ -123,7 +123,7 @@ const Container = observer((props: Props): ReactElement => {
rowClass={useCallback((value: any) => props.rowClass?.get(value)?.value ?? "", [props.rowClass])}
setPage={paginationCtrl.setPage}
styles={props.style}
- exporting={exportProgress.exporting}
+ exporting={exportProgress.inProgress}
processedRows={exportProgress.loaded}
visibleColumns={columnsStore.visibleColumns}
availableColumns={columnsStore.availableColumns}
@@ -146,14 +146,5 @@ const Container = observer((props: Props): ReactElement => {
Container.displayName = "DatagridComponent";
export default function Datagrid(props: DatagridContainerProps): ReactElement | null {
- const rootStore = useRootStore(props);
-
- return (
-
- );
+ return ;
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
index c19285b1cd..a2921010de 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
@@ -8,10 +8,6 @@
-
- Enable advanced options
-
-
Data source
@@ -20,51 +16,6 @@
Refresh time (in seconds)
-
- Selection
-
-
-
-
-
-
-
-
- Selection method
-
-
- Checkbox
- Row click
-
-
-
- Toggle on click
- Defines item selection behavior.
-
- Yes
- No
-
-
-
- Show (un)check all toggle
- Show a checkbox in the grid header to check or uncheck multiple items.
-
-
- Keep selection
- If enabled, selected items will stay selected unless cleared by the user or a Nanoflow.
-
-
- Loading type
-
-
- Spinner
- Skeleton
-
-
-
- Show refresh indicator
- Show a refresh indicator when the data is being loaded.
-
@@ -209,7 +160,88 @@
-
+
+
+ On click trigger
+
+
+ Single click
+ Double click
+
+
+
+ On click action
+
+
+
+ On selection change
+
+
+
+ Filters placeholder
+
+
+
+
+
+
+
+ Selection
+
+
+
+
+
+
+
+
+ Selection method
+
+
+ Checkbox
+ Row click
+
+
+
+ Toggle on click
+ Defines item selection behavior.
+
+ Yes
+ No
+
+
+
+ Show (un)check all toggle
+ Show a checkbox in the grid header to check or uncheck multiple items.
+
+
+ Keep selection
+ If enabled, selected items will stay selected unless cleared by the user or a Nanoflow.
+
+
+ Enable select all
+ Allow select all through multiple pages (based on current filter).
+
+
+ Select all page size
+ When selecting items from a large data source, items are selected in batches. This setting controls the size of the batches.
+
+
+
+
+ Loading type
+
+
+ Spinner
+ Skeleton
+
+
+
+ Show refresh indicator
+ Show a refresh indicator when the data is being loaded.
+
+
+
Page size
@@ -251,6 +283,8 @@
Load More
+
+
Empty list message
@@ -269,28 +303,6 @@
-
-
- On click trigger
-
-
- Single click
- Double click
-
-
-
- On click action
-
-
-
- On selection change
-
-
-
- Filters placeholder
-
-
-
@@ -337,7 +349,7 @@
-
+
Filter section
@@ -358,19 +370,35 @@
- Select row
+ Select row label
If selection is enabled, assistive technology will read this upon reaching a checkbox.
Select row
- Select all row
+ Select all label
If selection is enabled, assistive technology will read this upon reaching 'Select all' checkbox.
Select all rows
+
+ Selecting all label
+ ARIA label for the progress dialog when selecting all items
+
+ Selecting all items...
+
+
+
+ Cancel selection label
+ ARIA label for the cancel button in the selection progress dialog
+
+ Cancel selection
+
+
+
+
Row count singular
Must include '%d' to denote number position ('%d row selected')
@@ -379,6 +407,27 @@
Row count plural
Must include '%d' to denote number position ('%d rows selected')
+
+ Select all
+ This caption used when total count is available.
+
+ Select all %d rows in the data source
+
+
+
+ 2
+
+
+ Select remaining rows in the data source
+
+
+
+ Clear selection caption
+
+
+ Clear selection
+
+
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx
index 34ab1c9e4d..e6d0efe69f 100644
--- a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx
@@ -1,37 +1,40 @@
import { ThreeStateCheckBox } from "@mendix/widget-plugin-component-kit/ThreeStateCheckBox";
-import { createElement, Fragment, ReactElement, useCallback } from "react";
+import { SelectionStatus } from "@mendix/widget-plugin-grid/selection";
+import { createElement, Fragment, ReactElement } from "react";
import { useDatagridRootScope } from "../helpers/root-context";
export function CheckboxColumnHeader(): ReactElement {
- const { selectActionHelper, basicData } = useDatagridRootScope();
+ const { selectActionHelper, basicData, selectionHelper } = useDatagridRootScope();
const { showCheckboxColumn, showSelectAllToggle, onSelectAll } = selectActionHelper;
- const { selectionStatus, selectAllRowsLabel } = basicData;
-
- const onChange = useCallback(() => onSelectAll(), [onSelectAll]);
+ const { selectAllRowsLabel } = basicData;
if (showCheckboxColumn === false) {
return ;
}
- let checkbox = null;
-
- if (showSelectAllToggle) {
- if (selectionStatus === "unknown") {
- throw new Error("Don't know how to render checkbox with selectionStatus=unknown");
- }
-
- checkbox = (
-
- );
- }
-
return (
- {checkbox}
+ {showSelectAllToggle && (
+
+ )}
);
}
+
+function Checkbox(props: { status: SelectionStatus; onChange: () => void; "aria-label"?: string }): React.ReactNode {
+ if (props.status === "unknown") {
+ console.error("Data grid 2: don't know how to render column checkbox with selectionStatus=unknown");
+ return null;
+ }
+ return (
+
+ );
+}
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/SelectAllBar.tsx b/packages/pluggableWidgets/datagrid-web/src/components/SelectAllBar.tsx
new file mode 100644
index 0000000000..c0adc2a8e6
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-web/src/components/SelectAllBar.tsx
@@ -0,0 +1,26 @@
+import { If } from "@mendix/widget-plugin-component-kit/If";
+import { observer } from "mobx-react-lite";
+import { createElement } from "react";
+import { useDatagridRootScope } from "../helpers/root-context";
+
+export const SelectAllBar = observer(function SelectAllBar(): React.ReactNode {
+ const { selectAllBarViewModel: vm } = useDatagridRootScope();
+
+ if (!vm.barVisible) return null;
+
+ return (
+
+ {vm.selectionCountText}
+
+
+
+
+
+
+
+ );
+});
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/SelectionProgressDialog.tsx b/packages/pluggableWidgets/datagrid-web/src/components/SelectionProgressDialog.tsx
new file mode 100644
index 0000000000..5acad87b09
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-web/src/components/SelectionProgressDialog.tsx
@@ -0,0 +1,21 @@
+import { createElement, ReactElement } from "react";
+import { useDatagridRootScope } from "../helpers/root-context";
+import { ExportAlert } from "./ExportAlert";
+import { PseudoModal } from "./PseudoModal";
+
+export function SelectionProgressDialog(): ReactElement | null {
+ const { selectionProgressDialogViewModel: vm } = useDatagridRootScope();
+ if (!vm.open) return null;
+ return (
+
+ vm.onCancel()}
+ progress={vm.progress}
+ total={vm.total}
+ />
+
+ );
+}
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx
index dfb7831e90..533bafefec 100644
--- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx
@@ -20,6 +20,8 @@ import { Grid } from "./Grid";
import { GridBody } from "./GridBody";
import { GridHeader } from "./GridHeader";
import { RowsRenderer } from "./RowsRenderer";
+import { SelectAllBar } from "./SelectAllBar";
+import { SelectionProgressDialog } from "./SelectionProgressDialog";
import { WidgetContent } from "./WidgetContent";
import { WidgetFooter } from "./WidgetFooter";
import { WidgetHeader } from "./WidgetHeader";
@@ -80,7 +82,7 @@ export interface WidgetProps(props: WidgetProps): ReactElement => {
const { className, exporting, numberOfItems, onExportCancel, selectActionHelper } = props;
- const { basicData } = useDatagridRootScope();
+ const { basicData, selectAllProgressStore } = useDatagridRootScope();
const selectionEnabled = selectActionHelper.selectionType !== "None";
@@ -91,8 +93,10 @@ export const Widget = observer((props: WidgetProps): Re
selection={selectionEnabled}
style={{}}
exporting={exporting}
+ selectingAllPages={selectAllProgressStore.inProgress}
>
+
{exporting && (
(props: WidgetProps): ReactElemen
const showHeader = !!headerContent;
const showTopBar = paging && (pagingPosition === "top" || pagingPosition === "both");
+ const isSelectionEnabled = selectActionHelper.selectionType !== "None";
+ const isSelectionMulti = isSelectionEnabled ? selectActionHelper.selectionType === "Multi" : undefined;
+ const isSelectAllBarEnabled = isSelectionMulti;
const pagination = paging ? (
(props: WidgetProps): ReactElemen
visibilitySelectorColumn: columnsHidable
});
- const selectionEnabled = selectActionHelper.selectionType !== "None";
-
return (
{showTopBar && {pagination}}
{showHeader && {headerContent}}
-
+
(props: WidgetProps): ReactElemen
isLoading={props.columnsLoading}
preview={props.preview}
/>
+ {isSelectAllBarEnabled && }
{showRefreshIndicator ? : null}
- {selectionCountStore.displayCount} |
-