Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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% {
Expand Down
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/datagrid-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand All @@ -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");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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) => ({
Expand All @@ -87,17 +92,23 @@ 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,
selectActionHelper,
cellEventsController: eventsController,
checkboxEventsController: eventsController,
focusController,
selectionCountStore
selectionCountStore,
selectAllProgressStore: new ProgressStore(),
selectAllController,
rootStore: {} as any // Mock for preview
};
});

Expand Down
41 changes: 16 additions & 25 deletions packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);

Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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}
Expand All @@ -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 (
<Container
{...props}
rootStore={rootStore}
columnsStore={rootStore.columnsStore}
progressStore={rootStore.exportProgressCtrl}
/>
);
return <Container {...props} rootStore={useRootStore(props)} />;
}
22 changes: 22 additions & 0 deletions packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@
<caption>Keep selection</caption>
<description>If enabled, selected items will stay selected unless cleared by the user or a Nanoflow.</description>
</property>
<property key="selectAllPagesEnabled" type="boolean" defaultValue="false">
<caption>Enable select all pages</caption>
<description>Allow select all through multiple pages (based on current filter). Only works if total count is known.</description>
</property>
<property key="selectAllPagesPageSize" type="integer" defaultValue="500">
<caption>Select all page size</caption>
<description>When selecting items from a large data source, items are selected in batches. This setting controls the size of the batches.</description>
</property>
<property key="selectingAllLabel" type="textTemplate" required="false">
<caption>Selecting all label</caption>
<description>Label shown in the progress dialog when selecting all items</description>
<translations>
<translation lang="en_US">Selecting all items...</translation>
</translations>
</property>
<property key="cancelSelectionLabel" type="textTemplate" required="false">
<caption>Cancel selection label</caption>
<description>Label for the cancel button in the selection progress dialog</description>
<translations>
<translation lang="en_US">Cancel selection</translation>
</translations>
</property>
<property key="loadingType" type="enumeration" defaultValue="spinner" required="true">
<caption>Loading type</caption>
<description />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
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 { showCheckboxColumn, showSelectAllToggle, onSelectAll } = selectActionHelper;
const { selectionStatus, selectAllRowsLabel } = basicData;

const onChange = useCallback(() => onSelectAll(), [onSelectAll]);

if (showCheckboxColumn === false) {
return <Fragment />;
}

let checkbox = null;

if (showSelectAllToggle) {
if (selectionStatus === "unknown") {
throw new Error("Don't know how to render checkbox with selectionStatus=unknown");
}

checkbox = (
<ThreeStateCheckBox
value={selectionStatus}
onChange={onChange}
aria-label={selectAllRowsLabel ?? "Select all rows"}
/>
);
}

return (
<div className="th widget-datagrid-col-select" role="columnheader">
{checkbox}
{showSelectAllToggle && (
<Checkbox status={selectionStatus} onChange={onSelectAll} aria-label={selectAllRowsLabel} />
)}
</div>
);
}

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 (
<ThreeStateCheckBox
value={props.status}
onChange={props.onChange}
aria-label={props["aria-label"] ?? "Select all rows"}
/>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="widget-datagrid-select-all-bar">
<button onClick={() => selectAllController.selectAllPages()}>Select remaining</button>
</div>
);
});
Original file line number Diff line number Diff line change
@@ -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 (
<PseudoModal>
<ExportAlert
alertLabel={selectingLabel}
cancelLabel={cancelLabel}
failed={false}
onCancel={onCancel}
progress={progress}
total={total}
/>
</PseudoModal>
);
}
Loading
Loading