diff --git a/src/xlsx/functions/table.ts b/src/xlsx/functions/table.ts index 0ed171dc47..6a20a2c23b 100644 --- a/src/xlsx/functions/table.ts +++ b/src/xlsx/functions/table.ts @@ -76,6 +76,15 @@ function addTableColumns(table: ExcelTableData, sheetData: ExcelSheetData): XMLS ["id", i + 1], // id cannot be 0 ["name", colName], ]; + if (table.config.totalRow) { + // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag + // `` for the formula of the total. But those doesn't seem to be mandatory for Excel. + const colTotalXc = toXC(tableZone.left + i, tableZone.bottom); + const colTotalContent = sheetData.cells[colTotalXc]?.content; + if (colTotalContent?.startsWith("=")) { + colAttributes.push(["totalsRowFunction", "custom"]); + } + } columns.push(escapeXml/*xml*/ ``); } diff --git a/src/xlsx/functions/worksheet.ts b/src/xlsx/functions/worksheet.ts index 162f447e33..f950304443 100644 --- a/src/xlsx/functions/worksheet.ts +++ b/src/xlsx/functions/worksheet.ts @@ -105,11 +105,12 @@ export function addRows( ({ attrs: additionalAttrs, node: cellNode } = addContent(label, construct.sharedStrings)); } else if (cell.content && cell.content !== "") { const isTableHeader = isCellTableHeader(c, r, sheet); + const isTableTotal = isCellTableTotal(c, r, sheet); const isPlainText = !!(cell.format && data.formats[cell.format] === PLAIN_TEXT_FORMAT); ({ attrs: additionalAttrs, node: cellNode } = addContent( cell.content, construct.sharedStrings, - isTableHeader || isPlainText + isTableHeader || isTableTotal || isPlainText )); } attributes.push(...additionalAttrs); @@ -148,6 +149,17 @@ function isCellTableHeader(col: HeaderIndex, row: HeaderIndex, sheet: ExcelSheet }); } +function isCellTableTotal(col: HeaderIndex, row: HeaderIndex, sheet: ExcelSheetData): boolean { + return sheet.tables.some((table) => { + if (!table.config.totalRow) { + return false; + } + const zone = toZone(table.range); + const totalZone = { ...zone, top: zone.bottom }; + return isInside(col, row, totalZone); + }); +} + export function addHyperlinks( construct: XLSXStructure, data: ExcelWorkbookData, diff --git a/tests/xlsx/__snapshots__/xlsx_export.test.ts.snap b/tests/xlsx/__snapshots__/xlsx_export.test.ts.snap index 05b21d7eaf..f7b04f15ed 100644 --- a/tests/xlsx/__snapshots__/xlsx_export.test.ts.snap +++ b/tests/xlsx/__snapshots__/xlsx_export.test.ts.snap @@ -19095,11 +19095,12 @@ exports[`Test XLSX export Export data filters Export data filters snapshot 1`] = exports[`Test XLSX export Export data filters Table style is correctly exported 1`] = ` { - "content": " - + "content": "
+ - + +
", diff --git a/tests/xlsx/xlsx_export.test.ts b/tests/xlsx/xlsx_export.test.ts index a3b31d271d..748fe1f669 100644 --- a/tests/xlsx/xlsx_export.test.ts +++ b/tests/xlsx/xlsx_export.test.ts @@ -1494,7 +1494,7 @@ describe("Test XLSX export", () => { test("Table style is correctly exported", async () => { const model = new Model(); - createTable(model, "A1:A4", { + createTable(model, "A1:B4", { totalRow: true, firstColumn: true, lastColumn: true, @@ -1503,6 +1503,8 @@ describe("Test XLSX export", () => { bandedColumns: true, styleId: "TableStyleMedium9", }); + setCellContent(model, "A4", "5"); + setCellContent(model, "B4", "=65+9"); const exported = await exportPrettifiedXlsx(model); const tableFile = exported.files.find((file) => file.path === "xl/tables/table1.xml"); const xml = parseXML(new XMLString((tableFile as XLSXExportXMLFile)?.content)); @@ -1518,6 +1520,13 @@ describe("Test XLSX export", () => { expect(tableStyle?.getAttribute("showRowStripes")).toEqual("1"); expect(tableStyle?.getAttribute("showColumnStripes")).toEqual("1"); + const worksheet = exported.files.find((file) => file.path === "xl/worksheets/sheet0.xml"); + const sheetXML = parseXML(new XMLString((worksheet as XLSXExportXMLFile)?.content)); + const A4 = sheetXML.querySelector("worksheet row c[r='A4']"); + expect(A4?.getAttribute("t")).toEqual("s"); // A4 was exported as a string + const tableCol2 = xml.querySelector("tableColumn[id='2']"); + expect(tableCol2?.getAttribute("totalsRowFunction")).toEqual("custom"); // Column with B4 has a custom total row function + expect(tableFile).toMatchSnapshot(); });