diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index fbe7db8aa7..676ac9f7ab 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We fixed an issue where missing consistency checks for the captions were causing runtime errors instead of in Studio Pro +- We added a new property for export to excel. The new property allows to set the cell export type and also the format for type number and date. + ## [3.6.1] - 2025-10-14 ### Fixed diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts index abd7f1cfb2..100229c5e9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts @@ -65,6 +65,14 @@ export function getProperties( 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 (!values.advanced && platform === "web") { hideNestedPropertiesIn(defaultProperties, values, "columns", index, [ "columnClass", @@ -214,7 +222,10 @@ export const getPreview = ( minWidth: "auto", minWidthLimit: 100, allowEventPropagation: true, - exportValue: "" + exportValue: "", + exportType: "default", + exportDateFormat: "", + exportNumberFormat: "" } ]; const columns = rowLayout({ diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index 6086784e80..b41aa58393 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -55,7 +55,10 @@ const initColumns: ColumnsPreviewType[] = [ minWidth: "auto", minWidthLimit: 100, allowEventPropagation: true, - exportValue: "" + exportValue: "", + exportDateFormat: "", + exportNumberFormat: "", + exportType: "default" } ]; diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index afb1d00ec4..1a404dd6f9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -127,6 +127,26 @@ Export value + + Export type + + + Default + Number + Date + Boolean + + + + Export number format + Optional Excel number format for exported numeric values (e.g. "#,##0.00", "$0.00", "0.00%"). See all formats https://docs.sheetjs.com/docs/csf/features/nf/ + + + + Export date format + Excel date format for exported Date/DateTime values (e.g. "yyyy-mm-dd", "dd/mm/yyyy hh mm"). + + Caption diff --git a/packages/pluggableWidgets/datagrid-web/src/features/data-export/DSExportRequest.ts b/packages/pluggableWidgets/datagrid-web/src/features/data-export/DSExportRequest.ts index 7898b5a76e..47bb3f51a4 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/data-export/DSExportRequest.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/data-export/DSExportRequest.ts @@ -1,17 +1,29 @@ import { isAvailable } from "@mendix/widget-plugin-platform/framework/is-available"; import Big from "big.js"; -import { ListValue, ObjectItem, ValueStatus } from "mendix"; +import { DynamicValue, ListValue, ObjectItem, ValueStatus } from "mendix"; import { createNanoEvents, Emitter, Unsubscribe } from "nanoevents"; import { ColumnsType, ShowContentAsEnum } from "../../../typings/DatagridProps"; -type RowData = Array; +/** 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) => string | boolean | number; +type ValueReader = (item: ObjectItem, props: ColumnsType) => ExcelCell; type ReadersByType = Record; @@ -252,49 +264,119 @@ export class DSExportRequest { const readers: ReadersByType = { attribute(item, props) { - if (props.attribute === undefined) { - return ""; + const data = props.attribute?.get(item); + + if (data?.status !== "available") { + return makeEmptyCell(); } - const data = props.attribute.get(item); + const value = data.value; + const format = getCellFormat({ + exportType: props.exportType, + exportDateFormat: props.exportDateFormat, + exportNumberFormat: props.exportNumberFormat + }); - if (data.status !== "available") { - return ""; + if (value instanceof Date) { + return excelDate(format === undefined ? data.displayValue : value, format); } - if (typeof data.value === "boolean") { - return data.value; + if (typeof value === "boolean") { + return excelBoolean(value); } - if (data.value instanceof Big) { - return data.value.toNumber(); + if (value instanceof Big || typeof value === "number") { + const num = value instanceof Big ? value.toNumber() : value; + return excelNumber(num, format); } - return data.displayValue; + return excelString(data.displayValue ?? ""); }, dynamicText(item, props) { - if (props.dynamicText === undefined) { - return ""; - } - - const data = props.dynamicText.get(item); + const data = props.dynamicText?.get(item); - switch (data.status) { + switch (data?.status) { case "available": - return data.value; + const format = getCellFormat({ + exportType: props.exportType, + exportDateFormat: props.exportDateFormat, + exportNumberFormat: props.exportNumberFormat + }); + + return excelString(data.value ?? "", format); case "unavailable": - return "n/a"; + return excelString("n/a"); default: - return ""; + return makeEmptyCell(); } }, customContent(item, props) { - return props.exportValue?.get(item).value ?? ""; + 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; + exportNumberFormat?: DynamicValue; +} + +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 => { diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx index bd16125514..9fe9f153d5 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx @@ -28,7 +28,8 @@ export const column = (header = "Test", patch?: (col: ColumnsType) => void): Col visible: dynamicValue(true), minWidth: "auto", minWidthLimit: 100, - allowEventPropagation: true + allowEventPropagation: true, + exportType: "default" }; if (patch) { diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index 513d2d6282..2682962226 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -17,6 +17,8 @@ export type LoadingTypeEnum = "spinner" | "skeleton"; export type ShowContentAsEnum = "attribute" | "dynamicText" | "customContent"; +export type ExportTypeEnum = "default" | "number" | "date" | "boolean"; + export type HidableEnum = "yes" | "hidden" | "no"; export type WidthEnum = "autoFill" | "autoFit" | "manual"; @@ -31,6 +33,9 @@ export interface ColumnsType { content?: ListWidgetValue; dynamicText?: ListExpressionValue; exportValue?: ListExpressionValue; + exportType: ExportTypeEnum; + exportNumberFormat?: DynamicValue; + exportDateFormat?: DynamicValue; header?: DynamicValue; tooltip?: ListExpressionValue; filter?: ReactNode; @@ -67,6 +72,9 @@ export interface ColumnsPreviewType { content: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; dynamicText: string; exportValue: string; + exportType: ExportTypeEnum; + exportNumberFormat: string; + exportDateFormat: string; header: string; tooltip: string; filter: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };