Skip to content

Commit 3f755d2

Browse files
committed
[IMP] Excel: export link colors
Currently, cells that contain a hyperlink are given a default color after evaluation but this coloration is not propagated when we export the data as xlsx file. This revision adds the default link color to the excel export. closes #7634 Task: 5410289 Signed-off-by: Adrien Minne (adrm) <adrm@odoo.com>
1 parent b893ca7 commit 3f755d2

File tree

3 files changed

+121
-22
lines changed

3 files changed

+121
-22
lines changed

packages/o-spreadsheet-engine/src/plugins/ui_feature/cell_computed_style.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { LINK_COLOR } from "../../constants";
22
import { PositionMap } from "../../helpers/cells/position_map";
3+
import { toCartesian } from "../../helpers/coordinates";
4+
import { getItemId } from "../../helpers/data_normalization";
35
import { isObjectEmptyRecursive, removeFalsyAttributes } from "../../helpers/misc";
6+
import { recomputeZones } from "../../helpers/recompute_zones";
7+
import { isZoneInside, toZone, zoneToXc } from "../../helpers/zones";
48
import {
59
Command,
610
invalidateBordersCommands,
711
invalidateCFEvaluationCommands,
812
invalidateEvaluationCommands,
913
} from "../../types/commands";
1014
import { Border, CellPosition, Style } from "../../types/misc";
15+
import { ExcelWorkbookData } from "../../types/workbook_data";
1116
import { UIPlugin } from "../ui_plugin";
1217
import { doesCommandInvalidatesTableStyle } from "./table_computed_style";
1318

@@ -71,22 +76,79 @@ export class CellComputedStylePlugin extends UIPlugin {
7176
}
7277

