Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8afba9f
refactor: extract cell readers from DSExportRequest into separate module
samuelreichert Apr 20, 2026
9ddac3a
test: add baseline tests for cell reader export behavior
samuelreichert Apr 20, 2026
79bbd26
fix: export customContent columns as number cells when exportType is …
samuelreichert Apr 20, 2026
79898a2
fix: export customContent columns as date cells when exportType is date
samuelreichert Apr 20, 2026
2c57751
fix: strip time component from exported dates when format is date-only
samuelreichert Apr 20, 2026
0253720
fix: export boolean values as Yes/No strings instead of TRUE/FALSE
samuelreichert Apr 20, 2026
51c7499
fix: export large numbers as strings to preserve precision beyond 15 …
samuelreichert Apr 20, 2026
7cb0d25
docs: add changelog entries for data export bug fixes
samuelreichert Apr 20, 2026
ee0d828
refactor: remove dead boolean cell type and fix test name
samuelreichert Apr 20, 2026
a60107d
test(datagrid-web): pin date reference and assert display value in ce…
samuelreichert May 1, 2026
6ae2aea
refactor(datagrid-web): tighten excelDate overloads and remove dead code
samuelreichert May 12, 2026
9724db3
fix(datagrid-web): harden whitespace guard and locale-tag stripping
samuelreichert May 19, 2026
45c6730
test(datagrid-web): cover whitespace number fallback and unavailable …
samuelreichert May 19, 2026
1d0c4b2
docs(datagrid-web): reorder CHANGELOG to Added → Fixed per Keep a Cha…
samuelreichert May 19, 2026
6757af1
fix(datagrid-web): export booleans as native cells and dates with loc…
samuelreichert May 21, 2026
d8f6575
fix(datagrid-web): export birth year as string in Excel output
samuelreichert May 22, 2026
ccc7df9
fix(datagrid-web): guard customContent reader on status and drop z fr…
samuelreichert May 22, 2026
73d5735
test(datagrid-web): assert Birth year is a numeric cell, not a string
samuelreichert May 22, 2026
a376335
fix(line-chart-web): ensure elements are scrolled into view before vi…
samuelreichert May 22, 2026
b7255d4
fix(video-player-web): ensure video widget is scrolled into view and …
samuelreichert May 22, 2026
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 @@ -12,6 +12,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Fixed

- We fixed an issue where custom content columns ignored the export type setting, causing numbers and dates to always export as text in Excel.

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

- 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.

- 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.

- We fixed an issue where the vertical scrollbar disappeared after hiding a wide column while virtual scrolling was enabled.
- We fixed an issue where only the first page loaded when the grid had enough columns to require horizontal scrolling.

