From 5d430a6a223e3e8b2c081c7d0324b0c8bbb82f0d Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:24:13 +0100 Subject: [PATCH 1/4] refactor: split pagination service responsibilities --- .../datagrid-web/src/Datagrid.xml | 7 +++++++ .../src/model/configs/Datagrid.config.ts | 16 +++++++++++++++- .../src/model/containers/Datagrid.container.ts | 11 +++++++---- ...nationController.ts => Pagination.service.ts} | 16 +--------------- .../datagrid-web/src/model/tokens.ts | 4 ++-- .../datagrid-web/typings/DatagridProps.d.ts | 2 ++ .../datagrid-web/typings/MainGateProps.ts | 1 + 7 files changed, 35 insertions(+), 22 deletions(-) rename packages/pluggableWidgets/datagrid-web/src/model/services/{PaginationController.ts => Pagination.service.ts} (81%) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index 1366fbe612..2ffd4d9834 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -301,6 +301,13 @@ Both + + Dynamic page size attribute + Optional attribute to set the page size dynamically. The attribute requires Integer type. + + + + Load more caption diff --git a/packages/pluggableWidgets/datagrid-web/src/model/configs/Datagrid.config.ts b/packages/pluggableWidgets/datagrid-web/src/model/configs/Datagrid.config.ts index 1ce8da4b69..c9faffff76 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/configs/Datagrid.config.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/configs/Datagrid.config.ts @@ -28,6 +28,9 @@ export interface DatagridConfig { columnsResizable: boolean; columnsSortable: boolean; isInteractive: boolean; + dynamicPageSizeEnabled: boolean; + requestTotalCount: boolean; + constPageSize: number; } export function datagridConfig(props: DatagridContainerProps): DatagridConfig { @@ -56,7 +59,10 @@ export function datagridConfig(props: DatagridContainerProps): DatagridConfig { columnsFilterable: props.columnsFilterable, columnsResizable: props.columnsResizable, columnsSortable: props.columnsSortable, - isInteractive: isInteractive(props) + isInteractive: isInteractive(props), + dynamicPageSizeEnabled: dynamicPageSizeEnabled(props), + requestTotalCount: requestTotalCount(props), + constPageSize: props.pageSize }; return Object.freeze(config); @@ -93,3 +99,11 @@ function selectionType(props: DatagridContainerProps): SelectionType { function selectionMethod(props: DatagridContainerProps): SelectionMethod { return props.itemSelection ? props.itemSelectionMethod : "none"; } + +function dynamicPageSizeEnabled(props: DatagridContainerProps): boolean { + return props.dynamicPageSize !== undefined; +} + +function requestTotalCount(props: DatagridContainerProps): boolean { + return props.pagination === "buttons" || props.showNumberOfRows; +} 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 fd99730e77..cb5663c3cc 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -29,7 +29,7 @@ import { gridStyleAtom } from "../models/grid.model"; import { rowClassProvider } from "../models/rows.model"; import { DatasourceParamsController } from "../services/DatasourceParamsController"; import { DerivedLoaderController } from "../services/DerivedLoaderController"; -import { PaginationController } from "../services/PaginationController"; +import { PaginationService } from "../services/Pagination.service"; import { SelectionGate } from "../services/SelectionGate.service"; import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens"; @@ -37,7 +37,7 @@ import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens"; injected(ColumnGroupStore, CORE.setupService, CORE.mainGate, CORE.config, DG.filterHost); injected(DatasourceParamsController, CORE.setupService, DG.query, DG.combinedFilter, CORE.columnsStore); injected(DatasourceService, CORE.setupService, DG.queryGate, DG.refreshInterval.optional); -injected(PaginationController, CORE.setupService, DG.paginationConfig, DG.query); +injected(PaginationService, DG.paginationConfig, DG.query); injected(GridBasicData, CORE.mainGate); injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.selectionDialogVM); @@ -97,7 +97,7 @@ export class DatagridContainer extends Container { // Query service this.bind(DG.query).toInstance(DatasourceService).inSingletonScope(); // Pagination service - this.bind(DG.paginationService).toInstance(PaginationController).inSingletonScope(); + this.bind(DG.paginationService).toInstance(PaginationService).inSingletonScope(); // Datasource params service this.bind(DG.paramsService).toInstance(DatasourceParamsController).inSingletonScope(); // FilterAPI @@ -213,7 +213,10 @@ export class DatagridContainer extends Container { private postInit(props: MainGateProps, config: DatagridConfig): void { // Make sure essential services are created upfront this.get(DG.paramsService); - this.get(DG.paginationService); + + const query = this.get(DG.query); + query.requestTotalCount(config.requestTotalCount); + query.setBaseLimit(config.constPageSize); if (config.settingsStorageEnabled) { this.get(DG.personalizationService); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/services/PaginationController.ts b/packages/pluggableWidgets/datagrid-web/src/model/services/Pagination.service.ts similarity index 81% rename from packages/pluggableWidgets/datagrid-web/src/model/services/PaginationController.ts rename to packages/pluggableWidgets/datagrid-web/src/model/services/Pagination.service.ts index 8f2baff57c..27ec947d56 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/services/PaginationController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/services/Pagination.service.ts @@ -1,5 +1,4 @@ import { QueryService } from "@mendix/widget-plugin-grid/main"; -import { SetupComponent, SetupComponentHost } from "@mendix/widget-plugin-mobx-kit/main"; import { PaginationEnum, ShowPagingButtonsEnum } from "../../../typings/DatagridProps"; export interface PaginationConfig { @@ -11,21 +10,18 @@ export interface PaginationConfig { type PaginationKind = `${PaginationEnum}.${ShowPagingButtonsEnum}`; -export class PaginationController implements SetupComponent { +export class PaginationService { readonly pagination: PaginationEnum; readonly paginationKind: PaginationKind; readonly showPagingButtons: ShowPagingButtonsEnum; constructor( - host: SetupComponentHost, private config: PaginationConfig, private query: QueryService ) { - host.add(this); this.pagination = config.pagination; this.paginationKind = `${this.pagination}.${config.showPagingButtons}`; this.showPagingButtons = config.showPagingButtons; - this.setInitParams(); } get isLimitBased(): boolean { @@ -65,16 +61,6 @@ export class PaginationController implements SetupComponent { return this.query.totalCount; } - private setInitParams(): void { - if (this.pagination === "buttons" || this.config.showNumberOfRows) { - this.query.requestTotalCount(true); - } - - this.query.setBaseLimit(this.pageSize); - } - - setup(): void {} - setPage = (computePage: ((prevPage: number) => number) | number): void => { const newPage = typeof computePage === "function" ? computePage(this.currentPage) : computePage; if (this.isLimitBased) { diff --git a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index 26b3e38201..c5f71eee71 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -39,7 +39,7 @@ import { DatagridConfig } from "./configs/Datagrid.config"; import { RowClassProvider } from "./models/rows.model"; import { DatagridSetupService } from "./services/DatagridSetup.service"; import { DerivedLoaderController, DerivedLoaderControllerConfig } from "./services/DerivedLoaderController"; -import { PaginationConfig, PaginationController } from "./services/PaginationController"; +import { PaginationConfig, PaginationService } from "./services/Pagination.service"; import { TextsService } from "./services/Texts.service"; import { PageSizeStore } from "./stores/PageSize.store"; @@ -102,7 +102,7 @@ export const DG_TOKENS = { loaderVM: token("DatagridLoaderViewModel"), paginationConfig: token("PaginationConfig"), - paginationService: token("PaginationService"), + paginationService: token("PaginationService"), parentChannelName: token("parentChannelName"), refreshInterval: token("refreshInterval"), diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index 056c487def..0e60455b30 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -120,6 +120,7 @@ export interface DatagridContainerProps { showPagingButtons: ShowPagingButtonsEnum; showNumberOfRows: boolean; pagingPosition: PagingPositionEnum; + dynamicPageSize?: EditableValue; loadMoreButtonCaption?: DynamicValue; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder?: ReactNode; @@ -179,6 +180,7 @@ export interface DatagridPreviewProps { showPagingButtons: ShowPagingButtonsEnum; showNumberOfRows: boolean; pagingPosition: PagingPositionEnum; + dynamicPageSize: string; loadMoreButtonCaption: string; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; diff --git a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts index 5e93b89aea..9ed729a9e4 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts @@ -37,4 +37,5 @@ export type MainGateProps = Pick< | "showPagingButtons" | "storeFiltersInPersonalization" | "style" + | "dynamicPageSize" >; From ddfc141a683a738be88f5c34ba1f741e4c842c29 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:33:52 +0100 Subject: [PATCH 2/4] feat: dynamic pagination --- .../datagrid-web/CHANGELOG.md | 2 + .../datagrid-web/src/Datagrid.editorConfig.ts | 13 ++++ .../datagrid-web/src/Datagrid.xml | 36 +++++++-- .../datagrid-web/src/components/GridBody.tsx | 4 +- .../src/components/Pagination.tsx | 4 +- .../src/components/WidgetFooter.tsx | 4 +- .../pagination/DynamicPagination.feature.ts | 54 +++++++++++++ .../features/pagination/GridPageControl.ts | 5 ++ .../pagination/PageControl.service.ts | 29 +++++++ .../pagination/Pagination.viewModel.ts | 65 ++++++++++++++++ .../features/pagination/pagination.config.ts | 50 ++++++++++++ .../features/pagination/pagination.model.ts | 32 ++++++++ .../src/model/configs/Datagrid.config.ts | 6 -- .../model/containers/Datagrid.container.ts | 67 +++++++++++----- .../src/model/containers/Root.container.ts | 4 +- .../src/model/hooks/injection-hooks.ts | 2 +- .../src/model/hooks/useBodyScroll.ts | 4 +- .../src/model/models/paging.model.ts | 6 -- .../src/model/services/Pagination.service.ts | 72 ----------------- .../datagrid-web/src/model/tokens.ts | 19 ++++- .../datagrid-web/typings/DatagridProps.d.ts | 12 ++- .../datagrid-web/typings/MainGateProps.ts | 6 +- .../shared/widget-plugin-grid/src/main.ts | 2 + .../src/pagination/PageSize.store.ts | 14 ++++ .../src/pagination/pagination.model.ts | 77 +++++++++++++++++++ 25 files changed, 458 insertions(+), 131 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/GridPageControl.ts create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/PageControl.service.ts create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/Pagination.viewModel.ts create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts delete mode 100644 packages/pluggableWidgets/datagrid-web/src/model/models/paging.model.ts delete mode 100644 packages/pluggableWidgets/datagrid-web/src/model/services/Pagination.service.ts create mode 100644 packages/shared/widget-plugin-grid/src/pagination/PageSize.store.ts create mode 100644 packages/shared/widget-plugin-grid/src/pagination/pagination.model.ts diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 1f7339aa51..51fd72e4b5 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We added a new property for export to excel. The new property allows to set the cell export type and also the format for type number and date. +- We have introduced the "Page" and "Page Size" attributes to provide complete control over DataGrid pagination. + ## [3.7.0] - 2025-11-11 ### Added diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts index b58eaf95c2..ae1e5cfb62 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts @@ -72,12 +72,25 @@ export function getProperties(values: DatagridPreviewProps, defaultProperties: P if (values.pagination === "buttons") { hidePropertyIn(defaultProperties, values, "showNumberOfRows"); + + if (values.useCustomPagination === false) { + hidePropertyIn(defaultProperties, values, "customPagination"); + } else { + hidePropertiesIn(defaultProperties, values, ["pagingPosition", "showPagingButtons"]); + } } else { hidePropertyIn(defaultProperties, values, "showPagingButtons"); if (values.showNumberOfRows === false) { hidePropertyIn(defaultProperties, values, "pagingPosition"); } + + hidePropertiesIn(defaultProperties, values, [ + "dynamicPage", + "dynamicPageSize", + "useCustomPagination", + "customPagination" + ]); } if (values.pagination !== "loadMore") { diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index 2ffd4d9834..babd4678e6 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -280,6 +280,14 @@ Load more + + Custom pagination + + + + Custom pagination + + Show paging buttons @@ -301,13 +309,6 @@ Both - - Dynamic page size attribute - Optional attribute to set the page size dynamically. The attribute requires Integer type. - - - - Load more caption @@ -315,6 +316,27 @@ Load More + + Page size attribute + Attribute to set the page size dynamically. + + + + + + Page attribute + Attribute to set the page dynamically. + + + + + + Total count + Attribute to store current total count + + + + diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx index a8ededee80..7be72dd1fe 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx @@ -5,7 +5,7 @@ import { useDatagridConfig, useItemCount, useLoaderViewModel, - usePaginationService, + usePaginationVM, useVisibleColumnsCount } from "../model/hooks/injection-hooks"; import { useBodyScroll } from "../model/hooks/useBodyScroll"; @@ -31,7 +31,7 @@ export const GridBody = observer(function GridBody(props: PropsWithChildren): Re const ContentGuard = observer(function ContentGuard(props: PropsWithChildren): ReactNode { const loaderVM = useLoaderViewModel(); - const { pageSize } = usePaginationService(); + const { pageSize } = usePaginationVM(); const config = useDatagridConfig(); const columnsCount = useVisibleColumnsCount().get(); const itemCount = useItemCount().get(); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Pagination.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Pagination.tsx index fb8200c6d8..0068a2b798 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Pagination.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Pagination.tsx @@ -1,10 +1,10 @@ import { Pagination as PaginationComponent } from "@mendix/widget-plugin-grid/components/Pagination"; import { observer } from "mobx-react-lite"; import { ReactNode } from "react"; -import { usePaginationService } from "../model/hooks/injection-hooks"; +import { usePaginationVM } from "../model/hooks/injection-hooks"; export const Pagination = observer(function Pagination(): ReactNode { - const paging = usePaginationService(); + const paging = usePaginationVM(); if (!paging.paginationVisible) return null; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx index 4f5211c4b7..f1417c1aa8 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx @@ -3,12 +3,12 @@ import { observer } from "mobx-react-lite"; import { ReactElement } from "react"; import { SelectionCounter } from "../features/selection-counter/SelectionCounter"; import { useSelectionCounterViewModel } from "../features/selection-counter/injection-hooks"; -import { useDatagridConfig, usePaginationService, useTexts } from "../model/hooks/injection-hooks"; +import { useDatagridConfig, usePaginationVM, useTexts } from "../model/hooks/injection-hooks"; import { Pagination } from "./Pagination"; export const WidgetFooter = observer(function WidgetFooter(): ReactElement { const config = useDatagridConfig(); - const paging = usePaginationService(); + const paging = usePaginationVM(); const { loadMoreButtonCaption } = useTexts(); const selectionCounterVM = useSelectionCounterViewModel(); diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts new file mode 100644 index 0000000000..dc71cd4b18 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/DynamicPagination.feature.ts @@ -0,0 +1,54 @@ +import { ComputedAtom, disposeBatch, SetupComponent, SetupComponentHost } from "@mendix/widget-plugin-mobx-kit/main"; +import { autorun, reaction } from "mobx"; +import { GridPageControl } from "./GridPageControl"; + +export class DynamicPaginationFeature implements SetupComponent { + id = "DynamicPaginationFeature"; + constructor( + host: SetupComponentHost, + private config: { dynamicPageSizeEnabled: boolean; dynamicPageEnabled: boolean }, + private dynamicPage: ComputedAtom, + private dynamicPageSize: ComputedAtom, + private totalCount: ComputedAtom, + private service: GridPageControl + ) { + host.add(this); + } + + setup(): () => void { + const [add, disposeAll] = disposeBatch(); + + if (this.config.dynamicPageSizeEnabled) { + add( + reaction( + () => this.dynamicPageSize.get(), + pageSize => { + if (pageSize < 0) return; + this.service.setPageSize(pageSize); + }, + { delay: 250 } + ) + ); + } + + if (this.config.dynamicPageEnabled) { + add( + reaction( + () => this.dynamicPage.get(), + page => { + if (page < 0) return; + this.service.setPage(page); + }, + { delay: 250 } + ) + ); + add( + autorun(() => { + this.service.setTotalCount(this.totalCount.get()); + }) + ); + } + + return disposeAll; + } +} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/GridPageControl.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/GridPageControl.ts new file mode 100644 index 0000000000..27008c6d17 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/GridPageControl.ts @@ -0,0 +1,5 @@ +export interface GridPageControl { + setPage(page: number): void; + setPageSize(pageSize: number): void; + setTotalCount(totalCount: number): void; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/PageControl.service.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/PageControl.service.ts new file mode 100644 index 0000000000..69c7cf9a3c --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/PageControl.service.ts @@ -0,0 +1,29 @@ +import { SetPageAction, SetPageSizeAction } from "@mendix/widget-plugin-grid/main"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; +import { Big } from "big.js"; +import { EditableValue } from "mendix"; +import { GridPageControl } from "./GridPageControl"; + +export class PageControlService implements GridPageControl { + constructor( + private gate: DerivedPropsGate<{ + totalCountValue?: EditableValue; + }>, + private setPageSizeAction: SetPageSizeAction, + private setPageAction: SetPageAction + ) {} + + setPageSize(pageSize: number): void { + this.setPageSizeAction(pageSize); + } + + setPage(page: number): void { + this.setPageAction(page); + } + + setTotalCount(count: number): void { + const value = this.gate.props.totalCountValue; + if (!value || value.readOnly) return; + value.setValue(new Big(count)); + } +} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/Pagination.viewModel.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/Pagination.viewModel.ts new file mode 100644 index 0000000000..73dbacb148 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/Pagination.viewModel.ts @@ -0,0 +1,65 @@ +import { QueryService, SetPageAction } from "@mendix/widget-plugin-grid/main"; +import { ComputedAtom } from "@mendix/widget-plugin-mobx-kit/main"; +import { computed, makeObservable } from "mobx"; +import { PaginationEnum, ShowPagingButtonsEnum } from "../../../typings/DatagridProps"; +import { PaginationConfig } from "./pagination.config"; + +export class PaginationViewModel { + readonly pagination: PaginationEnum; + readonly showPagingButtons: ShowPagingButtonsEnum; + + constructor( + private config: PaginationConfig, + private query: QueryService, + private currentPageAtom: ComputedAtom, + private pageSizeAtom: ComputedAtom, + private setPageAction: SetPageAction + ) { + this.pagination = config.pagination; + this.showPagingButtons = config.showPagingButtons; + + makeObservable(this, { + pageSize: computed, + currentPage: computed, + paginationVisible: computed, + hasMoreItems: computed, + totalCount: computed + }); + } + + get pageSize(): number { + return this.pageSizeAtom.get(); + } + + get currentPage(): number { + return this.currentPageAtom.get(); + } + + get paginationVisible(): boolean { + switch (this.config.paginationKind) { + case "buttons.always": + return true; + case "buttons.auto": { + const { totalCount = -1 } = this.query; + return totalCount > this.query.limit; + } + case "custom": { + return false; + } + default: + return this.config.showNumberOfRows; + } + } + + get hasMoreItems(): boolean { + return this.query.hasMoreItems; + } + + get totalCount(): number | undefined { + return this.query.totalCount; + } + + setPage: SetPageAction = value => { + this.setPageAction(value); + }; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts new file mode 100644 index 0000000000..0bafe6aac6 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts @@ -0,0 +1,50 @@ +import { PaginationEnum, ShowPagingButtonsEnum } from "../../../typings/DatagridProps"; +import { MainGateProps } from "../../../typings/MainGateProps"; + +export interface PaginationConfig { + pagination: PaginationEnum; + paginationKind: PaginationKind; + showPagingButtons: ShowPagingButtonsEnum; + showNumberOfRows: boolean; + pageSize: number; + isLimitBased: boolean; + dynamicPageSizeEnabled: boolean; + dynamicPageEnabled: boolean; +} + +export type PaginationKind = `${PaginationEnum}.${ShowPagingButtonsEnum}` | "custom"; + +export function paginationConfig(props: MainGateProps): PaginationConfig { + const config: PaginationConfig = { + pagination: props.pagination, + showPagingButtons: props.showPagingButtons, + showNumberOfRows: props.showNumberOfRows, + pageSize: props.pageSize, + isLimitBased: isLimitBased(props), + paginationKind: paginationKind(props), + dynamicPageSizeEnabled: dynamicPageSizeEnabled(props), + dynamicPageEnabled: dynamicPageEnabled(props) + }; + + return Object.freeze(config); +} + +export function paginationKind(props: MainGateProps): PaginationKind { + if (props.useCustomPagination) { + return "custom"; + } + + return `${props.pagination}.${props.showPagingButtons}`; +} + +export function dynamicPageSizeEnabled(props: MainGateProps): boolean { + return props.dynamicPageSize !== undefined && !isLimitBased(props); +} + +export function dynamicPageEnabled(props: MainGateProps): boolean { + return props.dynamicPage !== undefined && !isLimitBased(props); +} + +function isLimitBased(props: MainGateProps): boolean { + return props.pagination === "virtualScrolling" || props.pagination === "loadMore"; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts new file mode 100644 index 0000000000..7d19cf3143 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts @@ -0,0 +1,32 @@ +import { boundPageSize } from "@mendix/widget-plugin-grid/main"; +import { ComputedAtom, DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; +import { computed } from "mobx"; +import { ReactNode } from "react"; + +/** Atom for the dynamic page index provided by the widget's props. */ +export function dynamicPageAtom( + gate: DerivedPropsGate<{ dynamicPage?: { value?: Big } }>, + config: { isLimitBased: boolean } +): ComputedAtom { + return computed(() => { + const page = gate.props.dynamicPage?.value?.toNumber() ?? -1; + if (config.isLimitBased) { + return Math.max(page, -1); + } + // Switch to zero-based index for offset-based pagination + return Math.max(page - 1, -1); + }); +} + +/** Atom for the dynamic page size. */ +export function dynamicPageSizeAtom( + gate: DerivedPropsGate<{ dynamicPageSize?: { value?: Big } }> +): ComputedAtom { + return boundPageSize(() => gate.props.dynamicPageSize?.value?.toNumber() ?? -1); +} + +export function customPaginationAtom( + gate: DerivedPropsGate<{ customPagination?: ReactNode }> +): ComputedAtom { + return computed(() => gate.props.customPagination); +} diff --git a/packages/pluggableWidgets/datagrid-web/src/model/configs/Datagrid.config.ts b/packages/pluggableWidgets/datagrid-web/src/model/configs/Datagrid.config.ts index c9faffff76..d59fdc5f03 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/configs/Datagrid.config.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/configs/Datagrid.config.ts @@ -28,7 +28,6 @@ export interface DatagridConfig { columnsResizable: boolean; columnsSortable: boolean; isInteractive: boolean; - dynamicPageSizeEnabled: boolean; requestTotalCount: boolean; constPageSize: number; } @@ -60,7 +59,6 @@ export function datagridConfig(props: DatagridContainerProps): DatagridConfig { columnsResizable: props.columnsResizable, columnsSortable: props.columnsSortable, isInteractive: isInteractive(props), - dynamicPageSizeEnabled: dynamicPageSizeEnabled(props), requestTotalCount: requestTotalCount(props), constPageSize: props.pageSize }; @@ -100,10 +98,6 @@ function selectionMethod(props: DatagridContainerProps): SelectionMethod { return props.itemSelection ? props.itemSelectionMethod : "none"; } -function dynamicPageSizeEnabled(props: DatagridContainerProps): boolean { - return props.dynamicPageSize !== undefined; -} - function requestTotalCount(props: DatagridContainerProps): boolean { return props.pagination === "buttons" || props.showNumberOfRows; } 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 cb5663c3cc..dd9db07605 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -6,8 +6,12 @@ import { createClickActionHelper, createFocusController, createSelectionHelper, + createSetPageAction, + createSetPageSizeAction, + currentPageAtom, DatasourceService, layoutAtom, + pageSizeAtom, SelectActionsProvider, TaskProgressService } from "@mendix/widget-plugin-grid/main"; @@ -18,6 +22,11 @@ import { Container, injected } from "brandi"; import { MainGateProps } from "../../../typings/MainGateProps"; import { WidgetRootViewModel } from "../../features/base/WidgetRoot.viewModel"; import { EmptyPlaceholderViewModel } from "../../features/empty-message/EmptyPlaceholder.viewModel"; +import { DynamicPaginationFeature } from "../../features/pagination/DynamicPagination.feature"; +import { PageControlService } from "../../features/pagination/PageControl.service"; +import { paginationConfig } from "../../features/pagination/pagination.config"; +import { dynamicPageAtom, dynamicPageSizeAtom } from "../../features/pagination/pagination.model"; +import { PaginationViewModel } from "../../features/pagination/Pagination.viewModel"; import { createCellEventsController } from "../../features/row-interaction/CellEventsController"; import { creteCheckboxEventsController } from "../../features/row-interaction/CheckboxEventsController"; import { SelectAllModule } from "../../features/select-all/SelectAllModule.container"; @@ -29,7 +38,6 @@ import { gridStyleAtom } from "../models/grid.model"; import { rowClassProvider } from "../models/rows.model"; import { DatasourceParamsController } from "../services/DatasourceParamsController"; import { DerivedLoaderController } from "../services/DerivedLoaderController"; -import { PaginationService } from "../services/Pagination.service"; import { SelectionGate } from "../services/SelectionGate.service"; import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens"; @@ -37,10 +45,28 @@ import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens"; injected(ColumnGroupStore, CORE.setupService, CORE.mainGate, CORE.config, DG.filterHost); injected(DatasourceParamsController, CORE.setupService, DG.query, DG.combinedFilter, CORE.columnsStore); injected(DatasourceService, CORE.setupService, DG.queryGate, DG.refreshInterval.optional); -injected(PaginationService, DG.paginationConfig, DG.query); injected(GridBasicData, CORE.mainGate); injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.selectionDialogVM); +/** Pagination **/ +injected(createSetPageAction, DG.query, DG.paginationConfig, DG.currentPage, DG.pageSize); +injected(createSetPageSizeAction, DG.query, DG.paginationConfig, DG.currentPage, CORE.pageSizeStore, DG.setPageAction); +injected(currentPageAtom, DG.query, DG.pageSize, DG.paginationConfig); +injected(dynamicPageAtom, CORE.mainGate, DG.paginationConfig); +injected(dynamicPageSizeAtom, CORE.mainGate); +injected(PageControlService, CORE.mainGate, DG.setPageSizeAction, DG.setPageAction); +injected(pageSizeAtom, CORE.pageSizeStore); +injected(PaginationViewModel, DG.paginationConfig, DG.query, DG.currentPage, DG.pageSize, DG.setPageAction); +injected( + DynamicPaginationFeature, + CORE.setupService, + DG.paginationConfig, + DG.dynamicPage, + DG.dynamicPageSize, + CORE.atoms.totalCount, + DG.pageControl +); + // loader injected(DerivedLoaderController, DG.query, DG.exportProgressService, CORE.columnsStore, DG.loaderConfig); @@ -64,17 +90,10 @@ injected(rowClassProvider, CORE.mainGate); // row-interaction injected(SelectActionsProvider, DG.selectionType, DG.selectionHelper); injected(createFocusController, CORE.setupService, DG.virtualLayout); -injected(creteCheckboxEventsController, CORE.config, DG.selectActions, DG.focusService, CORE.atoms.pageSize); -injected(layoutAtom, CORE.atoms.itemCount, CORE.atoms.columnCount, CORE.atoms.pageSize); +injected(creteCheckboxEventsController, CORE.config, DG.selectActions, DG.focusService, DG.pageSize); +injected(layoutAtom, CORE.atoms.itemCount, CORE.atoms.columnCount, DG.pageSize); injected(createClickActionHelper, CORE.setupService, CORE.mainGate); -injected( - createCellEventsController, - CORE.config, - DG.selectActions, - DG.focusService, - DG.clickActionHelper, - CORE.atoms.pageSize -); +injected(createCellEventsController, CORE.config, DG.selectActions, DG.focusService, DG.clickActionHelper, DG.pageSize); // selection counter injected( @@ -96,8 +115,6 @@ export class DatagridContainer extends Container { this.bind(CORE.columnsStore).toInstance(ColumnGroupStore).inSingletonScope(); // Query service this.bind(DG.query).toInstance(DatasourceService).inSingletonScope(); - // Pagination service - this.bind(DG.paginationService).toInstance(PaginationService).inSingletonScope(); // Datasource params service this.bind(DG.paramsService).toInstance(DatasourceParamsController).inSingletonScope(); // FilterAPI @@ -118,6 +135,17 @@ export class DatagridContainer extends Container { // Grid columns style this.bind(DG.gridColumnsStyle).toInstance(gridStyleAtom).inTransientScope(); + /** Pagination **/ + this.bind(DG.currentPage).toInstance(currentPageAtom).inTransientScope(); + this.bind(DG.dynamicPage).toInstance(dynamicPageAtom).inTransientScope(); + this.bind(DG.dynamicPageSize).toInstance(dynamicPageSizeAtom).inTransientScope(); + this.bind(DG.dynamicPagination).toInstance(DynamicPaginationFeature).inSingletonScope(); + this.bind(DG.pageSize).toInstance(pageSizeAtom).inTransientScope(); + this.bind(DG.pageControl).toInstance(PageControlService).inSingletonScope(); + this.bind(DG.paginationVM).toInstance(PaginationViewModel).inSingletonScope(); + this.bind(DG.setPageAction).toInstance(createSetPageAction).inSingletonScope(); + this.bind(DG.setPageSizeAction).toInstance(createSetPageSizeAction).inSingletonScope(); + // Selection gate this.bind(DG.selectionGate).toInstance(SelectionGate).inTransientScope(); // Selection helper @@ -188,12 +216,8 @@ export class DatagridContainer extends Container { }); // Bind pagination config - this.bind(DG.paginationConfig).toConstant({ - pagination: props.pagination, - showPagingButtons: props.showPagingButtons, - showNumberOfRows: props.showNumberOfRows, - pageSize: props.pageSize - }); + + this.bind(DG.paginationConfig).toConstant(paginationConfig(props)); // Bind init page size this.bind(CORE.initPageSize).toConstant(props.pageSize); @@ -212,7 +236,8 @@ export class DatagridContainer extends Container { /** Post init hook for final configuration. */ private postInit(props: MainGateProps, config: DatagridConfig): void { // Make sure essential services are created upfront - this.get(DG.paramsService); + this.get(DG.paramsService); // Enable sort & filtering + this.get(DG.dynamicPagination); // Enable dynamic pagination feature const query = this.get(DG.query); query.requestTotalCount(config.requestTotalCount); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts index 30ee19b4b9..d62d242228 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Root.container.ts @@ -14,8 +14,8 @@ import { } from "@mendix/widget-plugin-grid/core/models/selection.model"; import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; import { Container, injected } from "brandi"; + import { columnCount, visibleColumnsCountAtom } from "../models/columns.model"; -import { pageSizeAtom } from "../models/paging.model"; import { rowsAtom } from "../models/rows.model"; import { DatagridSetupService } from "../services/DatagridSetup.service"; import { TextsService } from "../services/Texts.service"; @@ -31,7 +31,6 @@ injected(hasMoreItemsAtom, CORE.mainGate); injected(visibleColumnsCountAtom, CORE.columnsStore); injected(isAllItemsPresentAtom, CORE.atoms.offset, CORE.atoms.hasMoreItems); injected(rowsAtom, CORE.mainGate); -injected(pageSizeAtom, CORE.pageSizeStore); injected(columnCount, CORE.atoms.visibleColumnsCount, CORE.config); // selection @@ -84,7 +83,6 @@ export class RootContainer extends Container { this.bind(CORE.texts).toInstance(TextsService).inTransientScope(); // paging - this.bind(CORE.atoms.pageSize).toInstance(pageSizeAtom).inTransientScope(); this.bind(CORE.pageSizeStore).toInstance(PageSizeStore).inSingletonScope(); } } diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts index 337a03637f..78688c4c48 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -8,7 +8,7 @@ export const [useDatagridFilterAPI] = createInjectionHooks(DG.filterAPI); export const [useExportProgressService] = createInjectionHooks(DG.exportProgressService); export const [useLoaderViewModel] = createInjectionHooks(DG.loaderVM); export const [useMainGate] = createInjectionHooks(CORE.mainGate); -export const [usePaginationService] = createInjectionHooks(DG.paginationService); +export const [usePaginationVM] = createInjectionHooks(DG.paginationVM); export const [useSelectionHelper] = createInjectionHooks(DG.selectionHelper); export const [useGridStyle] = createInjectionHooks(DG.gridColumnsStyle); export const [useQueryService] = createInjectionHooks(DG.query); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/useBodyScroll.ts b/packages/pluggableWidgets/datagrid-web/src/model/hooks/useBodyScroll.ts index e5fd2eed3a..9ecc7b8c41 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/useBodyScroll.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/useBodyScroll.ts @@ -1,6 +1,6 @@ import { useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; import { RefObject, UIEventHandler, useCallback } from "react"; -import { usePaginationService } from "./injection-hooks"; +import { usePaginationVM } from "./injection-hooks"; export function useBodyScroll(): { handleScroll: UIEventHandler | undefined; @@ -8,7 +8,7 @@ export function useBodyScroll(): { containerRef: RefObject; isInfinite: boolean; } { - const paging = usePaginationService(); + const paging = usePaginationVM(); const setPage = useCallback((cb: (n: number) => number) => paging.setPage(cb), [paging]); const isInfinite = paging.pagination === "virtualScrolling"; diff --git a/packages/pluggableWidgets/datagrid-web/src/model/models/paging.model.ts b/packages/pluggableWidgets/datagrid-web/src/model/models/paging.model.ts deleted file mode 100644 index b3c003f2d1..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/model/models/paging.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ComputedAtom } from "@mendix/widget-plugin-mobx-kit/main"; -import { computed } from "mobx"; - -export function pageSizeAtom(source: { pageSize: number }): ComputedAtom { - return computed(() => source.pageSize); -} diff --git a/packages/pluggableWidgets/datagrid-web/src/model/services/Pagination.service.ts b/packages/pluggableWidgets/datagrid-web/src/model/services/Pagination.service.ts deleted file mode 100644 index 27ec947d56..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/model/services/Pagination.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { QueryService } from "@mendix/widget-plugin-grid/main"; -import { PaginationEnum, ShowPagingButtonsEnum } from "../../../typings/DatagridProps"; - -export interface PaginationConfig { - pagination: PaginationEnum; - showPagingButtons: ShowPagingButtonsEnum; - showNumberOfRows: boolean; - pageSize: number; -} - -type PaginationKind = `${PaginationEnum}.${ShowPagingButtonsEnum}`; - -export class PaginationService { - readonly pagination: PaginationEnum; - readonly paginationKind: PaginationKind; - readonly showPagingButtons: ShowPagingButtonsEnum; - - constructor( - private config: PaginationConfig, - private query: QueryService - ) { - this.pagination = config.pagination; - this.paginationKind = `${this.pagination}.${config.showPagingButtons}`; - this.showPagingButtons = config.showPagingButtons; - } - - get isLimitBased(): boolean { - return this.pagination === "virtualScrolling" || this.pagination === "loadMore"; - } - - get pageSize(): number { - return this.config.pageSize; - } - - get currentPage(): number { - const { - query: { limit, offset }, - pageSize - } = this; - return this.isLimitBased ? limit / pageSize : offset / pageSize; - } - - get paginationVisible(): boolean { - switch (this.paginationKind) { - case "buttons.always": - return true; - case "buttons.auto": { - const { totalCount = -1 } = this.query; - return totalCount > this.query.limit; - } - default: - return this.config.showNumberOfRows; - } - } - - get hasMoreItems(): boolean { - return this.query.hasMoreItems; - } - - get totalCount(): number | undefined { - return this.query.totalCount; - } - - setPage = (computePage: ((prevPage: number) => number) | number): void => { - const newPage = typeof computePage === "function" ? computePage(this.currentPage) : computePage; - if (this.isLimitBased) { - this.query.setLimit(newPage * this.pageSize); - } else { - this.query.setOffset(newPage * this.pageSize); - } - }; -} diff --git a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index c5f71eee71..0c71d1ada7 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -10,6 +10,7 @@ import { SelectAllService, SelectionDynamicProps, SelectionHelperService, + SetPageAction, TaskProgressService } from "@mendix/widget-plugin-grid/main"; import { SelectAllFeature } from "@mendix/widget-plugin-grid/select-all/select-all.feature"; @@ -26,6 +27,10 @@ import { CSSProperties, ReactNode } from "react"; import { MainGateProps } from "../../typings/MainGateProps"; import { WidgetRootViewModel } from "../features/base/WidgetRoot.viewModel"; import { EmptyPlaceholderViewModel } from "../features/empty-message/EmptyPlaceholder.viewModel"; +import { DynamicPaginationFeature } from "../features/pagination/DynamicPagination.feature"; +import { GridPageControl } from "../features/pagination/GridPageControl"; +import { PaginationViewModel } from "../features/pagination/Pagination.viewModel"; +import { PaginationConfig } from "../features/pagination/pagination.config"; import { CellEventsController } from "../features/row-interaction/CellEventsController"; import { CheckboxEventsController } from "../features/row-interaction/CheckboxEventsController"; import { SelectAllBarViewModel } from "../features/select-all/SelectAllBar.viewModel"; @@ -39,7 +44,6 @@ import { DatagridConfig } from "./configs/Datagrid.config"; import { RowClassProvider } from "./models/rows.model"; import { DatagridSetupService } from "./services/DatagridSetup.service"; import { DerivedLoaderController, DerivedLoaderControllerConfig } from "./services/DerivedLoaderController"; -import { PaginationConfig, PaginationService } from "./services/Pagination.service"; import { TextsService } from "./services/Texts.service"; import { PageSizeStore } from "./stores/PageSize.store"; @@ -55,7 +59,6 @@ export const CORE_TOKENS = { totalCount: token>("@computed:totalCount"), visibleColumnsCount: token>("@computed:visibleColumnsCount"), isAllItemsPresent: token>("@computed:isAllItemsPresent"), - pageSize: token>("@computed:pageSize"), columnCount: token>("@computed:columnCount") }, columnsStore: token("ColumnGroupStore"), @@ -101,8 +104,16 @@ export const DG_TOKENS = { loaderConfig: token("DatagridLoaderConfig"), loaderVM: token("DatagridLoaderViewModel"), - paginationConfig: token("PaginationConfig"), - paginationService: token("PaginationService"), + currentPage: token>("@computed:currentPage"), + dynamicPage: token>("@computed:dynamicPage"), + dynamicPageSize: token>("@computed:dynamicPageSize"), + dynamicPagination: token("@feature:DynamicPaginationFeature"), + pageControl: token("@service:GridPageControl"), + pageSize: token>("@computed:pageSize"), + paginationConfig: token("@config:PaginationConfig"), + paginationVM: token("@viewModel:PaginationService"), + setPageAction: token("@action:setPage"), + setPageSizeAction: token("@action:setPageSize"), parentChannelName: token("parentChannelName"), refreshInterval: token("refreshInterval"), diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index 0e60455b30..ca9c2c6235 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -117,11 +117,15 @@ export interface DatagridContainerProps { refreshIndicator: boolean; pageSize: number; pagination: PaginationEnum; + useCustomPagination: boolean; + customPagination?: ReactNode; showPagingButtons: ShowPagingButtonsEnum; showNumberOfRows: boolean; pagingPosition: PagingPositionEnum; - dynamicPageSize?: EditableValue; loadMoreButtonCaption?: DynamicValue; + dynamicPageSize?: EditableValue; + dynamicPage?: EditableValue; + totalCountValue?: EditableValue; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder?: ReactNode; rowClass?: ListExpressionValue; @@ -177,11 +181,15 @@ export interface DatagridPreviewProps { refreshIndicator: boolean; pageSize: number | null; pagination: PaginationEnum; + useCustomPagination: boolean; + customPagination: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; showPagingButtons: ShowPagingButtonsEnum; showNumberOfRows: boolean; pagingPosition: PagingPositionEnum; - dynamicPageSize: string; loadMoreButtonCaption: string; + dynamicPageSize: string; + dynamicPage: string; + totalCountValue: 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 9ed729a9e4..f765d18ee2 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts @@ -12,6 +12,8 @@ export type MainGateProps = Pick< | "configurationAttribute" | "configurationStorageType" | "datasource" + | "dynamicPage" + | "dynamicPageSize" | "emptyPlaceholder" | "enableSelectAll" | "exportDialogLabel" @@ -37,5 +39,7 @@ export type MainGateProps = Pick< | "showPagingButtons" | "storeFiltersInPersonalization" | "style" - | "dynamicPageSize" + | "totalCountValue" + | "useCustomPagination" + | "customPagination" >; diff --git a/packages/shared/widget-plugin-grid/src/main.ts b/packages/shared/widget-plugin-grid/src/main.ts index 7bb034257b..07ddcf6a6c 100644 --- a/packages/shared/widget-plugin-grid/src/main.ts +++ b/packages/shared/widget-plugin-grid/src/main.ts @@ -8,6 +8,8 @@ export { type SelectionHelperService } from "./interfaces/SelectionHelperService export type { TaskProgressService } from "./interfaces/TaskProgressService"; export { createFocusController } from "./keyboard-navigation/createFocusController"; export { layoutAtom } from "./keyboard-navigation/layout.model"; +export { PageSizeStore } from "./pagination/PageSize.store"; +export * from "./pagination/pagination.model"; export { SelectAllService } from "./select-all/SelectAll.service"; export { SelectionCounterViewModel } from "./selection-counter/SelectionCounter.viewModel"; export * from "./selection/context"; diff --git a/packages/shared/widget-plugin-grid/src/pagination/PageSize.store.ts b/packages/shared/widget-plugin-grid/src/pagination/PageSize.store.ts new file mode 100644 index 0000000000..763d63d0cb --- /dev/null +++ b/packages/shared/widget-plugin-grid/src/pagination/PageSize.store.ts @@ -0,0 +1,14 @@ +import { action, makeAutoObservable } from "mobx"; + +export class PageSizeStore { + pageSize: number; + + constructor(initSize = 0) { + this.pageSize = initSize; + makeAutoObservable(this, { setPageSize: action.bound }); + } + + setPageSize(size: number): void { + this.pageSize = size; + } +} diff --git a/packages/shared/widget-plugin-grid/src/pagination/pagination.model.ts b/packages/shared/widget-plugin-grid/src/pagination/pagination.model.ts new file mode 100644 index 0000000000..82afcf7392 --- /dev/null +++ b/packages/shared/widget-plugin-grid/src/pagination/pagination.model.ts @@ -0,0 +1,77 @@ +import { ComputedAtom } from "@mendix/widget-plugin-mobx-kit/main"; +import { action, computed } from "mobx"; +import { QueryService } from "../main"; + +/** + * Return observable atom holding page size. Value -1 means no meaningful page size is set. + * @injectable + */ +export function boundPageSize(get: () => number): ComputedAtom { + return computed(() => Math.max(get(), -1)); +} + +/** + * Atom that computes the current page based on query parameters. + * @injectable + * @remark + * When pagination is limit-based, the atom value is the number + * of loaded pages instead of the current page index. + * In the case of offset-based pagination, the atom value is the zero-based page index. + */ +export function currentPageAtom( + query: QueryService, + pageSize: ComputedAtom, + config: { isLimitBased: boolean } +): ComputedAtom { + return computed(() => { + const size = pageSize.get(); + const { limit, offset } = query; + return Math.floor(config.isLimitBased ? limit / size : offset / size); + }); +} + +/** Main atom for the page size. */ +export function pageSizeAtom(store: { pageSize: number }): ComputedAtom { + return computed(() => store.pageSize); +} + +export type SetPageAction = (value: ((prevPage: number) => number) | number) => void; + +/** Main action to change page. */ +export function createSetPageAction( + query: QueryService, + config: { isLimitBased: boolean }, + currentPage: ComputedAtom, + pageSize: ComputedAtom +): SetPageAction { + return action(function setPageAction(value): void { + const newPage = typeof value === "function" ? value(currentPage.get()) : value; + if (config.isLimitBased) { + query.setLimit(newPage * pageSize.get()); + } else { + query.setOffset(newPage * pageSize.get()); + } + }); +} + +export type SetPageSizeAction = (newSize: number) => void; + +/** Main action to change page size. */ +export function createSetPageSizeAction( + query: QueryService, + config: { isLimitBased: boolean }, + currentPage: ComputedAtom, + pageSizeStore: { setPageSize: (n: number) => void }, + setPageAction: SetPageAction +): SetPageSizeAction { + return action(function setPageSizeAction(newSize: number): void { + const currentPageIndex = currentPage.get(); + + // Update limit in case of offset-based pagination + if (!config.isLimitBased) { + query.setBaseLimit(newSize); + } + pageSizeStore.setPageSize(newSize); + setPageAction(currentPageIndex); + }); +} From 57a4c9577df16525b9529115c5719f6740cd19e4 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:30:57 +0100 Subject: [PATCH 3/4] feat: add custom paginatino --- .../datagrid-web/src/components/WidgetFooter.tsx | 11 ++++++++++- .../src/features/pagination/pagination.config.ts | 4 +++- .../src/model/containers/Datagrid.container.ts | 4 +++- .../datagrid-web/src/model/hooks/injection-hooks.ts | 2 ++ .../pluggableWidgets/datagrid-web/src/model/tokens.ts | 1 + 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx index f1417c1aa8..f9b62f5d1d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx @@ -3,14 +3,22 @@ import { observer } from "mobx-react-lite"; import { ReactElement } from "react"; import { SelectionCounter } from "../features/selection-counter/SelectionCounter"; import { useSelectionCounterViewModel } from "../features/selection-counter/injection-hooks"; -import { useDatagridConfig, usePaginationVM, useTexts } from "../model/hooks/injection-hooks"; +import { + useCustomPagination, + useDatagridConfig, + usePaginationConfig, + usePaginationVM, + useTexts +} from "../model/hooks/injection-hooks"; import { Pagination } from "./Pagination"; export const WidgetFooter = observer(function WidgetFooter(): ReactElement { const config = useDatagridConfig(); + const pgConfig = usePaginationConfig(); const paging = usePaginationVM(); const { loadMoreButtonCaption } = useTexts(); const selectionCounterVM = useSelectionCounterViewModel(); + const customPagination = useCustomPagination(); return (
@@ -35,6 +43,7 @@ export const WidgetFooter = observer(function WidgetFooter(): ReactElement { + {customPagination.get()}
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 0bafe6aac6..3c3aaaa3f7 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.config.ts @@ -10,6 +10,7 @@ export interface PaginationConfig { isLimitBased: boolean; dynamicPageSizeEnabled: boolean; dynamicPageEnabled: boolean; + customPaginationEnabled: boolean; } export type PaginationKind = `${PaginationEnum}.${ShowPagingButtonsEnum}` | "custom"; @@ -23,7 +24,8 @@ export function paginationConfig(props: MainGateProps): PaginationConfig { isLimitBased: isLimitBased(props), paginationKind: paginationKind(props), dynamicPageSizeEnabled: dynamicPageSizeEnabled(props), - dynamicPageEnabled: dynamicPageEnabled(props) + dynamicPageEnabled: dynamicPageEnabled(props), + customPaginationEnabled: props.useCustomPagination }; return Object.freeze(config); 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 dd9db07605..963b61824b 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -25,7 +25,7 @@ import { EmptyPlaceholderViewModel } from "../../features/empty-message/EmptyPla import { DynamicPaginationFeature } from "../../features/pagination/DynamicPagination.feature"; import { PageControlService } from "../../features/pagination/PageControl.service"; import { paginationConfig } from "../../features/pagination/pagination.config"; -import { dynamicPageAtom, dynamicPageSizeAtom } from "../../features/pagination/pagination.model"; +import { customPaginationAtom, dynamicPageAtom, dynamicPageSizeAtom } from "../../features/pagination/pagination.model"; import { PaginationViewModel } from "../../features/pagination/Pagination.viewModel"; import { createCellEventsController } from "../../features/row-interaction/CellEventsController"; import { creteCheckboxEventsController } from "../../features/row-interaction/CheckboxEventsController"; @@ -66,6 +66,7 @@ injected( CORE.atoms.totalCount, DG.pageControl ); +injected(customPaginationAtom, CORE.mainGate); // loader injected(DerivedLoaderController, DG.query, DG.exportProgressService, CORE.columnsStore, DG.loaderConfig); @@ -137,6 +138,7 @@ export class DatagridContainer extends Container { /** Pagination **/ this.bind(DG.currentPage).toInstance(currentPageAtom).inTransientScope(); + this.bind(DG.customPagination).toInstance(customPaginationAtom).inTransientScope(); this.bind(DG.dynamicPage).toInstance(dynamicPageAtom).inTransientScope(); this.bind(DG.dynamicPageSize).toInstance(dynamicPageSizeAtom).inTransientScope(); this.bind(DG.dynamicPagination).toInstance(DynamicPaginationFeature).inSingletonScope(); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts index 78688c4c48..0886f8c1f6 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -24,3 +24,5 @@ export const [useClickActionHelper] = createInjectionHooks(DG.clickActionHelper) export const [useFocusService] = createInjectionHooks(DG.focusService); export const [useCheckboxEventsHandler] = createInjectionHooks(DG.checkboxEventsHandler); export const [useCellEventsHandler] = createInjectionHooks(DG.cellEventsHandler); +export const [useCustomPagination] = createInjectionHooks(DG.customPagination); +export const [usePaginationConfig] = createInjectionHooks(DG.paginationConfig); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index 0c71d1ada7..9b1f61e366 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -105,6 +105,7 @@ export const DG_TOKENS = { loaderVM: token("DatagridLoaderViewModel"), currentPage: token>("@computed:currentPage"), + customPagination: token>("@computed:customPagination"), dynamicPage: token>("@computed:dynamicPage"), dynamicPageSize: token>("@computed:dynamicPageSize"), dynamicPagination: token("@feature:DynamicPaginationFeature"), From 114f9fc7172312608fe2fc89cc0430cc43bb2b3e Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:37:16 +0100 Subject: [PATCH 4/4] fix: update tests --- packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx index e8bc5eb194..5890c5cfcb 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx @@ -84,6 +84,8 @@ export function mockContainerProps(overrides?: Partial): selectAllText: dynamic("Select all items"), selectAllTemplate: dynamic("Select all %d items"), allSelectedText: dynamic("All items selected"), + useCustomPagination: false, + customPagination: undefined, ...overrides }; }