Skip to content

Commit 601a799

Browse files
authored
[WC-3442] feat(datagrid-web): add onBeforeExport and onAfterExport event actions (#2392)
2 parents e9bf7ba + 45d60bc commit 601a799

14 files changed

Lines changed: 755 additions & 16 deletions

File tree

packages/modules/data-widgets/src/javascriptsource/datawidgets/actions/Export_To_Excel.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ export async function Export_To_Excel(datagridName, fileName, sheetName, include
7575

7676
controller.exportData(handler, {
7777
withHeaders: true,
78-
limit: chunkSize.toNumber()
78+
limit: chunkSize.toNumber(),
79+
fileName,
80+
sheetName
7981
})
8082
});
8183
// END USER CODE

packages/pluggableWidgets/datagrid-web/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- We added two optional export event actions — **On before export** and **On after export** — so developers can log export operations via a microflow or nanoflow. `On before export` fires just before the export starts and provides the grid name, visible column titles, chunk size, file name, sheet name, and start time. `On after export` fires after the export finishes (whether completed or canceled) and also provides the total number of exported rows, a status string (`"success"` or `"aborted"`), and an end time.
12+
913
## [3.11.3] - 2026-07-27
1014

1115
### Added
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-08-18
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
## Context
2+
3+
Data Grid 2 has a `data-export` feature (`src/features/data-export/`) that lets external modules (e.g. `data-exporter-web`) stream rows out of the widget. The flow:
4+
5+
1. `useDataExport` creates an `ExportController` on mount, registered in a global window map keyed by `props.name`.
6+
2. An external caller does `getExportRegistry().get("widgetName").exportData(handler, opts)`.
7+
3. `ExportController.exportData()` creates a `DSExportRequest`, streams pages from the Mendix datasource, then restores the datasource view state.
8+
4. `DSExportRequest` tracks `loaded` (rows streamed) and `limit` (rows per page) internally but exposes neither publicly.
9+
10+
There is currently no hook for the widget to observe the start or end of an export. The widget owns `ExportController`, which is the right place to add these hooks, but `ExportController` should not be coupled to Mendix `ActionValue` directly.
11+
12+
## Goals / Non-Goals
13+
14+
**Goals:**
15+
16+
- Fire `onBeforeExport` with context variables just before `req.send()` is called
17+
- Fire `onAfterExport` with outcome variables after the export resolves (success or abort)
18+
- Keep `ExportController` Mendix-API-agnostic (plain callbacks, not `ActionValue`)
19+
- Keep the export path itself unchanged in behavior and performance
20+
21+
**Non-Goals:**
22+
23+
- Awaiting the action callbacks before proceeding (fire-and-forget only)
24+
- Providing an ability to cancel the export from the callback
25+
- Surfacing chunk-level (per-page) events — only start and end
26+
- Changing how external callers trigger the export
27+
28+
## Decisions
29+
30+
### D1 — NanoEvents on ExportController, not stored callbacks
31+
32+
`ExportController` already uses a NanoEvents emitter for all internal communication (`sourcechange`, `propertieschange`, `columnschange`, `abort`, `exportend`). Storing plain callback fields and exposing setter methods would break this pattern and add a parallel, less composable mechanism.
33+
34+
**Decision**: Add `beforeexport` and `afterexport` to `ControllerEvents`. `exportData()` emits them via the existing emitter. `ExportController` exposes a public `on()` method (returning `Unsubscribe`) that mirrors the existing public `emit()`. `useDataExport` subscribes via `controller.on(...)` and uses React's `useEffect` cleanup to unsubscribe. This keeps `ExportController` fully Mendix-API-agnostic and testable without Mendix mocks.
35+
36+
**Alternative considered**: Store `onBeforeExport`/`onAfterExport` as plain callback fields with setter methods. Rejected because it breaks the existing NanoEvents communication pattern and makes the controller hold mutable state for what is fundamentally an event subscription.
37+
38+
### D2 — Subscribe once, read latest ActionValue from ref
39+
40+
Props can change between renders (e.g. action configuration changed in Studio Pro). The subscription handler must always invoke the current `ActionValue`, not the one captured at subscribe time.
41+
42+
**Decision**: Subscribe in a `useEffect` with `[entry]` deps (once per controller lifetime). Store `props.onBeforeExport` / `props.onAfterExport` in `useRef`s that are updated on every render (outside the effect). The handler closure reads from the ref at call time, so it always sees the latest `ActionValue` without resubscribing. This avoids unnecessary unsubscribe/resubscribe cycles when Mendix re-renders the widget with a new `ActionValue` reference.
43+
44+
### D3 — startTime captured in ExportController, shared between both callbacks
45+
46+
Both `onBeforeExport` and `onAfterExport` receive `startTime` (so `onAfterExport` callers can compute duration in a single microflow without storing intermediate state). The timestamp must be identical in both calls.
47+
48+
**Decision**: Capture `startTime = new Date()` in `ExportController.exportData()` before calling `onBeforeExport`, then pass the same `Date` object to `onAfterExport`.
49+
50+
### D4 — status: "success" | "aborted" via DSExportRequest.status
51+
52+
`DSExportRequest` already tracks its internal status (`"end"` vs `"aborted"`). After `await req.send()` resolves, the request's final status is readable. Map `"end"``"success"` and `"aborted"``"aborted"` for the `onAfterExport` variable.
53+
54+
**Decision**: Read `req.status` after `send()` resolves, before nulling `req`. No new state needed on `ExportController`.
55+
56+
### D5 — columnTitles from filtered column properties
57+
58+
The exported columns are the result of `filter(this.properties)` in `exportData()` — only visible, exportable columns. Column headers are in `ColumnsType.header` as `DynamicValue<string>`.
59+
60+
**Decision**: After computing `filter(this.properties)`, derive `columnTitles` as `columns.map(c => c.header?.value ?? "").join(",")`. This runs once per export start, not per page.
61+
62+
### D6 — fileName and sheetName passed from the export caller
63+
64+
The datagrid widget does not know the target file or sheet name — those are decided by the external module that calls `exportData()`. Adding them as widget props would duplicate state that already exists in the caller.
65+
66+
**Decision**: Extend `exportData()` options to accept `fileName?: string` and `sheetName?: string`. Both default to `""` when not provided. `ExportController` forwards them unchanged to the callbacks.
67+
68+
**Alternative considered**: Expose `fileName`/`sheetName` as widget XML properties (configurable in Studio Pro). Rejected because the file name is typically set by the export module, not the grid configuration.
69+
70+
### D7 — DSExportRequest public getters
71+
72+
`exportedItemCount` requires `DSExportRequest.loaded` (currently private). `chunkSize` requires the effective limit (currently private). Both are needed after `send()` resolves, before `req = null`.
73+
74+
**Decision**: Add `get loaded(): number` and `get limit(): number` as public getters on `DSExportRequest`. No behavior change, just access.
75+
76+
## Risks / Trade-offs
77+
78+
- **Action execution order**`onBeforeExport.execute()` calls are fire-and-forget and may outlive the export itself if they trigger a slow microflow. This is intentional and documented. [Risk: developer expects synchronous "before" semantics] → Mitigation: document clearly that the action fires concurrently with the export.
79+
- **Missing header values** — if a column's `header` DynamicValue is not yet available (status `"loading"`), its title will be an empty string in `columnTitles`. [Risk: incomplete column title list] → Mitigation: acceptable — the export itself has the same constraint on column headers; we use the same value.
80+
- **Empty fileName/sheetName** — when the export caller does not provide these values, they arrive in the action as empty strings. Microflow logic must guard against empty strings if it uses these values to route or name files.
81+
- **ActionValue change during export** — if `props.onAfterExport` changes while an export is in progress (e.g. a re-render updates the ref), the handler reads the new `ActionValue`. [Risk: unexpected microflow called] → Mitigation: during an export the datasource is locked, so re-renders that change action configuration are extremely unlikely in practice.
82+
83+
## Open Questions
84+
85+
- None. All design decisions were finalized during the exploration phase.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## Why
2+
3+
Developers using the Data Grid 2 export feature have no built-in way to observe export lifecycle — they cannot log when an export starts, how long it takes, how many rows were exported, or under what filter conditions. Adding `onBeforeExport` and `onAfterExport` action properties fills this gap with zero impact on the export path itself.
4+
5+
## What Changes
6+
7+
- Add `onBeforeExport` action property (optional) to Data Grid 2, firing just before the first datasource page fetch, with variables: `gridName`, `columnTitles`, `chunkSize`, `fileName`, `sheetName`, `startTime`
8+
- Add `onAfterExport` action property (optional) to Data Grid 2, firing after the export completes (success or abort), with variables: `gridName`, `columnTitles`, `chunkSize`, `fileName`, `sheetName`, `exportedItemCount`, `status`, `startTime`, `endTime`
9+
- Both actions are fire-and-forget — they do not block the export flow
10+
- `onAfterExport` fires on both successful completion and user abort; the `status` variable ("success" | "aborted") distinguishes them
11+
- `columnTitles` reflects only the visible (exported) columns at the time of export, comma-separated
12+
- `fileName` and `sheetName` are passed through from the export caller's options; both default to empty string when not provided
13+
- Expose public `loaded` and `limit` getters on `DSExportRequest` (internal refactor, not a public API change)
14+
15+
## Capabilities
16+
17+
### New Capabilities
18+
19+
- `export-events`: Two lifecycle action hooks (`onBeforeExport`, `onAfterExport`) on the Data Grid 2 widget for observing and logging export operations
20+
21+
### Modified Capabilities
22+
23+
<!-- No existing spec-level requirements change — this is a purely additive capability -->
24+
25+
## Impact
26+
27+
- **`src/Datagrid.xml`** — two new `<property>` blocks with `<actionVariables>` added to the Events `<propertyGroup>`
28+
- **`typings/DatagridProps.d.ts`** — auto-regenerated from XML; new `ActionValue` typed props appear
29+
- **`src/features/data-export/ExportController.ts`** — accepts two optional plain-function callbacks; calls them at the right points in `exportData()`
30+
- **`src/features/data-export/DSExportRequest.ts`** — adds `get loaded(): number` and `get limit(): number` public getters
31+
- **`src/features/data-export/useDataExport.ts`** — wires `props.onBeforeExport` / `props.onAfterExport` into `ExportController` callbacks
32+
- No new dependencies; no breaking changes; no runtime performance impact on the export itself
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
## ADDED Requirements
2+
3+
### Requirement: onBeforeExport action fires before export starts
4+
5+
The widget SHALL expose an optional `onBeforeExport` action property. When configured, the widget MUST call `onBeforeExport.execute(args)` once, fire-and-forget, immediately before the first datasource page fetch of an export operation.
6+
7+
The action MUST receive the following variables:
8+
9+
- `gridName` (String) — the Studio Pro widget name (`props.name`)
10+
- `columnTitles` (String) — comma-separated header captions of the visible, exported columns in their current display order (e.g. `"First name,Last Name,Date of Birth"`). Columns hidden by the user SHALL NOT be included.
11+
- `chunkSize` (Integer) — the effective number of rows fetched per datasource request during the export (`Math.max(requestedLimit, 10)`).
12+
- `fileName` (String) — the target file name for the export (e.g. `"export.xlsx"`), as provided by the export caller. SHALL be an empty string when not provided.
13+
- `sheetName` (String) — the target sheet/tab name within the export file (e.g. `"Sheet1"`), as provided by the export caller. SHALL be an empty string when not provided.
14+
- `startTime` (DateTime) — the timestamp captured immediately before `req.send()` is called.
15+
16+
The action execution MUST NOT block or delay the export flow.
17+
18+
#### Scenario: onBeforeExport fires with correct variables on normal export
19+
20+
- **WHEN** a configured `onBeforeExport` action exists and `canExecute` is true
21+
- **AND** an export is triggered on the grid
22+
- **THEN** `onBeforeExport.execute` is called once with `gridName`, `columnTitles`, `chunkSize`, `fileName`, `sheetName`, and `startTime` before any datasource page is fetched
23+
24+
#### Scenario: onBeforeExport is skipped when not configured
25+
26+
- **WHEN** `onBeforeExport` is not configured (optional property absent)
27+
- **AND** an export is triggered
28+
- **THEN** the export proceeds normally with no errors
29+
30+
#### Scenario: onBeforeExport columnTitles excludes hidden columns
31+
32+
- **WHEN** the user has hidden one or more columns
33+
- **AND** an export is triggered
34+
- **THEN** `columnTitles` contains only the headers of the currently visible, exported columns
35+
36+
---
37+
38+
### Requirement: onAfterExport action fires after export completes
39+
40+
The widget SHALL expose an optional `onAfterExport` action property. When configured, the widget MUST call `onAfterExport.execute(args)` once, fire-and-forget, after the export request resolves — whether it completed successfully or was aborted by the user.
41+
42+
The action MUST receive the following variables:
43+
44+
- `gridName` (String) — same as `onBeforeExport.gridName`
45+
- `columnTitles` (String) — same as `onBeforeExport.columnTitles`
46+
- `chunkSize` (Integer) — same as `onBeforeExport.chunkSize`
47+
- `fileName` (String) — same as `onBeforeExport.fileName`
48+
- `sheetName` (String) — same as `onBeforeExport.sheetName`
49+
- `exportedItemCount` (Integer) — total number of rows actually streamed to the export handler before the request ended
50+
- `status` (String) — `"success"` if all rows were exported; `"aborted"` if the user cancelled mid-export
51+
- `startTime` (DateTime) — the same timestamp passed to `onBeforeExport` (enables duration calculation in a single microflow)
52+
- `endTime` (DateTime) — the timestamp captured after the export request's `loadend` event fires
53+
54+
#### Scenario: onAfterExport fires with success status after complete export
55+
56+
- **WHEN** `onAfterExport` is configured and `canExecute` is true
57+
- **AND** the export completes without interruption
58+
- **THEN** `onAfterExport.execute` is called once with `status` equal to `"success"` and `exportedItemCount` equal to the total rows streamed
59+
60+
#### Scenario: onAfterExport fires with aborted status when user cancels
61+
62+
- **WHEN** the user clicks cancel on the export progress dialog mid-export
63+
- **THEN** `onAfterExport.execute` is called once with `status` equal to `"aborted"` and `exportedItemCount` equal to the number of rows streamed before cancellation
64+
65+
#### Scenario: onAfterExport is skipped when not configured
66+
67+
- **WHEN** `onAfterExport` is not configured
68+
- **AND** an export completes or is aborted
69+
- **THEN** no error occurs and the export lifecycle completes normally
70+
71+
#### Scenario: onAfterExport startTime matches onBeforeExport startTime
72+
73+
- **WHEN** both `onBeforeExport` and `onAfterExport` are configured
74+
- **AND** an export runs to completion
75+
- **THEN** the `startTime` value in `onAfterExport` is identical to the `startTime` value in `onBeforeExport`
76+
77+
#### Scenario: onAfterExport endTime is after startTime
78+
79+
- **WHEN** `onAfterExport` fires after a completed export
80+
- **THEN** `endTime` is greater than or equal to `startTime`
81+
82+
---
83+
84+
### Requirement: Both export event actions are optional and independent
85+
86+
The widget SHALL allow `onBeforeExport` and `onAfterExport` to be configured independently. Configuring one MUST NOT require configuring the other.
87+
88+
#### Scenario: Only onBeforeExport configured
89+
90+
- **WHEN** `onBeforeExport` is configured and `onAfterExport` is not
91+
- **AND** an export runs to completion
92+
- **THEN** `onBeforeExport` fires once and no error occurs for the missing `onAfterExport`
93+
94+
#### Scenario: Only onAfterExport configured
95+
96+
- **WHEN** `onAfterExport` is configured and `onBeforeExport` is not
97+
- **AND** an export runs to completion
98+
- **THEN** `onAfterExport` fires once and no error occurs for the missing `onBeforeExport`
99+
100+
#### Scenario: Neither action configured
101+
102+
- **WHEN** neither `onBeforeExport` nor `onAfterExport` is configured
103+
- **AND** an export runs
104+
- **THEN** the export behaves identically to before this feature was introduced

0 commit comments

Comments
 (0)