@odoo/o-spreadsheet 17.2.4 → 17.2.5

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.
@@ -3,9 +3,9 @@
3
3
  /**
4
4
  * This file is generated by o-spreadsheet build tools. Do not edit it.
5
5
  * @see https://github.com/odoo/o-spreadsheet
6
- * @version 17.2.4
7
- * @date 2024-04-18T16:41:38.407Z
8
- * @hash 0c66038
6
+ * @version 17.2.5
7
+ * @date 2024-04-26T07:41:13.193Z
8
+ * @hash a730f5c
9
9
  */
10
10
 
11
11
  import { reactive, useEnv, useSubEnv, useState, onWillUnmount, markRaw, toRaw, Component, useRef, onMounted, useEffect, onPatched, useComponent, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv } from '@odoo/owl';
@@ -32071,17 +32071,13 @@ class Composer extends Component {
32071
32071
  this.DOMFocusableElementStore.setFocusableElement(el);
32072
32072
  }
32073
32073
  this.contentHelper.updateEl(el);
32074
- this.processTokenAtCursor();
32075
32074
  });
32076
32075
  useEffect(() => {
32077
32076
  this.processContent();
32078
32077
  });
32079
- onPatched(() => {
32080
- // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider/ShowDescription true
32081
- if (this.composerStore.editionMode === "inactive") {
32082
- this.processTokenAtCursor();
32083
- }
32084
- });
32078
+ useEffect(() => {
32079
+ this.processTokenAtCursor();
32080
+ }, () => [this.composerStore.editionMode !== "inactive"]);
32085
32081
  }
32086
32082
  // ---------------------------------------------------------------------------
32087
32083
  // Handlers
