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..ed8c75235c 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,86 @@ $root: ".widget-datagrid";
display: contents;
}
+ &-refresh-container {
+ grid-column: 1 / -1;
+ padding: 0;
+ position: relative;
+ }
+
+ &-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%;
+ position: absolute;
+ left: 0;
+ right: 0;
+
+ &::-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/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md
index 359240dd41..ad66adaba6 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 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.
+
## [2.30.6] - 2025-05-28
### Fixed
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
index ba1df3f098..31c1184c3d 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
@@ -141,10 +141,12 @@ export function preview(props: DatagridPreviewProps): ReactElement {
cellEventsController={eventsController}
checkboxEventsController={eventsController}
focusController={focusController}
+ isFirstLoad={false}
isLoading={false}
isFetchingNextBatch={false}
loadingType="spinner"
columnsLoading={false}
+ refreshIndicator={props.refreshIndicator}
/>
);
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
index 78abad3963..1a18f8c186 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
@@ -118,10 +118,12 @@ 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/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/src/components/GridBody.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx
index dc3bac60c7..64c1ca93b8 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;
+ isFirstLoad: boolean;
isFetchingNextBatch?: boolean;
columnsHidable: boolean;
columnsSize: number;
@@ -20,7 +20,7 @@ export function GridBody(props: Props): ReactElement {
const { children } = props;
const content = (): React.ReactElement => {
- if (props.isLoading) {
+ if (props.isFirstLoad) {
return 0 ? props.rowsSize : props.pageSize} />;
}
return (
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..048f5b704c 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;
@@ -65,10 +66,12 @@ export interface WidgetProps(props: WidgetProps): ReactElemen
paging,
pagingPosition,
preview,
+ refreshIndicator,
selectActionHelper,
setPage,
visibleColumns
@@ -161,6 +165,8 @@ const Main = observer((props: WidgetProps): ReactElemen
const selectionEnabled = selectActionHelper.selectionType !== "None";
+ const showRefreshIndicator = refreshIndicator && props.isLoading && !props.isFirstLoad;
+
return (
{showTopBar && {pagination}}
@@ -189,8 +195,9 @@ const Main = observer((props: WidgetProps): ReactElemen
isLoading={props.columnsLoading}
preview={props.preview}
/>
+ {showRefreshIndicator ? : null}
{
selectActionHelper: mockSelectionProps(),
cellEventsController: { getProps: () => Object.create({}) },
checkboxEventsController: { getProps: () => Object.create({}) },
+ isFirstLoad: false,
isLoading: false,
isFetchingNextBatch: false,
loadingType: "spinner",
columnsLoading: false,
+ refreshIndicator: false,
focusController: new FocusTargetController(
new PositionController(),
new VirtualGridLayout(1, columns.length, 10)
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;
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;
}