From 0970aca5fd47dc8925d66bfa65cdd6c72f57a7c0 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Fri, 11 Jul 2025 11:06:33 +0200 Subject: [PATCH 01/19] feat(datagrid-web): add RefreshIndicator component and styles for loading state --- .../datawidgets/web/_datagrid.scss | 76 +++++++++++++++++++ .../src/components/RefreshIndicator.tsx | 11 +++ .../datagrid-web/src/components/Widget.tsx | 2 + 3 files changed, 89 insertions(+) create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index e311d2e0d9..4d60bd32f6 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -429,6 +429,82 @@ $root: ".widget-datagrid"; display: contents; } + &-refresh-container { + grid-column: 1 / -1; + padding: 0; + } + + &-refresh-indicator { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--border-color-default, #ced0d3); + border: none; + border-radius: 2px; + color: var(--brand-primary, $dg-brand-primary); + height: 4px; + width: 100%; + + &::-webkit-progress-bar { + background-color: transparent; + } + + &::-webkit-progress-value { + background-color: currentColor; + transition: all 0.2s; + } + + &::-moz-progress-bar { + background-color: currentColor; + transition: all 0.2s; + } + + &::-ms-fill { + border: none; + background-color: currentColor; + transition: all 0.2s; + } + + &:indeterminate { + background-size: 200% 100%; + background-image: linear-gradient( + to right, + transparent 50%, + currentColor 50%, + currentColor 60%, + transparent 60%, + transparent 71.5%, + currentColor 71.5%, + currentColor 84%, + transparent 84% + ); + animation: progress-linear 2s infinite linear; + } + + &:indeterminate::-moz-progress-bar { + background-color: transparent; + } + + &:indeterminate::-ms-fill { + animation-name: none; + } + + @keyframes progress-linear { + 0% { + background-size: 200% 100%; + background-position: left -31.25% top 0%; + } + 50% { + background-size: 800% 100%; + background-position: left -49% top 0%; + } + 100% { + background-size: 400% 100%; + background-position: left -102% top 0%; + } + } + } + &.widget-datagrid-selection-method-click { .tr.tr-selected .td { background-color: $dg-grid-selected-row-background; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx b/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx new file mode 100644 index 0000000000..79f3f3a10f --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx @@ -0,0 +1,11 @@ +import { createElement, ReactElement } from "react"; + +export function RefreshIndicator(): ReactElement { + return ( +
+
+ +
+
+ ); +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index c3d1beeba2..8d1de39ee8 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -25,6 +25,7 @@ import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navig import { observer } from "mobx-react-lite"; import { RowsRenderer } from "./RowsRenderer"; import { GridHeader } from "./GridHeader"; +import { RefreshIndicator } from "./RefreshIndicator"; export interface WidgetProps { CellComponent: CellComponent; @@ -189,6 +190,7 @@ const Main = observer((props: WidgetProps): ReactElemen isLoading={props.columnsLoading} preview={props.preview} /> + Date: Mon, 14 Jul 2025 10:22:24 +0200 Subject: [PATCH 02/19] feat(datagrid-web): add refresh indicator property to Datagrid2 --- packages/pluggableWidgets/datagrid-web/src/Datagrid.xml | 4 ++++ .../pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index 8782abf1a6..98b1ccd98f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -57,6 +57,10 @@ Skeleton + + Show refresh indicator + Show a refresh indicator when the data is being loaded. + diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index edbce23cfd..052bb22244 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -96,6 +96,7 @@ export interface DatagridContainerProps { itemSelectionMode: ItemSelectionModeEnum; showSelectAllToggle: boolean; loadingType: LoadingTypeEnum; + refreshIndicator: boolean; columns: ColumnsType[]; columnsFilterable: boolean; pageSize: number; @@ -144,6 +145,7 @@ export interface DatagridPreviewProps { itemSelectionMode: ItemSelectionModeEnum; showSelectAllToggle: boolean; loadingType: LoadingTypeEnum; + refreshIndicator: boolean; columns: ColumnsPreviewType[]; columnsFilterable: boolean; pageSize: number | null; From 01582b811b71af7621c5e4699cb6ec66647b1bb5 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 14 Jul 2025 10:22:56 +0200 Subject: [PATCH 03/19] feat(datagrid-web): add refreshIndicator prop to Datagrid component --- packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index 78abad3963..8a3fc339c5 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -122,6 +122,7 @@ const Container = observer((props: Props): ReactElement => { isFetchingNextBatch={rootStore.loaderCtrl.isFetchingNextBatch} loadingType={props.loadingType} columnsLoading={!columnsStore.loaded} + refreshIndicator={props.refreshIndicator} /> ); }); From 4131e560b440be008c214c7d2e9261721068d300 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 14 Jul 2025 10:23:17 +0200 Subject: [PATCH 04/19] feat(datagrid-web): add refreshIndicator prop to Widget and update GridBody component --- .../datagrid-web/src/components/GridBody.tsx | 8 ++++---- .../datagrid-web/src/components/Widget.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx index dc3bac60c7..4afda70144 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx @@ -8,7 +8,7 @@ interface Props { className?: string; children?: React.ReactNode; loadingType: LoadingTypeEnum; - isLoading: boolean; + // isLoading: boolean; isFetchingNextBatch?: boolean; columnsHidable: boolean; columnsSize: number; @@ -20,9 +20,9 @@ export function GridBody(props: Props): ReactElement { const { children } = props; const content = (): React.ReactElement => { - if (props.isLoading) { - return 0 ? props.rowsSize : props.pageSize} />; - } + // if (props.isLoading) { + // return 0 ? props.rowsSize : props.pageSize} />; + // } return ( {children} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 8d1de39ee8..6688dc2a46 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -70,6 +70,7 @@ export interface WidgetProps(props: WidgetProps): ReactElemen paging, pagingPosition, preview, + refreshIndicator, selectActionHelper, setPage, visibleColumns @@ -190,9 +192,9 @@ const Main = observer((props: WidgetProps): ReactElemen isLoading={props.columnsLoading} preview={props.preview} /> - + {refreshIndicator && props.isLoading ? : null} Date: Mon, 14 Jul 2025 15:51:50 +0200 Subject: [PATCH 05/19] feat(datagrid-web): add refreshIndicator prop to preview function and mockWidgetProps --- .../pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx | 1 + packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index ba1df3f098..d897bb93ab 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -145,6 +145,7 @@ export function preview(props: DatagridPreviewProps): ReactElement { isFetchingNextBatch={false} loadingType="spinner" columnsLoading={false} + refreshIndicator={props.refreshIndicator} /> ); } diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx index 9a347d20aa..cb172f815f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx @@ -109,6 +109,7 @@ export function mockWidgetProps(): WidgetProps { isFetchingNextBatch: false, loadingType: "spinner", columnsLoading: false, + refreshIndicator: false, focusController: new FocusTargetController( new PositionController(), new VirtualGridLayout(1, columns.length, 10) From 5898b6b4ca3cd19961d33e6ca8247ac37711ebe4 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 14 Jul 2025 15:55:40 +0200 Subject: [PATCH 06/19] refactor(datagrid-web): remove unused isLoading prop and related code from GridBody component --- .../pluggableWidgets/datagrid-web/src/components/GridBody.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx index 4afda70144..02d1fcd727 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx @@ -8,7 +8,6 @@ interface Props { className?: string; children?: React.ReactNode; loadingType: LoadingTypeEnum; - // isLoading: boolean; isFetchingNextBatch?: boolean; columnsHidable: boolean; columnsSize: number; @@ -20,9 +19,6 @@ export function GridBody(props: Props): ReactElement { const { children } = props; const content = (): React.ReactElement => { - // if (props.isLoading) { - // return 0 ? props.rowsSize : props.pageSize} />; - // } return ( {children} From b2df9b2344885500f493b8de4b394a122fb9016f Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 14 Jul 2025 16:24:58 +0200 Subject: [PATCH 07/19] chore(datagrid-web): update changelog --- packages/pluggableWidgets/datagrid-web/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 0c7ce269c4..0875c32c87 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We added a new refresh indicator to be showed when datagrid refreshes. + ## [3.2.0] - 2025-08-18 ### Changed From f22b7a75a5c090aeeebd6c3654d3e3e796919fcd Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 21 Jul 2025 17:21:44 +0200 Subject: [PATCH 08/19] feat(datagrid-web): replace isLoading with isFirstLoad and update related logic --- .../src/Datagrid.editorPreview.tsx | 1 + .../datagrid-web/src/Datagrid.tsx | 3 +- .../datagrid-web/src/components/GridBody.tsx | 4 +++ .../datagrid-web/src/components/Widget.tsx | 7 +++-- .../controllers/DerivedLoaderController.ts | 8 ++--- .../datagrid-web/src/utils/test-utils.tsx | 1 + .../__tests__/DatasourceController.spec.ts | 10 +++---- .../src/query/DatasourceController.ts | 30 ++++++++++++++----- .../src/query/query-controller.ts | 2 +- 9 files changed, 45 insertions(+), 21 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index d897bb93ab..31c1184c3d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -141,6 +141,7 @@ export function preview(props: DatagridPreviewProps): ReactElement { cellEventsController={eventsController} checkboxEventsController={eventsController} focusController={focusController} + isFirstLoad={false} isLoading={false} isFetchingNextBatch={false} loadingType="spinner" diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index 8a3fc339c5..1a18f8c186 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -118,8 +118,9 @@ const Container = observer((props: Props): ReactElement => { cellEventsController={cellEventsController} checkboxEventsController={checkboxEventsController} focusController={focusController} - isLoading={rootStore.loaderCtrl.isLoading} + isFirstLoad={rootStore.loaderCtrl.isFirstLoad} isFetchingNextBatch={rootStore.loaderCtrl.isFetchingNextBatch} + isLoading={props.datasource.status === "loading"} loadingType={props.loadingType} columnsLoading={!columnsStore.loaded} refreshIndicator={props.refreshIndicator} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx index 02d1fcd727..64c1ca93b8 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx @@ -8,6 +8,7 @@ interface Props { className?: string; children?: React.ReactNode; loadingType: LoadingTypeEnum; + isFirstLoad: boolean; isFetchingNextBatch?: boolean; columnsHidable: boolean; columnsSize: number; @@ -19,6 +20,9 @@ export function GridBody(props: Props): ReactElement { const { children } = props; const content = (): React.ReactElement => { + if (props.isFirstLoad) { + return 0 ? props.rowsSize : props.pageSize} />; + } return ( {children} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 6688dc2a46..048f5b704c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -66,6 +66,7 @@ export interface WidgetProps(props: WidgetProps): ReactElemen const selectionEnabled = selectActionHelper.selectionType !== "None"; + const showRefreshIndicator = refreshIndicator && props.isLoading && !props.isFirstLoad; + return ( {showTopBar && {pagination}} @@ -192,9 +195,9 @@ const Main = observer((props: WidgetProps): ReactElemen isLoading={props.columnsLoading} preview={props.preview} /> - {refreshIndicator && props.isLoading ? : null} + {showRefreshIndicator ? : null} { selectActionHelper: mockSelectionProps(), cellEventsController: { getProps: () => Object.create({}) }, checkboxEventsController: { getProps: () => Object.create({}) }, + isFirstLoad: false, isLoading: false, isFetchingNextBatch: false, loadingType: "spinner", diff --git a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts index c2cbe59476..fc5cd93dea 100644 --- a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts +++ b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts @@ -21,8 +21,8 @@ describe("DatasourceController loading states", () => { provider.setProps({ datasource }); }); - it("isLoading returns true by default", () => { - expect(controller.isLoading).toBe(true); + it("isFirstLoad returns true by default", () => { + expect(controller.isFirstLoad).toBe(true); }); it("refresh has no effect if ds is loading", () => { @@ -39,13 +39,13 @@ describe("DatasourceController loading states", () => { provider.setProps({ datasource: list.loading() }); expect(provider.gate.props.datasource.status).toBe("loading"); expect(controller.isRefreshing).toBe(true); - expect(controller.isLoading).toBe(false); + expect(controller.isFirstLoad).toBe(true); }); it("isFetchingNextBatch returns true after setLimit call", () => { controller.setLimit(20); expect(controller.isFetchingNextBatch).toBe(true); - expect(controller.isLoading).toBe(false); + expect(controller.isFirstLoad).toBe(true); }); }); @@ -56,7 +56,7 @@ describe("DatasourceController loading states", () => { }); it("all loading states return false", () => { - expect(controller.isLoading).toBe(false); + expect(controller.isFirstLoad).toBe(true); expect(controller.isRefreshing).toBe(false); expect(controller.isFetchingNextBatch).toBe(false); }); diff --git a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts index 5f5c5b0b1f..0804365bd2 100644 --- a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts +++ b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts @@ -1,7 +1,7 @@ import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; import { ListValue, ValueStatus } from "mendix"; -import { action, autorun, computed, IComputedValue, makeAutoObservable } from "mobx"; +import { action, autorun, computed, IComputedValue, makeAutoObservable, when } from "mobx"; import { QueryController } from "./query-controller"; type Gate = DerivedPropsGate<{ datasource: ListValue }>; @@ -13,19 +13,27 @@ export class DatasourceController implements ReactiveController, QueryController private refreshing = false; private fetching = false; private pageSize = Infinity; + private isLoaded = false; constructor(host: ReactiveControllerHost, spec: DatasourceControllerSpec) { host.addController(this); this.gate = spec.gate; - type PrivateMembers = "resetFlags" | "updateFlags" | "setRefreshing" | "setFetching" | "pageSize"; + type PrivateMembers = + | "resetFlags" + | "updateFlags" + | "setRefreshing" + | "setFetching" + | "pageSize" + | "setIsLoaded"; makeAutoObservable(this, { setup: false, pageSize: false, updateFlags: action, resetFlags: action, setRefreshing: action, - setFetching: action + setFetching: action, + setIsLoaded: action }); } @@ -51,6 +59,10 @@ export class DatasourceController implements ReactiveController, QueryController this.fetching = value; } + private setIsLoaded(value: boolean): void { + this.isLoaded = value; + } + private resetLimit(): void { this.datasource.setLimit(this.pageSize); } @@ -63,11 +75,8 @@ export class DatasourceController implements ReactiveController, QueryController return this.gate.props.datasource; } - get isLoading(): boolean { - if (this.isRefreshing || this.isFetchingNextBatch) { - return false; - } - return this.isDSLoading; + get isFirstLoad(): boolean { + return !this.isLoaded; } get isRefreshing(): boolean { @@ -105,6 +114,11 @@ export class DatasourceController implements ReactiveController, QueryController } setup(): () => void { + when( + () => !this.isDSLoading, + () => this.setIsLoaded(true) + ); + return autorun(() => { // Always use actions to set flags to avoid subscribing to them this.updateFlags(this.datasource.status); diff --git a/packages/shared/widget-plugin-grid/src/query/query-controller.ts b/packages/shared/widget-plugin-grid/src/query/query-controller.ts index bc20172a63..a3cf6a7214 100644 --- a/packages/shared/widget-plugin-grid/src/query/query-controller.ts +++ b/packages/shared/widget-plugin-grid/src/query/query-controller.ts @@ -15,7 +15,7 @@ export interface QueryController extends Pick { refresh(): void; setPageSize(size: number): void; hasMoreItems: boolean; - isLoading: boolean; + isFirstLoad: boolean; isRefreshing: boolean; isFetchingNextBatch: boolean; } From e30c1b6a6abf929e9761bee861633d05076a9bbc Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 22 Jul 2025 16:35:24 +0200 Subject: [PATCH 09/19] feat(datagrid-web): add show prop to RefreshIndicator and update usage in Widget component --- .../src/themesource/datawidgets/web/_datagrid.scss | 7 +++++++ .../datagrid-web/src/components/RefreshIndicator.tsx | 9 +++++++-- .../datagrid-web/src/components/Widget.tsx | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index 4d60bd32f6..e74096fc8a 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -432,6 +432,10 @@ $root: ".widget-datagrid"; &-refresh-container { grid-column: 1 / -1; padding: 0; + + &--hidden { + display: none; + } } &-refresh-indicator { @@ -444,6 +448,9 @@ $root: ".widget-datagrid"; color: var(--brand-primary, $dg-brand-primary); height: 4px; width: 100%; + position: absolute; + left: 0; + right: 0; &::-webkit-progress-bar { background-color: transparent; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx b/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx index 79f3f3a10f..a26fd7199a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx @@ -1,9 +1,14 @@ +import classNames from "classnames"; import { createElement, ReactElement } from "react"; -export function RefreshIndicator(): ReactElement { +export function RefreshIndicator({ show }: { show: boolean }): ReactElement { return (
-
+
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 048f5b704c..5fdfeadf61 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -195,7 +195,7 @@ const Main = observer((props: WidgetProps): ReactElemen isLoading={props.columnsLoading} preview={props.preview} /> - {showRefreshIndicator ? : null} + Date: Wed, 23 Jul 2025 13:30:40 +0200 Subject: [PATCH 10/19] refactor(datagrid-web): simplify RefreshIndicator usage --- .../src/themesource/datawidgets/web/_datagrid.scss | 5 +---- .../datagrid-web/src/components/RefreshIndicator.tsx | 9 ++------- .../datagrid-web/src/components/Widget.tsx | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index e74096fc8a..ed8c75235c 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -432,10 +432,7 @@ $root: ".widget-datagrid"; &-refresh-container { grid-column: 1 / -1; padding: 0; - - &--hidden { - display: none; - } + position: relative; } &-refresh-indicator { diff --git a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx b/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx index a26fd7199a..79f3f3a10f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx @@ -1,14 +1,9 @@ -import classNames from "classnames"; import { createElement, ReactElement } from "react"; -export function RefreshIndicator({ show }: { show: boolean }): ReactElement { +export function RefreshIndicator(): ReactElement { return (
-
+
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 5fdfeadf61..048f5b704c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -195,7 +195,7 @@ const Main = observer((props: WidgetProps): ReactElemen isLoading={props.columnsLoading} preview={props.preview} /> - + {showRefreshIndicator ? : null} Date: Wed, 23 Jul 2025 13:41:54 +0200 Subject: [PATCH 11/19] docs(datagrid-web): clarify description of the refresh indicator feature on changelog --- packages/pluggableWidgets/datagrid-web/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 0875c32c87..5fd325a808 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- We added a new refresh indicator to be showed when datagrid refreshes. +- We implemented a new property to show a refresh indicator. With the refresh indicator, any datasource change shows a progress bar on top of Datagrid 2. ## [3.2.0] - 2025-08-18 From 437db58db56b17e7a96c8752007a7400c9d0c6e4 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 31 Jul 2025 16:13:21 +0200 Subject: [PATCH 12/19] feat(datagrid-web): increase refresh indicator animation timing --- .../data-widgets/src/themesource/datawidgets/web/_datagrid.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index ed8c75235c..9159447bd4 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -482,7 +482,7 @@ $root: ".widget-datagrid"; currentColor 84%, transparent 84% ); - animation: progress-linear 2s infinite linear; + animation: progress-linear 3s infinite linear; } &:indeterminate::-moz-progress-bar { From 5ef396e720570ddbf06a0c8b99716fa047fb5f2b Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 31 Jul 2025 16:14:12 +0200 Subject: [PATCH 13/19] feat(datagrid-web): dont show refresh indicator when resfreh timer is 1s or less --- .../datagrid-web/src/Datagrid.editorPreview.tsx | 1 + packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx | 1 + .../pluggableWidgets/datagrid-web/src/components/Widget.tsx | 4 +++- .../pluggableWidgets/datagrid-web/src/utils/test-utils.tsx | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index 31c1184c3d..50ac91b0ab 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -147,6 +147,7 @@ export function preview(props: DatagridPreviewProps): ReactElement { loadingType="spinner" columnsLoading={false} refreshIndicator={props.refreshIndicator} + refreshInterval={props.refreshInterval ?? 0} /> ); } diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index 1a18f8c186..5676178c17 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -124,6 +124,7 @@ const Container = observer((props: Props): ReactElement => { loadingType={props.loadingType} columnsLoading={!columnsStore.loaded} refreshIndicator={props.refreshIndicator} + refreshInterval={props.refreshInterval} /> ); }); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 048f5b704c..c7986f17c9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -72,6 +72,7 @@ export interface WidgetProps(props: WidgetProps): ReactElemen pagingPosition, preview, refreshIndicator, + refreshInterval, selectActionHelper, setPage, visibleColumns @@ -165,7 +167,7 @@ const Main = observer((props: WidgetProps): ReactElemen const selectionEnabled = selectActionHelper.selectionType !== "None"; - const showRefreshIndicator = refreshIndicator && props.isLoading && !props.isFirstLoad; + const showRefreshIndicator = refreshIndicator && refreshInterval > 1 && props.isLoading && !props.isFirstLoad; return ( diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx index d36ea0f5af..f1bbd7ed12 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx @@ -111,6 +111,7 @@ export function mockWidgetProps(): WidgetProps { loadingType: "spinner", columnsLoading: false, refreshIndicator: false, + refreshInterval: 0, focusController: new FocusTargetController( new PositionController(), new VirtualGridLayout(1, columns.length, 10) From a48b7bf42688e4833edbdb977c93852f88a53e62 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 31 Jul 2025 16:14:45 +0200 Subject: [PATCH 14/19] chore(datagrid-web): add disposeBatch to follow standards on controllers --- .../src/query/DatasourceController.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts index 0804365bd2..56ce9c6d69 100644 --- a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts +++ b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts @@ -3,6 +3,7 @@ import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugi import { ListValue, ValueStatus } from "mendix"; import { action, autorun, computed, IComputedValue, makeAutoObservable, when } from "mobx"; import { QueryController } from "./query-controller"; +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; type Gate = DerivedPropsGate<{ datasource: ListValue }>; @@ -114,15 +115,22 @@ export class DatasourceController implements ReactiveController, QueryController } setup(): () => void { - when( - () => !this.isDSLoading, - () => this.setIsLoaded(true) + const [add, disposeAll] = disposeBatch(); + + add( + when( + () => !this.isDSLoading, + () => this.setIsLoaded(true) + ) + ); + add( + autorun(() => { + // Always use actions to set flags to avoid subscribing to them + this.updateFlags(this.datasource.status); + }) ); - return autorun(() => { - // Always use actions to set flags to avoid subscribing to them - this.updateFlags(this.datasource.status); - }); + return disposeAll; } refresh(): void { From 063b2e28995508f16aaaaa7913639e72aa780565 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 31 Jul 2025 16:15:22 +0200 Subject: [PATCH 15/19] test(widget-plugin-grid): fix tests to follow more real world scenario using host --- .../src/__tests__/DatasourceController.spec.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts index fc5cd93dea..4f3436f244 100644 --- a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts +++ b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts @@ -1,17 +1,21 @@ +import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost"; import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider"; -import { ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/main"; +// import { ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/main"; import { list } from "@mendix/widget-plugin-test-utils"; import { ListValue } from "mendix"; import { DatasourceController } from "../query/DatasourceController"; +class TestControllerHost extends BaseControllerHost {} + describe("DatasourceController loading states", () => { let controller: DatasourceController; let datasource: ListValue; let provider: GateProvider<{ datasource: ListValue }>; beforeEach(() => { - const host = { addController: jest.fn() } as unknown as ReactiveControllerHost; + const host = new TestControllerHost(); provider = new GateProvider({ datasource: list.loading() }); + host.setup(); controller = new DatasourceController(host, { gate: provider.gate }); }); @@ -34,12 +38,13 @@ describe("DatasourceController loading states", () => { it("isRefreshing is true after refresh call", () => { provider.setProps({ datasource: list(0) }); expect(provider.gate.props.datasource.status).toBe("available"); + controller.setup(); controller.refresh(); expect(controller.isRefreshing).toBe(true); provider.setProps({ datasource: list.loading() }); expect(provider.gate.props.datasource.status).toBe("loading"); expect(controller.isRefreshing).toBe(true); - expect(controller.isFirstLoad).toBe(true); + expect(controller.isFirstLoad).toBe(false); }); it("isFetchingNextBatch returns true after setLimit call", () => { @@ -55,7 +60,7 @@ describe("DatasourceController loading states", () => { provider.setProps({ datasource }); }); - it("all loading states return false", () => { + it("isFirstLoad returns true and loading states return false", () => { expect(controller.isFirstLoad).toBe(true); expect(controller.isRefreshing).toBe(false); expect(controller.isFetchingNextBatch).toBe(false); From 712db1efb54ff9f32d722ae36d6b00e5fbdfbe6b Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 31 Jul 2025 16:34:50 +0200 Subject: [PATCH 16/19] test(widget-plugin-grid): run controller setup to all test cases --- .../src/__tests__/DatasourceController.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts index 4f3436f244..43e1899518 100644 --- a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts +++ b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts @@ -17,6 +17,7 @@ describe("DatasourceController loading states", () => { provider = new GateProvider({ datasource: list.loading() }); host.setup(); controller = new DatasourceController(host, { gate: provider.gate }); + controller.setup(); }); describe("when datasource is loading", () => { @@ -38,7 +39,6 @@ describe("DatasourceController loading states", () => { it("isRefreshing is true after refresh call", () => { provider.setProps({ datasource: list(0) }); expect(provider.gate.props.datasource.status).toBe("available"); - controller.setup(); controller.refresh(); expect(controller.isRefreshing).toBe(true); provider.setProps({ datasource: list.loading() }); @@ -60,8 +60,8 @@ describe("DatasourceController loading states", () => { provider.setProps({ datasource }); }); - it("isFirstLoad returns true and loading states return false", () => { - expect(controller.isFirstLoad).toBe(true); + it("All loading states return false", () => { + expect(controller.isFirstLoad).toBe(false); expect(controller.isRefreshing).toBe(false); expect(controller.isFetchingNextBatch).toBe(false); }); From 151614137be77ca7afbf8af19bc0fb4e6784fe10 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 31 Jul 2025 17:00:27 +0200 Subject: [PATCH 17/19] feat(widget-plugin-grid): move showRefreshIndicator to DatasourceController --- .../datagrid-web/src/Datagrid.editorPreview.tsx | 3 +-- .../pluggableWidgets/datagrid-web/src/Datagrid.tsx | 3 +-- .../datagrid-web/src/components/Widget.tsx | 8 ++------ .../src/controllers/DerivedLoaderController.ts | 6 +++++- .../datagrid-web/src/utils/test-utils.tsx | 3 +-- .../src/__tests__/DatasourceController.spec.ts | 12 ++++++------ .../src/query/DatasourceController.ts | 7 ++++++- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index 50ac91b0ab..26839e163d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -146,8 +146,7 @@ export function preview(props: DatagridPreviewProps): ReactElement { isFetchingNextBatch={false} loadingType="spinner" columnsLoading={false} - refreshIndicator={props.refreshIndicator} - refreshInterval={props.refreshInterval ?? 0} + showRefreshIndicator={false} /> ); } diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index 5676178c17..b407b5e169 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -123,8 +123,7 @@ const Container = observer((props: Props): ReactElement => { isLoading={props.datasource.status === "loading"} loadingType={props.loadingType} columnsLoading={!columnsStore.loaded} - refreshIndicator={props.refreshIndicator} - refreshInterval={props.refreshInterval} + showRefreshIndicator={rootStore.loaderCtrl.showRefreshIndicator} /> ); }); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index c7986f17c9..f169d66fa8 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -71,8 +71,7 @@ export interface WidgetProps(props: WidgetProps): ReactElemen paging, pagingPosition, preview, - refreshIndicator, - refreshInterval, + showRefreshIndicator, selectActionHelper, setPage, visibleColumns @@ -167,8 +165,6 @@ const Main = observer((props: WidgetProps): ReactElemen const selectionEnabled = selectActionHelper.selectionType !== "None"; - const showRefreshIndicator = refreshIndicator && refreshInterval > 1 && props.isLoading && !props.isFirstLoad; - return ( {showTopBar && {pagination}} diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts index 2be3ee76bd..d03c90f62b 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts @@ -3,7 +3,7 @@ import { computed, makeObservable } from "mobx"; type DerivedLoaderControllerSpec = { exp: { exporting: boolean }; cols: { loaded: boolean }; - query: { isFetchingNextBatch: boolean; isFirstLoad: boolean; isRefreshing: boolean }; + query: { isFetchingNextBatch: boolean; isFirstLoad: boolean; isRefreshing: boolean; showRefreshIndicator: boolean }; }; export class DerivedLoaderController { @@ -35,4 +35,8 @@ export class DerivedLoaderController { get isRefreshing(): boolean { return this.spec.query.isRefreshing; } + + get showRefreshIndicator(): boolean { + return this.spec.query.showRefreshIndicator; + } } diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx index f1bbd7ed12..3eb9a22f31 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx @@ -110,8 +110,7 @@ export function mockWidgetProps(): WidgetProps { isFetchingNextBatch: false, loadingType: "spinner", columnsLoading: false, - refreshIndicator: false, - refreshInterval: 0, + showRefreshIndicator: false, focusController: new FocusTargetController( new PositionController(), new VirtualGridLayout(1, columns.length, 10) diff --git a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts index 43e1899518..bd4ff5be4d 100644 --- a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts +++ b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts @@ -10,11 +10,11 @@ class TestControllerHost extends BaseControllerHost {} describe("DatasourceController loading states", () => { let controller: DatasourceController; let datasource: ListValue; - let provider: GateProvider<{ datasource: ListValue }>; + let provider: GateProvider<{ datasource: ListValue; refreshIndicator: boolean; refreshInterval: number }>; beforeEach(() => { const host = new TestControllerHost(); - provider = new GateProvider({ datasource: list.loading() }); + provider = new GateProvider({ datasource: list.loading(), refreshIndicator: false, refreshInterval: 0 }); host.setup(); controller = new DatasourceController(host, { gate: provider.gate }); controller.setup(); @@ -23,7 +23,7 @@ describe("DatasourceController loading states", () => { describe("when datasource is loading", () => { beforeEach(() => { datasource = list.loading(); - provider.setProps({ datasource }); + provider.setProps({ datasource, refreshIndicator: false, refreshInterval: 0 }); }); it("isFirstLoad returns true by default", () => { @@ -37,11 +37,11 @@ describe("DatasourceController loading states", () => { }); it("isRefreshing is true after refresh call", () => { - provider.setProps({ datasource: list(0) }); + provider.setProps({ datasource: list(0), refreshIndicator: false, refreshInterval: 0 }); expect(provider.gate.props.datasource.status).toBe("available"); controller.refresh(); expect(controller.isRefreshing).toBe(true); - provider.setProps({ datasource: list.loading() }); + provider.setProps({ datasource: list.loading(), refreshIndicator: false, refreshInterval: 0 }); expect(provider.gate.props.datasource.status).toBe("loading"); expect(controller.isRefreshing).toBe(true); expect(controller.isFirstLoad).toBe(false); @@ -57,7 +57,7 @@ describe("DatasourceController loading states", () => { describe("when datasource is not loading", () => { beforeEach(() => { datasource = list(0); - provider.setProps({ datasource }); + provider.setProps({ datasource, refreshIndicator: false, refreshInterval: 0 }); }); it("All loading states return false", () => { diff --git a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts index 56ce9c6d69..8008fcad74 100644 --- a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts +++ b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts @@ -5,7 +5,7 @@ import { action, autorun, computed, IComputedValue, makeAutoObservable, when } f import { QueryController } from "./query-controller"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; -type Gate = DerivedPropsGate<{ datasource: ListValue }>; +type Gate = DerivedPropsGate<{ datasource: ListValue; refreshIndicator: boolean; refreshInterval: number }>; type DatasourceControllerSpec = { gate: Gate }; @@ -114,6 +114,11 @@ export class DatasourceController implements ReactiveController, QueryController return computed(data); } + get showRefreshIndicator(): boolean { + const { refreshIndicator, refreshInterval } = this.gate.props; + return refreshIndicator && refreshInterval > 1 && this.isDSLoading && !this.isFirstLoad; + } + setup(): () => void { const [add, disposeAll] = disposeBatch(); From 5e490a180a4cd363b803087fd8559fe5b0de3ad6 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:03:52 +0200 Subject: [PATCH 18/19] refactor: change flags --- .../datagrid-web/src/Datagrid.tsx | 14 +++--- .../controllers/DerivedLoaderController.ts | 23 ++++++++-- .../src/helpers/state/RootGridStore.ts | 2 + .../__tests__/DatasourceController.spec.ts | 6 +-- .../src/__tests__/RefreshController.spec.ts | 10 ++-- .../src/query/DatasourceController.ts | 46 +++++++------------ .../src/query/RefreshController.ts | 4 +- .../src/query/query-controller.ts | 2 +- 8 files changed, 57 insertions(+), 50 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index b407b5e169..d62e598cfe 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -1,22 +1,22 @@ +import { useOnResetFiltersEvent } from "@mendix/widget-plugin-external-events/hooks"; +import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; +import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; import { useSelectionHelper } from "@mendix/widget-plugin-grid/selection"; import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; +import { observer } from "mobx-react-lite"; import { ReactElement, ReactNode, createElement, useCallback, useMemo } from "react"; import { DatagridContainerProps } from "../typings/DatagridProps"; import { Cell } from "./components/Cell"; import { Widget } from "./components/Widget"; import { WidgetHeaderContext } from "./components/WidgetHeaderContext"; -import { useSelectActionHelper } from "./helpers/SelectActionHelper"; -import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; +import { ProgressStore } from "./features/data-export/ProgressStore"; +import { useDataExport } from "./features/data-export/useDataExport"; import { useCellEventsController } from "./features/row-interaction/CellEventsController"; import { useCheckboxEventsController } from "./features/row-interaction/CheckboxEventsController"; -import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; -import { useOnResetFiltersEvent } from "@mendix/widget-plugin-external-events/hooks"; +import { useSelectActionHelper } from "./helpers/SelectActionHelper"; import { IColumnGroupStore } from "./helpers/state/ColumnGroupStore"; -import { observer } from "mobx-react-lite"; import { RootGridStore } from "./helpers/state/RootGridStore"; import { useRootStore } from "./helpers/state/useRootStore"; -import { useDataExport } from "./features/data-export/useDataExport"; -import { ProgressStore } from "./features/data-export/ProgressStore"; interface Props extends DatagridContainerProps { columnsStore: IColumnGroupStore; diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts index d03c90f62b..02492e1f31 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DerivedLoaderController.ts @@ -1,9 +1,16 @@ import { computed, makeObservable } from "mobx"; type DerivedLoaderControllerSpec = { + showSilentRefresh: boolean; + refreshIndicator: boolean; exp: { exporting: boolean }; cols: { loaded: boolean }; - query: { isFetchingNextBatch: boolean; isFirstLoad: boolean; isRefreshing: boolean; showRefreshIndicator: boolean }; + query: { + isFetchingNextBatch: boolean; + isFirstLoad: boolean; + isRefreshing: boolean; + isSilentRefresh: boolean; + }; }; export class DerivedLoaderController { @@ -33,10 +40,20 @@ export class DerivedLoaderController { } get isRefreshing(): boolean { - return this.spec.query.isRefreshing; + const { isSilentRefresh, isRefreshing } = this.spec.query; + + if (this.spec.showSilentRefresh) { + return isSilentRefresh || isRefreshing; + } + + return !isSilentRefresh && isRefreshing; } get showRefreshIndicator(): boolean { - return this.spec.query.showRefreshIndicator; + if (!this.spec.refreshIndicator) { + return false; + } + + return this.isRefreshing; } } diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts index 805a4c751a..379b54a67a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts @@ -82,6 +82,8 @@ export class RootGridStore extends BaseControllerHost { this.loaderCtrl = new DerivedLoaderController({ exp: exportCtrl, cols: this.columnsStore, + showSilentRefresh: props.refreshInterval > 1, + refreshIndicator: props.refreshIndicator, query }); diff --git a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts index bd4ff5be4d..9a5c20e5d2 100644 --- a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts +++ b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts @@ -32,14 +32,14 @@ describe("DatasourceController loading states", () => { it("refresh has no effect if ds is loading", () => { expect(provider.gate.props.datasource.status).toBe("loading"); - controller.refresh(); + controller.backgroundRefresh(); expect(controller.isRefreshing).toBe(false); }); it("isRefreshing is true after refresh call", () => { provider.setProps({ datasource: list(0), refreshIndicator: false, refreshInterval: 0 }); expect(provider.gate.props.datasource.status).toBe("available"); - controller.refresh(); + controller.backgroundRefresh(); expect(controller.isRefreshing).toBe(true); provider.setProps({ datasource: list.loading(), refreshIndicator: false, refreshInterval: 0 }); expect(provider.gate.props.datasource.status).toBe("loading"); @@ -67,7 +67,7 @@ describe("DatasourceController loading states", () => { }); it("triggers refresh when called", () => { - controller.refresh(); + controller.backgroundRefresh(); expect(datasource.reload).toHaveBeenCalled(); }); diff --git a/packages/shared/widget-plugin-grid/src/__tests__/RefreshController.spec.ts b/packages/shared/widget-plugin-grid/src/__tests__/RefreshController.spec.ts index 63833d4c97..582b35e06f 100644 --- a/packages/shared/widget-plugin-grid/src/__tests__/RefreshController.spec.ts +++ b/packages/shared/widget-plugin-grid/src/__tests__/RefreshController.spec.ts @@ -3,8 +3,8 @@ import { RefreshController } from "../query/RefreshController"; describe("RefreshController", () => { let host: ReactiveControllerHost; - let queryHelper: { refresh: jest.Mock }; - let atom: { get: () => { refresh: jest.Mock } }; + let queryHelper: { backgroundRefresh: jest.Mock }; + let atom: { get: () => { backgroundRefresh: jest.Mock } }; let addControllerMock: jest.Mock; beforeEach(() => { @@ -13,7 +13,7 @@ describe("RefreshController", () => { addController: addControllerMock } as unknown as ReactiveControllerHost; queryHelper = { - refresh: jest.fn() + backgroundRefresh: jest.fn() }; atom = { get: () => queryHelper }; jest.useFakeTimers(); @@ -40,7 +40,7 @@ describe("RefreshController", () => { expect(dispose).toBeInstanceOf(Function); jest.advanceTimersByTime(1000); - expect(queryHelper.refresh).toHaveBeenCalledTimes(1); + expect(queryHelper.backgroundRefresh).toHaveBeenCalledTimes(1); }); it("should clear the timer when dispose is called", () => { @@ -50,6 +50,6 @@ describe("RefreshController", () => { dispose!(); jest.advanceTimersByTime(1000); - expect(queryHelper.refresh).not.toHaveBeenCalled(); + expect(queryHelper.backgroundRefresh).not.toHaveBeenCalled(); }); }); diff --git a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts index 8008fcad74..6fe2f5a66f 100644 --- a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts +++ b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts @@ -1,20 +1,19 @@ +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; import { ListValue, ValueStatus } from "mendix"; -import { action, autorun, computed, IComputedValue, makeAutoObservable, when } from "mobx"; +import { action, autorun, computed, IComputedValue, makeAutoObservable } from "mobx"; import { QueryController } from "./query-controller"; -import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; -type Gate = DerivedPropsGate<{ datasource: ListValue; refreshIndicator: boolean; refreshInterval: number }>; +type Gate = DerivedPropsGate<{ datasource: ListValue }>; type DatasourceControllerSpec = { gate: Gate }; export class DatasourceController implements ReactiveController, QueryController { private gate: Gate; - private refreshing = false; + private backgroundCheck = false; private fetching = false; private pageSize = Infinity; - private isLoaded = false; constructor(host: ReactiveControllerHost, spec: DatasourceControllerSpec) { host.addController(this); @@ -39,31 +38,27 @@ export class DatasourceController implements ReactiveController, QueryController } private resetFlags(): void { - this.refreshing = false; + this.backgroundCheck = false; this.fetching = false; } private updateFlags(status: ValueStatus): void { - if (this.refreshing) { - this.setRefreshing(status === "loading"); + if (this.backgroundCheck) { + this.setBackgroundCheck(status === "loading"); } if (this.fetching) { this.setFetching(status === "loading"); } } - private setRefreshing(value: boolean): void { - this.refreshing = value; + private setBackgroundCheck(value: boolean): void { + this.backgroundCheck = value; } private setFetching(value: boolean): void { this.fetching = value; } - private setIsLoaded(value: boolean): void { - this.isLoaded = value; - } - private resetLimit(): void { this.datasource.setLimit(this.pageSize); } @@ -77,11 +72,15 @@ export class DatasourceController implements ReactiveController, QueryController } get isFirstLoad(): boolean { - return !this.isLoaded; + return this.isDSLoading && this.datasource.items === undefined; } get isRefreshing(): boolean { - return this.refreshing; + return this.isDSLoading && this.datasource.items !== undefined; + } + + get isSilentRefresh(): boolean { + return this.backgroundCheck; } get isFetchingNextBatch(): boolean { @@ -114,20 +113,9 @@ export class DatasourceController implements ReactiveController, QueryController return computed(data); } - get showRefreshIndicator(): boolean { - const { refreshIndicator, refreshInterval } = this.gate.props; - return refreshIndicator && refreshInterval > 1 && this.isDSLoading && !this.isFirstLoad; - } - setup(): () => void { const [add, disposeAll] = disposeBatch(); - add( - when( - () => !this.isDSLoading, - () => this.setIsLoaded(true) - ) - ); add( autorun(() => { // Always use actions to set flags to avoid subscribing to them @@ -138,11 +126,11 @@ export class DatasourceController implements ReactiveController, QueryController return disposeAll; } - refresh(): void { + backgroundRefresh(): void { if (this.isDSLoading) { return; } - this.setRefreshing(true); + this.setBackgroundCheck(true); this.datasource.reload(); } diff --git a/packages/shared/widget-plugin-grid/src/query/RefreshController.ts b/packages/shared/widget-plugin-grid/src/query/RefreshController.ts index 51e9964b33..0df710f72f 100644 --- a/packages/shared/widget-plugin-grid/src/query/RefreshController.ts +++ b/packages/shared/widget-plugin-grid/src/query/RefreshController.ts @@ -2,7 +2,7 @@ import { autoEffect } from "@mendix/widget-plugin-mobx-kit/autoEffect"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/main"; interface QueryHelper { - refresh(): void; + backgroundRefresh(): void; } interface ObservableAtom { @@ -34,7 +34,7 @@ export class RefreshController implements ReactiveController { private scheduleRefresh(helper: QueryHelper, delay: number): () => void { const timerId = setTimeout(() => { - helper.refresh(); + helper.backgroundRefresh(); }, delay); return () => { clearTimeout(timerId); diff --git a/packages/shared/widget-plugin-grid/src/query/query-controller.ts b/packages/shared/widget-plugin-grid/src/query/query-controller.ts index a3cf6a7214..a5fb0421b3 100644 --- a/packages/shared/widget-plugin-grid/src/query/query-controller.ts +++ b/packages/shared/widget-plugin-grid/src/query/query-controller.ts @@ -12,7 +12,7 @@ type Members = | "hasMoreItems"; export interface QueryController extends Pick { - refresh(): void; + backgroundRefresh(): void; setPageSize(size: number): void; hasMoreItems: boolean; isFirstLoad: boolean; From f397317b2b8afe665a4fea067350be86ff80c59c Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 4 Aug 2025 15:18:07 +0200 Subject: [PATCH 19/19] test(widget-plugin-grid): enhance DatasourceController tests for loading states and add assertions --- .../__tests__/DatasourceController.spec.ts | 89 +++++++++++++++---- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts index 9a5c20e5d2..4385be3258 100644 --- a/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts +++ b/packages/shared/widget-plugin-grid/src/__tests__/DatasourceController.spec.ts @@ -1,7 +1,7 @@ import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost"; import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider"; // import { ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/main"; -import { list } from "@mendix/widget-plugin-test-utils"; +import { list, obj } from "@mendix/widget-plugin-test-utils"; import { ListValue } from "mendix"; import { DatasourceController } from "../query/DatasourceController"; @@ -26,31 +26,40 @@ describe("DatasourceController loading states", () => { provider.setProps({ datasource, refreshIndicator: false, refreshInterval: 0 }); }); - it("isFirstLoad returns true by default", () => { + it("isFirstLoad returns true when loading and items undefined", () => { expect(controller.isFirstLoad).toBe(true); }); - it("refresh has no effect if ds is loading", () => { - expect(provider.gate.props.datasource.status).toBe("loading"); + it("backgroundRefresh does not trigger reload if already loading", () => { + const reloadSpy = jest.spyOn(provider.gate.props.datasource, "reload"); controller.backgroundRefresh(); + expect(reloadSpy).not.toHaveBeenCalled(); + }); + + it("isRefreshing is false when loading and items undefined", () => { expect(controller.isRefreshing).toBe(false); }); - it("isRefreshing is true after refresh call", () => { - provider.setProps({ datasource: list(0), refreshIndicator: false, refreshInterval: 0 }); - expect(provider.gate.props.datasource.status).toBe("available"); + it("isRefreshing is true after backgroundRefresh when items are present", () => { + // Set datasource to available with items + provider.setProps({ datasource: list(1), refreshIndicator: false, refreshInterval: 0 }); + // Simulate refresh controller.backgroundRefresh(); + // Replace datasource with a mock in loading state and items present + const loadingWithItems = { ...list.loading(), items: [obj()] }; + provider.setProps({ datasource: loadingWithItems, refreshIndicator: false, refreshInterval: 0 }); expect(controller.isRefreshing).toBe(true); - provider.setProps({ datasource: list.loading(), refreshIndicator: false, refreshInterval: 0 }); - expect(provider.gate.props.datasource.status).toBe("loading"); - expect(controller.isRefreshing).toBe(true); - expect(controller.isFirstLoad).toBe(false); }); - it("isFetchingNextBatch returns true after setLimit call", () => { + it("isFetchingNextBatch is true after setLimit call", () => { controller.setLimit(20); expect(controller.isFetchingNextBatch).toBe(true); - expect(controller.isFirstLoad).toBe(true); + }); + + it("isSilentRefresh is true after backgroundRefresh", () => { + provider.setProps({ datasource: list(1), refreshIndicator: false, refreshInterval: 0 }); + controller.backgroundRefresh(); + expect(controller.isSilentRefresh).toBe(true); }); }); @@ -64,16 +73,62 @@ describe("DatasourceController loading states", () => { expect(controller.isFirstLoad).toBe(false); expect(controller.isRefreshing).toBe(false); expect(controller.isFetchingNextBatch).toBe(false); + expect(controller.isSilentRefresh).toBe(false); }); - it("triggers refresh when called", () => { + it("backgroundRefresh triggers reload when not loading", () => { + const reloadSpy = jest.spyOn(datasource, "reload"); controller.backgroundRefresh(); - expect(datasource.reload).toHaveBeenCalled(); + expect(reloadSpy).toHaveBeenCalled(); }); - it("triggers setLimit when called", () => { + it("setLimit triggers setLimit on datasource", () => { + const setLimitSpy = jest.spyOn(datasource, "setLimit"); controller.setLimit(20); - expect(datasource.setLimit).toHaveBeenCalledWith(20); + expect(setLimitSpy).toHaveBeenCalledWith(20); + }); + + it("setOffset triggers setOffset and resets flags", () => { + const setOffsetSpy = jest.spyOn(datasource, "setOffset"); + controller.setOffset(5); + expect(setOffsetSpy).toHaveBeenCalledWith(5); + }); + + it("setSortOrder triggers setSortOrder and resets flags", () => { + const setSortOrderSpy = jest.spyOn(datasource, "setSortOrder"); + // Use Mendix Option_2 structure (runtime shape) + const sortOption = { some: [{ attribute: "name", direction: "asc" }] }; + // @ts-expect-error: Mendix Option_2 type not available in workspace + controller.setSortOrder(sortOption); + expect(setSortOrderSpy).toHaveBeenCalledWith(sortOption); + }); + + it("setFilter triggers setFilter and resets flags", () => { + const setFilterSpy = jest.spyOn(datasource, "setFilter"); + // Use Mendix Option_2 structure (runtime shape) + const filterOption = { some: { attribute: "name", operator: "equals", value: "test" } }; + // @ts-expect-error: Mendix Option_2 type not available in workspace + controller.setFilter(filterOption); + expect(setFilterSpy).toHaveBeenCalledWith(filterOption); + }); + + it("setPageSize updates pageSize property", () => { + controller.setPageSize(50); + // @ts-expect-error: private property + expect(controller.pageSize).toBe(50); + }); + + it("requestTotalCount triggers datasource.requestTotalCount", () => { + const spy = jest.spyOn(datasource, "requestTotalCount"); + controller.requestTotalCount(true); + expect(spy).toHaveBeenCalledWith(true); + }); + + it("derivedQuery returns a computed value and updates on datasource change", () => { + const derived = controller.derivedQuery; + expect(typeof derived.get).toBe("function"); + provider.setProps({ datasource: list(2), refreshIndicator: false, refreshInterval: 0 }); + expect(derived.get()).toBeInstanceOf(DatasourceController); }); }); });