Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
78775eb
feat(widget-plugin-grid): sync dynamic pagination attrs and expose lo…
samuelreichert Mar 9, 2026
c6b9e20
feat(datagrid-web): expose and sync pagination attrs across modes wit…
samuelreichert Mar 9, 2026
72075e0
chore(datagrid-web): update changelog with new loaded rows attribute …
samuelreichert Mar 9, 2026
9e92ca3
feat(gallery-web): enhance pagination bindings with currentPage, page…
samuelreichert Mar 9, 2026
401cf62
fix(gallery-web): add clearSelectionButtonLabel to hidden properties …
samuelreichert Mar 10, 2026
680a544
fix(datagrid-web): move custom pagination to footer and add editor pr…
samuelreichert Mar 10, 2026
54df9f5
fix(widget-plugin-grid): prevent outbound autoruns from clobbering at…
samuelreichert Mar 10, 2026
062429c
fix(widget-plugin-grid): prevent infinite load loop in virtual scroll…
samuelreichert Mar 11, 2026
b36cdb4
test(widget-plugin-grid): add regression tests for virtual scroll inf…
samuelreichert Mar 11, 2026
c7c36f7
fix(datagrid-web): only clamp grid body height when content does not …
samuelreichert Mar 12, 2026
292821d
fix(widget-plugin-grid): skip writing sentinel -1 to totalCountValue …
samuelreichert Mar 12, 2026
e5036da
fix(gallery-web): enable dynamic page sync for virtual scroll and req…
samuelreichert Mar 12, 2026
05481c0
feat(gallery-web): add loadedRowsValue attribute for loaded rows count
samuelreichert Mar 12, 2026
eaf401c
fix(gallery-web): simplify property hiding for custom pagination sett…
samuelreichert Mar 12, 2026
4b97d5f
chore(gallery-web): document fix for pagination properties visibility…
samuelreichert Mar 12, 2026
6a8f64a
fix(widget-plugin-grid): ensure immediate application of externally-p…
samuelreichert Mar 13, 2026
6e4cd18
fix(widget-plugin-grid): apply external pageSize/currentPage before f…
samuelreichert Mar 13, 2026
dee064b
fix(widget-plugin-grid): guard limit-based dynamic page init
samuelreichert Mar 13, 2026
f42b838
test(datagrid-web): update ColumnSelector unit tests for latest selec…
samuelreichert Mar 13, 2026
1272272
fix(widget-plugin-grid): rename unused config param to _config in cre…
samuelreichert Mar 13, 2026
1f700f9
fix(widget-plugin-grid): add explicit TypeScript parser to SWC jest t…
samuelreichert Mar 13, 2026
fbb071d
refactor(widget-plugin-grid): move loadedRowsAtom to shared paginatio…
samuelreichert Mar 16, 2026
692208d
refactor(datagrid-web): import loadedRowsAtom from widget-plugin-grid…
samuelreichert Mar 16, 2026
5c06cce
fix(gallery-web): remove stale pageSize arg from DynamicPaginationFea…
samuelreichert Mar 16, 2026
ea2f2c8
refactor(widget-plugin-grid): replace boxAtom wrapper with IObservabl…
samuelreichert Mar 16, 2026
9a434a9
test(datagrid-web): fix column selector click on e2e tests
samuelreichert Mar 17, 2026
d6a0429
refactor(data-widgets): extract resolveInitPageSize into pagination
samuelreichert Mar 18, 2026
1012e74
fix(datagrid-web): prevent negative height lock and clear infinite
samuelreichert Mar 18, 2026
692365a
fix(widget-plugin-grid): skip setLimit if limit is unchanged
samuelreichert Mar 18, 2026
9ac8c77
refactor(data-widgets): simplify resolveInitPageSize to use dynamicPa…
samuelreichert Mar 18, 2026
2e5f6ed
fix(datagrid-web): recompute virtual scroll height on page size change
samuelreichert Mar 18, 2026
b953a32
docs(data-widgets): document why resolveInitPageSize returns 0 for dy…
samuelreichert Mar 18, 2026
c6856ed
feat: smart viewport in virtual scrolling
iobuhov Mar 18, 2026
ad8a238
test(datagrid-web): update e2e snapshot
samuelreichert Mar 18, 2026
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
8 changes: 8 additions & 0 deletions packages/pluggableWidgets/datagrid-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- We added accessibility support for column headers when single selection is enabled, making sure the purpose of the column is announced.

