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