diff --git a/packages/pluggableWidgets/gallery-web/CHANGELOG.md b/packages/pluggableWidgets/gallery-web/CHANGELOG.md index 8b781149aa..d26438f059 100644 --- a/packages/pluggableWidgets/gallery-web/CHANGELOG.md +++ b/packages/pluggableWidgets/gallery-web/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We fixed an issue where clicking a label inside the item content, such as the label of a checkbox, triggered the double click action with a single click. +- We fixed an issue where the "Refresh time (in seconds)" property had no effect and the gallery did not reload its data periodically. + ## [3.11.7] - 2026-10-02 ### Fixed diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/.openspec.yaml b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/.openspec.yaml new file mode 100644 index 0000000000..dc36fb1bf8 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-21 diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/design.md b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/design.md new file mode 100644 index 0000000000..e5b2ff3f2a --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/design.md @@ -0,0 +1,23 @@ +## Context + +`DatasourceService` (`@mendix/widget-plugin-grid/src/core/Datasource.service.ts`) already contains the refresh timer. It takes `refreshIntervalMs` as an optional third constructor argument and schedules `backgroundRefresh()` when the value is above 0. Gallery injects it with `injected(DatasourceService, CORE.setupService, GY.queryGate, GY.refreshInterval.optional)`. Because `GY.refreshInterval` is never bound, brandi resolves the optional token to `undefined`, the constructor default of `0` applies, and the timer is never scheduled. + +Datagrid wires the same service correctly: `datagridConfig` sets `refreshIntervalMs: props.refreshInterval * 1000`, and its main binding group runs `container.bind(DG.refreshInterval).toConstant(config.refreshIntervalMs)`. + +## Goals / Non-Goals + +**Goals:** restore the pre-migration behaviour with the smallest change, mirroring Datagrid. + +**Non-Goals:** + +- Changing `DatasourceService`. +- Changing the `showSilentRefresh` threshold (`props.refreshInterval > 1`, which was `>= 1` before the migration). That is a separate, cosmetic difference and is left as is. + +## Decisions + +- **Derive in config, bind in `_02_queryBindings.init`.** The ms value lives in `GalleryConfig`, the single source for derived settings, and the token is bound next to `GY.queryGate`, where `DatasourceService` gets its other dependencies. _Alternative:_ bind directly from `props` in `init`. Rejected because it would bypass `GalleryConfig`, which already declares `refreshIntervalMs`. +- **Container-level tests with fake timers.** These run `CORE.setupService.setup()` and advance timers, so they prove the full path from props through config, token and service to `datasource.reload()`. A config-only assertion would not have caught the missing binding. + +## Risks / Trade-offs + +- [Apps that set a refresh time while it was broken now start reloading] → This is the documented, intended behaviour, and it is called out in the changelog. diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/proposal.md b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/proposal.md new file mode 100644 index 0000000000..db724952f9 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/proposal.md @@ -0,0 +1,26 @@ +## Why + +Gallery's "Refresh time (in seconds)" property has no effect (WC-3630). When the widget moved to a DI container (`b5010ee79`, "feat: move to brandi", Jan 2026), `galleryConfig` was given a placeholder `refreshIntervalMs: 0`, and the `GY.refreshInterval` token injected into `DatasourceService` was never bound. Before the migration, `GalleryStore` passed `refreshInterval * 1000` to `DatasourceService`, so this is a regression. Apps that rely on Gallery refreshing periodically now show stale data. + +## What Changes + +- `galleryConfig` derives `refreshIntervalMs` from `props.refreshInterval * 1000`. +- The query binding group binds `GY.refreshInterval` to `config.refreshIntervalMs`, which is how Datagrid binds `DG.refreshInterval`. +- Regression tests at container level: the value reaches the config and token, the datasource reloads after the interval, and nothing reloads when the interval is 0. +- A CHANGELOG "Fixed" entry. + +## Capabilities + +### New Capabilities + +- `refresh-interval`: Gallery periodically reloads its datasource according to the "Refresh time (in seconds)" property. + +### Modified Capabilities + + + +## Impact + +- `src/model/configs/Gallery.config.ts`, `src/model/containers/Gallery.container.ts`, `src/model/containers/__tests__/createGalleryContainer.spec.ts`, `CHANGELOG.md`. +- No XML, typings or dependency changes. `DatasourceService` (`@mendix/widget-plugin-grid`) is unchanged. +- Patch-level fix. diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/specs/refresh-interval/spec.md b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/specs/refresh-interval/spec.md new file mode 100644 index 0000000000..f0e9128d67 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/specs/refresh-interval/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Refresh time drives periodic datasource reload + +When the Gallery's `refreshInterval` property is greater than `0`, the widget SHALL reload its datasource `refreshInterval` seconds after the most recent data change. It SHALL NOT start a reload while the datasource is already loading. + +#### Scenario: Reload after configured interval + +- **WHEN** a Gallery is rendered with `refreshInterval` = 5 and its data does not change +- **THEN** `datasource.reload()` is not called before 5 seconds and is called once at 5 seconds + +#### Scenario: Interval value converted to milliseconds + +- **WHEN** a Gallery is configured with `refreshInterval` = 5 +- **THEN** the datasource service receives a refresh interval of 5000 ms + +### Requirement: Refresh disabled at zero + +When `refreshInterval` is `0`, the Gallery SHALL NOT reload its datasource automatically. + +#### Scenario: No reload by default + +- **WHEN** a Gallery is rendered with `refreshInterval` = 0 +- **THEN** `datasource.reload()` is not called by the widget, however long it stays mounted diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/tasks.md b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/tasks.md new file mode 100644 index 0000000000..06b4da7e2c --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/fix-refresh-interval-wiring/tasks.md @@ -0,0 +1,17 @@ +## 1. Fix wiring + +- [x] 1.1 In `src/model/configs/Gallery.config.ts`, set `refreshIntervalMs: props.refreshInterval * 1000` +- [x] 1.2 In `src/model/containers/Gallery.container.ts` `_02_queryBindings.init`, bind `GY.refreshInterval` to `config.refreshIntervalMs` + +## 2. Regression tests (`createGalleryContainer.spec.ts`) + +- [x] 2.1 Test: `refreshInterval: 5` gives `config.refreshIntervalMs` and `GY.refreshInterval` equal to 5000 +- [x] 2.2 Test (fake timers): after `setupService.setup()`, `datasource.reload` is not called at 4999 ms and is called once at 5000 ms +- [x] 2.3 Test (fake timers): `refreshInterval: 0` means no reload after 60 s +- [x] 2.4 Confirm RED without the fix (2.1 and 2.2 fail) and GREEN with it + +## 3. Changelog and verification + +- [x] 3.1 Add a "Fixed" entry under `[Unreleased]` in `CHANGELOG.md` +- [x] 3.2 `pnpm run test` and `pnpm run lint` pass in `packages/pluggableWidgets/gallery-web` +- [ ] 3.3 Manual QA in Studio Pro: Gallery with Refresh time = 5 picks up data changed elsewhere within ~5 s; with 0 it does not diff --git a/packages/pluggableWidgets/gallery-web/src/model/configs/Gallery.config.ts b/packages/pluggableWidgets/gallery-web/src/model/configs/Gallery.config.ts index 325d1e1cd0..acf7d398c0 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/configs/Gallery.config.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/configs/Gallery.config.ts @@ -28,7 +28,7 @@ export function galleryConfig(props: GalleryContainerProps): GalleryConfig { return { id, name: props.name, - refreshIntervalMs: 0, + refreshIntervalMs: props.refreshInterval * 1000, selectionEnabled: isSelectionEnabled(props), selectionType: selectionType(props), selectionMode: props.itemSelectionMode, diff --git a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts index c773901f03..a83989432d 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts @@ -86,8 +86,9 @@ const _02_queryBindings: BindingGroup = { container.bind(GY.query).toInstance(DatasourceService).inSingletonScope(); container.bind(GY.queryParams).toInstance(QueryParamsService).inSingletonScope(); }, - init(container, { mainGate }) { + init(container, { mainGate, config }) { container.bind(GY.queryGate).toConstant(mainGate); + container.bind(GY.refreshInterval).toConstant(config.refreshIntervalMs); }, postInit(container) { // Create param service instance. diff --git a/packages/pluggableWidgets/gallery-web/src/model/containers/__tests__/createGalleryContainer.spec.ts b/packages/pluggableWidgets/gallery-web/src/model/containers/__tests__/createGalleryContainer.spec.ts index 56f13c9d19..008d4ad346 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/containers/__tests__/createGalleryContainer.spec.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/containers/__tests__/createGalleryContainer.spec.ts @@ -62,4 +62,40 @@ describe("createGalleryContainer", () => { selectionEnabled: false }); }); + + it("bind refresh interval in milliseconds to the container", () => { + const [container] = createGalleryContainer({ ...mockContainerProps(), refreshInterval: 5 }); + + expect(container.get(CORE.config).refreshIntervalMs).toBe(5000); + expect(container.get(GY.refreshInterval)).toBe(5000); + }); + + describe("refresh interval", () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + it("reloads datasource after the refresh interval", () => { + const props = { ...mockContainerProps(), refreshInterval: 5 }; + const [container] = createGalleryContainer(props); + const dispose = container.get(CORE.setupService).setup(); + + jest.advanceTimersByTime(4999); + expect(props.datasource.reload).not.toHaveBeenCalled(); + jest.advanceTimersByTime(1); + expect(props.datasource.reload).toHaveBeenCalledTimes(1); + + dispose(); + }); + + it("does not reload datasource when refresh interval is 0", () => { + const props = mockContainerProps(); + const [container] = createGalleryContainer(props); + const dispose = container.get(CORE.setupService).setup(); + + jest.advanceTimersByTime(60_000); + expect(props.datasource.reload).not.toHaveBeenCalled(); + + dispose(); + }); + }); });