Skip to content

Commit 5e0fb1f

Browse files
committed
fix(datagrid-web): derive default export decimals per value to avoid trailing dot
A static `{base}.########` mask emits the literal decimal point even when all fractional `#` digits collapse, so whole numbers exported as `1983.` and broke the export e2e test. Count the fractional digits from the value itself instead, so integers use `0` (no dot) and decimals mirror the grid.
1 parent d455d9e commit 5e0fb1f

4 files changed

Lines changed: 45 additions & 20 deletions

File tree

packages/pluggableWidgets/datagrid-web/openspec/design.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
### Reproduction Tests
44

55
- Attribute number with default export type mirrors the grid - (unit)
6-
- **Given**: Attribute column with `exportType = "default"`, attribute has `formatter = { type: "number", config: { groupDigits: true } }` (Mendix Decimal attributes only expose `groupDigits` at runtime, not a fixed `decimalPrecision`)
6+
- **Given**: Attribute column with `exportType = "default"`, value `1234.56`, attribute has `formatter = { type: "number", config: { groupDigits: true } }` (Mendix Decimal attributes only expose `groupDigits` at runtime, not a fixed `decimalPrecision`)
77
- **When**: Export reads the cell
8-
- **Then**: Cell is `{ t: "n", v: 1234.56, z: "#,##0.########" }``#` suppresses trailing zeros so Excel renders exactly what the grid shows (`1234.56` stays `1234.56`, `0.5` stays `0.5`, integers stay integers)
8+
- **Then**: Cell is `{ t: "n", v: 1234.56, z: "#,##0.00" }`the fractional-digit count is taken from the value itself so Excel renders exactly what the grid shows
99

1010
- Attribute date with default export type uses custom pattern - (unit)
1111
- **Given**: Attribute column with `exportType = "default"`, attribute has `formatter = { type: "datetime", config: { type: "custom", pattern: "dd/MM/yyyy" } }`
@@ -25,9 +25,14 @@
2525
- **Then**: Cell has `z: "0"` (no grouping, no decimals) — an explicit `decimalPrecision` takes priority over the grid-mirroring fallback
2626

2727
- Attribute number without grouping mirrors the grid - (unit)
28-
- **Given**: Attribute column with `exportType = "default"`, attribute has `formatter = { type: "number", config: { groupDigits: false } }` (no `decimalPrecision`)
28+
- **Given**: Attribute column with `exportType = "default"`, value `0.5`, attribute has `formatter = { type: "number", config: { groupDigits: false } }` (no `decimalPrecision`)
2929
- **When**: Export reads the cell
30-
- **Then**: Cell has `z: "0.########"` (no grouping, up to 8 fractional digits with trailing zeros suppressed)
30+
- **Then**: Cell has `z: "0.0"` (no grouping, one fractional digit derived from the value)
31+
32+
- Attribute whole-number value produces no fractional format - (unit)
33+
- **Given**: Attribute column with `exportType = "default"`, value `1983`, attribute has `formatter = { type: "number", config: { groupDigits: false } }`
34+
- **When**: Export reads the cell
35+
- **Then**: Cell has `z: "0"` — no fractional section, so Excel does not render a trailing dot (`"1983"`, not `"1983."`)
3136

3237
- Attribute with formatter lacking type field - (unit)
3338
- **Given**: Attribute column with `exportType = "default"`, attribute has basic formatter (no `type` property, e.g., default mock)
@@ -53,7 +58,7 @@
5358

5459
## Notes
5560

56-
- Mendix Decimal attributes do **not** expose a fixed `decimalPrecision` on the formatter config at runtime — only `groupDigits` is present (verified against the live client). The number-default format therefore mirrors the grid using `{base}.########` (8 = the Mendix DB fractional-digit maximum) with `#` suppressing trailing zeros, rather than deriving a fixed precision. The `decimalPrecision` branch is retained for the case where a future runtime does supply it.
61+
- Mendix Decimal attributes do **not** expose a fixed `decimalPrecision` on the formatter config at runtime — only `groupDigits` is present (verified against the live client). The number-default format therefore mirrors the grid by counting the fractional digits of the value itself and building `{base}.{0×n}`. A static `{base}.########` mask cannot be used because the literal `.` in the mask is always emitted, so whole numbers would render with a trailing dot (`1983``"1983."`). The `decimalPrecision` branch is retained for the case where a future runtime does supply it.
5762
- The `M → m` conversion is needed because Mendix uses Java-style date patterns (M = month) while Excel uses `m` for month.
5863
- `getAttributeDefaultFormat` returns `undefined` for string, boolean, and enum attributes — these fall through to existing logic (displayValue for strings, native boolean cells).
5964
- The `editorConfig` change is Studio Pro-only (design time) — no runtime test needed.

