From 4f3119f0b9a06d307904701489708ed0410f6182 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 10:56:34 +0100 Subject: [PATCH 1/9] feat(datagrid-web): add loadedRowsValue property for tracking loaded rows in datagrid --- packages/pluggableWidgets/datagrid-web/src/Datagrid.xml | 7 +++++++ .../datagrid-web/typings/DatagridProps.d.ts | 2 ++ .../pluggableWidgets/datagrid-web/typings/MainGateProps.ts | 1 + 3 files changed, 10 insertions(+) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index 6067391165..0e8a1b94a5 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -342,6 +342,13 @@ + + Loaded rows + Number of rows currently loaded (virtual scrolling read-only) + + + + diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index 46af1c62d3..edc6a9ff93 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -127,6 +127,7 @@ export interface DatagridContainerProps { dynamicPageSize?: EditableValue; dynamicPage?: EditableValue; totalCountValue?: EditableValue; + loadedRowsValue?: EditableValue; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder?: ReactNode; rowClass?: ListExpressionValue; @@ -192,6 +193,7 @@ export interface DatagridPreviewProps { dynamicPageSize: string; dynamicPage: string; totalCountValue: string; + loadedRowsValue: string; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; rowClass: string; diff --git a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts index 94460b547b..3eaa9f68ce 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts @@ -41,6 +41,7 @@ export type MainGateProps = Pick< | "storeFiltersInPersonalization" | "style" | "totalCountValue" + | "loadedRowsValue" | "useCustomPagination" | "customPagination" >; From 77c01b8a04b465595110d6e23ba7b43992adb934 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 11:07:26 +0100 Subject: [PATCH 2/9] feat(datagrid-web): update pagination handling to hide loadedRowsValue in buttons and loadMore modes --- .../datagrid-web/src/Datagrid.editorConfig.ts | 12 +++++----- .../src/__tests__/consistency-check.spec.ts | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts index 350d1bff18..36b3e46a99 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts @@ -85,17 +85,17 @@ export function getProperties(values: DatagridPreviewProps, defaultProperties: P hidePropertyIn(defaultProperties, values, "pagingPosition"); } - hidePropertiesIn(defaultProperties, values, [ - "dynamicPage", - "dynamicPageSize", - "useCustomPagination", - "customPagination" - ]); + hidePropertiesIn(defaultProperties, values, ["useCustomPagination", "customPagination"]); } if (values.pagination !== "loadMore") { hidePropertyIn(defaultProperties, values, "loadMoreButtonCaption"); } + + if (values.pagination !== "virtualScrolling") { + hidePropertyIn(defaultProperties, values, "loadedRowsValue"); + } + if (values.showEmptyPlaceholder === "none") { hidePropertyIn(defaultProperties, values, "emptyPlaceholder"); } diff --git a/packages/pluggableWidgets/datagrid-web/src/__tests__/consistency-check.spec.ts b/packages/pluggableWidgets/datagrid-web/src/__tests__/consistency-check.spec.ts index ceb3eda982..a617c7c722 100644 --- a/packages/pluggableWidgets/datagrid-web/src/__tests__/consistency-check.spec.ts +++ b/packages/pluggableWidgets/datagrid-web/src/__tests__/consistency-check.spec.ts @@ -75,4 +75,27 @@ describe("consistency check", () => { expect(check(props as unknown as DatagridPreviewProps)).toEqual([]); }); }); + + describe("editor config exposes pagination attributes correctly", () => { + const base: any = { + columns: [], + groupList: [], + groupAttrs: [] + }; + + test("buttons mode hides loadedRowsValue and shows dynamicPage/size", () => { + const props = { ...base, pagination: "buttons" }; + expect(check(props as DatagridPreviewProps)).toEqual([]); + }); + + test("virtualScrolling mode shows loadedRowsValue and dynamicPage/size", () => { + const props = { ...base, pagination: "virtualScrolling" }; + expect(check(props as DatagridPreviewProps)).toEqual([]); + }); + + test("loadMore mode hides loadedRowsValue but still shows dynamicPage/size", () => { + const props = { ...base, pagination: "loadMore" }; + expect(check(props as DatagridPreviewProps)).toEqual([]); + }); + }); }); From 1da4e08c9c376dcb54b3d7e245cae890a266fbd8 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 14:12:30 +0100 Subject: [PATCH 3/9] feat(datagrid-web): enhance dynamic page handling and total count request logic --- .../__tests__/pagination.config.spec.ts | 78 +++++++++++++++++++ .../features/pagination/pagination.config.ts | 16 +++- 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/pagination.config.spec.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/pagination.config.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/pagination.config.spec.ts new file mode 100644 index 0000000000..2fb340749b --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/pagination.config.spec.ts @@ -0,0 +1,78 @@ +import { requestTotalCount, dynamicPageEnabled, dynamicPageSizeEnabled } from "../pagination.config"; +import { MainGateProps } from "../../../typings/MainGateProps"; + +function makeProps(overrides: Partial = {}): MainGateProps { + // minimal set required by config functions + return { + pagination: "buttons", + showNumberOfRows: false, + pageSize: 10, + pagingPosition: "bottom", + useCustomPagination: false, + showPagingButtons: "always", + refreshIndicator: false, + refreshInterval: 0, + datasource: {} as any, + columns: [], + filtersPlaceholder: {} as any, + ...overrides + } as unknown as MainGateProps; +} + +describe("pagination.config helpers", () => { + describe("requestTotalCount", () => { + it("returns true when totalCountValue is provided no matter the pagination", () => { + const props = makeProps({ totalCountValue: {} as any, pagination: "virtualScrolling" }); + expect(requestTotalCount(props)).toBe(true); + }); + + it("returns true for buttons pagination even without attribute", () => { + const props = makeProps({ pagination: "buttons" }); + expect(requestTotalCount(props)).toBe(true); + }); + + it("returns true when showNumberOfRows is true", () => { + const props = makeProps({ pagination: "virtualScrolling", showNumberOfRows: true }); + expect(requestTotalCount(props)).toBe(true); + }); + + it("returns false when no hints present and non-buttons pagination", () => { + const props = makeProps({ pagination: "virtualScrolling", showNumberOfRows: false }); + expect(requestTotalCount(props)).toBe(false); + }); + }); + + describe("dynamicPageEnabled", () => { + it("is true when dynamicPage attribute exists", () => { + const props = makeProps({ dynamicPage: {} as any, pagination: "buttons" }); + expect(dynamicPageEnabled(props)).toBe(true); + }); + + it("stays true for limit-based pagination", () => { + const props = makeProps({ dynamicPage: {} as any, pagination: "virtualScrolling" }); + expect(dynamicPageEnabled(props)).toBe(true); + }); + + it("is false when no attribute provided", () => { + const props = makeProps({ pagination: "virtualScrolling" }); + expect(dynamicPageEnabled(props)).toBe(false); + }); + }); + + describe("dynamicPageSizeEnabled", () => { + it("is true when dynamicPageSize attribute exists", () => { + const props = makeProps({ dynamicPageSize: {} as any, pagination: "buttons" }); + expect(dynamicPageSizeEnabled(props)).toBe(true); + }); + + it("stays true for limit-based pagination", () => { + const props = makeProps({ dynamicPageSize: {} as any, pagination: "loadMore" }); + expect(dynamicPageSizeEnabled(props)).toBe(true); + }); + + it("is false when no attribute provided", () => { + const props = makeProps({ pagination: "loadMore" }); + expect(dynamicPageSizeEnabled(props)).toBe(false); + }); + }); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts index 9b66fc4efa..c0d6eb513a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts @@ -44,17 +44,25 @@ export function paginationKind(props: MainGateProps): PaginationKind { } export function dynamicPageSizeEnabled(props: MainGateProps): boolean { - return props.dynamicPageSize !== undefined && !isLimitBased(props); + // previously disabled for limit-based modes, but we now want the + // attribute available everywhere (buttons, virtual scroll, load more) + return props.dynamicPageSize !== undefined; } export function dynamicPageEnabled(props: MainGateProps): boolean { - return props.dynamicPage !== undefined && !isLimitBased(props); + // always allow dynamic page attribute regardless of pagination kind + return props.dynamicPage !== undefined; } -function isLimitBased(props: MainGateProps): boolean { +export function isLimitBased(props: MainGateProps): boolean { return props.pagination === "virtualScrolling" || props.pagination === "loadMore"; } -function requestTotalCount(props: MainGateProps): boolean { +export function requestTotalCount(props: MainGateProps): boolean { + // always ask for total count when user mapped an attribute, even in + // virtual or load-more modes. + if (props.totalCountValue !== undefined) { + return true; + } return props.pagination === "buttons" || props.showNumberOfRows; } From 4128b8f742feb4e7a0b100c455b67a41d125013b Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 14:50:46 +0100 Subject: [PATCH 4/9] feat(datagrid-web): implement custom pagination feature with dynamic page handling --- .../pagination/DynamicPagination.feature.ts | 53 +++++++- .../DynamicPagination.feature.spec.ts | 128 ++++++++++++++++++ .../__tests__/pagination.config.spec.ts | 6 +- .../model/containers/Datagrid.container.ts | 4 + 4 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/DynamicPagination.feature.spec.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts index dc71cd4b18..3796d155a4 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts @@ -1,15 +1,28 @@ -import { ComputedAtom, disposeBatch, SetupComponent, SetupComponentHost } from "@mendix/widget-plugin-mobx-kit/main"; +import { + ComputedAtom, + disposeBatch, + SetupComponent, + SetupComponentHost, + DerivedPropsGate +} from "@mendix/widget-plugin-mobx-kit/main"; import { autorun, reaction } from "mobx"; +import { Big } from "big.js"; +import { MainGateProps } from "../../../typings/MainGateProps"; import { GridPageControl } from "./GridPageControl"; +import { PaginationConfig } from "./pagination.config"; export class DynamicPaginationFeature implements SetupComponent { id = "DynamicPaginationFeature"; constructor( host: SetupComponentHost, - private config: { dynamicPageSizeEnabled: boolean; dynamicPageEnabled: boolean }, + private config: PaginationConfig, private dynamicPage: ComputedAtom, private dynamicPageSize: ComputedAtom, private totalCount: ComputedAtom, + private currentPage: ComputedAtom, + private pageSize: ComputedAtom, + private limit: ComputedAtom, + private gate: DerivedPropsGate, private service: GridPageControl ) { host.add(this); @@ -18,6 +31,7 @@ export class DynamicPaginationFeature implements SetupComponent { setup(): () => void { const [add, disposeAll] = disposeBatch(); + // inbound reactions (prop -> service) if (this.config.dynamicPageSizeEnabled) { add( reaction( @@ -49,6 +63,41 @@ export class DynamicPaginationFeature implements SetupComponent { ); } + // outbound autoruns (service/query state -> props) + add( + autorun(() => { + const attr = this.gate.props.dynamicPage; + if (!attr || attr.readOnly) return; + const val = this.currentPage.get(); + const pageValue = this.config.isLimitBased ? val : val + 1; + attr.setValue(new Big(pageValue)); + }) + ); + + add( + autorun(() => { + const attr = this.gate.props.dynamicPageSize; + if (!attr || attr.readOnly) return; + attr.setValue(new Big(this.pageSize.get())); + }) + ); + + add( + autorun(() => { + const attr = this.gate.props.totalCountValue; + if (!attr || attr.readOnly) return; + attr.setValue(new Big(this.totalCount.get())); + }) + ); + + add( + autorun(() => { + const attr = this.gate.props.loadedRowsValue; + if (!attr || attr.readOnly) return; + attr.setValue(new Big(this.limit.get())); + }) + ); + return disposeAll; } } diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/DynamicPagination.feature.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/DynamicPagination.feature.spec.ts new file mode 100644 index 0000000000..288d762076 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/DynamicPagination.feature.spec.ts @@ -0,0 +1,128 @@ +import { observable, computed, runInAction } from "mobx"; +import { DynamicPaginationFeature } from "../DynamicPagination.feature"; +import { PaginationConfig } from "../pagination.config"; +import { EditableValueBuilder } from "@mendix/widget-plugin-test-utils"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; +import { GridPageControl } from "../GridPageControl"; +import { Big } from "big.js"; + +// helper to create a simple computed atom along with a setter +function boxAtom(initial: number) { + const box = observable.box(initial); + return { + atom: computed(() => box.get()), + set: (v: number) => box.set(v) + }; +} + +describe("DynamicPaginationFeature sync", () => { + let config: PaginationConfig; + let service: jest.Mocked; + let gate: DerivedPropsGate; + let atoms: { + dynamicPage: ReturnType; + dynamicPageSize: ReturnType; + totalCount: ReturnType; + currentPage: ReturnType; + pageSize: ReturnType; + limit: ReturnType; + }; + // editable values used in outbound test + let dp: any; + let dps: any; + let tc: any; + let lr: any; + let dispose: () => void; + + beforeEach(() => { + jest.useFakeTimers(); + config = { + pagination: "buttons", + showPagingButtons: "always", + showNumberOfRows: false, + constPageSize: 10, + isLimitBased: false, + dynamicPageSizeEnabled: true, + dynamicPageEnabled: true, + customPaginationEnabled: false, + pagingPosition: "bottom", + requestTotalCount: true, + paginationKind: "buttons.always" + }; + service = { + setPage: jest.fn(), + setPageSize: jest.fn(), + setTotalCount: jest.fn() + } as any; + gate = { props: {} } as any; + + // create editable values for outbound sync and store in variables + dp = new EditableValueBuilder().build(); + dps = new EditableValueBuilder().build(); + tc = new EditableValueBuilder().build(); + lr = new EditableValueBuilder().build(); + gate.props.dynamicPage = dp; + gate.props.dynamicPageSize = dps; + gate.props.totalCountValue = tc; + gate.props.loadedRowsValue = lr; + + atoms = { + dynamicPage: boxAtom(-1), + dynamicPageSize: boxAtom(-1), + totalCount: boxAtom(0), + currentPage: boxAtom(0), + pageSize: boxAtom(10), + limit: boxAtom(0) + }; + const feature = new DynamicPaginationFeature( + { add: () => {} } as any, + config, + atoms.dynamicPage.atom, + atoms.dynamicPageSize.atom, + atoms.totalCount.atom, + atoms.currentPage.atom, + atoms.pageSize.atom, + atoms.limit.atom, + gate, + service + ); + dispose = feature.setup(); + }); + afterEach(() => { + dispose(); + jest.useRealTimers(); + }); + + test("inbound reactions call service", () => { + // use fake timers to advance past debounce delays + jest.advanceTimersByTime(250); + + // change observable by updating box (use action to satisfy strict-mode) + runInAction(() => atoms.dynamicPageSize.set(5)); + jest.advanceTimersByTime(250); + expect(service.setPageSize).toHaveBeenCalledWith(5); + + runInAction(() => atoms.dynamicPage.set(2)); + jest.advanceTimersByTime(250); + expect(service.setPage).toHaveBeenCalledWith(2); + + runInAction(() => atoms.totalCount.set(123)); + // total count uses autorun, no delay + expect(service.setTotalCount).toHaveBeenCalledWith(123); + }); + + test("outbound synchronises gate props", () => { + // props already assigned in beforeEach + runInAction(() => atoms.currentPage.set(3)); + expect(dp.setValue).toHaveBeenCalledWith(new Big(4)); // offset -> 1-based + + runInAction(() => atoms.pageSize.set(25)); + expect(dps.setValue).toHaveBeenCalledWith(new Big(25)); + + runInAction(() => atoms.totalCount.set(200)); + expect(tc.setValue).toHaveBeenCalledWith(new Big(200)); + + runInAction(() => atoms.limit.set(77)); + expect(lr.setValue).toHaveBeenCalledWith(new Big(77)); + }); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/pagination.config.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/pagination.config.spec.ts index 2fb340749b..321f491de5 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/pagination.config.spec.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/pagination.config.spec.ts @@ -1,5 +1,5 @@ -import { requestTotalCount, dynamicPageEnabled, dynamicPageSizeEnabled } from "../pagination.config"; -import { MainGateProps } from "../../../typings/MainGateProps"; +import { MainGateProps } from "../../../../typings/MainGateProps"; +import { dynamicPageEnabled, dynamicPageSizeEnabled, requestTotalCount } from "../pagination.config"; function makeProps(overrides: Partial = {}): MainGateProps { // minimal set required by config functions @@ -16,7 +16,7 @@ function makeProps(overrides: Partial = {}): MainGateProps { columns: [], filtersPlaceholder: {} as any, ...overrides - } as unknown as MainGateProps; + } as MainGateProps; } describe("pagination.config helpers", () => { diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts index 2493dfe82c..0fab05e214 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -66,6 +66,10 @@ injected( DG.dynamicPage, DG.dynamicPageSize, CORE.atoms.totalCount, + DG.currentPage, + DG.pageSize, + CORE.atoms.limit, + CORE.mainGate, DG.pageControl ); injected(customPaginationAtom, CORE.mainGate); From 7585588870720e2780e8089fe6a92513e9d9b49c Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 17:03:08 +0100 Subject: [PATCH 5/9] feat(datagrid-web): add tests for PageControlService methods and dynamic attribute mirroring --- .../pagination/PageControl.service.ts | 16 +++++++ .../__tests__/PageControl.service.spec.ts | 47 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/PageControl.service.spec.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/PageControl.service.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/PageControl.service.ts index 69c7cf9a3c..da657e8fb1 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/PageControl.service.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/PageControl.service.ts @@ -7,7 +7,10 @@ import { GridPageControl } from "./GridPageControl"; export class PageControlService implements GridPageControl { constructor( private gate: DerivedPropsGate<{ + dynamicPage?: EditableValue; + dynamicPageSize?: EditableValue; totalCountValue?: EditableValue; + loadedRowsValue?: EditableValue; }>, private setPageSizeAction: SetPageSizeAction, private setPageAction: SetPageAction @@ -15,10 +18,23 @@ export class PageControlService implements GridPageControl { setPageSize(pageSize: number): void { this.setPageSizeAction(pageSize); + + // mirror to editable attribute if mapped + const attr = this.gate.props.dynamicPageSize; + if (attr && !attr.readOnly) { + attr.setValue(new Big(pageSize)); + } } setPage(page: number): void { this.setPageAction(page); + + // mirror to editable attribute if mapped (offset-based pages are 1-based) + const attr = this.gate.props.dynamicPage; + if (attr && !attr.readOnly) { + // the grid itself uses 0-based page internally for buttons + attr.setValue(new Big(page + 1)); + } } setTotalCount(count: number): void { diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/PageControl.service.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/PageControl.service.spec.ts new file mode 100644 index 0000000000..7faa54c126 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/PageControl.service.spec.ts @@ -0,0 +1,47 @@ +import { PageControlService } from "../PageControl.service"; +import { EditableValueBuilder } from "@mendix/widget-plugin-test-utils"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; +import { Big } from "big.js"; + +describe("PageControlService", () => { + let gate: DerivedPropsGate; + let setPage: jest.Mock; + let setPageSize: jest.Mock; + let service: PageControlService; + + beforeEach(() => { + gate = { props: {} } as any; + setPage = jest.fn(); + setPageSize = jest.fn(); + service = new PageControlService(gate, setPageSize as any, setPage as any); + }); + + it("delegates setPageSize action and mirrors attribute when mapped", () => { + const attr = new EditableValueBuilder().build(); + gate.props.dynamicPageSize = attr; + + service.setPageSize(42); + expect(setPageSize).toHaveBeenCalledWith(42); + expect(attr.setValue).toHaveBeenCalledWith(new Big(42)); + }); + + it("delegates setPage action and mirrors dynamicPage attribute with +1 offset", () => { + const attr = new EditableValueBuilder().build(); + gate.props.dynamicPage = attr; + + service.setPage(3); + expect(setPage).toHaveBeenCalledWith(3); + expect(attr.setValue).toHaveBeenCalledWith(new Big(4)); + }); + + it("writes totalCountValue when requested", () => { + const attr = new EditableValueBuilder().build(); + gate.props.totalCountValue = attr; + service.setTotalCount(123); + expect(attr.setValue).toHaveBeenCalledWith(new Big(123)); + }); + + it("skips write when totalCountValue is missing or readOnly", () => { + service.setTotalCount(5); // nothing should throw + }); +}); From 03fd294e4c907c5209b5f1ff2b25ea999f265790 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 17:21:42 +0100 Subject: [PATCH 6/9] refactor(datagrid-web): reorganize imports and enhance type definitions in tests --- .../src/features/pagination/DynamicPagination.feature.ts | 4 ++-- .../pagination/__tests__/DynamicPagination.feature.spec.ts | 4 ++-- .../features/pagination/__tests__/PageControl.service.spec.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts index 3796d155a4..bb0f9d3599 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts @@ -1,9 +1,9 @@ import { ComputedAtom, + DerivedPropsGate, disposeBatch, SetupComponent, - SetupComponentHost, - DerivedPropsGate + SetupComponentHost } from "@mendix/widget-plugin-mobx-kit/main"; import { autorun, reaction } from "mobx"; import { Big } from "big.js"; diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/DynamicPagination.feature.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/DynamicPagination.feature.spec.ts index 288d762076..a75a8ac2af 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/DynamicPagination.feature.spec.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/DynamicPagination.feature.spec.ts @@ -1,4 +1,4 @@ -import { observable, computed, runInAction } from "mobx"; +import { computed, observable, runInAction } from "mobx"; import { DynamicPaginationFeature } from "../DynamicPagination.feature"; import { PaginationConfig } from "../pagination.config"; import { EditableValueBuilder } from "@mendix/widget-plugin-test-utils"; @@ -7,7 +7,7 @@ import { GridPageControl } from "../GridPageControl"; import { Big } from "big.js"; // helper to create a simple computed atom along with a setter -function boxAtom(initial: number) { +function boxAtom(initial: number): { atom: any; set: (v: number) => void } { const box = observable.box(initial); return { atom: computed(() => box.get()), diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/PageControl.service.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/PageControl.service.spec.ts index 7faa54c126..fa058922dd 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/PageControl.service.spec.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/__tests__/PageControl.service.spec.ts @@ -42,6 +42,6 @@ describe("PageControlService", () => { }); it("skips write when totalCountValue is missing or readOnly", () => { - service.setTotalCount(5); // nothing should throw + expect(() => service.setTotalCount(5)).not.toThrow(); // nothing should throw }); }); From b201384f6147c9420f4bd7cb9e2d137f913d617d Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 17:22:31 +0100 Subject: [PATCH 7/9] feat(datagrid-web): add pagination attribute synchronization tests for buttons and virtual scroll --- .../datagrid-web/e2e/DataGrid.spec.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js index 8a390bba23..cf6a3544ac 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js @@ -174,6 +174,42 @@ test.describe("manual column width", () => { }); }); +// pagination attributes sync tests +// pages referenced here must be created in the playground project with appropriate +// datagrids and Mendix attributes bound to textboxes named accordingly. +test.describe("pagination attribute synchronization", () => { + test("buttons pagination updates page/pageSize/totalCount attributes", async ({ page }) => { + await page.goto("/p/pagination-attributes"); + await page.waitForLoadState("networkidle"); + const pageInput = page.locator(".mx-name-PageTextBox input"); + const sizeInput = page.locator(".mx-name-PageSizeTextBox input"); + + // initial values should be populated + await expect(pageInput).toHaveValue("1"); + await expect(sizeInput).toHaveValue("10"); + + // navigate to next page via pagination control (adjust selector to match your layout) + await page.locator(".mx-name-dataGrid1 .pagination-next").click(); + await expect(pageInput).toHaveValue("2"); + }); + + test("virtual scroll pagination updates totalCount and loadedRows attributes", async ({ page }) => { + await page.goto("/p/virtual-scroll-attributes"); + await page.waitForLoadState("networkidle"); + const totalInput = page.locator(".mx-name-TotalCountTextBox input"); + const loadedInput = page.locator(".mx-name-LoadedRowsTextBox input"); + + // start empty + await expect(totalInput).toHaveValue(""); + await expect(loadedInput).toHaveValue("0"); + + // scroll down to trigger loading more rows + await page.locator(".mx-name-dataGrid21").scrollTo(0, 1000); + await expect(loadedInput).not.toHaveValue("0"); + await expect(totalInput).not.toHaveValue(""); + }); +}); + test.describe("visual testing:", () => { test("compares with a screenshot baseline and checks if all datagrid and filter elements are rendered as expected", async ({ page From 3593c87280630ade32fc90f263d5be20b866e729 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 17:23:08 +0100 Subject: [PATCH 8/9] chore(datagrid-web): update changelog --- packages/pluggableWidgets/datagrid-web/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 7144838bb3..652ad055af 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- Pagination attributes (`Page`, `Page Size`, `Total Count`) now sync correctly in all pagination modes and are exposed even when not using custom pagination. +- Added read-only `Loaded Rows` attribute for virtual scrolling and ensured the total count is requested whenever a `totalCountValue` attribute is mapped. +- Updated editor configuration to show/hide attributes appropriately across modes and cleaned up pagination config logic. + ## [3.8.1] - 2026-02-19 ### Fixed From 6dd4e1c614eec7f9392affcde035ecda19663d0b Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 26 Feb 2026 17:30:44 +0100 Subject: [PATCH 9/9] test(datagrid-web): enhance pagination tests with visibility checks and updated scrolling logic --- .../datagrid-web/e2e/DataGrid.spec.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js index cf6a3544ac..42a9864319 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js @@ -181,14 +181,17 @@ test.describe("pagination attribute synchronization", () => { test("buttons pagination updates page/pageSize/totalCount attributes", async ({ page }) => { await page.goto("/p/pagination-attributes"); await page.waitForLoadState("networkidle"); + await page.locator(".mx-name-dataGrid1").waitFor({ state: "visible" }); const pageInput = page.locator(".mx-name-PageTextBox input"); const sizeInput = page.locator(".mx-name-PageSizeTextBox input"); + const totalInput = page.locator(".mx-name-TotalCountTextBox input"); // initial values should be populated await expect(pageInput).toHaveValue("1"); await expect(sizeInput).toHaveValue("10"); + await expect(totalInput).not.toHaveValue(""); - // navigate to next page via pagination control (adjust selector to match your layout) + // navigate to next page via pagination control await page.locator(".mx-name-dataGrid1 .pagination-next").click(); await expect(pageInput).toHaveValue("2"); }); @@ -196,6 +199,7 @@ test.describe("pagination attribute synchronization", () => { test("virtual scroll pagination updates totalCount and loadedRows attributes", async ({ page }) => { await page.goto("/p/virtual-scroll-attributes"); await page.waitForLoadState("networkidle"); + await expect(page.locator(".mx-name-dataGrid21")).toBeVisible(); const totalInput = page.locator(".mx-name-TotalCountTextBox input"); const loadedInput = page.locator(".mx-name-LoadedRowsTextBox input"); @@ -204,7 +208,10 @@ test.describe("pagination attribute synchronization", () => { await expect(loadedInput).toHaveValue("0"); // scroll down to trigger loading more rows - await page.locator(".mx-name-dataGrid21").scrollTo(0, 1000); + await page.evaluate(() => { + const el = document.querySelector(".mx-name-dataGrid21"); + if (el) el.scrollTop = 1000; + }); await expect(loadedInput).not.toHaveValue("0"); await expect(totalInput).not.toHaveValue(""); });