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