@odoo/o-spreadsheet 18.0.0 → 18.0.2

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,13 +2,119 @@
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.0.0
6
- * @date 2024-09-25T12:54:19.974Z
7
- * @hash cee2e47
5
+ * @version 18.0.2
6
+ * @date 2024-10-24T08:54:21.934Z
7
+ * @hash 788df92
8
8
  */
9
9
 
10
10
  import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, useState, onPatched, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
11
11
 
12
+ function createActions(menuItems) {
13
+ return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
14
+ }
15
+ let nextItemId = 1;
16
+ function createAction(item) {
17
+ const name = item.name;
18
+ const children = item.children;
19
+ const description = item.description;
20
+ const icon = item.icon;
21
+ const secondaryIcon = item.secondaryIcon;
22
+ const itemId = item.id || nextItemId++;
23
+ return {
24
+ id: itemId.toString(),
25
+ name: typeof name === "function" ? name : () => name,
26
+ isVisible: item.isVisible ? item.isVisible : () => true,
27
+ isEnabled: item.isEnabled ? item.isEnabled : () => true,
28
+ isActive: item.isActive,
29
+ execute: item.execute,
30
+ children: children
31
+ ? (env) => {
32
+ return children
33
+ .map((child) => (typeof child === "function" ? child(env) : child))
34
+ .flat()
35
+ .map(createAction);
36
+ }
37
+ : () => [],
38
+ isReadonlyAllowed: item.isReadonlyAllowed || false,
39
+ separator: item.separator || false,
40
+ icon: typeof icon === "function" ? icon : () => icon || "",
41
+ iconColor: item.iconColor,
42
+ secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
43
+ description: typeof description === "function" ? description : () => description || "",
44
+ textColor: item.textColor,
45
+ sequence: item.sequence || 0,
46
+ onStartHover: item.onStartHover,
47
+ onStopHover: item.onStopHover,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Registry
53
+ *
54
+ * The Registry class is basically just a mapping from a string key to an object.
55
+ * It is really not much more than an object. It is however useful for the
56
+ * following reasons:
57
+ *
58
+ * 1. it let us react and execute code when someone add something to the registry
59
+ * (for example, the FunctionRegistry subclass this for this purpose)
60
+ * 2. it throws an error when the get operation fails
61
+ * 3. it provides a chained API to add items to the registry.
62
+ */
63
+ class Registry {
64
+ content = {};
65
+ /**
66
+ * Add an item to the registry
67
+ *
68
+ * Note that this also returns the registry, so another add method call can
69
+ * be chained
70
+ */
71
+ add(key, value) {
72
+ this.content[key] = value;
73
+ return this;
74
+ }
75
+ /**
76
+ * Get an item from the registry
77
+ */
78
+ get(key) {
79
+ /**
80
+ * Note: key in {} is ~12 times slower than {}[key].
81
+ * So, we check the absence of key only when the direct access returns
82
+ * a falsy value. It's done to ensure that the registry can contains falsy values
83
+ */
84
+ const content = this.content[key];
85
+ if (!content) {
86
+ if (!(key in this.content)) {
87
+ throw new Error(`Cannot find ${key} in this registry!`);
88
+ }
89
+ }
90
+ return content;
91
+ }
92
+ /**
93
+ * Check if the key is already in the registry
94
+ */
95
+ contains(key) {
96
+ return key in this.content;
97
+ }
98
+ /**
99
+ * Get a list of all elements in the registry
100
+ */
101
+ getAll() {
102
+ return Object.values(this.content);
103
+ }
104
+ /**
105
+ * Get a list of all keys in the registry
106
+ */
107
+ getKeys() {
108
+ return Object.keys(this.content);
109
+ }
110
+ /**
111
+ * Remove an item from the registry
112
+ */
113
+ remove(key) {
114
+ delete this.content[key];
115
+ }
116
+ }
117
+
12
118
  const CANVAS_SHIFT = 0.5;
13
119
  // Colors
14
120
  const HIGHLIGHT_COLOR = "#37A850";
@@ -268,7 +374,7 @@ const PIVOT_TABLE_CONFIG = {
268
374
  bandedRows: true,
269
375
  bandedColumns: false,
270
376
  styleId: "TableStyleMedium5",
271
- automaticAutofill: true,
377
+ automaticAutofill: false,
272
378
  };
273
379
  const DEFAULT_CURRENCY = {
274
380
  symbol: "$",
@@ -5993,111 +6099,6 @@ class UuidGenerator {
5993
6099
  }
5994
6100
  }
5995
6101
 
5996
- function createActions(menuItems) {
5997
- return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
5998
- }
5999
- const uuidGenerator$1 = new UuidGenerator();
6000
- function createAction(item) {
6001
- const name = item.name;
6002
- const children = item.children;
6003
- const description = item.description;
6004
- const icon = item.icon;
6005
- const secondaryIcon = item.secondaryIcon;
6006
- return {
6007
- id: item.id || uuidGenerator$1.uuidv4(),
6008
- name: typeof name === "function" ? name : () => name,
6009
- isVisible: item.isVisible ? item.isVisible : () => true,
6010
- isEnabled: item.isEnabled ? item.isEnabled : () => true,
6011
- isActive: item.isActive,
6012
- execute: item.execute,
6013
- children: children
6014
- ? (env) => {
6015
- return children
6016
- .map((child) => (typeof child === "function" ? child(env) : child))
6017
- .flat()
6018
- .map(createAction);
6019
- }
6020
- : () => [],
6021
- isReadonlyAllowed: item.isReadonlyAllowed || false,
6022
- separator: item.separator || false,
6023
- icon: typeof icon === "function" ? icon : () => icon || "",
6024
- iconColor: item.iconColor,
6025
- secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
6026
- description: typeof description === "function" ? description : () => description || "",
6027
- textColor: item.textColor,
6028
- sequence: item.sequence || 0,
6029
- onStartHover: item.onStartHover,
6030
- onStopHover: item.onStopHover,
6031
- };
6032
- }
6033
-
6034
- /**
6035
- * Registry
6036
- *
6037
- * The Registry class is basically just a mapping from a string key to an object.
6038
- * It is really not much more than an object. It is however useful for the
6039
- * following reasons:
6040
- *
6041
- * 1. it let us react and execute code when someone add something to the registry
6042
- * (for example, the FunctionRegistry subclass this for this purpose)
6043
- * 2. it throws an error when the get operation fails
6044
- * 3. it provides a chained API to add items to the registry.
6045
- */
6046
- class Registry {
6047
- content = {};
6048
- /**
6049
- * Add an item to the registry
6050
- *
6051
- * Note that this also returns the registry, so another add method call can
6052
- * be chained
6053
- */
6054
- add(key, value) {
6055
- this.content[key] = value;
6056
- return this;
6057
- }
6058
- /**
6059
- * Get an item from the registry
6060
- */
6061
- get(key) {
6062
- /**
6063
- * Note: key in {} is ~12 times slower than {}[key].
6064
- * So, we check the absence of key only when the direct access returns
6065
- * a falsy value. It's done to ensure that the registry can contains falsy values
6066
- */
6067
- const content = this.content[key];
6068
- if (!content) {
6069
- if (!(key in this.content)) {
6070
- throw new Error(`Cannot find ${key} in this registry!`);
6071
- }
6072
- }
6073
- return content;
6074
- }
6075
- /**
6076
- * Check if the key is already in the registry
6077
- */
6078
- contains(key) {
6079
- return key in this.content;
6080
- }
6081
- /**
6082
- * Get a list of all elements in the registry
6083
- */
6084
- getAll() {
6085
- return Object.values(this.content);
6086
- }
6087
- /**
6088
- * Get a list of all keys in the registry
6089
- */
6090
- getKeys() {
6091
- return Object.keys(this.content);
6092
- }
6093
- /**
6094
- * Remove an item from the registry
6095
- */
6096
- remove(key) {
6097
- delete this.content[key];
6098
- }
6099
- }
6100
-
6101
6102
  function getClipboardDataPositions(sheetId, zones) {
6102
6103
  const lefts = new Set(zones.map((z) => z.left));
6103
6104
  const rights = new Set(zones.map((z) => z.right));
@@ -8452,31 +8453,34 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8452
8453
  for (let col of columnsIndexes) {
8453
8454
  const position = { col, row, sheetId };
8454
8455
  const table = this.getters.getTable(position);
8455
- if (!table || copiedTablesIds.has(table.id)) {
8456
+ if (!table) {
8456
8457
  tableCellsInRow.push({});
8457
8458
  continue;
8458
8459
  }
8459
8460
  const coreTable = this.getters.getCoreTable(position);
8460
8461
  const tableZone = coreTable?.range.zone;
8462
+ let copiedTable = undefined;
8461
8463
  // Copy whole table
8462
- if (coreTable && tableZone && zones.some((z) => isZoneInside(tableZone, z))) {
8463
- copiedTablesIds.add(coreTable.id);
8464
+ if (!copiedTablesIds.has(table.id) &&
8465
+ coreTable &&
8466
+ tableZone &&
8467
+ zones.some((z) => isZoneInside(tableZone, z))) {
8468
+ copiedTablesIds.add(table.id);
8464
8469
  const values = [];
8465
8470
  for (const col of range(tableZone.left, tableZone.right + 1)) {
8466
8471
  values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));
8467
8472
  }
8468
- tableCellsInRow.push({
8469
- table: {
8470
- range: coreTable.range.rangeData,
8471
- config: coreTable.config,
8472
- type: coreTable.type,
8473
- },
8474
- });
8475
- }
8476
- // Copy only style of cell
8477
- else if (table) {
8478
- tableCellsInRow.push({ style: this.getTableStyleToCopy(position) });
8473
+ copiedTable = {
8474
+ range: coreTable.range.rangeData,
8475
+ config: coreTable.config,
8476
+ type: coreTable.type,
8477
+ };
8479
8478
  }
8479
+ tableCellsInRow.push({
8480
+ table: copiedTable,
8481
+ style: this.getTableStyleToCopy(position),
8482
+ isWholeTableCopied: copiedTablesIds.has(table.id),
8483
+ });
8480
8484
  }
8481
8485
  }
8482
8486
  return {
@@ -8557,11 +8561,14 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8557
8561
  tableType: tableCell.table.type,
8558
8562
  });
8559
8563
  }
