@odoo/o-spreadsheet 18.0.0 → 18.0.1

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,15 +2,121 @@
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.1
6
+ * @date 2024-10-14T07:54:24.768Z
7
+ * @hash 1771f68
8
8
  */
9
9
 
10
10
  'use strict';
11
11
 
12
12
  var owl = require('@odoo/owl');
13
13
 
14
+ function createActions(menuItems) {
15
+ return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
16
+ }
17
+ let nextItemId = 1;
18
+ function createAction(item) {
19
+ const name = item.name;
20
+ const children = item.children;
21
+ const description = item.description;
22
+ const icon = item.icon;
23
+ const secondaryIcon = item.secondaryIcon;
24
+ const itemId = item.id || nextItemId++;
25
+ return {
26
+ id: itemId.toString(),
27
+ name: typeof name === "function" ? name : () => name,
28
+ isVisible: item.isVisible ? item.isVisible : () => true,
29
+ isEnabled: item.isEnabled ? item.isEnabled : () => true,
30
+ isActive: item.isActive,
31
+ execute: item.execute,
32
+ children: children
33
+ ? (env) => {
34
+ return children
35
+ .map((child) => (typeof child === "function" ? child(env) : child))
36
+ .flat()
37
+ .map(createAction);
38
+ }
39
+ : () => [],
40
+ isReadonlyAllowed: item.isReadonlyAllowed || false,
41
+ separator: item.separator || false,
42
+ icon: typeof icon === "function" ? icon : () => icon || "",
43
+ iconColor: item.iconColor,
44
+ secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
45
+ description: typeof description === "function" ? description : () => description || "",
46
+ textColor: item.textColor,
47
+ sequence: item.sequence || 0,
48
+ onStartHover: item.onStartHover,
49
+ onStopHover: item.onStopHover,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Registry
55
+ *
56
+ * The Registry class is basically just a mapping from a string key to an object.
57
+ * It is really not much more than an object. It is however useful for the
58
+ * following reasons:
59
+ *
60
+ * 1. it let us react and execute code when someone add something to the registry
61
+ * (for example, the FunctionRegistry subclass this for this purpose)
62
+ * 2. it throws an error when the get operation fails
63
+ * 3. it provides a chained API to add items to the registry.
64
+ */
65
+ class Registry {
66
+ content = {};
67
+ /**
68
+ * Add an item to the registry
69
+ *
70
+ * Note that this also returns the registry, so another add method call can
71
+ * be chained
72
+ */
73
+ add(key, value) {
74
+ this.content[key] = value;
75
+ return this;
76
+ }
77
+ /**
78
+ * Get an item from the registry
79
+ */
80
+ get(key) {
81
+ /**
82
+ * Note: key in {} is ~12 times slower than {}[key].
83
+ * So, we check the absence of key only when the direct access returns
84
+ * a falsy value. It's done to ensure that the registry can contains falsy values
85
+ */
86
+ const content = this.content[key];
87
+ if (!content) {
88
+ if (!(key in this.content)) {
89
+ throw new Error(`Cannot find ${key} in this registry!`);
90
+ }
91
+ }
92
+ return content;
93
+ }
94
+ /**
95
+ * Check if the key is already in the registry
96
+ */
97
+ contains(key) {
98
+ return key in this.content;
99
+ }
100
+ /**
101
+ * Get a list of all elements in the registry
102
+ */
103
+ getAll() {
104
+ return Object.values(this.content);
105
+ }
106
+ /**
107
+ * Get a list of all keys in the registry
108
+ */
109
+ getKeys() {
110
+ return Object.keys(this.content);
111
+ }
112
+ /**
113
+ * Remove an item from the registry
114
+ */
115
+ remove(key) {
116
+ delete this.content[key];
117
+ }
118
+ }
119
+
14
120
  const CANVAS_SHIFT = 0.5;
15
121
  // Colors
16
122
  const HIGHLIGHT_COLOR = "#37A850";
@@ -5995,111 +6101,6 @@ class UuidGenerator {
5995
6101
  }
5996
6102
  }
5997
6103
 
5998
- function createActions(menuItems) {
5999
- return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
6000
- }
6001
- const uuidGenerator$1 = new UuidGenerator();
6002
- function createAction(item) {
6003
- const name = item.name;
6004
- const children = item.children;
6005
- const description = item.description;
6006
- const icon = item.icon;
6007
- const secondaryIcon = item.secondaryIcon;
6008
- return {
6009
- id: item.id || uuidGenerator$1.uuidv4(),
6010
- name: typeof name === "function" ? name : () => name,
6011
- isVisible: item.isVisible ? item.isVisible : () => true,
6012
- isEnabled: item.isEnabled ? item.isEnabled : () => true,
6013
- isActive: item.isActive,
6014
- execute: item.execute,
6015
- children: children
6016
- ? (env) => {
6017
- return children
6018
- .map((child) => (typeof child === "function" ? child(env) : child))
6019
- .flat()
6020
- .map(createAction);
6021
- }
6022
- : () => [],
6023
- isReadonlyAllowed: item.isReadonlyAllowed || false,
6024
- separator: item.separator || false,
6025
- icon: typeof icon === "function" ? icon : () => icon || "",
6026
- iconColor: item.iconColor,
6027
- secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
6028
- description: typeof description === "function" ? description : () => description || "",
6029
- textColor: item.textColor,
6030
- sequence: item.sequence || 0,
6031
- onStartHover: item.onStartHover,
6032
- onStopHover: item.onStopHover,
6033
- };
6034
- }
6035
-
6036
- /**
6037
- * Registry
6038
- *
6039
- * The Registry class is basically just a mapping from a string key to an object.
6040
- * It is really not much more than an object. It is however useful for the
6041
- * following reasons:
6042
- *
6043
- * 1. it let us react and execute code when someone add something to the registry
6044
- * (for example, the FunctionRegistry subclass this for this purpose)
6045
- * 2. it throws an error when the get operation fails
6046
- * 3. it provides a chained API to add items to the registry.
6047
- */
6048
- class Registry {
6049
- content = {};
6050
- /**
6051
- * Add an item to the registry
6052
- *
6053
- * Note that this also returns the registry, so another add method call can
6054
- * be chained
6055
- */
6056
- add(key, value) {
6057
- this.content[key] = value;
6058
- return this;
6059
- }
6060
- /**
6061
- * Get an item from the registry
6062
- */
6063
- get(key) {
6064
- /**
6065
- * Note: key in {} is ~12 times slower than {}[key].
6066
- * So, we check the absence of key only when the direct access returns
6067
- * a falsy value. It's done to ensure that the registry can contains falsy values
6068
- */
6069
- const content = this.content[key];
6070
- if (!content) {
6071
- if (!(key in this.content)) {
6072
- throw new Error(`Cannot find ${key} in this registry!`);
6073
- }
6074
- }
6075
- return content;
6076
- }
6077
- /**
6078
- * Check if the key is already in the registry
6079
- */
6080
- contains(key) {
6081
- return key in this.content;
6082
- }
6083
- /**
6084
- * Get a list of all elements in the registry
6085
- */
6086
- getAll() {
6087
- return Object.values(this.content);
6088
- }
6089
- /**
6090
- * Get a list of all keys in the registry
6091
- */
6092
- getKeys() {
6093
- return Object.keys(this.content);
6094
- }
6095
- /**
6096
- * Remove an item from the registry
6097
- */
6098
- remove(key) {
6099
- delete this.content[key];
6100
- }
6101
- }
6102
-
6103
6104
  function getClipboardDataPositions(sheetId, zones) {
6104
6105
  const lefts = new Set(zones.map((z) => z.left));
6105
6106
  const rights = new Set(zones.map((z) => z.right));
@@ -8454,31 +8455,34 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8454
8455
  for (let col of columnsIndexes) {
8455
8456
  const position = { col, row, sheetId };
8456
8457
  const table = this.getters.getTable(position);
8457
- if (!table || copiedTablesIds.has(table.id)) {
8458
+ if (!table) {
8458
8459
  tableCellsInRow.push({});
8459
8460
  continue;
8460
8461
  }
8461
8462
  const coreTable = this.getters.getCoreTable(position);
8462
8463
  const tableZone = coreTable?.range.zone;
8464
+ let copiedTable = undefined;
8463
8465
  // Copy whole table
8464
- if (coreTable && tableZone && zones.some((z) => isZoneInside(tableZone, z))) {
8465
- copiedTablesIds.add(coreTable.id);
8466
+ if (!copiedTablesIds.has(table.id) &&
8467
+ coreTable &&
8468
+ tableZone &&
8469
+ zones.some((z) => isZoneInside(tableZone, z))) {
8470
+ copiedTablesIds.add(table.id);
8466
8471
  const values = [];
8467
8472
  for (const col of range(tableZone.left, tableZone.right + 1)) {
8468
8473
  values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));
8469
8474
  }
8470
- tableCellsInRow.push({
8471
- table: {
8472
- range: coreTable.range.rangeData,
8473
- config: coreTable.config,
8474
- type: coreTable.type,
8475
- },
8476
- });
8477
- }
8478
- // Copy only style of cell
8479
- else if (table) {
8480
- tableCellsInRow.push({ style: this.getTableStyleToCopy(position) });
8475
+ copiedTable = {
8476
+ range: coreTable.range.rangeData,
8477
+ config: coreTable.config,
8478
+ type: coreTable.type,
8479
+ };
8481
8480
  }
8481
+ tableCellsInRow.push({
8482
+ table: copiedTable,
8483
+ style: this.getTableStyleToCopy(position),
8484
+ isWholeTableCopied: copiedTablesIds.has(table.id),
8485
+ });
8482
8486
  }
8483
8487
  }
8484
8488
  return {
@@ -8559,11 +8563,14 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8559
8563
  tableType: tableCell.table.type,
8560
8564
  });