- We added a new `Loaded rows` attribute that reflects the number of rows currently loaded for virtual scrolling and load-more pagination modes.

- We exposed the `Page`, `Page size`, and `Total count` attributes for virtual scrolling and load-more pagination modes so they are kept in sync at all times.

### Fixed

- We fixed an issue with Data export crashing on some Android devices.

- We fixed an issue where the `Page` attribute was not updated when navigating pages using the default (buttons) paging controls.

- We fixed an issue where configuring the `Total count` attribute had no effect for virtual scrolling and load-more pagination modes.

## [3.8.1] - 2026-02-19

### Fixed
Expand Down
6 changes: 3 additions & 3 deletions packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import path from "path";
import { test, expect } from "@playwright/test";
import * as XLSX from "xlsx";
import AxeBuilder from "@axe-core/playwright";

test.afterEach("Cleanup session", async ({ page }) => {
// Because the test isolation that will open a new session for every test executed, and that exceeds Mendix's license limit of 5 sessions, so we need to force logout after each test.
Expand Down Expand Up @@ -144,7 +144,7 @@ test.describe("capabilities: hiding", () => {
await page.locator(".column-selectors > li").nth(2).click();
await page.locator(".column-selectors > li").nth(1).click();
await expect(page.locator(".column-selectors input:checked")).toHaveCount(1);
await page.locator(".column-selectors > li").nth(0).click();
await page.locator(".column-selectors > li").nth(0).click({ force: true });
await expect(page.locator(".column-selectors input:checked")).toHaveCount(1);
// Trigger Enter keypress
await page.locator(".column-selectors > li").nth(0).press("Enter");
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function getProperties(values: DatagridPreviewProps, defaultProperties: P

if (values.pagination === "buttons") {
hidePropertyIn(defaultProperties, values, "showNumberOfRows");
hidePropertyIn(defaultProperties, values, "loadedRowsValue");

if (values.useCustomPagination === false) {
hidePropertyIn(defaultProperties, values, "customPagination");
Expand All @@ -82,13 +83,7 @@ export function getProperties(values: DatagridPreviewProps, defaultProperties: P
hidePropertyIn(defaultProperties, values, "pagingPosition");
}

hidePropertiesIn(defaultProperties, values, [
"dynamicPage",
"dynamicPageSize",
"useCustomPagination",
"customPagination",
"totalCountValue"
]);
hidePropertiesIn(defaultProperties, values, ["useCustomPagination", "customPagination"]);
}

if (values.pagination !== "loadMore") {
Expand Down Expand Up @@ -335,13 +330,27 @@ export const getPreview = (
)
]
: [];
const customPaginationWidgets = values.useCustomPagination
? [
rowLayout({
columnSize: "fixed",
borders: true
})(
dropzone(
dropzone.placeholder("Custom pagination: Place widgets here"),
dropzone.hideDataSourceHeaderIf(canHideDataSourceHeader)
)(values.customPagination)
)
]
: [];

return container()(
gridTitle,
...(canHideDataSourceHeader ? [datasource(values.datasource)()] : []),
gridHeaderWidgets,
columnHeaders,
...Array.from({ length: 5 }).map(() => columns),
...customPaginationWidgets,
...customEmptyMessageWidgets
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@ function WidgetTopBar(): ReactElement {
<div className={cls.topBar}>
<div className={cls.pagingTop}>
<div className={cls.ptStart}>{useTopCounter() ? <SelectionCounter /> : null}</div>
<div className={cls.ptEnd}>
{usePagingTop() ? <Pagination /> : null}
{useCustomPagination("top") ? <CustomPagination /> : null}
</div>
<div className={cls.ptEnd}>{usePagingTop() ? <Pagination /> : null}</div>
</div>
</div>
);
Expand Down Expand Up @@ -142,7 +139,7 @@ function WidgetFooter(): ReactElement {
</div>
<div className={cls.pbEnd}>
{usePagingBot() ? <Pagination /> : null}
{useCustomPagination("bottom") ? <CustomPagination /> : null}
{useCustomPagination() ? <CustomPagination /> : null}
</div>
</div>
</div>
Expand Down Expand Up @@ -406,7 +403,7 @@ function usePagingBot(): boolean {
return visible && props.pagingPosition !== "top";
}

function useCustomPagination(location: "top" | "bottom"): boolean {
function useCustomPagination(): boolean {
const props = useProps();
return props.useCustomPagination && (props.pagingPosition === location || props.pagingPosition === "both");
return props.useCustomPagination;
}
7 changes: 7 additions & 0 deletions packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,13 @@
<attributeType name="Integer" />
</attributeTypes>
</property>
<property key="loadedRowsValue" type="attribute" required="false">
<caption>Loaded rows</caption>
<description>Read-only attribute reflecting the number of rows currently loaded.</description>
<attributeTypes>
<attributeType name="Integer" />
</attributeTypes>
</property>
</propertyGroup>
<propertyGroup caption="Appearance">
<property key="showEmptyPlaceholder" type="enumeration" defaultValue="none">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export const WidgetFooter = observer(function WidgetFooter(): ReactElement | nul
const selectionCounterVM = useSelectionCounterViewModel();
const customPagination = useCustomPagination();

const showFooter = selectionCounterVM.isBottomCounterVisible || paging.paginationVisible || paging.loadMoreVisible;
const showFooter =
selectionCounterVM.isBottomCounterVisible ||
paging.paginationVisible ||
paging.loadMoreVisible ||
pgConfig.customPaginationEnabled;

if (!showFooter) {
return null;
Expand All @@ -39,7 +43,7 @@ export const WidgetFooter = observer(function WidgetFooter(): ReactElement | nul
</div>
</If>
<div className="widget-datagrid-pb-end">
<If condition={pgConfig.pagingPosition !== "top"}>
<If condition={!pgConfig.customPaginationEnabled && pgConfig.pagingPosition !== "top"}>
<Pagination />
</If>
<If condition={pgConfig.customPaginationEnabled}>{customPagination.get()}</If>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const WidgetTopBar = observer(function WidgetTopBar(): ReactElement {
</If>
</div>
<div className="widget-datagrid-tb-end">
<If condition={pgConfig.pagingPosition !== "bottom"}>
<If condition={!pgConfig.customPaginationEnabled && pgConfig.pagingPosition !== "bottom"}>
<Pagination />
</If>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import userEvent from "@testing-library/user-event";
import { ColumnSelector, ColumnSelectorProps } from "../ColumnSelector";
import { ColumnId, GridColumn } from "../../typings/GridColumn";

beforeAll(() => {
Object.defineProperty(global, "ResizeObserver", {
writable: true,
value: jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}))
});
});

jest.useFakeTimers();

describe("Column Selector", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { MainGateProps } from "typings/MainGateProps";
import {
dynamicPageEnabled,
dynamicPageSizeEnabled,
requestTotalCount,
resolveInitPageSize
} from "../pagination.config";

function makeProps(overrides = {}): Partial<MainGateProps> {
return {
pagination: "buttons",
showNumberOfRows: false,
pageSize: 10,
pagingPosition: "bottom",
useCustomPagination: false,
showPagingButtons: "always",
refreshIndicator: false,
refreshInterval: 0,
datasource: undefined,
columns: [],
filtersPlaceholder: undefined,
...overrides
};
}

describe("pagination.config helpers", () => {
describe("requestTotalCount", () => {
it("returns true when totalCountValue attribute is mapped regardless of pagination mode", () => {
const props = makeProps({ totalCountValue: {}, pagination: "virtualScrolling" });
expect(requestTotalCount(props as MainGateProps)).toBe(true);
});

it("returns true for buttons pagination even without attribute", () => {
const props = makeProps({ pagination: "buttons" });
expect(requestTotalCount(props as MainGateProps)).toBe(true);
});

it("returns true when showNumberOfRows is true", () => {
const props = makeProps({ pagination: "virtualScrolling", showNumberOfRows: true });
expect(requestTotalCount(props as MainGateProps)).toBe(true);
});

it("returns false for virtual scrolling without totalCountValue or showNumberOfRows", () => {
const props = makeProps({ pagination: "virtualScrolling", showNumberOfRows: false });
expect(requestTotalCount(props as MainGateProps)).toBe(false);
});
});

describe("dynamicPageEnabled", () => {
it("is true when dynamicPage attribute is mapped for buttons mode", () => {
const props = makeProps({ dynamicPage: {}, pagination: "buttons" });
expect(dynamicPageEnabled(props as MainGateProps)).toBe(true);
});

it("is true when dynamicPage attribute is mapped for virtualScrolling mode", () => {
const props = makeProps({ dynamicPage: {}, pagination: "virtualScrolling" });
expect(dynamicPageEnabled(props as MainGateProps)).toBe(true);
});

it("is true when dynamicPage attribute is mapped for loadMore mode", () => {
const props = makeProps({ dynamicPage: {}, pagination: "loadMore" });
expect(dynamicPageEnabled(props as MainGateProps)).toBe(true);
});

it("is false when no dynamicPage attribute is provided", () => {
const props = makeProps({ pagination: "virtualScrolling" });
expect(dynamicPageEnabled(props as MainGateProps)).toBe(false);
});
});

describe("dynamicPageSizeEnabled", () => {
it("is true when dynamicPageSize attribute is mapped for buttons mode", () => {
const props = makeProps({ dynamicPageSize: {}, pagination: "buttons" });
expect(dynamicPageSizeEnabled(props as MainGateProps)).toBe(true);
});

it("is true when dynamicPageSize attribute is mapped for virtualScrolling mode", () => {
const props = makeProps({ dynamicPageSize: {}, pagination: "virtualScrolling" });
expect(dynamicPageSizeEnabled(props as MainGateProps)).toBe(true);
});

it("is true when dynamicPageSize attribute is mapped for loadMore mode", () => {
const props = makeProps({ dynamicPageSize: {}, pagination: "loadMore" });
expect(dynamicPageSizeEnabled(props as MainGateProps)).toBe(true);
});

it("is false when no dynamicPageSize attribute is provided", () => {
const props = makeProps({ pagination: "loadMore" });
expect(dynamicPageSizeEnabled(props as MainGateProps)).toBe(false);
});
});

describe("resolveInitPageSize", () => {
it("returns 0 when dynamicPageSize attribute is set", () => {
const props = makeProps({ dynamicPageSize: {} });
expect(resolveInitPageSize(props as MainGateProps)).toBe(0);
});

it("falls back to constPageSize when dynamicPageSize is not set", () => {
const props = makeProps({ pageSize: 10 });
expect(resolveInitPageSize(props as MainGateProps)).toBe(10);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface PaginationConfig {
showPagingButtons: ShowPagingButtonsEnum;
showNumberOfRows: boolean;
constPageSize: number;
initPageSize: number;
isLimitBased: boolean;
dynamicPageSizeEnabled: boolean;
dynamicPageEnabled: boolean;
Expand All @@ -23,6 +24,7 @@ export function paginationConfig(props: MainGateProps): PaginationConfig {
showPagingButtons: props.showPagingButtons,
showNumberOfRows: props.showNumberOfRows,
constPageSize: props.pageSize,
initPageSize: resolveInitPageSize(props),
isLimitBased: isLimitBased(props),
paginationKind: paginationKind(props),
dynamicPageSizeEnabled: dynamicPageSizeEnabled(props),
Expand All @@ -35,6 +37,19 @@ export function paginationConfig(props: MainGateProps): PaginationConfig {
return Object.freeze(config);
}

/**
* Resolves the initial page size for the first datasource fetch.
* Returns 0 when `dynamicPageSize` is configured so that no rows are fetched
* before the attribute value is available — the real limit is applied once
* `DynamicPaginationFeature` syncs the attribute on setup.
*/
export function resolveInitPageSize(props: MainGateProps): number {
if (props.dynamicPageSize !== undefined) {
return 0;
}
return props.pageSize;
}

export function paginationKind(props: MainGateProps): PaginationKind {
if (props.useCustomPagination) {
return "custom";
Expand All @@ -44,17 +59,17 @@ export function paginationKind(props: MainGateProps): PaginationKind {
}

export function dynamicPageSizeEnabled(props: MainGateProps): boolean {
return props.dynamicPageSize !== undefined && !isLimitBased(props);
return props.dynamicPageSize !== undefined;
}

export function dynamicPageEnabled(props: MainGateProps): boolean {
return props.dynamicPage !== undefined && !isLimitBased(props);
return props.dynamicPage !== undefined;
}

function isLimitBased(props: MainGateProps): boolean {
return props.pagination === "virtualScrolling" || props.pagination === "loadMore";
}

function requestTotalCount(props: MainGateProps): boolean {
return props.pagination === "buttons" || props.showNumberOfRows;
export function requestTotalCount(props: MainGateProps): boolean {
return props.pagination === "buttons" || props.showNumberOfRows || props.totalCountValue !== undefined;
}
Loading
Loading