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;