Expand Down
20 changes: 14 additions & 6 deletions packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,28 @@ test.describe("datagrid-web export to Excel", () => {
// Read file and convert to JSON.
const workbook = XLSX.readFile("./e2e/downloads/testFilename.xlsx");
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
const rawData = XLSX.utils.sheet_to_json(worksheet, { raw: true });
const formattedData = XLSX.utils.sheet_to_json(worksheet, { raw: false });

expect(jsonData).toHaveLength(50);
expect(rawData).toHaveLength(50);

expect(jsonData[0]).toEqual({
// Verify raw cell types — numbers must be t:"n", not t:"s"
expect(rawData[0]["Birth year"]).toBe(1983);
expect(typeof rawData[0]["Birth year"]).toBe("number");
expect(rawData[1]["Birth year"]).toBe(1970);
expect(typeof rawData[1]["Birth year"]).toBe("number");

// Verify formatted display values
expect(formattedData[0]).toEqual({
"Birth date": "2/15/1983",
"Birth year": 1983,
"Birth year": "1983",
"Color (enum)": "Black",
"First name": "Loretta"
});

expect(jsonData[1]).toEqual({
expect(formattedData[1]).toEqual({
"Birth date": "9/30/1970",
"Birth year": 1970,
"Birth year": "1970",
"Color (enum)": "Red",
"First name": "Chad"
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,8 @@
import { isAvailable } from "@mendix/widget-plugin-platform/framework/is-available";
import Big from "big.js";
import { DynamicValue, ListValue, ObjectItem, ValueStatus } from "mendix";
import { ListValue, ObjectItem, ValueStatus } from "mendix";
import { createNanoEvents, Emitter, Unsubscribe } from "nanoevents";
import { ColumnsType, ShowContentAsEnum } from "../../../typings/DatagridProps";

/** Represents a single Excel cell (SheetJS compatible) */
interface ExcelCell {
/** Cell type: 's' = string, 'n' = number, 'b' = boolean, 'd' = date */
t: "s" | "n" | "b" | "d";
/** Underlying value */
v: string | number | boolean | Date;
/** Optional Excel number/date format, e.g. "yyyy-mm-dd" or "$0.00" */
z?: string;
/** Optional pre-formatted display text */
w?: string;
}

type RowData = ExcelCell[];

type HeaderDefinition = {
name: string;
type: string;
};

type ValueReader = (item: ObjectItem, props: ColumnsType) => ExcelCell;

type ReadersByType = Record<ShowContentAsEnum, ValueReader>;

type RowReader = (item: ObjectItem) => RowData;
import { ColumnsType } from "../../../typings/DatagridProps";
import { HeaderDefinition, RowData, readChunk } from "./cell-readers";

type ColumnReader = (props: ColumnsType) => HeaderDefinition;

Expand Down Expand Up @@ -262,132 +237,6 @@ export class DSExportRequest {
}
}

const readers: ReadersByType = {
attribute(item, props) {
const data = props.attribute?.get(item);

if (data?.status !== "available") {
return makeEmptyCell();
}

const value = data.value;
const format = getCellFormat({
exportType: props.exportType,
exportDateFormat: props.exportDateFormat,
exportNumberFormat: props.exportNumberFormat
});

if (value instanceof Date) {
return excelDate(format === undefined ? data.displayValue : value, format);
}

if (typeof value === "boolean") {
return excelBoolean(value);
}

if (value instanceof Big || typeof value === "number") {
const num = value instanceof Big ? value.toNumber() : value;
return excelNumber(num, format);
}

return excelString(data.displayValue ?? "");
},

dynamicText(item, props) {
const data = props.dynamicText?.get(item);

switch (data?.status) {
case "available":
const format = getCellFormat({
exportType: props.exportType,
exportDateFormat: props.exportDateFormat,
exportNumberFormat: props.exportNumberFormat
});

return excelString(data.value ?? "", format);
case "unavailable":
return excelString("n/a");
default:
return makeEmptyCell();
}
},

customContent(item, props) {
const value = props.exportValue?.get(item).value ?? "";
const format = getCellFormat({
exportType: props.exportType,
exportDateFormat: props.exportDateFormat,
exportNumberFormat: props.exportNumberFormat
});

return excelString(value, format);
}
};

function makeEmptyCell(): ExcelCell {
return { t: "s", v: "" };
}

function excelNumber(value: number, format?: string): ExcelCell {
return {
t: "n",
v: value,
z: format
};
}

function excelString(value: string, format?: string): ExcelCell {
return {
t: "s",
v: value,
z: format ?? undefined
};
}

function excelDate(value: string | Date, format?: string): ExcelCell {
return {
t: format === undefined ? "s" : "d",
v: value,
z: format
};
}

function excelBoolean(value: boolean): ExcelCell {
return {
t: "b",
v: value,
w: value ? "TRUE" : "FALSE"
};
}

interface DataExportProps {
exportType: "default" | "number" | "date" | "boolean";
exportDateFormat?: DynamicValue<string>;
exportNumberFormat?: DynamicValue<string>;
}

function getCellFormat({ exportType, exportDateFormat, exportNumberFormat }: DataExportProps): string | undefined {
switch (exportType) {
case "date":
return exportDateFormat?.status === "available" ? exportDateFormat.value : undefined;
case "number":
return exportNumberFormat?.status === "available" ? exportNumberFormat.value : undefined;
default:
return undefined;
}
}

function createRowReader(columns: ColumnsType[]): RowReader {
return item =>
columns.map(col => {
return readers[col.showContentAs](item, col);
});
}

function readChunk(data: ObjectItem[], columns: ColumnsType[]): RowData[] {
return data.map(createRowReader(columns));
}

declare global {
interface Window {
scheduler: {
Expand Down
Loading
Loading