You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bug fix (non-breaking change which fixes an issue)
Description
Exporting a Data Grid 2 date column to Excel wrote the wrong wall clock: a value the grid renders as 1/1/2007 exported as serial 39082 — 31-Dec-2006, one full calendar day early. Before the 3.11.0/3.11.3 export work the same cell held 39082.958333 (31-Dec-2006 23:00), so users saw a stray time instead; the day shift was always present, the serial fraction merely masked which day it landed on.
Root cause is in the widget's own export path, not in the Mendix client or the date picker. SheetJS is internally inconsistent about which fields of a JS Date represent the sheet's wall clock: a raw Date through utils.aoa_to_sheet converts on the local fields, but a cell object ({ t: "d", v: Date }) defers conversion to write time, where the UTC fields are read. cell-readers.ts builds cell objects, and the Mendix client hands over a local-anchored Date, so the session's UTC offset leaked into every cell — then stripTime(), truncating on those same UTC fields, turned the stray hour into the previous day. Localize = OFF on the attribute is incidental; localized attributes were affected identically.
The fix re-anchors a date's local fields onto UTC before the cell is built, so a t: "d" cell carries exactly the wall clock the grid displays, independent of the session offset and of DST. This also fixes a second, unreported symptom found while diagnosing: time-bearing formats (dd-MMM-yyyy hh:mm) skipped stripTime() and exported the time shifted by the offset (13:35 for a 14:35 value). customContent date strings now get a zone-aware parse — strings naming a zone, and date-only ISO strings (UTC per the ECMAScript spec), are left as-is; only genuinely local-parsed strings are re-anchored.
Design and rationale: packages/pluggableWidgets/datagrid-web/openspec/changes/fix-excel-export-date-timezone/.
Scope note — WC-3536 reports three complaints, only this one needed code:
Long-number precision: already fixed in Data Widgets 3.11.3, re-confirmed here.
Boolean TRUE/FALSE vs the grid's Yes/No: not a defect (Excel renders a typed boolean cell as TRUE/FALSE by definition). Referred to the PM for a product decision; deliberately unimplemented in either direction.
No XML or property changes, so no mendix/docs PR is needed. Changelog entry added under [Unreleased] in datagrid-web. Export_To_Excel.js and the bundled SheetJS in @mendix/data-widgets are untouched.
Unit suite is green 235/235 under eight timezones (Europe/Amsterdam, America/New_York, America/Anchorage, Pacific/Kiritimati +14, Pacific/Niue −11, Asia/Kathmandu +5:45, Australia/Lord_Howe +10:30 with 30-minute DST, UTC). Verified end to end against the reporter's own app on Mendix 10.24.16.
Setup: a Data Grid 2 over an entity with a Date and time attribute (test with Localize both on and off — both were affected), plus an Export to Excel button wired to the Export_To_Excel JS action. Use a session timezone with a non-zero UTC offset — the bug is invisible in UTC.
Date-only format, midnight values. Set the column's export type to Date with format dd-MMM-yyyy. Add rows at midnight in both DST states (e.g. 1/1/2007, 3/30/2004, 9/3/2012). Export and confirm every exported date is the same calendar day the grid shows, with no time component and no fractional serial.
Time-bearing format. Add a second column on the same attribute with format dd-MMM-yyyy hh:mm and a row at a non-midnight time (e.g. 14:35). Confirm the exported time matches the grid exactly and is not shifted by the session offset.
Non-midnight value with a date-only format. Confirm the 14:35 row exports on the correct day with the time dropped. (This case was already correct before the fix — it should stay correct.)
Default export type. Add a third column on the same attribute with export type Default. Confirm the exported date is on the day the grid shows.
Custom content column. Add a column with custom content and export type Date, exporting a date string. Check a zoneless string (2007-01-01T00:00:00), a date-only ISO string (2007-01-01), and a string with an explicit zone (2007-01-01T00:00:00Z). All three should land on 01-Jan-2007. Worth repeating in a negative-offset timezone (e.g. America/New_York), which is where the date-only ISO case is most likely to regress.
Negative and fractional offsets. Repeat step 1 with the session timezone set to a negative offset and to a half-hour offset (e.g. Asia/Kathmandu).
Regression check on the 3.11.3 work. Confirm numeric columns still export with the grid's decimals and thousands grouping, and that values over 15 significant digits still export as exact text.
r0b1n
changed the title
[WC-3536]: Fix Data Grid 2 Excel date export writing the wrong day
[WC-3536][WC-3545]: Fix Data Grid 2 Excel date export writing the wrong day
Aug 20, 2026
Skipped (out of scope): dist/, pnpm-lock.yaml, OpenSpec YAML frontmatter
Findings
⚠️ Low — Duplicate it.each test titles for the two summer cases
File:src/features/data-export/__tests__/cell-readers.spec.ts line 371–384 Note: Both 2004-03-30 and 2012-09-03 produce the title "exports a summer midnight date on the same calendar day as the grid". If one fails, the Jest reporter shows two failures with identical titles and the only way to tell them apart is the expected value. Including the date in the label makes diagnostics faster:
it.each([["winter 2007-01-01",2007,0,1],["summer 2004-03-30",2004,2,30],["summer 2012-09-03",2012,8,3]])("exports a %s midnight date on the same calendar day as the grid", ...)
⚠️ Low — _season prefix signals "unused" but the parameter drives the test title
File:src/features/data-export/__tests__/cell-readers.spec.ts line 375 Note: The leading underscore convention says "not referenced in the function body", but %s in the it.each title string does reference it via Jest's printf interpolation. Renaming to season (no underscore) is more accurate and consistent with the other non-underscore parameters.
⚠️ Low — new Date("1/1/2007") in the locale-style test relies on V8-specific parsing
File:src/features/data-export/__tests__/cell-readers.spec.ts line 423 Note: ECMAScript does not define MM/DD/YYYY parsing — new Date("1/1/2007") is implementation-defined behavior that works in V8 (Node/Jest) but is not portable. The production code path is correct (an NaN result falls through to a string cell), and the test documents intentional behaviour; a short inline comment noting the V8 assumption would help a future reader who runs the spec on a non-V8 engine.
Positives
Root cause diagnosed with a throwaway Node harness that reproduced the reported serial 39082.958333333336 to the digit — no guessing involved.
toExcelWallClock is a clean single-responsibility helper; the decision to document the SheetJS contract at the function rather than inline is the right call given how non-obvious the UTC-field read is.
parseExportDate correctly guards date-only ISO strings (YYYY, YYYY-MM, YYYY-MM-DD) from re-anchoring — the subtle ECMAScript spec difference between date-only and date-time forms is handled precisely.
Test inputs are built with the local Date constructor and asserted against Date.UTC, making every case timezone-agnostic by construction — the design correctly rejected process.env.TZ pinning to avoid polluting the rest of the suite.
Five pre-existing TZ-fragile tests rewritten without weakening a single assertion; the commit message clearly explains why the old inputs were wrong.
CHANGELOG entry is user-facing and behaviour-focused, with no implementation detail leaking in.
The design doc explicitly records the SheetJS write-path inconsistency, the three decisions made and the alternatives rejected, and two residual risks with their mitigations — well above the bar for a surgical one-file fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull request type
Bug fix (non-breaking change which fixes an issue)
Description
Exporting a Data Grid 2 date column to Excel wrote the wrong wall clock: a value the grid renders as
1/1/2007exported as serial39082—31-Dec-2006, one full calendar day early. Before the 3.11.0/3.11.3 export work the same cell held39082.958333(31-Dec-2006 23:00), so users saw a stray time instead; the day shift was always present, the serial fraction merely masked which day it landed on.Root cause is in the widget's own export path, not in the Mendix client or the date picker. SheetJS is internally inconsistent about which fields of a JS
Daterepresent the sheet's wall clock: a rawDatethroughutils.aoa_to_sheetconverts on the local fields, but a cell object ({ t: "d", v: Date }) defers conversion to write time, where the UTC fields are read.cell-readers.tsbuilds cell objects, and the Mendix client hands over a local-anchoredDate, so the session's UTC offset leaked into every cell — thenstripTime(), truncating on those same UTC fields, turned the stray hour into the previous day.Localize = OFFon the attribute is incidental; localized attributes were affected identically.The fix re-anchors a date's local fields onto UTC before the cell is built, so a
t: "d"cell carries exactly the wall clock the grid displays, independent of the session offset and of DST. This also fixes a second, unreported symptom found while diagnosing: time-bearing formats (dd-MMM-yyyy hh:mm) skippedstripTime()and exported the time shifted by the offset (13:35for a14:35value).customContentdate strings now get a zone-aware parse — strings naming a zone, and date-only ISO strings (UTC per the ECMAScript spec), are left as-is; only genuinely local-parsed strings are re-anchored.Design and rationale:
packages/pluggableWidgets/datagrid-web/openspec/changes/fix-excel-export-date-timezone/.Scope note — WC-3536 reports three complaints, only this one needed code:
TRUE/FALSEvs the grid'sYes/No: not a defect (Excel renders a typed boolean cell as TRUE/FALSE by definition). Referred to the PM for a product decision; deliberately unimplemented in either direction.No XML or property changes, so no mendix/docs PR is needed. Changelog entry added under
[Unreleased]indatagrid-web.Export_To_Excel.jsand the bundled SheetJS in@mendix/data-widgetsare untouched.Ticket: https://mendix.atlassian.net/browse/WC-3536
What should be covered while testing?
Unit suite is green 235/235 under eight timezones (
Europe/Amsterdam,America/New_York,America/Anchorage,Pacific/Kiritimati+14,Pacific/Niue−11,Asia/Kathmandu+5:45,Australia/Lord_Howe+10:30 with 30-minute DST,UTC). Verified end to end against the reporter's own app on Mendix 10.24.16.Setup: a Data Grid 2 over an entity with a
Date and timeattribute (test with Localize both on and off — both were affected), plus an Export to Excel button wired to theExport_To_ExcelJS action. Use a session timezone with a non-zero UTC offset — the bug is invisible in UTC.Datewith formatdd-MMM-yyyy. Add rows at midnight in both DST states (e.g.1/1/2007,3/30/2004,9/3/2012). Export and confirm every exported date is the same calendar day the grid shows, with no time component and no fractional serial.dd-MMM-yyyy hh:mmand a row at a non-midnight time (e.g.14:35). Confirm the exported time matches the grid exactly and is not shifted by the session offset.14:35row exports on the correct day with the time dropped. (This case was already correct before the fix — it should stay correct.)Default. Confirm the exported date is on the day the grid shows.Date, exporting a date string. Check a zoneless string (2007-01-01T00:00:00), a date-only ISO string (2007-01-01), and a string with an explicit zone (2007-01-01T00:00:00Z). All three should land on01-Jan-2007. Worth repeating in a negative-offset timezone (e.g.America/New_York), which is where the date-only ISO case is most likely to regress.Asia/Kathmandu).