@odoo/o-spreadsheet 18.4.0-alpha.3 → 18.4.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.4.0-alpha.3
6
- * @date 2025-05-13T17:54:54.061Z
7
- * @hash 70ad365
5
+ * @version 18.4.0-alpha.4
6
+ * @date 2025-05-20T05:57:45.452Z
7
+ * @hash 5c28bca
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -185,7 +185,7 @@
185
185
  const ALERT_INFO_BORDER = "#98DBE2";
186
186
  const ALERT_INFO_TEXT_COLOR = "#09414A";
187
187
  const BADGE_SELECTED_COLOR = "#E6F2F3";
188
- const CHART_PADDING$1 = 20;
188
+ const CHART_PADDING = 20;
189
189
  const CHART_PADDING_BOTTOM = 10;
190
190
  const CHART_PADDING_TOP = 15;
191
191
  const CHART_TITLE_FONT_SIZE = 16;
@@ -347,6 +347,8 @@
347
347
  const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
348
348
  const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#43C5B1";
349
349
  const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#EA6175";
350
+ const DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE = 32;
351
+ const DEFAULT_SCORECARD_BASELINE_FONT_SIZE = 16;
350
352
  const LINE_FILL_TRANSPARENCY = 0.4;
351
353
  const DEFAULT_WINDOW_SIZE = 2;
352
354
  // session
@@ -396,6 +398,7 @@
396
398
  styleId: "TableStyleMedium5",
397
399
  automaticAutofill: false,
398
400
  };
401
+ const PIVOT_INDENT = 15;
399
402
  const DEFAULT_CURRENCY = {
400
403
  symbol: "$",
401
404
  position: "before",
@@ -1637,18 +1640,53 @@
1637
1640
  let result = 0;
1638
1641
  const l = letters.length;
1639
1642
  for (let i = 0; i < l; i++) {
1640
- const charCode = letters.charCodeAt(i);
1641
- const colIndex = charCode >= 65 && charCode <= 90 ? charCode - 64 : charCode - 96;
1643
+ const colIndex = charToNumber(letters[i]);
1642
1644
  result = result * 26 + colIndex;
1643
1645
  }
1644
1646
  return result - 1;
1645
1647
  }
1648
+ function charToNumber(char) {
1649
+ const charCode = char.charCodeAt(0);
1650
+ return charCode >= 65 && charCode <= 90 ? charCode - 64 : charCode - 96;
1651
+ }
1646
1652
  function isCharALetter(char) {
1647
1653
  return (char >= "A" && char <= "Z") || (char >= "a" && char <= "z");
1648
1654
  }
1649
1655
  function isCharADigit(char) {
1650
1656
  return char >= "0" && char <= "9";
1651
1657
  }
1658
+ // we limit the max column to 3 letters and max row to 7 digits for performance reasons
1659
+ const MAX_COL = lettersToNumber("ZZZ");
1660
+ const MAX_ROW = 9999998;
1661
+ function consumeSpaces(chars) {
1662
+ while (chars.current === " ") {
1663
+ chars.advanceBy(1);
1664
+ }
1665
+ }
1666
+ function consumeLetters(chars) {
1667
+ if (chars.current === "$")
1668
+ chars.advanceBy(1);
1669
+ if (!chars.current || !isCharALetter(chars.current)) {
1670
+ return -1;
1671
+ }
1672
+ let colCoordinate = 0;
1673
+ while (chars.current && isCharALetter(chars.current)) {
1674
+ colCoordinate = colCoordinate * 26 + charToNumber(chars.shift());
1675
+ }
1676
+ return colCoordinate;
1677
+ }
1678
+ function consumeDigits(chars) {
1679
+ if (chars.current === "$")
1680
+ chars.advanceBy(1);
1681
+ if (!chars.current || !isCharADigit(chars.current)) {
1682
+ return -1;
1683
+ }
1684
+ let num = 0;
1685
+ while (chars.current && isCharADigit(chars.current)) {
1686
+ num = num * 10 + Number(chars.shift());
1687
+ }
1688
+ return num;
1689
+ }
1652
1690
  /**
1653
1691
  * Convert a "XC" coordinate to cartesian coordinates.
1654
1692
  *
@@ -1659,33 +1697,17 @@
1659
1697
  * Note: it also accepts lowercase coordinates, but not fixed references
1660
1698
  */
