@odoo/o-spreadsheet 19.2.0-alpha.1 → 19.2.0-alpha.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,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.2.0-alpha.1
6
- * @date 2025-12-26T10:20:28.360Z
7
- * @hash 3296c7e
5
+ * @version 19.2.0-alpha.2
6
+ * @date 2026-01-07T16:21:35.251Z
7
+ * @hash ac2fa3e
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -10483,6 +10483,7 @@
10483
10483
  case "dateIsAfter":
10484
10484
  case "dateIsOnOrAfter":
10485
10485
  case "dateIsOnOrBefore":
10486
+ case "top10":
10486
10487
  rule.values = rule.values.map((v) => changeContentLocale(v));
10487
10488
  return rule;
10488
10489
  case "beginsWithText":
@@ -14632,6 +14633,7 @@
14632
14633
  dateValue: _t("The value must be a date"),
14633
14634
  validRange: _t("The value must be a valid range"),
14634
14635
  validFormula: _t("The formula must be valid"),
14636
+ positiveNumber: _t("The value must be a positive number"),
14635
14637
  },
14636
14638
  Errors: {
14637
14639
  ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid."),
@@ -19609,17 +19611,41 @@ stores.inject(MyMetaStore, storeInstance);
19609
19611
  const today = DateTime.now();
19610
19612
  switch (dateValue) {
19611
19613
  case "today":
19612
- return jsDateToNumber(today);
19613
- case "yesterday":
19614
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 1)));
19615
- case "tomorrow":
19616
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() + 1)));
19614
+ return Math.floor(jsDateToNumber(today));
19615
+ case "yesterday": {
19616
+ today.setDate(today.getDate() - 1);
19617
+ return Math.floor(jsDateToNumber(today));
19618
+ }
19619
+ case "tomorrow": {
19620
+ today.setDate(today.getDate() + 1);
19621
+ return Math.floor(jsDateToNumber(today));
19622
+ }
19617
19623
  case "lastWeek":
19618
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 7)));
19619
- case "lastMonth":
19620
- return jsDateToNumber(DateTime.fromTimestamp(today.setMonth(today.getMonth() - 1)));
19624
+ today.setDate(today.getDate() - 6);
19625
+ return Math.floor(jsDateToNumber(today));
19626
+ case "lastMonth": {
19627
+ const lastMonth = today.getMonth() === 0 ? 11 : today.getMonth() - 1;
19628
+ const dateInLastMonth = new DateTime(today.getFullYear(), lastMonth, 1);
19629
+ if (today.getDate() > getDaysInMonth(dateInLastMonth)) {
19630
+ today.setDate(1);
19631
+ }
19632
+ else {
19633
+ today.setDate(today.getDate() + 1);
19634
+ today.setMonth(today.getMonth() - 1);
19635
+ }
19636
+ return Math.floor(jsDateToNumber(today));
19637
+ }
19621
19638
  case "lastYear":
19622
- return jsDateToNumber(DateTime.fromTimestamp(today.setFullYear(today.getFullYear() - 1)));
19639
+ // Handle leap year case
19640
+ if (today.getMonth() === 1 && today.getDate() === 29) {
19641
+ today.setDate(28);
19642
+ today.setFullYear(today.getFullYear() - 1);
19643
+ }
19644
+ else {
19645
+ today.setDate(today.getDate() + 1);
19646
+ today.setFullYear(today.getFullYear() - 1);
19647
+ }
19648
+ return Math.floor(jsDateToNumber(today));
19623
19649
  }
19624
19650
  }
19625
19651
  /** Get all the dates values of a criterion converted to numbers, converting date values such as "today" to actual dates */
@@ -31116,7 +31142,7 @@ stores.inject(MyMetaStore, storeInstance);
31116
31142
  if (message.type === "CLIENT_JOINED" ||
31117
31143
  message.type === "CLIENT_LEFT" ||
31118
31144
  message.type === "CLIENT_MOVED") {
31119
- this.transportService.sendMessage(message);
31145
+ await this.transportService.sendMessage(message);
31120
31146
  }
31121
31147
  // ignore all other messages
31122
31148
  }
@@ -32290,7 +32316,7 @@ stores.inject(MyMetaStore, storeInstance);
32290
32316
  }
32291
32317
  delete this.clients[this.clientId];
32292
32318
  this.transportService.leave(this.clientId);
