Skip to content

Commit 6757af1

Browse files
fix(datagrid-web): export booleans as native cells and dates with locale format
1 parent 1d0c4b2 commit 6757af1

5 files changed

Lines changed: 136 additions & 39 deletions

File tree

packages/pluggableWidgets/datagrid-web/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1616

1717
- We fixed an issue where exported date values included a hidden time component even when the format specified date-only parts.
1818

19-
- We fixed an issue where boolean values exported as TRUE/FALSE instead of Yes/No to match the display in the grid.
19+
- We fixed an issue where boolean values were not exported as proper Excel boolean cells. Both attribute and custom content columns now export as native booleans (TRUE/FALSE) recognized by Excel.
2020

2121
- We fixed an issue where numbers with more than 15 significant digits lost precision during Excel export. Such values are now exported as text to preserve all digits.
2222

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

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import Big from "big.js";
2-
import { listAttribute, listExpression, dynamic, obj } from "@mendix/widget-plugin-test-utils";
1+
jest.mock("mendix", () => ({}), { virtual: true });
2+
3+
import { Big } from "big.js";
34
import { ObjectItem } from "mendix";
5+
import { listAttribute, listExpression, dynamic, obj } from "@mendix/widget-plugin-test-utils";
46
import { column } from "../../../utils/test-utils";
57
import { readChunk, ExcelCell } from "../cell-readers";
68

@@ -46,24 +48,24 @@ describe("cell-readers", () => {
4648
expect(cell.z).toBe("#,##0.00");
4749
});
4850