8560
- // Do not paste table style if we're inside another table
8561
8564
  // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
8562
8565
  // dynamic tables are not yet computed
8563
- if (!this.getters.getCoreTable(position)) {
8564
- if (tableCell.style?.style && options?.pasteOption !== "asValue") {
8566
+ if (this.getters.getCoreTable(position) || options?.pasteOption === "asValue") {
8567
+ return;
8568
+ }
8569
+ if ((!options?.pasteOption && !tableCell.isWholeTableCopied) ||
8570
+ options?.pasteOption === "onlyFormat") {
8571
+ if (tableCell.style?.style) {
8565
8572
  this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
8566
8573
  }
8567
8574
  if (tableCell.style?.border) {
@@ -9402,70 +9409,114 @@ const chartShowValuesPlugin = {
9402
9409
  ctx.save();
9403
9410
  ctx.textAlign = "center";
9404
9411
  ctx.textBaseline = "middle";
9405
- ctx.fillStyle = chartFontColor(options.background);
9406
- ctx.strokeStyle = chartFontColor(ctx.fillStyle);
9407
- chart._metasets.forEach(function (dataset) {
9408
- if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9409
- return; // ignore trend lines
9410
- }
9411
- switch (dataset.type) {
9412
- case "doughnut":
9413
- case "pie": {
9414
- for (let i = 0; i < dataset._parsed.length; i++) {
9415
- const bar = dataset.data[i];
9416
- const { startAngle, endAngle, innerRadius, outerRadius } = bar;
9417
- const midAngle = (startAngle + endAngle) / 2;
9418
- const midRadius = (innerRadius + outerRadius) / 2;
9419
- const x = bar.x + midRadius * Math.cos(midAngle);
9420
- const y = bar.y + midRadius * Math.sin(midAngle) + 7;
9421
- ctx.fillStyle = chartFontColor(bar.options.backgroundColor);
9422
- ctx.strokeStyle = chartFontColor(ctx.fillStyle);
9423
- const value = options.callback(dataset._parsed[i]);
9424
- ctx.strokeText(value, x, y);
9425
- ctx.fillText(value, x, y);
9426
- }
9427
- break;
9428
- }
9429
- case "bar":
9430
- case "line": {
9431
- const yOffset = dataset.type === "bar" && !options.horizontal ? 0 : 3;
9432
- for (let i = 0; i < dataset._parsed.length; i++) {
9433
- const point = dataset.data[i];
9434
- const value = options.horizontal ? dataset._parsed[i].x : dataset._parsed[i].y;
9435
- const displayedValue = options.callback(value - 0);
9436
- let xPosition = 0, yPosition = 0;
9437
- if (options.horizontal) {
9438
- yPosition = point.y;
9439
- if (value < 0) {
9440
- ctx.textAlign = "right";
9441
- xPosition = point.x - yOffset;
9442
- }
9443
- else {
9444
- ctx.textAlign = "left";
9445
- xPosition = point.x + yOffset;
9446
- }
9447
- }
9448
- else {
9449
- xPosition = point.x;
9450
- if (value < 0) {
9451
- ctx.textBaseline = "top";
9452
- yPosition = point.y + yOffset;
9453
- }
9454
- else {
9455
- ctx.textBaseline = "bottom";
9456
- yPosition = point.y - yOffset;
9457
- }
9458
- }
9459
- ctx.strokeText(displayedValue, xPosition, yPosition);
9460
- ctx.fillText(displayedValue, xPosition, yPosition);
9461
- }
9462
- break;
9463
- }
9464
- }
9465
- });
9412
+ ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
9413
+ switch (chart.config.type) {
9414
+ case "pie":
9415
+ case "doughnut":
9416
+ drawPieChartValues(chart, options, ctx);
9417
+ break;
9418
+ case "bar":
9419
+ case "line":
9420
+ options.horizontal
9421
+ ? drawHorizontalBarChartValues(chart, options, ctx)
9422
+ : drawLineOrBarChartValues(chart, options, ctx);
9423
+ break;
9424
+ }
9466
9425
  ctx.restore();
9467
9426
  },
9468
9427
  };
9428
+ function drawTextWithBackground(text, x, y, ctx) {
9429
+ ctx.lineWidth = 3; // Stroke the text with a big lineWidth width to have some kind of background
9430
+ ctx.strokeText(text, x, y);
9431
+ ctx.lineWidth = 1;
9432
+ ctx.fillText(text, x, y);
9433
+ }
9434
+ function drawLineOrBarChartValues(chart, options, ctx) {
9435
+ const yMax = chart.chartArea.bottom;
9436
+ const yMin = chart.chartArea.top;
9437
+ const textsPositions = {};
9438
+ for (const dataset of chart._metasets) {
9439
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9440
+ return; // ignore trend lines
9441
+ }
9442
+ for (let i = 0; i < dataset._parsed.length; i++) {
9443
+ const value = dataset._parsed[i].y;
9444
+ const point = dataset.data[i];
9445
+ const xPosition = point.x;
9446
+ let yPosition = 0;
9447
+ if (chart.config.type === "line") {
9448
+ yPosition = point.y - 10;
9449
+ }
9450
+ else {
9451
+ yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
9452
+ }
9453
+ yPosition = Math.min(yPosition, yMax);
9454
+ yPosition = Math.max(yPosition, yMin);
9455
+ // Avoid overlapping texts with same X
9456
+ if (!textsPositions[xPosition]) {
9457
+ textsPositions[xPosition] = [];
9458
+ }
9459
+ for (const otherPosition of textsPositions[xPosition] || []) {
9460
+ if (Math.abs(otherPosition - yPosition) < 13) {
9461
+ yPosition = otherPosition - 13;
9462
+ }
9463
+ }
9464
+ textsPositions[xPosition].push(yPosition);
9465
+ ctx.fillStyle = point.options.backgroundColor;
9466
+ ctx.strokeStyle = options.background || "#ffffff";
9467
+ drawTextWithBackground(options.callback(value - 0), xPosition, yPosition, ctx);
9468
+ }
9469
+ }
9470
+ }
9471
+ function drawHorizontalBarChartValues(chart, options, ctx) {
9472
+ const xMax = chart.chartArea.right;
9473
+ const xMin = chart.chartArea.left;
9474
+ const textsPositions = {};
9475
+ for (const dataset of chart._metasets) {
9476
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9477
+ return; // ignore trend lines
9478
+ }
9479
+ for (let i = 0; i < dataset._parsed.length; i++) {
9480
+ const value = dataset._parsed[i].x;
9481
+ const displayValue = options.callback(value - 0);
9482
+ const point = dataset.data[i];
9483
+ const yPosition = point.y;
9484
+ let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
9485
+ xPosition = Math.min(xPosition, xMax);
9486
+ xPosition = Math.max(xPosition, xMin);
9487
+ // Avoid overlapping texts with same Y
9488
+ if (!textsPositions[yPosition]) {
9489
+ textsPositions[yPosition] = [];
9490
+ }
9491
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
9492
+ for (const otherPosition of textsPositions[yPosition]) {
9493
+ if (Math.abs(otherPosition - xPosition) < textWidth) {
9494
+ xPosition = otherPosition + textWidth + 3;
9495
+ }
9496
+ }
9497
+ textsPositions[yPosition].push(xPosition);
9498
+ ctx.fillStyle = point.options.backgroundColor;
9499
+ ctx.strokeStyle = options.background || "#ffffff";
9500
+ drawTextWithBackground(displayValue, xPosition, yPosition, ctx);
9501
+ }
9502
+ }
9503
+ }
9504
+ function drawPieChartValues(chart, options, ctx) {
9505
+ for (const dataset of chart._metasets) {
9506
+ for (let i = 0; i < dataset._parsed.length; i++) {
9507
+ const bar = dataset.data[i];
9508
+ const { startAngle, endAngle, innerRadius, outerRadius } = bar;
9509
+ const midAngle = (startAngle + endAngle) / 2;
9510
+ const midRadius = (innerRadius + outerRadius) / 2;
9511
+ const x = bar.x + midRadius * Math.cos(midAngle);
9512
+ const y = bar.y + midRadius * Math.sin(midAngle) + 7;
9513
+ ctx.fillStyle = chartFontColor(options.background);
9514
+ ctx.strokeStyle = options.background || "#ffffff";
9515
+ const value = options.callback(dataset._parsed[i]);
9516
+ drawTextWithBackground(value, x, y, ctx);
9517
+ }
9518
+ }
9519
+ }
9469
9520
 
