diff --git a/packages/pluggableWidgets/gallery-web/CHANGELOG.md b/packages/pluggableWidgets/gallery-web/CHANGELOG.md
index 7b53818115..cf7ad62fe3 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 added a refresh interval property, to allow defining an interval (in seconds) for refreshing the content in Gallery
+
## [3.7.0] - 2025-11-11
### Added
diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml
index 1c9a0b931e..ffe357ae00 100644
--- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml
+++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml
@@ -16,6 +16,10 @@
Data source
+
+ Refresh time (in seconds)
+
+
Selection
diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx b/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx
index 4d92d8ba90..3551501fef 100644
--- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx
+++ b/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx
@@ -1,11 +1,16 @@
-import { listAction, listExp, setupIntersectionObserverStub } from "@mendix/widget-plugin-test-utils";
+import { listAction, listExpression, setupIntersectionObserverStub } from "@mendix/widget-plugin-test-utils";
import "@testing-library/jest-dom";
import { render, waitFor } from "@testing-library/react";
import { ObjectItem } from "mendix";
+import { createElement } from "react";
import { ItemHelperBuilder } from "../../utils/builders/ItemHelperBuilder";
import { mockItemHelperWithAction, mockProps, setup, withGalleryContext } from "../../utils/test-utils";
import { Gallery } from "../Gallery";
+jest.mock("@mendix/widget-plugin-component-kit/RefreshIndicator", () => ({
+ RefreshIndicator: (_props: any) => createElement("div", { "data-testid": "refresh-indicator" })
+}));
+
describe("Gallery", () => {
beforeAll(() => {
setupIntersectionObserverStub();
@@ -24,6 +29,20 @@ describe("Gallery", () => {
expect(asFragment()).toMatchSnapshot();
});
+
+ it("renders RefreshIndicator when `showRefreshIndicator` is true", () => {
+ const base = mockProps();
+ const props = { ...base, showRefreshIndicator: true };
+ const { getByTestId } = render(withGalleryContext());
+ expect(getByTestId("refresh-indicator")).toBeInTheDocument();
+ });
+
+ it("does not render RefreshIndicator when `showRefreshIndicator` is false", () => {
+ const base = mockProps();
+ const props = { ...base, showRefreshIndicator: false };
+ const { queryByTestId } = render(withGalleryContext());
+ expect(queryByTestId("refresh-indicator")).toBeNull();
+ });
});
describe("with on click action", () => {
@@ -84,7 +103,9 @@ describe("Gallery", () => {
withGalleryContext(
b.withItemClass(listExp(() => "custom-class")))}
+ itemHelper={ItemHelperBuilder.sample(b =>
+ b.withItemClass(listExpression(() => "custom-class"))
+ )}
/>
)
);
diff --git a/packages/pluggableWidgets/gallery-web/src/controllers/DerivedLoaderController.ts b/packages/pluggableWidgets/gallery-web/src/controllers/DerivedLoaderController.ts
index c4ff2931d0..0585e621db 100644
--- a/packages/pluggableWidgets/gallery-web/src/controllers/DerivedLoaderController.ts
+++ b/packages/pluggableWidgets/gallery-web/src/controllers/DerivedLoaderController.ts
@@ -4,20 +4,39 @@ import { computed, makeObservable } from "mobx";
export class DerivedLoaderController {
constructor(
private datasourceService: DatasourceService,
- private refreshIndicator: boolean
+ private refreshIndicator: boolean,
+ private showSilentRefresh: boolean
) {
makeObservable(this, {
- isRefreshing: computed,
- showRefreshIndicator: computed
+ isFirstLoad: computed,
+ isFetchingNextBatch: computed,
+ isRefreshing: computed
});
}
+ get isFirstLoad(): boolean {
+ return this.datasourceService.isFirstLoad;
+ }
+
+ get isFetchingNextBatch(): boolean {
+ return this.datasourceService.isFetchingNextBatch;
+ }
+
get isRefreshing(): boolean {
const { isSilentRefresh, isRefreshing } = this.datasourceService;
+
+ if (this.showSilentRefresh) {
+ return isSilentRefresh || isRefreshing;
+ }
+
return !isSilentRefresh && isRefreshing;
}
get showRefreshIndicator(): boolean {
- return this.refreshIndicator && this.isRefreshing;
+ if (!this.refreshIndicator) {
+ return false;
+ }
+
+ return this.isRefreshing;
}
}
diff --git a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts
index c92645b252..448a280229 100644
--- a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts
+++ b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts
@@ -36,6 +36,7 @@ interface StaticProps {
storeFilters: boolean;
storeSort: boolean;
refreshIndicator: boolean;
+ refreshInterval: number;
}
export type GalleryPropsGate = DerivedPropsGate;
@@ -63,7 +64,7 @@ export class GalleryStore extends SetupHost {
this.name = spec.name;
- this._query = new DatasourceService(this, spec.gate, 0 * 1000);
+ this._query = new DatasourceService(this, spec.gate, spec.refreshInterval * 1000);
this.paging = new PaginationController({
query: this._query,
@@ -95,7 +96,7 @@ export class GalleryStore extends SetupHost {
host: this._sortHost
};
- this.loaderCtrl = new DerivedLoaderController(this._query, spec.refreshIndicator);
+ this.loaderCtrl = new DerivedLoaderController(this._query, spec.refreshIndicator, spec.refreshInterval >= 1);
const useStorage = spec.storeFilters || spec.storeSort;
if (useStorage) {
diff --git a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx
index 4fc8a0b41b..0d5ac84aa3 100644
--- a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx
+++ b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx
@@ -58,7 +58,8 @@ export function createMockGalleryContext(): GalleryRootScope {
storeSort: false,
refreshIndicator: false,
keepSelection: false,
- selectionCountPosition: "bottom"
+ selectionCountPosition: "bottom",
+ refreshInterval: 0
};
// Create a proper gate provider and gate
@@ -76,7 +77,8 @@ export function createMockGalleryContext(): GalleryRootScope {
stateStorageType: "localStorage",
storeFilters: false,
storeSort: false,
- refreshIndicator: false
+ refreshIndicator: false,
+ refreshInterval: 0
});
const mockSelectHelper = new SelectActionHandler("None", undefined);
diff --git a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
index b0cfa98317..8f5b2b1f24 100644
--- a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
+++ b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
@@ -29,6 +29,7 @@ export interface GalleryContainerProps {
tabIndex?: number;
filtersPlaceholder?: ReactNode;
datasource: ListValue;
+ refreshInterval: number;
itemSelection?: SelectionSingleValue | SelectionMultiValue;
itemSelectionMode: ItemSelectionModeEnum;
keepSelection: boolean;
@@ -76,6 +77,7 @@ export interface GalleryPreviewProps {
translate: (text: string) => string;
filtersPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
datasource: {} | { caption: string } | { type: string } | null;
+ refreshInterval: number | null;
itemSelection: "None" | "Single" | "Multi";
itemSelectionMode: ItemSelectionModeEnum;
keepSelection: boolean;