7378
private computeCellStyle(position: CellPosition): Style {
79+
const evaluatedCell = this.getters.getEvaluatedCell(position);
80+
const computedStyle = this.getComputedStyle(position);
81+
if (evaluatedCell.link && !computedStyle.textColor) {
82+
computedStyle.textColor = LINK_COLOR;
83+
}
84+
return computedStyle;
85+
}
86+
87+
private getComputedStyle(position: CellPosition): Style {
7488
const cell = this.getters.getCell(position);
7589
const cfStyle = this.getters.getCellConditionalFormatStyle(position);
7690
const tableStyle = this.getters.getCellTableStyle(position);
7791
const dataValidationStyle = this.getters.getDataValidationCellStyle(position);
78-
const computedStyle = {
92+
return {
7993
...removeFalsyAttributes(tableStyle),
8094
...removeFalsyAttributes(dataValidationStyle),
8195
...removeFalsyAttributes(cell?.style),
8296
...removeFalsyAttributes(cfStyle),
8397
};
84-
const evaluatedCell = this.getters.getEvaluatedCell(position);
85-
if (evaluatedCell.link && !computedStyle.textColor) {
86-
computedStyle.textColor = LINK_COLOR;
87-
}
98+
}
8899

89-
return computedStyle;
100+
exportForExcel(data: ExcelWorkbookData) {
101+
for (const sheet of data.sheets) {
102+
// Collect all link cells that need LINK_COLOR, grouped by their containing style zone to avoid O(n^2) calls to `recomputeZones`
103+
const linkCellsByStyleZone: Record<string, string[]> = {};
104+
// Some link cells might not be part of any style zone, handled separately
105+
const linkCellsWithoutStyleZone: string[] = [];
106+
107+
for (const xc in sheet.cells) {
108+
const position = { sheetId: sheet.id, ...toCartesian(xc) };
109+
const evaluatedCell = this.getters.getEvaluatedCell(position);
110+
const computedStyle = this.getComputedStyle(position);
111+
if (!evaluatedCell.link || computedStyle.textColor) {
112+
continue;
113+
}
114+
const styleXc = Object.keys(sheet.styles).find((styleXc) =>
115+
isZoneInside(toZone(xc), toZone(styleXc))
116+
);
117+
if (styleXc) {
118+
if (!linkCellsByStyleZone[styleXc]) {
119+
linkCellsByStyleZone[styleXc] = [];
120+
}
121+
linkCellsByStyleZone[styleXc].push(xc);
122+
} else {
123+
linkCellsWithoutStyleZone.push(xc);
124+
}
125+
}
126+
127+
for (const [styleXc, linkXcs] of Object.entries(linkCellsByStyleZone)) {
128+
const existingStyleId = sheet.styles[styleXc];
129+
if (data.styles[existingStyleId].textColor) {
130+
continue;
131+
}
132+
const existingStyle = data.styles[existingStyleId];
133+
const linkZones = linkXcs.map(toZone);
134+
const remainingZones = recomputeZones([toZone(styleXc)], linkZones);
135+
136+
delete sheet.styles[styleXc];
137+
138+
for (const zone of remainingZones) {
139+
sheet.styles[zoneToXc(zone)] = existingStyleId;
140+
}
141+
const linkStyleId = getItemId({ ...existingStyle, textColor: LINK_COLOR }, data.styles);
142+
for (const xc of linkXcs) {
143+
sheet.styles[xc] = linkStyleId;
144+
}
145+
}
146+
147+
for (const xc of linkCellsWithoutStyleZone) {
148+
const cell = this.getters.getCell({ sheetId: sheet.id, ...toCartesian(xc) });
149+
sheet.styles[xc] = getItemId({ ...cell?.style, textColor: LINK_COLOR }, data.styles);
150+
}
151+
}
90152
}
91153

92154
private computeCellBorder(position: CellPosition): Border | null {

tests/xlsx/__snapshots__/xlsx_export.test.ts.snap

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33788,7 +33788,7 @@ exports[`Test XLSX export formulas All exportable formulas 1`] = `
3378833788
</c>
3378933789
</row>
3379033790
<row r="165">
33791-
<c r="A165" cm="1" t="str">
33791+
<c r="A165" s="1" cm="1" t="str">
3379233792
<f t="array" ref="A165">
3379333793
HYPERLINK("https://www.odoo.com","Odoo")
3379433794
</f>
@@ -33867,12 +33867,17 @@ exports[`Test XLSX export formulas All exportable formulas 1`] = `
3386733867
"content": "<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
3386833868
<numFmts count="0">
3386933869
</numFmts>
33870-
<fonts count="1">
33870+
<fonts count="2">
3387133871
<font>
3387233872
<sz val="10"/>
3387333873
<color rgb="000000"/>
3387433874
<name val="Arial"/>
3387533875
</font>
33876+
<font>
33877+
<sz val="10"/>
33878+
<color rgb="017E84"/>
33879+
<name val="Arial"/>
33880+
</font>
3387633881
</fonts>
3387733882
<fills count="2">
3387833883
<fill>
@@ -33896,8 +33901,9 @@ exports[`Test XLSX export formulas All exportable formulas 1`] = `
3389633901
</diagonal>
3389733902
</border>
3389833903
</borders>
33899-
<cellXfs count="1">
33904+
<cellXfs count="2">
3390033905
<xf numFmtId="0" fillId="0" fontId="0" borderId="0"/>
33906+
<xf numFmtId="0" fillId="0" fontId="1" borderId="0"/>
3390133907
</cellXfs>
3390233908
<dxfs count="0">
3390333909
</dxfs>
@@ -37183,7 +37189,7 @@ exports[`Test XLSX export formulas Multi-Sheets exportable functions 1`] = `
3718337189
</c>
3718437190
</row>
3718537191
<row r="165">
37186-
<c r="A165" cm="1" t="str">
37192+
<c r="A165" s="1" cm="1" t="str">
3718737193
<f t="array" ref="A165">
3718837194
HYPERLINK("https://www.odoo.com","Odoo")
3718937195
</f>
@@ -37313,12 +37319,17 @@ exports[`Test XLSX export formulas Multi-Sheets exportable functions 1`] = `
3731337319
"content": "<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
3731437320
<numFmts count="0">
3731537321
</numFmts>
37316-
<fonts count="1">
37322+
<fonts count="2">
3731737323
<font>
3731837324
<sz val="10"/>
3731937325
<color rgb="000000"/>
3732037326
<name val="Arial"/>
3732137327
</font>
37328+
<font>
37329+
<sz val="10"/>
37330+
<color rgb="017E84"/>
37331+
<name val="Arial"/>
37332+
</font>
3732237333
</fonts>
3732337334
<fills count="2">
3732437335
<fill>
@@ -37342,8 +37353,9 @@ exports[`Test XLSX export formulas Multi-Sheets exportable functions 1`] = `
3734237353
</diagonal>
3734337354
</border>
3734437355
</borders>
37345-
<cellXfs count="1">
37356+
<cellXfs count="2">
3734637357
<xf numFmtId="0" fillId="0" fontId="0" borderId="0"/>
37358+
<xf numFmtId="0" fillId="0" fontId="1" borderId="0"/>
3734737359
</cellXfs>
3734837360
<dxfs count="0">
3734937361
</dxfs>
@@ -37891,35 +37903,35 @@ exports[`Test XLSX export link cells 1`] = `
3789137903
</cols>
3789237904
<sheetData>
3789337905
<row r="1">
37894-
<c r="A1" t="s">
37906+
<c r="A1" s="1" t="s">
3789537907
<v>
3789637908
0
3789737909
</v>
3789837910
</c>
3789937911
</row>
3790037912
<row r="2">
37901-
<c r="A2" t="s">
37913+
<c r="A2" s="1" t="s">
3790237914
<v>
3790337915
0
3790437916
</v>
3790537917
</c>
3790637918
</row>
3790737919
<row r="3">
37908-
<c r="A3" t="s">
37920+
<c r="A3" s="1" t="s">
3790937921
<v>
3791037922
1
3791137923
</v>
3791237924
</c>
3791337925
</row>
3791437926
<row r="4">
37915-
<c r="A4" t="s">
37927+
<c r="A4" s="1" t="s">
3791637928
<v>
3791737929
2
3791837930
</v>
3791937931
</c>
3792037932
</row>
3792137933
<row r="5">
37922-
<c r="A5" t="s">
37934+
<c r="A5" s="1" t="s">
3792337935
<v>
3792437936
1
3792537937
</v>
@@ -37964,12 +37976,17 @@ exports[`Test XLSX export link cells 1`] = `
3796437976
"content": "<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
3796537977
<numFmts count="0">
3796637978
</numFmts>
37967-
<fonts count="1">
37979+
<fonts count="2">
3796837980
<font>
3796937981
<sz val="10"/>
3797037982
<color rgb="000000"/>
3797137983
<name val="Arial"/>
3797237984
</font>
37985+
<font>
37986+
<sz val="10"/>
37987+
<color rgb="017E84"/>
37988+
<name val="Arial"/>
37989+
</font>
3797337990
</fonts>
3797437991
<fills count="2">
3797537992
<fill>
@@ -37993,8 +38010,9 @@ exports[`Test XLSX export link cells 1`] = `
3799338010
</diagonal>
3799438011
</border>
3799538012
</borders>
37996-
<cellXfs count="1">
38013+
<cellXfs count="2">
3799738014
<xf numFmtId="0" fillId="0" fontId="0" borderId="0"/>
38015+
<xf numFmtId="0" fillId="0" fontId="1" borderId="0"/>
3799838016
</cellXfs>
3799938017
<dxfs count="0">
3800038018
</dxfs>
@@ -38477,7 +38495,7 @@ exports[`Test XLSX export multiple elements are exported in the correct order 1`
3847738495
</cols>
3847838496
<sheetData>
3847938497
<row r="1">
38480-
<c r="A1" t="s">
38498+
<c r="A1" s="1" t="s">
3848138499
<v>
3848238500
0
3848338501
</v>
@@ -38529,12 +38547,17 @@ exports[`Test XLSX export multiple elements are exported in the correct order 1`
3852938547
"content": "<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
3853038548
<numFmts count="0">
3853138549
</numFmts>
38532-
<fonts count="1">
38550+
<fonts count="2">
3853338551
<font>
3853438552
<sz val="10"/>
3853538553
<color rgb="000000"/>
3853638554
<name val="Arial"/>
3853738555
</font>
38556+
<font>
38557+
<sz val="10"/>
38558+
<color rgb="017E84"/>
38559+
<name val="Arial"/>
38560+
</font>
3853838561
</fonts>
3853938562
<fills count="2">
3854038563
<fill>
@@ -38558,8 +38581,9 @@ exports[`Test XLSX export multiple elements are exported in the correct order 1`
3855838581
</diagonal>
3855938582
</border>
3856038583
</borders>
38561-
<cellXfs count="1">
38584+
<cellXfs count="2">
3856238585
<xf numFmtId="0" fillId="0" fontId="0" borderId="0"/>
38586+
<xf numFmtId="0" fillId="0" fontId="1" borderId="0"/>
3856338587
</cellXfs>
3856438588
<dxfs count="1">
3856538589
<dxf>

tests/xlsx/xlsx_import_export.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { LINK_COLOR } from "@odoo/o-spreadsheet-engine/constants";
12
import { isXLSXExportXMLFile } from "@odoo/o-spreadsheet-engine/xlsx/helpers/xlsx_helper";
23
import { Model } from "../../src";
34
import { buildSheetLink, toZone } from "../../src/helpers";
@@ -31,6 +32,7 @@ import {
3132
getCell,
3233
getCellRawContent,
3334
getEvaluatedCell,
35+
getStyle,
3436
} from "../test_helpers/getters_helpers";
3537
import { toRangesData } from "../test_helpers/helpers";
3638

@@ -432,6 +434,17 @@ describe("Export data to xlsx then import it", () => {
432434
expect(cell.link?.url).toBe(sheetLink2);
433435
});
434436

437+
test("hyperlinks are exported with their own style", async () => {
438+
const sheetLink = buildSheetLink("42");
439+
setCellContent(model, "A1", `[my label](${sheetLink})`);
440+
setFormatting(model, "A1:A3", { fillColor: "#FF0000" });
441+
const importedModel = await exportToXlsxThenImport(model);
442+
expect(getStyle(importedModel, "A1")).toMatchObject({
443+
fillColor: "#FF0000",
444+
textColor: LINK_COLOR,
445+
});
446+
});
447+
435448
test("Image", async () => {
436449
createImage(model, {
437450
figureId: "1",

0 commit comments

Comments
 (0)