8561
8565
  }
8562
- // Do not paste table style if we're inside another table
8563
8566
  // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
8564
8567
  // dynamic tables are not yet computed
8565
- if (!this.getters.getCoreTable(position)) {
8566
- if (tableCell.style?.style && options?.pasteOption !== "asValue") {
8568
+ if (this.getters.getCoreTable(position) || options?.pasteOption === "asValue") {
8569
+ return;
8570
+ }
8571
+ if ((!options?.pasteOption && !tableCell.isWholeTableCopied) ||
8572
+ options?.pasteOption === "onlyFormat") {
8573
+ if (tableCell.style?.style) {
8567
8574
  this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
8568
8575
  }
8569
8576
  if (tableCell.style?.border) {
@@ -17843,7 +17850,7 @@ function assertDomainLength(domain) {
17843
17850
  throw new EvaluationError(_t("Function PIVOT takes an even number of arguments."));
17844
17851
  }
17845
17852
  }
17846
- function addPivotDependencies(evalContext, coreDefinition) {
17853
+ function addPivotDependencies(evalContext, coreDefinition, forMeasures) {
17847
17854
  //TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER
17848
17855
  const dependencies = [];
17849
17856
  if (coreDefinition.type === "SPREADSHEET" && coreDefinition.dataSet) {
@@ -17855,7 +17862,7 @@ function addPivotDependencies(evalContext, coreDefinition) {
17855
17862
  }
17856
17863
  dependencies.push(range);
17857
17864
  }
17858
- for (const measure of coreDefinition.measures) {
17865
+ for (const measure of forMeasures) {
17859
17866
  if (measure.computedBy) {
17860
17867
  const formula = evalContext.getters.getMeasureCompiledFormula(measure);
17861
17868
  dependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));
@@ -18288,7 +18295,7 @@ const PIVOT_VALUE = {
18288
18295
  assertDomainLength(domainArgs);
18289
18296
  const pivot = this.getters.getPivot(pivotId);
18290
18297
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18291
- addPivotDependencies(this, coreDefinition);
18298
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures.filter((m) => m.id === _measure));
18292
18299
  pivot.init({ reload: pivot.needsReevaluation });
18293
18300
  const error = pivot.assertIsValid({ throwOnError: false });
18294
18301
  if (error) {
@@ -18318,7 +18325,7 @@ const PIVOT_HEADER = {
18318
18325
  assertDomainLength(domainArgs);
18319
18326
  const pivot = this.getters.getPivot(_pivotId);
18320
18327
  const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);
18321
- addPivotDependencies(this, coreDefinition);
18328
+ addPivotDependencies(this, coreDefinition, []);
18322
18329
  pivot.init({ reload: pivot.needsReevaluation });
18323
18330
  const error = pivot.assertIsValid({ throwOnError: false });
18324
18331
  if (error) {
@@ -18369,7 +18376,7 @@ const PIVOT = {
18369
18376
  const pivotId = getPivotId(_pivotFormulaId, this.getters);
18370
18377
  const pivot = this.getters.getPivot(pivotId);
18371
18378
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18372
- addPivotDependencies(this, coreDefinition);
18379
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures);
18373
18380
  pivot.init({ reload: pivot.needsReevaluation });
18374
18381
  const error = pivot.assertIsValid({ throwOnError: false });
18375
18382
  if (error) {
@@ -21578,6 +21585,7 @@ function insertTokenAfterArgSeparator(tokenAtCursor, value) {
21578
21585
  // replace the whole token
21579
21586
  start = tokenAtCursor.start;
21580
21587
  }
21588
+ this.composer.stopComposerRangeSelection();
21581
21589
  this.composer.changeComposerCursorSelection(start, end);
21582
21590
  this.composer.replaceComposerCursorSelection(value);
21583
21591
  }
@@ -21595,6 +21603,7 @@ function insertTokenAfterLeftParenthesis(tokenAtCursor, value) {
21595
21603
  // replace the whole token
21596
21604
  start = tokenAtCursor.start;
21597
21605
  }
21606
+ this.composer.stopComposerRangeSelection();
21598
21607
  this.composer.changeComposerCursorSelection(start, end);
21599
21608
  this.composer.replaceComposerCursorSelection(value);
21600
21609
  }
@@ -22187,6 +22196,27 @@ autofillModifiersRegistry
22187
22196
  tooltip: content ? { props: { content: tooltipValue } } : undefined,
22188
22197
  };
22189
22198
  },
22199
+ })
22200
+ .add("DATE_INCREMENT_MODIFIER", {
22201
+ apply: (rule, data, getters) => {
22202
+ const date = toJsDate(rule.current, getters.getLocale());
22203
+ date.setFullYear(date.getFullYear() + rule.increment.years || 0);
22204
+ date.setMonth(date.getMonth() + rule.increment.months || 0);
22205
+ date.setDate(date.getDate() + rule.increment.days || 0);
22206
+ const value = jsDateToNumber(date);
22207
+ rule.current = value;
22208
+ const locale = getters.getLocale();
22209
+ const tooltipValue = formatValue(value, { format: data.cell?.format, locale });
22210
+ return {
22211
+ cellData: {
22212
+ border: data.border,
22213
+ style: data.cell && data.cell.style,
22214
+ format: data.cell && data.cell.format,
22215
+ content: value.toString(),
22216
+ },
22217
+ tooltip: value ? { props: { content: tooltipValue } } : undefined,
22218
+ };
22219
+ },
22190
22220
  })
22191
22221
  .add("COPY_MODIFIER", {
22192
22222
  apply: (rule, data, getters) => {
@@ -22267,7 +22297,9 @@ function getGroup(cell, cells, filter) {
22267
22297
  if (x === cell) {
22268
22298
  found = true;
22269
22299
  }
22270
- const cellValue = x === undefined || x.isFormula ? undefined : evaluateLiteral(x, { locale: DEFAULT_LOCALE });
22300
+ const cellValue = x === undefined || x.isFormula
22301
+ ? undefined
22302
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22271
22303
  if (cellValue && filter(cellValue)) {
22272
22304
  group.push(cellValue);
22273
22305
  }
@@ -22303,6 +22335,72 @@ function calculateIncrementBasedOnGroup(group) {
22303
22335
  }
22304
22336
  return increment;
22305
22337
  }
22338
+ /**
22339
+ * Iterates on a list of date intervals.
22340
+ * if every interval is the same, return the interval
22341
+ * Otherwise return undefined
22342
+ *
22343
+ */
22344
+ function getEqualInterval(intervals) {
22345
+ if (intervals.length < 2) {
22346
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22347
+ }
22348
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22349
+ interval.months === intervals[0].months &&
22350
+ interval.days === intervals[0].days);
22351
+ return equal ? intervals[0] : undefined;
22352
+ }
22353
+ /**
22354
+ * Based on a group of dates, calculate the increment that should be applied
22355
+ * to the next date.
22356
+ *
22357
+ * This will compute the date difference in calendar terms (years, months, days)
22358
+ * In order to make abstraction of leap years and months with different number of days.
22359
+ *
22360
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22361
+ * In case of equidistant dates, we either have in that order:
22362
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22363
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22364
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22365
+ *
22366
+ * */
22367
+ function calculateDateIncrementBasedOnGroup(group) {
22368
+ if (group.length < 2) {
22369
+ return 1;
22370
+ }
22371
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22372
+ const datesIntervals = getDateIntervals(jsDates);
22373
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22374
+ if (datesEquidistantInterval === undefined) {
22375
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22376
+ return undefined;
22377
+ }
22378
+ // The dates are apart by an exact interval of years, months or days
22379
+ // but not a combination of them
22380
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22381
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22382
+ if (!exactDateInterval || isSameDay) {
22383
+ const timeIntervals = jsDates
22384
+ .map((date, index) => {
22385
+ if (index === 0) {
22386
+ return 0;
22387
+ }
22388
+ const previous = jsDates[index - 1];
22389
+ const days = Math.floor(date.getTime()) - Math.floor(previous.getTime());
22390
+ return days;
22391
+ })
22392
+ .slice(1);
22393
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22394
+ if (equidistantDates) {
22395
+ return group.length * (group[1] - group[0]);
22396
+ }
22397
+ }
22398
+ return {
22399
+ years: datesEquidistantInterval.years * group.length,
22400
+ months: datesEquidistantInterval.months * group.length,
22401
+ days: datesEquidistantInterval.days * group.length,
22402
+ };
22403
+ }
22306
22404
  autofillRulesRegistry
22307
22405
  .add("simple_value_copy", {
22308
22406
  condition: (cell, cells) => {
@@ -22350,12 +22448,47 @@ autofillRulesRegistry
22350
22448
  return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22351
22449
  },
22352
22450
  sequence: 30,
22451
+ })
22452
+ .add("increment_dates", {
22453
+ condition: (cell, cells) => {
22454
+ return (!cell.isFormula &&
22455
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22456
+ !!cell.format &&
22457
+ isDateTimeFormat(cell.format));
22458
+ },
22459
+ generateRule: (cell, cells) => {
22460
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22461
+ !!evaluatedCell.format &&
22462
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22463
+ const increment = calculateDateIncrementBasedOnGroup(group);
22464
+ if (increment === undefined) {
22465
+ return { type: "COPY_MODIFIER" };
22466
+ }
22467
+ /** requires to detect the current date (requires to be an integer value with the right format)
22468
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22469
+ */
22470
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22471
+ if (typeof increment === "object") {
22472
+ return {
22473
+ type: "DATE_INCREMENT_MODIFIER",
22474
+ increment,
22475
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22476
+ };
22477
+ }
22478
+ return {
22479
+ type: "INCREMENT_MODIFIER",
22480
+ increment,
22481
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22482
+ };
22483
+ },
22484
+ sequence: 25,
22353
22485
  })
22354
22486
  .add("increment_number", {
22355
22487
  condition: (cell) => !cell.isFormula &&
22356
22488
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22357
22489
  generateRule: (cell, cells) => {
22358
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
22490
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22491
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22359
22492
  const increment = calculateIncrementBasedOnGroup(group);
22360
22493
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22361
22494
  return {
@@ -22366,6 +22499,37 @@ autofillRulesRegistry
22366
22499
  },
22367
22500
  sequence: 40,
22368
22501
  });
22502
+ /**
22503
+ * Returns the date intervals between consecutive dates of an array
22504
+ * in the format of { years: number, months: number, days: number }
22505
+ *
22506
+ * The split is necessary to make abstraction of leap years and
22507
+ * months with different number of days.
22508
+ *
22509
+ * @param dates
22510
+ */
22511
+ function getDateIntervals(dates) {
22512
+ if (dates.length < 2) {
22513
+ return [{ years: 0, months: 0, days: 0 }];
22514
+ }
22515
+ const res = dates.map((date, index) => {
22516
+ if (index === 0) {
22517
+ return { years: 0, months: 0, days: 0 };
22518
+ }
22519
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22520
+ const years = getTimeDifferenceInWholeYears(previous, date);
22521
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22522
+ previous.setFullYear(previous.getFullYear() + years);
22523
+ previous.setMonth(previous.getMonth() + months);
22524
+ const days = getTimeDifferenceInWholeDays(previous, date);
22525
+ return {
22526
+ years,
22527
+ months,
22528
+ days,
22529
+ };
22530
+ });
22531
+ return res.slice(1);
22532
+ }
22369
22533
 
22370
22534
  const cellPopoverRegistry = new Registry();
22371
22535
 
@@ -28900,6 +29064,7 @@ class ComboChart extends AbstractChart {
28900
29064
  ranges.push({
28901
29065
  ...this.dataSetDesign?.[i],
28902
29066
  dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
29067
+ type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"),
28903
29068
  });
28904
29069
  }
28905
29070
  return {
@@ -28945,9 +29110,13 @@ class ComboChart extends AbstractChart {
28945
29110
  return new ComboChart(definition, this.sheetId, this.getters);
28946
29111
  }
28947
29112
  static getDefinitionFromContextCreation(context) {
29113
+ const dataSets = (context.range ?? []).map((ds, index) => ({
29114
+ ...ds,
29115
+ type: index ? "line" : "bar",
29116
+ }));
28948
29117
  return {
28949
29118
  background: context.background,
28950
- dataSets: context.range ?? [],
29119
+ dataSets,
28951
29120
  dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
28952
29121
  aggregated: context.aggregated,
28953
29122
  legendPosition: context.legendPosition ?? "top",
@@ -28992,7 +29161,6 @@ function createComboChartRuntime(chart, getters) {
28992
29161
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
28993
29162
  const legend = {
28994
29163
  labels: { color: fontColor },
28995
- reverse: true,
28996
29164
  };
28997
29165
  if (chart.legendPosition === "none") {
28998
29166
  legend.display = false;
@@ -29060,14 +29228,15 @@ function createComboChartRuntime(chart, getters) {
29060
29228
  for (let [index, { label, data }] of dataSetsValues.entries()) {
29061
29229
  const design = definition.dataSets[index];
29062
29230
  const color = colors.next();
29231
+ const type = design?.type ?? "line";
29063
29232
  const dataset = {
29064
29233
  label: design?.label ?? label,
29065
29234
  data,
29066
29235
  borderColor: color,
29067
29236
  backgroundColor: color,
29068
29237
  yAxisID: design?.yAxisId ?? "y",
29069
- type: index === 0 ? "bar" : "line",
29070
- order: -index,
29238
+ type,
29239
+ order: type === "bar" ? dataSetsValues.length + index : index,
29071
29240
  };
29072
29241
  config.data.datasets.push(dataset);
29073
29242
  const trend = definition.dataSets?.[index].trend;
@@ -35296,6 +35465,7 @@ const CHECK_SVG = /*xml*/ `
35296
35465
  <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35297
35466
  </svg>
35298
35467
  `;
35468
+ const CHECKBOX_WIDTH = 14;
35299
35469
  css /* scss */ `
35300
35470
  label.o-checkbox {
35301
35471
  input {
@@ -35303,8 +35473,8 @@ css /* scss */ `
35303
35473
  -webkit-appearance: none;
35304
35474
  -moz-appearance: none;
35305
35475
  border-radius: 0;
35306
- width: 14px;
35307
- height: 14px;
35476
+ width: ${CHECKBOX_WIDTH}px;
35477
+ height: ${CHECKBOX_WIDTH}px;
35308
35478
  vertical-align: top;
35309
35479
  box-sizing: border-box;
35310
35480
  outline: none;
@@ -37214,6 +37384,32 @@ class ChartWithAxisDesignPanel extends owl.Component {
37214
37384
  }
37215
37385
  }
37216
37386
 
37387
+ class ComboChartDesignPanel extends ChartWithAxisDesignPanel {
37388
+ static template = "o-spreadsheet-ComboChartDesignPanel";
37389
+ seriesTypeChoices = [
37390
+ { value: "bar", label: _t("Bar") },
37391
+ { value: "line", label: _t("Line") },
37392
+ ];
37393
+ updateDataSeriesType(type) {
37394
+ const dataSets = [...this.props.definition.dataSets];
37395
+ if (!dataSets?.[this.state.index]) {
37396
+ return;
37397
+ }
37398
+ dataSets[this.state.index] = {
37399
+ ...dataSets[this.state.index],
37400
+ type,
37401
+ };
37402
+ this.props.updateChart(this.props.figureId, { dataSets });
37403
+ }
37404
+ getDataSeriesType() {
37405
+ const dataSets = this.props.definition.dataSets;
37406
+ if (!dataSets?.[this.state.index]) {
37407
+ return "bar";
37408
+ }
37409
+ return dataSets[this.state.index].type ?? "line";
37410
+ }
37411
+ }
37412
+
37217
37413
  class GaugeChartConfigPanel extends owl.Component {
37218
37414
  static template = "o-spreadsheet-GaugeChartConfigPanel";
37219
37415
  static components = { ChartErrorSection, ChartDataSeries };
@@ -37640,7 +37836,7 @@ chartSidePanelComponentRegistry
37640
37836
  })
37641
37837
  .add("combo", {
37642
37838
  configuration: GenericChartConfigPanel,
37643
- design: ChartWithAxisDesignPanel,
37839
+ design: ComboChartDesignPanel,
37644
37840
  })
37645
37841
  .add("pie", {
37646
37842
  configuration: GenericChartConfigPanel,
@@ -42819,8 +43015,9 @@ const EMPTY_PIVOT_CELL = { type: "EMPTY" };
42819
43015
  * This function converts a list of data entry into a spreadsheet pivot table.
42820
43016
  */
42821
43017
  function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
43018
+ const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42822
43019
  const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
42823
- computeWidthOfColumnsNodes(columnsTree, definition.measures.length);
43020
+ computeWidthOfColumnsNodes(columnsTree, measureIds.length);
42824
43021
  const cols = columnsTreeToColumns(columnsTree, definition);
42825
43022
  const rows = dataEntriesToRows(dataEntries, 0, definition.rows, [], []);
42826
43023
  // Add the total row
@@ -42829,7 +43026,6 @@ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
42829
43026
  values: [],
42830
43027
  indent: 0,
42831
43028
  });
42832
- const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42833
43029
  const fieldsType = {};
42834
43030
  for (const columns of definition.columns) {
42835
43031
  fieldsType[columns.fieldName] = columns.type;
@@ -43590,7 +43786,7 @@ pivotRegistry.add("SPREADSHEET", {
43590
43786
  onIterationEndEvaluation: (pivot) => pivot.markAsDirtyForEvaluation(),
43591
43787
  dateGranularities: [...dateGranularities],
43592
43788
  datetimeGranularities: [...dateGranularities, "hour_number", "minute_number", "second_number"],
43593
- isMeasureCandidate: (field) => !["date", "boolean"].includes(field.type),
43789
+ isMeasureCandidate: (field) => !["datetime", "boolean"].includes(field.type),
43594
43790
  isGroupable: () => true,
43595
43791
  });
43596
43792
 
@@ -46119,13 +46315,10 @@ class GridCellIcon extends owl.Component {
46119
46315
  }
46120
46316
  }
46121
46317
 
46122
- const CHECKBOX_WIDTH = 15;
46123
46318
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
46124
46319
  css /* scss */ `
46125
46320
  .o-dv-checkbox {
46126
46321
  box-sizing: border-box !important;
46127
- width: ${CHECKBOX_WIDTH}px;
46128
- height: ${CHECKBOX_WIDTH}px;
46129
46322
  accent-color: #808080;
46130
46323
  margin: ${MARGIN}px;
46131
46324
  /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
@@ -46134,13 +46327,15 @@ css /* scss */ `
46134
46327
  `;
46135
46328
  class DataValidationCheckbox extends owl.Component {
46136
46329
  static template = "o-spreadsheet-DataValidationCheckbox";
46330
+ static components = {
46331
+ Checkbox,
46332
+ };
46137
46333
  static props = {
46138
46334
  cellPosition: Object,
46139
46335
  };
46140
- onCheckboxChange(ev) {
46141
- const newValue = ev.target.checked;
46336
+ onCheckboxChange(value) {
46142
46337
  const { sheetId, col, row } = this.props.cellPosition;
46143
- const cellContent = newValue ? "TRUE" : "FALSE";
46338
+ const cellContent = value ? "TRUE" : "FALSE";
46144
46339
  this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
46145
46340
  }
46146
46341
  get checkBoxValue() {
@@ -46954,7 +47149,11 @@ class GridAddRowsFooter extends owl.Component {
46954
47149
  class PaintFormatStore extends SpreadsheetStore {
46955
47150
  mutators = ["activate", "cancel", "pasteFormat"];
46956
47151
  highlightStore = this.get(HighlightStore);
46957
- cellClipboardHandler = new CellClipboardHandler(this.getters, this.model.dispatch);
47152
+ clipboardHandlers = [
47153
+ new CellClipboardHandler(this.getters, this.model.dispatch),
47154
+ new BorderClipboardHandler(this.getters, this.model.dispatch),
47155
+ new TableClipboardHandler(this.getters, this.model.dispatch),
47156
+ ];
46958
47157
  status = "inactive";
46959
47158
  copiedData;
46960
47159
  constructor(get) {
@@ -46975,10 +47174,12 @@ class PaintFormatStore extends SpreadsheetStore {
46975
47174
  pasteFormat(target) {
46976
47175
  if (this.copiedData) {
46977
47176
  const sheetId = this.getters.getActiveSheetId();
46978
- this.cellClipboardHandler.paste({ zones: target, sheetId }, this.copiedData, {
46979
- isCutOperation: false,
46980
- pasteOption: "onlyFormat",
46981
- });
47177
+ for (const handler of this.clipboardHandlers) {
47178
+ handler.paste({ zones: target, sheetId }, this.copiedData, {
47179
+ isCutOperation: false,
47180
+ pasteOption: "onlyFormat",
47181
+ });
47182
+ }
46982
47183
  }
46983
47184
  if (this.status === "oneOff") {
46984
47185
  this.cancel();
@@ -46990,7 +47191,11 @@ class PaintFormatStore extends SpreadsheetStore {
46990
47191
  copyFormats() {
46991
47192
  const sheetId = this.getters.getActiveSheetId();
46992
47193
  const zones = this.getters.getSelectedZones();
46993
- return this.cellClipboardHandler.copy(getClipboardDataPositions(sheetId, zones));
47194
+ const copiedData = {};
47195
+ for (const handler of this.clipboardHandlers) {
47196
+ Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones)));
47197
+ }
47198
+ return copiedData;
46994
47199
  }
46995
47200
  get highlights() {
46996
47201
  const data = this.copiedData;
@@ -59052,7 +59257,7 @@ function withPivotPresentationLayer (PivotClass) {
59052
59257
  const ranking = {};
59053
59258
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59054
59259
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59055
- let pivotCells = this.getPivotValueCells();
59260
+ let pivotCells = this.getPivotValueCells(measure.id);
59056
59261
  if (mainDimension === "column") {
59057
59262
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59058
59263
  // This means that we need to transpose back the ranking at the end
@@ -59096,7 +59301,7 @@ function withPivotPresentationLayer (PivotClass) {
59096
59301
  const cellsRunningTotals = {};
59097
59302
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59098
59303
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59099
- let pivotCells = this.getPivotValueCells();
59304
+ let pivotCells = this.getPivotValueCells(measure.id);
59100
59305
  if (mainDimension === "column") {
59101
59306
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59102
59307
  // This means that we need to transpose back the totals at the end
@@ -59175,10 +59380,10 @@ function withPivotPresentationLayer (PivotClass) {
59175
59380
  const comparedValueNumber = this.strictMeasureValueToNumber(comparedValue);
59176
59381
  return comparedValueNumber;
59177
59382
  }
59178
- getPivotValueCells() {
59383
+ getPivotValueCells(measureId) {
59179
59384
  return this.getTableStructure()
59180
59385
  .getPivotCells()
59181
- .map((col) => col.filter((cell) => cell.type === "VALUE"))
59386
+ .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
59182
59387
  .filter((col) => col.length > 0);
59183
59388
  }
59184
59389
  measureValueToNumber(result) {
@@ -60946,6 +61151,7 @@ class Session extends EventBus {
60946
61151
  case "REMOTE_REVISION":
60947
61152
  case "REVISION_REDONE":
60948
61153
  case "REVISION_UNDONE":
61154
+ case "SNAPSHOT_CREATED":
60949
61155
  return this.processedRevisions.has(message.nextRevisionId);
60950
61156
  default:
60951
61157
  return false;
@@ -61508,7 +61714,7 @@ class InsertPivotPlugin extends UIPlugin {
61508
61714
  const position = this.getters.getSheetIds().indexOf(activeSheetId) + 1;
61509
61715
  const formulaId = this.getters.getPivotFormulaId(newPivotId);
61510
61716
  const newPivotName = this.getters.getPivotName(newPivotId);
61511
- this.dispatch("CREATE_SHEET", {
61717
+ const result = this.dispatch("CREATE_SHEET", {
61512
61718
  sheetId: newSheetId,
61513
61719
  name: this.getPivotDuplicateSheetName(_t("%(newPivotName)s (Pivot #%(formulaId)s)", {
61514
61720
  newPivotName,
@@ -61516,20 +61722,19 @@ class InsertPivotPlugin extends UIPlugin {
61516
61722
  })),
61517
61723
  position,
61518
61724
  });
61519
- this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61520
- this.dispatch("UPDATE_CELL", {
61521
- sheetId: newSheetId,
61522
- col: 0,
61523
- row: 0,
61524
- content: `=PIVOT(${formulaId})`,
61525
- });
61725
+ if (result.isSuccessful) {
61726
+ this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61727
+ const pivot = this.getters.getPivot(pivotId);
61728
+ this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), "dynamic");
61729
+ }
61526
61730
  }
61527
61731
  getPivotDuplicateSheetName(pivotName) {
61528
61732
  let i = 1;
61529
61733
  const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
61530
- let name = pivotName;
61734
+ const sanitizedName = pivotName.replace(new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g"), " ");
61735
+ let name = sanitizedName;
61531
61736
  while (names.includes(name)) {
61532
- name = `${pivotName} (${i})`;
61737
+ name = `${sanitizedName} (${i})`;
61533
61738
  i++;
61534
61739
  }
61535
61740
  return name;
@@ -70882,6 +71087,15 @@ function addTableColumns(table, sheetData) {
70882
71087
  ["id", i + 1], // id cannot be 0
70883
71088
  ["name", colName],
70884
71089
  ];
71090
+ if (table.config.totalRow) {
71091
+ // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag
71092
+ // `<totalsRowFormula>` for the formula of the total. But those doesn't seem to be mandatory for Excel.
71093
+ const colTotalXc = toXC(tableZone.left + i, tableZone.bottom);
71094
+ const colTotalContent = sheetData.cells[colTotalXc]?.content;
71095
+ if (colTotalContent?.startsWith("=")) {
71096
+ colAttributes.push(["totalsRowFunction", "custom"]);
71097
+ }
71098
+ }
70885
71099
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
70886
71100
  }
70887
71101
  return escapeXml /*xml*/ `
@@ -70976,8 +71190,9 @@ function addRows(construct, data, sheet) {
70976
71190
  }
70977
71191
  else if (cell.content && cell.content !== "") {
70978
71192
  const isTableHeader = isCellTableHeader(c, r, sheet);
71193
+ const isTableTotal = isCellTableTotal(c, r, sheet);
70979
71194
  const isPlainText = !!(cell.format && isTextFormat(data.formats[cell.format]));
70980
- ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isPlainText));
71195
+ ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isTableTotal || isPlainText));
70981
71196
  }
70982
71197
  attributes.push(...additionalAttrs);
70983
71198
  // prettier-ignore
@@ -71011,6 +71226,16 @@ function isCellTableHeader(col, row, sheet) {
71011
71226
  return isInside(col, row, headerZone);
71012
71227
  });
71013
71228
  }
71229
+ function isCellTableTotal(col, row, sheet) {
71230
+ return sheet.tables.some((table) => {
71231
+ if (!table.config.totalRow) {
71232
+ return false;
71233
+ }
71234
+ const zone = toZone(table.range);
71235
+ const totalZone = { ...zone, top: zone.bottom };
71236
+ return isInside(col, row, totalZone);
71237
+ });
71238
+ }
71014
71239
  function addHyperlinks(construct, data, sheetIndex) {
71015
71240
  const sheet = data.sheets[sheetIndex];
71016
71241
  const cells = sheet.cells;
@@ -71501,7 +71726,6 @@ class Model extends EventBus {
71501
71726
  isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
71502
71727
  isDashboard: () => this.config.mode === "dashboard",
71503
71728
  };
71504
- this.uuidGenerator.setIsFastStrategy(true);
71505
71729
  // Initiate stream processor
71506
71730
  this.selection = new SelectionStreamProcessorImpl(this.getters);
71507
71731
  this.coreHandlers.push(this.range);
@@ -72175,6 +72399,6 @@ exports.tokenColors = tokenColors;
72175
72399
  exports.tokenize = tokenize;
72176
72400
 
72177
72401
 
72178
- __info__.version = "18.0.0";
72179
- __info__.date = "2024-09-25T12:54:19.974Z";
72180
- __info__.hash = "cee2e47";
72402
+ __info__.version = "18.0.1";
72403
+ __info__.date = "2024-10-14T07:54:24.768Z";
72404
+ __info__.hash = "1771f68";