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