49-
it("exports boolean attribute as Yes/No string cell", () => {
51+
it("exports boolean attribute as boolean cell", () => {
5052
const col = column("Active", c => {
5153
c.showContentAs = "attribute";
5254
c.attribute = listAttribute(() => true);
5355
});
5456
const cell = readSingleCell(col);
55-
expect(cell.t).toBe("s");
56-
expect(cell.v).toBe("Yes");
57+
expect(cell.t).toBe("b");
58+
expect(cell.v).toBe(true);
5759
});
5860

59-
it("exports false boolean attribute as No", () => {
61+
it("exports false boolean attribute as boolean cell", () => {
6062
const col = column("Active", c => {
6163
c.showContentAs = "attribute";
6264
c.attribute = listAttribute(() => false);
6365
});
6466
const cell = readSingleCell(col);
65-
expect(cell.t).toBe("s");
66-
expect(cell.v).toBe("No");
67+
expect(cell.t).toBe("b");
68+
expect(cell.v).toBe(false);
6769
});
6870

6971
it("exports date attribute with format as date cell", () => {
@@ -80,16 +82,17 @@ describe("cell-readers", () => {
8082
expect(cell.z).toBe("yyyy-mm-dd");
8183
});
8284

83-
it("exports date attribute without format as string cell (displayValue)", () => {
85+
it("exports date attribute without format using default date format", () => {
8486
const testDate = new Date("2024-06-15T10:30:00Z");
8587
const col = column("Created", c => {
8688
c.showContentAs = "attribute";
8789
c.attribute = listAttribute(() => testDate);
8890
c.exportType = "default";
8991
});
9092
const cell = readSingleCell(col);
91-
expect(cell.t).toBe("s");
92-
expect(cell.v).toBe(`Formatted ${testDate}`);
93+
expect(cell.t).toBe("d");
94+
expect(cell.v).toEqual(new Date(Date.UTC(2024, 5, 15)));
95+
expect(cell.z).toBe("dd-mm-yyyy");
9396
});
9497

9598
it("returns empty cell when attribute is not available", () => {
@@ -125,7 +128,7 @@ describe("cell-readers", () => {
125128
it("returns n/a cell when dynamicText is unavailable", () => {
126129
const col = column("Label", c => {
127130
c.showContentAs = "dynamicText";
128-
c.dynamicText = listExpression(() => "text", "unavailable");
131+
c.dynamicText = { get: () => ({ status: "unavailable", value: undefined }) } as any;
129132
});
130133
const cell = readSingleCell(col);
131134
expect(cell.t).toBe("s");
@@ -226,15 +229,16 @@ describe("cell-readers", () => {
226229
expect(cell.z).toBe("yyyy-mm-dd");
227230
});
228231

229-
it("exports date as string when no format provided", () => {
232+
it("exports date with default format when no format provided", () => {
230233
const col = column("Created", c => {
231234
c.showContentAs = "customContent";
232235
c.exportValue = listExpression(() => "2024-06-15T10:30:00Z");
233236
c.exportType = "date";
234237
});
235238
const cell = readSingleCell(col);
236-
expect(cell.t).toBe("s");
237-
expect(cell.v).toBe("2024-06-15T10:30:00Z");
239+
expect(cell.t).toBe("d");
240+
expect(cell.v).toEqual(new Date(Date.UTC(2024, 5, 15)));
241+
expect(cell.z).toBe("dd-mm-yyyy");
238242
});
239243

240244
it("falls back to string when date parse fails", () => {
@@ -260,6 +264,65 @@ describe("cell-readers", () => {
260264
expect(cell.t).toBe("s");
261265
expect(cell.v).toBe("");
262266
});
267+
268+
it("exports as boolean true when exportType is boolean and value is 'true'", () => {
269+
const col = column("Active", c => {
270+
c.showContentAs = "customContent";
271+
c.exportValue = listExpression(() => "true");
272+
c.exportType = "boolean";
273+
});
274+
const cell = readSingleCell(col);
275+
expect(cell.t).toBe("b");
276+
expect(cell.v).toBe(true);
277+
});
278+
279+
it("exports as boolean false when exportType is boolean and value is 'false'", () => {
280+
const col = column("Active", c => {
281+
c.showContentAs = "customContent";
282+
c.exportValue = listExpression(() => "false");
283+
c.exportType = "boolean";
284+
});
285+
const cell = readSingleCell(col);
286+
expect(cell.t).toBe("b");
287+
expect(cell.v).toBe(false);
288+
});
289+
290+
it("exports boolean true for case-insensitive values", () => {
291+
for (const val of ["True", "YES", "1"]) {
292+
const col = column("Active", c => {
293+
c.showContentAs = "customContent";
294+
c.exportValue = listExpression(() => val);
295+
c.exportType = "boolean";
296+
});
297+
const cell = readSingleCell(col);
298+
expect(cell.t).toBe("b");
299+
expect(cell.v).toBe(true);
300+
}
301+
});
302+
303+
it("exports boolean false for case-insensitive values", () => {
304+
for (const val of ["False", "NO", "0"]) {
305+
const col = column("Active", c => {
306+
c.showContentAs = "customContent";
307+
c.exportValue = listExpression(() => val);
308+
c.exportType = "boolean";
309+
});
310+
const cell = readSingleCell(col);
311+
expect(cell.t).toBe("b");
312+
expect(cell.v).toBe(false);
313+
}
314+
});
315+
316+
it("falls back to string for unrecognized boolean value", () => {
317+
const col = column("Active", c => {
318+
c.showContentAs = "customContent";
319+
c.exportValue = listExpression(() => "maybe");
320+
c.exportType = "boolean";
321+
});
322+
const cell = readSingleCell(col);
323+
expect(cell.t).toBe("s");
324+
expect(cell.v).toBe("maybe");
325+
});
263326
});
264327

265328
describe("long number precision", () => {

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

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import Big from "big.js";
1+
import { Big } from "big.js";
22
import { DynamicValue, ObjectItem } from "mendix";
33
import { ColumnsType, ShowContentAsEnum } from "../../../typings/DatagridProps";
44

55
/** Represents a single Excel cell (SheetJS compatible) */
66
export interface ExcelCell {
7-
/** Cell type: 's' = string, 'n' = number, 'd' = date */
8-
t: "s" | "n" | "d";
7+
/** Cell type: 's' = string, 'n' = number, 'b' = boolean, 'd' = date */
8+
t: "s" | "n" | "b" | "d";
99
/** Underlying value */
10-
v: string | number | Date;
10+
v: string | number | boolean | Date;
1111
/** Optional Excel number/date format, e.g. "yyyy-mm-dd" or "$0.00" */
1212
z?: string;
1313
/** Optional pre-formatted display text */
@@ -68,20 +68,28 @@ export function excelString(value: string, format?: string): ExcelCell {
6868
};
6969
}
7070

71-
export function excelDate(value: string): ExcelCell;
72-
export function excelDate(value: Date, format: string): ExcelCell;
73-
export function excelDate(value: string | Date, format?: string): ExcelCell {
71+
const FALLBACK_DATE_FORMAT = "dd-mm-yyyy";
72+
73+
function getDefaultDateFormat(): string {
74+
const pattern = window.mx?.session.getConfig().locale.patterns.date;
75+
if (!pattern) {
76+
return FALLBACK_DATE_FORMAT;
77+
}
78+
return pattern.replace(/M/g, "m");
79+
}
80+
81+
export function excelDate(value: Date, format?: string): ExcelCell {
7482
return {
75-
t: value instanceof Date && format !== undefined ? "d" : "s",
83+
t: "d",
7684
v: value,
77-
z: format
85+
z: format ?? getDefaultDateFormat()
7886
};
7987
}
8088

8189
export function excelBoolean(value: boolean): ExcelCell {
8290
return {
83-
t: "s",
84-
v: value ? "Yes" : "No"
91+
t: "b",
92+
v: value
8593
};
8694
}
8795

@@ -121,10 +129,7 @@ const readers: ReadersByType = {
121129
});
122130

123131
if (value instanceof Date) {
124-
if (format === undefined) {
125-
return excelDate(data.displayValue);
126-
}
127-
const dateValue = hasTimeComponent(format) ? value : stripTime(value);
132+
const dateValue = format && hasTimeComponent(format) ? value : stripTime(value);
128133
return excelDate(dateValue, format);
129134
}
130135

@@ -158,30 +163,38 @@ const readers: ReadersByType = {
158163

159164
customContent(item, props) {
160165
const value = props.exportValue?.get(item).value ?? "";
166+
const { exportType } = props;
161167
const format = getCellFormat({
162-
exportType: props.exportType,
168+
exportType,
163169
exportDateFormat: props.exportDateFormat,
164170
exportNumberFormat: props.exportNumberFormat
165171
});
166172

167-
if (props.exportType === "number" && value.trim() !== "") {
173+
if (exportType === "number" && value.trim() !== "") {
168174
const parsed = Number(value);
169175
if (!Number.isNaN(parsed)) {
170176
return excelNumber(parsed, format);
171177
}
172178
}
173179

174-
if (props.exportType === "date" && value !== "") {
180+
if (exportType === "date" && value !== "") {
175181
const parsed = new Date(value);
176182
if (!isNaN(parsed.getTime())) {
177-
if (format === undefined) {
178-
return excelDate(value);
179-
}
180-
const dateValue = hasTimeComponent(format) ? parsed : stripTime(parsed);
183+
const dateValue = format && hasTimeComponent(format) ? parsed : stripTime(parsed);
181184
return excelDate(dateValue, format);
182185
}
183186
}
184187

188+
if (exportType === "boolean") {
189+
const lower = value.trim().toLowerCase();
190+
if (lower === "true" || lower === "yes" || lower === "1") {
191+
return excelBoolean(true);
192+
}
193+
if (lower === "false" || lower === "no" || lower === "0") {
194+
return excelBoolean(false);
195+
}
196+
}
197+
185198
return excelString(value, format);
186199
}
187200
};

packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* WARNING: All changes made to this file will be overwritten
44
* @author Mendix Widgets Framework Team
55
*/
6+
import { ActionValue, DynamicValue, EditableValue, ListActionValue, ListAttributeListValue, ListAttributeValue, ListExpressionValue, ListValue, ListWidgetValue, SelectionMultiValue, SelectionSingleValue } from "mendix";
67
import { ComponentType, CSSProperties, ReactNode } from "react";
7-
import { ActionValue, DynamicValue, EditableValue, ListValue, ListActionValue, ListAttributeValue, ListAttributeListValue, ListExpressionValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix";
88
import { Big } from "big.js";
99

1010
export type ShowContentAsEnum = "attribute" | "dynamicText" | "customContent";
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
interface MXSessionLocale {
2+
patterns: {
3+
date: string;
4+
datetime: string;
5+
time: string;
6+
};
7+
}
8+
9+
interface MXGlobalObject {
10+
session: {
11+
getConfig(): { locale: MXSessionLocale };
12+
};
13+
}
14+
15+
declare global {
16+
interface Window {
17+
mx?: MXGlobalObject;
18+
}
19+
}
20+
21+
export {};

0 commit comments

Comments
 (0)