32293
- this.transportService.sendMessage({
32319
+ this.sendToTransport({
32294
32320
  type: "CLIENT_LEFT",
32295
32321
  clientId: this.clientId,
32296
32322
  version: MESSAGE_VERSION,
@@ -32304,7 +32330,7 @@ stores.inject(MyMetaStore, storeInstance);
32304
32330
  return;
32305
32331
  }
32306
32332
  const snapshotId = this.uuidGenerator.uuidv4();
32307
- await this.transportService.sendMessage({
32333
+ await this.sendToTransport({
32308
32334
  type: "SNAPSHOT",
32309
32335
  nextRevisionId: snapshotId,
32310
32336
  serverRevisionId: this.serverRevisionId,
@@ -32352,10 +32378,14 @@ stores.inject(MyMetaStore, storeInstance);
32352
32378
  const type = currentPosition ? "CLIENT_MOVED" : "CLIENT_JOINED";
32353
32379
  const client = this.getCurrentClient();
32354
32380
  this.clients[this.clientId] = { ...client, position };
32355
- this.transportService.sendMessage({
32381
+ this.sendToTransport({
32356
32382
  type,
32357
32383
  version: MESSAGE_VERSION,
32358
32384
  client: { ...client, position },
32385
+ }).then(() => {
32386
+ if (this.pendingMessages.length > 0 && !this.waitingAck) {
32387
+ this.sendPendingMessage();
32388
+ }
32359
32389
  });
32360
32390
  }
32361
32391
  /**
@@ -32436,7 +32466,7 @@ stores.inject(MyMetaStore, storeInstance);
32436
32466
  if (client) {
32437
32467
  const { position } = client;
32438
32468
  if (position) {
32439
- this.transportService.sendMessage({
32469
+ this.sendToTransport({
32440
32470
  type: "CLIENT_MOVED",
32441
32471
  version: MESSAGE_VERSION,
32442
32472
  client: { ...client, position },
@@ -32457,6 +32487,10 @@ stores.inject(MyMetaStore, storeInstance);
32457
32487
  }
32458
32488
  this.sendPendingMessage();
32459
32489
  }
32490
+ async sendToTransport(message) {
32491
+ // wrap in an async function to ensure it returns a promise
32492
+ return this.transportService.sendMessage(message);
32493
+ }
32460
32494
  /**
32461
32495
  * Send the next pending message
32462
32496
  */
@@ -32485,9 +32519,14 @@ stores.inject(MyMetaStore, storeInstance);
32485
32519
  ${JSON.stringify(message)}`);
32486
32520
  }
32487
32521
  this.waitingAck = true;
32488
- this.transportService.sendMessage({
32522
+ this.sendToTransport({
32489
32523
  ...message,
32490
32524
  serverRevisionId: this.serverRevisionId,
32525
+ }).catch((e) => {
32526
+ if (!(e instanceof ClientDisconnectedError)) {
32527
+ throw e.cause || e;
32528
+ }
32529
+ this.waitingAck = false;
32491
32530
  });
32492
32531
  }
32493
32532
  acknowledge(message) {
@@ -33983,6 +34022,7 @@ stores.inject(MyMetaStore, storeInstance);
33983
34022
  greaterThanOrEqual: "isGreaterOrEqualTo",
33984
34023
  lessThan: "isLessThan",
33985
34024
  lessThanOrEqual: "isLessOrEqualTo",
34025
+ top10: "top10",
33986
34026
  };
33987
34027
  /** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */
33988
34028
  const CF_TYPE_CONVERSION_MAP = {
@@ -34523,6 +34563,7 @@ stores.inject(MyMetaStore, storeInstance);
34523
34563
  const rule = cf.cfRules[0];
34524
34564
  let operator;
34525
34565
  const values = [];
34566
+ const cfAdditionalProperties = {};
34526
34567
  if (rule.dxfId === undefined &&
34527
34568
  !(rule.type === "colorScale" || rule.type === "iconSet" || rule.type === "dataBar"))
34528
34569
  continue;
@@ -34531,7 +34572,6 @@ stores.inject(MyMetaStore, storeInstance);
34531
34572
  case "containsErrors":
34532
34573
  case "notContainsErrors":
34533
34574
  case "duplicateValues":
34534
- case "top10":
34535
34575
  case "uniqueValues":
34536
34576
  case "timePeriod":
34537
34577
  // Not supported
@@ -34582,6 +34622,18 @@ stores.inject(MyMetaStore, storeInstance);
34582
34622
  values.push(prefixFormulaWithEqual(rule.formula[1]));
34583
34623
  }
34584
34624
  break;
34625
+ case "top10":
34626
+ if (rule.rank === undefined)
34627
+ continue;
34628
+ operator = CF_OPERATOR_TYPE_CONVERSION_MAP[rule.type];
34629
+ values.push(rule.rank.toString());
34630
+ if (rule.percent) {
34631
+ cfAdditionalProperties.isPercent = true;
34632
+ }
34633
+ if (rule.bottom) {
34634
+ cfAdditionalProperties.isBottom = true;
34635
+ }
34636
+ break;
34585
34637
  }
34586
34638
  if (operator && rule.dxfId !== undefined) {
34587
34639
  cfs.push({
@@ -34592,6 +34644,7 @@ stores.inject(MyMetaStore, storeInstance);
34592
34644
  type: "CellIsRule",
34593
34645
  operator: operator,
34594
34646
  values: values,
34647
+ ...cfAdditionalProperties,
34595
34648
  style: convertStyle({ fontStyle: dxfs[rule.dxfId].font, fillStyle: dxfs[rule.dxfId].fill }, warningManager),
34596
34649
  },
34597
34650
  });
@@ -34810,6 +34863,8 @@ stores.inject(MyMetaStore, storeInstance);
34810
34863
  return "greaterThanOrEqual";
34811
34864
  case "dateIsOnOrBefore":
34812
34865
  return "lessThanOrEqual";
34866
+ case "top10":
34867
+ return "top10";
34813
34868
  }
34814
34869
  }
34815
34870
  // -------------------------------------
@@ -40211,7 +40266,7 @@ stores.inject(MyMetaStore, storeInstance);
40211
40266
  return false;
40212
40267
  }
40213
40268
  if (["lastWeek", "lastMonth", "lastYear"].includes(criterion.dateValue)) {
40214
- const today = jsDateToRoundNumber(DateTime.now());
40269
+ const today = Math.floor(jsDateToNumber(DateTime.now()));
40215
40270
  return isDateBetween(dateValue, today, criterionValue);
40216
40271
  }
40217
40272
  return areDatesSameDay(dateValue, criterionValue);
@@ -40593,15 +40648,20 @@ stores.inject(MyMetaStore, storeInstance);
40593
40648
  getPreview: (criterion) => _t("Value one of: %s", criterion.values.join(", ")),
40594
40649
  });
40595
40650
  criterionEvaluatorRegistry.add("isValueInRange", {
40596
- type: "isValueInList",
40597
- isValueValid: (value, criterion, getters, sheetId) => {
40651
+ type: "isValueInRange",
40652
+ preComputeCriterion: (criterion, criterionRanges, getters) => {
40653
+ if (criterionRanges.length === 0) {
40654
+ return new Set();
40655
+ }
40656
+ const sheetId = criterionRanges[0].sheetId;
40657
+ const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
40658
+ return new Set(criterionValues.map((value) => value.value.toString().toLowerCase()));
40659
+ },
40660
+ isValueValid: (value, criterion, valuesSet) => {
40598
40661
  if (!value) {
40599
40662
  return false;
40600
40663
  }
40601
- const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
40602
- return criterionValues
40603
- .map((value) => value.value.toLowerCase())
40604
- .includes(value.toString().toLowerCase());
40664
+ return valuesSet.has(value.toString().toLowerCase());
40605
40665
  },
40606
40666
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
40607
40667
  isCriterionValueValid: (value) => rangeReference.test(value),
@@ -40678,6 +40738,67 @@ stores.inject(MyMetaStore, storeInstance);
40678
40738
  name: _t("Is not empty"),
40679
40739
  getPreview: () => _t("Is not empty"),
40680
40740
  });
40741
+ criterionEvaluatorRegistry.add("top10", {
40742
+ type: "top10",
40743
+ preComputeCriterion: (criterion, criterionRanges, getters) => {
40744
+ let value = tryToNumber(criterion.values[0], DEFAULT_LOCALE);
40745
+ if (value === undefined || value <= 0) {
40746
+ return undefined;
40747
+ }
40748
+ const numberValues = [];
40749
+ for (const range of criterionRanges) {
40750
+ for (const value of getters.getRangeValues(range)) {
40751
+ if (typeof value === "number") {
40752
+ numberValues.push(value);
40753
+ }
40754
+ }
40755
+ }
40756
+ const sortedValues = numberValues.sort((a, b) => a - b);
40757
+ if (criterion.isPercent) {
40758
+ value = clip(value, 1, 100);
40759
+ }
40760
+ let index = 0;
40761
+ if (criterion.isBottom && !criterion.isPercent) {
40762
+ index = value - 1;
40763
+ }
40764
+ else if (criterion.isBottom && criterion.isPercent) {
40765
+ index = Math.floor((sortedValues.length * value) / 100) - 1;
40766
+ }
40767
+ else if (!criterion.isBottom && criterion.isPercent) {
40768
+ index = sortedValues.length - Math.floor((sortedValues.length * value) / 100);
40769
+ }
40770
+ else {
40771
+ index = sortedValues.length - value;
40772
+ }
40773
+ index = clip(index, 0, sortedValues.length - 1);
40774
+ return sortedValues[index];
40775
+ },
40776
+ isValueValid: (value, criterion, threshold) => {
40777
+ if (typeof value !== "number" || threshold === undefined) {
40778
+ return false;
40779
+ }
40780
+ return criterion.isBottom ? value <= threshold : value >= threshold;
40781
+ },
40782
+ getErrorString: (criterion) => {
40783
+ const args = {
40784
+ value: String(criterion.values[0]),
40785
+ percentSymbol: criterion.isPercent ? "%" : "",
40786
+ };
40787
+ return criterion.isBottom
40788
+ ? _t("The value must be in bottom %(value)s%(percentSymbol)s", args)
40789
+ : _t("The value must be in top %(value)s%(percentSymbol)s", args);
40790
+ },
40791
+ isCriterionValueValid: (value) => checkValueIsPositiveNumber(value),
40792
+ criterionValueErrorString: DVTerms.CriterionError.positiveNumber,
40793
+ numberOfValues: () => 1,
40794
+ name: _t("Is in Top/Bottom ranking"),
40795
+ getPreview: (criterion) => {
40796
+ const args = { value: criterion.values[0], percentSymbol: criterion.isPercent ? "%" : "" };
40797
+ return criterion.isBottom
40798
+ ? _t("Value is in bottom %(value)s%(percentSymbol)s", args)
40799
+ : _t("Value is in top %(value)s%(percentSymbol)s", args);
40800
+ },
40801
+ });
40681
40802
  function getNumberCriterionlocalizedValues(criterion, locale) {
40682
40803
  return criterion.values.map((value) => {
40683
40804
  return value !== undefined
@@ -40699,6 +40820,10 @@ stores.inject(MyMetaStore, storeInstance);
40699
40820
  const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
40700
40821
  return valueAsNumber !== undefined;
40701
40822
  }
40823
+ function checkValueIsPositiveNumber(value) {
40824
+ const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
40825
+ return valueAsNumber !== undefined && valueAsNumber > 0;
40826
+ }
40702
40827
 
40703
40828
  // -----------------------------------------------------------------------------
40704
40829
  // Constants
@@ -51073,6 +51198,10 @@ stores.inject(MyMetaStore, storeInstance);
51073
51198
  break;
51074
51199
  case "CellIsRule":
51075
51200
  const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
51201
+ const evaluator = criterionEvaluatorRegistry.get(cf.rule.operator);
51202
+ const criterion = { ...cf.rule, type: cf.rule.operator };
51203
+ const ranges = cf.ranges.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
51204
+ const preComputedCriterion = evaluator.preComputeCriterion?.(criterion, ranges, this.getters);
51076
51205
  for (const ref of cf.ranges) {
51077
51206
  const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
51078
51207
  for (let row = zone.top; row <= zone.bottom; row++) {
@@ -51085,7 +51214,7 @@ stores.inject(MyMetaStore, storeInstance);
51085
51214
  }
51086
51215
  return value;
51087
51216
  });
51088
- if (this.getRuleResultForTarget(target, { ...cf.rule, values })) {
51217
+ if (this.getRuleResultForTarget(target, { ...cf.rule, values }, preComputedCriterion)) {
51089
51218
  if (!computedStyle[col])
51090
51219
  computedStyle[col] = [];
51091
51220
  // we must combine all the properties of all the CF rules applied to the given cell
@@ -51248,7 +51377,7 @@ stores.inject(MyMetaStore, storeInstance);
51248
51377
  }
51249
51378
  }
51250
51379
  }
51251
- getRuleResultForTarget(target, rule) {
51380
+ getRuleResultForTarget(target, rule, preComputedCriterion) {
51252
51381
  const cell = this.getters.getEvaluatedCell(target);
51253
51382
  if (cell.type === CellValueType.error) {
51254
51383
  return false;
@@ -51265,11 +51394,12 @@ stores.inject(MyMetaStore, storeInstance);
51265
51394
  return false;
51266
51395
  }
51267
51396
  const evaluatedCriterion = {
51397
+ ...rule,
51268
51398
  type: rule.operator,
51269
51399
  values: evaluatedCriterionValues.map(toScalar),
51270
51400
  dateValue: rule.dateValue || "exactDate",
51271
51401
  };
51272
- return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, this.getters, sheetId);
51402
+ return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, preComputedCriterion);
51273
51403
  }
51274
51404
  }
51275
51405
 
@@ -51286,17 +51416,20 @@ stores.inject(MyMetaStore, storeInstance);
51286
51416
  "isDataValidationInvalid",
51287
51417
  ];
51288
51418
  validationResults = {};
51419
+ criterionPreComputeResult = {};
51289
51420
  handle(cmd) {
51290
51421
  if (invalidateEvaluationCommands.has(cmd.type) ||
51291
51422
  cmd.type === "EVALUATE_CELLS" ||
51292
51423
  (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd))) {
51293
51424
  this.validationResults = {};
51425
+ this.criterionPreComputeResult = {};
51294
51426
  return;
51295
51427
  }
51296
51428
  switch (cmd.type) {
51297
51429
  case "ADD_DATA_VALIDATION_RULE":
51298
51430
  case "REMOVE_DATA_VALIDATION_RULE":
51299
51431
  delete this.validationResults[cmd.sheetId];
51432
+ delete this.criterionPreComputeResult[cmd.sheetId];
51300
51433
  break;
51301
51434
  }
51302
51435
  }
@@ -51439,7 +51572,15 @@ stores.inject(MyMetaStore, storeInstance);
51439
51572
  return undefined;
51440
51573
  }
51441
51574
  const evaluatedCriterion = { ...criterion, values: evaluatedCriterionValues.map(toScalar) };
51442
- if (evaluator.isValueValid(cellValue, evaluatedCriterion, this.getters, sheetId)) {
51575
+ if (!this.criterionPreComputeResult[sheetId]) {
51576
+ this.criterionPreComputeResult[sheetId] = {};
51577
+ }
51578
+ let preComputedCriterion = this.criterionPreComputeResult[sheetId][rule.id];
51579
+ if (preComputedCriterion === undefined) {
51580
+ preComputedCriterion = evaluator.preComputeCriterion?.(rule.criterion, rule.ranges, this.getters);
51581
+ this.criterionPreComputeResult[sheetId][rule.id] = preComputedCriterion;
51582
+ }
51583
+ if (evaluator.isValueValid(cellValue, evaluatedCriterion, preComputedCriterion)) {
51443
51584
  return undefined;
51444
51585
  }
51445
51586
  return evaluator.getErrorString(evaluatedCriterion, this.getters, sheetId);
@@ -57134,6 +57275,7 @@ stores.inject(MyMetaStore, storeInstance);
57134
57275
  if (filterValue.type === "none")
57135
57276
  continue;
57136
57277
  const evaluator = criterionEvaluatorRegistry.get(filterValue.type);
57278
+ const preComputedCriterion = evaluator.preComputeCriterion?.(filterValue, [filter.filteredRange], this.getters);
57137
57279
  const evaluatedCriterionValues = filterValue.values.map((value) => {
57138
57280
  if (!value.startsWith("=")) {
57139
57281
  return parseLiteral(value, DEFAULT_LOCALE);
@@ -57151,7 +57293,7 @@ stores.inject(MyMetaStore, storeInstance);
57151
57293
  for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
57152
57294
  const position = { sheetId, col: filter.col, row };
57153
57295
  const value = this.getters.getEvaluatedCell(position).value ?? "";
57154
- if (!evaluator.isValueValid(value, evaluatedCriterion, this.getters, sheetId)) {
57296
+ if (!evaluator.isValueValid(value, evaluatedCriterion, preComputedCriterion)) {
57155
57297
  hiddenRows.add(row);
57156
57298
  }
57157
57299
  }
@@ -61278,6 +61420,8 @@ stores.inject(MyMetaStore, storeInstance);
61278
61420
  case undefined:
61279
61421
  throw new Error("dateValue should be defined");
61280
61422
  }
61423
+ case "top10":
61424
+ return [];
61281
61425
  }
61282
61426
  }
61283
61427
  function cellRuleTypeAttributes(rule) {
@@ -61310,6 +61454,14 @@ stores.inject(MyMetaStore, storeInstance);
61310
61454
  case "dateIs":
61311
61455
  case "customFormula":
61312
61456
  return [["type", "expression"]];
61457
+ case "top10": {
61458
+ return [
61459
+ ["type", "top10"],
61460
+ ["rank", rule.values[0]],
61461
+ ["percent", rule.isPercent ? "1" : "0"],
61462
+ ["bottom", rule.isBottom ? "1" : "0"],
61463
+ ];
61464
+ }
61313
61465
  }
61314
61466
  }
61315
61467
  function addDataBarRule(cf, rule) {
@@ -65030,6 +65182,7 @@ stores.inject(MyMetaStore, storeInstance);
65030
65182
  "dateIsAfter",
65031
65183
  "dateIsOnOrBefore",
65032
65184
  "dateIsOnOrAfter",
65185
+ "top10",
65033
65186
  ];
65034
65187
  const availableConditionalFormatOperators = new Set(cfOperators);
65035
65188
 
@@ -73001,6 +73154,26 @@ stores.inject(MyMetaStore, storeInstance);
73001
73154
  }
73002
73155
  }
73003
73156
 
73157
+ class Top10CriterionForm extends CriterionForm {
73158
+ static template = "o-spreadsheet-Top10CriterionForm";
73159
+ static components = { CriterionInput };
73160
+ onValueChanged(value) {
73161
+ const criterion = deepCopy(this.props.criterion);
73162
+ criterion.values[0] = value;
73163
+ this.updateCriterion(criterion);
73164
+ }
73165
+ updateIsBottom(ev) {
73166
+ const criterion = deepCopy(this.props.criterion);
73167
+ criterion.isBottom = ev.target.value === "bottom";
73168
+ this.updateCriterion(criterion);
73169
+ }
73170
+ updateIsPercent(ev) {
73171
+ const criterion = deepCopy(this.props.criterion);
73172
+ criterion.isPercent = ev.target.value === "percent";
73173
+ this.updateCriterion(criterion);
73174
+ }
73175
+ }
73176
+
73004
73177
  /**
73005
73178
  * Start listening to pointer events and apply the given callbacks.
73006
73179
  *
@@ -74170,6 +74343,7 @@ stores.inject(MyMetaStore, storeInstance);
74170
74343
  text: 20,
74171
74344
  number: 30,
74172
74345
  date: 40,
74346
+ relative: 45,
74173
74347
  misc: 50,
74174
74348
  };
74175
74349
  const criterionComponentRegistry = new Registry$1();
@@ -74347,6 +74521,12 @@ stores.inject(MyMetaStore, storeInstance);
74347
74521
  category: "misc",
74348
74522
  sequence: 6,
74349
74523
  });
74524
+ criterionComponentRegistry.add("top10", {
74525
+ type: "top10",
74526
+ component: Top10CriterionForm,
74527
+ category: "relative",
74528
+ sequence: 7,
74529
+ });
74350
74530
  function getCriterionMenuItems(callback, availableTypes) {
74351
74531
  const items = criterionComponentRegistry
74352
74532
  .getAll()
@@ -75977,7 +76157,17 @@ stores.inject(MyMetaStore, storeInstance);
75977
76157
  // Side panel
75978
76158
  //------------------------------------------------------------------------------
75979
76159
  const OPEN_CF_SIDEPANEL_ACTION = (env) => {
75980
- env.openSidePanel("ConditionalFormatting", { selection: env.model.getters.getSelectedZones() });
76160
+ const sheetId = env.model.getters.getActiveSheetId();
76161
+ const zones = env.model.getters.getSelectedZones();
76162
+ const rules = env.model.getters.getConditionalFormats(sheetId);
76163
+ const ruleIds = env.model.getters.getRulesSelection(sheetId, zones);
76164
+ if (ruleIds.length === 1) {
76165
+ return env.openSidePanel("ConditionalFormattingEditor", {
76166
+ cf: rules.find((r) => r.id === ruleIds[0]),
76167
+ isNewCf: false,
76168
+ });
76169
+ }
76170
+ return env.openSidePanel("ConditionalFormatting");
75981
76171
  };
75982
76172
  const INSERT_LINK = (env) => {
75983
76173
  const { col, row } = env.model.getters.getActivePosition();
@@ -76975,12 +77165,12 @@ stores.inject(MyMetaStore, storeInstance);
76975
77165
  const zones = env.model.getters.getSelectedZones();
76976
77166
  const sheetId = env.model.getters.getActiveSheetId();
76977
77167
  const ranges = zones.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
76978
- const ruleID = env.model.uuidGenerator.smallUuid();
77168
+ const ruleId = env.model.uuidGenerator.smallUuid();
76979
77169
  env.model.dispatch("ADD_DATA_VALIDATION_RULE", {
76980
77170
  ranges,
76981
77171
  sheetId,
76982
77172
  rule: {
76983
- id: ruleID,
77173
+ id: ruleId,
76984
77174
  criterion: {
76985
77175
  type: "isValueInList",
76986
77176
  values: [],
@@ -76988,16 +77178,11 @@ stores.inject(MyMetaStore, storeInstance);
76988
77178
  },
76989
77179
  },
76990
77180
  });
76991
- const rule = env.model.getters.getDataValidationRule(sheetId, ruleID);
77181
+ const rule = env.model.getters.getDataValidationRule(sheetId, ruleId);
76992
77182
  if (!rule) {
76993
77183
  return;
76994
77184
  }
76995
- env.openSidePanel("DataValidationEditor", {
76996
- rule: localizeDataValidationRule(rule, env.model.getters.getLocale()),
76997
- onExit: () => {
76998
- env.replaceSidePanel("DataValidation", "DataValidationEditor");
76999
- },
77000
- });
77185
+ env.openSidePanel("DataValidationEditor", { ruleId });
77001
77186
  },
77002
77187
  isEnabled: (env) => !env.isSmall,
77003
77188
  icon: "o-spreadsheet-Icon.INSERT_DROPDOWN",
@@ -80272,7 +80457,6 @@ stores.inject(MyMetaStore, storeInstance);
80272
80457
  onGridMoved: Function,
80273
80458
  gridOverlayDimensions: String,
80274
80459
  slots: { type: Object, optional: true },
80275
- getGridSize: Function,
80276
80460
  };
80277
80461
  static components = {
80278
80462
  FiguresContainer,
@@ -80291,14 +80475,7 @@ stores.inject(MyMetaStore, storeInstance);
80291
80475
  setup() {
80292
80476
  useCellHovered(this.env, this.gridOverlay);
80293
80477
  const resizeObserver = new ResizeObserver(() => {
80294
- const boundingRect = this.gridOverlayEl.getBoundingClientRect();
80295
- const { width, height } = this.props.getGridSize();
80296
- this.props.onGridResized({
80297
- x: boundingRect.left,
80298
- y: boundingRect.top,
80299
- height: height,
80300
- width: width,
80301
- });
80478
+ this.props.onGridResized();
80302
80479
  });
80303
80480
  owl.onMounted(() => {
80304
80481
  resizeObserver.observe(this.gridOverlayEl);
@@ -85457,210 +85634,36 @@ stores.inject(MyMetaStore, storeInstance);
85457
85634
  }
85458
85635
  }
85459
85636
 
85460
- function useHighlightsOnHover(ref, highlightProvider) {
85461
- const hoverState = useHoveredElement(ref);
85462
- useHighlights({
85463
- get highlights() {
85464
- return hoverState.hovered ? highlightProvider.highlights : [];
85465
- },
85466
- });
85467
- }
85468
- function useHighlights(highlightProvider) {
85469
- const stores = useStoreProvider();
85470
- const store = useLocalStore(HighlightStore);
85471
- owl.onMounted(() => {
85472
- store.register(highlightProvider);
85473
- });
85474
- let currentHighlights = highlightProvider.highlights;
85475
- owl.useEffect((highlights) => {
85476
- if (!deepEquals(highlights, currentHighlights)) {
85477
- currentHighlights = highlights;
85478
- stores.trigger("store-updated");
85479
- }
85480
- }, () => [highlightProvider.highlights]);
85481
- }
85482
-
85483
- class ConditionalFormatPreview extends owl.Component {
85484
- static template = "o-spreadsheet-ConditionalFormatPreview";
85485
- icons = ICONS;
85486
- ref = owl.useRef("cfPreview");
85487
- setup() {
85488
- useHighlightsOnHover(this.ref, this);
85489
- }
85490
- getPreviewImageStyle() {
85491
- const rule = this.props.conditionalFormat.rule;
85492
- if (rule.type === "CellIsRule") {
85493
- return cssPropertiesToCss(cellStyleToCss(rule.style));
85494
- }
85495
- else if (rule.type === "ColorScaleRule") {
85496
- const minColor = colorNumberToHex(rule.minimum.color);
85497
- const midColor = rule.midpoint ? colorNumberToHex(rule.midpoint.color) : null;
85498
- const maxColor = colorNumberToHex(rule.maximum.color);
85499
- const baseString = "background-image: linear-gradient(to right, ";
85500
- return midColor
85501
- ? baseString + minColor + ", " + midColor + ", " + maxColor + ")"
85502
- : baseString + minColor + ", " + maxColor + ")";
85503
- }
85504
- else if (rule.type === "DataBarRule") {
85505
- const barColor = colorNumberToHex(rule.color);
85506
- const gradient = `background-image: linear-gradient(to right, ${barColor} 50%, white 50%)`;
85507
- return `${gradient}; color: ${TEXT_BODY};`;
85508
- }
85509
- return "";
85510
- }
85511
- getDescription() {
85512
- const cf = this.props.conditionalFormat;
85513
- switch (cf.rule.type) {
85514
- case "CellIsRule":
85515
- return criterionEvaluatorRegistry
85516
- .get(cf.rule.operator)
85517
- .getPreview({ ...cf.rule, type: cf.rule.operator }, this.env.model.getters);
85518
- case "ColorScaleRule":
85519
- return CfTerms.ColorScale;
85520
- case "IconSetRule":
85521
- return CfTerms.IconSet;
85522
- case "DataBarRule":
85523
- return CfTerms.DataBar;
85524
- }
85525
- }
85526
- deleteConditionalFormat() {
85527
- this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
85528
- id: this.props.conditionalFormat.id,
85529
- sheetId: this.env.model.getters.getActiveSheetId(),
85530
- });
85531
- }
85532
- onMouseDown(event) {
85533
- this.props.onMouseDown(event);
85534
- }
85535
- get highlights() {
85536
- const sheetId = this.env.model.getters.getActiveSheetId();
85537
- return this.props.conditionalFormat.ranges.map((range) => ({
85538
- range: this.env.model.getters.getRangeFromSheetXC(sheetId, range),
85539
- color: HIGHLIGHT_COLOR,
85540
- fillAlpha: 0.06,
85541
- }));
85542
- }
85543
- }
85544
- ConditionalFormatPreview.props = {
85545
- conditionalFormat: Object,
85546
- onPreviewClick: Function,
85547
- onMouseDown: Function,
85548
- class: String,
85549
- };
85550
-
85551
- class ConditionalFormatPreviewList extends owl.Component {
85552
- static template = "o-spreadsheet-ConditionalFormatPreviewList";
85553
- static props = {
85554
- conditionalFormats: Array,
85555
- onPreviewClick: Function,
85556
- onAddConditionalFormat: Function,
85557
- };
85558
- static components = { ConditionalFormatPreview };
85559
- icons = ICONS;
85560
- dragAndDrop = useDragAndDropListItems();
85561
- cfListRef = owl.useRef("cfList");
85562
- setup() {
85563
- owl.onWillUpdateProps((nextProps) => {
85564
- if (!deepEquals(this.props.conditionalFormats, nextProps.conditionalFormats)) {
85565
- this.dragAndDrop.cancel();
85566
- }
85567
- });
85568
- }
85569
- getPreviewDivStyle(cf) {
85570
- return this.dragAndDrop.itemsStyle[cf.id] || "";
85571
- }
85572
- onPreviewMouseDown(cf, event) {
85573
- if (event.button !== 0)
85574
- return;
85575
- const previewRects = Array.from(this.cfListRef.el.children).map((previewEl) => getBoundingRectAsPOJO(previewEl));
85576
- const items = this.props.conditionalFormats.map((cf, index) => ({
85577
- id: cf.id,
85578
- size: previewRects[index].height,
85579
- position: previewRects[index].y,
85580
- }));
85581
- this.dragAndDrop.start("vertical", {
85582
- draggedItemId: cf.id,
85583
- initialMousePosition: event.clientY,
85584
- items: items,
85585
- scrollableContainerEl: this.cfListRef.el,
85586
- onDragEnd: (cfId, finalIndex) => this.onDragEnd(cfId, finalIndex),
85587
- });
85588
- }
85589
- onDragEnd(cfId, finalIndex) {
85590
- const originalIndex = this.props.conditionalFormats.findIndex((sheet) => sheet.id === cfId);
85591
- const delta = originalIndex - finalIndex;
85592
- if (delta !== 0) {
85593
- this.env.model.dispatch("CHANGE_CONDITIONAL_FORMAT_PRIORITY", {
85594
- cfId,
85595
- delta,
85596
- sheetId: this.env.model.getters.getActiveSheetId(),
85597
- });
85598
- }
85599
- }
85600
- }
85601
-
85602
- class ConditionalFormattingEditor extends owl.Component {
85603
- static template = "o-spreadsheet-ConditionalFormattingEditor";
85604
- static props = {
85605
- editedCf: Object,
85606
- onCancel: Function,
85607
- onExit: Function,
85608
- isNewCf: Boolean,
85609
- };
85610
- static components = {
85611
- SelectionInput,
85612
- IconPicker,
85613
- ColorPickerWidget,
85614
- ConditionalFormatPreviewList,
85615
- Section,
85616
- RoundColorPicker,
85617
- StandaloneComposer,
85618
- BadgeSelection,
85619
- ValidationMessages,
85620
- SelectMenu,
85621
- };
85637
+ class ConditionalFormattingEditorStore extends SpreadsheetStore {
85638
+ mutators = ["updateConditionalFormat", "closeMenus"];
85622
85639
  icons = ICONS;
85623
85640
  iconSets = ICON_SETS;
85624
- getTextDecoration = getTextDecoration;
85625
- colorNumberToHex = colorNumberToHex;
85626
85641
  state;
85627
- setup() {
85642
+ cfId;
85643
+ constructor(get, cf, isNewCf) {
85644
+ super(get);
85645
+ this.cfId = cf.id;
85628
85646
  this.state = owl.useState({
85629
85647
  errors: [],
85630
- currentCFType: this.props.editedCf.rule.type,
85631
- ranges: this.props.editedCf.ranges,
85648
+ currentCFType: cf.rule.type,
85649
+ ranges: cf.ranges,
85632
85650
  rules: this.getDefaultRules(),
85633
- hasEditedCf: this.props.isNewCf,
85651
+ hasEditedCf: isNewCf,
85634
85652
  });
85635
- switch (this.props.editedCf.rule.type) {
85653
+ switch (cf.rule.type) {
85636
85654
  case "CellIsRule":
85637
- this.state.rules.cellIs = this.props.editedCf.rule;
85655
+ this.state.rules.cellIs = cf.rule;
85638
85656
  break;
85639
85657
  case "ColorScaleRule":
85640
- this.state.rules.colorScale = this.props.editedCf.rule;
85658
+ this.state.rules.colorScale = cf.rule;
85641
85659
  break;
85642
85660
  case "IconSetRule":
85643
- this.state.rules.iconSet = this.props.editedCf.rule;
85661
+ this.state.rules.iconSet = cf.rule;
85644
85662
  break;
85645
85663
  case "DataBarRule":
85646
- this.state.rules.dataBar = this.props.editedCf.rule;
85664
+ this.state.rules.dataBar = cf.rule;
85647
85665
  break;
85648
85666
  }
85649
- owl.useExternalListener(window, "click", this.closeMenus);
85650
- }
85651
- get isRangeValid() {
85652
- return this.state.errors.includes("EmptyRange" /* CommandResult.EmptyRange */);
85653
- }
85654
- get errorMessages() {
85655
- return this.state.errors.map((error) => CfTerms.Errors[error] || CfTerms.Errors.Unexpected);
85656
- }
85657
- get cfTypesValues() {
85658
- return [
85659
- { value: "CellIsRule", label: _t("Single color") },
85660
- { value: "ColorScaleRule", label: _t("Color scale") },
85661
- { value: "IconSetRule", label: _t("Icon set") },
85662
- { value: "DataBarRule", label: _t("Data bar") },
85663
- ];
85664
85667
  }
85665
85668
  updateConditionalFormat(newCf) {
85666
85669
  const ranges = newCf.ranges || this.state.ranges;
@@ -85669,17 +85672,17 @@ stores.inject(MyMetaStore, storeInstance);
85669
85672
  if (!newCf.suppressErrors) {
85670
85673
  this.state.errors = ["InvalidRange" /* CommandResult.InvalidRange */];
85671
85674
  }
85672
- return ["InvalidRange" /* CommandResult.InvalidRange */];
85675
+ return;
85673
85676
  }
85674
- const sheetId = this.env.model.getters.getActiveSheetId();
85675
- const locale = this.env.model.getters.getLocale();
85677
+ const sheetId = this.model.getters.getActiveSheetId();
85678
+ const locale = this.model.getters.getLocale();
85676
85679
  const rule = newCf.rule || this.getEditedRule(this.state.currentCFType);
85677
- const result = this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
85680
+ const result = this.model.dispatch("ADD_CONDITIONAL_FORMAT", {
85678
85681
  cf: {
85682
+ id: this.cfId,
85679
85683
  rule: canonicalizeCFRule(rule, locale),
85680
- id: this.props.editedCf.id,
85681
85684
  },
85682
- ranges: ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
85685
+ ranges: ranges.map((xc) => this.model.getters.getRangeDataFromXc(sheetId, xc)),
85683
85686
  sheetId,
85684
85687
  });
85685
85688
  if (result.isSuccessful) {
@@ -85689,71 +85692,18 @@ stores.inject(MyMetaStore, storeInstance);
85689
85692
  if (!newCf.suppressErrors) {
85690
85693
  this.state.errors = reasons;
85691
85694
  }
85692
- return reasons;
85693
85695
  }
85694
- getEditedRule(ruleType) {
85695
- switch (ruleType) {
85696
- case "CellIsRule":
85697
- return this.state.rules.cellIs;
85698
- case "ColorScaleRule":
85699
- return this.state.rules.colorScale;
85700
- case "IconSetRule":
85701
- return this.state.rules.iconSet;
85702
- case "DataBarRule":
85703
- return this.state.rules.dataBar;
85704
- }
85696
+ get isRangeValid() {
85697
+ return this.state.errors.includes("EmptyRange" /* CommandResult.EmptyRange */);
85705
85698
  }
85706
- onSave() {
85707
- const result = this.updateConditionalFormat({});
85708
- if (result.length === 0) {
85709
- this.props.onExit();
85710
- }
85699
+ get errorMessages() {
85700
+ return this.state.errors.map((error) => CfTerms.Errors[error] || CfTerms.Errors.Unexpected);
85711
85701
  }
85712
- onCancel() {
85713
- if (this.state.hasEditedCf) {
85714
- this.props.onCancel();
85715
- }
85716
- else {
85717
- this.props.onExit();
85718
- }
85702
+ onRangeUpdate(ranges) {
85703
+ this.state.ranges = ranges;
85719
85704
  }
85720
- getDefaultRules() {
85721
- return {
85722
- cellIs: {
85723
- type: "CellIsRule",
85724
- operator: "isNotEmpty",
85725
- values: [],
85726
- style: { fillColor: "#b6d7a8" },
85727
- },
85728
- colorScale: {
85729
- type: "ColorScaleRule",
85730
- minimum: { type: "value", color: hexaToInt("EFF7FF") },
85731
- midpoint: undefined,
85732
- maximum: { type: "value", color: 0x6aa84f },
85733
- },
85734
- iconSet: {
85735
- type: "IconSetRule",
85736
- icons: {
85737
- upper: "arrowGood",
85738
- middle: "arrowNeutral",
85739
- lower: "arrowBad",
85740
- },
85741
- upperInflectionPoint: {
85742
- type: "percentage",
85743
- value: "66",
85744
- operator: "gt",
85745
- },
85746
- lowerInflectionPoint: {
85747
- type: "percentage",
85748
- value: "33",
85749
- operator: "gt",
85750
- },
85751
- },
85752
- dataBar: {
85753
- type: "DataBarRule",
85754
- color: 0xd9ead3,
85755
- },
85756
- };
85705
+ onRangeConfirmed() {
85706
+ this.updateConditionalFormat({ ranges: this.state.ranges });
85757
85707
  }
85758
85708
  changeRuleType(ruleType) {
85759
85709
  if (this.state.currentCFType === ruleType) {
@@ -85763,34 +85713,45 @@ stores.inject(MyMetaStore, storeInstance);
85763
85713
  this.state.currentCFType = ruleType;
85764
85714
  this.updateConditionalFormat({ rule: this.getEditedRule(ruleType), suppressErrors: true });
85765
85715
  }
85766
- onRangeUpdate(ranges) {
85767
- this.state.ranges = ranges;
85768
- }
85769
- onRangeConfirmed() {
85770
- this.updateConditionalFormat({ ranges: this.state.ranges });
85771
- }
85772
- /*****************************************************************************
85773
- * Common
85774
- ****************************************************************************/
85775
- toggleMenu(menu) {
85776
- const isSelected = this.state.openedMenu === menu;
85777
- this.closeMenus();
85778
- if (!isSelected) {
85779
- this.state.openedMenu = menu;
85716
+ getEditedRule(ruleType) {
85717
+ switch (ruleType) {
85718
+ case "CellIsRule":
85719
+ return this.state.rules.cellIs;
85720
+ case "ColorScaleRule":
85721
+ return this.state.rules.colorScale;
85722
+ case "IconSetRule":
85723
+ return this.state.rules.iconSet;
85724
+ case "DataBarRule":
85725
+ return this.state.rules.dataBar;
85780
85726
  }
85781
85727
  }
85782
- closeMenus() {
85783
- this.state.openedMenu = undefined;
85784
- }
85785
85728
  /*****************************************************************************
85786
85729
  * Cell Is Rule
85787
85730
  ****************************************************************************/
85788
- get isValue1Invalid() {
85789
- return (this.state.errors.includes("FirstArgMissing" /* CommandResult.FirstArgMissing */) ||
85790
- this.state.errors.includes("ValueCellIsInvalidFormula" /* CommandResult.ValueCellIsInvalidFormula */));
85731
+ get cfCriterionMenuItems() {
85732
+ return getCriterionMenuItems((type) => this.editOperator(type), availableConditionalFormatOperators);
85733
+ }
85734
+ get selectedCriterionName() {
85735
+ return criterionEvaluatorRegistry.get(this.state.rules.cellIs.operator).name;
85736
+ }
85737
+ get criterionComponent() {
85738
+ return criterionComponentRegistry.get(this.state.rules.cellIs.operator).component;
85791
85739
  }
85792
- get isValue2Invalid() {
85793
- return this.state.errors.includes("SecondArgMissing" /* CommandResult.SecondArgMissing */);
85740
+ get genericCriterion() {
85741
+ return {
85742
+ ...this.state.rules.cellIs,
85743
+ type: this.state.rules.cellIs.operator,
85744
+ };
85745
+ }
85746
+ onRuleValuesChanged(criterion) {
85747
+ const newRule = {
85748
+ ...criterion,
85749
+ operator: criterion.type,
85750
+ type: "CellIsRule",
85751
+ style: this.state.rules.cellIs.style,
85752
+ };
85753
+ this.state.rules.cellIs = newRule;
85754
+ this.updateConditionalFormat({ rule: newRule });
85794
85755
  }
85795
85756
  toggleStyle(tool) {
85796
85757
  const style = this.state.rules.cellIs.style;
@@ -85798,18 +85759,6 @@ stores.inject(MyMetaStore, storeInstance);
85798
85759
  this.updateConditionalFormat({ rule: this.state.rules.cellIs });
85799
85760
  this.closeMenus();
85800
85761
  }
85801
- onKeydown(event) {
85802
- if (event.key === "F4") {
85803
- const target = event.target;
85804
- const update = cycleFixedReference({ start: target.selectionStart ?? 0, end: target.selectionEnd ?? 0 }, target.value, this.env.model.getters.getLocale());
85805
- if (!update) {
85806
- return;
85807
- }
85808
- target.value = update.content;
85809
- target.setSelectionRange(update.selection.start, update.selection.end);
85810
- target.dispatchEvent(new Event("input"));
85811
- }
85812
- }
85813
85762
  setColor(target, color) {
85814
85763
  this.state.rules.cellIs.style[target] = color;
85815
85764
  this.updateConditionalFormat({ rule: this.state.rules.cellIs });
@@ -85823,62 +85772,10 @@ stores.inject(MyMetaStore, storeInstance);
85823
85772
  this.updateConditionalFormat({ rule: this.state.rules.cellIs, suppressErrors: true });
85824
85773
  this.closeMenus();
85825
85774
  }
85826
- get cfCriterionMenuItems() {
85827
- return getCriterionMenuItems((type) => this.editOperator(type), availableConditionalFormatOperators);
85828
- }
85829
- get selectedCriterionName() {
85830
- return criterionEvaluatorRegistry.get(this.state.rules.cellIs.operator).name;
85831
- }
85832
- get criterionComponent() {
85833
- return criterionComponentRegistry.get(this.state.rules.cellIs.operator).component;
85834
- }
85835
- get genericCriterion() {
85836
- return {
85837
- type: this.state.rules.cellIs.operator,
85838
- values: this.state.rules.cellIs.values,
85839
- dateValue: this.state.rules.cellIs.dateValue,
85840
- };
85841
- }
85842
- onRuleValuesChanged(rule) {
85843
- this.state.rules.cellIs.values = rule.values;
85844
- this.state.rules.cellIs.dateValue = rule.dateValue;
85845
- this.updateConditionalFormat({
85846
- rule: { ...this.state.rules.cellIs, values: rule.values, dateValue: rule.dateValue },
85847
- });
85848
- }
85849
85775
  /*****************************************************************************
85850
85776
  * Color Scale Rule
85851
85777
  ****************************************************************************/
85852
- isValueInvalid(threshold) {
85853
- switch (threshold) {
85854
- case "minimum":
85855
- return (this.state.errors.includes("MinInvalidFormula" /* CommandResult.MinInvalidFormula */) ||
85856
- this.state.errors.includes("MinBiggerThanMid" /* CommandResult.MinBiggerThanMid */) ||
85857
- this.state.errors.includes("MinBiggerThanMax" /* CommandResult.MinBiggerThanMax */) ||
85858
- this.state.errors.includes("MinNaN" /* CommandResult.MinNaN */));
85859
- case "midpoint":
85860
- return (this.state.errors.includes("MidInvalidFormula" /* CommandResult.MidInvalidFormula */) ||
85861
- this.state.errors.includes("MidNaN" /* CommandResult.MidNaN */) ||
85862
- this.state.errors.includes("MidBiggerThanMax" /* CommandResult.MidBiggerThanMax */));
85863
- case "maximum":
85864
- return (this.state.errors.includes("MaxInvalidFormula" /* CommandResult.MaxInvalidFormula */) ||
85865
- this.state.errors.includes("MaxNaN" /* CommandResult.MaxNaN */));
85866
- default:
85867
- return false;
85868
- }
85869
- }
85870
- setColorScaleColor(target, color) {
85871
- if (!isColorValid(color)) {
85872
- return;
85873
- }
85874
- const point = this.state.rules.colorScale[target];
85875
- if (point) {
85876
- point.color = colorToNumber(color);
85877
- }
85878
- this.updateConditionalFormat({ rule: this.state.rules.colorScale });
85879
- this.closeMenus();
85880
- }
85881
- getColorScalePreviewStyle() {
85778
+ get previewGradient() {
85882
85779
  const rule = this.state.rules.colorScale;
85883
85780
  const minColor = colorNumberToHex(rule.minimum.color);
85884
85781
  const midColor = colorNumberToHex(rule.midpoint?.color || DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);
@@ -85892,13 +85789,7 @@ stores.inject(MyMetaStore, storeInstance);
85892
85789
  color: "#000",
85893
85790
  });
85894
85791
  }
85895
- getThresholdColor(threshold) {
85896
- return threshold
85897
- ? colorNumberToHex(threshold.color)
85898
- : colorNumberToHex(DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);
85899
- }
85900
- onMidpointChange(ev) {
85901
- const type = ev.target.value;
85792
+ onMidpointChange(type) {
85902
85793
  const rule = this.state.rules.colorScale;
85903
85794
  if (type === "none") {
85904
85795
  rule.midpoint = undefined;
@@ -85921,23 +85812,20 @@ stores.inject(MyMetaStore, storeInstance);
85921
85812
  this.state.rules.colorScale[threshold].value = value;
85922
85813
  this.updateConditionalFormat({ rule: this.state.rules.colorScale });
85923
85814
  }
85815
+ setColorScaleColor(target, color) {
85816
+ if (!isColorValid(color)) {
85817
+ return;
85818
+ }
85819
+ const point = this.state.rules.colorScale[target];
85820
+ if (point) {
85821
+ point.color = colorToNumber(color);
85822
+ }
85823
+ this.updateConditionalFormat({ rule: this.state.rules.colorScale });
85824
+ this.closeMenus();
85825
+ }
85924
85826
  /*****************************************************************************
85925
85827
  * Icon Set
85926
85828
  ****************************************************************************/
85927
- isInflectionPointInvalid(inflectionPoint) {
85928
- switch (inflectionPoint) {
85929
- case "lowerInflectionPoint":
85930
- return (this.state.errors.includes("ValueLowerInflectionNaN" /* CommandResult.ValueLowerInflectionNaN */) ||
85931
- this.state.errors.includes("ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */) ||
85932
- this.state.errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
85933
- case "upperInflectionPoint":
85934
- return (this.state.errors.includes("ValueUpperInflectionNaN" /* CommandResult.ValueUpperInflectionNaN */) ||
85935
- this.state.errors.includes("ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */) ||
85936
- this.state.errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
85937
- default:
85938
- return true;
85939
- }
85940
- }
85941
85829
  reverseIcons() {
85942
85830
  const icons = this.state.rules.iconSet.icons;
85943
85831
  const upper = icons.upper;
@@ -85964,12 +85852,176 @@ stores.inject(MyMetaStore, storeInstance);
85964
85852
  this.state.rules.iconSet[inflectionPoint].value = value;
85965
85853
  this.updateConditionalFormat({ rule: this.state.rules.iconSet });
85966
85854
  }
85967
- setInflectionType(inflectionPoint, type, ev) {
85855
+ setInflectionType(inflectionPoint, type) {
85968
85856
  this.state.rules.iconSet[inflectionPoint].type = type;
85969
85857
  this.updateConditionalFormat({ rule: this.state.rules.iconSet, suppressErrors: true });
85970
85858
  }
85859
+ /*****************************************************************************
85860
+ * DataBar
85861
+ ****************************************************************************/
85862
+ get rangeValues() {
85863
+ return [this.state.rules.dataBar.rangeValues || ""];
85864
+ }
85865
+ updateDataBarColor(color) {
85866
+ if (!isColorValid(color)) {
85867
+ return;
85868
+ }
85869
+ this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
85870
+ this.updateConditionalFormat({ rule: this.state.rules.dataBar });
85871
+ }
85872
+ onDataBarRangeUpdate(ranges) {
85873
+ this.state.rules.dataBar.rangeValues = ranges[0];
85874
+ }
85875
+ onDataBarRangeChange() {
85876
+ this.updateConditionalFormat({ rule: this.state.rules.dataBar });
85877
+ }
85878
+ /*****************************************************************************
85879
+ * Common
85880
+ ****************************************************************************/
85881
+ toggleMenu(menu) {
85882
+ const isSelected = this.state.openedMenu === menu;
85883
+ this.closeMenus();
85884
+ if (!isSelected) {
85885
+ this.state.openedMenu = menu;
85886
+ }
85887
+ }
85888
+ closeMenus() {
85889
+ this.state.openedMenu = undefined;
85890
+ }
85891
+ getDefaultRules() {
85892
+ return {
85893
+ cellIs: {
85894
+ type: "CellIsRule",
85895
+ operator: "isNotEmpty",
85896
+ values: [],
85897
+ style: { fillColor: "#b6d7a8" },
85898
+ },
85899
+ colorScale: {
85900
+ type: "ColorScaleRule",
85901
+ minimum: { type: "value", color: hexaToInt("EFF7FF") },
85902
+ midpoint: undefined,
85903
+ maximum: { type: "value", color: 0x6aa84f },
85904
+ },
85905
+ iconSet: {
85906
+ type: "IconSetRule",
85907
+ icons: {
85908
+ upper: "arrowGood",
85909
+ middle: "arrowNeutral",
85910
+ lower: "arrowBad",
85911
+ },
85912
+ upperInflectionPoint: {
85913
+ type: "percentage",
85914
+ value: "66",
85915
+ operator: "gt",
85916
+ },
85917
+ lowerInflectionPoint: {
85918
+ type: "percentage",
85919
+ value: "33",
85920
+ operator: "gt",
85921
+ },
85922
+ },
85923
+ dataBar: {
85924
+ type: "DataBarRule",
85925
+ color: 0xd9ead3,
85926
+ },
85927
+ };
85928
+ }
85929
+ }
85930
+
85931
+ class ConditionalFormattingEditor extends owl.Component {
85932
+ static template = "o-spreadsheet-ConditionalFormattingEditor";
85933
+ static components = {
85934
+ SelectionInput,
85935
+ IconPicker,
85936
+ ColorPickerWidget,
85937
+ Section,
85938
+ RoundColorPicker,
85939
+ StandaloneComposer,
85940
+ BadgeSelection,
85941
+ ValidationMessages,
85942
+ SelectMenu,
85943
+ };
85944
+ static props = { cf: Object, isNewCf: Boolean, onCloseSidePanel: Function };
85945
+ getTextDecoration = getTextDecoration;
85946
+ colorNumberToHex = colorNumberToHex;
85947
+ activeSheetId;
85948
+ store;
85949
+ setup() {
85950
+ this.activeSheetId = this.env.model.getters.getActiveSheetId();
85951
+ this.store = useLocalStore(ConditionalFormattingEditorStore, deepCopy(this.props.cf), this.props.isNewCf);
85952
+ owl.useEffect((sheetId, isCfRemoved) => {
85953
+ if (this.activeSheetId !== sheetId || isCfRemoved) {
85954
+ this.env.replaceSidePanel("ConditionalFormatting", `ConditionalFormattingEditor_${this.props.cf.id}`);
85955
+ }
85956
+ }, () => [this.env.model.getters.getActiveSheetId(), this.isEditedCfRemoved]);
85957
+ owl.useExternalListener(window, "click", () => this.store.closeMenus());
85958
+ }
85959
+ get isEditedCfRemoved() {
85960
+ return !Boolean(this.env.model.getters
85961
+ .getConditionalFormats(this.activeSheetId)
85962
+ .find((cf) => cf.id === this.props.cf.id));
85963
+ }
85964
+ get cfTypesValues() {
85965
+ return [
85966
+ { value: "CellIsRule", label: _t("Single color") },
85967
+ { value: "ColorScaleRule", label: _t("Color scale") },
85968
+ { value: "IconSetRule", label: _t("Icon set") },
85969
+ { value: "DataBarRule", label: _t("Data bar") },
85970
+ ];
85971
+ }
85972
+ onSave() {
85973
+ this.store.updateConditionalFormat({});
85974
+ const isSuccessful = this.store.state.errors.length === 0;
85975
+ if (isSuccessful) {
85976
+ this.env.replaceSidePanel("ConditionalFormatting", `ConditionalFormattingEditor_${this.props.cf.id}`);
85977
+ }
85978
+ }
85979
+ onCancel() {
85980
+ if (this.store.state.hasEditedCf) {
85981
+ if (this.props.isNewCf) {
85982
+ this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
85983
+ sheetId: this.activeSheetId,
85984
+ id: this.props.cf.id,
85985
+ });
85986
+ }
85987
+ else {
85988
+ this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
85989
+ cf: this.props.cf,
85990
+ ranges: this.props.cf.ranges.map((range) => this.env.model.getters.getRangeDataFromXc(this.activeSheetId, range)),
85991
+ sheetId: this.activeSheetId,
85992
+ });
85993
+ }
85994
+ }
85995
+ this.env.replaceSidePanel("ConditionalFormatting", `ConditionalFormattingEditor_${this.props.cf.id}`);
85996
+ }
85997
+ /*****************************************************************************
85998
+ * Color Scale Rule
85999
+ ****************************************************************************/
86000
+ getThresholdColor(threshold) {
86001
+ return threshold
86002
+ ? colorNumberToHex(threshold.color)
86003
+ : colorNumberToHex(DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);
86004
+ }
86005
+ isValueInvalid(threshold) {
86006
+ const errors = this.store.state.errors;
86007
+ switch (threshold) {
86008
+ case "minimum":
86009
+ return (errors.includes("MinInvalidFormula" /* CommandResult.MinInvalidFormula */) ||
86010
+ errors.includes("MinBiggerThanMid" /* CommandResult.MinBiggerThanMid */) ||
86011
+ errors.includes("MinBiggerThanMax" /* CommandResult.MinBiggerThanMax */) ||
86012
+ errors.includes("MinNaN" /* CommandResult.MinNaN */));
86013
+ case "midpoint":
86014
+ return (errors.includes("MidInvalidFormula" /* CommandResult.MidInvalidFormula */) ||
86015
+ errors.includes("MidNaN" /* CommandResult.MidNaN */) ||
86016
+ errors.includes("MidBiggerThanMax" /* CommandResult.MidBiggerThanMax */));
86017
+ case "maximum":
86018
+ return (errors.includes("MaxInvalidFormula" /* CommandResult.MaxInvalidFormula */) || errors.includes("MaxNaN" /* CommandResult.MaxNaN */));
86019
+ default:
86020
+ return false;
86021
+ }
86022
+ }
85971
86023
  getColorScaleComposerProps(thresholdType) {
85972
- const threshold = this.state.rules.colorScale[thresholdType];
86024
+ const threshold = this.store.state.rules.colorScale[thresholdType];
85973
86025
  if (!threshold) {
85974
86026
  throw new Error("Threshold not found");
85975
86027
  }
@@ -85977,103 +86029,153 @@ stores.inject(MyMetaStore, storeInstance);
85977
86029
  return {
85978
86030
  onConfirm: (str) => {
85979
86031
  threshold.value = str;
85980
- this.updateConditionalFormat({ rule: this.state.rules.colorScale });
86032
+ this.store.updateConditionalFormat({ rule: this.store.state.rules.colorScale });
85981
86033
  },
85982
86034
  composerContent: threshold.value || "",
85983
86035
  placeholder: _t("Formula"),
85984
86036
  defaultStatic: true,
85985
86037
  invalid: isInvalid,
85986
86038
  class: "o-sidePanel-composer",
85987
- defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
86039
+ defaultRangeSheetId: this.activeSheetId,
85988
86040
  };
85989
86041
  }
86042
+ /*****************************************************************************
86043
+ * Icon Set
86044
+ ****************************************************************************/
86045
+ isInflectionPointInvalid(inflectionPoint) {
86046
+ const errors = this.store.state.errors;
86047
+ switch (inflectionPoint) {
86048
+ case "lowerInflectionPoint":
86049
+ return (errors.includes("ValueLowerInflectionNaN" /* CommandResult.ValueLowerInflectionNaN */) ||
86050
+ errors.includes("ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */) ||
86051
+ errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
86052
+ case "upperInflectionPoint":
86053
+ return (errors.includes("ValueUpperInflectionNaN" /* CommandResult.ValueUpperInflectionNaN */) ||
86054
+ errors.includes("ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */) ||
86055
+ errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
86056
+ default:
86057
+ return true;
86058
+ }
86059
+ }
85990
86060
  getColorIconSetComposerProps(inflectionPoint) {
85991
- const inflection = this.state.rules.iconSet[inflectionPoint];
86061
+ const inflection = this.store.state.rules.iconSet[inflectionPoint];
85992
86062
  const isInvalid = this.isInflectionPointInvalid(inflectionPoint);
85993
86063
  return {
85994
86064
  onConfirm: (str) => {
85995
86065
  inflection.value = str;
85996
- this.updateConditionalFormat({ rule: this.state.rules.iconSet });
86066
+ this.store.updateConditionalFormat({ rule: this.store.state.rules.iconSet });
85997
86067
  },
85998
86068
  composerContent: inflection.value || "",
85999
86069
  placeholder: _t("Formula"),
86000
86070
  defaultStatic: true,
86001
86071
  invalid: isInvalid,
86002
86072
  class: "o-sidePanel-composer",
86003
- defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
86073
+ defaultRangeSheetId: this.activeSheetId,
86004
86074
  };
86005
86075
  }
86006
- /*****************************************************************************
86007
- * DataBar
86008
- ****************************************************************************/
86009
- getRangeValues() {
86010
- return [this.state.rules.dataBar.rangeValues || ""];
86076
+ }
86077
+
86078
+ function useHighlightsOnHover(ref, highlightProvider) {
86079
+ const hoverState = useHoveredElement(ref);
86080
+ useHighlights({
86081
+ get highlights() {
86082
+ return hoverState.hovered ? highlightProvider.highlights : [];
86083
+ },
86084
+ });
86085
+ }
86086
+ function useHighlights(highlightProvider) {
86087
+ const stores = useStoreProvider();
86088
+ const store = useLocalStore(HighlightStore);
86089
+ owl.onMounted(() => {
86090
+ store.register(highlightProvider);
86091
+ });
86092
+ let currentHighlights = highlightProvider.highlights;
86093
+ owl.useEffect((highlights) => {
86094
+ if (!deepEquals(highlights, currentHighlights)) {
86095
+ currentHighlights = highlights;
86096
+ stores.trigger("store-updated");
86097
+ }
86098
+ }, () => [highlightProvider.highlights]);
86099
+ }
86100
+
86101
+ class ConditionalFormatPreview extends owl.Component {
86102
+ static template = "o-spreadsheet-ConditionalFormatPreview";
86103
+ static props = {
86104
+ conditionalFormat: Object,
86105
+ onMouseDown: Function,
86106
+ class: String,
86107
+ };
86108
+ icons = ICONS;
86109
+ ref = owl.useRef("cfPreview");
86110
+ setup() {
86111
+ useHighlightsOnHover(this.ref, this);
86011
86112
  }
86012
- updateDataBarColor(color) {
86013
- if (!isColorValid(color)) {
86014
- return;
86113
+ get previewImageStyle() {
86114
+ const rule = this.props.conditionalFormat.rule;
86115
+ if (rule.type === "CellIsRule") {
86116
+ return cssPropertiesToCss(cellStyleToCss(rule.style));
86015
86117
  }
86016
- this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
86017
- this.updateConditionalFormat({ rule: this.state.rules.dataBar });
86118
+ else if (rule.type === "ColorScaleRule") {
86119
+ const minColor = colorNumberToHex(rule.minimum.color);
86120
+ const midColor = rule.midpoint ? colorNumberToHex(rule.midpoint.color) : null;
86121
+ const maxColor = colorNumberToHex(rule.maximum.color);
86122
+ const baseString = "background-image: linear-gradient(to right, ";
86123
+ return midColor
86124
+ ? baseString + minColor + ", " + midColor + ", " + maxColor + ")"
86125
+ : baseString + minColor + ", " + maxColor + ")";
86126
+ }
86127
+ else if (rule.type === "DataBarRule") {
86128
+ const barColor = colorNumberToHex(rule.color);
86129
+ const gradient = `background-image: linear-gradient(to right, ${barColor} 50%, white 50%)`;
86130
+ return `${gradient}; color: ${TEXT_BODY};`;
86131
+ }
86132
+ return "";
86018
86133
  }
86019
- onDataBarRangeUpdate(ranges) {
86020
- this.state.rules.dataBar.rangeValues = ranges[0];
86134
+ get description() {
86135
+ const cf = this.props.conditionalFormat;
86136
+ switch (cf.rule.type) {
86137
+ case "CellIsRule":
86138
+ return criterionEvaluatorRegistry
86139
+ .get(cf.rule.operator)
86140
+ .getPreview({ ...cf.rule, type: cf.rule.operator }, this.env.model.getters);
86141
+ case "ColorScaleRule":
86142
+ return CfTerms.ColorScale;
86143
+ case "IconSetRule":
86144
+ return CfTerms.IconSet;
86145
+ case "DataBarRule":
86146
+ return CfTerms.DataBar;
86147
+ }
86021
86148
  }
86022
- onDataBarRangeChange() {
86023
- this.updateConditionalFormat({ rule: this.state.rules.dataBar });
86149
+ get highlights() {
86150
+ const sheetId = this.env.model.getters.getActiveSheetId();
86151
+ return this.props.conditionalFormat.ranges.map((range) => ({
86152
+ range: this.env.model.getters.getRangeFromSheetXC(sheetId, range),
86153
+ color: HIGHLIGHT_COLOR,
86154
+ fillAlpha: 0.06,
86155
+ }));
86156
+ }
86157
+ editConditionalFormat() {
86158
+ this.env.replaceSidePanel("ConditionalFormattingEditor", "ConditionalFormatting", {
86159
+ cf: this.props.conditionalFormat,
86160
+ isNewCf: false,
86161
+ });
86162
+ }
86163
+ deleteConditionalFormat() {
86164
+ this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
86165
+ id: this.props.conditionalFormat.id,
86166
+ sheetId: this.env.model.getters.getActiveSheetId(),
86167
+ });
86024
86168
  }
86025
86169
  }
86026
86170
 
86027
- class ConditionalFormattingPanel extends owl.Component {
86028
- static template = "o-spreadsheet-ConditionalFormattingPanel";
86171
+ class ConditionalFormatPreviewList extends owl.Component {
86172
+ static template = "o-spreadsheet-ConditionalFormatPreviewList";
86029
86173
  static props = {
86030
- selection: { type: Object, optional: true },
86031
86174
  onCloseSidePanel: Function,
86032
86175
  };
86033
- static components = {
86034
- ConditionalFormatPreviewList,
86035
- ConditionalFormattingEditor,
86036
- Section,
86037
- };
86038
- activeSheetId;
86039
- originalEditedCf = undefined;
86040
- state = owl.useState({
86041
- mode: "list",
86042
- });
86043
- setup() {
86044
- this.activeSheetId = this.env.model.getters.getActiveSheetId();
86045
- const sheetId = this.env.model.getters.getActiveSheetId();
86046
- const rules = this.env.model.getters.getRulesSelection(sheetId, this.props.selection || []);
86047
- if (rules.length === 1) {
86048
- const cf = this.conditionalFormats.find((c) => c.id === rules[0]);
86049
- if (cf) {
86050
- this.editConditionalFormat(cf);
86051
- }
86052
- }
86053
- owl.onWillUpdateProps((nextProps) => {
86054
- const newActiveSheetId = this.env.model.getters.getActiveSheetId();
86055
- if (newActiveSheetId !== this.activeSheetId) {
86056
- this.activeSheetId = newActiveSheetId;
86057
- this.switchToList();
86058
- }
86059
- else if (nextProps.selection !== this.props.selection) {
86060
- const sheetId = this.env.model.getters.getActiveSheetId();
86061
- const rules = this.env.model.getters.getRulesSelection(sheetId, nextProps.selection || []);
86062
- if (rules.length === 1) {
86063
- const cf = this.conditionalFormats.find((c) => c.id === rules[0]);
86064
- if (cf) {
86065
- this.editConditionalFormat(cf);
86066
- }
86067
- }
86068
- else {
86069
- this.switchToList();
86070
- }
86071
- }
86072
- else if (!this.editedCF) {
86073
- this.switchToList();
86074
- }
86075
- });
86076
- }
86176
+ static components = { ConditionalFormatPreview };
86177
+ dragAndDrop = useDragAndDropListItems();
86178
+ cfListRef = owl.useRef("cfList");
86077
86179
  get conditionalFormats() {
86078
86180
  const cfs = this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
86079
86181
  return cfs.map((cf) => ({
@@ -86081,66 +86183,129 @@ stores.inject(MyMetaStore, storeInstance);
86081
86183
  rule: localizeCFRule(cf.rule, this.env.model.getters.getLocale()),
86082
86184
  }));
86083
86185
  }
86084
- switchToList() {
86085
- this.state.mode = "list";
86086
- this.state.editedCfId = undefined;
86087
- this.originalEditedCf = undefined;
86186
+ getPreviewDivStyle(cf) {
86187
+ return this.dragAndDrop.itemsStyle[cf.id] || "";
86088
86188
  }
86089
- addConditionalFormat() {
86090
- const cfId = this.env.model.uuidGenerator.smallUuid();
86189
+ onPreviewMouseDown(cf, event) {
86190
+ if (event.button !== 0)
86191
+ return;
86192
+ const previewRects = Array.from(this.cfListRef.el.children).map((previewEl) => getBoundingRectAsPOJO(previewEl));
86193
+ const items = this.conditionalFormats.map((cf, index) => ({
86194
+ id: cf.id,
86195
+ size: previewRects[index].height,
86196
+ position: previewRects[index].y,
86197
+ }));
86198
+ this.dragAndDrop.start("vertical", {
86199
+ draggedItemId: cf.id,
86200
+ initialMousePosition: event.clientY,
86201
+ items: items,
86202
+ scrollableContainerEl: this.cfListRef.el,
86203
+ onDragEnd: (cfId, finalIndex) => this.onDragEnd(cfId, finalIndex),
86204
+ });
86205
+ }
86206
+ onAddConditionalFormat() {
86207
+ const sheetId = this.env.model.getters.getActiveSheetId();
86208
+ const zones = this.env.model.getters.getSelectedZones();
86209
+ const cf = {
86210
+ id: this.env.model.uuidGenerator.smallUuid(),
86211
+ rule: {
86212
+ type: "CellIsRule",
86213
+ operator: "isNotEmpty",
86214
+ style: { fillColor: "#b6d7a8" },
86215
+ values: [],
86216
+ },
86217
+ };
86091
86218
  this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
86092
- sheetId: this.activeSheetId,
86093
- ranges: this.env.model.getters
86094
- .getSelectedZones()
86095
- .map((zone) => this.env.model.getters.getRangeDataFromZone(this.activeSheetId, zone)),
86219
+ cf,
86220
+ ranges: zones.map((zone) => this.env.model.getters.getRangeDataFromZone(sheetId, zone)),
86221
+ sheetId,
86222
+ });
86223
+ return this.env.replaceSidePanel("ConditionalFormattingEditor", "ConditionalFormatting", {
86096
86224
  cf: {
86097
- id: cfId,
86098
- rule: {
86099
- type: "CellIsRule",
86100
- operator: "isNotEmpty",
86101
- style: { fillColor: "#b6d7a8" },
86102
- values: [],
86103
- },
86225
+ ...cf,
86226
+ ranges: zones.map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone))),
86104
86227
  },
86228
+ isNewCf: true,
86105
86229
  });
86106
- this.state.editedCfId = cfId;
86107
- this.state.mode = "edit";
86108
- this.originalEditedCf = undefined;
86109
- }
86110
- editConditionalFormat(cf) {
86111
- this.state.mode = "edit";
86112
- this.state.editedCfId = cf.id;
86113
- this.originalEditedCf = cf;
86114
86230
  }
86115
- cancelEdition() {
86116
- if (this.originalEditedCf) {
86117
- this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
86118
- sheetId: this.activeSheetId,
86119
- ranges: this.originalEditedCf.ranges.map((range) => this.env.model.getters.getRangeDataFromXc(this.activeSheetId, range)),
86120
- cf: this.originalEditedCf,
86121
- });
86122
- }
86123
- else if (this.state.editedCfId) {
86124
- this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
86125
- sheetId: this.activeSheetId,
86126
- id: this.state.editedCfId,
86231
+ onDragEnd(cfId, finalIndex) {
86232
+ const originalIndex = this.conditionalFormats.findIndex((sheet) => sheet.id === cfId);
86233
+ const delta = originalIndex - finalIndex;
86234
+ if (delta !== 0) {
86235
+ this.env.model.dispatch("CHANGE_CONDITIONAL_FORMAT_PRIORITY", {
86236
+ cfId,
86237
+ delta,
86238
+ sheetId: this.env.model.getters.getActiveSheetId(),
86127
86239
  });
86128
86240
  }
86129
- this.switchToList();
86130
86241
  }
86131
- get editedCF() {
86132
- return this.conditionalFormats.find((cf) => cf.id === this.state.editedCfId);
86242
+ }
86243
+
86244
+ class DataValidationPreview extends owl.Component {
86245
+ static template = "o-spreadsheet-DataValidationPreview";
86246
+ static props = {
86247
+ rule: Object,
86248
+ };
86249
+ ref = owl.useRef("dvPreview");
86250
+ setup() {
86251
+ useHighlightsOnHover(this.ref, this);
86252
+ }
86253
+ onPreviewClick() {
86254
+ this.env.replaceSidePanel("DataValidationEditor", "DataValidation", {
86255
+ ruleId: this.props.rule.id,
86256
+ });
86257
+ }
86258
+ deleteDataValidation() {
86259
+ const sheetId = this.env.model.getters.getActiveSheetId();
86260
+ this.env.model.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: this.props.rule.id });
86261
+ }
86262
+ get highlights() {
86263
+ return this.props.rule.ranges.map((range) => ({
86264
+ range,
86265
+ color: HIGHLIGHT_COLOR,
86266
+ fillAlpha: 0.06,
86267
+ }));
86268
+ }
86269
+ get rangesString() {
86270
+ const sheetId = this.env.model.getters.getActiveSheetId();
86271
+ return this.props.rule.ranges
86272
+ .map((range) => this.env.model.getters.getRangeString(range, sheetId))
86273
+ .join(", ");
86274
+ }
86275
+ get descriptionString() {
86276
+ return criterionEvaluatorRegistry
86277
+ .get(this.props.rule.criterion.type)
86278
+ .getPreview(this.props.rule.criterion, this.env.model.getters);
86279
+ }
86280
+ }
86281
+
86282
+ class DataValidationPanel extends owl.Component {
86283
+ static template = "o-spreadsheet-DataValidationPanel";
86284
+ static props = {
86285
+ onCloseSidePanel: Function,
86286
+ };
86287
+ static components = { DataValidationPreview };
86288
+ addDataValidationRule() {
86289
+ this.env.replaceSidePanel("DataValidationEditor", "DataValidation", {
86290
+ ruleId: this.env.model.uuidGenerator.smallUuid(),
86291
+ });
86292
+ }
86293
+ localizeDVRule(rule) {
86294
+ if (!rule)
86295
+ return rule;
86296
+ const locale = this.env.model.getters.getLocale();
86297
+ return localizeDataValidationRule(rule, locale);
86298
+ }
86299
+ get validationRules() {
86300
+ const sheetId = this.env.model.getters.getActiveSheetId();
86301
+ return this.env.model.getters.getDataValidationRules(sheetId);
86133
86302
  }
86134
86303
  }
86135
86304
 
86136
86305
  class DataValidationEditor extends owl.Component {
86137
86306
  static template = "o-spreadsheet-DataValidationEditor";
86138
86307
  static components = { SelectionInput, SelectMenu, Section, ValidationMessages };
86139
- static props = {
86140
- rule: { type: Object, optional: true },
86141
- onExit: Function,
86142
- onCloseSidePanel: { type: Function, optional: true },
86143
- };
86308
+ static props = { ruleId: String, onCloseSidePanel: Function };
86144
86309
  state = owl.useState({
86145
86310
  rule: this.defaultDataValidationRule,
86146
86311
  errors: [],
@@ -86149,12 +86314,13 @@ stores.inject(MyMetaStore, storeInstance);
86149
86314
  editingSheetId;
86150
86315
  setup() {
86151
86316
  this.editingSheetId = this.env.model.getters.getActiveSheetId();
86152
- if (this.props.rule) {
86317
+ const rule = this.env.model.getters.getDataValidationRule(this.editingSheetId, this.props.ruleId);
86318
+ if (rule) {
86319
+ const locale = this.env.model.getters.getLocale();
86153
86320
  this.state.rule = {
86154
- ...this.props.rule,
86155
- ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, this.editingSheetId)),
86321
+ ...localizeDataValidationRule(rule, locale),
86322
+ ranges: rule.ranges.map((range) => this.env.model.getters.getRangeString(range, this.editingSheetId)),
86156
86323
  };
86157
- this.state.rule.criterion.type = this.props.rule.criterion.type;
86158
86324
  }
86159
86325
  }
86160
86326
  onCriterionTypeChanged(type) {
@@ -86171,16 +86337,16 @@ stores.inject(MyMetaStore, storeInstance);
86171
86337
  const isBlocking = ev.target.value;
86172
86338
  this.state.rule.isBlocking = isBlocking === "true";
86173
86339
  }
86340
+ onCancel() {
86341
+ this.env.replaceSidePanel("DataValidation", `DataValidationEditor_${this.props.ruleId}`);
86342
+ }
86174
86343
  onSave() {
86175
- if (this.state.rule) {
86176
- const result = this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
86177
- if (!result.isSuccessful) {
86178
- this.state.errors = result.reasons;
86179
- }
86180
- else {
86181
- this.props.onExit();
86182
- }
86344
+ const result = this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
86345
+ if (!result.isSuccessful) {
86346
+ this.state.errors = result.reasons;
86347
+ return;
86183
86348
  }
86349
+ this.env.replaceSidePanel("DataValidation", `DataValidationEditor_${this.props.ruleId}`);
86184
86350
  }
86185
86351
  get dispatchPayload() {
86186
86352
  const rule = { ...this.state.rule, ranges: undefined };
@@ -86212,7 +86378,7 @@ stores.inject(MyMetaStore, storeInstance);
86212
86378
  .getSelectedZones()
86213
86379
  .map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone)));
86214
86380
  return {
86215
- id: this.env.model.uuidGenerator.smallUuid(),
86381
+ id: this.props.ruleId,
86216
86382
  criterion: { type: "containsText", values: [""] },
86217
86383
  ranges,
86218
86384
  };
@@ -86225,75 +86391,6 @@ stores.inject(MyMetaStore, storeInstance);
86225
86391
  }
86226
86392
  }
86227
86393
 
86228
- class DataValidationPreview extends owl.Component {
86229
- static template = "o-spreadsheet-DataValidationPreview";
86230
- static props = {
86231
- onClick: Function,
86232
- rule: Object,
86233
- };
86234
- ref = owl.useRef("dvPreview");
86235
- setup() {
86236
- useHighlightsOnHover(this.ref, this);
86237
- }
86238
- deleteDataValidation() {
86239
- const sheetId = this.env.model.getters.getActiveSheetId();
86240
- this.env.model.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: this.props.rule.id });
86241
- }
86242
- get highlights() {
86243
- return this.props.rule.ranges.map((range) => ({
86244
- range,
86245
- color: HIGHLIGHT_COLOR,
86246
- fillAlpha: 0.06,
86247
- }));
86248
- }
86249
- get rangesString() {
86250
- const sheetId = this.env.model.getters.getActiveSheetId();
86251
- return this.props.rule.ranges
86252
- .map((range) => this.env.model.getters.getRangeString(range, sheetId))
86253
- .join(", ");
86254
- }
86255
- get descriptionString() {
86256
- return criterionEvaluatorRegistry
86257
- .get(this.props.rule.criterion.type)
86258
- .getPreview(this.props.rule.criterion, this.env.model.getters);
86259
- }
86260
- }
86261
-
86262
- class DataValidationPanel extends owl.Component {
86263
- static template = "o-spreadsheet-DataValidationPanel";
86264
- static props = {
86265
- onCloseSidePanel: Function,
86266
- };
86267
- static components = { DataValidationPreview, DataValidationEditor };
86268
- state = owl.useState({ mode: "list", activeRule: undefined });
86269
- onPreviewClick(id) {
86270
- const sheetId = this.env.model.getters.getActiveSheetId();
86271
- const rule = this.env.model.getters.getDataValidationRule(sheetId, id);
86272
- if (rule) {
86273
- this.state.mode = "edit";
86274
- this.state.activeRule = rule;
86275
- }
86276
- }
86277
- addDataValidationRule() {
86278
- this.state.mode = "edit";
86279
- this.state.activeRule = undefined;
86280
- }
86281
- onExitEditMode() {
86282
- this.state.mode = "list";
86283
- this.state.activeRule = undefined;
86284
- }
86285
- localizeDVRule(rule) {
86286
- if (!rule)
86287
- return rule;
86288
- const locale = this.env.model.getters.getLocale();
86289
- return localizeDataValidationRule(rule, locale);
86290
- }
86291
- get validationRules() {
86292
- const sheetId = this.env.model.getters.getActiveSheetId();
86293
- return this.env.model.getters.getDataValidationRules(sheetId);
86294
- }
86295
- }
86296
-
86297
86394
  const FIND_AND_REPLACE_HIGHLIGHT_COLOR = "#8B008B";
86298
86395
  var Direction;
86299
86396
  (function (Direction) {
@@ -89171,7 +89268,18 @@ stores.inject(MyMetaStore, storeInstance);
89171
89268
  const sidePanelRegistry = new Registry$1();
89172
89269
  sidePanelRegistry.add("ConditionalFormatting", {
89173
89270
  title: _t("Conditional formatting"),
89174
- Body: ConditionalFormattingPanel,
89271
+ Body: ConditionalFormatPreviewList,
89272
+ });
89273
+ sidePanelRegistry.add("ConditionalFormattingEditor", {
89274
+ title: _t("Conditional formatting"),
89275
+ Body: ConditionalFormattingEditor,
89276
+ computeState: (getters, props) => {
89277
+ return {
89278
+ isOpen: true,
89279
+ props,
89280
+ key: `ConditionalFormattingEditor_${props.cf.id}`,
89281
+ };
89282
+ },
89175
89283
  });
89176
89284
  sidePanelRegistry.add("ChartPanel", {
89177
89285
  title: _t("Chart"),
@@ -89208,6 +89316,13 @@ stores.inject(MyMetaStore, storeInstance);
89208
89316
  sidePanelRegistry.add("DataValidationEditor", {
89209
89317
  title: _t("Data validation"),
89210
89318
  Body: DataValidationEditor,
89319
+ computeState: (getters, props) => {
89320
+ return {
89321
+ isOpen: true,
89322
+ props,
89323
+ key: `DataValidationEditor_${props.ruleId}`,
89324
+ };
89325
+ },
89211
89326
  });
89212
89327
  sidePanelRegistry.add("MoreFormats", {
89213
89328
  title: _t("More formats"),
@@ -89894,7 +90009,8 @@ stores.inject(MyMetaStore, storeInstance);
89894
90009
  });
89895
90010
  return !(rect.width === 0 || rect.height === 0);
89896
90011
  }
89897
- onGridResized({ height, width }) {
90012
+ onGridResized() {
90013
+ const { height, width } = this.props.getGridSize();
89898
90014
  this.env.model.dispatch("RESIZE_SHEETVIEW", {
89899
90015
  width: width - HEADER_WIDTH,
89900
90016
  height: height - HEADER_HEIGHT,
@@ -93442,10 +93558,8 @@ stores.inject(MyMetaStore, storeInstance);
93442
93558
  });
93443
93559
  }
93444
93560
  get gridContainer() {
93445
- const sheetId = this.env.model.getters.getActiveSheetId();
93446
- const { right } = this.env.model.getters.getSheetZone(sheetId);
93447
- const { end } = this.env.model.getters.getColDimensions(sheetId, right);
93448
- return cssPropertiesToCss({ "max-width": `${end}px` });
93561
+ const maxWidth = this.getMaxSheetWidth();
93562
+ return cssPropertiesToCss({ "max-width": `${maxWidth}px` });
93449
93563
  }
93450
93564
  get gridOverlayDimensions() {
93451
93565
  return cssPropertiesToCss({
@@ -93477,10 +93591,12 @@ stores.inject(MyMetaStore, storeInstance);
93477
93591
  onClosePopover() {
93478
93592
  this.cellPopovers.close();
93479
93593
  }
93480
- onGridResized({ height, width }) {
93594
+ onGridResized() {
93595
+ const { height, width } = this.props.getGridSize();
93596
+ const maxWidth = this.getMaxSheetWidth();
93481
93597
  this.env.model.dispatch("RESIZE_SHEETVIEW", {
93482
- width: width,
93483
- height: height,
93598
+ width: Math.min(maxWidth, width),
93599
+ height,
93484
93600
  gridOffsetX: 0,
93485
93601
  gridOffsetY: 0,
93486
93602
  });
@@ -93498,6 +93614,11 @@ stores.inject(MyMetaStore, storeInstance);
93498
93614
  ...this.env.model.getters.getSheetViewDimensionWithHeaders(),
93499
93615
  };
93500
93616
  }
93617
+ getMaxSheetWidth() {
93618
+ const sheetId = this.env.model.getters.getActiveSheetId();
93619
+ const { right } = this.env.model.getters.getSheetZone(sheetId);
93620
+ return this.env.model.getters.getColDimensions(sheetId, right).end;
93621
+ }
93501
93622
  }
93502
93623
 
93503
93624
  class AbstractHeaderGroup extends owl.Component {
@@ -97953,9 +98074,9 @@ stores.inject(MyMetaStore, storeInstance);
97953
98074
  exports.tokenize = tokenize;
97954
98075
 
97955
98076
 
97956
- __info__.version = "19.2.0-alpha.1";
97957
- __info__.date = "2025-12-26T10:20:28.360Z";
97958
- __info__.hash = "3296c7e";
98077
+ __info__.version = "19.2.0-alpha.2";
98078
+ __info__.date = "2026-01-07T16:21:35.251Z";
98079
+ __info__.hash = "ac2fa3e";
97959
98080
 
97960
98081
 
97961
98082
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);