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 9159447bd4..e311d2e0d9 100644
--- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss
+++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss
@@ -429,86 +429,6 @@ $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 3s 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/modules/data-widgets/src/themesource/datawidgets/web/_refresh-indicator.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_refresh-indicator.scss
new file mode 100644
index 0000000000..8213ceab85
--- /dev/null
+++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_refresh-indicator.scss
@@ -0,0 +1,83 @@
+.mx-refresh-container {
+ grid-column: 1 / -1;
+ padding: 0;
+ position: relative;
+
+ &-padding {
+ padding: var(--spacing-small) 0;
+ }
+}
+
+.mx-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 3s 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%;
+ }
+ }
+}
diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss
index 19fbcb8846..c1d5304319 100644
--- a/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss
+++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss
@@ -6,6 +6,7 @@
@import "drop-down-sort";
@import "gallery";
@import "gallery-design-properties";
+@import "refresh-indicator";
@import "three-state-checkbox";
@import "tree-node";
@import "tree-node-design-properties";
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx b/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx
deleted file mode 100644
index 79f3f3a10f..0000000000
--- a/packages/pluggableWidgets/datagrid-web/src/components/RefreshIndicator.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-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 f169d66fa8..f94ad45f91 100644
--- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx
@@ -1,3 +1,4 @@
+import { RefreshIndicator } from "@mendix/widget-plugin-component-kit/RefreshIndicator";
import { Pagination } from "@mendix/widget-plugin-grid/components/Pagination";
import { SelectionStatus } from "@mendix/widget-plugin-grid/selection";
import classNames from "classnames";
@@ -25,7 +26,6 @@ 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;
diff --git a/packages/pluggableWidgets/gallery-web/CHANGELOG.md b/packages/pluggableWidgets/gallery-web/CHANGELOG.md
index 07d419d898..04c091a039 100644
--- a/packages/pluggableWidgets/gallery-web/CHANGELOG.md
+++ b/packages/pluggableWidgets/gallery-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 Gallery.
+
## [3.2.0] - 2025-08-18
### Changed
diff --git a/packages/pluggableWidgets/gallery-web/package.json b/packages/pluggableWidgets/gallery-web/package.json
index 70c2de87a9..97cfa5a144 100644
--- a/packages/pluggableWidgets/gallery-web/package.json
+++ b/packages/pluggableWidgets/gallery-web/package.json
@@ -1,7 +1,7 @@
{
"name": "@mendix/gallery-web",
"widgetName": "Gallery",
- "version": "3.2.0",
+ "version": "3.3.0",
"description": "A flexible gallery widget that renders columns, rows and layouts.",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx
index e6471a9e03..13ca8e18cf 100644
--- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx
+++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx
@@ -104,6 +104,7 @@ function Preview(props: GalleryPreviewProps): ReactElement {
itemEventsController={itemEventsController}
focusController={focusController}
getPosition={getPositionCallback}
+ showRefreshIndicator={false}
preview
/>
);
diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx
index 37e665ed95..d769ffbc1e 100644
--- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx
+++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx
@@ -91,6 +91,7 @@ const Container = observer(function GalleryContainer(props: GalleryContainerProp
focusController={focusController}
getPosition={getPositionCallback}
loadMoreButtonCaption={props.loadMoreButtonCaption?.value}
+ showRefreshIndicator={rootStore.loaderCtrl.showRefreshIndicator}
/>
);
});
diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml
index c613422fa9..5853598503 100644
--- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml
+++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml
@@ -37,6 +37,10 @@
Content placeholder
+
+ Show refresh indicator
+ Show a refresh indicator when the data is being loaded.
+
diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx
index 485e00f053..9407dafaf8 100644
--- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx
+++ b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx
@@ -1,3 +1,4 @@
+import { RefreshIndicator } from "@mendix/widget-plugin-component-kit/RefreshIndicator";
import { Pagination } from "@mendix/widget-plugin-grid/components/Pagination";
import { KeyNavProvider } from "@mendix/widget-plugin-grid/keyboard-navigation/context";
import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController";
@@ -51,6 +52,7 @@ export interface GalleryProps {
selectHelper: SelectActionHandler;
getPosition: (index: number) => PositionInGrid;
loadMoreButtonCaption?: string;
+ showRefreshIndicator: boolean;
}
export function Gallery(props: GalleryProps): ReactElement {
@@ -86,6 +88,7 @@ export function Gallery(props: GalleryProps): ReactElem
>
{showTopPagination && pagination}
{props.showHeader && {props.header}}
+ {props.showRefreshIndicator ? : null}
-
+
diff --git a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts
index 59417e1e73..e0ab4c84f0 100644
--- a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts
+++ b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts
@@ -11,6 +11,7 @@ import { SortAPI } from "@mendix/widget-plugin-sorting/react/context";
import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost";
import { EditableValue, ListValue } from "mendix";
import { PaginationEnum, StateStorageTypeEnum } from "../../typings/GalleryProps";
+import { DerivedLoaderController } from "../controllers/DerivedLoaderController";
import { QueryParamsController } from "../controllers/QueryParamsController";
import { ObservableStorage } from "../typings/storage";
import { AttributeStorage } from "./AttributeStorage";
@@ -31,6 +32,7 @@ interface StaticProps {
stateStorageType: StateStorageTypeEnum;
storeFilters: boolean;
storeSort: boolean;
+ refreshIndicator: boolean;
}
export type GalleryPropsGate = DerivedPropsGate;
@@ -50,6 +52,7 @@ export class GalleryStore extends BaseControllerHost {
readonly paging: PaginationController;
readonly filterAPI: FilterAPI;
readonly sortAPI: SortAPI;
+ loaderCtrl: DerivedLoaderController;
constructor(spec: GalleryStoreSpec) {
super();
@@ -86,6 +89,8 @@ export class GalleryStore extends BaseControllerHost {
host: this._sortHost
};
+ this.loaderCtrl = new DerivedLoaderController(this._query, spec.refreshIndicator);
+
new RefreshController(this, {
delay: 0,
query: this._query.derivedQuery
diff --git a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx
index 3d53682d85..5dc93b61b2 100644
--- a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx
+++ b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx
@@ -55,7 +55,8 @@ export function createMockGalleryContext(): GalleryRootScope {
onClickTrigger: "single",
stateStorageType: "localStorage",
storeFilters: false,
- storeSort: false
+ storeSort: false,
+ refreshIndicator: false
};
// Create a proper gate provider and gate
@@ -72,7 +73,8 @@ export function createMockGalleryContext(): GalleryRootScope {
pageSize: 10,
stateStorageType: "localStorage",
storeFilters: false,
- storeSort: false
+ storeSort: false,
+ refreshIndicator: false
});
const mockSelectHelper = new SelectActionHandler("None", undefined);
@@ -148,6 +150,7 @@ export function mockProps(params: Helpers & Mocks = {}): GalleryProps,
itemEventsController,
focusController,
- getPosition: (index: number) => getColumnAndRowBasedOnIndex(3, 3, index)
+ getPosition: (index: number) => getColumnAndRowBasedOnIndex(3, 3, index),
+ showRefreshIndicator: false
};
}
diff --git a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
index a1cd505ccb..2f32acbdc7 100644
--- a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
+++ b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
@@ -30,6 +30,7 @@ export interface GalleryContainerProps {
itemSelection?: SelectionSingleValue | SelectionMultiValue;
itemSelectionMode: ItemSelectionModeEnum;
content?: ListWidgetValue;
+ refreshIndicator: boolean;
desktopItems: number;
tabletItems: number;
phoneItems: number;
@@ -71,6 +72,7 @@ export interface GalleryPreviewProps {
itemSelection: "None" | "Single" | "Multi";
itemSelectionMode: ItemSelectionModeEnum;
content: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
+ refreshIndicator: boolean;
desktopItems: number | null;
tabletItems: number | null;
phoneItems: number | null;
diff --git a/packages/shared/widget-plugin-component-kit/src/RefreshIndicator.tsx b/packages/shared/widget-plugin-component-kit/src/RefreshIndicator.tsx
new file mode 100644
index 0000000000..a1f4789689
--- /dev/null
+++ b/packages/shared/widget-plugin-component-kit/src/RefreshIndicator.tsx
@@ -0,0 +1,16 @@
+import classNames from "classnames";
+import { createElement, ReactElement } from "react";
+
+type RefreshIndicatorProps = {
+ className?: string;
+};
+
+export function RefreshIndicator({ className }: RefreshIndicatorProps): ReactElement {
+ return (
+
+ );
+}