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
8 changes: 8 additions & 0 deletions packages/pluggableWidgets/datagrid-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: tdd-refactor
created: 2026-06-24
53 changes: 53 additions & 0 deletions packages/pluggableWidgets/datagrid-web/openspec/design.md
Original file line number Diff line number Diff line change
@@ -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: <stripped-date>, 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: <format> }` (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.
30 changes: 30 additions & 0 deletions packages/pluggableWidgets/datagrid-web/openspec/proposal.md
Original file line number Diff line number Diff line change
@@ -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.
29 changes: 29 additions & 0 deletions packages/pluggableWidgets/datagrid-web/openspec/tasks.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Comment thread
yordan-st marked this conversation as resolved.
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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Loading