Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/pluggableWidgets/gallery-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-21
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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

<!-- None: gallery-web has no existing spec for refresh behaviour. -->

## 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.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
Loading