@@ -40378,7 +40374,7 @@ class BordersPlugin extends CorePlugin {
40378
40374
  this.clearBorders(cmd.sheetId, cmd.target);
40379
40375
  break;
40380
40376
  case "REMOVE_COLUMNS_ROWS":
40381
- for (let el of cmd.elements) {
40377
+ for (let el of [...cmd.elements].sort((a, b) => b - a)) {
40382
40378
  if (cmd.dimension === "COL") {
40383
40379
  this.shiftBordersHorizontally(cmd.sheetId, el + 1, -1);
40384
40380
  }
@@ -45917,6 +45913,353 @@ class CompilationParametersBuilder {
45917
45913
  }
45918
45914
  }
45919
45915
 
45916
+ /**
45917
+ * ####################################################
45918
+ * # INTRODUCTION
45919
+ * ####################################################
45920
+ *
45921
+ * This file contain the function recomputeZones.
45922
+ * This function try to recompute in a performant way
45923
+ * an ensemble of zones possibly overlapping to avoid
45924
+ * overlapping and to reduce the number of zones.
45925
+ *
45926
+ * It also allows to remove some zones from the ensemble.
45927
+ *
45928
+ * In the following example, 2 zones are overlapping.
45929
+ * Applying recomputeZones will return zones without
45930
+ * overlapping:
45931
+ *
45932
+ * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
45933
+ *
45934
+ * A B C D E A B C D E
45935
+ * 1 ___ 1 ___
45936
+ * 2 ___|_ | 2 ___| | |
45937
+ * 3 | |_|_| ---> 3 | | |_|
45938
+ * 4 |_____| 4 |___|_|
45939
+ * 6 6
45940
+ * 7 7
45941
+ *
45942
+ *
45943
+ * In the following example, 2 zones are contiguous.
45944
+ * Applying recomputeZones will return only one zone:
45945
+ *
45946
+ * ["B2:B3", "C2:D3"] ["B2:D3"]
45947
+ *
45948
+ * A B C D E A B C D E
45949
+ * 1 _ ___ 1 _____
45950
+ * 2 | | | ---> 2 | |
45951
+ * 3 |_|___| 3 |_____|
45952
+ * 4 4
45953
+ *
45954
+ *
45955
+ * In the following example, we want to remove a zone
45956
+ * from the ensemble. Applying recomputeZones will
45957
+ * return the ensemble without the zone to remove:
45958
+ *
45959
+ * remove ["C3:D3"] ["B2:B4", "C2:D2",
45960
+ * "C4:D4", "E2:E4"]
45961
+ *
45962
+ * A B C D E F A B C D E F
45963
+ * 1 _______ 1 _______
45964
+ * 2 | | ---> 2 | |___| |
45965
+ * 3 | xxx | 3 | |___| |
45966
+ * 4 |_______| 4 |_|___|_|
45967
+ * 5 5
45968
+ *
45969
+ *
45970
+ * The exercise seems simple when we have only 2 zones.
45971
+ * But with n zones and in a performant way, we want to
45972
+ * avoid comparing each zone with all the others.
45973
+ *
45974
+ *
45975
+ * ####################################################
45976
+ * # Methodological approach
45977
+ * ####################################################
45978
+ *
45979
+ * The methodological approach to avoid comparing each
45980
+ * zone with all the others is to use a data structure
45981
+ * that allow to quickly find which zones are
45982
+ * overlapping with any other given zone.
45983
+ *
45984
+ * Here the idea is to profile the zones at the columns level.
45985
+ *
45986
+ * To do that, we propose to use a data structure
45987
+ * composed of 2 parts:
45988
+ * - profilesStartingPosition: a sorted number array
45989
+ * indicating on which columns a new profile begins.
45990
+ * - profiles: a map where the key is a column
45991
+ * position (from profilesStartingPosition) and the
45992
+ * value is a sorted number array representing a
45993
+ * profile.
45994
+ *
45995
+ *
45996
+ * See the following example: here profileStartingPosition
45997
+ * corresponds to [A,C,E,G,K]
45998
+ * A B C D E F G H I J K so with number [0,2,4,6,10]
45999
+ * 1 ' ' ' '
46000
+ * 2 ' ' '_______' here profile correspond
46001
+ * 3 '___' |_______| for A to []
46002
+ * 4 | | for C to [3, 5]
46003
+ * 5 |___| for E to []
46004
+ * 6 for G to [2, 3]
46005
+ * 7 for K to []
46006
+ *
46007
+ *
46008
+ * Now we can easily find which zones are overlapping
46009
+ * with a given zone. Suppose we want to add a new zone
46010
+ * D5:H6 to the ensemble:
46011
+ *
46012
+ * With a binary search of left and right
46013
+ * A B C D E F G H I J K on profilesStartingPosition, we can
46014
+ * 1 ' ' ' ' find the indexes of the profiles on which
46015
+ * 2 ' ' '_______' to apply a modification.
46016
+ * 3 '___' |_______|
46017
+ * 4 | _|_______ Here we will:
46018
+ * 5 |_|_| | - add a new profile in D --> become [3, 6]
46019
+ * 6 |_________| - modify the profile in E --> become [4, 6]
46020
+ * 7 - modify the profile in G --> become [2, 3, 4, 6]
46021
+ * - add a new profile in I --> become [8, 10]
46022
+ *
46023
+ * See below the result:
46024
+ *
46025
+ * Note the particularity of the profile
46026
+ * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
46027
+ * 1 ' ' ' ' ' '
46028
+ * 2 ' ' ' '___'___' To know how to modify the profile (add a
46029
+ * 3 '_'_' |___|___| zone or remove it) we do a binary
46030
+ * 4 | | |___ ___ search of the top and bottom value on the
46031
+ * 5 |_| | | | profile array. Depending on the result index
46032
+ * 6 |_|___|___| parity (odd or even), because zone boundaries
46033
+ * 7 go by pairs, we know if we are in a zone or
46034
+ * not and how operate.
46035
+ */
46036
+ /**
46037
+ * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
46038
+ * This compute is particularly useful because after this function:
46039
+ * - you will find coordinate of a cell only once among all the zones
46040
+ * - the number of zones will be reduced to the minimum
46041
+ */
46042
+ function futureRecomputeZones(zones, zonesToRemove = []) {
46043
+ const profilesStartingPosition = [0];
46044
+ const profiles = new Map([[0, []]]);
46045
+ modifyProfiles(profilesStartingPosition, profiles, zones, false);
46046
+ modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
46047
+ return constructZonesFromProfiles(profilesStartingPosition, profiles);
46048
+ }
46049
+ function modifyProfiles(// export for testing only
46050
+ profilesStartingPosition, profiles, zones, toRemove = false) {
46051
+ for (const zone of zones) {
46052
+ const leftValue = zone.left;
46053
+ const rightValue = zone.right === undefined ? undefined : zone.right + 1;
46054
+ const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
46055
+ const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
46056
+ for (let i = leftIndex; i <= rightIndex; i++) {
46057
+ const profile = profiles.get(profilesStartingPosition[i]);
46058
+ modifyProfile(profile, zone, toRemove);
46059
+ }
46060
+ // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
46061
+ removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
46062
+ }
46063
+ }
46064
+ function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
46065
+ if (value === undefined) {
46066
+ // this is only the case when the value correspond to a bottom value that could be undefined
46067
+ return profilesStartingPosition.length - 1;
46068
+ }
46069
+ const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
46070
+ if (value != profilesStartingPosition[predecessorIndex]) {
46071
+ // mean that the value is not ending/starting at the same position as the previous/next profile
46072
+ // --> it's a new profile
46073
+ // --> we need to add it
46074
+ profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
46075
+ // suppose the we want to add the for the left value
46076
+ // following profile following zone: 'C', the predecessor index
46077
+ // for B: [1, 3] "C3:D4" correspond to 'B'.
46078
+ // The next line code will
46079
+ // A B C D A B C D copy the profile of 'B'
46080
+ // 1 '___' 1 '___' to 'C'. In the rest of the
46081
+ // 2 | | ---> 2 | _|_ process the 'modifyProfile'
46082
+ // 3 |___| 3 |_|_| | function will adapt the waiting
46083
+ // 4 4 |___| 'C' profile [1, 3] to the
46084
+ // correct 'C' profile [1, 4]
46085
+ profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
46086
+ return searchLeft ? predecessorIndex + 1 : predecessorIndex;
46087
+ }
46088
+ return searchLeft ? predecessorIndex : predecessorIndex - 1;
46089
+ }
46090
+ /**
46091
+ * Suppose the following Suppose we want to add We want to have the
46092
+ * profile: the following zone: following profile:
46093
+ *
46094
+ * A B C D E F A B C D E F A B C D E F
46095
+ * 1 '___' 1 ' ' 1 '___'
46096
+ * 2 |___| 2 '___' 2 | |
46097
+ * 3 ' ' 3 | | 3 | |
46098
+ * 4 '___' --> 4 | | --> 4 | |
46099
+ * 6 | | 6 |___| 6 | |
46100
+ * 7 |___| 7 7 |___|
46101
+ * 8 8 8
46102
+ *
46103
+ * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
46104
+ * corresponds to: to 3 and the bottom zone would be merged into [2, 8]
46105
+ * ____ ____ correspond to 6
46106
+ * [2, 3, 5, 8] would be the profile: The difficulty of modify profile
46107
+ * ____ is to know what must be deleted
46108
+ * Note that the 'filled [3, 7] and what must be added to the
46109
+ * zone' are always between existing profile.
46110
+ * an even index and its
46111
+ * next index
46112
+ *
46113
+ */
46114
+ function modifyProfile(profile, zone, toRemove = false) {
46115
+ const topValue = zone.top;
46116
+ const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
46117
+ const newPoints = [];
46118
+ // Case we want to add a zone to the profile:
46119
+ // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
46120
+ // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
46121
+ // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
46122
+ // so we can keep the index of the top position
46123
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
46124
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
46125
+ if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
46126
+ newPoints.push(topValue);
46127
+ }
46128
+ if (bottomValue === undefined) {
46129
+ // The following two code lines will not impact the final result,
46130
+ // but they will impact the intermediate profile.
46131
+ // We keep them for performance reason
46132
+ profile.splice(topPredIndex + 1);
46133
+ profile.push(...newPoints);
46134
+ return;
46135
+ }
46136
+ // Case we want to add a zone to the profile:
46137
+ // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
46138
+ // so we can keep the index of the bottom position.
46139
+ // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
46140
+ // so we don't need to add it to the profile. we can keep in reference the index of the successor
46141
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
46142
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
46143
+ if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
46144
+ newPoints.push(bottomValue);
46145
+ }
46146
+ // add the top and bottom value to the profile and
46147
+ // remove all information between the top and bottom index
46148
+ profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
46149
+ }
46150
+ function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
46151
+ const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
46152
+ const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
46153
+ for (let i = end; i > start; i--) {
46154
+ if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
46155
+ profiles.delete(profilesStartingPosition[i]);
46156
+ profilesStartingPosition.splice(i, 1);
46157
+ }
46158
+ }
46159
+ }
46160
+ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
46161
+ const mergedZone = [];
46162
+ let pendingZones = [];
46163
+ for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
46164
+ const left = profilesStartingPosition[colIndex];
46165
+ const profile = profiles.get(left);
46166
+ if (!profile || profile.length === 0) {
46167
+ mergedZone.push(...pendingZones);
46168
+ pendingZones = [];
46169
+ continue;
46170
+ }
46171
+ let right = profilesStartingPosition[colIndex + 1];
46172
+ if (right !== undefined) {
46173
+ right--;
46174
+ }
46175
+ const nextPendingZones = [];
46176
+ for (let i = 0; i < profile.length; i += 2) {
46177
+ const top = profile[i];
46178
+ let bottom = profile[i + 1];
46179
+ if (bottom !== undefined) {
46180
+ bottom--;
46181
+ }
46182
+ const profileZone = {
46183
+ top,
46184
+ left,
46185
+ bottom,
46186
+ right,
46187
+ hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
46188
+ };
46189
+ let findCorrespondingZone = false;
46190
+ for (let j = pendingZones.length - 1; j >= 0; j--) {
46191
+ const pendingZone = pendingZones[j];
46192
+ if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
46193
+ pendingZone.right = profileZone.right;
46194
+ pendingZones.splice(j, 1);
46195
+ nextPendingZones.push(pendingZone);
46196
+ findCorrespondingZone = true;
46197
+ break;
46198
+ }
46199
+ }
46200
+ if (!findCorrespondingZone) {
46201
+ nextPendingZones.push(profileZone);
46202
+ }
46203
+ }
46204
+ mergedZone.push(...pendingZones);
46205
+ pendingZones = nextPendingZones;
46206
+ }
46207
+ mergedZone.push(...pendingZones);
46208
+ return mergedZone;
46209
+ }
46210
+ function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
46211
+ let end = arr.length - 1;
46212
+ let result = -1;
46213
+ while (start <= end) {
46214
+ const mid = Math.floor((start + end) / 2);
46215
+ if (arr[mid] === val && matchEqual) {
46216
+ return mid;
46217
+ }
46218
+ else if (arr[mid] < val) {
46219
+ result = mid;
46220
+ start = mid + 1;
46221
+ }
46222
+ else {
46223
+ end = mid - 1;
46224
+ }
46225
+ }
46226
+ return result;
46227
+ }
46228
+ function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
46229
+ let end = arr.length - 1;
46230
+ let result = arr.length;
46231
+ while (start <= end) {
46232
+ const mid = Math.floor((start + end) / 2);
46233
+ if (arr[mid] === val && matchEqual) {
46234
+ return mid;
46235
+ }
46236
+ else if (arr[mid] > val) {
46237
+ result = mid;
46238
+ end = mid - 1;
46239
+ }
46240
+ else {
46241
+ start = mid + 1;
46242
+ }
46243
+ }
46244
+ return result;
46245
+ }
46246
+ /**
46247
+ * Compares two arrays.
46248
+ * For performance reasons, this function is to be preferred
46249
+ * to 'deepEquals' in the case we know that the inputs are arrays.
46250
+ */
46251
+ function deepEqualsArray(arr1, arr2) {
46252
+ if (arr1.length !== arr2.length) {
46253
+ return false;
46254
+ }
46255
+ for (let i = 0; i < arr1.length; i++) {
46256
+ if (!deepEquals(arr1[i], arr2[i])) {
46257
+ return false;
46258
+ }
46259
+ }
46260
+ return true;
46261
+ }
46262
+
45920
46263
  class PositionMap {
45921
46264
  map = {};
45922
46265
  set({ sheetId, col, row }, value) {
@@ -46715,7 +47058,7 @@ class FormulaDependencyGraph {
46715
47058
  }
46716
47059
  }
46717
47060
  /**
46718
- * Return the cell and all cells that depend on it,
47061
+ * Return all the cells that depend on the provided ranges,
46719
47062
  * in the correct order they should be evaluated.
46720
47063
  * This is called a topological ordering (excluding cycles)
46721
47064
  */
@@ -46726,11 +47069,19 @@ class FormulaDependencyGraph {
46726
47069
  const range = queue.pop();
46727
47070
  visited.addMany(positions(range.zone).map((position) => ({ sheetId: range.sheetId, ...position })));
46728
47071
  const impactedPositions = this.rTree.search(range).map((dep) => dep.data);
47072
+ const nextInQueue = {};
46729
47073
  for (const position of impactedPositions) {
46730
47074
  if (!visited.has(position)) {
46731
- queue.push({ sheetId: position.sheetId, zone: positionToZone(position) });
47075
+ if (!nextInQueue[position.sheetId]) {
47076
+ nextInQueue[position.sheetId] = [];
47077
+ }
47078
+ nextInQueue[position.sheetId].push(positionToZone(position));
46732
47079
  }
46733
47080
  }
47081
+ for (const sheetId in nextInQueue) {
47082
+ const zones = futureRecomputeZones(nextInQueue[sheetId]);
47083
+ queue.push(...zones.map((zone) => ({ sheetId, zone })));
47084
+ }
46734
47085
  }
46735
47086
  visited.deleteMany(ranges.flatMap((r) => positions(r.zone).map((position) => ({ sheetId: r.sheetId, ...position }))));
46736
47087
  return visited;
@@ -47069,7 +47420,7 @@ class Evaluator {
47069
47420
  }
47070
47421
  if (!content) {
47071
47422
  // The previous content could have blocked some array formulas
47072
- impactedPositions.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(position));
47423
+ impactedPositions.addMany(this.getArrayFormulasBlockedBy(position));
47073
47424
  }
