diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 5f08d33055..2ed5a9f445 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We added support for using the attribute's own formatting when exporting to Excel with the "Default" export type. Numbers and dates now export with their configured precision and pattern instead of raw values. + +### Fixed + +- We fixed an issue where export type and format properties were visible in Studio Pro for dynamic text columns, even though they have no effect. + ## [3.11.1] - 2026-06-18 ### Fixed diff --git a/packages/pluggableWidgets/datagrid-web/openspec/.openspec.yaml b/packages/pluggableWidgets/datagrid-web/openspec/.openspec.yaml new file mode 100644 index 0000000000..39f46ea2fa --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/openspec/.openspec.yaml @@ -0,0 +1,2 @@ +schema: tdd-refactor +created: 2026-06-24 diff --git a/packages/pluggableWidgets/datagrid-web/openspec/design.md b/packages/pluggableWidgets/datagrid-web/openspec/design.md new file mode 100644 index 0000000000..587cbcb7a9 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/openspec/design.md @@ -0,0 +1,53 @@ +## Test Cases + +### Reproduction Tests + +- Attribute number with default export type uses formatter - (unit) + - **Given**: Attribute column with `exportType = "default"`, attribute has `formatter = { type: "number", config: { groupDigits: true, decimalPrecision: 2 } }` + - **When**: Export reads the cell + - **Then**: Cell is `{ t: "n", v: 1234.56, z: "#,##0.00" }` + +- Attribute date with default export type uses custom pattern - (unit) + - **Given**: Attribute column with `exportType = "default"`, attribute has `formatter = { type: "datetime", config: { type: "custom", pattern: "dd/MM/yyyy" } }` + - **When**: Export reads the cell + - **Then**: Cell is `{ t: "d", v: , z: "dd/mm/yyyy" }` (M→m converted for Excel) + +### Edge Cases + +- Attribute date with non-custom datetime config falls back to locale default - (unit) + - **Given**: Attribute column with `exportType = "default"`, attribute has `formatter = { type: "datetime", config: { type: "date" } }` + - **When**: Export reads the cell + - **Then**: Cell has `z: "dd-mm-yyyy"` (locale fallback, not derived from formatter) + +- Attribute number with no decimal precision - (unit) + - **Given**: Attribute column with `exportType = "default"`, attribute has `formatter = { type: "number", config: { groupDigits: false, decimalPrecision: 0 } }` + - **When**: Export reads the cell + - **Then**: Cell has `z: "0"` (no grouping, no decimals) + +- Attribute with formatter lacking type field - (unit) + - **Given**: Attribute column with `exportType = "default"`, attribute has basic formatter (no `type` property, e.g., default mock) + - **When**: Export reads the cell + - **Then**: Cell has `z: undefined` for numbers, locale fallback for dates (graceful degradation) + +### Regression Tests + +- Custom export type still uses explicit format - (unit) + - **Given**: Attribute column with `exportType = "number"` and `exportNumberFormat = "#,##0.00"` + - **When**: Export reads the cell + - **Then**: Cell uses the explicit format, NOT the attribute formatter + +- Dynamic text always exports as string regardless of export settings - (unit) + - **Given**: Dynamic text column with any `exportType` set + - **When**: Export reads the cell + - **Then**: Cell is always `{ t: "s" }`, export type ignored + +- Custom content export unchanged - (unit) + - **Given**: Custom content column with `exportType = "number"` and `exportValue = "1234.56"` + - **When**: Export reads the cell + - **Then**: Cell is `{ t: "n", v: 1234.56, z: }` (same as before) + +## Notes + +- The `M → m` conversion is needed because Mendix uses Java-style date patterns (M = month) while Excel uses `m` for month. +- `getAttributeDefaultFormat` returns `undefined` for string, boolean, and enum attributes — these fall through to existing logic (displayValue for strings, native boolean cells). +- The `editorConfig` change is Studio Pro-only (design time) — no runtime test needed. diff --git a/packages/pluggableWidgets/datagrid-web/openspec/proposal.md b/packages/pluggableWidgets/datagrid-web/openspec/proposal.md new file mode 100644 index 0000000000..6c4896ea59 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/openspec/proposal.md @@ -0,0 +1,30 @@ +## Why + +When exporting a Data Grid 2 to Excel with `exportType = "default"` on attribute columns, the exported cells had no Excel format applied. Numbers exported as raw values without thousand separators or decimal precision, and dates used only the browser locale fallback. Users expected "default" to mean "use the attribute's own configured format" (e.g., 2 decimal places for a Decimal, `dd/MM/yyyy` for a DateTime). + +Additionally, the export type and format properties were visible in Studio Pro for dynamic text columns even though the export logic ignores them — confusing for configurators. + +## Root Cause + +`getCellFormat()` returned `undefined` when `exportType === "default"`, meaning no Excel format code (`z` field) was written to the cell. The attribute's `formatter` property (available on `ListAttributeValue` since Mendix 10) was never consulted. + +The `editorConfig.ts` visibility logic only conditionally hid `exportNumberFormat`/`exportDateFormat` based on the export type value, but never hid all three export properties when `showContentAs === "dynamicText"`. + +## What Changes + +Package: `packages/pluggableWidgets/datagrid-web` + +- `src/features/data-export/cell-readers.ts` — New `getAttributeDefaultFormat(props)` function reads `props.attribute.formatter` and derives an Excel format string: + - `formatter.type === "number"` → builds format from `groupDigits` and `decimalPrecision` (e.g., `#,##0.00`) + - `formatter.type === "datetime"` with `config.type === "custom"` → converts Mendix pattern to Excel format (M→m replacement) + - Otherwise returns `undefined` (falls through to existing locale default for dates) + - Attribute reader now branches: `exportType === "default"` → `getAttributeDefaultFormat()`, else → existing `getCellFormat()`. + +- `src/Datagrid.editorConfig.ts` — When `showContentAs === "dynamicText"`, hide `exportType`, `exportNumberFormat`, and `exportDateFormat` properties in Studio Pro. + +## Impact + +- Attribute columns with `exportType = "default"` now export with their configured format. This is an enhancement, not a breaking change — previously these cells had no format, now they have one. +- No XML property changes. No migration needed. +- No behavioral change for `exportType = "number"` / `"date"` / `"boolean"` (custom path unchanged). +- Dynamic text columns: cosmetic-only change in Studio Pro property panel (properties hidden). Runtime behavior identical. diff --git a/packages/pluggableWidgets/datagrid-web/openspec/tasks.md b/packages/pluggableWidgets/datagrid-web/openspec/tasks.md new file mode 100644 index 0000000000..cbb1c4b7cd --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/openspec/tasks.md @@ -0,0 +1,29 @@ +## 1. Test Setup + +- [x] 1.1 Write test: attribute number with default exportType uses formatter config to derive Excel format +- [x] 1.2 Write test: attribute date with default exportType and custom pattern uses converted pattern +- [x] 1.3 Write test: attribute date with non-custom config falls back to locale default +- [x] 1.4 Write test: attribute number with groupDigits=false and decimalPrecision=0 produces format "0" + +## 2. Implementation + +- [x] 2.1 Add `getAttributeDefaultFormat(props)` function in `cell-readers.ts` that reads `props.attribute.formatter` and derives Excel format string +- [x] 2.2 Branch attribute reader: `exportType === "default"` calls `getAttributeDefaultFormat()`, else calls existing `getCellFormat()` +- [x] 2.3 Hide `exportType`, `exportNumberFormat`, `exportDateFormat` in editorConfig when `showContentAs === "dynamicText"` + +## 3. Refactoring + +- [x] 3.1 No refactoring needed — implementation is minimal and isolated + +## 4. Verification + +- [x] 4.1 All 221 tests passing (4 new + 217 existing) +- [x] 4.2 Full test suite passes (no regressions) +- [x] 4.3 Build compiles without TypeScript errors +- [x] 4.4 PR submitted and reviewed + +## Notes + +- No XML property changes were needed — existing `exportType` enum with "default" value covers the use case. +- The `formatter` property is available on `ListAttributeValue` since Mendix 10 runtime. +- iobuhov review feedback: use consistent assertion syntax (assert `cell.v` in all date tests). Fixed. diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts index 49ca0a3436..0db6fb4b60 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts @@ -57,13 +57,19 @@ export function getProperties(values: DatagridPreviewProps, defaultProperties: P if (column.minWidth !== "manual") { hidePropertyIn(defaultProperties, values, "columns", index, "minWidthLimit"); } - // Hide exportNumberFormat if exportType is not 'number' - if (column.exportType !== "number") { - hidePropertyIn(defaultProperties, values, "columns", index, "exportNumberFormat" as any); - } - // Hide exportDateFormat if exportType is not 'date' - if (column.exportType !== "date") { - hidePropertyIn(defaultProperties, values, "columns", index, "exportDateFormat" as any); + if (column.showContentAs === "dynamicText") { + hideNestedPropertiesIn(defaultProperties, values, "columns", index, [ + "exportType", + "exportNumberFormat", + "exportDateFormat" + ] as any); + } else { + if (column.exportType !== "number") { + hidePropertyIn(defaultProperties, values, "columns", index, "exportNumberFormat" as any); + } + if (column.exportType !== "date") { + hidePropertyIn(defaultProperties, values, "columns", index, "exportDateFormat" as any); + } } }); diff --git a/packages/pluggableWidgets/datagrid-web/src/features/data-export/__tests__/cell-readers.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/data-export/__tests__/cell-readers.spec.ts index 73f8c2ef1f..791f25c87d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/data-export/__tests__/cell-readers.spec.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/data-export/__tests__/cell-readers.spec.ts @@ -68,6 +68,64 @@ describe("cell-readers", () => { expect(cell.v).toBe(false); }); + it("uses attribute number formatter when exportType is default", () => { + const attr = listAttribute(() => new Big("1234.56")) as any; + attr.formatter = { type: "number", config: { groupDigits: true, decimalPrecision: 2 } }; + const col = column("Amount", c => { + c.showContentAs = "attribute"; + c.attribute = attr; + c.exportType = "default"; + }); + const cell = readSingleCell(col); + expect(cell.t).toBe("n"); + expect(cell.v).toBe(1234.56); + expect(cell.z).toBe("#,##0.00"); + }); + + it("uses attribute date formatter when exportType is default", () => { + const testDate = new Date("2024-06-15T10:30:00Z"); + const attr = listAttribute(() => testDate) as any; + attr.formatter = { type: "datetime", config: { type: "custom", pattern: "dd/MM/yyyy" } }; + const col = column("Created", c => { + c.showContentAs = "attribute"; + c.attribute = attr; + c.exportType = "default"; + }); + const cell = readSingleCell(col); + expect(cell.t).toBe("d"); + expect(cell.v).toEqual(new Date(Date.UTC(2024, 5, 15))); + expect(cell.z).toBe("dd/mm/yyyy"); + }); + + it("returns no format for default datetime with non-custom config", () => { + const testDate = new Date("2024-06-15T10:30:00Z"); + const attr = listAttribute(() => testDate) as any; + attr.formatter = { type: "datetime", config: { type: "date" } }; + const col = column("Created", c => { + c.showContentAs = "attribute"; + c.attribute = attr; + c.exportType = "default"; + }); + const cell = readSingleCell(col); + expect(cell.t).toBe("d"); + expect(cell.v).toEqual(new Date(Date.UTC(2024, 5, 15))); + expect(cell.z).toBe("dd-mm-yyyy"); + }); + + it("uses attribute number formatter without decimals", () => { + const attr = listAttribute(() => new Big("42")) as any; + attr.formatter = { type: "number", config: { groupDigits: false, decimalPrecision: 0 } }; + const col = column("Count", c => { + c.showContentAs = "attribute"; + c.attribute = attr; + c.exportType = "default"; + }); + const cell = readSingleCell(col); + expect(cell.t).toBe("n"); + expect(cell.v).toBe(42); + expect(cell.z).toBe("0"); + }); + it("exports date attribute with format as date cell", () => { const testDate = new Date("2024-06-15T10:30:00Z"); const col = column("Created", c => { diff --git a/packages/pluggableWidgets/datagrid-web/src/features/data-export/cell-readers.ts b/packages/pluggableWidgets/datagrid-web/src/features/data-export/cell-readers.ts index c1227592b9..1175bf5651 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/data-export/cell-readers.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/data-export/cell-readers.ts @@ -113,6 +113,27 @@ function countSignificantDigits(value: Big): number { return stripped.length || 1; } +function getAttributeDefaultFormat(props: ColumnsType): string | undefined { + const formatter = props.attribute?.formatter; + if (!formatter) { + return undefined; + } + + if (formatter.type === "datetime") { + const cfg = formatter.config; + return cfg.type === "custom" ? cfg.pattern.replace(/M/g, "m") : undefined; + } + + if (formatter.type === "number") { + const cfg = formatter.config; + const decimals = cfg.decimalPrecision ?? 0; + const base = cfg.groupDigits ? "#,##0" : "0"; + return decimals > 0 ? `${base}.${"0".repeat(decimals)}` : base; + } + + return undefined; +} + const readers: ReadersByType = { attribute(item, props) { const data = props.attribute?.get(item); @@ -122,11 +143,14 @@ const readers: ReadersByType = { } const value = data.value; - const format = getCellFormat({ - exportType: props.exportType, - exportDateFormat: props.exportDateFormat, - exportNumberFormat: props.exportNumberFormat - }); + const format = + props.exportType === "default" + ? getAttributeDefaultFormat(props) + : getCellFormat({ + exportType: props.exportType, + exportDateFormat: props.exportDateFormat, + exportNumberFormat: props.exportNumberFormat + }); if (value instanceof Date) { const dateValue = format && hasTimeComponent(format) ? value : stripTime(value);