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..6d9f439eca 100644
--- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss
+++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss
@@ -433,6 +433,30 @@ $root: ".widget-datagrid";
}
}
+ // Better positioning for multi-page selection modal
+ // &-selecting-all-pages {
+ // .widget-datagrid-modal {
+ // &-overlay {
+ // position: fixed;
+ // top: 0;
+ // right: 0;
+ // bottom: 0;
+ // left: 0;
+ // }
+
+ // &-main {
+ // position: fixed;
+ // top: 0;
+ // left: 0;
+ // right: 0;
+ // bottom: 0;
+ // display: flex;
+ // align-items: center;
+ // justify-content: center;
+ // }
+ // }
+ // }
+
&-col-select input:focus-visible {
outline-offset: 0;
}
@@ -566,6 +590,9 @@ $root: ".widget-datagrid";
padding: 0;
display: inline-block;
}
+:where(#{$root}-select-all-bar) {
+ grid-column: 1 / -1;
+}
@keyframes skeleton-loading {
0% {
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..849cfa2805 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts
@@ -154,7 +154,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 +170,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..25a8319d6a 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";
@@ -18,8 +23,6 @@ import { ColumnPreview } from "./helpers/ColumnPreview";
import { DatagridContext } 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 "./ui/DatagridPreview.scss";
// Fix type definition for Selectable
@@ -61,6 +64,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 +92,12 @@ 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 });
return {
basicData,
selectionHelper: undefined,
@@ -97,7 +105,10 @@ export function preview(props: DatagridPreviewProps): ReactElement {
cellEventsController: eventsController,
checkboxEventsController: eventsController,
focusController,
- selectionCountStore
+ selectionCountStore,
+ selectAllProgressStore: new ProgressStore(),
+ selectAllController,
+ rootStore: {} as any // Mock for preview
};
});
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
index 9ff9f6bb04..43bdfcc4e5 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"
);
@@ -66,15 +62,19 @@ const Container = observer((props: Props): ReactElement => {
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,
+ selectAllController: rootStore.selectAllController,
+ selectAllProgressStore: rootStore.selectAllProgressStore
};
+
+ 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..19fbbe7254 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
@@ -53,6 +53,28 @@
Keep selection
If enabled, selected items will stay selected unless cleared by the user or a Nanoflow.
+
+ Enable select all pages
+ Allow select all through multiple pages (based on current filter). Only works if total count is known.
+
+
+ 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.
+
+
+ Selecting all label
+ Label shown in the progress dialog when selecting all items
+
+ Selecting all items...
+
+
+
+ Cancel selection label
+ Label for the cancel button in the selection progress dialog
+
+ Cancel selection
+
+
Loading type
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx
index 34ab1c9e4d..21d211a717 100644
--- a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx
@@ -1,5 +1,6 @@
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 {
@@ -7,31 +8,29 @@ export function CheckboxColumnHeader(): ReactElement {
const { showCheckboxColumn, showSelectAllToggle, onSelectAll } = selectActionHelper;
const { selectionStatus, selectAllRowsLabel } = basicData;
- const onChange = useCallback(() => onSelectAll(), [onSelectAll]);
-
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..bb20d7b534
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-web/src/components/SelectAllBar.tsx
@@ -0,0 +1,20 @@
+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 {
+ selectAllController,
+ basicData: { selectionStatus }
+ } = useDatagridRootScope();
+
+ if (selectionStatus === "unknown") return null;
+
+ if (selectionStatus === "none") return null;
+
+ return (
+
+
+
+ );
+});
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..6babcc9b7b
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-web/src/components/SelectionProgressDialog.tsx
@@ -0,0 +1,35 @@
+import { createElement, ReactElement } from "react";
+import { PseudoModal } from "./PseudoModal";
+import { ExportAlert } from "./ExportAlert";
+
+export type SelectionProgressDialogProps = {
+ open: boolean;
+ selectingLabel: string;
+ cancelLabel: string;
+ onCancel: () => void;
+ progress: number;
+ total: number;
+};
+
+export function SelectionProgressDialog({
+ open,
+ selectingLabel,
+ cancelLabel,
+ onCancel,
+ progress,
+ total
+}: SelectionProgressDialogProps): ReactElement | null {
+ if (!open) return null;
+ return (
+
+
+
+ );
+}
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx
index dfb7831e90..15cd9d1351 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, selectAllController } = useDatagridRootScope();
const selectionEnabled = selectActionHelper.selectionType !== "None";
@@ -91,8 +93,17 @@ export const Widget = observer((props: WidgetProps): Re
selection={selectionEnabled}
style={{}}
exporting={exporting}
+ selectingAllPages={selectAllProgressStore.inProgress}
>
+ selectAllController.abort()}
+ progress={selectAllProgressStore.loaded}
+ total={selectAllProgressStore.total}
+ />
{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}
(null);
- const { className, selectionMethod, selection, exporting, children, ...rest } = props;
+ const { className, selectionMethod, selection, exporting, selectingAllPages, children, ...rest } = props;
const style = useMemo(() => {
const s = { ...props.style };
- if (exporting && ref.current) {
+ if ((exporting || selectingAllPages) && ref.current) {
s.height = ref.current.offsetHeight;
}
return s;
- }, [props.style, exporting]);
+ }, [props.style, exporting, selectingAllPages]);
return (
({
@@ -59,8 +70,11 @@ function withCtx(
selectActionHelper: widgetProps.selectActionHelper,
cellEventsController: widgetProps.cellEventsController,
checkboxEventsController: widgetProps.checkboxEventsController,
+ multiPageSelectionController: {} as unknown as MultiPageSelectionController,
focusController: widgetProps.focusController,
selectionCountStore: defaultSelectionCountStore as unknown as SelectionCountStore,
+ selectAllProgressStore: {} as unknown as SelectAllProgressStore,
+ rootStore: {} as unknown as RootGridStore,
...contextOverrides
};
@@ -209,7 +223,15 @@ describe("Table", () => {
beforeEach(() => {
props = mockWidgetProps();
- props.selectActionHelper = new SelectActionHelper("Single", undefined, "checkbox", false, 5, "clear");
+ props.selectActionHelper = new SelectActionHelper(
+ "Single",
+ undefined,
+ "checkbox",
+ false,
+ 5,
+ "clear",
+ new ListValueBuilder().build()
+ );
props.paging = true;
props.data = objectItems(3);
});
@@ -308,7 +330,15 @@ describe("Table", () => {
const props = mockWidgetProps();
props.data = objectItems(5);
props.paging = true;
- props.selectActionHelper = new SelectActionHelper("Multi", undefined, "checkbox", false, 5, "clear");
+ props.selectActionHelper = new SelectActionHelper(
+ "Multi",
+ undefined,
+ "checkbox",
+ false,
+ 5,
+ "clear",
+ new ListValueBuilder().build()
+ );
renderWithRootContext(props);
const colheader = screen.getAllByRole("columnheader")[0];
@@ -320,7 +350,15 @@ describe("Table", () => {
const props = mockWidgetProps();
props.data = objectItems(5);
props.paging = true;
- props.selectActionHelper = new SelectActionHelper("Multi", undefined, "checkbox", true, 5, "clear");
+ props.selectActionHelper = new SelectActionHelper(
+ "Multi",
+ undefined,
+ "checkbox",
+ true,
+ 5,
+ "clear",
+ new ListValueBuilder().build()
+ );
const renderWithStatus = (status: MultiSelectionStatus): ReturnType
=> {
return renderWithRootContext(props, {
@@ -342,7 +380,15 @@ describe("Table", () => {
it("not render header checkbox if method is rowClick", () => {
const props = mockWidgetProps();
- props.selectActionHelper = new SelectActionHelper("Multi", undefined, "rowClick", false, 5, "clear");
+ props.selectActionHelper = new SelectActionHelper(
+ "Multi",
+ undefined,
+ "rowClick",
+ false,
+ 5,
+ "clear",
+ new ListValueBuilder().build()
+ );
renderWithRootContext(props);
@@ -352,7 +398,15 @@ describe("Table", () => {
it("call onSelectAll when header checkbox is clicked", async () => {
const props = mockWidgetProps();
- props.selectActionHelper = new SelectActionHelper("Multi", undefined, "checkbox", true, 5, "clear");
+ props.selectActionHelper = new SelectActionHelper(
+ "Multi",
+ undefined,
+ "checkbox",
+ true,
+ 5,
+ "clear",
+ new ListValueBuilder().build()
+ );
props.selectActionHelper.onSelectAll = jest.fn();
renderWithRootContext(props, {
@@ -374,7 +428,15 @@ describe("Table", () => {
beforeEach(() => {
props = mockWidgetProps();
- props.selectActionHelper = new SelectActionHelper("Single", undefined, "rowClick", true, 5, "clear");
+ props.selectActionHelper = new SelectActionHelper(
+ "Single",
+ undefined,
+ "rowClick",
+ true,
+ 5,
+ "clear",
+ new ListValueBuilder().build()
+ );
props.paging = true;
props.data = objectItems(3);
});
@@ -480,7 +542,10 @@ describe("Table", () => {
itemSelectionMethod: selectionMethod,
itemSelectionMode: "clear",
showSelectAllToggle: false,
- pageSize: 5
+ pageSize: 5,
+ datasource: ds,
+ selectAllPagesEnabled: false,
+ selectAllPagesBufferSize: 500
},
helper
);
@@ -502,7 +567,9 @@ describe("Table", () => {
cellEventsController,
checkboxEventsController,
focusController: props.focusController,
- selectionCountStore: {} as unknown as SelectionCountStore
+ selectionCountStore: {} as unknown as SelectionCountStore,
+ selectAllProgressStore: {} as unknown as SelectAllProgressStore,
+ rootStore: {} as unknown as RootGridStore
};
return (
diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts
index 02492e1f31..c32a876f52 100644
--- a/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts
@@ -3,7 +3,7 @@ import { computed, makeObservable } from "mobx";
type DerivedLoaderControllerSpec = {
showSilentRefresh: boolean;
refreshIndicator: boolean;
- exp: { exporting: boolean };
+ exp: { inProgress: boolean };
cols: { loaded: boolean };
query: {
isFetchingNextBatch: boolean;
@@ -24,14 +24,9 @@ export class DerivedLoaderController {
get isFirstLoad(): boolean {
const { cols, exp, query } = this.spec;
- if (!cols.loaded) {
- return true;
- }
-
- if (exp.exporting) {
- return false;
- }
+ if (!cols.loaded) return true;
+ if (exp.inProgress) return false;
return query.isFirstLoad;
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/features/data-export/DSExportRequest.ts b/packages/pluggableWidgets/datagrid-web/src/features/data-export/DSExportRequest.ts
index 7898b5a76e..82fe8622e6 100644
--- a/packages/pluggableWidgets/datagrid-web/src/features/data-export/DSExportRequest.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/features/data-export/DSExportRequest.ts
@@ -114,6 +114,7 @@ export class DSExportRequest {
}
send = (): Promise => {
+ performance.mark("DSExportRequest_send");
this.emitLoadStart();
this._status = "awaiting";
this.offset = 0;
@@ -230,6 +231,9 @@ export class DSExportRequest {
this.emitEnd();
this.emitLoadEnd();
this.dispose();
+ performance.mark("DSExportRequest_end");
+ const measure = performance.measure("DSExportRequest", "DSExportRequest_send", "DSExportRequest_end");
+ console.debug(`DSExportRequest: export took ${(measure.duration / 1000).toFixed(2)} seconds`);
}
private dispose(): void {
diff --git a/packages/pluggableWidgets/datagrid-web/src/features/data-export/ProgressStore.ts b/packages/pluggableWidgets/datagrid-web/src/features/data-export/ProgressStore.ts
index 18980bd0c8..fa0327b6c6 100644
--- a/packages/pluggableWidgets/datagrid-web/src/features/data-export/ProgressStore.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/features/data-export/ProgressStore.ts
@@ -1,7 +1,7 @@
import { makeAutoObservable } from "mobx";
export class ProgressStore {
- exporting = false;
+ inProgress = false;
lengthComputable = false;
loaded = 0;
total = 0;
@@ -10,7 +10,7 @@ export class ProgressStore {
}
onloadstart = (event: ProgressEvent): void => {
- this.exporting = true;
+ this.inProgress = true;
this.lengthComputable = event.lengthComputable;
this.total = event.total;
this.loaded = 0;
@@ -21,7 +21,7 @@ export class ProgressStore {
};
onloadend = (): void => {
- this.exporting = false;
+ this.inProgress = false;
this.lengthComputable = false;
this.loaded = 0;
this.total = 0;
diff --git a/packages/pluggableWidgets/datagrid-web/src/features/data-export/useDataExport.ts b/packages/pluggableWidgets/datagrid-web/src/features/data-export/useDataExport.ts
index 8f853e9611..6733b2d7b5 100644
--- a/packages/pluggableWidgets/datagrid-web/src/features/data-export/useDataExport.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/features/data-export/useDataExport.ts
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useState } from "react";
+import { DatagridContainerProps } from "../../../typings/DatagridProps";
+import { IColumnGroupStore } from "../../helpers/state/ColumnGroupStore";
import { ExportController } from "./ExportController";
import { ProgressStore } from "./ProgressStore";
import { getExportRegistry } from "./registry";
-import { DatagridContainerProps } from "../../../typings/DatagridProps";
-import { IColumnGroupStore } from "../../helpers/state/ColumnGroupStore";
type ResourceEntry = {
key: string;
@@ -11,7 +11,7 @@ type ResourceEntry = {
};
export function useDataExport(
- props: DatagridContainerProps,
+ props: Pick,
columnsStore: IColumnGroupStore,
progress: ProgressStore
): [store: ProgressStore, abort: () => void] {
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/SelectActionHelper.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/SelectActionHelper.ts
index 9b4b28a056..aa8a854c03 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/SelectActionHelper.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/SelectActionHelper.ts
@@ -1,10 +1,11 @@
-import { useMemo } from "react";
import {
SelectActionHandler,
SelectionHelper,
SelectionMode,
WidgetSelectionProperty
} from "@mendix/widget-plugin-grid/selection";
+import { ListValue } from "mendix";
+import { useMemo } from "react";
import { DatagridContainerProps, DatagridPreviewProps, ItemSelectionMethodEnum } from "../../typings/DatagridProps";
export type SelectionMethod = "rowClick" | "checkbox" | "none";
@@ -12,6 +13,9 @@ export class SelectActionHelper extends SelectActionHandler {
pageSize: number;
private _selectionMethod: ItemSelectionMethodEnum;
private _showSelectAllToggle: boolean;
+ private _datasource: ListValue;
+ private _selectAllPagesEnabled: boolean;
+ private _selectAllPagesBufferSize: number;
constructor(
selection: WidgetSelectionProperty,
@@ -19,12 +23,18 @@ export class SelectActionHelper extends SelectActionHandler {
_selectionMethod: ItemSelectionMethodEnum,
_showSelectAllToggle: boolean,
pageSize: number,
- private _selectionMode: SelectionMode
+ private _selectionMode: SelectionMode,
+ datasource: ListValue,
+ selectAllPagesEnabled?: boolean,
+ selectAllPagesBufferSize?: number
) {
super(selection, selectionHelper);
this._selectionMethod = _selectionMethod;
this._showSelectAllToggle = _showSelectAllToggle;
this.pageSize = pageSize;
+ this._datasource = datasource;
+ this._selectAllPagesEnabled = selectAllPagesEnabled ?? false;
+ this._selectAllPagesBufferSize = selectAllPagesBufferSize ?? 500;
}
get selectionMethod(): SelectionMethod {
@@ -42,26 +52,51 @@ export class SelectActionHelper extends SelectActionHandler {
get selectionMode(): SelectionMode {
return this.selectionMethod === "checkbox" ? "toggle" : this._selectionMode;
}
+
+ get canSelectAllPages(): boolean {
+ return this._selectAllPagesEnabled && this.selectionType === "Multi";
+ }
+
+ get totalCount(): number | undefined {
+ return this._datasource?.totalCount;
+ }
}
export function useSelectActionHelper(
props: Pick<
DatagridContainerProps | DatagridPreviewProps,
- "itemSelection" | "itemSelectionMethod" | "showSelectAllToggle" | "pageSize" | "itemSelectionMode"
+ | "itemSelection"
+ | "itemSelectionMethod"
+ | "showSelectAllToggle"
+ | "pageSize"
+ | "itemSelectionMode"
+ | "datasource"
+ | "selectAllPagesEnabled"
+ | "selectAllPagesPageSize"
>,
selectionHelper?: SelectionHelper
): SelectActionHelper {
- return useMemo(
- () =>
- new SelectActionHelper(
- props.itemSelection,
- selectionHelper,
- props.itemSelectionMethod,
- props.showSelectAllToggle,
- props.pageSize ?? 5,
- props.itemSelectionMode
- ),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [selectionHelper]
- );
+ return useMemo(() => {
+ return new SelectActionHelper(
+ props.itemSelection,
+ selectionHelper,
+ props.itemSelectionMethod,
+ props.showSelectAllToggle,
+ props.pageSize ?? 5,
+ props.itemSelectionMode,
+ props.datasource as ListValue,
+ props.selectAllPagesEnabled,
+ props.selectAllPagesPageSize ?? 500
+ );
+ }, [
+ props.itemSelection,
+ selectionHelper,
+ props.itemSelectionMethod,
+ props.showSelectAllToggle,
+ props.pageSize,
+ props.itemSelectionMode,
+ props.datasource,
+ props.selectAllPagesEnabled,
+ props.selectAllPagesPageSize
+ ]);
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts
index 51386f8d90..03ee7ac3f9 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/root-context.ts
@@ -1,6 +1,7 @@
import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController";
-import { SelectionHelper } from "@mendix/widget-plugin-grid/selection";
-import { SelectionCountStore } from "@mendix/widget-plugin-grid/selection/stores/SelectionCountStore";
+import { SelectAllController, SelectionHelper } 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 { createContext, useContext } from "react";
import { GridBasicData } from "../helpers/state/GridBasicData";
import { EventsController } from "../typings/CellComponent";
@@ -13,8 +14,10 @@ export interface DatagridRootScope {
selectActionHelper: SelectActionHelper;
cellEventsController: EventsController;
checkboxEventsController: EventsController;
+ selectAllController: SelectAllController;
focusController: FocusTargetController;
selectionCountStore: SelectionCountStore;
+ selectAllProgressStore: ProgressStore;
}
export const DatagridContext = createContext(null);
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridBasicData.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridBasicData.ts
index 1b0b1ed909..b67c9726ee 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridBasicData.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridBasicData.ts
@@ -5,7 +5,14 @@ import { DatagridContainerProps } from "../../../typings/DatagridProps";
type Props = Pick<
DatagridContainerProps,
- "exportDialogLabel" | "cancelExportLabel" | "selectRowLabel" | "selectAllRowsLabel" | "itemSelection" | "onClick"
+ | "exportDialogLabel"
+ | "cancelExportLabel"
+ | "selectRowLabel"
+ | "selectAllRowsLabel"
+ | "itemSelection"
+ | "onClick"
+ | "selectingAllLabel"
+ | "cancelSelectionLabel"
>;
type Gate = DerivedPropsGate;
@@ -36,6 +43,14 @@ export class GridBasicData {
return this.gate.props.selectAllRowsLabel?.value;
}
+ get selectingAllLabel(): string | undefined {
+ return this.gate.props.selectingAllLabel?.value;
+ }
+
+ get cancelSelectionLabel(): string | undefined {
+ return this.gate.props.cancelSelectionLabel?.value;
+ }
+
get gridInteractive(): boolean {
return !!(this.gate.props.itemSelection || this.gate.props.onClick);
}
@@ -44,6 +59,10 @@ export class GridBasicData {
return this.selectionHelper?.type === "Multi" ? this.selectionHelper.selectionStatus : "none";
}
+ get currentSelectionHelper(): SelectionHelper | null {
+ return this.selectionHelper;
+ }
+
setSelectionHelper(selectionHelper: SelectionHelper | undefined): void {
this.selectionHelper = selectionHelper ?? null;
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
index 7e64c08ec0..5d48ebe815 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
@@ -2,8 +2,11 @@ import { createContextWithStub, FilterAPI } from "@mendix/widget-plugin-filterin
import { CombinedFilter } from "@mendix/widget-plugin-filtering/stores/generic/CombinedFilter";
import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost";
import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController";
+import { QueryController } from "@mendix/widget-plugin-grid/query/query-controller";
import { RefreshController } from "@mendix/widget-plugin-grid/query/RefreshController";
-import { SelectionCountStore } from "@mendix/widget-plugin-grid/selection/stores/SelectionCountStore";
+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 { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
@@ -14,7 +17,6 @@ import { DatagridContainerProps } from "../../../typings/DatagridProps";
import { DatasourceParamsController } from "../../controllers/DatasourceParamsController";
import { DerivedLoaderController } from "../../controllers/DerivedLoaderController";
import { PaginationController } from "../../controllers/PaginationController";
-import { ProgressStore } from "../../features/data-export/ProgressStore";
import { StaticInfo } from "../../typings/static-info";
import { ColumnGroupStore } from "./ColumnGroupStore";
import { GridPersonalizationStore } from "./GridPersonalizationStore";
@@ -34,13 +36,18 @@ type RequiredProps = Pick<
| "pagination"
| "showPagingButtons"
| "showNumberOfRows"
+ | "selectAllPagesEnabled"
+ | "selectAllPagesPageSize"
+ | "onSelectionChange"
>;
type Gate = DerivedPropsGate;
type Spec = {
gate: Gate;
- exportCtrl: ProgressStore;
+ exportProgressStore: ProgressStore;
+ selectAllProgressStore: ProgressStore;
+ selectAllController: SelectAllController;
};
export class RootGridStore extends BaseControllerHost {
@@ -49,14 +56,16 @@ export class RootGridStore extends BaseControllerHost {
selectionCountStore: SelectionCountStore;
basicData: GridBasicData;
staticInfo: StaticInfo;
- exportProgressCtrl: ProgressStore;
+ exportProgressStore: ProgressStore;
+ selectAllController: SelectAllController;
+ selectAllProgressStore: ProgressStore;
loaderCtrl: DerivedLoaderController;
paginationCtrl: PaginationController;
- readonly filterAPI: FilterAPI;
-
- private gate: Gate;
+ filterAPI: FilterAPI;
+ query: QueryController;
+ gate: Gate;
- constructor({ gate, exportCtrl }: Spec) {
+ constructor({ gate, exportProgressStore, selectAllProgressStore, selectAllController }: Spec) {
super();
const { props } = gate;
@@ -69,7 +78,7 @@ export class RootGridStore extends BaseControllerHost {
const filterHost = new CustomFilterHost();
- const query = new DatasourceController(this, { gate });
+ const query = (this.query = new DatasourceController(this, { gate }));
this.filterAPI = createContextWithStub({
filterObserver: filterHost,
@@ -91,7 +100,11 @@ export class RootGridStore extends BaseControllerHost {
this.paginationCtrl = new PaginationController(this, { gate, query });
- this.exportProgressCtrl = exportCtrl;
+ this.exportProgressStore = exportProgressStore;
+
+ this.selectAllProgressStore = selectAllProgressStore;
+
+ this.selectAllController = selectAllController;
new DatasourceParamsController(this, {
query,
@@ -105,7 +118,7 @@ export class RootGridStore extends BaseControllerHost {
});
this.loaderCtrl = new DerivedLoaderController({
- exp: exportCtrl,
+ exp: exportProgressStore,
cols: this.columnsStore,
showSilentRefresh: props.refreshInterval > 1,
refreshIndicator: props.refreshIndicator,
@@ -120,13 +133,10 @@ export class RootGridStore extends BaseControllerHost {
add(super.setup());
add(this.columnsStore.setup());
add(() => this.settingsStore.dispose());
- add(autorun(() => this.updateProps(this.gate.props)));
-
+ // Column store & settings store is still using old `updateProps`
+ // approach. So, we use autorun to sync props.
+ add(autorun(() => this.columnsStore.updateProps(this.gate.props)));
+ add(autorun(() => this.settingsStore.updateProps(this.gate.props)));
return disposeAll;
}
-
- private updateProps(props: RequiredProps): void {
- this.columnsStore.updateProps(props);
- this.settingsStore.updateProps(props);
- }
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/useRootStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/useRootStore.ts
index 40903de468..b853e8c7c3 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/useRootStore.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/useRootStore.ts
@@ -1,21 +1,44 @@
+import { SelectAllHost } from "@mendix/widget-plugin-grid/selection";
+import { ProgressStore } from "@mendix/widget-plugin-grid/stores/ProgressStore";
import { ClosableGateProvider } from "@mendix/widget-plugin-mobx-kit/ClosableGateProvider";
+import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider";
import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
import { useEffect } from "react";
import { DatagridContainerProps } from "../../../typings/DatagridProps";
-import { ProgressStore } from "../../features/data-export/ProgressStore";
import { RootGridStore } from "./RootGridStore";
export function useRootStore(props: DatagridContainerProps): RootGridStore {
- const [gateProvider, exportProgressCtrl] = useConst(() => {
- const epc = new ProgressStore();
- const gp = new ClosableGateProvider(props, () => epc.exporting);
- return [gp, epc] as const;
+ const exportProgressStore = useConst(() => new ProgressStore());
+
+ const selectAllProgressStore = useConst(() => new ProgressStore());
+
+ const mainGateProvider = useConst(() => {
+ // Closed when exporting or selecting all
+ return new ClosableGateProvider(props, () => {
+ return exportProgressStore.inProgress || selectAllProgressStore.inProgress;
+ });
});
- const rootStore = useSetup(() => new RootGridStore({ gate: gateProvider.gate, exportCtrl: exportProgressCtrl }));
+
+ const selectAllGateProvider = useConst(() => new GateProvider(props));
+
+ const selectAllHost = useSetup(
+ () => new SelectAllHost({ gate: selectAllGateProvider.gate, selectAllProgressStore })
+ );
+
+ const rootStore = useSetup(
+ () =>
+ new RootGridStore({
+ gate: mainGateProvider.gate,
+ exportProgressStore,
+ selectAllProgressStore,
+ selectAllController: selectAllHost.selectAllController
+ })
+ );
useEffect(() => {
- gateProvider.setProps(props);
+ mainGateProvider.setProps(props);
+ selectAllGateProvider.setProps(props);
});
return rootStore;
diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx
index bc9e5953d4..c9826d0c70 100644
--- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx
@@ -1,7 +1,7 @@
import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController";
import { PositionController } from "@mendix/widget-plugin-grid/keyboard-navigation/PositionController";
import { VirtualGridLayout } from "@mendix/widget-plugin-grid/keyboard-navigation/VirtualGridLayout";
-import { dynamicValue, listAttr, listExp } from "@mendix/widget-plugin-test-utils";
+import { dynamicValue, listAttr, listExp, ListValueBuilder } from "@mendix/widget-plugin-test-utils";
import { GUID, ObjectItem } from "mendix";
import { createElement } from "react";
import { ColumnsType } from "../../typings/DatagridProps";
@@ -40,7 +40,17 @@ export const column = (header = "Test", patch?: (col: ColumnsType) => void): Col
};
export function mockSelectionProps(patch?: (props: SelectActionHelper) => SelectActionHelper): SelectActionHelper {
- const props = new SelectActionHelper("None", undefined, "checkbox", false, 5, "clear");
+ const props = new SelectActionHelper(
+ "None",
+ undefined,
+ "checkbox",
+ false,
+ 5,
+ "clear",
+ new ListValueBuilder().build(),
+ false,
+ 500
+ );
if (patch) {
patch(props);
diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts
index a3e01d2e2d..0508cfa3aa 100644
--- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts
+++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts
@@ -96,6 +96,10 @@ export interface DatagridContainerProps {
itemSelectionMode: ItemSelectionModeEnum;
showSelectAllToggle: boolean;
keepSelection: boolean;
+ selectAllPagesEnabled: boolean;
+ selectAllPagesPageSize: number;
+ selectingAllLabel?: DynamicValue;
+ cancelSelectionLabel?: DynamicValue;
loadingType: LoadingTypeEnum;
refreshIndicator: boolean;
columns: ColumnsType[];
@@ -148,6 +152,10 @@ export interface DatagridPreviewProps {
itemSelectionMode: ItemSelectionModeEnum;
showSelectAllToggle: boolean;
keepSelection: boolean;
+ selectAllPagesEnabled: boolean;
+ selectAllPagesPageSize: number | null;
+ selectingAllLabel: string;
+ cancelSelectionLabel: string;
loadingType: LoadingTypeEnum;
refreshIndicator: boolean;
columns: ColumnsPreviewType[];
diff --git a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts
index 6fe2f5a66f..224890de45 100644
--- a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts
+++ b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts
@@ -1,8 +1,8 @@
import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller";
-import { ListValue, ValueStatus } from "mendix";
-import { action, autorun, computed, IComputedValue, makeAutoObservable } from "mobx";
+import { ListValue, ObjectItem, ValueStatus } from "mendix";
+import { action, autorun, computed, IComputedValue, makeAutoObservable, when } from "mobx";
import { QueryController } from "./query-controller";
type Gate = DerivedPropsGate<{ datasource: ListValue }>;
@@ -103,6 +103,10 @@ export class DatasourceController implements ReactiveController, QueryController
return this.datasource.hasMoreItems ?? false;
}
+ get items(): ObjectItem[] | undefined {
+ return this.datasource.items;
+ }
+
/**
* Returns computed value that holds controller copy.
* Recomputes the copy every time the datasource changes.
@@ -164,4 +168,39 @@ export class DatasourceController implements ReactiveController, QueryController
setPageSize(size: number): void {
this.pageSize = size;
}
+
+ reload(): Promise {
+ const ds = this.datasource;
+ this.datasource.reload();
+ return when(() => this.datasource !== ds);
+ }
+
+ fetchPage({
+ limit,
+ offset,
+ signal
+ }: {
+ limit: number;
+ offset: number;
+ signal?: AbortSignal;
+ }): Promise {
+ return new Promise((resolve, reject) => {
+ if (signal && signal.aborted) {
+ return reject(signal.reason);
+ }
+
+ const predicate = when(
+ () =>
+ this.datasource.offset === offset &&
+ this.datasource.limit === limit &&
+ this.datasource.status === "available",
+ { signal }
+ );
+
+ predicate.then(() => resolve(this.datasource.items ?? []), reject);
+
+ this.datasource.setOffset(offset);
+ this.datasource.setLimit(limit);
+ });
+ }
}
diff --git a/packages/shared/widget-plugin-grid/src/query/query-controller.ts b/packages/shared/widget-plugin-grid/src/query/query-controller.ts
index a5fb0421b3..306374b068 100644
--- a/packages/shared/widget-plugin-grid/src/query/query-controller.ts
+++ b/packages/shared/widget-plugin-grid/src/query/query-controller.ts
@@ -1,4 +1,4 @@
-import { ListValue } from "mendix";
+import { ListValue, ObjectItem } from "mendix";
type Members =
| "setOffset"
@@ -9,6 +9,7 @@ type Members =
| "totalCount"
| "limit"
| "offset"
+ | "items"
| "hasMoreItems";
export interface QueryController extends Pick {
@@ -18,4 +19,6 @@ export interface QueryController extends Pick {
isFirstLoad: boolean;
isRefreshing: boolean;
isFetchingNextBatch: boolean;
+ fetchPage(params: { limit: number; offset: number; signal?: AbortSignal }): Promise;
+ reload(): Promise;
}
diff --git a/packages/shared/widget-plugin-grid/src/select-all/SelectAllController.ts b/packages/shared/widget-plugin-grid/src/select-all/SelectAllController.ts
new file mode 100644
index 0000000000..372a171e5d
--- /dev/null
+++ b/packages/shared/widget-plugin-grid/src/select-all/SelectAllController.ts
@@ -0,0 +1,178 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
+import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller";
+import { ObjectItem, SelectionMultiValue, SelectionSingleValue } from "mendix";
+import { action, computed, makeObservable, observable, when } from "mobx";
+import { QueryController } from "../query/query-controller";
+
+type Gate = DerivedPropsGate<{ itemSelection?: SelectionMultiValue | SelectionSingleValue }>;
+
+interface SelectAllControllerSpec {
+ gate: Gate;
+ query: QueryController;
+ pageSize: number;
+}
+
+type SelectAllEventType = "loadstart" | "progress" | "abort" | "loadend";
+
+export class SelectAllController implements ReactiveController {
+ private readonly gate: Gate;
+ private readonly query: QueryController;
+ private abortController?: AbortController;
+ private locked = false;
+ readonly pageSize: number = 1024;
+ private readonly emitter = new EventTarget();
+
+ constructor(host: ReactiveControllerHost, spec: SelectAllControllerSpec) {
+ host.addController(this);
+ this.gate = spec.gate;
+ this.query = spec.query;
+
+ type PrivateMembers = "setIsLocked" | "locked";
+ makeObservable(this, {
+ setIsLocked: action,
+ canExecute: computed,
+ isExecuting: computed,
+ selection: computed,
+ locked: observable,
+ selectAllPages: action,
+ clearSelection: action,
+ abort: action
+ });
+ }
+
+ setup(): () => void {
+ return () => this.abort();
+ }
+
+ on(type: SelectAllEventType, listener: (pe: ProgressEvent) => void): void {
+ this.emitter.addEventListener(type, listener);
+ }
+
+ off(type: SelectAllEventType, listener: (pe: ProgressEvent) => void): void {
+ this.emitter.removeEventListener(type, listener);
+ }
+
+ get selection(): SelectionMultiValue | undefined {
+ const selection = this.gate.props.itemSelection;
+ if (selection === undefined) return;
+ if (selection.type === "Single") return;
+ return selection;
+ }
+
+ get canExecute(): boolean {
+ return this.gate.props.itemSelection?.type === "Multi" && !this.locked;
+ }
+
+ get isExecuting(): boolean {
+ return this.locked;
+ }
+
+ private setIsLocked(value: boolean): void {
+ this.locked = value;
+ }
+
+ private beforeRunChecks(): boolean {
+ const selection = this.gate.props.itemSelection;
+
+ if (selection === undefined) {
+ console.debug("SelectAllController: selection is undefined. Check widget selection setting.");
+ return false;
+ }
+ if (selection.type !== "Multi") {
+ console.debug("SelectAllController: action can't be executed when selection is 'Single'.");
+ return false;
+ }
+
+ if (this.locked) {
+ console.debug("SelectAllController: action is already executing.");
+ return false;
+ }
+ return true;
+ }
+
+ async selectAllPages(): Promise {
+ if (!this.beforeRunChecks()) {
+ return;
+ }
+
+ this.setIsLocked(true);
+
+ const { offset: initOffset, limit: initLimit } = this.query;
+ const hasTotal = typeof this.query.totalCount === "number";
+ const totalCount = this.query.totalCount ?? 0;
+ let loaded = 0;
+ let offset = 0;
+ let success = false;
+ const pe = (type: SelectAllEventType): ProgressEvent =>
+ new ProgressEvent(type, { loaded, total: totalCount, lengthComputable: hasTotal });
+ // We should avoid duplicates, so, we start with clean array.
+ const allItems: ObjectItem[] = [];
+ this.abortController = new AbortController();
+ const signal = this.abortController.signal;
+
+ performance.mark("SelectAll_Start");
+ try {
+ this.emitter.dispatchEvent(pe("loadstart"));
+ let loading = true;
+ while (loading) {
+ const loadedItems = await this.query.fetchPage({
+ limit: this.pageSize,
+ offset,
+ signal
+ });
+
+ allItems.push(...loadedItems);
+ loaded += loadedItems.length;
+ offset += this.pageSize;
+ this.emitter.dispatchEvent(pe("progress"));
+ loading = !signal.aborted && this.query.hasMoreItems;
+ }
+ success = true;
+ } catch (error) {
+ if (!signal.aborted) {
+ console.error("SelectAllController: an error was encountered during the 'select all' action.");
+ console.error(error);
+ }
+ } finally {
+ // Restore init view
+ // This step should be done before loadend to avoid UI flickering
+ await this.query.fetchPage({
+ limit: initLimit,
+ offset: initOffset
+ });
+
+ this.emitter.dispatchEvent(pe("loadend"));
+
+ const selectionBeforeReload = this.selection?.selection ?? [];
+ // Reload selection to make sure setSelection is working as expected.
+ await this.reloadSelection();
+ this.selection?.setSelection(success ? allItems : selectionBeforeReload);
+ this.locked = false;
+ this.abortController = undefined;
+
+ performance.mark("SelectAll_End");
+ const measure1 = performance.measure("Measure1", "SelectAll_Start", "SelectAll_End");
+ console.debug(`Data grid 2: 'select all' took ${(measure1.duration / 1000).toFixed(2)} seconds.`);
+ }
+ }
+
+ reloadSelection(): Promise {
+ const selection = this.selection;
+ selection?.setSelection([]);
+ // `when` resolves when selection value is updated
+ return when(() => this.selection !== selection);
+ }
+
+ clearSelection(): void {
+ if (this.locked) {
+ console.debug("SelectAllController: can't clear selection while executing.");
+ return;
+ }
+ this.selection?.setSelection([]);
+ }
+
+ abort(): void {
+ this.abortController?.abort();
+ this.emitter.dispatchEvent(new ProgressEvent("abort"));
+ }
+}
diff --git a/packages/shared/widget-plugin-grid/src/select-all/SelectAllHost.ts b/packages/shared/widget-plugin-grid/src/select-all/SelectAllHost.ts
new file mode 100644
index 0000000000..0b423bdd8c
--- /dev/null
+++ b/packages/shared/widget-plugin-grid/src/select-all/SelectAllHost.ts
@@ -0,0 +1,50 @@
+import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
+import { ListValue, SelectionMultiValue, SelectionSingleValue } from "mendix";
+import { DatasourceController } from "../query/DatasourceController";
+import { ProgressStore } from "../stores/ProgressStore";
+import { SelectAllController } from "./SelectAllController";
+
+type SelectAllHostSpec = {
+ gate: DerivedPropsGate<{ itemSelection?: SelectionMultiValue | SelectionSingleValue; datasource: ListValue }>;
+ selectAllProgressStore: ProgressStore;
+};
+
+export class SelectAllHost extends BaseControllerHost {
+ readonly selectAllController: SelectAllController;
+ readonly selectAllProgressStore: ProgressStore;
+
+ constructor(spec: SelectAllHostSpec) {
+ super();
+ const query = new DatasourceController(this, { gate: spec.gate });
+ this.selectAllController = new SelectAllController(this, { gate: spec.gate, query, pageSize: 30 });
+ this.selectAllProgressStore = spec.selectAllProgressStore;
+ }
+
+ setup(): () => void {
+ const [add, disposeAll] = disposeBatch();
+
+ add(super.setup());
+ add(this.setupSelectAllProgressStore());
+
+ return disposeAll;
+ }
+
+ private setupSelectAllProgressStore() {
+ const controller = this.selectAllController;
+ const loadstart = (e: ProgressEvent): void => this.selectAllProgressStore.onloadstart(e);
+ const loadend = (): void => this.selectAllProgressStore.onloadend();
+ const progress = (e: ProgressEvent): void => this.selectAllProgressStore.onprogress(e);
+
+ controller.on("loadstart", loadstart);
+ controller.on("loadend", loadend);
+ controller.on("progress", progress);
+
+ return () => {
+ controller.off("loadstart", loadstart);
+ controller.off("loadend", loadend);
+ controller.off("progress", progress);
+ };
+ }
+}
diff --git a/packages/shared/widget-plugin-grid/src/selection.ts b/packages/shared/widget-plugin-grid/src/selection.ts
index c7514487c6..067c28ecba 100644
--- a/packages/shared/widget-plugin-grid/src/selection.ts
+++ b/packages/shared/widget-plugin-grid/src/selection.ts
@@ -1,9 +1,11 @@
-export * from "./selection/types.js";
-export * from "./selection/helpers.js";
-export * from "./selection/keyboard.js";
+export { SelectAllController } from "./select-all/SelectAllController.js";
+export { SelectAllHost } from "./select-all/SelectAllHost.js";
export {
getGlobalSelectionContext,
useCreateSelectionContextValue,
useSelectionContextValue
} from "./selection/context.js";
+export * from "./selection/helpers.js";
+export * from "./selection/keyboard.js";
export { SelectActionHandler } from "./selection/select-action-handler.js";
+export * from "./selection/types.js";
diff --git a/packages/shared/widget-plugin-grid/src/selection/__tests__/SelectionCountStore.spec.ts b/packages/shared/widget-plugin-grid/src/selection/__tests__/SelectionCountStore.spec.ts
index e9026e01dc..4765f40138 100644
--- a/packages/shared/widget-plugin-grid/src/selection/__tests__/SelectionCountStore.spec.ts
+++ b/packages/shared/widget-plugin-grid/src/selection/__tests__/SelectionCountStore.spec.ts
@@ -1,7 +1,7 @@
import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider";
import { objectItems, SelectionMultiValueBuilder, SelectionSingleValueBuilder } from "@mendix/widget-plugin-test-utils";
import { SelectionMultiValue, SelectionSingleValue } from "mendix";
-import { SelectionCountStore } from "../stores/SelectionCountStore";
+import { SelectionCountStore } from "../../stores/SelectionCountStore";
type Props = {
itemSelection?: SelectionSingleValue | SelectionMultiValue;
diff --git a/packages/shared/widget-plugin-grid/src/stores/ProgressStore.ts b/packages/shared/widget-plugin-grid/src/stores/ProgressStore.ts
new file mode 100644
index 0000000000..fc5e17b5e4
--- /dev/null
+++ b/packages/shared/widget-plugin-grid/src/stores/ProgressStore.ts
@@ -0,0 +1,49 @@
+import { makeAutoObservable } from "mobx";
+
+export class ProgressStore {
+ inProgress = false;
+ /**
+ * If `false`, then `ProgressStore.total` and
+ * `ProgressStore.progress` has no meaningful value.
+ */
+ lengthComputable = false;
+ loaded = 0;
+ total = 0;
+ constructor() {
+ makeAutoObservable(this);
+ }
+
+ get percentage(): number {
+ if (!this.lengthComputable || !this.inProgress || this.total <= 0) {
+ return 0;
+ }
+
+ const percentage = (this.loaded / this.total) * 100;
+ switch (true) {
+ case isNaN(percentage):
+ return 0;
+ case isFinite(percentage):
+ return percentage;
+ default:
+ return 0;
+ }
+ }
+
+ onloadstart = (event: ProgressEvent): void => {
+ this.inProgress = true;
+ this.lengthComputable = event.lengthComputable;
+ this.total = event.total;
+ this.loaded = 0;
+ };
+
+ onprogress = (event: ProgressEvent): void => {
+ this.loaded = event.loaded;
+ };
+
+ onloadend = (): void => {
+ this.inProgress = false;
+ this.lengthComputable = false;
+ this.loaded = 0;
+ this.total = 0;
+ };
+}
diff --git a/packages/shared/widget-plugin-grid/src/selection/stores/SelectionCountStore.ts b/packages/shared/widget-plugin-grid/src/stores/SelectionCountStore.ts
similarity index 100%
rename from packages/shared/widget-plugin-grid/src/selection/stores/SelectionCountStore.ts
rename to packages/shared/widget-plugin-grid/src/stores/SelectionCountStore.ts