47074
47425
  }
47075
47426
  return impactedPositions;
@@ -47112,14 +47463,23 @@ class Evaluator {
47112
47463
  positions.fillAllPositions();
47113
47464
  return positions;
47114
47465
  }
47115
- getArrayFormulasBlockedByOrSpreadingOn(position) {
47466
+ /**
47467
+ * Return the position of formulas blocked by the given position
47468
+ * as well as all their dependencies.
47469
+ */
47470
+ getArrayFormulasBlockedBy(position) {
47116
47471
  if (!this.spreadingRelations.hasArrayFormulaResult(position)) {
47117
47472
  return [];
47118
47473
  }
47119
47474
  const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(position);
47120
47475
  const positions = this.createEmptyPositionSet();
47121
47476
  positions.addMany(arrayFormulas);
47122
- positions.addMany(this.getCellsDependingOn(arrayFormulas));
47477
+ const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
47478
+ if (arrayFormulaPosition) {
47479
+ // ignore the formula spreading on the position. Keep only the blocked ones
47480
+ positions.delete(arrayFormulaPosition);
47481
+ }
47482
+ positions.addMany(this.getCellsDependingOn(positions));
47123
47483
  return positions;
47124
47484
  }
47125
47485
  nextPositionsToUpdate = new PositionSet({});
@@ -47259,7 +47619,7 @@ class Evaluator {
47259
47619
  }
47260
47620
  this.evaluatedCells.delete(child);
47261
47621
  this.nextPositionsToUpdate.addMany(this.getCellsDependingOn([child]));
47262
- this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(child));
47622
+ this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(child));
47263
47623
  }
47264
47624
  this.spreadingRelations.removeNode(position);
47265
47625
  }
@@ -60197,6 +60557,6 @@ const constants = {
60197
60557
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
60198
60558
 
60199
60559
 
60200
- __info__.version = "17.2.4";
60201
- __info__.date = "2024-04-18T16:41:38.407Z";
60202
- __info__.hash = "0c66038";
60560
+ __info__.version = "17.2.5";
60561
+ __info__.date = "2024-04-26T07:41:13.193Z";
60562
+ __info__.hash = "a730f5c";