9470
9521
  /** This is a chartJS plugin that will draw connector lines between the bars of a Waterfall chart */
9471
9522
  const waterfallLinesPlugin = {
@@ -17841,7 +17892,7 @@ function assertDomainLength(domain) {
17841
17892
  throw new EvaluationError(_t("Function PIVOT takes an even number of arguments."));
17842
17893
  }
17843
17894
  }
17844
- function addPivotDependencies(evalContext, coreDefinition) {
17895
+ function addPivotDependencies(evalContext, coreDefinition, forMeasures) {
17845
17896
  //TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER
17846
17897
  const dependencies = [];
17847
17898
  if (coreDefinition.type === "SPREADSHEET" && coreDefinition.dataSet) {
@@ -17853,7 +17904,7 @@ function addPivotDependencies(evalContext, coreDefinition) {
17853
17904
  }
17854
17905
  dependencies.push(range);
17855
17906
  }
17856
- for (const measure of coreDefinition.measures) {
17907
+ for (const measure of forMeasures) {
17857
17908
  if (measure.computedBy) {
17858
17909
  const formula = evalContext.getters.getMeasureCompiledFormula(measure);
17859
17910
  dependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));
@@ -18286,7 +18337,7 @@ const PIVOT_VALUE = {
18286
18337
  assertDomainLength(domainArgs);
18287
18338
  const pivot = this.getters.getPivot(pivotId);
18288
18339
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18289
- addPivotDependencies(this, coreDefinition);
18340
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures.filter((m) => m.id === _measure));
18290
18341
  pivot.init({ reload: pivot.needsReevaluation });
18291
18342
  const error = pivot.assertIsValid({ throwOnError: false });
18292
18343
  if (error) {
@@ -18316,7 +18367,7 @@ const PIVOT_HEADER = {
18316
18367
  assertDomainLength(domainArgs);
18317
18368
  const pivot = this.getters.getPivot(_pivotId);
18318
18369
  const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);
18319
- addPivotDependencies(this, coreDefinition);
18370
+ addPivotDependencies(this, coreDefinition, []);
18320
18371
  pivot.init({ reload: pivot.needsReevaluation });
18321
18372
  const error = pivot.assertIsValid({ throwOnError: false });
18322
18373
  if (error) {
@@ -18367,7 +18418,7 @@ const PIVOT = {
18367
18418
  const pivotId = getPivotId(_pivotFormulaId, this.getters);
18368
18419
  const pivot = this.getters.getPivot(pivotId);
18369
18420
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18370
- addPivotDependencies(this, coreDefinition);
18421
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures);
18371
18422
  pivot.init({ reload: pivot.needsReevaluation });
18372
18423
  const error = pivot.assertIsValid({ throwOnError: false });
18373
18424
  if (error) {
@@ -21576,6 +21627,7 @@ function insertTokenAfterArgSeparator(tokenAtCursor, value) {
21576
21627
  // replace the whole token
21577
21628
  start = tokenAtCursor.start;
21578
21629
  }
21630
+ this.composer.stopComposerRangeSelection();
21579
21631
  this.composer.changeComposerCursorSelection(start, end);
21580
21632
  this.composer.replaceComposerCursorSelection(value);
21581
21633
  }
@@ -21593,6 +21645,7 @@ function insertTokenAfterLeftParenthesis(tokenAtCursor, value) {
21593
21645
  // replace the whole token
21594
21646
  start = tokenAtCursor.start;
21595
21647
  }
21648
+ this.composer.stopComposerRangeSelection();
21596
21649
  this.composer.changeComposerCursorSelection(start, end);
21597
21650
  this.composer.replaceComposerCursorSelection(value);
21598
21651
  }
@@ -21726,9 +21779,13 @@ autoCompleteProviders.add("pivot_group_fields", {
21726
21779
  const colFields = columns.map((groupBy) => groupBy.nameWithGranularity);
21727
21780
  const rowFields = rows.map((groupBy) => groupBy.nameWithGranularity);
21728
21781
  const proposals = [];
21729
- const previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
21782
+ let previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
21730
21783
  ? argGroupBys.at(-1)
21731
21784
  : argGroupBys.at(-2);
21785
+ const isPositionalSupported = supportedPivotPositionalFormulaRegistry.get(pivot.type);
21786
+ if (isPositionalSupported && previousGroupBy?.startsWith("#")) {
21787
+ previousGroupBy = previousGroupBy.slice(1);
21788
+ }
21732
21789
  if (previousGroupBy === undefined) {
21733
21790
  proposals.push(colFields[0]);
21734
21791
  proposals.push(rowFields[0]);
@@ -21750,7 +21807,7 @@ autoCompleteProviders.add("pivot_group_fields", {
21750
21807
  return field ? makeFieldProposal(field, granularity) : undefined;
21751
21808
  })
21752
21809
  .concat(groupBys.map((groupBy) => {
21753
- if (!supportedPivotPositionalFormulaRegistry.get(pivot.type)) {
21810
+ if (!isPositionalSupported) {
21754
21811
  return undefined;
21755
21812
  }
21756
21813
  const fieldName = groupBy.split(":")[0];
@@ -22185,6 +22242,27 @@ autofillModifiersRegistry
22185
22242
  tooltip: content ? { props: { content: tooltipValue } } : undefined,
22186
22243
  };
22187
22244
  },
22245
+ })
22246
+ .add("DATE_INCREMENT_MODIFIER", {
22247
+ apply: (rule, data, getters) => {
22248
+ const date = toJsDate(rule.current, getters.getLocale());
22249
+ date.setFullYear(date.getFullYear() + rule.increment.years || 0);
22250
+ date.setMonth(date.getMonth() + rule.increment.months || 0);
22251
+ date.setDate(date.getDate() + rule.increment.days || 0);
22252
+ const value = jsDateToNumber(date);
22253
+ rule.current = value;
22254
+ const locale = getters.getLocale();
22255
+ const tooltipValue = formatValue(value, { format: data.cell?.format, locale });
22256
+ return {
22257
+ cellData: {
22258
+ border: data.border,
22259
+ style: data.cell && data.cell.style,
22260
+ format: data.cell && data.cell.format,
22261
+ content: value.toString(),
22262
+ },
22263
+ tooltip: value ? { props: { content: tooltipValue } } : undefined,
22264
+ };
22265
+ },
22188
22266
  })
22189
22267
  .add("COPY_MODIFIER", {
22190
22268
  apply: (rule, data, getters) => {
@@ -22265,7 +22343,9 @@ function getGroup(cell, cells, filter) {
22265
22343
  if (x === cell) {
22266
22344
  found = true;
22267
22345
  }
22268
- const cellValue = x === undefined || x.isFormula ? undefined : evaluateLiteral(x, { locale: DEFAULT_LOCALE });
22346
+ const cellValue = x === undefined || x.isFormula
22347
+ ? undefined
22348
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22269
22349
  if (cellValue && filter(cellValue)) {
22270
22350
  group.push(cellValue);
22271
22351
  }
@@ -22301,6 +22381,72 @@ function calculateIncrementBasedOnGroup(group) {
22301
22381
  }
22302
22382
  return increment;
22303
22383
  }
22384
+ /**
22385
+ * Iterates on a list of date intervals.
22386
+ * if every interval is the same, return the interval
22387
+ * Otherwise return undefined
22388
+ *
22389
+ */
22390
+ function getEqualInterval(intervals) {
22391
+ if (intervals.length < 2) {
22392
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22393
+ }
22394
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22395
+ interval.months === intervals[0].months &&
22396
+ interval.days === intervals[0].days);
22397
+ return equal ? intervals[0] : undefined;
22398
+ }
22399
+ /**
22400
+ * Based on a group of dates, calculate the increment that should be applied
22401
+ * to the next date.
22402
+ *
22403
+ * This will compute the date difference in calendar terms (years, months, days)
22404
+ * In order to make abstraction of leap years and months with different number of days.
22405
+ *
22406
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22407
+ * In case of equidistant dates, we either have in that order:
22408
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22409
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22410
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22411
+ *
22412
+ * */
22413
+ function calculateDateIncrementBasedOnGroup(group) {
22414
+ if (group.length < 2) {
22415
+ return 1;
22416
+ }
22417
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22418
+ const datesIntervals = getDateIntervals(jsDates);
22419
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22420
+ if (datesEquidistantInterval === undefined) {
22421
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22422
+ return undefined;
22423
+ }
22424
+ // The dates are apart by an exact interval of years, months or days
22425
+ // but not a combination of them
22426
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22427
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22428
+ if (!exactDateInterval || isSameDay) {
22429
+ const timeIntervals = jsDates
22430
+ .map((date, index) => {
22431
+ if (index === 0) {
22432
+ return 0;
22433
+ }
22434
+ const previous = jsDates[index - 1];
22435
+ const days = Math.floor(date.getTime()) - Math.floor(previous.getTime());
22436
+ return days;
22437
+ })
22438
+ .slice(1);
22439
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22440
+ if (equidistantDates) {
22441
+ return group.length * (group[1] - group[0]);
22442
+ }
22443
+ }
22444
+ return {
22445
+ years: datesEquidistantInterval.years * group.length,
22446
+ months: datesEquidistantInterval.months * group.length,
22447
+ days: datesEquidistantInterval.days * group.length,
22448
+ };
22449
+ }
22304
22450
  autofillRulesRegistry
22305
22451
  .add("simple_value_copy", {
22306
22452
  condition: (cell, cells) => {
@@ -22348,12 +22494,47 @@ autofillRulesRegistry
22348
22494
  return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22349
22495
  },
22350
22496
  sequence: 30,
22497
+ })
22498
+ .add("increment_dates", {
22499
+ condition: (cell, cells) => {
22500
+ return (!cell.isFormula &&
22501
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22502
+ !!cell.format &&
22503
+ isDateTimeFormat(cell.format));
22504
+ },
22505
+ generateRule: (cell, cells) => {
22506
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22507
+ !!evaluatedCell.format &&
22508
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22509
+ const increment = calculateDateIncrementBasedOnGroup(group);
22510
+ if (increment === undefined) {
22511
+ return { type: "COPY_MODIFIER" };
22512
+ }
22513
+ /** requires to detect the current date (requires to be an integer value with the right format)
22514
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22515
+ */
22516
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22517
+ if (typeof increment === "object") {
22518
+ return {
22519
+ type: "DATE_INCREMENT_MODIFIER",
22520
+ increment,
22521
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22522
+ };
22523
+ }
22524
+ return {
22525
+ type: "INCREMENT_MODIFIER",
22526
+ increment,
22527
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22528
+ };
22529
+ },
22530
+ sequence: 25,
22351
22531
  })
22352
22532
  .add("increment_number", {
22353
22533
  condition: (cell) => !cell.isFormula &&
22354
22534
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22355
22535
  generateRule: (cell, cells) => {
22356
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
22536
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22537
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22357
22538
  const increment = calculateIncrementBasedOnGroup(group);
22358
22539
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22359
22540
  return {
@@ -22364,6 +22545,37 @@ autofillRulesRegistry
22364
22545
  },
22365
22546
  sequence: 40,
22366
22547
  });
22548
+ /**
22549
+ * Returns the date intervals between consecutive dates of an array
22550
+ * in the format of { years: number, months: number, days: number }
22551
+ *
22552
+ * The split is necessary to make abstraction of leap years and
22553
+ * months with different number of days.
22554
+ *
22555
+ * @param dates
22556
+ */
22557
+ function getDateIntervals(dates) {
22558
+ if (dates.length < 2) {
22559
+ return [{ years: 0, months: 0, days: 0 }];
22560
+ }
22561
+ const res = dates.map((date, index) => {
22562
+ if (index === 0) {
22563
+ return { years: 0, months: 0, days: 0 };
22564
+ }
22565
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22566
+ const years = getTimeDifferenceInWholeYears(previous, date);
22567
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22568
+ previous.setFullYear(previous.getFullYear() + years);
22569
+ previous.setMonth(previous.getMonth() + months);
22570
+ const days = getTimeDifferenceInWholeDays(previous, date);
22571
+ return {
22572
+ years,
22573
+ months,
22574
+ days,
22575
+ };
22576
+ });
22577
+ return res.slice(1);
22578
+ }
22367
22579
 
22368
22580
  const cellPopoverRegistry = new Registry();
22369
22581
 
@@ -27291,7 +27503,7 @@ function load(data, verboseImport) {
27291
27503
  if (!data) {
27292
27504
  return createEmptyWorkbookData();
27293
27505
  }
27294
- console.group("Loading data");
27506
+ console.debug("### Loading data ###");
27295
27507
  const start = performance.now();
27296
27508
  if (data["[Content_Types].xml"]) {
27297
27509
  const reader = new XlsxReader(data);
@@ -27305,13 +27517,13 @@ function load(data, verboseImport) {
27305
27517
  // apply migrations, if needed
27306
27518
  if ("version" in data) {
27307
27519
  if (data.version < CURRENT_VERSION) {
27308
- console.info("Migrating data from version", data.version);
27520
+ console.debug("Migrating data from version", data.version);
27309
27521
  data = migrate(data);
27310
27522
  }
27311
27523
  }
27312
27524
  data = repairData(data);
27313
- console.info("Data loaded in", performance.now() - start, "ms");
27314
- console.groupEnd();
27525
+ console.debug("Data loaded in", performance.now() - start, "ms");
27526
+ console.debug("###");
27315
27527
  return data;
27316
27528
  }
27317
27529
  // -----------------------------------------------------------------------------
@@ -27341,7 +27553,7 @@ function migrate(data) {
27341
27553
  for (let i = index; i < steps.length; i++) {
27342
27554
  data = steps[i].migrate(data);
27343
27555
  }
27344
- console.info("Data migrated in", performance.now() - start, "ms");
27556
+ console.debug("Data migrated in", performance.now() - start, "ms");
27345
27557
  return data;
27346
27558
  }
27347
27559
  /**
@@ -27920,7 +28132,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, tr
27920
28132
  const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
27921
28133
  // tooltipItem.parsed can be an object or a number for pie charts
27922
28134
  let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;
27923
- if (!yLabel) {
28135
+ if (yLabel === undefined || yLabel === null) {
27924
28136
  yLabel = tooltipItem.parsed;
27925
28137
  }
27926
28138
  const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
@@ -28361,13 +28573,10 @@ function createBarChartRuntime(chart, getters) {
28361
28573
  * datasets to ensure the way we distinguish the originals and trendLine datasets after
28362
28574
  */
28363
28575
  trendDatasets.forEach((x) => config.data.datasets.push(x));
28364
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
28365
28576
  config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
28366
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
28367
- // @ts-expect-error
28368
- return originalTooltipTitle?.(tooltipItems);
28369
- }
28370
- return "";
28577
+ return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
28578
+ ? undefined
28579
+ : "";
28371
28580
  };
28372
28581
  }
28373
28582
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
@@ -28737,7 +28946,6 @@ function createLineOrScatterChartRuntime(chart, getters) {
28737
28946
  else if (axisType === "linear") {
28738
28947
  config.options.scales.x.type = "linear";
28739
28948
  config.options.scales.x.ticks.callback = (value) => formatValue(value, { format: labelFormat, locale });
28740
- config.options.plugins.tooltip.callbacks.title = () => "";
28741
28949
  config.options.plugins.tooltip.callbacks.label = (tooltipItem) => {
28742
28950
  const dataSetPoint = dataSetsValues[tooltipItem.datasetIndex].data[tooltipItem.dataIndex];
28743
28951
  let label = tooltipItem.label || labelValues.values[tooltipItem.dataIndex];
@@ -28825,15 +29033,12 @@ function createLineOrScatterChartRuntime(chart, getters) {
28825
29033
  * distinguish the originals and trendLine datasets after
28826
29034
  */
28827
29035
  trendDatasets.forEach((x) => config.data.datasets.push(x));
28828
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
28829
- config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
28830
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
28831
- // @ts-expect-error
28832
- return originalTooltipTitle?.(tooltipItems);
28833
- }
28834
- return "";
28835
- };
28836
29036
  }
29037
+ config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
29038
+ const displayTooltipTitle = axisType !== "linear" &&
29039
+ tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);
29040
+ return displayTooltipTitle ? undefined : "";
29041
+ };
28837
29042
  return {
28838
29043
  chartJsConfig: config,
28839
29044
  background: chart.background || BACKGROUND_CHART_COLOR,
@@ -28898,6 +29103,7 @@ class ComboChart extends AbstractChart {
28898
29103
  ranges.push({
28899
29104
  ...this.dataSetDesign?.[i],
28900
29105
  dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
29106
+ type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"),
28901
29107
  });
28902
29108
  }
28903
29109
  return {
@@ -28943,9 +29149,13 @@ class ComboChart extends AbstractChart {
28943
29149
  return new ComboChart(definition, this.sheetId, this.getters);
28944
29150
  }
28945
29151
  static getDefinitionFromContextCreation(context) {
29152
+ const dataSets = (context.range ?? []).map((ds, index) => ({
29153
+ ...ds,
29154
+ type: index ? "line" : "bar",
29155
+ }));
28946
29156
  return {
28947
29157
  background: context.background,
28948
- dataSets: context.range ?? [],
29158
+ dataSets,
28949
29159
  dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
28950
29160
  aggregated: context.aggregated,
28951
29161
  legendPosition: context.legendPosition ?? "top",
@@ -28990,7 +29200,6 @@ function createComboChartRuntime(chart, getters) {
28990
29200
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
28991
29201
  const legend = {
28992
29202
  labels: { color: fontColor },
28993
- reverse: true,
28994
29203
  };
28995
29204
  if (chart.legendPosition === "none") {
28996
29205
  legend.display = false;
@@ -29058,14 +29267,15 @@ function createComboChartRuntime(chart, getters) {
29058
29267
  for (let [index, { label, data }] of dataSetsValues.entries()) {
29059
29268
  const design = definition.dataSets[index];
29060
29269
  const color = colors.next();
29270
+ const type = design?.type ?? "line";
29061
29271
  const dataset = {
29062
29272
  label: design?.label ?? label,
29063
29273
  data,
29064
29274
  borderColor: color,
29065
29275
  backgroundColor: color,
29066
29276
  yAxisID: design?.yAxisId ?? "y",
29067
- type: index === 0 ? "bar" : "line",
29068
- order: -index,
29277
+ type,
29278
+ order: type === "bar" ? dataSetsValues.length + index : index,
29069
29279
  };
29070
29280
  config.data.datasets.push(dataset);
29071
29281
  const trend = definition.dataSets?.[index].trend;
@@ -29092,13 +29302,10 @@ function createComboChartRuntime(chart, getters) {
29092
29302
  * distinguish the originals and trendLine datasets after
29093
29303
  */
29094
29304
  trendDatasets.forEach((x) => config.data.datasets.push(x));
29095
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
29096
29305
  config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
29097
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
29098
- // @ts-expect-error
29099
- return originalTooltipTitle?.(tooltipItems);
29100
- }
29101
- return "";
29306
+ return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
29307
+ ? undefined
29308
+ : "";
29102
29309
  };
29103
29310
  }
29104
29311
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
@@ -35294,6 +35501,7 @@ const CHECK_SVG = /*xml*/ `
35294
35501
  <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35295
35502
  </svg>
35296
35503
  `;
35504
+ const CHECKBOX_WIDTH = 14;
35297
35505
  css /* scss */ `
35298
35506
  label.o-checkbox {
35299
35507
  input {
@@ -35301,8 +35509,8 @@ css /* scss */ `
35301
35509
  -webkit-appearance: none;
35302
35510
  -moz-appearance: none;
35303
35511
  border-radius: 0;
35304
- width: 14px;
35305
- height: 14px;
35512
+ width: ${CHECKBOX_WIDTH}px;
35513
+ height: ${CHECKBOX_WIDTH}px;
35306
35514
  vertical-align: top;
35307
35515
  box-sizing: border-box;
35308
35516
  outline: none;
@@ -36087,7 +36295,6 @@ class BarConfigPanel extends GenericChartConfigPanel {
36087
36295
  css /* scss */ `
36088
36296
  .o_side_panel_collapsible_title {
36089
36297
  font-size: 16px;
36090
- font-weight: bold;
36091
36298
  cursor: pointer;
36092
36299
  padding: 6px 0px 6px 6px !important;
36093
36300
 
@@ -37067,6 +37274,9 @@ class ChartWithAxisDesignPanel extends Component {
37067
37274
  getDataSeries() {
37068
37275
  return this.props.definition.dataSets.map((d, i) => d.label ?? `${ChartTerms.Series} ${i + 1}`);
37069
37276
  }
37277
+ getPolynomialDegrees() {
37278
+ return range(1, this.getMaxPolynomialDegree() + 1);
37279
+ }
37070
37280
  updateSerieEditor(ev) {
37071
37281
  const chartId = this.props.figureId;
37072
37282
  const selectedIndex = ev.target.selectedIndex;
@@ -37183,12 +37393,7 @@ class ChartWithAxisDesignPanel extends Component {
37183
37393
  }
37184
37394
  onChangePolynomialDegree(ev) {
37185
37395
  const element = ev.target;
37186
- const order = parseInt(element.value || "1");
37187
- if (order < 2) {
37188
- element.value = `${this.getTrendLineConfiguration()?.order ?? 2}`;
37189
- return;
37190
- }
37191
- this.updateTrendLineValue({ order });
37396
+ this.updateTrendLineValue({ order: parseInt(element.value) });
37192
37397
  }
37193
37398
  getTrendLineColor() {
37194
37399
  return this.getTrendLineConfiguration()?.color ?? setColorAlpha(this.getDataSerieColor(), 0.5);
@@ -37210,6 +37415,36 @@ class ChartWithAxisDesignPanel extends Component {
37210
37415
  };
37211
37416
  this.props.updateChart(this.props.figureId, { dataSets });
37212
37417
  }
37418
+ getMaxPolynomialDegree() {
37419
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figureId);
37420
+ return Math.min(10, runtime.chartJsConfig.data.datasets[this.state.index].data.length - 1);
37421
+ }
37422
+ }
37423
+
37424
+ class ComboChartDesignPanel extends ChartWithAxisDesignPanel {
37425
+ static template = "o-spreadsheet-ComboChartDesignPanel";
37426
+ seriesTypeChoices = [
37427
+ { value: "bar", label: _t("Bar") },
37428
+ { value: "line", label: _t("Line") },
37429
+ ];
37430
+ updateDataSeriesType(type) {
37431
+ const dataSets = [...this.props.definition.dataSets];
37432
+ if (!dataSets?.[this.state.index]) {
37433
+ return;
37434
+ }
37435
+ dataSets[this.state.index] = {
37436
+ ...dataSets[this.state.index],
37437
+ type,
37438
+ };
37439
+ this.props.updateChart(this.props.figureId, { dataSets });
37440
+ }
37441
+ getDataSeriesType() {
37442
+ const dataSets = this.props.definition.dataSets;
37443
+ if (!dataSets?.[this.state.index]) {
37444
+ return "bar";
37445
+ }
37446
+ return dataSets[this.state.index].type ?? "line";
37447
+ }
37213
37448
  }
37214
37449
 
37215
37450
  class GaugeChartConfigPanel extends Component {
@@ -37638,7 +37873,7 @@ chartSidePanelComponentRegistry
37638
37873
  })
37639
37874
  .add("combo", {
37640
37875
  configuration: GenericChartConfigPanel,
37641
- design: ChartWithAxisDesignPanel,
37876
+ design: ComboChartDesignPanel,
37642
37877
  })
37643
37878
  .add("pie", {
37644
37879
  configuration: GenericChartConfigPanel,
@@ -39012,7 +39247,6 @@ css /* scss */ `
39012
39247
  width: 142px;
39013
39248
  .o-cf-preview-description-rule {
39014
39249
  margin-bottom: 4px;
39015
- font-weight: 600;
39016
39250
  max-height: 2.8em;
39017
39251
  line-height: 1.4em;
39018
39252
  }
@@ -42014,7 +42248,6 @@ function createMeasureAutoComplete(pivot, forComputedMeasure) {
42014
42248
  sequence: 0,
42015
42249
  autoSelectFirstProposal: true,
42016
42250
  getProposals(tokenAtCursor) {
42017
- // return []
42018
42251
  const measureProposals = pivot.measures
42019
42252
  .filter((m) => m !== forComputedMeasure)
42020
42253
  .map((measure) => {
@@ -42817,8 +43050,9 @@ const EMPTY_PIVOT_CELL = { type: "EMPTY" };
42817
43050
  * This function converts a list of data entry into a spreadsheet pivot table.
42818
43051
  */
42819
43052
  function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
43053
+ const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42820
43054
  const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
42821
- computeWidthOfColumnsNodes(columnsTree, definition.measures.length);
43055
+ computeWidthOfColumnsNodes(columnsTree, measureIds.length);
42822
43056
  const cols = columnsTreeToColumns(columnsTree, definition);
42823
43057
  const rows = dataEntriesToRows(dataEntries, 0, definition.rows, [], []);
42824
43058
  // Add the total row
@@ -42827,7 +43061,6 @@ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
42827
43061
  values: [],
42828
43062
  indent: 0,
42829
43063
  });
42830
- const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42831
43064
  const fieldsType = {};
42832
43065
  for (const columns of definition.columns) {
42833
43066
  fieldsType[columns.fieldName] = columns.type;
@@ -43588,7 +43821,7 @@ pivotRegistry.add("SPREADSHEET", {
43588
43821
  onIterationEndEvaluation: (pivot) => pivot.markAsDirtyForEvaluation(),
43589
43822
  dateGranularities: [...dateGranularities],
43590
43823
  datetimeGranularities: [...dateGranularities, "hour_number", "minute_number", "second_number"],
43591
- isMeasureCandidate: (field) => !["date", "boolean"].includes(field.type),
43824
+ isMeasureCandidate: (field) => !["datetime", "boolean"].includes(field.type),
43592
43825
  isGroupable: () => true,
43593
43826
  });
43594
43827
 
@@ -46117,13 +46350,10 @@ class GridCellIcon extends Component {
46117
46350
  }
46118
46351
  }
46119
46352
 
46120
- const CHECKBOX_WIDTH = 15;
46121
46353
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
46122
46354
  css /* scss */ `
46123
46355
  .o-dv-checkbox {
46124
46356
  box-sizing: border-box !important;
46125
- width: ${CHECKBOX_WIDTH}px;
46126
- height: ${CHECKBOX_WIDTH}px;
46127
46357
  accent-color: #808080;
46128
46358
  margin: ${MARGIN}px;
46129
46359
  /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
@@ -46132,13 +46362,15 @@ css /* scss */ `
46132
46362
  `;
46133
46363
  class DataValidationCheckbox extends Component {
46134
46364
  static template = "o-spreadsheet-DataValidationCheckbox";
46365
+ static components = {
46366
+ Checkbox,
46367
+ };
46135
46368
  static props = {
46136
46369
  cellPosition: Object,
46137
46370
  };
46138
- onCheckboxChange(ev) {
46139
- const newValue = ev.target.checked;
46371
+ onCheckboxChange(value) {
46140
46372
  const { sheetId, col, row } = this.props.cellPosition;
46141
- const cellContent = newValue ? "TRUE" : "FALSE";
46373
+ const cellContent = value ? "TRUE" : "FALSE";
46142
46374
  this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
46143
46375
  }
46144
46376
  get checkBoxValue() {
@@ -46952,7 +47184,12 @@ class GridAddRowsFooter extends Component {
46952
47184
  class PaintFormatStore extends SpreadsheetStore {
46953
47185
  mutators = ["activate", "cancel", "pasteFormat"];
46954
47186
  highlightStore = this.get(HighlightStore);
46955
- cellClipboardHandler = new CellClipboardHandler(this.getters, this.model.dispatch);
47187
+ clipboardHandlers = [
47188
+ new CellClipboardHandler(this.getters, this.model.dispatch),
47189
+ new BorderClipboardHandler(this.getters, this.model.dispatch),
47190
+ new TableClipboardHandler(this.getters, this.model.dispatch),
47191
+ new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),
47192
+ ];
46956
47193
  status = "inactive";
46957
47194
  copiedData;
46958
47195
  constructor(get) {
@@ -46962,6 +47199,13 @@ class PaintFormatStore extends SpreadsheetStore {
46962
47199
  this.highlightStore.unRegister(this);
46963
47200
  });
46964
47201
  }
47202
+ handle(cmd) {
47203
+ switch (cmd.type) {
47204
+ case "PAINT_FORMAT":
47205
+ this.paintFormat(cmd.sheetId, cmd.target);
47206
+ break;
47207
+ }
47208
+ }
46965
47209
  activate(args) {
46966
47210
  this.copiedData = this.copyFormats();
46967
47211
  this.status = args.persistent ? "persistent" : "oneOff";
@@ -46971,16 +47215,7 @@ class PaintFormatStore extends SpreadsheetStore {
46971
47215
  this.copiedData = undefined;
46972
47216
  }
46973
47217
  pasteFormat(target) {
46974
- if (this.copiedData) {
46975
- const sheetId = this.getters.getActiveSheetId();
46976
- this.cellClipboardHandler.paste({ zones: target, sheetId }, this.copiedData, {
46977
- isCutOperation: false,
46978
- pasteOption: "onlyFormat",
46979
- });
46980
- }
46981
- if (this.status === "oneOff") {
46982
- this.cancel();
46983
- }
47218
+ this.model.dispatch("PAINT_FORMAT", { target, sheetId: this.getters.getActiveSheetId() });
46984
47219
  }
46985
47220
  get isActive() {
46986
47221
  return this.status !== "inactive";
@@ -46988,7 +47223,24 @@ class PaintFormatStore extends SpreadsheetStore {
46988
47223
  copyFormats() {
46989
47224
  const sheetId = this.getters.getActiveSheetId();
46990
47225
  const zones = this.getters.getSelectedZones();
46991
- return this.cellClipboardHandler.copy(getClipboardDataPositions(sheetId, zones));
47226
+ const copiedData = {};
47227
+ for (const handler of this.clipboardHandlers) {
47228
+ Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones)));
47229
+ }
47230
+ return copiedData;
47231
+ }
47232
+ paintFormat(sheetId, target) {
47233
+ if (this.copiedData) {
47234
+ for (const handler of this.clipboardHandlers) {
47235
+ handler.paste({ zones: target, sheetId }, this.copiedData, {
47236
+ isCutOperation: false,
47237
+ pasteOption: "onlyFormat",
47238
+ });
47239
+ }
47240
+ }
47241
+ if (this.status === "oneOff") {
47242
+ this.cancel();
47243
+ }
46992
47244
  }
46993
47245
  get highlights() {
46994
47246
  const data = this.copiedData;
@@ -55233,7 +55485,7 @@ class PivotCorePlugin extends CorePlugin {
55233
55485
  case "DUPLICATE_PIVOT": {
55234
55486
  const { pivotId, newPivotId } = cmd;
55235
55487
  const pivot = deepCopy(this.getPivotCore(pivotId).definition);
55236
- pivot.name = _t("%s (copy)", pivot.name);
55488
+ pivot.name = cmd.duplicatedPivotName ?? pivot.name + " (copy)";
55237
55489
  this.addPivot(newPivotId, pivot);
55238
55490
  break;
55239
55491
  }
@@ -55273,7 +55525,7 @@ class PivotCorePlugin extends CorePlugin {
55273
55525
  return `(#${formulaId}) ${this.getPivotName(pivotId)}`;
55274
55526
  }
55275
55527
  getPivotName(pivotId) {
55276
- return _t(this.getPivotCore(pivotId).definition.name);
55528
+ return this.getPivotCore(pivotId).definition.name;
55277
55529
  }
55278
55530
  /**
55279
55531
  * Returns the pivot core definition of the pivot with the given id.
@@ -56915,7 +57167,7 @@ class Evaluator {
56915
57167
  cellsToCompute.addMany(arrayFormulasPositions);
56916
57168
  cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
56917
57169
  this.evaluate(cellsToCompute);
56918
- console.info("evaluate Cells", performance.now() - start, "ms");
57170
+ console.debug("evaluate Cells", performance.now() - start, "ms");
56919
57171
  }
56920
57172
  getArrayFormulasImpactedByChangesOf(positions) {
56921
57173
  const impactedPositions = this.createEmptyPositionSet();
@@ -56959,7 +57211,7 @@ class Evaluator {
56959
57211
  const start = performance.now();
56960
57212
  this.evaluatedCells = new PositionMap();
56961
57213
  this.evaluate(this.getAllCells());
56962
- console.info("evaluate all cells", performance.now() - start, "ms");
57214
+ console.debug("evaluate all cells", performance.now() - start, "ms");
56963
57215
  }
56964
57216
  evaluateFormulaResult(sheetId, formulaString) {
56965
57217
  const compiledFormula = compile(formulaString);
@@ -59050,7 +59302,7 @@ function withPivotPresentationLayer (PivotClass) {
59050
59302
  const ranking = {};
59051
59303
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59052
59304
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59053
- let pivotCells = this.getPivotValueCells();
59305
+ let pivotCells = this.getPivotValueCells(measure.id);
59054
59306
  if (mainDimension === "column") {
59055
59307
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59056
59308
  // This means that we need to transpose back the ranking at the end
@@ -59094,7 +59346,7 @@ function withPivotPresentationLayer (PivotClass) {
59094
59346
  const cellsRunningTotals = {};
59095
59347
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59096
59348
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59097
- let pivotCells = this.getPivotValueCells();
59349
+ let pivotCells = this.getPivotValueCells(measure.id);
59098
59350
  if (mainDimension === "column") {
59099
59351
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59100
59352
  // This means that we need to transpose back the totals at the end
@@ -59173,10 +59425,10 @@ function withPivotPresentationLayer (PivotClass) {
59173
59425
  const comparedValueNumber = this.strictMeasureValueToNumber(comparedValue);
59174
59426
  return comparedValueNumber;
59175
59427
  }
59176
- getPivotValueCells() {
59428
+ getPivotValueCells(measureId) {
59177
59429
  return this.getTableStructure()
59178
59430
  .getPivotCells()
59179
- .map((col) => col.filter((cell) => cell.type === "VALUE"))
59431
+ .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
59180
59432
  .filter((col) => col.length > 0);
59181
59433
  }
59182
59434
  measureValueToNumber(result) {
@@ -60705,7 +60957,7 @@ class Session extends EventBus {
60705
60957
  this.onMessageReceived(message);
60706
60958
  }
60707
60959
  this.isReplayingInitialRevisions = false;
60708
- console.info("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
60960
+ console.debug("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
60709
60961
  }
60710
60962
  /**
60711
60963
  * Notify the server that the user client left the collaborative session
@@ -60944,6 +61196,7 @@ class Session extends EventBus {
60944
61196
  case "REMOTE_REVISION":
60945
61197
  case "REVISION_REDONE":
60946
61198
  case "REVISION_UNDONE":
61199
+ case "SNAPSHOT_CREATED":
60947
61200
  return this.processedRevisions.has(message.nextRevisionId);
60948
61201
  default:
60949
61202
  return false;
@@ -61501,12 +61754,13 @@ class InsertPivotPlugin extends UIPlugin {
61501
61754
  this.dispatch("DUPLICATE_PIVOT", {
61502
61755
  pivotId,
61503
61756
  newPivotId,
61757
+ duplicatedPivotName: _t("%s (copy)", this.getters.getPivotCoreDefinition(pivotId).name),
61504
61758
  });
61505
61759
  const activeSheetId = this.getters.getActiveSheetId();
61506
61760
  const position = this.getters.getSheetIds().indexOf(activeSheetId) + 1;
61507
61761
  const formulaId = this.getters.getPivotFormulaId(newPivotId);
61508
61762
  const newPivotName = this.getters.getPivotName(newPivotId);
61509
- this.dispatch("CREATE_SHEET", {
61763
+ const result = this.dispatch("CREATE_SHEET", {
61510
61764
  sheetId: newSheetId,
61511
61765
  name: this.getPivotDuplicateSheetName(_t("%(newPivotName)s (Pivot #%(formulaId)s)", {
61512
61766
  newPivotName,
@@ -61514,20 +61768,19 @@ class InsertPivotPlugin extends UIPlugin {
61514
61768
  })),
61515
61769
  position,
61516
61770
  });
61517
- this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61518
- this.dispatch("UPDATE_CELL", {
61519
- sheetId: newSheetId,
61520
- col: 0,
61521
- row: 0,
61522
- content: `=PIVOT(${formulaId})`,
61523
- });
61771
+ if (result.isSuccessful) {
61772
+ this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61773
+ const pivot = this.getters.getPivot(pivotId);
61774
+ this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), "dynamic");
61775
+ }
61524
61776
  }
61525
61777
  getPivotDuplicateSheetName(pivotName) {
61526
61778
  let i = 1;
61527
61779
  const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
61528
- let name = pivotName;
61780
+ const sanitizedName = pivotName.replace(new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g"), " ");
61781
+ let name = sanitizedName;
61529
61782
  while (names.includes(name)) {
61530
- name = `${pivotName} (${i})`;
61783
+ name = `${sanitizedName} (${i})`;
61531
61784
  i++;
61532
61785
  }
61533
61786
  return name;
@@ -66635,10 +66888,9 @@ css /* scss */ `
66635
66888
  user-select: none;
66636
66889
  color: ${TEXT_BODY};
66637
66890
 
66638
- .o-heading-3 {
66891
+ .o-sidePanelTitle {
66639
66892
  line-height: 20px;
66640
66893
  font-size: 16px;
66641
- font-weight: 600;
66642
66894
  }
66643
66895
 
66644
66896
  .o-sidePanelHeader {
@@ -66723,6 +66975,10 @@ css /* scss */ `
66723
66975
  }
66724
66976
  }
66725
66977
  }
66978
+
66979
+ .o-fw-bold {
66980
+ font-weight: 500;
66981
+ }
66726
66982
  `;
66727
66983
  class SidePanel extends Component {
66728
66984
  static template = "o-spreadsheet-SidePanel";
@@ -70880,6 +71136,15 @@ function addTableColumns(table, sheetData) {
70880
71136
  ["id", i + 1], // id cannot be 0
70881
71137
  ["name", colName],
70882
71138
  ];
71139
+ if (table.config.totalRow) {
71140
+ // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag
71141
+ // `<totalsRowFormula>` for the formula of the total. But those doesn't seem to be mandatory for Excel.
71142
+ const colTotalXc = toXC(tableZone.left + i, tableZone.bottom);
71143
+ const colTotalContent = sheetData.cells[colTotalXc]?.content;
71144
+ if (colTotalContent?.startsWith("=")) {
71145
+ colAttributes.push(["totalsRowFunction", "custom"]);
71146
+ }
71147
+ }
70883
71148
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
70884
71149
  }
70885
71150
  return escapeXml /*xml*/ `
@@ -70974,8 +71239,9 @@ function addRows(construct, data, sheet) {
70974
71239
  }
70975
71240
  else if (cell.content && cell.content !== "") {
70976
71241
  const isTableHeader = isCellTableHeader(c, r, sheet);
71242
+ const isTableTotal = isCellTableTotal(c, r, sheet);
70977
71243
  const isPlainText = !!(cell.format && isTextFormat(data.formats[cell.format]));
70978
- ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isPlainText));
71244
+ ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isTableTotal || isPlainText));
70979
71245
  }
70980
71246
  attributes.push(...additionalAttrs);
70981
71247
  // prettier-ignore
@@ -71009,6 +71275,16 @@ function isCellTableHeader(col, row, sheet) {
71009
71275
  return isInside(col, row, headerZone);
71010
71276
  });
71011
71277
  }
71278
+ function isCellTableTotal(col, row, sheet) {
71279
+ return sheet.tables.some((table) => {
71280
+ if (!table.config.totalRow) {
71281
+ return false;
71282
+ }
71283
+ const zone = toZone(table.range);
71284
+ const totalZone = { ...zone, top: zone.bottom };
71285
+ return isInside(col, row, totalZone);
71286
+ });
71287
+ }
71012
71288
  function addHyperlinks(construct, data, sheetIndex) {
71013
71289
  const sheet = data.sheets[sheetIndex];
71014
71290
  const cells = sheet.cells;
@@ -71472,7 +71748,7 @@ class Model extends EventBus {
71472
71748
  coreHandlers = [];
71473
71749
  constructor(data = {}, config = {}, stateUpdateMessages = [], uuidGenerator = new UuidGenerator(), verboseImport = false) {
71474
71750
  const start = performance.now();
71475
- console.group("Model creation");
71751
+ console.debug("##### Model creation #####");
71476
71752
  super();
71477
71753
  setDefaultTranslationMethod();
71478
71754
  stateUpdateMessages = repairInitialMessages(data, stateUpdateMessages);
@@ -71499,7 +71775,6 @@ class Model extends EventBus {
71499
71775
  isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
71500
71776
  isDashboard: () => this.config.mode === "dashboard",
71501
71777
  };
71502
- this.uuidGenerator.setIsFastStrategy(true);
71503
71778
  // Initiate stream processor
71504
71779
  this.selection = new SelectionStreamProcessorImpl(this.getters);
71505
71780
  this.coreHandlers.push(this.range);
@@ -71545,16 +71820,16 @@ class Model extends EventBus {
71545
71820
  this.joinSession();
71546
71821
  if (config.snapshotRequested) {
71547
71822
  const startSnapshot = performance.now();
71548
- console.info("Snapshot requested");
71823
+ console.debug("Snapshot requested");
71549
71824
  this.session.snapshot(this.exportData());
71550
71825
  this.garbageCollectExternalResources();
71551
- console.info("Snapshot taken in", performance.now() - startSnapshot, "ms");
71826
+ console.debug("Snapshot taken in", performance.now() - startSnapshot, "ms");
71552
71827
  }
71553
71828
  // mark all models as "raw", so they will not be turned into reactive objects
71554
71829
  // by owl, since we do not rely on reactivity
71555
71830
  markRaw(this);
71556
- console.info("Model created in", performance.now() - start, "ms");
71557
- console.groupEnd();
71831
+ console.debug("Model created in", performance.now() - start, "ms");
71832
+ console.debug("######");
71558
71833
  }
71559
71834
  joinSession() {
71560
71835
  this.session.join(this.config.client);
@@ -71778,7 +72053,7 @@ class Model extends EventBus {
71778
72053
  this.finalize();
71779
72054
  const time = performance.now() - start;
71780
72055
  if (time > 5) {
71781
- console.info(type, time, "ms");
72056
+ console.debug(type, time, "ms");
71782
72057
  }
71783
72058
  });
71784
72059
  this.session.save(command, commands, changes);
@@ -72130,6 +72405,6 @@ const constants = {
72130
72405
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
72131
72406
 
72132
72407
 
72133
- __info__.version = "18.0.0";
72134
- __info__.date = "2024-09-25T12:54:19.974Z";
72135
- __info__.hash = "cee2e47";
72408
+ __info__.version = "18.0.2";
72409
+ __info__.date = "2024-10-24T08:54:21.934Z";
72410
+ __info__.hash = "788df92";