packages/pluggableWidgets/datagrid-web/openspec/proposal.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ The `editorConfig.ts` visibility logic only conditionally hid `exportNumberForma
1616

1717
Package: `packages/pluggableWidgets/datagrid-web`
1818

19-
- `src/features/data-export/cell-readers.ts` — New `getAttributeDefaultFormat(props)` function reads `props.attribute.formatter` and derives an Excel format string:
20-
- `formatter.type === "number"` → builds format from `groupDigits`; when `decimalPrecision` is present it is honoured, otherwise the format mirrors the grid as `{base}.########` (up to 8 fractional digits, trailing zeros suppressed by `#`), so `1234.56` exports as `1234.56` and integers stay integers
19+
- `src/features/data-export/cell-readers.ts` — New `getAttributeDefaultFormat(props, value)` function reads `props.attribute.formatter` and derives an Excel format string:
20+
- `formatter.type === "number"` → builds format from `groupDigits`; when `decimalPrecision` is present it is honoured, otherwise the fractional-digit count is taken from the value itself (`{base}.{0×n}`), so `1234.56` exports as `1234.56` and whole numbers export without a trailing dot
2121
- `formatter.type === "datetime"` with `config.type === "custom"` → converts Mendix pattern to Excel format (M→m replacement)
2222
- Otherwise returns `undefined` (falls through to existing locale default for dates)
2323
- Attribute reader now branches: `exportType === "default"``getAttributeDefaultFormat()`, else → existing `getCellFormat()`.

packages/pluggableWidgets/datagrid-web/src/features/data-export/__tests__/cell-readers.spec.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ describe("cell-readers", () => {
127127
});
128128

129129
it("mirrors the grid when number formatter config omits decimalPrecision (grouped)", () => {
130-
// Real Mendix Decimal attributes only expose `groupDigits` at runtime.
130+
// Real Mendix Decimal attributes only expose `groupDigits` at runtime, so the
131+
// decimal count is derived from the value itself.
131132
const attr = listAttribute(() => new Big("1234.56")) as any;
132133
attr.formatter = { type: "number", config: { groupDigits: true } };
133134
const col = column("Amount", c => {
@@ -138,7 +139,7 @@ describe("cell-readers", () => {
138139
const cell = readSingleCell(col);
139140
expect(cell.t).toBe("n");
140141
expect(cell.v).toBe(1234.56);
141-
expect(cell.z).toBe("#,##0.########");
142+
expect(cell.z).toBe("#,##0.00");
142143
});
143144

144145
it("mirrors the grid when number formatter config omits decimalPrecision (ungrouped)", () => {
@@ -152,8 +153,22 @@ describe("cell-readers", () => {
152153
const cell = readSingleCell(col);
153154
expect(cell.t).toBe("n");
154155
expect(cell.v).toBe(0.5);
155-
// `#` suppresses trailing zeros, so Excel renders exactly what the grid shows.
156-
expect(cell.z).toBe("0.########");
156+
expect(cell.z).toBe("0.0");
157+
});
158+
159+
it("uses no fractional format for a whole-number value (no trailing dot)", () => {
160+
// Regression: a static `0.########` mask renders 1983 as "1983." in Excel.
161+
const attr = listAttribute(() => new Big("1983")) as any;
162+
attr.formatter = { type: "number", config: { groupDigits: false } };
163+
const col = column("Birth year", c => {
164+
c.showContentAs = "attribute";
165+
c.attribute = attr;
166+
c.exportType = "default";
167+
});
168+
const cell = readSingleCell(col);
169+
expect(cell.t).toBe("n");
170+
expect(cell.v).toBe(1983);
171+
expect(cell.z).toBe("0");
157172
});
158173

159174
it("exports date attribute with format as date cell", () => {

packages/pluggableWidgets/datagrid-web/src/features/data-export/cell-readers.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ function countSignificantDigits(value: Big): number {
113113
return stripped.length || 1;
114114
}
115115

116-
function getAttributeDefaultFormat(props: ColumnsType): string | undefined {
116+
function countDecimalPlaces(value: Big): number {
117+
const fixed = value.toFixed();
118+
const dot = fixed.indexOf(".");
119+
return dot === -1 ? 0 : fixed.length - dot - 1;
120+
}
121+
122+
function getAttributeDefaultFormat(props: ColumnsType, value: unknown): string | undefined {
117123
const formatter = props.attribute?.formatter;
118124
if (!formatter) {
119125
return undefined;
@@ -129,13 +135,12 @@ function getAttributeDefaultFormat(props: ColumnsType): string | undefined {
129135
const base = cfg.groupDigits ? "#,##0" : "0";
130136
// Mendix Decimal attributes do not expose a fixed `decimalPrecision` on the
131137
// formatter config at runtime (only `groupDigits`). Honour it when present,
132-
// otherwise mirror the grid: show up to 8 fractional digits (the Mendix DB
133-
// maximum) with trailing zeros suppressed via `#`, so 1234.56 stays 1234.56
134-
// and integers stay integers — instead of collapsing to a whole number.
135-
if (cfg.decimalPrecision != null) {
136-
return cfg.decimalPrecision > 0 ? `${base}.${"0".repeat(cfg.decimalPrecision)}` : base;
137-
}
138-
return `${base}.########`;
138+
// otherwise mirror the grid by taking the decimal count from the value itself.
139+
// A per-value count (rather than a `0.########` mask) is required because a
140+
// static fractional mask emits a trailing dot for whole numbers (1983 -> "1983.").
141+
const decimals =
142+
cfg.decimalPrecision != null ? cfg.decimalPrecision : value instanceof Big ? countDecimalPlaces(value) : 0;
143+
return decimals > 0 ? `${base}.${"0".repeat(decimals)}` : base;
139144
}
140145

141146
return undefined;
@@ -152,7 +157,7 @@ const readers: ReadersByType = {
152157
const value = data.value;
153158
const format =
154159
props.exportType === "default"
155-
? getAttributeDefaultFormat(props)
160+
? getAttributeDefaultFormat(props, value)
156161
: getCellFormat({
157162
exportType: props.exportType,
158163
exportDateFormat: props.exportDateFormat,

0 commit comments

Comments
 (0)