1661
1699
  function toCartesian(xc) {
1662
- xc = xc.trim();
1663
- let letterPart = "";
1664
- let numberPart = "";
1665
- let i = 0;
1666
- // Process letter part
1667
- if (xc[i] === "$")
1668
- i++;
1669
- while (i < xc.length && isCharALetter(xc[i])) {
1670
- letterPart += xc[i++];
1671
- }
1672
- if (letterPart.length === 0 || letterPart.length > 3) {
1673
- // limit to max 3 letters for performance reasons
1700
+ const chars = new TokenizingChars(xc);
1701
+ consumeSpaces(chars);
1702
+ const letterPart = consumeLetters(chars);
1703
+ if (letterPart === -1 || !chars.current) {
1674
1704
  throw new Error(`Invalid cell description: ${xc}`);
1675
1705
  }
1676
- // Process number part
1677
- if (xc[i] === "$")
1678
- i++;
1679
- while (i < xc.length && isCharADigit(xc[i])) {
1680
- numberPart += xc[i++];
1681
- }
1682
- if (i !== xc.length || numberPart.length === 0 || numberPart.length > 7) {
1683
- // limit to max 7 numbers for performance reasons
1684
- throw new Error(`Invalid cell description: ${xc}`);
1685
- }
1686
- const col = lettersToNumber(letterPart);
1687
- const row = Number(numberPart) - 1;
1688
- if (isNaN(row)) {
1706
+ const num = consumeDigits(chars);
1707
+ consumeSpaces(chars);
1708
+ const col = letterPart - 1;
1709
+ const row = num - 1;
1710
+ if (!chars.isOver() || col > MAX_COL || row > MAX_ROW) {
1689
1711
  throw new Error(`Invalid cell description: ${xc}`);
1690
1712
  }
1691
1713
  return { col, row };
@@ -5374,67 +5396,6 @@
5374
5396
  return result;
5375
5397
  }
5376
5398
 
5377
- /** Reference of a cell (eg. A1, $B$5) */
5378
- const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
5379
- // Same as above, but matches the exact string (nothing before or after)
5380
- const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
5381
- /** Reference of a column header (eg. A, AB, $A) */
5382
- const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
5383
- /** Reference of a row header (eg. 1, $1) */
5384
- const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
5385
- /** Reference of a column (eg. A, $CA, Sheet1!B) */
5386
- const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
5387
- /** Reference of a row (eg. 1, 59, Sheet1!9) */
5388
- const rowReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([0-9]{1,7})$/, "i");
5389
- /** Reference of a normal range or a full row range (eg. A1:B1, 1:$5, $A2:5) */
5390
- const fullRowXc = /(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*:\s*(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*/i;
5391
- /** Reference of a normal range or a column row range (eg. A1:B1, A:$B, $A1:C) */
5392
- const fullColXc = /\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*:\s*\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*/i;
5393
- /** Reference of a cell or a range, it can be a bounded range, a full row or a full column */
5394
- const rangeReference = new RegExp(/^\s*('.+'!|[^']+!)?/.source +
5395
- "(" +
5396
- [cellReference.source, fullRowXc.source, fullColXc.source].join("|") +
5397
- ")" +
5398
- /$/.source, "i");
5399
- /**
5400
- * Return true if the given xc is the reference of a column (e.g. A or AC or Sheet1!A)
5401
- */
5402
- function isColReference(xc) {
5403
- return colReference.test(xc);
5404
- }
5405
- /**
5406
- * Return true if the given xc is the reference of a column (e.g. 1 or Sheet1!1)
5407
- */
5408
- function isRowReference(xc) {
5409
- return rowReference.test(xc);
5410
- }
5411
- function isColHeader(str) {
5412
- return colHeader.test(str);
5413
- }
5414
- function isRowHeader(str) {
5415
- return rowHeader.test(str);
5416
- }
5417
- /**
5418
- * Return true if the given xc is the reference of a single cell,
5419
- * without any specified sheet (e.g. A1)
5420
- */
5421
- function isSingleCellReference(xc) {
5422
- return singleCellReference.test(xc);
5423
- }
5424
- function splitReference(ref) {
5425
- if (!ref.includes("!")) {
5426
- return { xc: ref };
5427
- }
5428
- const parts = ref.split("!");
5429
- const xc = parts.pop();
5430
- const sheetName = getUnquotedSheetName(parts.join("!")) || undefined;
5431
- return { sheetName, xc };
5432
- }
5433
- /** Return a reference SheetName!xc from the given arguments */
5434
- function getFullReference(sheetName, xc) {
5435
- return sheetName !== undefined ? `${getCanonicalSymbolName(sheetName)}!${xc}` : xc;
5436
- }
5437
-
5438
5399
  /**
5439
5400
  * Convert from a cartesian reference to a Zone
5440
5401
  * The range boundaries will be kept in the same order as the
@@ -5452,63 +5413,55 @@
5452
5413
  *
5453
5414
  */
5454
5415
  function toZoneWithoutBoundaryChanges(xc) {
5455
- if (xc.includes("!")) {
5456
- xc = xc.split("!").at(-1);
5457
- }
5458
- if (xc.includes("$")) {
5459
- xc = xc.replaceAll("$", "");
5460
- }
5461
- let firstRangePart = "";
5462
- let secondRangePart;
5463
- if (xc.includes(":")) {
5464
- [firstRangePart, secondRangePart] = xc.split(":");
5465
- firstRangePart = firstRangePart.trim();
5466
- secondRangePart = secondRangePart.trim();
5467
- }
5468
- else {
5469
- firstRangePart = xc.trim();
5470
- }
5416
+ const chars = new TokenizingChars(xc);
5417
+ consumeSpaces(chars);
5418
+ const sheetSeparatorIndex = xc.indexOf("!");
5419
+ if (sheetSeparatorIndex !== -1) {
5420
+ chars.advanceBy(sheetSeparatorIndex + 1);
5421
+ }
5422
+ const leftLetters = consumeLetters(chars);
5423
+ const leftNumbers = consumeDigits(chars);
5471
5424
  let top, bottom, left, right;
5472
5425
  let fullCol = false;
5473
5426
  let fullRow = false;
5474
5427
  let hasHeader = false;
5475
- if (isColReference(firstRangePart)) {
5476
- left = right = lettersToNumber(firstRangePart);
5428
+ if (leftNumbers === -1) {
5429
+ left = right = leftLetters - 1;
5477
5430
  top = bottom = 0;
5478
5431
  fullCol = true;
5479
5432
  }
5480
- else if (isRowReference(firstRangePart)) {
5481
- top = bottom = parseInt(firstRangePart, 10) - 1;
5433
+ else if (leftLetters === -1) {
5434
+ top = bottom = leftNumbers - 1;
5482
5435
  left = right = 0;
5483
5436
  fullRow = true;
5484
5437
  }
5485
5438
  else {
5486
- const c = toCartesian(firstRangePart);
5487
- left = right = c.col;
5488
- top = bottom = c.row;
5439
+ left = right = leftLetters - 1;
5440
+ top = bottom = leftNumbers - 1;
5489
5441
  hasHeader = true;
5490
5442
  }
5491
- if (secondRangePart) {
5492
- if (isColReference(secondRangePart)) {
5493
- right = lettersToNumber(secondRangePart);
5443
+ consumeSpaces(chars);
5444
+ if (chars.current === ":") {
5445
+ chars.advanceBy(1);
5446
+ consumeSpaces(chars);
5447
+ const rightLetters = consumeLetters(chars);
5448
+ const rightNumbers = consumeDigits(chars);
5449
+ if (rightNumbers === -1) {
5450
+ right = rightLetters - 1;
5494
5451
  fullCol = true;
5495
5452
  }
5496
- else if (isRowReference(secondRangePart)) {
5497
- bottom = parseInt(secondRangePart, 10) - 1;
5453
+ else if (rightLetters === -1) {
5454
+ bottom = rightNumbers - 1;
5498
5455
  fullRow = true;
5499
5456
  }
5500
5457
  else {
5501
- const c = toCartesian(secondRangePart);
5502
- right = c.col;
5503
- bottom = c.row;
5458
+ right = rightLetters - 1;
5459
+ bottom = rightNumbers - 1;
5504
5460
  top = fullCol ? bottom : top;
5505
5461
  left = fullRow ? right : left;
5506
5462
  hasHeader = true;
5507
5463
  }
5508
5464
  }
5509
- if (fullCol && fullRow) {
5510
- throw new Error("Wrong zone xc. The zone cannot be at the same time a full column and a full row");
5511
- }
5512
5465
  const zone = {
5513
5466
  top,
5514
5467
  left,
@@ -5537,7 +5490,16 @@
5537
5490
  */
5538
5491
  function toUnboundedZone(xc) {
5539
5492
  const zone = toZoneWithoutBoundaryChanges(xc);
5540
- return reorderZone(zone);
5493
+ const orderedZone = reorderZone(zone);
5494
+ const bottom = orderedZone.bottom;
5495
+ const right = orderedZone.right;
5496
+ if ((bottom !== undefined && bottom > MAX_ROW) || (right !== undefined && right > MAX_COL)) {
5497
+ throw new Error(`Range string out of bounds: ${xc}`); // limit the size of the zone for performance
5498
+ }
5499
+ if (bottom === undefined && right === undefined) {
5500
+ throw new Error("Wrong zone xc. The zone cannot be at the same time a full column and a full row");
5501
+ }
5502
+ return orderedZone;
5541
5503
  }
5542
5504
  /**
5543
5505
  * Convert from a cartesian reference to a Zone.
@@ -6151,6 +6113,67 @@
6151
6113
  return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1));
6152
6114
  }
6153
6115
 
6116
+ /** Reference of a cell (eg. A1, $B$5) */
6117
+ const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
6118
+ // Same as above, but matches the exact string (nothing before or after)
6119
+ const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
6120
+ /** Reference of a column header (eg. A, AB, $A) */
6121
+ const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
6122
+ /** Reference of a row header (eg. 1, $1) */
6123
+ const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
6124
+ /** Reference of a column (eg. A, $CA, Sheet1!B) */
6125
+ const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
6126
+ /** Reference of a row (eg. 1, 59, Sheet1!9) */
6127
+ const rowReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([0-9]{1,7})$/, "i");
6128
+ /** Reference of a normal range or a full row range (eg. A1:B1, 1:$5, $A2:5) */
6129
+ const fullRowXc = /(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*:\s*(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*/i;
6130
+ /** Reference of a normal range or a column row range (eg. A1:B1, A:$B, $A1:C) */
6131
+ const fullColXc = /\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*:\s*\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*/i;
6132
+ /** Reference of a cell or a range, it can be a bounded range, a full row or a full column */
6133
+ const rangeReference = new RegExp(/^\s*('.+'!|[^']+!)?/.source +
6134
+ "(" +
6135
+ [cellReference.source, fullRowXc.source, fullColXc.source].join("|") +
6136
+ ")" +
6137
+ /$/.source, "i");
6138
+ /**
6139
+ * Return true if the given xc is the reference of a column (e.g. A or AC or Sheet1!A)
6140
+ */
6141
+ function isColReference(xc) {
6142
+ return colReference.test(xc);
6143
+ }
6144
+ /**
6145
+ * Return true if the given xc is the reference of a column (e.g. 1 or Sheet1!1)
6146
+ */
6147
+ function isRowReference(xc) {
6148
+ return rowReference.test(xc);
6149
+ }
6150
+ function isColHeader(str) {
6151
+ return colHeader.test(str);
6152
+ }
6153
+ function isRowHeader(str) {
6154
+ return rowHeader.test(str);
6155
+ }
6156
+ /**
6157
+ * Return true if the given xc is the reference of a single cell,
6158
+ * without any specified sheet (e.g. A1)
6159
+ */
6160
+ function isSingleCellReference(xc) {
6161
+ return singleCellReference.test(xc);
6162
+ }
6163
+ function splitReference(ref) {
6164
+ if (!ref.includes("!")) {
6165
+ return { xc: ref };
6166
+ }
6167
+ const parts = ref.split("!");
6168
+ const xc = parts.pop();
6169
+ const sheetName = getUnquotedSheetName(parts.join("!")) || undefined;
6170
+ return { sheetName, xc };
6171
+ }
6172
+ /** Return a reference SheetName!xc from the given arguments */
6173
+ function getFullReference(sheetName, xc) {
6174
+ return sheetName !== undefined ? `${getCanonicalSymbolName(sheetName)}!${xc}` : xc;
6175
+ }
6176
+
6154
6177
  function createDefaultRows(rowNumber) {
6155
6178
  const rows = [];
6156
6179
  for (let i = 0; i < rowNumber; i++) {
@@ -6847,9 +6870,6 @@
6847
6870
  }
6848
6871
  return fontSize;
6849
6872
  }
6850
- function computeIconWidth(style) {
6851
- return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN;
6852
- }
6853
6873
  /** Transform a string to lower case. If the string is undefined, return an empty string */
6854
6874
  function toLowerCase(str) {
6855
6875
  return str ? str.toLowerCase() : "";
@@ -8083,208 +8103,6 @@
8083
8103
  return values;
8084
8104
  }
8085
8105
 
8086
- const PREVIOUS_VALUE = "(previous)";
8087
- const NEXT_VALUE = "(next)";
8088
- function getDomainOfParentRow(pivot, domain) {
8089
- const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
8090
- return [...colDomain, ...rowDomain.slice(0, rowDomain.length - 1)];
8091
- }
8092
- function getDomainOfParentCol(pivot, domain) {
8093
- const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
8094
- return [...colDomain.slice(0, colDomain.length - 1), ...rowDomain];
8095
- }
8096
- /**
8097
- * Split a pivot domain into the part related to the rows of the pivot, and the part related to the columns.
8098
- */
8099
- function domainToColRowDomain(pivot, domain) {
8100
- const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
8101
- const rowDomain = domain.filter((node) => rowFields.includes(node.field));
8102
- const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
8103
- const colDomain = domain.filter((node) => columnFields.includes(node.field));
8104
- return { colDomain, rowDomain };
8105
- }
8106
- function getDimensionDomain(pivot, dimension, domain) {
8107
- return dimension === "column"
8108
- ? domainToColRowDomain(pivot, domain).colDomain
8109
- : domainToColRowDomain(pivot, domain).rowDomain;
8110
- }
8111
- function getFieldValueInDomain(fieldNameWithGranularity, domain) {
8112
- const node = domain.find((n) => n.field === fieldNameWithGranularity);
8113
- return node?.value;
8114
- }
8115
- function isDomainIsInPivot(pivot, domain) {
8116
- const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
8117
- return (checkIfDomainInInTree(rowDomain, pivot.getTableStructure().getRowTree()) &&
8118
- checkIfDomainInInTree(colDomain, pivot.getTableStructure().getColTree()));
8119
- }
8120
- function checkIfDomainInInTree(domain, tree) {
8121
- return walkDomainTree(domain, tree) !== undefined;
8122
- }
8123
- /**
8124
- * Given a tree of the col/rows of a pivot, and a domain related to those col/rows, return the node of the tree
8125
- * corresponding to the domain.
8126
- *
8127
- * @param domain The domain to find in the tree
8128
- * @param tree The tree to search in7
8129
- * @param stopAtField If provided, the search will stop at the field with this name
8130
- */
8131
- function walkDomainTree(domain, tree, stopAtField) {
8132
- let currentTreeNode = tree;
8133
- for (const node of domain) {
8134
- const child = currentTreeNode.find((n) => n.value === node.value);
8135
- if (!child) {
8136
- return undefined;
8137
- }
8138
- if (child.field === stopAtField) {
8139
- return currentTreeNode;
8140
- }
8141
- currentTreeNode = child.children;
8142
- }
8143
- return currentTreeNode;
8144
- }
8145
- /**
8146
- * Get the domain parent of the given domain with the field `parentFieldName` as leaf of the domain.
8147
- *
8148
- * In practice, if the `parentFieldName` is a row in the pivot, the helper will return a domain with the same column
8149
- * domain, and with a row domain all groupBys children to `parentFieldName` removed.
8150
- */
8151
- function getFieldParentDomain(pivot, parentFieldName, domain) {
8152
- let { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
8153
- const dimension = getFieldDimensionType(pivot, parentFieldName);
8154
- if (dimension === "row") {
8155
- const index = rowDomain.findIndex((node) => node.field === parentFieldName);
8156
- if (index === -1) {
8157
- return domain;
8158
- }
8159
- rowDomain = rowDomain.slice(0, index + 1);
8160
- }
8161
- else {
8162
- const index = colDomain.findIndex((node) => node.field === parentFieldName);
8163
- if (index === -1) {
8164
- return domain;
8165
- }
8166
- colDomain = colDomain.slice(0, index + 1);
8167
- }
8168
- return [...rowDomain, ...colDomain];
8169
- }
8170
- /**
8171
- * Replace in the domain the value of the field `fieldNameWithGranularity` with the given `value`
8172
- */
8173
- function replaceFieldValueInDomain(domain, fieldNameWithGranularity, value) {
8174
- domain = deepCopy(domain);
8175
- const node = domain.find((n) => n.field === fieldNameWithGranularity);
8176
- if (!node) {
8177
- return domain;
8178
- }
8179
- node.value = value;
8180
- return domain;
8181
- }
8182
- function isFieldInDomain(nameWithGranularity, domain) {
8183
- return domain.some((node) => node.field === nameWithGranularity);
8184
- }
8185
- /**
8186
- * Check if the field is in the rows or columns of the pivot
8187
- */
8188
- function getFieldDimensionType(pivot, nameWithGranularity) {
8189
- const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
8190
- if (rowFields.includes(nameWithGranularity)) {
8191
- return "row";
8192
- }
8193
- const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
8194
- if (columnFields.includes(nameWithGranularity)) {
8195
- return "column";
8196
- }
8197
- throw new Error(`Field ${nameWithGranularity} not found in pivot`);
8198
- }
8199
- /**
8200
- * Replace in the given domain the value of the field `fieldNameWithGranularity` with the previous or next value.
8201
- */
8202
- function getPreviousOrNextValueDomain(pivot, domain, fieldNameWithGranularity, direction) {
8203
- const dimension = getFieldDimensionType(pivot, fieldNameWithGranularity);
8204
- const tree = dimension === "row"
8205
- ? pivot.getTableStructure().getRowTree()
8206
- : pivot.getTableStructure().getColTree();
8207
- const dimDomain = getDimensionDomain(pivot, dimension, domain);
8208
- const currentTreeNode = walkDomainTree(dimDomain, tree, fieldNameWithGranularity);
8209
- const values = currentTreeNode?.map((n) => n.value) ?? [];
8210
- const value = getFieldValueInDomain(fieldNameWithGranularity, domain);
8211
- if (value === undefined) {
8212
- return undefined;
8213
- }
8214
- const valueIndex = values.indexOf(value);
8215
- if (value === undefined || valueIndex === -1) {
8216
- return undefined;
8217
- }
8218
- const offset = direction === PREVIOUS_VALUE ? -1 : 1;
8219
- const newIndex = clip(valueIndex + offset, 0, values.length - 1);
8220
- return replaceFieldValueInDomain(domain, fieldNameWithGranularity, values[newIndex]);
8221
- }
8222
- function domainToString(domain) {
8223
- return domain ? domain.map(domainNodeToString).join(", ") : "";
8224
- }
8225
- function domainNodeToString(domainNode) {
8226
- return domainNode ? `${domainNode.field}=${domainNode.value}` : "";
8227
- }
8228
- /**
8229
- *
8230
- * For the ranking, the pivot cell values of a column (or row) at the same depth are grouped together before being sorted
8231
- * and ranked.
8232
- *
8233
- * The grouping of a pivot cell is done with both the value of the domain nodes that are parent of the field
8234
- * `fieldNameWithGranularity` and the value of the last node of the domain of the pivot cell, if it's not the field
8235
- * `fieldNameWithGranularity`.
8236
- *
8237
- * For example, let's take a pivot grouped by (Date:year, Stage, User, Product), where we want to rank by "Stage" field.
8238
- * The domain nodes parents of the "Stage" are [Date:year]. The pivot cell with domain:
8239
- * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
8240
- * - [Date:year=2021, Stage=Lead] is grouped with the cells [Date:year=2021, Stage=*, User=None, Product=None],
8241
- * and then ranked within the group
8242
- * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=*, User=Bob, Product=None],
8243
- * and then ranked within the group
8244
- * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells [Date:year=2021, Stage=*, User=*, Product=Table],
8245
- * and then ranked within the group
8246
- *
8247
- * If we rank the pivot on "User" instead, the parent domain becomes [Date:year, Sage] .The cell with domain:
8248
- * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
8249
- * - [Date:year=2021, Stage=Lead] is not ranked because it does not contain the "User" field
8250
- * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=Lead, User=Bob, Product=None],
8251
- * and then ranked within the group
8252
- * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells with [Date:year=2021, Stage=Lead, User=*, Product=Table],
8253
- * and then ranked within the group
8254
- *
8255
- */
8256
- function getRankingDomainKey(domain, fieldNameWithGranularity) {
8257
- const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
8258
- if (index === -1) {
8259
- return "";
8260
- }
8261
- const parent = domain.slice(0, index);
8262
- const lastNode = domain.at(-1);
8263
- return domainToString(lastNode.field === fieldNameWithGranularity ? parent : [...parent, lastNode]);
8264
- }
8265
- /**
8266
- * The running total domain is the domain without the field `fieldNameWithGranularity`, ie. we do the running total of
8267
- * all the pivot cells of the column that have any value for the field `fieldNameWithGranularity` and the same value for
8268
- * the other fields.
8269
- */
8270
- function getRunningTotalDomainKey(domain, fieldNameWithGranularity) {
8271
- const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
8272
- if (index === -1) {
8273
- return "";
8274
- }
8275
- return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
8276
- }
8277
- function sortPivotTree(tree, baseDomain, sortFn) {
8278
- const sortedTree = [...tree];
8279
- const domain = [...baseDomain];
8280
- sortedTree.sort((node1, node2) => sortFn([...domain, node1], [...domain, node2]));
8281
- for (const node of tree) {
8282
- const children = sortPivotTree(node.children, [...domain, node], sortFn);
8283
- node.children = children;
8284
- }
8285
- return sortedTree;
8286
- }
8287
-
8288
8106
  const pivotTimeAdapterRegistry = new Registry();
8289
8107
  function pivotTimeAdapter(granularity) {
8290
8108
  return pivotTimeAdapterRegistry.get(granularity);
@@ -8795,23 +8613,11 @@
8795
8613
  function getFieldDisplayName(field) {
8796
8614
  return field.displayName + (field.granularity ? ` (${ALL_PERIODS[field.granularity]})` : "");
8797
8615
  }
8798
- function addIndentAndAlignToPivotHeader(pivot, domain, functionResult) {
8799
- const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
8800
- if (rowDomain.length === 0 && colDomain.length === 0) {
8616
+ function addAlignFormatToPivotHeader(domain, functionResult) {
8617
+ if (domain.length === 0) {
8801
8618
  return functionResult;
8802
8619
  }
8803
- if (rowDomain.length === 0 && colDomain.length > 0) {
8804
- return {
8805
- ...functionResult,
8806
- format: (functionResult.format || "@") + "* ",
8807
- };
8808
- }
8809
- const indent = rowDomain.length - 1;
8810
- const format = functionResult.format || "@";
8811
- return {
8812
- ...functionResult,
8813
- format: `${" ".repeat(indent)}${format}* `,
8814
- };
8620
+ return { ...functionResult, format: (functionResult.format || "@") + "* " };
8815
8621
  }
8816
8622
  function isSortedColumnValid(sortedColumn, pivot) {
8817
8623
  try {
@@ -9842,6 +9648,13 @@
9842
9648
  resetStores() {
9843
9649
  this.dependencies.clear();
9844
9650
  }
9651
+ dispose() {
9652
+ for (const instance of this.dependencies.values()) {
9653
+ if ("dispose" in instance && typeof instance.dispose === "function") {
9654
+ instance.dispose();
9655
+ }
9656
+ }
9657
+ }
9845
9658
  }
9846
9659
  class StoreFactory {
9847
9660
  get;
@@ -9920,6 +9733,7 @@ stores.inject(MyMetaStore, storeInstance);
9920
9733
  return proxifyStoreMutation(store, () => container.trigger("store-updated"));
9921
9734
  },
9922
9735
  });
9736
+ owl.onWillUnmount(() => container.dispose());
9923
9737
  return container;
9924
9738
  }
9925
9739
  /**
@@ -10273,6 +10087,19 @@ stores.inject(MyMetaStore, storeInstance);
10273
10087
  }
10274
10088
  }
10275
10089
 
10090
+ class ChartAnimationStore extends SpreadsheetStore {
10091
+ mutators = ["disableAnimationForChart", "enableAnimationForChart"];
10092
+ animationPlayed = {};
10093
+ disableAnimationForChart(chartId, chartType) {
10094
+ this.animationPlayed[chartId] = chartType;
10095
+ return "noStateChange";
10096
+ }
10097
+ enableAnimationForChart(chartId) {
10098
+ this.animationPlayed[chartId] = undefined;
10099
+ return "noStateChange";
10100
+ }
10101
+ }
10102
+
10276
10103
  function getFunnelChartController() {
10277
10104
  return class FunnelChartController extends window.Chart.BarController {
10278
10105
  static id = "funnel";
@@ -11926,10 +11753,12 @@ stores.inject(MyMetaStore, storeInstance);
11926
11753
  static template = "o-spreadsheet-ChartJsComponent";
11927
11754
  static props = {
11928
11755
  figureUI: Object,
11756
+ isFullScreen: { type: Boolean, optional: true },
11929
11757
  };
11930
11758
  canvas = owl.useRef("graphContainer");
11931
11759
  chart;
11932
11760
  currentRuntime;
11761
+ animationStore;
11933
11762
  currentDevicePixelRatio = window.devicePixelRatio;
11934
11763
  get background() {
11935
11764
  return this.chartRuntime.background;
@@ -11945,6 +11774,9 @@ stores.inject(MyMetaStore, storeInstance);
11945
11774
  return runtime;
11946
11775
  }
11947
11776
  setup() {
11777
+ if (this.env.model.getters.isDashboard()) {
11778
+ this.animationStore = useStore(ChartAnimationStore);
11779
+ }
11948
11780
  owl.onMounted(() => {
11949
11781
  const runtime = this.chartRuntime;
11950
11782
  this.currentRuntime = runtime;
@@ -11971,11 +11803,25 @@ stores.inject(MyMetaStore, storeInstance);
11971
11803
  });
11972
11804
  }
11973
11805
  createChart(chartData) {
11806
+ if (this.env.model.getters.isDashboard() && this.animationStore) {
11807
+ const chartType = this.env.model.getters.getChart(this.props.figureUI.id)?.type;
11808
+ if (chartType && this.animationStore.animationPlayed[this.animationFigureId] !== chartType) {
11809
+ chartData = this.enableAnimationInChartData(chartData);
11810
+ this.animationStore.disableAnimationForChart(this.animationFigureId, chartType);
11811
+ }
11812
+ }
11974
11813
  const canvas = this.canvas.el;
11975
11814
  const ctx = canvas.getContext("2d");
11976
11815
  this.chart = new window.Chart(ctx, chartData);
11977
11816
  }
11978
11817
  updateChartJs(chartData) {
11818
+ if (this.env.model.getters.isDashboard()) {
11819
+ const chartType = this.env.model.getters.getChart(this.props.figureUI.id)?.type;
11820
+ if (chartType && this.hasChartDataChanged() && this.animationStore) {
11821
+ chartData = this.enableAnimationInChartData(chartData);
11822
+ this.animationStore.disableAnimationForChart(this.animationFigureId, chartType);
11823
+ }
11824
+ }
11979
11825
  if (chartData.data && chartData.data.datasets) {
11980
11826
  this.chart.data = chartData.data;
11981
11827
  if (chartData.options?.plugins?.title) {
@@ -11988,6 +11834,20 @@ stores.inject(MyMetaStore, storeInstance);
11988
11834
  this.chart.config.options = chartData.options;
11989
11835
  this.chart.update();
11990
11836
  }
11837
+ hasChartDataChanged() {
11838
+ return !deepEquals(this.currentRuntime.chartJsConfig.data, this.chartRuntime.chartJsConfig.data);
11839
+ }
11840
+ enableAnimationInChartData(chartData) {
11841
+ return {
11842
+ ...chartData,
11843
+ options: { ...chartData.options, animation: { animateRotate: true } },
11844
+ };
11845
+ }
11846
+ get animationFigureId() {
11847
+ return this.props.isFullScreen
11848
+ ? this.props.figureUI.id + "-fullscreen"
11849
+ : this.props.figureUI.id;
11850
+ }
11991
11851
  }
11992
11852
 
11993
11853
  /**
@@ -12113,6 +11973,7 @@ stores.inject(MyMetaStore, storeInstance);
12113
11973
  const arrowUpPath = new window.Path2D("M8.7 5.5a.5.5 0 0 0 0-.75l-3.8-4a.5.5 0 0 0-.75 0l-3.8 4a.5.5 0 0 0 0 .75l.4.4a.5.5 0 0 0 .75 0l2.3-2.4v5.8c0 .25.25.5.5.5h.6c.25 0 .5-.25.5-.5v-5.8l2.2 2.4a.5.5 0 0 0 .75 0z");
12114
11974
  let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
12115
11975
  keyValue;
11976
+ keyDescr;
12116
11977
  baseline;
12117
11978
  baselineMode;
12118
11979
  baselineDescr;
@@ -12126,6 +11987,7 @@ stores.inject(MyMetaStore, storeInstance);
12126
11987
  constructor(definition, sheetId, getters) {
12127
11988
  super(definition, sheetId, getters);
12128
11989
  this.keyValue = createValidRange(getters, sheetId, definition.keyValue);
11990
+ this.keyDescr = definition.keyDescr;
12129
11991
  this.baseline = createValidRange(getters, sheetId, definition.baseline);
12130
11992
  this.baselineMode = definition.baselineMode;
12131
11993
  this.baselineDescr = definition.baselineDescr;
@@ -12203,6 +12065,7 @@ stores.inject(MyMetaStore, storeInstance);
12203
12065
  keyValue: keyValue
12204
12066
  ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)
12205
12067
  : undefined,
12068
+ keyDescr: this.keyDescr,
12206
12069
  humanize: this.humanize,
12207
12070
  };
12208
12071
  }
@@ -12226,7 +12089,7 @@ stores.inject(MyMetaStore, storeInstance);
12226
12089
  canvas.width = dpr * structure.canvas.width;
12227
12090
  canvas.height = dpr * structure.canvas.height;
12228
12091
  ctx.scale(dpr, dpr);
12229
- const availableWidth = structure.canvas.width - CHART_PADDING$1 * 2;
12092
+ const availableWidth = structure.canvas.width - CHART_PADDING;
12230
12093
  ctx.fillStyle = structure.canvas.backgroundColor;
12231
12094
  ctx.fillRect(0, 0, structure.canvas.width, structure.canvas.height);
12232
12095
  if (structure.title) {
@@ -12262,18 +12125,22 @@ stores.inject(MyMetaStore, storeInstance);
12262
12125
  ctx.restore();
12263
12126
  }
12264
12127
  if (structure.baselineDescr) {
12265
- const descr = structure.baselineDescr[0];
12128
+ const descr = structure.baselineDescr;
12266
12129
  ctx.font = descr.style.font;
12267
12130
  ctx.fillStyle = descr.style.color;
12268
- for (const description of structure.baselineDescr) {
12269
- ctx.fillText(clipTextWithEllipsis(ctx, description.text, availableWidth - description.position.x), description.position.x, description.position.y);
12270
- }
12131
+ ctx.fillText(clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position.x, descr.position.y);
12271
12132
  }
12272
12133
  if (structure.key) {
12273
12134
  ctx.font = structure.key.style.font;
12274
12135
  ctx.fillStyle = structure.key.style.color;
12275
12136
  drawDecoratedText(ctx, clipTextWithEllipsis(ctx, structure.key.text, availableWidth - structure.key.position.x), structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
12276
12137
  }
12138
+ if (structure.keyDescr) {
12139
+ const descr = structure.keyDescr;
12140
+ ctx.font = structure.keyDescr?.style.font ?? descr.style.font;
12141
+ ctx.fillStyle = descr.style.color;
12142
+ ctx.fillText(clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position.x, descr.position.y);
12143
+ }
12277
12144
  if (structure.progressBar) {
12278
12145
  ctx.fillStyle = structure.progressBar.style.backgroundColor;
12279
12146
  ctx.beginPath();
@@ -12328,28 +12195,41 @@ stores.inject(MyMetaStore, storeInstance);
12328
12195
  text: chart.title.text ? _t(chart.title.text) : "",
12329
12196
  },
12330
12197
  keyValue: formattedKeyValue,
12198
+ keyDescr: chart.keyDescr?.text
12199
+ ? _t(chart.keyDescr.text) // descriptions are extracted from .json files and they are translated at runtime here
12200
+ : "",
12331
12201
  baselineDisplay,
12332
12202
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
12333
12203
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
12334
- baselineDescr: chart.baselineMode !== "progress" && chart.baselineDescr
12335
- ? _t(chart.baselineDescr) // descriptions are extracted from .json files and they are translated at runtime here
12204
+ baselineDescr: chart.baselineMode !== "progress" && chart.baselineDescr?.text
12205
+ ? _t(chart.baselineDescr.text) // descriptions are extracted from .json files and they are translated at runtime here
12336
12206
  : "",
12337
12207
  fontColor,
12338
12208
  background,
12339
- baselineStyle: chart.baselineMode !== "percentage" && chart.baselineMode !== "progress" && baseline
12340
- ? getters.getCellStyle({
12341
- sheetId: baseline.sheetId,
12342
- col: baseline.zone.left,
12343
- row: baseline.zone.top,
12344
- })
12345
- : undefined,
12346
- keyValueStyle: chart.keyValue
12347
- ? getters.getCellStyle({
12348
- sheetId: chart.keyValue.sheetId,
12349
- col: chart.keyValue.zone.left,
12350
- row: chart.keyValue.zone.top,
12351
- })
12352
- : undefined,
12209
+ baselineStyle: {
12210
+ ...(chart.baselineMode !== "percentage" && chart.baselineMode !== "progress" && baseline
12211
+ ? getters.getCellComputedStyle({
12212
+ sheetId: baseline.sheetId,
12213
+ col: baseline.zone.left,
12214
+ row: baseline.zone.top,
12215
+ })
12216
+ : undefined),
12217
+ fontSize: chart.baselineDescr?.fontSize,
12218
+ align: chart.baselineDescr?.align,
12219
+ },
12220
+ baselineDescrStyle: { textColor: chart.baselineDescr?.color, ...chart.baselineDescr },
12221
+ keyValueStyle: {
12222
+ ...(chart.keyValue
12223
+ ? getters.getCellComputedStyle({
12224
+ sheetId: chart.keyValue.sheetId,
12225
+ col: chart.keyValue.zone.left,
12226
+ row: chart.keyValue.zone.top,
12227
+ })
12228
+ : undefined),
12229
+ fontSize: chart.keyDescr?.fontSize,
12230
+ align: chart.keyDescr?.align,
12231
+ },
12232
+ keyValueDescrStyle: { textColor: chart.keyDescr?.color, ...chart.keyDescr },
12353
12233
  progressBar: chart.baselineMode === "progress"
12354
12234
  ? {
12355
12235
  value: baselineValue,
@@ -12360,11 +12240,7 @@ stores.inject(MyMetaStore, storeInstance);
12360
12240
  }
12361
12241
 
12362
12242
  /* Padding at the border of the chart */
12363
- const CHART_PADDING = 10;
12364
12243
  const BOTTOM_PADDING_RATIO = 0.05;
12365
- /* Maximum font sizes of each element */
12366
- const KEY_VALUE_FONT_SIZE = 32;
12367
- const BASELINE_MAX_FONT_SIZE = 16;
12368
12244
  function formatBaselineDescr(baselineDescr, baseline) {
12369
12245
  const _baselineDescr = baselineDescr || "";
12370
12246
  return baseline && _baselineDescr ? " " + _baselineDescr : _baselineDescr;
@@ -12414,7 +12290,7 @@ stores.inject(MyMetaStore, storeInstance);
12414
12290
  style: style.title,
12415
12291
  position: {
12416
12292
  x,
12417
- y: CHART_PADDING + titleHeight / 2,
12293
+ y: CHART_PADDING_BOTTOM + titleHeight / 2,
12418
12294
  },
12419
12295
  };
12420
12296
  }
@@ -12424,42 +12300,49 @@ stores.inject(MyMetaStore, storeInstance);
12424
12300
  baselineHeight = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).height;
12425
12301
  }
12426
12302
  const baselineDescrWidth = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).width;
12427
- structure.baseline = {
12428
- text: this.baseline,
12429
- style: style.baselineValue,
12430
- position: {
12431
- x: (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2,
12432
- y: this.keyValue
12433
- ? this.height * (1 - BOTTOM_PADDING_RATIO * (this.runtime.progressBar ? 1 : 2))
12434
- : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING,
12435
- },
12436
- };
12437
- const minimalBaselinePosition = baselineArrowSize + CHART_PADDING * 2;
12438
- if (structure.baseline.position.x < minimalBaselinePosition) {
12439
- structure.baseline.position.x = minimalBaselinePosition;
12440
- }
12441
- if (style.baselineArrow && !this.runtime.progressBar) {
12442
- structure.baselineArrow = {
12443
- direction: this.baselineArrow,
12444
- style: style.baselineArrow,
12303
+ let baselineX;
12304
+ switch (this.runtime.baselineStyle?.align) {
12305
+ case "right":
12306
+ baselineX = this.width - CHART_PADDING - baselineDescrWidth - baselineWidth;
12307
+ break;
12308
+ case "left":
12309
+ baselineX = CHART_PADDING + baselineArrowSize;
12310
+ break;
12311
+ default:
12312
+ baselineX = (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2;
12313
+ }
12314
+ if (this.baseline) {
12315
+ structure.baseline = {
12316
+ text: this.baseline,
12317
+ style: style.baselineValue,
12445
12318
  position: {
12446
- x: structure.baseline.position.x - baselineArrowSize,
12447
- y: structure.baseline.position.y - (baselineHeight + baselineArrowSize) / 2,
12319
+ x: baselineX,
12320
+ y: this.keyValue
12321
+ ? this.height * (1 - BOTTOM_PADDING_RATIO * (this.runtime.progressBar ? 1 : 2))
12322
+ : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING_BOTTOM,
12448
12323
  },
12449
12324
  };
12325
+ if (style.baselineArrow && !this.runtime.progressBar) {
12326
+ structure.baselineArrow = {
12327
+ direction: this.baselineArrow,
12328
+ style: style.baselineArrow,
12329
+ position: {
12330
+ x: structure.baseline.position.x - baselineArrowSize,
12331
+ y: structure.baseline.position.y - (baselineHeight + baselineArrowSize) / 2,
12332
+ },
12333
+ };
12334
+ }
12450
12335
  }
12451
- if (this.baselineDescr) {
12336
+ if (structure.baseline && this.baselineDescr) {
12452
12337
  const position = {
12453
12338
  x: structure.baseline.position.x + baselineWidth,
12454
12339
  y: structure.baseline.position.y,
12455
12340
  };
12456
- structure.baselineDescr = [
12457
- {
12458
- text: this.baselineDescr,
12459
- style: style.baselineDescr,
12460
- position,
12461
- },
12462
- ];
12341
+ structure.baselineDescr = {
12342
+ text: this.baselineDescr,
12343
+ style: style.baselineDescr,
12344
+ position,
12345
+ };
12463
12346
  }
12464
12347
  let progressBarHeight = 0;
12465
12348
  if (this.runtime.progressBar) {
@@ -12481,18 +12364,41 @@ stores.inject(MyMetaStore, storeInstance);
12481
12364
  };
12482
12365
  }
12483
12366
  const { width: keyWidth, height: keyHeight } = this.getFullTextDimensions(this.keyValue, style.keyValue.font);
12367
+ const keyDescrWidth = this.getTextDimensions(this.keyDescr, style.keyDescr.font).width;
12368
+ let keyX;
12369
+ switch (this.runtime.keyValueStyle?.align) {
12370
+ case "right":
12371
+ keyX = this.width - CHART_PADDING - keyDescrWidth - keyWidth;
12372
+ break;
12373
+ case "left":
12374
+ keyX = CHART_PADDING;
12375
+ break;
12376
+ default:
12377
+ keyX = (this.width - keyWidth - keyDescrWidth) / 2;
12378
+ }
12484
12379
  if (this.keyValue) {
12485
12380
  structure.key = {
12486
12381
  text: this.keyValue,
12487
12382
  style: style.keyValue,
12488
12383
  position: {
12489
- x: Math.max(CHART_PADDING, (this.width - keyWidth) / 2),
12384
+ x: Math.max(CHART_PADDING, keyX),
12490
12385
  y: this.height * (0.5 - BOTTOM_PADDING_RATIO * 2) +
12491
- CHART_PADDING / 2 +
12386
+ CHART_PADDING_BOTTOM / 2 +
12492
12387
  (titleHeight + keyHeight / 2) / 2,
12493
12388
  },
12494
12389
  };
12495
12390
  }
12391
+ if (structure.key && this.keyDescr) {
12392
+ const position = {
12393
+ x: structure.key.position.x + keyWidth,
12394
+ y: structure.key.position.y,
12395
+ };
12396
+ structure.keyDescr = {
12397
+ text: this.keyDescr,
12398
+ style: style.keyDescr,
12399
+ position,
12400
+ };
12401
+ }
12496
12402
  return structure;
12497
12403
  }
12498
12404
  get title() {
@@ -12501,6 +12407,9 @@ stores.inject(MyMetaStore, storeInstance);
12501
12407
  get keyValue() {
12502
12408
  return this.runtime.keyValue;
12503
12409
  }
12410
+ get keyDescr() {
12411
+ return formatBaselineDescr(this.runtime.keyDescr, this.keyValue);
12412
+ }
12504
12413
  get baseline() {
12505
12414
  return this.runtime.baselineDisplay;
12506
12415
  }
@@ -12533,7 +12442,9 @@ stores.inject(MyMetaStore, storeInstance);
12533
12442
  };
12534
12443
  }
12535
12444
  getTextStyles() {
12536
- let baselineValueFontSize = BASELINE_MAX_FONT_SIZE;
12445
+ const keyValueFontSize = this.runtime.keyValueStyle?.fontSize ?? DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE;
12446
+ const keyValueDescrFontSize = Math.floor(0.9 * keyValueFontSize);
12447
+ let baselineValueFontSize = this.runtime.baselineStyle?.fontSize ?? DEFAULT_SCORECARD_BASELINE_FONT_SIZE;
12537
12448
  const baselineDescrFontSize = Math.floor(0.9 * baselineValueFontSize);
12538
12449
  if (this.runtime.progressBar) {
12539
12450
  baselineValueFontSize /= 1.5;
@@ -12545,27 +12456,37 @@ stores.inject(MyMetaStore, storeInstance);
12545
12456
  },
12546
12457
  keyValue: {
12547
12458
  color: this.runtime.keyValueStyle?.textColor || this.runtime.fontColor,
12548
- font: getDefaultContextFont(KEY_VALUE_FONT_SIZE, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),
12459
+ font: getDefaultContextFont(keyValueFontSize, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),
12549
12460
  strikethrough: this.runtime.keyValueStyle?.strikethrough,
12550
12461
  underline: this.runtime.keyValueStyle?.underline,
12551
12462
  },
12463
+ keyDescr: {
12464
+ color: this.runtime.keyValueDescrStyle?.textColor || this.runtime.fontColor,
12465
+ font: getDefaultContextFont(keyValueDescrFontSize, this.runtime.keyValueDescrStyle?.bold, this.runtime.keyValueDescrStyle?.italic),
12466
+ strikethrough: this.runtime.keyValueDescrStyle?.strikethrough,
12467
+ underline: this.runtime.keyValueDescrStyle?.underline,
12468
+ },
12552
12469
  baselineValue: {
12553
12470
  font: getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
12554
12471
  strikethrough: this.runtime.baselineStyle?.strikethrough,
12555
12472
  underline: this.runtime.baselineStyle?.underline,
12556
- color: this.runtime.baselineStyle?.textColor ||
12557
- this.runtime.baselineColor ||
12473
+ color: this.runtime.baselineColor ||
12474
+ this.runtime.baselineStyle?.textColor ||
12558
12475
  this.secondaryFontColor,
12559
12476
  },
12560
12477
  baselineDescr: {
12561
- font: getDefaultContextFont(baselineDescrFontSize),
12562
- color: this.secondaryFontColor,
12478
+ font: getDefaultContextFont(baselineDescrFontSize, this.runtime.baselineDescrStyle?.bold, this.runtime.baselineDescrStyle?.italic),
12479
+ strikethrough: this.runtime.baselineDescrStyle?.strikethrough,
12480
+ underline: this.runtime.baselineDescrStyle?.underline,
12481
+ color: this.runtime.baselineDescrStyle?.textColor ?? this.secondaryFontColor,
12563
12482
  },
12564
12483
  baselineArrow: this.baselineArrow === "neutral" || this.runtime.progressBar
12565
12484
  ? undefined
12566
12485
  : {
12567
12486
  size: this.keyValue ? 0.8 * baselineValueFontSize : 0,
12568
- color: this.runtime.baselineColor || this.secondaryFontColor,
12487
+ color: this.runtime.baselineColor ||
12488
+ this.runtime.baselineStyle?.textColor ||
12489
+ this.secondaryFontColor,
12569
12490
  },
12570
12491
  };
12571
12492
  }
@@ -12959,14 +12880,14 @@ stores.inject(MyMetaStore, storeInstance);
12959
12880
  }
12960
12881
  switch (runtime.title.align) {
12961
12882
  case "right":
12962
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
12883
+ x = boundingRect.width - titleWidth - CHART_PADDING;
12963
12884
  break;
12964
12885
  case "center":
12965
12886
  x = (boundingRect.width - titleWidth) / 2;
12966
12887
  break;
12967
12888
  case "left":
12968
12889
  default:
12969
- x = CHART_PADDING$1;
12890
+ x = CHART_PADDING;
12970
12891
  break;
12971
12892
  }
12972
12893
  return {
@@ -14288,8 +14209,8 @@ stores.inject(MyMetaStore, storeInstance);
14288
14209
  function getChartLayout(definition, args) {
14289
14210
  return {
14290
14211
  padding: {
14291
- left: CHART_PADDING$1,
14292
- right: CHART_PADDING$1,
14212
+ left: CHART_PADDING,
14213
+ right: CHART_PADDING,
14293
14214
  top: Math.max(CHART_PADDING_TOP, args.topPadding || 0),
14294
14215
  bottom: CHART_PADDING_BOTTOM,
14295
14216
  },
@@ -14840,11 +14761,11 @@ stores.inject(MyMetaStore, storeInstance);
14840
14761
  case "right":
14841
14762
  const hasTitle = !!definition.title.text;
14842
14763
  const topMargin = hasTitle ? CHART_PADDING_TOP + 30 : CHART_PADDING_TOP;
14843
- return { top: topMargin, left: CHART_PADDING$1, right: CHART_PADDING$1 };
14764
+ return { top: topMargin, left: CHART_PADDING, right: CHART_PADDING };
14844
14765
  case "bottom":
14845
14766
  case "left":
14846
14767
  case "none":
14847
- return { left: CHART_PADDING$1, right: CHART_PADDING$1, bottom: CHART_PADDING_BOTTOM };
14768
+ return { left: CHART_PADDING, right: CHART_PADDING, bottom: CHART_PADDING_BOTTOM };
14848
14769
  }
14849
14770
  }
14850
14771
  function legendPositionToGeoLegendPosition(position) {
@@ -14912,7 +14833,7 @@ stores.inject(MyMetaStore, storeInstance);
14912
14833
  padding: {
14913
14834
  // Disable title top/left/right padding to use the chart padding instead.
14914
14835
  // The legend already has a top padding, so bottom padding is useless for the title there.
14915
- bottom: definition.legendPosition === "top" ? 0 : CHART_PADDING$1,
14836
+ bottom: definition.legendPosition === "top" ? 0 : CHART_PADDING,
14916
14837
  },
14917
14838
  };
14918
14839
  }
@@ -15475,6 +15396,10 @@ stores.inject(MyMetaStore, storeInstance);
15475
15396
 
15476
15397
  class GaugeChartComponent extends owl.Component {
15477
15398
  static template = "o-spreadsheet-GaugeChartComponent";
15399
+ static props = {
15400
+ figureUI: Object,
15401
+ isFullScreen: { type: Boolean, optional: true },
15402
+ };
15478
15403
  canvas = owl.useRef("chartContainer");
15479
15404
  get runtime() {
15480
15405
  return this.env.model.getters.getChartRuntime(this.props.figureUI.id);
@@ -15487,9 +15412,6 @@ stores.inject(MyMetaStore, storeInstance);
15487
15412
  });
15488
15413
  }
15489
15414
  }
15490
- GaugeChartComponent.props = {
15491
- figureUI: Object,
15492
- };
15493
15415
 
15494
15416
  class ComboChart extends AbstractChart {
15495
15417
  dataSets;
@@ -18293,6 +18215,26 @@ stores.inject(MyMetaStore, storeInstance);
18293
18215
  };
18294
18216
  }
18295
18217
 
18218
+ class FullScreenChartStore extends SpreadsheetStore {
18219
+ mutators = ["toggleFullScreenChart"];
18220
+ fullScreenFigure = undefined;
18221
+ toggleFullScreenChart(figureId) {
18222
+ if (this.fullScreenFigure?.id === figureId) {
18223
+ this.fullScreenFigure = undefined;
18224
+ }
18225
+ else {
18226
+ this.makeFullScreen(figureId);
18227
+ }
18228
+ }
18229
+ makeFullScreen(figureId) {
18230
+ const sheetId = this.getters.getActiveSheetId();
18231
+ const figure = this.getters.getFigure(sheetId, figureId);
18232
+ if (figure) {
18233
+ this.fullScreenFigure = { ...figure, x: 0, y: 0, width: 0, height: 0 };
18234
+ }
18235
+ }
18236
+ }
18237
+
18296
18238
  /**
18297
18239
  * Repeatedly calls a callback function with a time delay between calls.
18298
18240
  */
@@ -18412,7 +18354,9 @@ stores.inject(MyMetaStore, storeInstance);
18412
18354
  const spreadsheetRect = useSpreadsheetRect();
18413
18355
  function updateRect() {
18414
18356
  const env = component.env;
18415
- const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
18357
+ const newRect = "getPopoverContainerRect" in env && env.getPopoverContainerRect
18358
+ ? env.getPopoverContainerRect()
18359
+ : spreadsheetRect;
18416
18360
  container.x = newRect.x;
18417
18361
  container.y = newRect.y;
18418
18362
  container.width = newRect.width;
@@ -18937,9 +18881,11 @@ stores.inject(MyMetaStore, storeInstance);
18937
18881
  static components = { Menu };
18938
18882
  static props = { figureUI: Object };
18939
18883
  originalChartDefinition;
18884
+ fullScreenFigureStore;
18940
18885
  menuState = owl.useState({ isOpen: false, anchorRect: null, menuItems: [] });
18941
18886
  setup() {
18942
18887
  super.setup();
18888
+ this.fullScreenFigureStore = useStore(FullScreenChartStore);
18943
18889
  this.originalChartDefinition = this.env.model.getters.getChartDefinition(this.props.figureUI.id);
18944
18890
  owl.onWillUpdateProps(({ figureUI }) => {
18945
18891
  if (figureUI.id !== this.props.figureUI.id) {
@@ -18947,7 +18893,10 @@ stores.inject(MyMetaStore, storeInstance);
18947
18893
  }
18948
18894
  });
18949
18895
  }
18950
- getAvailableTypes() {
18896
+ getMenuItems() {
18897
+ return [this.fullScreenMenuItem, ...this.changeChartTypeMenuItems].filter(isDefined);
18898
+ }
18899
+ get changeChartTypeMenuItems() {
18951
18900
  const definition = this.env.model.getters.getChartDefinition(this.props.figureUI.id);
18952
18901
  if (!["line", "bar", "pie"].includes(definition.type)) {
18953
18902
  return [];
@@ -18955,8 +18904,11 @@ stores.inject(MyMetaStore, storeInstance);
18955
18904
  return ["column", "line", "pie"].map((type) => {
18956
18905
  const item = chartSubtypeRegistry.get(type);
18957
18906
  return {
18958
- ...item,
18959
- icon: this.getIconClasses(item.chartType),
18907
+ id: item.chartType,
18908
+ label: item.displayName,
18909
+ onClick: () => this.onTypeChange(item.chartType),
18910
+ isSelected: item.chartType === this.selectedChartType,
18911
+ iconClass: this.getIconClasses(item.chartType),
18960
18912
  };
18961
18913
  });
18962
18914
  }
@@ -19011,6 +18963,30 @@ stores.inject(MyMetaStore, storeInstance);
19011
18963
  this.menuState.anchorRect = { x: ev.clientX, y: ev.clientY, width: 0, height: 0 };
19012
18964
  this.menuState.menuItems = getChartMenuActions(this.props.figureUI.id, () => { }, this.env);
19013
18965
  }
18966
+ get fullScreenMenuItem() {
18967
+ const definition = this.env.model.getters.getChartDefinition(this.props.figureUI.id);
18968
+ if (definition.type === "scorecard") {
18969
+ return undefined;
18970
+ }
18971
+ if (this.props.figureUI.id === this.fullScreenFigureStore.fullScreenFigure?.id) {
18972
+ return {
18973
+ id: "fullScreenChart",
18974
+ label: _t("Exit Full Screen"),
18975
+ iconClass: "fa fa-compress",
18976
+ onClick: () => {
18977
+ this.fullScreenFigureStore.toggleFullScreenChart(this.props.figureUI.id);
18978
+ },
18979
+ };
18980
+ }
18981
+ return {
18982
+ id: "fullScreenChart",
18983
+ label: _t("Full Screen"),
18984
+ iconClass: "fa fa-expand",
18985
+ onClick: () => {
18986
+ this.fullScreenFigureStore.toggleFullScreenChart(this.props.figureUI.id);
18987
+ },
18988
+ };
18989
+ }
19014
18990
  }
19015
18991
 
19016
18992
  // -----------------------------------------------------------------------------
@@ -29029,7 +29005,7 @@ stores.inject(MyMetaStore, storeInstance);
29029
29005
  if (error) {
29030
29006
  return error;
29031
29007
  }
29032
- const table = pivot.getTableStructure();
29008
+ const table = pivot.getCollapsedTableStructure();
29033
29009
  const cells = table.getPivotCells(_includedTotal, _includeColumnHeaders);
29034
29010
  const headerRows = _includeColumnHeaders ? table.columns.length : 0;
29035
29011
  const pivotTitle = this.getters.getPivotDisplayName(pivotId);
@@ -29049,7 +29025,7 @@ stores.inject(MyMetaStore, storeInstance);
29049
29025
  break;
29050
29026
  case "HEADER":
29051
29027
  const valueAndFormat = pivot.getPivotHeaderValueAndFormat(pivotCell.domain);
29052
- result[col].push(addIndentAndAlignToPivotHeader(pivot, pivotCell.domain, valueAndFormat));
29028
+ result[col].push(addAlignFormatToPivotHeader(pivotCell.domain, valueAndFormat));
29053
29029
  break;
29054
29030
  case "MEASURE_HEADER":
29055
29031
  result[col].push(pivot.getPivotMeasureValue(pivotCell.measure, pivotCell.domain));
@@ -32976,12 +32952,13 @@ stores.inject(MyMetaStore, storeInstance);
32976
32952
  return res;
32977
32953
  }
32978
32954
  getComposerContent() {
32955
+ let content = this._currentContent;
32979
32956
  if (this.editionMode === "inactive") {
32980
32957
  // References in the content might not be linked to the current active sheet
32981
32958
  // We here force the sheet name prefix for all references that are not in
32982
32959
  // the current active sheet
32983
32960
  const defaultRangeSheetId = this.args().defaultRangeSheetId;
32984
- return rangeTokenize(this.args().content)
32961
+ content = rangeTokenize(this.args().content)
32985
32962
  .map((token) => {
32986
32963
  if (token.type === "REFERENCE") {
32987
32964
  const range = this.getters.getRangeFromSheetXC(defaultRangeSheetId, token.value);
@@ -32991,7 +32968,7 @@ stores.inject(MyMetaStore, storeInstance);
32991
32968
  })
32992
32969
  .join("");
32993
32970
  }
32994
- return this._currentContent;
32971
+ return localizeContent(content, this.getters.getLocale());
32995
32972
  }
32996
32973
  stopEdition() {
32997
32974
  this._stopEdition();
@@ -35670,11 +35647,6 @@ stores.inject(MyMetaStore, storeInstance);
35670
35647
  };
35671
35648
  }
35672
35649
 
35673
- /**
35674
- * Registry to draw icons on cells
35675
- */
35676
- const iconsOnCellRegistry = new Registry();
35677
-
35678
35650
  css /* scss */ `
35679
35651
  .o-spreadsheet {
35680
35652
  .o-icon {
@@ -35811,13 +35783,6 @@ stores.inject(MyMetaStore, storeInstance);
35811
35783
  bad: "dotBad",
35812
35784
  },
35813
35785
  };
35814
- iconsOnCellRegistry.add("conditional_formatting", (getters, position) => {
35815
- const icon = getters.getConditionalIcon(position);
35816
- if (icon) {
35817
- return ICONS[icon].svg;
35818
- }
35819
- return undefined;
35820
- });
35821
35786
 
35822
35787
  /**
35823
35788
  * Map of the different types of conversions warnings and their name in error messages
@@ -39925,6 +39890,22 @@ stores.inject(MyMetaStore, storeInstance);
39925
39890
  }
39926
39891
  return data;
39927
39892
  },
39893
+ })
39894
+ .add("18.4.2", {
39895
+ migrate(data) {
39896
+ for (const sheet of data.sheets || []) {
39897
+ for (const figure of sheet.figures || []) {
39898
+ if (figure.tag !== "chart" || figure.data.type !== "scorecard") {
39899
+ continue;
39900
+ }
39901
+ const scData = figure.data;
39902
+ if (scData.baselineDescr) {
39903
+ scData.baselineDescr = { text: scData.baselineDescr };
39904
+ }
39905
+ }
39906
+ }
39907
+ return data;
39908
+ },
39928
39909
  });
39929
39910
  function fixOverlappingFilters(data) {
39930
39911
  for (const sheet of data.sheets || []) {
@@ -40799,7 +40780,7 @@ stores.inject(MyMetaStore, storeInstance);
40799
40780
  sequence: index,
40800
40781
  execute: (env) => {
40801
40782
  const zone = env.model.getters.getSelectedZone();
40802
- const table = env.model.getters.getPivot(pivotId).getTableStructure().export();
40783
+ const table = env.model.getters.getPivot(pivotId).getCollapsedTableStructure().export();
40803
40784
  env.model.dispatch("INSERT_PIVOT_WITH_TABLE", {
40804
40785
  pivotId,
40805
40786
  table,
@@ -40818,7 +40799,7 @@ stores.inject(MyMetaStore, storeInstance);
40818
40799
  sequence: index,
40819
40800
  execute: (env) => {
40820
40801
  const zone = env.model.getters.getSelectedZone();
40821
- const table = env.model.getters.getPivot(pivotId).getTableStructure().export();
40802
+ const table = env.model.getters.getPivot(pivotId).getExpandedTableStructure().export();
40822
40803
  env.model.dispatch("INSERT_PIVOT_WITH_TABLE", {
40823
40804
  pivotId,
40824
40805
  table,
@@ -42199,6 +42180,218 @@ stores.inject(MyMetaStore, storeInstance);
42199
42180
  }
42200
42181
  }
42201
42182
 
42183
+ const PREVIOUS_VALUE = "(previous)";
42184
+ const NEXT_VALUE = "(next)";
42185
+ function getDomainOfParentRow(pivot, domain) {
42186
+ const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
42187
+ return [...colDomain, ...rowDomain.slice(0, rowDomain.length - 1)];
42188
+ }
42189
+ function getDomainOfParentCol(pivot, domain) {
42190
+ const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
42191
+ return [...colDomain.slice(0, colDomain.length - 1), ...rowDomain];
42192
+ }
42193
+ /**
42194
+ * Split a pivot domain into the part related to the rows of the pivot, and the part related to the columns.
42195
+ */
42196
+ function domainToColRowDomain(pivot, domain) {
42197
+ const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
42198
+ const rowDomain = domain.filter((node) => rowFields.includes(node.field));
42199
+ const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
42200
+ const colDomain = domain.filter((node) => columnFields.includes(node.field));
42201
+ return { colDomain, rowDomain };
42202
+ }
42203
+ function getDimensionDomain(pivot, dimension, domain) {
42204
+ return dimension === "column"
42205
+ ? domainToColRowDomain(pivot, domain).colDomain
42206
+ : domainToColRowDomain(pivot, domain).rowDomain;
42207
+ }
42208
+ function getFieldValueInDomain(fieldNameWithGranularity, domain) {
42209
+ const node = domain.find((n) => n.field === fieldNameWithGranularity);
42210
+ return node?.value;
42211
+ }
42212
+ function isDomainIsInPivot(pivot, domain) {
42213
+ for (const node of domain) {
42214
+ if (pivot.definition.rows.find((row) => row.nameWithGranularity === node.field) === undefined &&
42215
+ pivot.definition.columns.find((col) => col.nameWithGranularity === node.field) === undefined) {
42216
+ return false;
42217
+ }
42218
+ }
42219
+ const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
42220
+ return (checkIfDomainInInTree(rowDomain, pivot.getExpandedTableStructure().getRowTree()) &&
42221
+ checkIfDomainInInTree(colDomain, pivot.getExpandedTableStructure().getColTree()));
42222
+ }
42223
+ function checkIfDomainInInTree(domain, tree) {
42224
+ return walkDomainTree(domain, tree) !== undefined;
42225
+ }
42226
+ /**
42227
+ * Given a tree of the col/rows of a pivot, and a domain related to those col/rows, return the node of the tree
42228
+ * corresponding to the domain.
42229
+ *
42230
+ * @param domain The domain to find in the tree
42231
+ * @param tree The tree to search in7
42232
+ * @param stopAtField If provided, the search will stop at the field with this name
42233
+ */
42234
+ function walkDomainTree(domain, tree, stopAtField) {
42235
+ let currentTreeNode = tree;
42236
+ for (const node of domain) {
42237
+ const child = currentTreeNode.find((n) => n.value === node.value);
42238
+ if (!child) {
42239
+ return undefined;
42240
+ }
42241
+ if (child.field === stopAtField) {
42242
+ return currentTreeNode;
42243
+ }
42244
+ currentTreeNode = child.children;
42245
+ }
42246
+ return currentTreeNode;
42247
+ }
42248
+ /**
42249
+ * Get the domain parent of the given domain with the field `parentFieldName` as leaf of the domain.
42250
+ *
42251
+ * In practice, if the `parentFieldName` is a row in the pivot, the helper will return a domain with the same column
42252
+ * domain, and with a row domain all groupBys children to `parentFieldName` removed.
42253
+ */
42254
+ function getFieldParentDomain(pivot, parentFieldName, domain) {
42255
+ let { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
42256
+ const dimension = getFieldDimensionType(pivot, parentFieldName);
42257
+ if (dimension === "row") {
42258
+ const index = rowDomain.findIndex((node) => node.field === parentFieldName);
42259
+ if (index === -1) {
42260
+ return domain;
42261
+ }
42262
+ rowDomain = rowDomain.slice(0, index + 1);
42263
+ }
42264
+ else {
42265
+ const index = colDomain.findIndex((node) => node.field === parentFieldName);
42266
+ if (index === -1) {
42267
+ return domain;
42268
+ }
42269
+ colDomain = colDomain.slice(0, index + 1);
42270
+ }
42271
+ return [...rowDomain, ...colDomain];
42272
+ }
42273
+ /**
42274
+ * Replace in the domain the value of the field `fieldNameWithGranularity` with the given `value`
42275
+ */
42276
+ function replaceFieldValueInDomain(domain, fieldNameWithGranularity, value) {
42277
+ domain = deepCopy(domain);
42278
+ const node = domain.find((n) => n.field === fieldNameWithGranularity);
42279
+ if (!node) {
42280
+ return domain;
42281
+ }
42282
+ node.value = value;
42283
+ return domain;
42284
+ }
42285
+ function isFieldInDomain(nameWithGranularity, domain) {
42286
+ return domain.some((node) => node.field === nameWithGranularity);
42287
+ }
42288
+ /**
42289
+ * Check if the field is in the rows or columns of the pivot
42290
+ */
42291
+ function getFieldDimensionType(pivot, nameWithGranularity) {
42292
+ const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
42293
+ if (rowFields.includes(nameWithGranularity)) {
42294
+ return "row";
42295
+ }
42296
+ const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
42297
+ if (columnFields.includes(nameWithGranularity)) {
42298
+ return "column";
42299
+ }
42300
+ throw new Error(`Field ${nameWithGranularity} not found in pivot`);
42301
+ }
42302
+ /**
42303
+ * Replace in the given domain the value of the field `fieldNameWithGranularity` with the previous or next value.
42304
+ */
42305
+ function getPreviousOrNextValueDomain(pivot, domain, fieldNameWithGranularity, direction) {
42306
+ const dimension = getFieldDimensionType(pivot, fieldNameWithGranularity);
42307
+ const tree = dimension === "row"
42308
+ ? pivot.getExpandedTableStructure().getRowTree()
42309
+ : pivot.getExpandedTableStructure().getColTree();
42310
+ const dimDomain = getDimensionDomain(pivot, dimension, domain);
42311
+ const currentTreeNode = walkDomainTree(dimDomain, tree, fieldNameWithGranularity);
42312
+ const values = currentTreeNode?.map((n) => n.value) ?? [];
42313
+ const value = getFieldValueInDomain(fieldNameWithGranularity, domain);
42314
+ if (value === undefined) {
42315
+ return undefined;
42316
+ }
42317
+ const valueIndex = values.indexOf(value);
42318
+ if (value === undefined || valueIndex === -1) {
42319
+ return undefined;
42320
+ }
42321
+ const offset = direction === PREVIOUS_VALUE ? -1 : 1;
42322
+ const newIndex = clip(valueIndex + offset, 0, values.length - 1);
42323
+ return replaceFieldValueInDomain(domain, fieldNameWithGranularity, values[newIndex]);
42324
+ }
42325
+ function domainToString(domain) {
42326
+ return domain ? domain.map(domainNodeToString).join(", ") : "";
42327
+ }
42328
+ function domainNodeToString(domainNode) {
42329
+ return domainNode ? `${domainNode.field}=${domainNode.value}` : "";
42330
+ }
42331
+ /**
42332
+ *
42333
+ * For the ranking, the pivot cell values of a column (or row) at the same depth are grouped together before being sorted
42334
+ * and ranked.
42335
+ *
42336
+ * The grouping of a pivot cell is done with both the value of the domain nodes that are parent of the field
42337
+ * `fieldNameWithGranularity` and the value of the last node of the domain of the pivot cell, if it's not the field
42338
+ * `fieldNameWithGranularity`.
42339
+ *
42340
+ * For example, let's take a pivot grouped by (Date:year, Stage, User, Product), where we want to rank by "Stage" field.
42341
+ * The domain nodes parents of the "Stage" are [Date:year]. The pivot cell with domain:
42342
+ * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
42343
+ * - [Date:year=2021, Stage=Lead] is grouped with the cells [Date:year=2021, Stage=*, User=None, Product=None],
42344
+ * and then ranked within the group
42345
+ * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=*, User=Bob, Product=None],
42346
+ * and then ranked within the group
42347
+ * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells [Date:year=2021, Stage=*, User=*, Product=Table],
42348
+ * and then ranked within the group
42349
+ *
42350
+ * If we rank the pivot on "User" instead, the parent domain becomes [Date:year, Sage] .The cell with domain:
42351
+ * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
42352
+ * - [Date:year=2021, Stage=Lead] is not ranked because it does not contain the "User" field
42353
+ * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=Lead, User=Bob, Product=None],
42354
+ * and then ranked within the group
42355
+ * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells with [Date:year=2021, Stage=Lead, User=*, Product=Table],
42356
+ * and then ranked within the group
42357
+ *
42358
+ */
42359
+ function getRankingDomainKey(domain, fieldNameWithGranularity) {
42360
+ const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
42361
+ if (index === -1) {
42362
+ return "";
42363
+ }
42364
+ const parent = domain.slice(0, index);
42365
+ const lastNode = domain.at(-1);
42366
+ return domainToString(lastNode.field === fieldNameWithGranularity ? parent : [...parent, lastNode]);
42367
+ }
42368
+ /**
42369
+ * The running total domain is the domain without the field `fieldNameWithGranularity`, ie. we do the running total of
42370
+ * all the pivot cells of the column that have any value for the field `fieldNameWithGranularity` and the same value for
42371
+ * the other fields.
42372
+ */
42373
+ function getRunningTotalDomainKey(domain, fieldNameWithGranularity) {
42374
+ const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
42375
+ if (index === -1) {
42376
+ return "";
42377
+ }
42378
+ return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
42379
+ }
42380
+ function sortPivotTree(tree, baseDomain, sortFn) {
42381
+ const sortedTree = [...tree];
42382
+ const domain = [...baseDomain];
42383
+ sortedTree.sort((node1, node2) => sortFn([...domain, node1], [...domain, node2]));
42384
+ for (const node of tree) {
42385
+ const children = sortPivotTree(node.children, [...domain, node], sortFn);
42386
+ node.children = children;
42387
+ }
42388
+ return sortedTree;
42389
+ }
42390
+ function isParentDomain(domain, parentDomain) {
42391
+ return (domain.length > parentDomain.length &&
42392
+ parentDomain.every((node, i) => deepEquals(node, domain[i])));
42393
+ }
42394
+
42202
42395
  const pivotProperties = {
42203
42396
  name: _t("See pivot properties"),
42204
42397
  execute(env) {
@@ -43199,6 +43392,66 @@ stores.inject(MyMetaStore, storeInstance);
43199
43392
  }
43200
43393
  }
43201
43394
 
43395
+ class ClientFocusStore extends SpreadsheetStore {
43396
+ mutators = [
43397
+ "focusClient",
43398
+ "unfocusClient",
43399
+ "showClientTag",
43400
+ "hideClientTag",
43401
+ "jumpToClient",
43402
+ ];
43403
+ _showClientTag = false;
43404
+ clientFocusTimeout = {};
43405
+ constructor(get) {
43406
+ super(get);
43407
+ this.onDispose(() => {
43408
+ for (const clientId in this.clientFocusTimeout) {
43409
+ this.unfocusClient(clientId);
43410
+ }
43411
+ });
43412
+ }
43413
+ get focusedClients() {
43414
+ const focused = new Set();
43415
+ this.model.getters.getConnectedClients().forEach((client) => {
43416
+ if (this._showClientTag || this.clientFocusTimeout[client.id] !== undefined) {
43417
+ focused.add(client.id);
43418
+ }
43419
+ });
43420
+ return focused;
43421
+ }
43422
+ jumpToClient(clientId) {
43423
+ const client = this.model.getters.getClient(clientId);
43424
+ this.focusClient(clientId);
43425
+ if (client.position) {
43426
+ this.model.dispatch("ACTIVATE_SHEET", {
43427
+ sheetIdTo: client.position.sheetId,
43428
+ sheetIdFrom: this.getters.getActiveSheetId(),
43429
+ });
43430
+ this.model.dispatch("SCROLL_TO_CELL", { col: client.position.col, row: client.position.row });
43431
+ }
43432
+ }
43433
+ showClientTag() {
43434
+ this._showClientTag = true;
43435
+ }
43436
+ hideClientTag() {
43437
+ this._showClientTag = false;
43438
+ }
43439
+ focusClient(clientId) {
43440
+ if (this.clientFocusTimeout[clientId]) {
43441
+ clearTimeout(this.clientFocusTimeout[clientId]);
43442
+ }
43443
+ // This call to unfocus client isn't proxyfied and doesn't trigger a render.
43444
+ // The focus will visually disappear when the next render is triggered
43445
+ this.clientFocusTimeout[clientId] = setTimeout(() => this.unfocusClient(clientId), 3000);
43446
+ }
43447
+ unfocusClient(clientId) {
43448
+ if (this.clientFocusTimeout[clientId]) {
43449
+ clearTimeout(this.clientFocusTimeout[clientId]);
43450
+ }
43451
+ this.clientFocusTimeout[clientId] = undefined;
43452
+ }
43453
+ }
43454
+
43202
43455
  /**
43203
43456
  * Function to be used during a pointerdown event, this function allows to
43204
43457
  * perform actions related to the pointermove and pointerup events and adjusts the viewport
@@ -43472,6 +43725,11 @@ stores.inject(MyMetaStore, storeInstance);
43472
43725
  get tagStyle() {
43473
43726
  const { col, row, color } = this.props;
43474
43727
  const { height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
43728
+ const visible = this.env.model.getters.isVisibleInViewport({
43729
+ sheetId: this.env.model.getters.getActiveSheetId(),
43730
+ col,
43731
+ row,
43732
+ });
43475
43733
  const { x, y } = this.env.model.getters.getVisibleRect({
43476
43734
  left: col,
43477
43735
  top: row,
@@ -43483,6 +43741,7 @@ stores.inject(MyMetaStore, storeInstance);
43483
43741
  left: `${x - 1}px`,
43484
43742
  border: `1px solid ${color}`,
43485
43743
  "background-color": color,
43744
+ visibility: visible ? "visible" : "hidden",
43486
43745
  });
43487
43746
  }
43488
43747
  }
@@ -43908,151 +44167,6 @@ stores.inject(MyMetaStore, storeInstance);
43908
44167
  }
43909
44168
  }
43910
44169
 
43911
- css /* scss */ `
43912
- .o-grid-cell-icon {
43913
- width: ${GRID_ICON_EDGE_LENGTH}px;
43914
- height: ${GRID_ICON_EDGE_LENGTH}px;
43915
- }
43916
- `;
43917
- class GridCellIcon extends owl.Component {
43918
- static template = "o-spreadsheet-GridCellIcon";
43919
- static props = {
43920
- cellPosition: Object,
43921
- horizontalAlign: { type: String, optional: true },
43922
- verticalAlign: { type: String, optional: true },
43923
- slots: Object,
43924
- };
43925
- get iconStyle() {
43926
- const cellPosition = this.props.cellPosition;
43927
- const merge = this.env.model.getters.getMerge(cellPosition);
43928
- const zone = merge || positionToZone(cellPosition);
43929
- const rect = this.env.model.getters.getVisibleRectWithoutHeaders(zone);
43930
- const x = this.getIconHorizontalPosition(rect, cellPosition);
43931
- const y = this.getIconVerticalPosition(rect, cellPosition);
43932
- return cssPropertiesToCss({
43933
- top: `${y}px`,
43934
- left: `${x}px`,
43935
- });
43936
- }
43937
- getIconVerticalPosition(rect, cellPosition) {
43938
- const start = rect.y;
43939
- const end = rect.y + rect.height;
43940
- const cell = this.env.model.getters.getCell(cellPosition);
43941
- const align = this.props.verticalAlign || cell?.style?.verticalAlign || DEFAULT_VERTICAL_ALIGN;
43942
- switch (align) {
43943
- case "bottom":
43944
- return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
43945
- case "top":
43946
- return start + GRID_ICON_MARGIN;
43947
- default:
43948
- const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
43949
- return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
43950
- }
43951
- }
43952
- getIconHorizontalPosition(rect, cellPosition) {
43953
- const start = rect.x;
43954
- const end = rect.x + rect.width;
43955
- const cell = this.env.model.getters.getCell(cellPosition);
43956
- const evaluatedCell = this.env.model.getters.getEvaluatedCell(cellPosition);
43957
- const align = this.props.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;
43958
- switch (align) {
43959
- case "right":
43960
- return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
43961
- case "left":
43962
- return start + GRID_ICON_MARGIN;
43963
- default:
43964
- const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
43965
- return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
43966
- }
43967
- }
43968
- isPositionVisible(position) {
43969
- const rect = this.env.model.getters.getVisibleRect(positionToZone(position));
43970
- return !(rect.width === 0 || rect.height === 0);
43971
- }
43972
- }
43973
-
43974
- const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
43975
- css /* scss */ `
43976
- .o-dv-checkbox {
43977
- margin: ${MARGIN}px;
43978
- /* required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
43979
- position: absolute;
43980
- }
43981
- `;
43982
- class DataValidationCheckbox extends owl.Component {
43983
- static template = "o-spreadsheet-DataValidationCheckbox";
43984
- static components = {
43985
- Checkbox,
43986
- };
43987
- static props = {
43988
- cellPosition: Object,
43989
- };
43990
- onCheckboxChange(value) {
43991
- const { sheetId, col, row } = this.props.cellPosition;
43992
- const cellContent = value ? "TRUE" : "FALSE";
43993
- this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
43994
- }
43995
- get checkBoxValue() {
43996
- return !!this.env.model.getters.getEvaluatedCell(this.props.cellPosition).value;
43997
- }
43998
- get isDisabled() {
43999
- const cell = this.env.model.getters.getCell(this.props.cellPosition);
44000
- return this.env.model.getters.isReadonly() || !!cell?.isFormula;
44001
- }
44002
- }
44003
-
44004
- const ICON_WIDTH = 13;
44005
- css /* scss */ `
44006
- .o-dv-list-icon {
44007
- color: ${TEXT_BODY_MUTED};
44008
- border-radius: 1px;
44009
- height: ${GRID_ICON_EDGE_LENGTH}px;
44010
- width: ${GRID_ICON_EDGE_LENGTH}px;
44011
-
44012
- &:hover {
44013
- color: #ffffff;
44014
- background-color: ${TEXT_BODY_MUTED};
44015
- }
44016
-
44017
- svg {
44018
- width: ${ICON_WIDTH}px;
44019
- height: ${ICON_WIDTH}px;
44020
- }
44021
- }
44022
- `;
44023
- class DataValidationListIcon extends owl.Component {
44024
- static template = "o-spreadsheet-DataValidationListIcon";
44025
- static props = {
44026
- cellPosition: Object,
44027
- };
44028
- onClick() {
44029
- const { col, row } = this.props.cellPosition;
44030
- this.env.model.selection.selectCell(col, row);
44031
- this.env.startCellEdition();
44032
- }
44033
- }
44034
-
44035
- class DataValidationOverlay extends owl.Component {
44036
- static template = "o-spreadsheet-DataValidationOverlay";
44037
- static props = {};
44038
- static components = { GridCellIcon, DataValidationCheckbox, DataValidationListIcon };
44039
- get checkBoxCellPositions() {
44040
- return this.env.model.getters
44041
- .getVisibleCellPositions()
44042
- .filter((position) => this.env.model.getters.isCellValidCheckbox(position) &&
44043
- !this.env.model.getters.isFilterHeader(position));
44044
- }
44045
- get listIconsCellPositions() {
44046
- if (this.env.model.getters.isReadonly()) {
44047
- return [];
44048
- }
44049
- return this.env.model.getters
44050
- .getVisibleCellPositions()
44051
- .filter((position) => this.env.model.getters.cellHasListDataValidationIcon(position) &&
44052
- !this.env.model.getters.isFilterHeader(position));
44053
- }
44054
- }
44055
-
44056
44170
  function dragFigureForMove({ x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, initialFigure, { maxX, maxY }, { scrollX: initialScrollX, scrollY: initialScrollY }, { scrollX, scrollY }) {
44057
44171
  const deltaX = mouseX - mouseInitialX + scrollX - initialScrollX;
44058
44172
  const newX = clip(initialFigure.x + deltaX, 0, maxX - initialFigure.width);
@@ -44066,14 +44180,14 @@ stores.inject(MyMetaStore, storeInstance);
44066
44180
  const deltaX = Math.min(dirX * (mouseInitialX - mouseX + scrollX - initialScrollX), width - minFigSize);
44067
44181
  const deltaY = Math.min(dirY * (mouseInitialY - mouseY + scrollY - initialScrollY), height - minFigSize);
44068
44182
  const fraction = Math.min(deltaX / width, deltaY / height);
44069
- width = width * (1 - fraction);
44070
- height = height * (1 - fraction);
44071
44183
  if (dirX < 0) {
44072
44184
  x = x + width * fraction;
44073
44185
  }
44074
44186
  if (dirY < 0) {
44075
44187
  y = y + height * fraction;
44076
44188
  }
44189
+ width = width * (1 - fraction);
44190
+ height = height * (1 - fraction);
44077
44191
  }
44078
44192
  else {
44079
44193
  const deltaX = Math.max(dirX * (mouseX - mouseInitialX + scrollX - initialScrollX), minFigSize - width);
@@ -44654,78 +44768,6 @@ stores.inject(MyMetaStore, storeInstance);
44654
44768
  }
44655
44769
  }
44656
44770
 
44657
- css /* scss */ `
44658
- .o-filter-icon {
44659
- color: ${FILTERS_COLOR};
44660
- display: flex;
44661
- align-items: center;
44662
- justify-content: center;
44663
- width: ${GRID_ICON_EDGE_LENGTH}px;
44664
- height: ${GRID_ICON_EDGE_LENGTH}px;
44665
-
44666
- &:hover {
44667
- background: ${FILTERS_COLOR};
44668
- color: #fff;
44669
- }
44670
-
44671
- &.o-high-contrast {
44672
- color: #defade;
44673
- }
44674
- &.o-high-contrast:hover {
44675
- color: ${FILTERS_COLOR};
44676
- background: #fff;
44677
- }
44678
- }
44679
- .o-filter-icon:hover {
44680
- background: ${FILTERS_COLOR};
44681
- color: #fff;
44682
- }
44683
- `;
44684
- class FilterIcon extends owl.Component {
44685
- static template = "o-spreadsheet-FilterIcon";
44686
- static props = {
44687
- cellPosition: Object,
44688
- };
44689
- cellPopovers;
44690
- setup() {
44691
- this.cellPopovers = useStore(CellPopoverStore);
44692
- }
44693
- onClick() {
44694
- const position = this.props.cellPosition;
44695
- const activePopover = this.cellPopovers.persistentCellPopover;
44696
- const { col, row } = position;
44697
- if (activePopover.isOpen &&
44698
- activePopover.col === col &&
44699
- activePopover.row === row &&
44700
- activePopover.type === "FilterMenu") {
44701
- this.cellPopovers.close();
44702
- return;
44703
- }
44704
- this.cellPopovers.open({ col, row }, "FilterMenu");
44705
- }
44706
- get isFilterActive() {
44707
- return this.env.model.getters.isFilterActive(this.props.cellPosition);
44708
- }
44709
- get iconClass() {
44710
- const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
44711
- const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
44712
- return luminance < 0.45 ? "o-high-contrast" : "";
44713
- }
44714
- }
44715
-
44716
- class FilterIconsOverlay extends owl.Component {
44717
- static template = "o-spreadsheet-FilterIconsOverlay";
44718
- static props = {};
44719
- static components = {
44720
- GridCellIcon,
44721
- FilterIcon,
44722
- };
44723
- getFilterHeadersPositions() {
44724
- const sheetId = this.env.model.getters.getActiveSheetId();
44725
- return this.env.model.getters.getFilterHeaders(sheetId);
44726
- }
44727
- }
44728
-
44729
44771
  css /* scss */ `
44730
44772
  .o-validation {
44731
44773
  border-radius: 4px;
@@ -44866,6 +44908,78 @@ stores.inject(MyMetaStore, storeInstance);
44866
44908
  }
44867
44909
  }
44868
44910
 
44911
+ class GridCellIcon extends owl.Component {
44912
+ static template = "o-spreadsheet-GridCellIcon";
44913
+ static props = {
44914
+ icon: Object,
44915
+ verticalAlign: { type: String, optional: true },
44916
+ slots: Object,
44917
+ };
44918
+ get iconStyle() {
44919
+ const cellPosition = this.props.icon.position;
44920
+ const merge = this.env.model.getters.getMerge(cellPosition);
44921
+ const zone = merge || positionToZone(cellPosition);
44922
+ const rect = this.env.model.getters.getVisibleRectWithoutHeaders(zone);
44923
+ const x = this.getIconHorizontalPosition(rect, cellPosition);
44924
+ const y = this.getIconVerticalPosition(rect, cellPosition);
44925
+ return cssPropertiesToCss({
44926
+ top: `${y}px`,
44927
+ left: `${x}px`,
44928
+ width: `${this.props.icon.size}px`,
44929
+ height: `${this.props.icon.size}px`,
44930
+ });
44931
+ }
44932
+ getIconVerticalPosition(rect, cellPosition) {
44933
+ const start = rect.y;
44934
+ const end = rect.y + rect.height;
44935
+ const cell = this.env.model.getters.getCell(cellPosition);
44936
+ const align = this.props.verticalAlign || cell?.style?.verticalAlign || DEFAULT_VERTICAL_ALIGN;
44937
+ switch (align) {
44938
+ case "bottom":
44939
+ return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
44940
+ case "top":
44941
+ return start + GRID_ICON_MARGIN;
44942
+ default:
44943
+ const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
44944
+ return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
44945
+ }
44946
+ }
44947
+ getIconHorizontalPosition(rect, cellPosition) {
44948
+ const start = rect.x;
44949
+ const end = rect.x + rect.width;
44950
+ const cell = this.env.model.getters.getCell(cellPosition);
44951
+ const evaluatedCell = this.env.model.getters.getEvaluatedCell(cellPosition);
44952
+ const align = this.props.icon.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;
44953
+ switch (align) {
44954
+ case "right":
44955
+ return end - this.props.icon.size - this.props.icon.margin;
44956
+ case "left":
44957
+ return start + this.props.icon.margin;
44958
+ default:
44959
+ const centeringOffset = Math.floor((end - start - this.props.icon.size) / 2);
44960
+ return end - this.props.icon.size - centeringOffset;
44961
+ }
44962
+ }
44963
+ isPositionVisible(position) {
44964
+ const rect = this.env.model.getters.getVisibleRect(positionToZone(position));
44965
+ return !(rect.width === 0 || rect.height === 0);
44966
+ }
44967
+ }
44968
+
44969
+ class GridCellIconOverlay extends owl.Component {
44970
+ static template = "o-spreadsheet-GridCellIconOverlay";
44971
+ static props = {};
44972
+ static components = { GridCellIcon };
44973
+ get icons() {
44974
+ const icons = [];
44975
+ for (const position of this.env.model.getters.getVisibleCellPositions()) {
44976
+ const cellIcons = this.env.model.getters.getCellIcons(position);
44977
+ icons.push(...cellIcons.filter((icon) => icon.component));
44978
+ }
44979
+ return icons;
44980
+ }
44981
+ }
44982
+
44869
44983
  /**
44870
44984
  * Manages an event listener on a ref. Useful for hooks that want to manage
44871
44985
  * event listeners, especially more than one. Prefer using t-on directly in
@@ -45117,9 +45231,8 @@ stores.inject(MyMetaStore, storeInstance);
45117
45231
  };
45118
45232
  static components = {
45119
45233
  FiguresContainer,
45120
- DataValidationOverlay,
45121
45234
  GridAddRowsFooter,
45122
- FilterIconsOverlay,
45235
+ GridCellIconOverlay,
45123
45236
  };
45124
45237
  static defaultProps = {
45125
45238
  onCellDoubleClicked: () => { },
@@ -46129,13 +46242,12 @@ stores.inject(MyMetaStore, storeInstance);
46129
46242
  // compute horizontal align start point parameter
46130
46243
  let x = box.x;
46131
46244
  if (align === "left") {
46132
- x += MIN_CELL_TEXT_MARGIN + (box.image ? box.image.size + MIN_CF_ICON_MARGIN : 0);
46245
+ const leftIconSize = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
46246
+ x += MIN_CELL_TEXT_MARGIN + leftIconSize;
46133
46247
  }
46134
46248
  else if (align === "right") {
46135
- x +=
46136
- box.width -
46137
- MIN_CELL_TEXT_MARGIN -
46138
- (box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0);
46249
+ const rightIconSize = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
46250
+ x += box.width - MIN_CELL_TEXT_MARGIN - rightIconSize;
46139
46251
  }
46140
46252
  else {
46141
46253
  x += box.width / 2;
@@ -46169,18 +46281,28 @@ stores.inject(MyMetaStore, storeInstance);
46169
46281
  drawIcon(renderingContext, boxes) {
46170
46282
  const { ctx } = renderingContext;
46171
46283
  for (const box of boxes) {
46172
- if (box.image && box.image.svg) {
46284
+ for (const icon of Object.values(box.icons)) {
46285
+ if (!icon || !icon.svg) {
46286
+ continue;
46287
+ }
46173
46288
  ctx.save();
46174
- if (box.image.clipIcon) {
46175
- ctx.beginPath();
46176
- const { x, y, width, height } = box.image.clipIcon;
46177
- ctx.rect(x, y, width, height);
46178
- ctx.clip();
46289
+ ctx.beginPath();
46290
+ ctx.rect(box.x, box.y, box.width, box.height);
46291
+ ctx.clip();
46292
+ const iconSize = icon.size;
46293
+ const iconY = this.computeTextYCoordinate(box, iconSize);
46294
+ const svg = icon.svg;
46295
+ let x;
46296
+ if (icon.horizontalAlign === "left") {
46297
+ x = box.x + icon.margin;
46298
+ }
46299
+ else if (icon.horizontalAlign === "right") {
46300
+ x = box.x + box.width - iconSize - icon.margin;
46179
46301
  }
46180
- const iconSize = box.image.size;
46181
- const y = this.computeTextYCoordinate(box, iconSize);
46182
- const svg = box.image.svg;
46183
- ctx.translate(box.x + MIN_CF_ICON_MARGIN, y);
46302
+ else {
46303
+ x = box.x + (box.width - iconSize) / 2;
46304
+ }
46305
+ ctx.translate(x, iconY);
46184
46306
  ctx.scale(iconSize / svg.width, iconSize / svg.height);
46185
46307
  ctx.fillStyle = svg.fillColor;
46186
46308
  ctx.fill(new Path2D(svg.path));
@@ -46357,13 +46479,11 @@ stores.inject(MyMetaStore, storeInstance);
46357
46479
  const position = { sheetId, col: col + 1, row };
46358
46480
  const nextCell = this.getters.getEvaluatedCell(position);
46359
46481
  const nextCellBorder = this.getters.getCellComputedBorder(position);
46360
- const cellHasIcon = this.getters.doesCellHaveGridIcon(position);
46361
- const cellHasCheckbox = this.getters.isCellValidCheckbox(position);
46482
+ const doesCellHaveGridIcon = this.getters.doesCellHaveGridIcon(position);
46362
46483
  if (nextCell.type !== CellValueType.empty ||
46363
46484
  this.getters.isInMerge(position) ||
46364
46485
  nextCellBorder?.left ||
46365
- cellHasIcon ||
46366
- cellHasCheckbox) {
46486
+ doesCellHaveGridIcon) {
46367
46487
  return col;
46368
46488
  }
46369
46489
  col++;
@@ -46377,13 +46497,11 @@ stores.inject(MyMetaStore, storeInstance);
46377
46497
  const position = { sheetId, col: col - 1, row };
46378
46498
  const previousCell = this.getters.getEvaluatedCell(position);
46379
46499
  const previousCellBorder = this.getters.getCellComputedBorder(position);
46380
- const cellHasIcon = this.getters.doesCellHaveGridIcon(position);
46381
- const cellHasCheckbox = this.getters.isCellValidCheckbox(position);
46500
+ const doesCellHaveGridIcon = this.getters.doesCellHaveGridIcon(position);
46382
46501
  if (previousCell.type !== CellValueType.empty ||
46383
46502
  this.getters.isInMerge(position) ||
46384
46503
  previousCellBorder?.right ||
46385
- cellHasIcon ||
46386
- cellHasCheckbox) {
46504
+ doesCellHaveGridIcon) {
46387
46505
  return col;
46388
46506
  }
46389
46507
  col--;
@@ -46419,6 +46537,12 @@ stores.inject(MyMetaStore, storeInstance);
46419
46537
  const dataBarFill = this.fingerprints.isEnabled
46420
46538
  ? undefined
46421
46539
  : this.getters.getConditionalDataBar(position);
46540
+ const iconsList = this.getters.getCellIcons(position);
46541
+ const cellIcons = {
46542
+ left: iconsList.find((icon) => icon?.horizontalAlign === "left"),
46543
+ right: iconsList.find((icon) => icon?.horizontalAlign === "right"),
46544
+ center: iconsList.find((icon) => icon?.horizontalAlign === "center"),
46545
+ };
46422
46546
  const box = {
46423
46547
  x,
46424
46548
  y,
@@ -46431,32 +46555,21 @@ stores.inject(MyMetaStore, storeInstance);
46431
46555
  overlayColor: this.hoveredTables.overlayColors.get(position),
46432
46556
  isError: (cell.type === CellValueType.error && !!cell.message) ||
46433
46557
  this.getters.isDataValidationInvalid(position),
46558
+ icons: cellIcons,
46434
46559
  };
46435
- /** Icon */
46436
- const iconSvg = this.getters.getCellIconSvg(position);
46437
46560
  const fontSizePX = computeTextFontSizeInPixels(box.style);
46438
- const iconBoxWidth = iconSvg ? MIN_CF_ICON_MARGIN + fontSizePX : 0;
46439
- if (iconSvg) {
46440
- box.image = {
46441
- type: "icon",
46442
- size: fontSizePX,
46443
- clipIcon: { x: box.x, y: box.y, width: Math.min(iconBoxWidth, width), height },
46444
- svg: iconSvg,
46445
- };
46446
- }
46447
- if (cell.type === CellValueType.empty || this.getters.isCellValidCheckbox(position)) {
46561
+ if (cell.type === CellValueType.empty || box.icons.center) {
46448
46562
  return box;
46449
46563
  }
46450
- /** Filter Header or data validation icon */
46451
- box.hasIcon = this.getters.doesCellHaveGridIcon(position);
46452
- const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;
46453
46564
  /** Content */
46454
46565
  const wrapping = style.wrapping || "overflow";
46455
46566
  const wrapText = wrapping === "wrap" && !showFormula;
46456
46567
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
46457
46568
  const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });
46458
46569
  const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));
46459
- const contentWidth = iconBoxWidth + textWidth + headerIconWidth;
46570
+ const leftIconWidth = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
46571
+ const rightIconWidth = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
46572
+ const contentWidth = leftIconWidth + textWidth + rightIconWidth;
46460
46573
  const align = this.computeCellAlignment(position, contentWidth > width);
46461
46574
  box.content = {
46462
46575
  textLines: multiLineText,
@@ -46465,11 +46578,11 @@ stores.inject(MyMetaStore, storeInstance);
46465
46578
  };
46466
46579
  /** ClipRect */
46467
46580
  const isOverflowing = contentWidth > width || fontSizePX > height;
46468
- if (iconSvg || box.hasIcon) {
46581
+ if (box.icons.left || box.icons.right) {
46469
46582
  box.clipRect = {
46470
- x: box.x + iconBoxWidth,
46583
+ x: box.x + leftIconWidth,
46471
46584
  y: box.y,
46472
- width: Math.max(0, width - iconBoxWidth - headerIconWidth),
46585
+ width: Math.max(0, width - leftIconWidth - rightIconWidth),
46473
46586
  height,
46474
46587
  };
46475
46588
  }
@@ -48331,14 +48444,16 @@ stores.inject(MyMetaStore, storeInstance);
48331
48444
  static components = { Section, TextStyler };
48332
48445
  static props = {
48333
48446
  title: { type: String, optional: true },
48447
+ placeholder: { type: String, optional: true },
48334
48448
  updateTitle: Function,
48335
- name: { type: String, optional: true },
48449
+ name: { type: String },
48336
48450
  style: Object,
48337
48451
  defaultStyle: { type: Object, optional: true },
48338
48452
  updateStyle: Function,
48339
48453
  };
48340
48454
  static defaultProps = {
48341
48455
  title: "",
48456
+ placeholder: "",
48342
48457
  };
48343
48458
  updateTitle(ev) {
48344
48459
  this.props.updateTitle(ev.target.value);
@@ -49368,6 +49483,7 @@ stores.inject(MyMetaStore, storeInstance);
49368
49483
  SidePanelCollapsible,
49369
49484
  Section,
49370
49485
  Checkbox,
49486
+ ChartTitle,
49371
49487
  };
49372
49488
  static props = {
49373
49489
  figureId: String,
@@ -49392,9 +49508,6 @@ stores.inject(MyMetaStore, storeInstance);
49392
49508
  translate(term) {
49393
49509
  return _t(term);
49394
49510
  }
49395
- updateBaselineDescr(ev) {
49396
- this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
49397
- }
49398
49511
  setColor(color, colorPickerId) {
49399
49512
  switch (colorPickerId) {
49400
49513
  case "backgroundColor":
@@ -49408,6 +49521,38 @@ stores.inject(MyMetaStore, storeInstance);
49408
49521
  break;
49409
49522
  }
49410
49523
  }
49524
+ get keyStyle() {
49525
+ return {
49526
+ align: "center",
49527
+ fontSize: DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE,
49528
+ ...this.props.definition.keyDescr,
49529
+ };
49530
+ }
49531
+ get baselineStyle() {
49532
+ return {
49533
+ align: "center",
49534
+ fontSize: DEFAULT_SCORECARD_BASELINE_FONT_SIZE,
49535
+ ...this.props.definition.baselineDescr,
49536
+ };
49537
+ }
49538
+ setKeyText(text) {
49539
+ this.props.updateChart(this.props.figureId, {
49540
+ keyDescr: { ...this.props.definition.keyDescr, text },
49541
+ });
49542
+ }
49543
+ updateKeyStyle(style) {
49544
+ const keyDescr = { ...this.keyStyle, ...style };
49545
+ this.props.updateChart(this.props.figureId, { keyDescr });
49546
+ }
49547
+ setBaselineText(text) {
49548
+ this.props.updateChart(this.props.figureId, {
49549
+ baselineDescr: { ...this.props.definition.baselineDescr, text },
49550
+ });
49551
+ }
49552
+ updateBaselineStyle(style) {
49553
+ const baselineDescr = { ...this.baselineStyle, ...style };
49554
+ this.props.updateChart(this.props.figureId, { baselineDescr });
49555
+ }
49411
49556
  }
49412
49557
 
49413
49558
  class SunburstChartDesignPanel extends owl.Component {
@@ -52318,6 +52463,9 @@ stores.inject(MyMetaStore, storeInstance);
52318
52463
  }
52319
52464
  return undefined;
52320
52465
  }
52466
+ get isCalculatedMeasureInvalid() {
52467
+ return this.env.model.getters.getMeasureCompiledFormula(this.props.measure).isBadExpression;
52468
+ }
52321
52469
  }
52322
52470
 
52323
52471
  css /* scss */ `
@@ -52808,11 +52956,13 @@ stores.inject(MyMetaStore, storeInstance);
52808
52956
  columns;
52809
52957
  rows;
52810
52958
  sortedColumn;
52959
+ collapsedDomains;
52811
52960
  constructor(definition, fields) {
52812
52961
  this.measures = definition.measures.map((measure) => createMeasure(fields, measure));
52813
52962
  this.columns = definition.columns.map((dimension) => createPivotDimension(fields, dimension));
52814
52963
  this.rows = definition.rows.map((dimension) => createPivotDimension(fields, dimension));
52815
52964
  this.sortedColumn = definition.sortedColumn;
52965
+ this.collapsedDomains = definition.collapsedDomains;
52816
52966
  }
52817
52967
  getDimension(nameWithGranularity) {
52818
52968
  const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||
@@ -52967,24 +53117,62 @@ stores.inject(MyMetaStore, storeInstance);
52967
53117
  rowTree;
52968
53118
  colTree;
52969
53119
  isSorted = false;
52970
- constructor(columns, rows, measures, fieldsType) {
52971
- this.columns = columns.map((row) => {
53120
+ constructor(columns, rows, measures, fieldsType, collapsedDomains = { COL: [], ROW: [] }) {
53121
+ this.measures = measures;
53122
+ this.fieldsType = fieldsType;
53123
+ if (collapsedDomains.COL.length) {
53124
+ columns = this.removeCollapsedColumns(columns, measures, collapsedDomains.COL);
53125
+ }
53126
+ this.columns = columns.map((cols) => {
52972
53127
  // offset in the pivot table
52973
53128
  // starts at 1 because the first column is the row title
52974
53129
  let offset = 1;
52975
- return row.map((col) => {
53130
+ return cols.map((col) => {
52976
53131
  col = { ...col, offset };
52977
53132
  offset += col.width;
52978
53133
  return col;
52979
53134
  });
52980
53135
  });
52981
- this.rows = rows;
52982
- this.measures = measures;
52983
- this.fieldsType = fieldsType;
53136
+ this.rows = rows.filter((row) => !this.isParentCollapsed(collapsedDomains.ROW, row));
52984
53137
  this.maxIndent = Math.max(...this.rows.map((row) => row.indent));
52985
53138
  this.rowTree = lazy(() => this.buildRowsTree());
52986
53139
  this.colTree = lazy(() => this.buildColumnsTree());
52987
53140
  }
53141
+ removeCollapsedColumns(columns, measures, collapsedDomains) {
53142
+ const replaceCollapsedChildrenWithSubTotalColumns = (parentCol, depth) => {
53143
+ const parentDomain = this.getDomain(parentCol);
53144
+ const cols = columns[depth];
53145
+ const startIndex = cols.findIndex((col) => isParentDomain(this.getDomain(col), parentDomain));
53146
+ const endIndex = cols.findLastIndex((col) => isParentDomain(this.getDomain(col), parentDomain));
53147
+ const isLeaf = depth === columns.length - 1;
53148
+ const newColumns = measures.map((measure) => {
53149
+ const fields = isLeaf ? [...parentCol.fields, "measure"] : [];
53150
+ const values = isLeaf ? [...parentCol.values, measure] : [];
53151
+ return { fields, values, width: 1, offset: 0, collapsedHeader: !isLeaf };
53152
+ });
53153
+ cols.splice(startIndex, endIndex - startIndex + 1, ...newColumns);
53154
+ };
53155
+ return columns.map((cols, i) => {
53156
+ for (const col of cols) {
53157
+ if (i >= columns.length - 2) {
53158
+ return cols;
53159
+ }
53160
+ const domain = this.getDomain(col);
53161
+ if (!collapsedDomains.some((collapsedDomain) => deepEquals(domain, collapsedDomain))) {
53162
+ continue;
53163
+ }
53164
+ col.width = measures.length;
53165
+ for (let depth = i + 1; depth < columns.length; depth++) {
53166
+ replaceCollapsedChildrenWithSubTotalColumns(col, depth);
53167
+ }
53168
+ }
53169
+ return cols;
53170
+ });
53171
+ }
53172
+ isParentCollapsed(collapsedDomains, dim) {
53173
+ const domain = this.getDomain(dim);
53174
+ return collapsedDomains.some((collapsedDomain) => isParentDomain(domain, collapsedDomain));
53175
+ }
52988
53176
  /**
52989
53177
  * Get the number of columns leafs (i.e. the number of the last row of columns)
52990
53178
  */
@@ -53040,19 +53228,19 @@ stores.inject(MyMetaStore, storeInstance);
53040
53228
  }
53041
53229
  else if (row <= colHeadersHeight - 1) {
53042
53230
  const domain = this.getColHeaderDomain(col, row);
53043
- return domain ? { type: "HEADER", domain } : EMPTY_PIVOT_CELL;
53231
+ return domain ? { type: "HEADER", domain, dimension: "COL" } : EMPTY_PIVOT_CELL;
53044
53232
  }
53045
53233
  else if (col === 0) {
53046
53234
  const rowIndex = row - colHeadersHeight;
53047
- const domain = this.getRowDomain(rowIndex);
53048
- return { type: "HEADER", domain };
53235
+ const domain = this.getDomain(this.rows[rowIndex]);
53236
+ return { type: "HEADER", domain, dimension: "ROW" };
53049
53237
  }
53050
53238
  else {
53051
53239
  const rowIndex = row - colHeadersHeight;
53052
53240
  if (!includeTotal && this.isTotalRow(rowIndex)) {
53053
53241
  return EMPTY_PIVOT_CELL;
53054
53242
  }
53055
- const domain = [...this.getRowDomain(rowIndex), ...this.getColDomain(col)];
53243
+ const domain = [...this.getDomain(this.rows[rowIndex]), ...this.getColDomain(col)];
53056
53244
  const measure = this.getColMeasure(col);
53057
53245
  return { type: "VALUE", domain, measure };
53058
53246
  }
@@ -53061,31 +53249,31 @@ stores.inject(MyMetaStore, storeInstance);
53061
53249
  if (col === 0) {
53062
53250
  return undefined;
53063
53251
  }
53064
- const domain = [];
53065
53252
  const pivotCol = this.columns[row].find((pivotCol) => pivotCol.offset === col);
53066
- if (!pivotCol) {
53253
+ if (!pivotCol || pivotCol.collapsedHeader) {
53067
53254
  return undefined;
53068
53255
  }
53069
- for (let i = 0; i < pivotCol.fields.length; i++) {
53070
- const fieldWithGranularity = pivotCol.fields[i];
53256
+ return this.getDomain(pivotCol);
53257
+ }
53258
+ getDomain(dim) {
53259
+ return dim.fields.map((fieldWithGranularity, i) => {
53071
53260
  if (fieldWithGranularity === "measure") {
53072
- domain.push({
53261
+ return {
53073
53262
  type: "char",
53074
53263
  field: fieldWithGranularity,
53075
- value: toNormalizedPivotValue({ displayName: "measure", type: "char" }, pivotCol.values[i]),
53076
- });
53264
+ value: toNormalizedPivotValue({ displayName: "measure", type: "char" }, dim.values[i]),
53265
+ };
53077
53266
  }
53078
53267
  else {
53079
53268
  const { fieldName, granularity } = parseDimension(fieldWithGranularity);
53080
53269
  const type = this.fieldsType[fieldName] || "char";
53081
- domain.push({
53270
+ return {
53082
53271
  type,
53083
53272
  field: fieldWithGranularity,
53084
- value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, pivotCol.values[i]),
53085
- });
53273
+ value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, dim.values[i]),
53274
+ };
53086
53275
  }
53087
- }
53088
- return domain;
53276
+ });
53089
53277
  }
53090
53278
  getColDomain(col) {
53091
53279
  const domain = this.getColHeaderDomain(col, this.columns.length - 1);
@@ -53099,20 +53287,6 @@ stores.inject(MyMetaStore, storeInstance);
53099
53287
  }
53100
53288
  return measure.toString();
53101
53289
  }
53102
- getRowDomain(row) {
53103
- const domain = [];
53104
- for (let i = 0; i < this.rows[row].fields.length; i++) {
53105
- const fieldWithGranularity = this.rows[row].fields[i];
53106
- const { fieldName, granularity } = parseDimension(fieldWithGranularity);
53107
- const type = this.fieldsType[fieldName] || "char";
53108
- domain.push({
53109
- type,
53110
- field: fieldWithGranularity,
53111
- value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, this.rows[row].values[i]),
53112
- });
53113
- }
53114
- return domain;
53115
- }
53116
53290
  buildRowsTree() {
53117
53291
  const tree = [];
53118
53292
  let depth = 0;
@@ -53217,7 +53391,7 @@ stores.inject(MyMetaStore, storeInstance);
53217
53391
  /**
53218
53392
  * This function converts a list of data entry into a spreadsheet pivot table.
53219
53393
  */
53220
- function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
53394
+ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition, mode) {
53221
53395
  const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
53222
53396
  const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
53223
53397
  computeWidthOfColumnsNodes(columnsTree, measureIds.length);
@@ -53236,7 +53410,8 @@ stores.inject(MyMetaStore, storeInstance);
53236
53410
  for (const row of definition.rows) {
53237
53411
  fieldsType[row.fieldName] = row.type;
53238
53412
  }
53239
- return new SpreadsheetPivotTable(cols, rows, measureIds, fieldsType);
53413
+ const collapsedDomains = mode === "collapsed" ? definition.collapsedDomains : undefined;
53414
+ return new SpreadsheetPivotTable(cols, rows, measureIds, fieldsType, collapsedDomains);
53240
53415
  }
53241
53416
  // -----------------------------------------------------------------------------
53242
53417
  // ROWS
@@ -53609,7 +53784,8 @@ stores.inject(MyMetaStore, storeInstance);
53609
53784
  * This object contains the pivot table structure. It is created from the
53610
53785
  * data entries and the pivot definition.
53611
53786
  */
53612
- table;
53787
+ collapsedTable;
53788
+ expandedTable;
53613
53789
  /**
53614
53790
  * This error is set when the range is invalid. It is used to show an error
53615
53791
  * message to the user.
@@ -53642,7 +53818,8 @@ stores.inject(MyMetaStore, storeInstance);
53642
53818
  this.dataEntries = this.loadData();
53643
53819
  }
53644
53820
  if (type >= ReloadType.TABLE) {
53645
- this.table = undefined;
53821
+ this.collapsedTable = undefined;
53822
+ this.expandedTable = undefined;
53646
53823
  }
53647
53824
  }
53648
53825
  onDefinitionChange(nextDefinition) {
@@ -53802,14 +53979,23 @@ stores.inject(MyMetaStore, storeInstance);
53802
53979
  }
53803
53980
  return values;
53804
53981
  }
53805
- getTableStructure() {
53982
+ getCollapsedTableStructure() {
53806
53983
  if (!this.isValid()) {
53807
53984
  throw new Error("Pivot is not valid !");
53808
53985
  }
53809
- if (!this.table) {
53810
- this.table = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition);
53986
+ if (!this.collapsedTable) {
53987
+ this.collapsedTable = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition, "collapsed");
53988
+ }
53989
+ return this.collapsedTable;
53990
+ }
53991
+ getExpandedTableStructure() {
53992
+ if (!this.isValid()) {
53993
+ throw new Error("Pivot is not valid !");
53994
+ }
53995
+ if (!this.expandedTable) {
53996
+ this.expandedTable = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition, "expanded");
53811
53997
  }
53812
- return this.table;
53998
+ return this.expandedTable;
53813
53999
  }
53814
54000
  getFields() {
53815
54001
  return this.metaData.fields;
@@ -54167,6 +54353,13 @@ stores.inject(MyMetaStore, storeInstance);
54167
54353
  })),
54168
54354
  sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
54169
54355
  };
54356
+ if (cleanedDefinition.collapsedDomains) {
54357
+ const { COL, ROW } = cleanedDefinition.collapsedDomains;
54358
+ cleanedDefinition.collapsedDomains = {
54359
+ COL: COL.filter((domain) => this.areDomainFieldsValid(domain, cleanedDefinition.columns)),
54360
+ ROW: ROW.filter((domain) => this.areDomainFieldsValid(domain, cleanedDefinition.rows)),
54361
+ };
54362
+ }
54170
54363
  if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
54171
54364
  return;
54172
54365
  }
@@ -54256,6 +54449,15 @@ stores.inject(MyMetaStore, storeInstance);
54256
54449
  return (newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
54257
54450
  deepEquals(oldDefinition.columns, newDefinition.columns));
54258
54451
  }
54452
+ areDomainFieldsValid(domain, dims) {
54453
+ const fieldsNameWithGranularity = dims.map(({ fieldName, granularity }) => fieldName + (granularity ? `:${granularity}` : ""));
54454
+ for (let i = 0; i < domain.length; i++) {
54455
+ if (domain[i].field !== fieldsNameWithGranularity[i]) {
54456
+ return false;
54457
+ }
54458
+ }
54459
+ return true;
54460
+ }
54259
54461
  }
54260
54462
 
54261
54463
  class PivotSpreadsheetSidePanel extends owl.Component {
@@ -55523,6 +55725,7 @@ stores.inject(MyMetaStore, storeInstance);
55523
55725
  composerFocusStore;
55524
55726
  DOMFocusableElementStore;
55525
55727
  paintFormatStore;
55728
+ clientFocusStore;
55526
55729
  dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);
55527
55730
  onMouseWheel;
55528
55731
  hoveredCell;
@@ -55540,6 +55743,7 @@ stores.inject(MyMetaStore, storeInstance);
55540
55743
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
55541
55744
  this.sidePanel = useStore(SidePanelStore);
55542
55745
  this.paintFormatStore = useStore(PaintFormatStore);
55746
+ this.clientFocusStore = useStore(ClientFocusStore);
55543
55747
  useStore(ArrayFormulaHighlight);
55544
55748
  owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });
55545
55749
  owl.useExternalListener(document.body, "cut", this.copy.bind(this, true));
@@ -55822,6 +56026,9 @@ stores.inject(MyMetaStore, storeInstance);
55822
56026
  isCellHovered(col, row) {
55823
56027
  return this.hoveredCell.col === col && this.hoveredCell.row === row;
55824
56028
  }
56029
+ get focusedClients() {
56030
+ return this.clientFocusStore.focusedClients;
56031
+ }
55825
56032
  getGridRect() {
55826
56033
  return {
55827
56034
  ...getRefBoundingRect(this.gridRef),
@@ -56140,6 +56347,51 @@ stores.inject(MyMetaStore, storeInstance);
56140
56347
  const supportedPivotPositionalFormulaRegistry = new Registry();
56141
56348
  supportedPivotPositionalFormulaRegistry.add("SPREADSHEET", false);
56142
56349
 
56350
+ class FullScreenChart extends owl.Component {
56351
+ static template = "o-spreadsheet-FullScreenChart";
56352
+ static props = {};
56353
+ static components = { ChartDashboardMenu };
56354
+ fullScreenChartStore;
56355
+ ref = owl.useRef("fullScreenChart");
56356
+ spreadsheetRect = useSpreadsheetRect();
56357
+ figureRegistry = figureRegistry;
56358
+ setup() {
56359
+ this.fullScreenChartStore = useStore(FullScreenChartStore);
56360
+ const animationStore = useStore(ChartAnimationStore);
56361
+ let lastFigureId = undefined;
56362
+ owl.onWillUpdateProps(() => {
56363
+ if (lastFigureId !== this.figureUI?.id) {
56364
+ animationStore.enableAnimationForChart(this.figureUI?.id + "-fullscreen");
56365
+ }
56366
+ lastFigureId = this.figureUI?.id;
56367
+ });
56368
+ owl.useEffect((el) => el?.focus(), () => [this.ref.el]);
56369
+ }
56370
+ get figureUI() {
56371
+ return this.fullScreenChartStore.fullScreenFigure;
56372
+ }
56373
+ exitFullScreen() {
56374
+ if (this.figureUI) {
56375
+ this.fullScreenChartStore.toggleFullScreenChart(this.figureUI.id);
56376
+ }
56377
+ }
56378
+ onKeyDown(ev) {
56379
+ if (ev.key === "Escape") {
56380
+ this.exitFullScreen();
56381
+ }
56382
+ }
56383
+ get chartComponent() {
56384
+ if (!this.figureUI)
56385
+ return undefined;
56386
+ const type = this.env.model.getters.getChartType(this.figureUI.id);
56387
+ const component = chartComponentRegistry.get(type);
56388
+ if (!component) {
56389
+ throw new Error(`Component is not defined for type ${type}`);
56390
+ }
56391
+ return component;
56392
+ }
56393
+ }
56394
+
56143
56395
  css /* scss */ `
56144
56396
  .o_pivot_html_renderer {
56145
56397
  width: 100%;
@@ -56194,7 +56446,7 @@ stores.inject(MyMetaStore, storeInstance);
56194
56446
  showMissingValuesOnly: false,
56195
56447
  });
56196
56448
  setup() {
56197
- const table = this.pivot.getTableStructure();
56449
+ const table = this.pivot.getExpandedTableStructure();
56198
56450
  const formulaId = this.env.model.getters.getPivotFormulaId(this.props.pivotId);
56199
56451
  this.data = {
56200
56452
  columns: this._buildColHeaders(formulaId, table),
@@ -56238,7 +56490,7 @@ stores.inject(MyMetaStore, storeInstance);
56238
56490
  * The parent of "January" is "Australia"
56239
56491
  */
56240
56492
  addRecursiveRow(index) {
56241
- const rows = this.pivot.getTableStructure().rows;
56493
+ const rows = this.pivot.getExpandedTableStructure().rows;
56242
56494
  const row = [...rows[index].values];
56243
56495
  if (row.length <= 1) {
56244
56496
  return [index];
@@ -63647,10 +63899,9 @@ stores.inject(MyMetaStore, storeInstance);
63647
63899
  return this.evaluatedCells.keysForSheet(sheetId);
63648
63900
  }
63649
63901
  getArrayFormulaSpreadingOn(position) {
63650
- const hasArrayFormulaResult = this.getEvaluatedCell(position).type !== CellValueType.empty &&
63651
- !this.getters.getCell(position)?.isFormula;
63652
- if (!hasArrayFormulaResult) {
63653
- return this.spreadingRelations.isArrayFormula(position) ? position : undefined;
63902
+ const isEmpty = this.getEvaluatedCell(position).type === CellValueType.empty;
63903
+ if (isEmpty) {
63904
+ return undefined;
63654
63905
  }
63655
63906
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
63656
63907
  return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));
@@ -65052,6 +65303,323 @@ stores.inject(MyMetaStore, storeInstance);
65052
65303
  }
65053
65304
  }
65054
65305
 
65306
+ const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
65307
+ css /* scss */ `
65308
+ .o-dv-checkbox {
65309
+ margin: ${MARGIN}px;
65310
+ /* required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
65311
+ position: absolute;
65312
+ }
65313
+ `;
65314
+ class DataValidationCheckbox extends owl.Component {
65315
+ static template = "o-spreadsheet-DataValidationCheckbox";
65316
+ static components = {
65317
+ Checkbox,
65318
+ };
65319
+ static props = {
65320
+ cellPosition: Object,
65321
+ };
65322
+ onCheckboxChange(value) {
65323
+ const { sheetId, col, row } = this.props.cellPosition;
65324
+ const cellContent = value ? "TRUE" : "FALSE";
65325
+ this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
65326
+ }
65327
+ get checkBoxValue() {
65328
+ return !!this.env.model.getters.getEvaluatedCell(this.props.cellPosition).value;
65329
+ }
65330
+ get isDisabled() {
65331
+ const cell = this.env.model.getters.getCell(this.props.cellPosition);
65332
+ return this.env.model.getters.isReadonly() || !!cell?.isFormula;
65333
+ }
65334
+ }
65335
+
65336
+ const ICON_WIDTH = 13;
65337
+ css /* scss */ `
65338
+ .o-dv-list-icon {
65339
+ color: ${TEXT_BODY_MUTED};
65340
+ border-radius: 1px;
65341
+ height: ${GRID_ICON_EDGE_LENGTH}px;
65342
+ width: ${GRID_ICON_EDGE_LENGTH}px;
65343
+
65344
+ &:hover {
65345
+ color: #ffffff;
65346
+ background-color: ${TEXT_BODY_MUTED};
65347
+ }
65348
+
65349
+ svg {
65350
+ width: ${ICON_WIDTH}px;
65351
+ height: ${ICON_WIDTH}px;
65352
+ }
65353
+ }
65354
+ `;
65355
+ class DataValidationListIcon extends owl.Component {
65356
+ static template = "o-spreadsheet-DataValidationListIcon";
65357
+ static props = {
65358
+ cellPosition: Object,
65359
+ };
65360
+ onClick() {
65361
+ const { col, row } = this.props.cellPosition;
65362
+ this.env.model.selection.selectCell(col, row);
65363
+ this.env.startCellEdition();
65364
+ }
65365
+ }
65366
+
65367
+ css /* scss */ `
65368
+ .o-filter-icon {
65369
+ color: ${FILTERS_COLOR};
65370
+ display: flex;
65371
+ align-items: center;
65372
+ justify-content: center;
65373
+ width: ${GRID_ICON_EDGE_LENGTH}px;
65374
+ height: ${GRID_ICON_EDGE_LENGTH}px;
65375
+
65376
+ &:hover {
65377
+ background: ${FILTERS_COLOR};
65378
+ color: #fff;
65379
+ }
65380
+
65381
+ &.o-high-contrast {
65382
+ color: #defade;
65383
+ }
65384
+ &.o-high-contrast:hover {
65385
+ color: ${FILTERS_COLOR};
65386
+ background: #fff;
65387
+ }
65388
+ }
65389
+ .o-filter-icon:hover {
65390
+ background: ${FILTERS_COLOR};
65391
+ color: #fff;
65392
+ }
65393
+ `;
65394
+ class FilterIcon extends owl.Component {
65395
+ static template = "o-spreadsheet-FilterIcon";
65396
+ static props = {
65397
+ cellPosition: Object,
65398
+ };
65399
+ cellPopovers;
65400
+ setup() {
65401
+ this.cellPopovers = useStore(CellPopoverStore);
65402
+ }
65403
+ onClick() {
65404
+ const position = this.props.cellPosition;
65405
+ const activePopover = this.cellPopovers.persistentCellPopover;
65406
+ const { col, row } = position;
65407
+ if (activePopover.isOpen &&
65408
+ activePopover.col === col &&
65409
+ activePopover.row === row &&
65410
+ activePopover.type === "FilterMenu") {
65411
+ this.cellPopovers.close();
65412
+ return;
65413
+ }
65414
+ this.cellPopovers.open({ col, row }, "FilterMenu");
65415
+ }
65416
+ get isFilterActive() {
65417
+ return this.env.model.getters.isFilterActive(this.props.cellPosition);
65418
+ }
65419
+ get iconClass() {
65420
+ const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
65421
+ const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
65422
+ return luminance < 0.45 ? "o-high-contrast" : "";
65423
+ }
65424
+ }
65425
+
65426
+ css /* scss */ `
65427
+ .o-spreadsheet {
65428
+ .o-pivot-collapse-icon {
65429
+ cursor: pointer;
65430
+ width: 11px;
65431
+ height: 11px;
65432
+ border: 1px solid #777;
65433
+ background-color: #eee;
65434
+ margin: 3px 0 3px 6px;
65435
+
65436
+ .o-icon {
65437
+ width: 5px;
65438
+ height: 5px;
65439
+ }
65440
+ }
65441
+ }
65442
+ `;
65443
+ class PivotCollapseIcon extends owl.Component {
65444
+ static template = "o-spreadsheet-PivotCollapseIcon";
65445
+ static props = {
65446
+ cellPosition: Object,
65447
+ };
65448
+ onClick() {
65449
+ const pivotCell = this.env.model.getters.getPivotCellFromPosition(this.props.cellPosition);
65450
+ const pivotId = this.env.model.getters.getPivotIdFromPosition(this.props.cellPosition);
65451
+ if (!pivotId || pivotCell.type !== "HEADER") {
65452
+ return;
65453
+ }
65454
+ const definition = this.env.model.getters.getPivotCoreDefinition(pivotId);
65455
+ const collapsedDomains = definition.collapsedDomains?.[pivotCell.dimension]
65456
+ ? [...definition.collapsedDomains[pivotCell.dimension]]
65457
+ : [];
65458
+ const index = collapsedDomains.findIndex((domain) => deepEquals(domain, pivotCell.domain));
65459
+ if (index !== -1) {
65460
+ collapsedDomains.splice(index, 1);
65461
+ }
65462
+ else {
65463
+ collapsedDomains.push(pivotCell.domain);
65464
+ }
65465
+ const newDomains = definition.collapsedDomains
65466
+ ? { ...definition.collapsedDomains }
65467
+ : { COL: [], ROW: [] };
65468
+ newDomains[pivotCell.dimension] = collapsedDomains;
65469
+ this.env.model.dispatch("UPDATE_PIVOT", {
65470
+ pivotId,
65471
+ pivot: { ...definition, collapsedDomains: newDomains },
65472
+ });
65473
+ }
65474
+ get isCollapsed() {
65475
+ const pivotCell = this.env.model.getters.getPivotCellFromPosition(this.props.cellPosition);
65476
+ const pivotId = this.env.model.getters.getPivotIdFromPosition(this.props.cellPosition);
65477
+ if (!pivotId || pivotCell.type !== "HEADER") {
65478
+ return false;
65479
+ }
65480
+ const definition = this.env.model.getters.getPivotCoreDefinition(pivotId);
65481
+ const domains = definition.collapsedDomains?.[pivotCell.dimension] ?? [];
65482
+ return domains?.some((domain) => deepEquals(domain, pivotCell.domain));
65483
+ }
65484
+ }
65485
+
65486
+ /**
65487
+ * Registry to draw icons on cells
65488
+ */
65489
+ const iconsOnCellRegistry = new Registry();
65490
+ iconsOnCellRegistry.add("data_validation_checkbox", (getters, position) => {
65491
+ const hasIcon = getters.isCellValidCheckbox(position);
65492
+ if (hasIcon) {
65493
+ return {
65494
+ svg: undefined,
65495
+ priority: 2,
65496
+ horizontalAlign: "center",
65497
+ size: GRID_ICON_EDGE_LENGTH,
65498
+ margin: GRID_ICON_MARGIN,
65499
+ component: DataValidationCheckbox,
65500
+ position,
65501
+ };
65502
+ }
65503
+ return undefined;
65504
+ });
65505
+ iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
65506
+ const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
65507
+ if (hasIcon) {
65508
+ return {
65509
+ svg: undefined,
65510
+ priority: 2,
65511
+ horizontalAlign: "right",
65512
+ size: GRID_ICON_EDGE_LENGTH,
65513
+ margin: GRID_ICON_MARGIN,
65514
+ component: DataValidationListIcon,
65515
+ position,
65516
+ };
65517
+ }
65518
+ return undefined;
65519
+ });
65520
+ iconsOnCellRegistry.add("filter_icon", (getters, position) => {
65521
+ const hasIcon = getters.isFilterHeader(position);
65522
+ if (hasIcon) {
65523
+ return {
65524
+ svg: undefined,
65525
+ priority: 3,
65526
+ horizontalAlign: "right",
65527
+ size: GRID_ICON_EDGE_LENGTH,
65528
+ margin: GRID_ICON_MARGIN,
65529
+ component: FilterIcon,
65530
+ position,
65531
+ };
65532
+ }
65533
+ return undefined;
65534
+ });
65535
+ iconsOnCellRegistry.add("conditional_formatting", (getters, position) => {
65536
+ const icon = getters.getConditionalIcon(position);
65537
+ if (icon) {
65538
+ const style = getters.getCellStyle(position);
65539
+ return {
65540
+ svg: ICONS[icon].svg,
65541
+ priority: 1,
65542
+ horizontalAlign: "left",
65543
+ size: computeTextFontSizeInPixels(style),
65544
+ margin: MIN_CF_ICON_MARGIN,
65545
+ position,
65546
+ };
65547
+ }
65548
+ return undefined;
65549
+ });
65550
+ iconsOnCellRegistry.add("pivot_collapse", (getters, position) => {
65551
+ if (!getters.isSpillPivotFormula(position)) {
65552
+ return undefined;
65553
+ }
65554
+ const pivotCell = getters.getPivotCellFromPosition(position);
65555
+ const pivotId = getters.getPivotIdFromPosition(position);
65556
+ if (pivotCell.type === "HEADER" && pivotId && pivotCell.domain.length) {
65557
+ const definition = getters.getPivotCoreDefinition(pivotId);
65558
+ const isDashboard = getters.isDashboard();
65559
+ const fields = pivotCell.dimension === "COL" ? definition.columns : definition.rows;
65560
+ const component = !isDashboard && pivotCell.domain.length !== fields.length ? PivotCollapseIcon : undefined;
65561
+ return {
65562
+ priority: 4,
65563
+ horizontalAlign: "left",
65564
+ size: !!component || (!isDashboard && pivotCell.dimension === "ROW" && definition.rows.length > 1)
65565
+ ? GRID_ICON_EDGE_LENGTH
65566
+ : 0,
65567
+ margin: pivotCell.dimension === "ROW" ? (pivotCell.domain.length - 1) * PIVOT_INDENT : 0,
65568
+ component,
65569
+ position,
65570
+ };
65571
+ }
65572
+ return undefined;
65573
+ });
65574
+
65575
+ class CellIconPlugin extends CoreViewPlugin {
65576
+ static getters = ["doesCellHaveGridIcon", "getCellIcons"];
65577
+ cellIconsCache = {};
65578
+ handle(cmd) {
65579
+ if (cmd.type !== "SET_VIEWPORT_OFFSET") {
65580
+ this.cellIconsCache = {};
65581
+ }
65582
+ }
65583
+ getCellIcons(position) {
65584
+ if (!this.cellIconsCache[position.sheetId]) {
65585
+ this.cellIconsCache[position.sheetId] = {};
65586
+ }
65587
+ if (!this.cellIconsCache[position.sheetId][position.col]) {
65588
+ this.cellIconsCache[position.sheetId][position.col] = {};
65589
+ }
65590
+ if (!this.cellIconsCache[position.sheetId][position.col][position.row]) {
65591
+ this.cellIconsCache[position.sheetId][position.col][position.row] =
65592
+ this.computeCellIcons(position);
65593
+ }
65594
+ return this.cellIconsCache[position.sheetId][position.col][position.row];
65595
+ }
65596
+ computeCellIcons(position) {
65597
+ const icons = { left: undefined, right: undefined, center: undefined };
65598
+ const callbacks = iconsOnCellRegistry.getAll();
65599
+ for (const callback of callbacks) {
65600
+ const icon = callback(this.getters, position);
65601
+ if (icon &&
65602
+ (!icons[icon.horizontalAlign] || icon.priority > icons[icon.horizontalAlign].priority)) {
65603
+ icons[icon.horizontalAlign] = icon;
65604
+ }
65605
+ }
65606
+ if (icons.center && (icons.left || icons.right)) {
65607
+ const sideIconsPriority = Math.max(icons.left?.priority || 0, icons.right?.priority || 0);
65608
+ if (icons.center.priority < sideIconsPriority) {
65609
+ icons.center = undefined;
65610
+ }
65611
+ else {
65612
+ icons.left = undefined;
65613
+ icons.right = undefined;
65614
+ }
65615
+ }
65616
+ return Object.values(icons).filter(isDefined);
65617
+ }
65618
+ doesCellHaveGridIcon(position) {
65619
+ return Boolean(this.getCellIcons(position).length);
65620
+ }
65621
+ }
65622
+
65055
65623
  class DynamicTablesPlugin extends CoreViewPlugin {
65056
65624
  static getters = [
65057
65625
  "canCreateDynamicTableOnZones",
@@ -65491,7 +66059,7 @@ stores.inject(MyMetaStore, storeInstance);
65491
66059
  }
65492
66060
  getValuesToAggregate(measure, domain) {
65493
66061
  const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
65494
- const table = super.getTableStructure();
66062
+ const table = super.getExpandedTableStructure();
65495
66063
  const values = [];
65496
66064
  if (colDomain.length === 0 &&
65497
66065
  rowDomain.length < this.definition.rows.length &&
@@ -65911,7 +66479,7 @@ stores.inject(MyMetaStore, storeInstance);
65911
66479
  return this.strictMeasureValueToNumber(comparedValue);
65912
66480
  }
65913
66481
  getPivotValueCells(measureId) {
65914
- return this.getTableStructure()
66482
+ return this.getCollapsedTableStructure()
65915
66483
  .getPivotCells()
65916
66484
  .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
65917
66485
  .filter((col) => col.length > 0);
@@ -65935,8 +66503,13 @@ stores.inject(MyMetaStore, storeInstance);
65935
66503
  }
65936
66504
  throw new Error(`Value ${result.value} is not a number`);
65937
66505
  }
65938
- getTableStructure() {
65939
- const table = super.getTableStructure();
66506
+ getCollapsedTableStructure() {
66507
+ const table = super.getCollapsedTableStructure();
66508
+ this.sortTableStructure(table);
66509
+ return table;
66510
+ }
66511
+ getExpandedTableStructure() {
66512
+ const table = super.getExpandedTableStructure();
65940
66513
  this.sortTableStructure(table);
65941
66514
  return table;
65942
66515
  }
@@ -66126,7 +66699,7 @@ stores.inject(MyMetaStore, storeInstance);
66126
66699
  const includeColumnHeaders = toScalar(args[3]);
66127
66700
  const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);
66128
66701
  const pivotCells = pivot
66129
- .getTableStructure()
66702
+ .getCollapsedTableStructure()
66130
66703
  .getPivotCells(shouldIncludeTotal, shouldIncludeColumnHeaders);
66131
66704
  const pivotCol = position.col - mainPosition.col;
66132
66705
  const pivotRow = position.row - mainPosition.row;
@@ -66143,9 +66716,11 @@ stores.inject(MyMetaStore, storeInstance);
66143
66716
  }
66144
66717
  else if (functionName === "PIVOT.HEADER") {
66145
66718
  const domain = pivot.parseArgsToPivotDomain(args.slice(1).map((value) => ({ value })));
66719
+ const colRowDomain = domainToColRowDomain(pivot, domain);
66146
66720
  return {
66147
66721
  type: "HEADER",
66148
66722
  domain,
66723
+ dimension: colRowDomain.colDomain.length ? "COL" : "ROW",
66149
66724
  };
66150
66725
  }
66151
66726
  const [measure, ...domainArgs] = args.slice(1);
@@ -68145,8 +68720,11 @@ stores.inject(MyMetaStore, storeInstance);
68145
68720
  version: MESSAGE_VERSION,
68146
68721
  });
68147
68722
  }
68148
- getClient() {
68149
- const client = this.clients[this.clientId];
68723
+ getCurrentClient() {
68724
+ return this.getClient(this.clientId);
68725
+ }
68726
+ getClient(clientId) {
68727
+ const client = this.clients[clientId];
68150
68728
  if (!client) {
68151
68729
  throw new ClientDisconnectedError("The client left the session");
68152
68730
  }
@@ -68180,7 +68758,7 @@ stores.inject(MyMetaStore, storeInstance);
68180
68758
  return;
68181
68759
  }
68182
68760
  const type = currentPosition ? "CLIENT_MOVED" : "CLIENT_JOINED";
68183
- const client = this.getClient();
68761
+ const client = this.getCurrentClient();
68184
68762
  this.clients[this.clientId] = { ...client, position };
68185
68763
  this.transportService.sendMessage({
68186
68764
  type,
@@ -68391,6 +68969,7 @@ stores.inject(MyMetaStore, storeInstance);
68391
68969
  static getters = [
68392
68970
  "getClientsToDisplay",
68393
68971
  "getClient",
68972
+ "getCurrentClient",
68394
68973
  "getConnectedClients",
68395
68974
  "isFullySynchronized",
68396
68975
  ];
@@ -68406,11 +68985,16 @@ stores.inject(MyMetaStore, storeInstance);
68406
68985
  return (position.row < this.getters.getNumberRows(position.sheetId) &&
68407
68986
  position.col < this.getters.getNumberCols(position.sheetId));
68408
68987
  }
68409
- getClient() {
68410
- return this.session.getClient();
68988
+ getClient(clientId) {
68989
+ return this.session.getClient(clientId);
68990
+ }
68991
+ getCurrentClient() {
68992
+ return this.session.getCurrentClient();
68411
68993
  }
68412
68994
  getConnectedClients() {
68413
- return this.session.getConnectedClients();
68995
+ return [...this.session.getConnectedClients()].map((client) => {
68996
+ return { ...client, color: this.colors[client.id] };
68997
+ });
68414
68998
  }
68415
68999
  isFullySynchronized() {
68416
69000
  return this.session.isFullySynchronized();
@@ -68421,7 +69005,7 @@ stores.inject(MyMetaStore, storeInstance);
68421
69005
  */
68422
69006
  getClientsToDisplay() {
68423
69007
  try {
68424
- this.getters.getClient();
69008
+ this.getters.getCurrentClient();
68425
69009
  }
68426
69010
  catch (e) {
68427
69011
  if (e instanceof ClientDisconnectedError) {
@@ -68434,16 +69018,14 @@ stores.inject(MyMetaStore, storeInstance);
68434
69018
  const sheetId = this.getters.getActiveSheetId();
68435
69019
  const clients = [];
68436
69020
  for (const client of this.getters.getConnectedClients()) {
68437
- if (client.id !== this.getters.getClient().id &&
69021
+ if (client.id !== this.getters.getCurrentClient().id &&
68438
69022
  client.position &&
68439
69023
  client.position.sheetId === sheetId &&
68440
69024
  this.isPositionValid(client.position)) {
68441
- const position = client.position;
68442
69025
  if (!this.colors[client.id]) {
68443
69026
  this.colors[client.id] = this.availableColors.next();
68444
69027
  }
68445
- const color = this.colors[client.id];
68446
- clients.push({ ...client, position, color });
69028
+ clients.push({ ...client, color: this.colors[client.id], position: client.position });
68447
69029
  }
68448
69030
  }
68449
69031
  return clients;
@@ -68969,7 +69551,7 @@ stores.inject(MyMetaStore, storeInstance);
68969
69551
  sheetIdTo: sheetId,
68970
69552
  });
68971
69553
  const pivot = this.getters.getPivot(pivotId);
68972
- this.insertPivotWithTable(sheetId, 0, 0, pivotId, pivot.getTableStructure().export(), "dynamic");
69554
+ this.insertPivotWithTable(sheetId, 0, 0, pivotId, pivot.getCollapsedTableStructure().export(), "dynamic");
68973
69555
  }
68974
69556
  duplicatePivotInNewSheet(pivotId, newPivotId, newSheetId) {
68975
69557
  this.dispatch("DUPLICATE_PIVOT", {
@@ -68992,7 +69574,7 @@ stores.inject(MyMetaStore, storeInstance);
68992
69574
  if (result.isSuccessful) {
68993
69575
  this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
68994
69576
  const pivot = this.getters.getPivot(pivotId);
68995
- this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), "dynamic");
69577
+ this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getCollapsedTableStructure().export(), "dynamic");
68996
69578
  }
68997
69579
  }
68998
69580
  getPivotDuplicateSheetName(pivotName) {
@@ -69284,9 +69866,7 @@ stores.inject(MyMetaStore, storeInstance);
69284
69866
 
69285
69867
  class SheetUIPlugin extends UIPlugin {
69286
69868
  static getters = [
69287
- "doesCellHaveGridIcon",
69288
69869
  "getCellWidth",
69289
- "getCellIconSvg",
69290
69870
  "getTextWidth",
69291
69871
  "getCellText",
69292
69872
  "getCellMultiLineText",
@@ -69341,12 +69921,8 @@ stores.inject(MyMetaStore, storeInstance);
69341
69921
  const multiLineText = splitTextToWidth(this.ctx, content, style, undefined);
69342
69922
  contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style)));
69343
69923
  }
69344
- const icon = this.getters.getCellIconSvg(position);
69345
- if (icon) {
69346
- contentWidth += computeIconWidth(style);
69347
- }
69348
- if (this.getters.doesCellHaveGridIcon(position)) {
69349
- contentWidth += ICON_EDGE_LENGTH + GRID_ICON_MARGIN;
69924
+ for (const icon of this.getters.getCellIcons(position)) {
69925
+ contentWidth += icon.margin + icon.size;
69350
69926
  }
69351
69927
  if (contentWidth === 0) {
69352
69928
  return 0;
@@ -69358,16 +69934,6 @@ stores.inject(MyMetaStore, storeInstance);
69358
69934
  }
69359
69935
  return contentWidth;
69360
69936
  }
69361
- getCellIconSvg(position) {
69362
- const callbacks = iconsOnCellRegistry.getAll();
69363
- for (const callback of callbacks) {
69364
- const imageSrc = callback(this.getters, position);
69365
- if (imageSrc) {
69366
- return imageSrc;
69367
- }
69368
- }
69369
- return undefined;
69370
- }
69371
69937
  getTextWidth(text, style) {
69372
69938
  return computeTextWidth(this.ctx, text, style);
69373
69939
  }
@@ -69407,11 +69973,6 @@ stores.inject(MyMetaStore, storeInstance);
69407
69973
  });
69408
69974
  return splitTextToWidth(this.ctx, text, style, args.wrapText ? args.maxWidth : undefined);
69409
69975
  }
69410
- doesCellHaveGridIcon(position) {
69411
- const isFilterHeader = this.getters.isFilterHeader(position);
69412
- const hasListIcon = !this.getters.isReadonly() && this.getters.cellHasListDataValidationIcon(position);
69413
- return isFilterHeader || hasListIcon;
69414
- }
69415
69976
  /**
69416
69977
  * Expands the given zone until bordered by empty cells or reached the sheet boundaries.
69417
69978
  */
@@ -73303,7 +73864,8 @@ stores.inject(MyMetaStore, storeInstance);
73303
73864
  .add("data_validation_ui", EvaluationDataValidationPlugin)
73304
73865
  .add("dynamic_tables", DynamicTablesPlugin)
73305
73866
  .add("custom_colors", CustomColorsPlugin)
73306
- .add("pivot_ui", PivotUIPlugin);
73867
+ .add("pivot_ui", PivotUIPlugin)
73868
+ .add("cell_icon", CellIconPlugin);
73307
73869
 
73308
73870
  autoCompleteProviders.add("dataValidation", {
73309
73871
  displayAllOnInitialContent: true,
@@ -74453,6 +75015,21 @@ stores.inject(MyMetaStore, storeInstance);
74453
75015
  }
74454
75016
  const topbarComponentRegistry = new TopBarComponentRegistry();
74455
75017
 
75018
+ class LocalTransportService {
75019
+ listeners = [];
75020
+ async sendMessage(message) {
75021
+ for (const { callback } of this.listeners) {
75022
+ callback(message);
75023
+ }
75024
+ }
75025
+ onNewMessage(id, callback) {
75026
+ this.listeners.push({ id, callback });
75027
+ }
75028
+ leave(id) {
75029
+ this.listeners = this.listeners.filter((listener) => listener.id !== id);
75030
+ }
75031
+ }
75032
+
74456
75033
  class ImageProvider {
74457
75034
  fileStore;
74458
75035
  constructor(fileStore) {
@@ -77290,6 +77867,7 @@ stores.inject(MyMetaStore, storeInstance);
77290
77867
  SidePanel,
77291
77868
  SpreadsheetDashboard,
77292
77869
  HeaderGroupContainer,
77870
+ FullScreenChart,
77293
77871
  };
77294
77872
  sidePanel;
77295
77873
  spreadsheetRef = owl.useRef("spreadsheet");
@@ -77448,21 +78026,6 @@ stores.inject(MyMetaStore, storeInstance);
77448
78026
  }
77449
78027
  }
77450
78028
 
77451
- class LocalTransportService {
77452
- listeners = [];
77453
- async sendMessage(message) {
77454
- for (const { callback } of this.listeners) {
77455
- callback(message);
77456
- }
77457
- }
77458
- onNewMessage(id, callback) {
77459
- this.listeners.push({ id, callback });
77460
- }
77461
- leave(id) {
77462
- this.listeners = this.listeners.filter((listener) => listener.id !== id);
77463
- }
77464
- }
77465
-
77466
78029
  function inverseCommand(cmd) {
77467
78030
  return inverseCommandRegistry.get(cmd.type)(cmd);
77468
78031
  }
@@ -81886,6 +82449,7 @@ stores.inject(MyMetaStore, storeInstance);
81886
82449
  RadioSelection,
81887
82450
  GeoChartRegionSelectSection,
81888
82451
  ChartDashboardMenu,
82452
+ FullScreenChart,
81889
82453
  };
81890
82454
  const hooks = {
81891
82455
  useDragAndDropListItems,
@@ -81912,6 +82476,7 @@ stores.inject(MyMetaStore, storeInstance);
81912
82476
  SidePanelStore,
81913
82477
  PivotSidePanelStore,
81914
82478
  PivotMeasureDisplayPanelStore,
82479
+ ClientFocusStore,
81915
82480
  };
81916
82481
  function addFunction(functionName, functionDescription) {
81917
82482
  functionRegistry.add(functionName, functionDescription);
@@ -81935,6 +82500,7 @@ stores.inject(MyMetaStore, storeInstance);
81935
82500
  exports.CoreViewPlugin = CoreViewPlugin;
81936
82501
  exports.DispatchResult = DispatchResult;
81937
82502
  exports.EvaluationError = EvaluationError;
82503
+ exports.LocalTransportService = LocalTransportService;
81938
82504
  exports.Model = Model;
81939
82505
  exports.PivotRuntimeDefinition = PivotRuntimeDefinition;
81940
82506
  exports.Registry = Registry;
@@ -81976,9 +82542,9 @@ stores.inject(MyMetaStore, storeInstance);
81976
82542
  exports.tokenize = tokenize;
81977
82543
 
81978
82544
 
81979
- __info__.version = "18.4.0-alpha.3";
81980
- __info__.date = "2025-05-13T17:54:54.061Z";
81981
- __info__.hash = "70ad365";
82545
+ __info__.version = "18.4.0-alpha.4";
82546
+ __info__.date = "2025-05-20T05:57:45.452Z";
82547
+ __info__.hash = "5c28bca";
81982
82548
 
81983
82549
 
81984
82550
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);