@odoo/o-spreadsheet 19.1.2 → 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.1.2
6
- * @date 2026-01-07T16:21:36.757Z
7
- * @hash febc3e9
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."),
@@ -34020,6 +34022,7 @@ stores.inject(MyMetaStore, storeInstance);
34020
34022
  greaterThanOrEqual: "isGreaterOrEqualTo",
34021
34023
  lessThan: "isLessThan",
34022
34024
  lessThanOrEqual: "isLessOrEqualTo",
34025
+ top10: "top10",
34023
34026
  };
34024
34027
  /** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */
34025
34028
  const CF_TYPE_CONVERSION_MAP = {
@@ -34560,6 +34563,7 @@ stores.inject(MyMetaStore, storeInstance);
34560
34563
  const rule = cf.cfRules[0];
34561
34564
  let operator;
34562
34565
  const values = [];
34566
+ const cfAdditionalProperties = {};
34563
34567
  if (rule.dxfId === undefined &&
34564
34568
  !(rule.type === "colorScale" || rule.type === "iconSet" || rule.type === "dataBar"))
34565
34569
  continue;
@@ -34568,7 +34572,6 @@ stores.inject(MyMetaStore, storeInstance);
34568
34572
  case "containsErrors":
34569
34573
  case "notContainsErrors":
34570
34574
  case "duplicateValues":
34571
- case "top10":
34572
34575
  case "uniqueValues":
34573
34576
  case "timePeriod":
34574
34577
  // Not supported
@@ -34619,6 +34622,18 @@ stores.inject(MyMetaStore, storeInstance);
34619
34622
  values.push(prefixFormulaWithEqual(rule.formula[1]));
34620
34623
  }
34621
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;
34622
34637
  }
34623
34638
  if (operator && rule.dxfId !== undefined) {
34624
34639
  cfs.push({
@@ -34629,6 +34644,7 @@ stores.inject(MyMetaStore, storeInstance);
34629
34644
  type: "CellIsRule",
34630
34645
  operator: operator,
34631
34646
  values: values,
34647
+ ...cfAdditionalProperties,
34632
34648
  style: convertStyle({ fontStyle: dxfs[rule.dxfId].font, fillStyle: dxfs[rule.dxfId].fill }, warningManager),
34633
34649
  },
34634
34650
  });
@@ -34847,6 +34863,8 @@ stores.inject(MyMetaStore, storeInstance);
34847
34863
  return "greaterThanOrEqual";
34848
34864
  case "dateIsOnOrBefore":
34849
34865
  return "lessThanOrEqual";
34866
+ case "top10":
34867
+ return "top10";
34850
34868
  }
34851
34869
  }
34852
34870
  // -------------------------------------
@@ -40630,15 +40648,20 @@ stores.inject(MyMetaStore, storeInstance);
40630
40648
  getPreview: (criterion) => _t("Value one of: %s", criterion.values.join(", ")),
40631
40649
  });
40632
40650
  criterionEvaluatorRegistry.add("isValueInRange", {
40633
- type: "isValueInList",
40634
- 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) => {
40635
40661
  if (!value) {
40636
40662
  return false;
40637
40663
  }
40638
- const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
40639
- return criterionValues
40640
- .map((value) => value.value.toLowerCase())
40641
- .includes(value.toString().toLowerCase());
40664
+ return valuesSet.has(value.toString().toLowerCase());
40642
40665
  },
40643
40666
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
40644
40667
  isCriterionValueValid: (value) => rangeReference.test(value),
@@ -40715,6 +40738,67 @@ stores.inject(MyMetaStore, storeInstance);
40715
40738
  name: _t("Is not empty"),
40716
40739
  getPreview: () => _t("Is not empty"),
40717
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
+ });
40718
40802
  function getNumberCriterionlocalizedValues(criterion, locale) {
40719
40803
  return criterion.values.map((value) => {
40720
40804
  return value !== undefined
@@ -40736,6 +40820,10 @@ stores.inject(MyMetaStore, storeInstance);
40736
40820
  const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
40737
40821
  return valueAsNumber !== undefined;
40738
40822
  }
40823
+ function checkValueIsPositiveNumber(value) {
40824
+ const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
40825
+ return valueAsNumber !== undefined && valueAsNumber > 0;
40826
+ }
40739
40827
 
40740
40828
  // -----------------------------------------------------------------------------
40741
40829
  // Constants
@@ -51110,6 +51198,10 @@ stores.inject(MyMetaStore, storeInstance);
51110
51198
  break;
51111
51199
  case "CellIsRule":
51112
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);
51113
51205
  for (const ref of cf.ranges) {
51114
51206
  const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
51115
51207
  for (let row = zone.top; row <= zone.bottom; row++) {
@@ -51122,7 +51214,7 @@ stores.inject(MyMetaStore, storeInstance);
51122
51214
  }
51123
51215
  return value;
51124
51216
  });
51125
- if (this.getRuleResultForTarget(target, { ...cf.rule, values })) {
51217
+ if (this.getRuleResultForTarget(target, { ...cf.rule, values }, preComputedCriterion)) {
51126
51218
  if (!computedStyle[col])
51127
51219
  computedStyle[col] = [];
51128
51220
  // we must combine all the properties of all the CF rules applied to the given cell
@@ -51285,7 +51377,7 @@ stores.inject(MyMetaStore, storeInstance);
51285
51377
  }
51286
51378
  }
51287
51379
  }
51288
- getRuleResultForTarget(target, rule) {
51380
+ getRuleResultForTarget(target, rule, preComputedCriterion) {
51289
51381
  const cell = this.getters.getEvaluatedCell(target);
51290
51382
  if (cell.type === CellValueType.error) {
51291
51383
  return false;
@@ -51302,11 +51394,12 @@ stores.inject(MyMetaStore, storeInstance);
51302
51394
  return false;
51303
51395
  }
51304
51396
  const evaluatedCriterion = {
51397
+ ...rule,
51305
51398
  type: rule.operator,
51306
51399
  values: evaluatedCriterionValues.map(toScalar),
51307
51400
  dateValue: rule.dateValue || "exactDate",
51308
51401
  };
51309
- return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, this.getters, sheetId);
51402
+ return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, preComputedCriterion);
51310
51403
  }
51311
51404
  }
51312
51405
 
@@ -51323,17 +51416,20 @@ stores.inject(MyMetaStore, storeInstance);
51323
51416
  "isDataValidationInvalid",
51324
51417
  ];
51325
51418
  validationResults = {};
51419
+ criterionPreComputeResult = {};
51326
51420
  handle(cmd) {
51327
51421
  if (invalidateEvaluationCommands.has(cmd.type) ||
51328
51422
  cmd.type === "EVALUATE_CELLS" ||
51329
51423
  (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd))) {
51330
51424
  this.validationResults = {};
51425
+ this.criterionPreComputeResult = {};
51331
51426
  return;
51332
51427
  }
51333
51428
  switch (cmd.type) {
51334
51429
  case "ADD_DATA_VALIDATION_RULE":
51335
51430
  case "REMOVE_DATA_VALIDATION_RULE":
51336
51431
  delete this.validationResults[cmd.sheetId];
51432
+ delete this.criterionPreComputeResult[cmd.sheetId];
51337
51433
  break;
51338
51434
  }
51339
51435
  }
@@ -51476,7 +51572,15 @@ stores.inject(MyMetaStore, storeInstance);
51476
51572
  return undefined;
51477
51573
  }
51478
51574
  const evaluatedCriterion = { ...criterion, values: evaluatedCriterionValues.map(toScalar) };
51479
- 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)) {
51480
51584
  return undefined;
51481
51585
  }
51482
51586
  return evaluator.getErrorString(evaluatedCriterion, this.getters, sheetId);
@@ -57171,6 +57275,7 @@ stores.inject(MyMetaStore, storeInstance);
57171
57275
  if (filterValue.type === "none")
57172
57276
  continue;
57173
57277
  const evaluator = criterionEvaluatorRegistry.get(filterValue.type);
57278
+ const preComputedCriterion = evaluator.preComputeCriterion?.(filterValue, [filter.filteredRange], this.getters);
57174
57279
  const evaluatedCriterionValues = filterValue.values.map((value) => {
57175
57280
  if (!value.startsWith("=")) {
57176
57281
  return parseLiteral(value, DEFAULT_LOCALE);
@@ -57188,7 +57293,7 @@ stores.inject(MyMetaStore, storeInstance);
57188
57293
  for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
57189
57294
  const position = { sheetId, col: filter.col, row };
57190
57295
  const value = this.getters.getEvaluatedCell(position).value ?? "";
57191
- if (!evaluator.isValueValid(value, evaluatedCriterion, this.getters, sheetId)) {
57296
+ if (!evaluator.isValueValid(value, evaluatedCriterion, preComputedCriterion)) {
57192
57297
  hiddenRows.add(row);
57193
57298
  }
57194
57299
  }
@@ -61315,6 +61420,8 @@ stores.inject(MyMetaStore, storeInstance);
61315
61420
  case undefined:
61316
61421
  throw new Error("dateValue should be defined");
61317
61422
  }
61423
+ case "top10":
61424
+ return [];
61318
61425
  }
61319
61426
  }
61320
61427
  function cellRuleTypeAttributes(rule) {
@@ -61347,6 +61454,14 @@ stores.inject(MyMetaStore, storeInstance);
61347
61454
  case "dateIs":
61348
61455
  case "customFormula":
61349
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
+ }
61350
61465
  }
61351
61466
  }
61352
61467
  function addDataBarRule(cf, rule) {
@@ -64929,13 +65044,16 @@ stores.inject(MyMetaStore, storeInstance);
64929
65044
  document.body.removeChild(a);
64930
65045
  }
64931
65046
  /**
64932
- * Detects if the current browser is Firefox
65047
+ * Detects the current browser brand and subsequent rendering engine
64933
65048
  */
64934
65049
  function isBrowserFirefox() {
64935
65050
  return /Firefox/i.test(navigator.userAgent);
64936
65051
  }
65052
+ function isBrowserChrome() {
65053
+ return /Chrome/i.test(navigator.userAgent);
65054
+ }
64937
65055
  function isBrowserSafari() {
64938
- return /Safari/i.test(navigator.userAgent);
65056
+ return !isBrowserChrome() && /Safari/i.test(navigator.userAgent);
64939
65057
  }
64940
65058
  // Mobile detection
64941
65059
  function maxTouchPoints() {
@@ -65064,6 +65182,7 @@ stores.inject(MyMetaStore, storeInstance);
65064
65182
  "dateIsAfter",
65065
65183
  "dateIsOnOrBefore",
65066
65184
  "dateIsOnOrAfter",
65185
+ "top10",
65067
65186
  ];
65068
65187
  const availableConditionalFormatOperators = new Set(cfOperators);
65069
65188
 
@@ -73035,6 +73154,26 @@ stores.inject(MyMetaStore, storeInstance);
73035
73154
  }
73036
73155
  }
73037
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
+
73038
73177
  /**
73039
73178
  * Start listening to pointer events and apply the given callbacks.
73040
73179
  *
@@ -74204,6 +74343,7 @@ stores.inject(MyMetaStore, storeInstance);
74204
74343
  text: 20,
74205
74344
  number: 30,
74206
74345
  date: 40,
74346
+ relative: 45,
74207
74347
  misc: 50,
74208
74348
  };
74209
74349
  const criterionComponentRegistry = new Registry$1();
@@ -74381,6 +74521,12 @@ stores.inject(MyMetaStore, storeInstance);
74381
74521
  category: "misc",
74382
74522
  sequence: 6,
74383
74523
  });
74524
+ criterionComponentRegistry.add("top10", {
74525
+ type: "top10",
74526
+ component: Top10CriterionForm,
74527
+ category: "relative",
74528
+ sequence: 7,
74529
+ });
74384
74530
  function getCriterionMenuItems(callback, availableTypes) {
74385
74531
  const items = criterionComponentRegistry
74386
74532
  .getAll()
@@ -76011,7 +76157,17 @@ stores.inject(MyMetaStore, storeInstance);
76011
76157
  // Side panel
76012
76158
  //------------------------------------------------------------------------------
76013
76159
  const OPEN_CF_SIDEPANEL_ACTION = (env) => {
76014
- 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");
76015
76171
  };
76016
76172
  const INSERT_LINK = (env) => {
76017
76173
  const { col, row } = env.model.getters.getActivePosition();
@@ -77009,12 +77165,12 @@ stores.inject(MyMetaStore, storeInstance);
77009
77165
  const zones = env.model.getters.getSelectedZones();
77010
77166
  const sheetId = env.model.getters.getActiveSheetId();
77011
77167
  const ranges = zones.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
77012
- const ruleID = env.model.uuidGenerator.smallUuid();
77168
+ const ruleId = env.model.uuidGenerator.smallUuid();
77013
77169
  env.model.dispatch("ADD_DATA_VALIDATION_RULE", {
77014
77170
  ranges,
77015
77171
  sheetId,
77016
77172
  rule: {
77017
- id: ruleID,
77173
+ id: ruleId,
77018
77174
  criterion: {
77019
77175
  type: "isValueInList",
77020
77176
  values: [],
@@ -77022,16 +77178,11 @@ stores.inject(MyMetaStore, storeInstance);
77022
77178
  },
77023
77179
  },
77024
77180
  });
77025
- const rule = env.model.getters.getDataValidationRule(sheetId, ruleID);
77181
+ const rule = env.model.getters.getDataValidationRule(sheetId, ruleId);
77026
77182
  if (!rule) {
77027
77183
  return;
77028
77184
  }
77029
- env.openSidePanel("DataValidationEditor", {
77030
- rule: localizeDataValidationRule(rule, env.model.getters.getLocale()),
77031
- onExit: () => {
77032
- env.replaceSidePanel("DataValidation", "DataValidationEditor");
77033
- },
77034
- });
77185
+ env.openSidePanel("DataValidationEditor", { ruleId });
77035
77186
  },
77036
77187
  isEnabled: (env) => !env.isSmall,
77037
77188
  icon: "o-spreadsheet-Icon.INSERT_DROPDOWN",
@@ -85483,210 +85634,36 @@ stores.inject(MyMetaStore, storeInstance);
85483
85634
  }
85484
85635
  }
85485
85636
 
85486
- function useHighlightsOnHover(ref, highlightProvider) {
85487
- const hoverState = useHoveredElement(ref);
85488
- useHighlights({
85489
- get highlights() {
85490
- return hoverState.hovered ? highlightProvider.highlights : [];
85491
- },
85492
- });
85493
- }
85494
- function useHighlights(highlightProvider) {
85495
- const stores = useStoreProvider();
85496
- const store = useLocalStore(HighlightStore);
85497
- owl.onMounted(() => {
85498
- store.register(highlightProvider);
85499
- });
85500
- let currentHighlights = highlightProvider.highlights;
85501
- owl.useEffect((highlights) => {
85502
- if (!deepEquals(highlights, currentHighlights)) {
85503
- currentHighlights = highlights;
85504
- stores.trigger("store-updated");
85505
- }
85506
- }, () => [highlightProvider.highlights]);
85507
- }
85508
-
85509
- class ConditionalFormatPreview extends owl.Component {
85510
- static template = "o-spreadsheet-ConditionalFormatPreview";
85511
- icons = ICONS;
85512
- ref = owl.useRef("cfPreview");
85513
- setup() {
85514
- useHighlightsOnHover(this.ref, this);
85515
- }
85516
- getPreviewImageStyle() {
85517
- const rule = this.props.conditionalFormat.rule;
85518
- if (rule.type === "CellIsRule") {
85519
- return cssPropertiesToCss(cellStyleToCss(rule.style));
85520
- }
85521
- else if (rule.type === "ColorScaleRule") {
85522
- const minColor = colorNumberToHex(rule.minimum.color);
85523
- const midColor = rule.midpoint ? colorNumberToHex(rule.midpoint.color) : null;
85524
- const maxColor = colorNumberToHex(rule.maximum.color);
85525
- const baseString = "background-image: linear-gradient(to right, ";
85526
- return midColor
85527
- ? baseString + minColor + ", " + midColor + ", " + maxColor + ")"
85528
- : baseString + minColor + ", " + maxColor + ")";
85529
- }
85530
- else if (rule.type === "DataBarRule") {
85531
- const barColor = colorNumberToHex(rule.color);
85532
- const gradient = `background-image: linear-gradient(to right, ${barColor} 50%, white 50%)`;
85533
- return `${gradient}; color: ${TEXT_BODY};`;
85534
- }
85535
- return "";
85536
- }
85537
- getDescription() {
85538
- const cf = this.props.conditionalFormat;
85539
- switch (cf.rule.type) {
85540
- case "CellIsRule":
85541
- return criterionEvaluatorRegistry
85542
- .get(cf.rule.operator)
85543
- .getPreview({ ...cf.rule, type: cf.rule.operator }, this.env.model.getters);
85544
- case "ColorScaleRule":
85545
- return CfTerms.ColorScale;
85546
- case "IconSetRule":
85547
- return CfTerms.IconSet;
85548
- case "DataBarRule":
85549
- return CfTerms.DataBar;
85550
- }
85551
- }
85552
- deleteConditionalFormat() {
85553
- this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
85554
- id: this.props.conditionalFormat.id,
85555
- sheetId: this.env.model.getters.getActiveSheetId(),
85556
- });
85557
- }
85558
- onMouseDown(event) {
85559
- this.props.onMouseDown(event);
85560
- }
85561
- get highlights() {
85562
- const sheetId = this.env.model.getters.getActiveSheetId();
85563
- return this.props.conditionalFormat.ranges.map((range) => ({
85564
- range: this.env.model.getters.getRangeFromSheetXC(sheetId, range),
85565
- color: HIGHLIGHT_COLOR,
85566
- fillAlpha: 0.06,
85567
- }));
85568
- }
85569
- }
85570
- ConditionalFormatPreview.props = {
85571
- conditionalFormat: Object,
85572
- onPreviewClick: Function,
85573
- onMouseDown: Function,
85574
- class: String,
85575
- };
85576
-
85577
- class ConditionalFormatPreviewList extends owl.Component {
85578
- static template = "o-spreadsheet-ConditionalFormatPreviewList";
85579
- static props = {
85580
- conditionalFormats: Array,
85581
- onPreviewClick: Function,
85582
- onAddConditionalFormat: Function,
85583
- };
85584
- static components = { ConditionalFormatPreview };
85585
- icons = ICONS;
85586
- dragAndDrop = useDragAndDropListItems();
85587
- cfListRef = owl.useRef("cfList");
85588
- setup() {
85589
- owl.onWillUpdateProps((nextProps) => {
85590
- if (!deepEquals(this.props.conditionalFormats, nextProps.conditionalFormats)) {
85591
- this.dragAndDrop.cancel();
85592
- }
85593
- });
85594
- }
85595
- getPreviewDivStyle(cf) {
85596
- return this.dragAndDrop.itemsStyle[cf.id] || "";
85597
- }
85598
- onPreviewMouseDown(cf, event) {
85599
- if (event.button !== 0)
85600
- return;
85601
- const previewRects = Array.from(this.cfListRef.el.children).map((previewEl) => getBoundingRectAsPOJO(previewEl));
85602
- const items = this.props.conditionalFormats.map((cf, index) => ({
85603
- id: cf.id,
85604
- size: previewRects[index].height,
85605
- position: previewRects[index].y,
85606
- }));
85607
- this.dragAndDrop.start("vertical", {
85608
- draggedItemId: cf.id,
85609
- initialMousePosition: event.clientY,
85610
- items: items,
85611
- scrollableContainerEl: this.cfListRef.el,
85612
- onDragEnd: (cfId, finalIndex) => this.onDragEnd(cfId, finalIndex),
85613
- });
85614
- }
85615
- onDragEnd(cfId, finalIndex) {
85616
- const originalIndex = this.props.conditionalFormats.findIndex((sheet) => sheet.id === cfId);
85617
- const delta = originalIndex - finalIndex;
85618
- if (delta !== 0) {
85619
- this.env.model.dispatch("CHANGE_CONDITIONAL_FORMAT_PRIORITY", {
85620
- cfId,
85621
- delta,
85622
- sheetId: this.env.model.getters.getActiveSheetId(),
85623
- });
85624
- }
85625
- }
85626
- }
85627
-
85628
- class ConditionalFormattingEditor extends owl.Component {
85629
- static template = "o-spreadsheet-ConditionalFormattingEditor";
85630
- static props = {
85631
- editedCf: Object,
85632
- onCancel: Function,
85633
- onExit: Function,
85634
- isNewCf: Boolean,
85635
- };
85636
- static components = {
85637
- SelectionInput,
85638
- IconPicker,
85639
- ColorPickerWidget,
85640
- ConditionalFormatPreviewList,
85641
- Section,
85642
- RoundColorPicker,
85643
- StandaloneComposer,
85644
- BadgeSelection,
85645
- ValidationMessages,
85646
- SelectMenu,
85647
- };
85637
+ class ConditionalFormattingEditorStore extends SpreadsheetStore {
85638
+ mutators = ["updateConditionalFormat", "closeMenus"];
85648
85639
  icons = ICONS;
85649
85640
  iconSets = ICON_SETS;
85650
- getTextDecoration = getTextDecoration;
85651
- colorNumberToHex = colorNumberToHex;
85652
85641
  state;
85653
- setup() {
85642
+ cfId;
85643
+ constructor(get, cf, isNewCf) {
85644
+ super(get);
85645
+ this.cfId = cf.id;
85654
85646
  this.state = owl.useState({
85655
85647
  errors: [],
85656
- currentCFType: this.props.editedCf.rule.type,
85657
- ranges: this.props.editedCf.ranges,
85648
+ currentCFType: cf.rule.type,
85649
+ ranges: cf.ranges,
85658
85650
  rules: this.getDefaultRules(),
85659
- hasEditedCf: this.props.isNewCf,
85651
+ hasEditedCf: isNewCf,
85660
85652
  });
85661
- switch (this.props.editedCf.rule.type) {
85653
+ switch (cf.rule.type) {
85662
85654
  case "CellIsRule":
85663
- this.state.rules.cellIs = this.props.editedCf.rule;
85655
+ this.state.rules.cellIs = cf.rule;
85664
85656
  break;
85665
85657
  case "ColorScaleRule":
85666
- this.state.rules.colorScale = this.props.editedCf.rule;
85658
+ this.state.rules.colorScale = cf.rule;
85667
85659
  break;
85668
85660
  case "IconSetRule":
85669
- this.state.rules.iconSet = this.props.editedCf.rule;
85661
+ this.state.rules.iconSet = cf.rule;
85670
85662
  break;
85671
85663
  case "DataBarRule":
85672
- this.state.rules.dataBar = this.props.editedCf.rule;
85664
+ this.state.rules.dataBar = cf.rule;
85673
85665
  break;
85674
85666
  }
85675
- owl.useExternalListener(window, "click", this.closeMenus);
85676
- }
85677
- get isRangeValid() {
85678
- return this.state.errors.includes("EmptyRange" /* CommandResult.EmptyRange */);
85679
- }
85680
- get errorMessages() {
85681
- return this.state.errors.map((error) => CfTerms.Errors[error] || CfTerms.Errors.Unexpected);
85682
- }
85683
- get cfTypesValues() {
85684
- return [
85685
- { value: "CellIsRule", label: _t("Single color") },
85686
- { value: "ColorScaleRule", label: _t("Color scale") },
85687
- { value: "IconSetRule", label: _t("Icon set") },
85688
- { value: "DataBarRule", label: _t("Data bar") },
85689
- ];
85690
85667
  }
85691
85668
  updateConditionalFormat(newCf) {
85692
85669
  const ranges = newCf.ranges || this.state.ranges;
@@ -85695,17 +85672,17 @@ stores.inject(MyMetaStore, storeInstance);
85695
85672
  if (!newCf.suppressErrors) {
85696
85673
  this.state.errors = ["InvalidRange" /* CommandResult.InvalidRange */];
85697
85674
  }
85698
- return ["InvalidRange" /* CommandResult.InvalidRange */];
85675
+ return;
85699
85676
  }
85700
- const sheetId = this.env.model.getters.getActiveSheetId();
85701
- const locale = this.env.model.getters.getLocale();
85677
+ const sheetId = this.model.getters.getActiveSheetId();
85678
+ const locale = this.model.getters.getLocale();
85702
85679
  const rule = newCf.rule || this.getEditedRule(this.state.currentCFType);
85703
- const result = this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
85680
+ const result = this.model.dispatch("ADD_CONDITIONAL_FORMAT", {
85704
85681
  cf: {
85682
+ id: this.cfId,
85705
85683
  rule: canonicalizeCFRule(rule, locale),
85706
- id: this.props.editedCf.id,
85707
85684
  },
85708
- ranges: ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
85685
+ ranges: ranges.map((xc) => this.model.getters.getRangeDataFromXc(sheetId, xc)),
85709
85686
  sheetId,
85710
85687
  });
85711
85688
  if (result.isSuccessful) {
@@ -85715,71 +85692,18 @@ stores.inject(MyMetaStore, storeInstance);
85715
85692
  if (!newCf.suppressErrors) {
85716
85693
  this.state.errors = reasons;
85717
85694
  }
85718
- return reasons;
85719
85695
  }
85720
- getEditedRule(ruleType) {
85721
- switch (ruleType) {
85722
- case "CellIsRule":
85723
- return this.state.rules.cellIs;
85724
- case "ColorScaleRule":
85725
- return this.state.rules.colorScale;
85726
- case "IconSetRule":
85727
- return this.state.rules.iconSet;
85728
- case "DataBarRule":
85729
- return this.state.rules.dataBar;
85730
- }
85696
+ get isRangeValid() {
85697
+ return this.state.errors.includes("EmptyRange" /* CommandResult.EmptyRange */);
85731
85698
  }
85732
- onSave() {
85733
- const result = this.updateConditionalFormat({});
85734
- if (result.length === 0) {
85735
- this.props.onExit();
85736
- }
85699
+ get errorMessages() {
85700
+ return this.state.errors.map((error) => CfTerms.Errors[error] || CfTerms.Errors.Unexpected);
85737
85701
  }
85738
- onCancel() {
85739
- if (this.state.hasEditedCf) {
85740
- this.props.onCancel();
85741
- }
85742
- else {
85743
- this.props.onExit();
85744
- }
85702
+ onRangeUpdate(ranges) {
85703
+ this.state.ranges = ranges;
85745
85704
  }
85746
- getDefaultRules() {
85747
- return {
85748
- cellIs: {
85749
- type: "CellIsRule",
85750
- operator: "isNotEmpty",
85751
- values: [],
85752
- style: { fillColor: "#b6d7a8" },
85753
- },
85754
- colorScale: {
85755
- type: "ColorScaleRule",
85756
- minimum: { type: "value", color: hexaToInt("EFF7FF") },
85757
- midpoint: undefined,
85758
- maximum: { type: "value", color: 0x6aa84f },
85759
- },
85760
- iconSet: {
85761
- type: "IconSetRule",
85762
- icons: {
85763
- upper: "arrowGood",
85764
- middle: "arrowNeutral",
85765
- lower: "arrowBad",
85766
- },
85767
- upperInflectionPoint: {
85768
- type: "percentage",
85769
- value: "66",
85770
- operator: "gt",
85771
- },
85772
- lowerInflectionPoint: {
85773
- type: "percentage",
85774
- value: "33",
85775
- operator: "gt",
85776
- },
85777
- },
85778
- dataBar: {
85779
- type: "DataBarRule",
85780
- color: 0xd9ead3,
85781
- },
85782
- };
85705
+ onRangeConfirmed() {
85706
+ this.updateConditionalFormat({ ranges: this.state.ranges });
85783
85707
  }
85784
85708
  changeRuleType(ruleType) {
85785
85709
  if (this.state.currentCFType === ruleType) {
@@ -85789,34 +85713,45 @@ stores.inject(MyMetaStore, storeInstance);
85789
85713
  this.state.currentCFType = ruleType;
85790
85714
  this.updateConditionalFormat({ rule: this.getEditedRule(ruleType), suppressErrors: true });
85791
85715
  }
85792
- onRangeUpdate(ranges) {
85793
- this.state.ranges = ranges;
85794
- }
85795
- onRangeConfirmed() {
85796
- this.updateConditionalFormat({ ranges: this.state.ranges });
85797
- }
85798
- /*****************************************************************************
85799
- * Common
85800
- ****************************************************************************/
85801
- toggleMenu(menu) {
85802
- const isSelected = this.state.openedMenu === menu;
85803
- this.closeMenus();
85804
- if (!isSelected) {
85805
- 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;
85806
85726
  }
85807
85727
  }
85808
- closeMenus() {
85809
- this.state.openedMenu = undefined;
85810
- }
85811
85728
  /*****************************************************************************
85812
85729
  * Cell Is Rule
85813
85730
  ****************************************************************************/
85814
- get isValue1Invalid() {
85815
- return (this.state.errors.includes("FirstArgMissing" /* CommandResult.FirstArgMissing */) ||
85816
- this.state.errors.includes("ValueCellIsInvalidFormula" /* CommandResult.ValueCellIsInvalidFormula */));
85731
+ get cfCriterionMenuItems() {
85732
+ return getCriterionMenuItems((type) => this.editOperator(type), availableConditionalFormatOperators);
85817
85733
  }
85818
- get isValue2Invalid() {
85819
- return this.state.errors.includes("SecondArgMissing" /* CommandResult.SecondArgMissing */);
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;
85739
+ }
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 });
85820
85755
  }
85821
85756
  toggleStyle(tool) {
85822
85757
  const style = this.state.rules.cellIs.style;
@@ -85824,18 +85759,6 @@ stores.inject(MyMetaStore, storeInstance);
85824
85759
  this.updateConditionalFormat({ rule: this.state.rules.cellIs });
85825
85760
  this.closeMenus();
85826
85761
  }
85827
- onKeydown(event) {
85828
- if (event.key === "F4") {
85829
- const target = event.target;
85830
- const update = cycleFixedReference({ start: target.selectionStart ?? 0, end: target.selectionEnd ?? 0 }, target.value, this.env.model.getters.getLocale());
85831
- if (!update) {
85832
- return;
85833
- }
85834
- target.value = update.content;
85835
- target.setSelectionRange(update.selection.start, update.selection.end);
85836
- target.dispatchEvent(new Event("input"));
85837
- }
85838
- }
85839
85762
  setColor(target, color) {
85840
85763
  this.state.rules.cellIs.style[target] = color;
85841
85764
  this.updateConditionalFormat({ rule: this.state.rules.cellIs });
@@ -85849,62 +85772,10 @@ stores.inject(MyMetaStore, storeInstance);
85849
85772
  this.updateConditionalFormat({ rule: this.state.rules.cellIs, suppressErrors: true });
85850
85773
  this.closeMenus();
85851
85774
  }
85852
- get cfCriterionMenuItems() {
85853
- return getCriterionMenuItems((type) => this.editOperator(type), availableConditionalFormatOperators);
85854
- }
85855
- get selectedCriterionName() {
85856
- return criterionEvaluatorRegistry.get(this.state.rules.cellIs.operator).name;
85857
- }
85858
- get criterionComponent() {
85859
- return criterionComponentRegistry.get(this.state.rules.cellIs.operator).component;
85860
- }
85861
- get genericCriterion() {
85862
- return {
85863
- type: this.state.rules.cellIs.operator,
85864
- values: this.state.rules.cellIs.values,
85865
- dateValue: this.state.rules.cellIs.dateValue,
85866
- };
85867
- }
85868
- onRuleValuesChanged(rule) {
85869
- this.state.rules.cellIs.values = rule.values;
85870
- this.state.rules.cellIs.dateValue = rule.dateValue;
85871
- this.updateConditionalFormat({
85872
- rule: { ...this.state.rules.cellIs, values: rule.values, dateValue: rule.dateValue },
85873
- });
85874
- }
85875
85775
  /*****************************************************************************
85876
85776
  * Color Scale Rule
85877
85777
  ****************************************************************************/
85878
- isValueInvalid(threshold) {
85879
- switch (threshold) {
85880
- case "minimum":
85881
- return (this.state.errors.includes("MinInvalidFormula" /* CommandResult.MinInvalidFormula */) ||
85882
- this.state.errors.includes("MinBiggerThanMid" /* CommandResult.MinBiggerThanMid */) ||
85883
- this.state.errors.includes("MinBiggerThanMax" /* CommandResult.MinBiggerThanMax */) ||
85884
- this.state.errors.includes("MinNaN" /* CommandResult.MinNaN */));
85885
- case "midpoint":
85886
- return (this.state.errors.includes("MidInvalidFormula" /* CommandResult.MidInvalidFormula */) ||
85887
- this.state.errors.includes("MidNaN" /* CommandResult.MidNaN */) ||
85888
- this.state.errors.includes("MidBiggerThanMax" /* CommandResult.MidBiggerThanMax */));
85889
- case "maximum":
85890
- return (this.state.errors.includes("MaxInvalidFormula" /* CommandResult.MaxInvalidFormula */) ||
85891
- this.state.errors.includes("MaxNaN" /* CommandResult.MaxNaN */));
85892
- default:
85893
- return false;
85894
- }
85895
- }
85896
- setColorScaleColor(target, color) {
85897
- if (!isColorValid(color)) {
85898
- return;
85899
- }
85900
- const point = this.state.rules.colorScale[target];
85901
- if (point) {
85902
- point.color = colorToNumber(color);
85903
- }
85904
- this.updateConditionalFormat({ rule: this.state.rules.colorScale });
85905
- this.closeMenus();
85906
- }
85907
- getColorScalePreviewStyle() {
85778
+ get previewGradient() {
85908
85779
  const rule = this.state.rules.colorScale;
85909
85780
  const minColor = colorNumberToHex(rule.minimum.color);
85910
85781
  const midColor = colorNumberToHex(rule.midpoint?.color || DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);
@@ -85918,13 +85789,7 @@ stores.inject(MyMetaStore, storeInstance);
85918
85789
  color: "#000",
85919
85790
  });
85920
85791
  }
85921
- getThresholdColor(threshold) {
85922
- return threshold
85923
- ? colorNumberToHex(threshold.color)
85924
- : colorNumberToHex(DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);
85925
- }
85926
- onMidpointChange(ev) {
85927
- const type = ev.target.value;
85792
+ onMidpointChange(type) {
85928
85793
  const rule = this.state.rules.colorScale;
85929
85794
  if (type === "none") {
85930
85795
  rule.midpoint = undefined;
@@ -85947,23 +85812,20 @@ stores.inject(MyMetaStore, storeInstance);
85947
85812
  this.state.rules.colorScale[threshold].value = value;
85948
85813
  this.updateConditionalFormat({ rule: this.state.rules.colorScale });
85949
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
+ }
85950
85826
  /*****************************************************************************
85951
85827
  * Icon Set
85952
85828
  ****************************************************************************/
85953
- isInflectionPointInvalid(inflectionPoint) {
85954
- switch (inflectionPoint) {
85955
- case "lowerInflectionPoint":
85956
- return (this.state.errors.includes("ValueLowerInflectionNaN" /* CommandResult.ValueLowerInflectionNaN */) ||
85957
- this.state.errors.includes("ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */) ||
85958
- this.state.errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
85959
- case "upperInflectionPoint":
85960
- return (this.state.errors.includes("ValueUpperInflectionNaN" /* CommandResult.ValueUpperInflectionNaN */) ||
85961
- this.state.errors.includes("ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */) ||
85962
- this.state.errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
85963
- default:
85964
- return true;
85965
- }
85966
- }
85967
85829
  reverseIcons() {
85968
85830
  const icons = this.state.rules.iconSet.icons;
85969
85831
  const upper = icons.upper;
@@ -85990,12 +85852,176 @@ stores.inject(MyMetaStore, storeInstance);
85990
85852
  this.state.rules.iconSet[inflectionPoint].value = value;
85991
85853
  this.updateConditionalFormat({ rule: this.state.rules.iconSet });
85992
85854
  }
85993
- setInflectionType(inflectionPoint, type, ev) {
85855
+ setInflectionType(inflectionPoint, type) {
85994
85856
  this.state.rules.iconSet[inflectionPoint].type = type;
85995
85857
  this.updateConditionalFormat({ rule: this.state.rules.iconSet, suppressErrors: true });
85996
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
+ }
85997
86023
  getColorScaleComposerProps(thresholdType) {
85998
- const threshold = this.state.rules.colorScale[thresholdType];
86024
+ const threshold = this.store.state.rules.colorScale[thresholdType];
85999
86025
  if (!threshold) {
86000
86026
  throw new Error("Threshold not found");
86001
86027
  }
@@ -86003,103 +86029,153 @@ stores.inject(MyMetaStore, storeInstance);
86003
86029
  return {
86004
86030
  onConfirm: (str) => {
86005
86031
  threshold.value = str;
86006
- this.updateConditionalFormat({ rule: this.state.rules.colorScale });
86032
+ this.store.updateConditionalFormat({ rule: this.store.state.rules.colorScale });
86007
86033
  },
86008
86034
  composerContent: threshold.value || "",
86009
86035
  placeholder: _t("Formula"),
86010
86036
  defaultStatic: true,
86011
86037
  invalid: isInvalid,
86012
86038
  class: "o-sidePanel-composer",
86013
- defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
86039
+ defaultRangeSheetId: this.activeSheetId,
86014
86040
  };
86015
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
+ }
86016
86060
  getColorIconSetComposerProps(inflectionPoint) {
86017
- const inflection = this.state.rules.iconSet[inflectionPoint];
86061
+ const inflection = this.store.state.rules.iconSet[inflectionPoint];
86018
86062
  const isInvalid = this.isInflectionPointInvalid(inflectionPoint);
86019
86063
  return {
86020
86064
  onConfirm: (str) => {
86021
86065
  inflection.value = str;
86022
- this.updateConditionalFormat({ rule: this.state.rules.iconSet });
86066
+ this.store.updateConditionalFormat({ rule: this.store.state.rules.iconSet });
86023
86067
  },
86024
86068
  composerContent: inflection.value || "",
86025
86069
  placeholder: _t("Formula"),
86026
86070
  defaultStatic: true,
86027
86071
  invalid: isInvalid,
86028
86072
  class: "o-sidePanel-composer",
86029
- defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
86073
+ defaultRangeSheetId: this.activeSheetId,
86030
86074
  };
86031
86075
  }
86032
- /*****************************************************************************
86033
- * DataBar
86034
- ****************************************************************************/
86035
- getRangeValues() {
86036
- 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);
86037
86112
  }
86038
- updateDataBarColor(color) {
86039
- if (!isColorValid(color)) {
86040
- return;
86113
+ get previewImageStyle() {
86114
+ const rule = this.props.conditionalFormat.rule;
86115
+ if (rule.type === "CellIsRule") {
86116
+ return cssPropertiesToCss(cellStyleToCss(rule.style));
86041
86117
  }
86042
- this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
86043
- 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 "";
86044
86133
  }
86045
- onDataBarRangeUpdate(ranges) {
86046
- 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
+ }
86047
86148
  }
86048
- onDataBarRangeChange() {
86049
- 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
+ });
86050
86168
  }
86051
86169
  }
86052
86170
 
86053
- class ConditionalFormattingPanel extends owl.Component {
86054
- static template = "o-spreadsheet-ConditionalFormattingPanel";
86171
+ class ConditionalFormatPreviewList extends owl.Component {
86172
+ static template = "o-spreadsheet-ConditionalFormatPreviewList";
86055
86173
  static props = {
86056
- selection: { type: Object, optional: true },
86057
86174
  onCloseSidePanel: Function,
86058
86175
  };
86059
- static components = {
86060
- ConditionalFormatPreviewList,
86061
- ConditionalFormattingEditor,
86062
- Section,
86063
- };
86064
- activeSheetId;
86065
- originalEditedCf = undefined;
86066
- state = owl.useState({
86067
- mode: "list",
86068
- });
86069
- setup() {
86070
- this.activeSheetId = this.env.model.getters.getActiveSheetId();
86071
- const sheetId = this.env.model.getters.getActiveSheetId();
86072
- const rules = this.env.model.getters.getRulesSelection(sheetId, this.props.selection || []);
86073
- if (rules.length === 1) {
86074
- const cf = this.conditionalFormats.find((c) => c.id === rules[0]);
86075
- if (cf) {
86076
- this.editConditionalFormat(cf);
86077
- }
86078
- }
86079
- owl.onWillUpdateProps((nextProps) => {
86080
- const newActiveSheetId = this.env.model.getters.getActiveSheetId();
86081
- if (newActiveSheetId !== this.activeSheetId) {
86082
- this.activeSheetId = newActiveSheetId;
86083
- this.switchToList();
86084
- }
86085
- else if (nextProps.selection !== this.props.selection) {
86086
- const sheetId = this.env.model.getters.getActiveSheetId();
86087
- const rules = this.env.model.getters.getRulesSelection(sheetId, nextProps.selection || []);
86088
- if (rules.length === 1) {
86089
- const cf = this.conditionalFormats.find((c) => c.id === rules[0]);
86090
- if (cf) {
86091
- this.editConditionalFormat(cf);
86092
- }
86093
- }
86094
- else {
86095
- this.switchToList();
86096
- }
86097
- }
86098
- else if (!this.editedCF) {
86099
- this.switchToList();
86100
- }
86101
- });
86102
- }
86176
+ static components = { ConditionalFormatPreview };
86177
+ dragAndDrop = useDragAndDropListItems();
86178
+ cfListRef = owl.useRef("cfList");
86103
86179
  get conditionalFormats() {
86104
86180
  const cfs = this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
86105
86181
  return cfs.map((cf) => ({
@@ -86107,66 +86183,129 @@ stores.inject(MyMetaStore, storeInstance);
86107
86183
  rule: localizeCFRule(cf.rule, this.env.model.getters.getLocale()),
86108
86184
  }));
86109
86185
  }
86110
- switchToList() {
86111
- this.state.mode = "list";
86112
- this.state.editedCfId = undefined;
86113
- this.originalEditedCf = undefined;
86186
+ getPreviewDivStyle(cf) {
86187
+ return this.dragAndDrop.itemsStyle[cf.id] || "";
86188
+ }
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
+ });
86114
86205
  }
86115
- addConditionalFormat() {
86116
- const cfId = this.env.model.uuidGenerator.smallUuid();
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
+ };
86117
86218
  this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
86118
- sheetId: this.activeSheetId,
86119
- ranges: this.env.model.getters
86120
- .getSelectedZones()
86121
- .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", {
86122
86224
  cf: {
86123
- id: cfId,
86124
- rule: {
86125
- type: "CellIsRule",
86126
- operator: "isNotEmpty",
86127
- style: { fillColor: "#b6d7a8" },
86128
- values: [],
86129
- },
86225
+ ...cf,
86226
+ ranges: zones.map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone))),
86130
86227
  },
86228
+ isNewCf: true,
86131
86229
  });
86132
- this.state.editedCfId = cfId;
86133
- this.state.mode = "edit";
86134
- this.originalEditedCf = undefined;
86135
86230
  }
86136
- editConditionalFormat(cf) {
86137
- this.state.mode = "edit";
86138
- this.state.editedCfId = cf.id;
86139
- this.originalEditedCf = cf;
86140
- }
86141
- cancelEdition() {
86142
- if (this.originalEditedCf) {
86143
- this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
86144
- sheetId: this.activeSheetId,
86145
- ranges: this.originalEditedCf.ranges.map((range) => this.env.model.getters.getRangeDataFromXc(this.activeSheetId, range)),
86146
- cf: this.originalEditedCf,
86147
- });
86148
- }
86149
- else if (this.state.editedCfId) {
86150
- this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
86151
- sheetId: this.activeSheetId,
86152
- 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(),
86153
86239
  });
86154
86240
  }
86155
- this.switchToList();
86156
86241
  }
86157
- get editedCF() {
86158
- 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);
86159
86302
  }
86160
86303
  }
86161
86304
 
86162
86305
  class DataValidationEditor extends owl.Component {
86163
86306
  static template = "o-spreadsheet-DataValidationEditor";
86164
86307
  static components = { SelectionInput, SelectMenu, Section, ValidationMessages };
86165
- static props = {
86166
- rule: { type: Object, optional: true },
86167
- onExit: Function,
86168
- onCloseSidePanel: { type: Function, optional: true },
86169
- };
86308
+ static props = { ruleId: String, onCloseSidePanel: Function };
86170
86309
  state = owl.useState({
86171
86310
  rule: this.defaultDataValidationRule,
86172
86311
  errors: [],
@@ -86175,12 +86314,13 @@ stores.inject(MyMetaStore, storeInstance);
86175
86314
  editingSheetId;
86176
86315
  setup() {
86177
86316
  this.editingSheetId = this.env.model.getters.getActiveSheetId();
86178
- 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();
86179
86320
  this.state.rule = {
86180
- ...this.props.rule,
86181
- 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)),
86182
86323
  };
86183
- this.state.rule.criterion.type = this.props.rule.criterion.type;
86184
86324
  }
86185
86325
  }
86186
86326
  onCriterionTypeChanged(type) {
@@ -86197,16 +86337,16 @@ stores.inject(MyMetaStore, storeInstance);
86197
86337
  const isBlocking = ev.target.value;
86198
86338
  this.state.rule.isBlocking = isBlocking === "true";
86199
86339
  }
86340
+ onCancel() {
86341
+ this.env.replaceSidePanel("DataValidation", `DataValidationEditor_${this.props.ruleId}`);
86342
+ }
86200
86343
  onSave() {
86201
- if (this.state.rule) {
86202
- const result = this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
86203
- if (!result.isSuccessful) {
86204
- this.state.errors = result.reasons;
86205
- }
86206
- else {
86207
- this.props.onExit();
86208
- }
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;
86209
86348
  }
86349
+ this.env.replaceSidePanel("DataValidation", `DataValidationEditor_${this.props.ruleId}`);
86210
86350
  }
86211
86351
  get dispatchPayload() {
86212
86352
  const rule = { ...this.state.rule, ranges: undefined };
@@ -86238,7 +86378,7 @@ stores.inject(MyMetaStore, storeInstance);
86238
86378
  .getSelectedZones()
86239
86379
  .map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone)));
86240
86380
  return {
86241
- id: this.env.model.uuidGenerator.smallUuid(),
86381
+ id: this.props.ruleId,
86242
86382
  criterion: { type: "containsText", values: [""] },
86243
86383
  ranges,
86244
86384
  };
@@ -86251,75 +86391,6 @@ stores.inject(MyMetaStore, storeInstance);
86251
86391
  }
86252
86392
  }
86253
86393
 
86254
- class DataValidationPreview extends owl.Component {
86255
- static template = "o-spreadsheet-DataValidationPreview";
86256
- static props = {
86257
- onClick: Function,
86258
- rule: Object,
86259
- };
86260
- ref = owl.useRef("dvPreview");
86261
- setup() {
86262
- useHighlightsOnHover(this.ref, this);
86263
- }
86264
- deleteDataValidation() {
86265
- const sheetId = this.env.model.getters.getActiveSheetId();
86266
- this.env.model.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: this.props.rule.id });
86267
- }
86268
- get highlights() {
86269
- return this.props.rule.ranges.map((range) => ({
86270
- range,
86271
- color: HIGHLIGHT_COLOR,
86272
- fillAlpha: 0.06,
86273
- }));
86274
- }
86275
- get rangesString() {
86276
- const sheetId = this.env.model.getters.getActiveSheetId();
86277
- return this.props.rule.ranges
86278
- .map((range) => this.env.model.getters.getRangeString(range, sheetId))
86279
- .join(", ");
86280
- }
86281
- get descriptionString() {
86282
- return criterionEvaluatorRegistry
86283
- .get(this.props.rule.criterion.type)
86284
- .getPreview(this.props.rule.criterion, this.env.model.getters);
86285
- }
86286
- }
86287
-
86288
- class DataValidationPanel extends owl.Component {
86289
- static template = "o-spreadsheet-DataValidationPanel";
86290
- static props = {
86291
- onCloseSidePanel: Function,
86292
- };
86293
- static components = { DataValidationPreview, DataValidationEditor };
86294
- state = owl.useState({ mode: "list", activeRule: undefined });
86295
- onPreviewClick(id) {
86296
- const sheetId = this.env.model.getters.getActiveSheetId();
86297
- const rule = this.env.model.getters.getDataValidationRule(sheetId, id);
86298
- if (rule) {
86299
- this.state.mode = "edit";
86300
- this.state.activeRule = rule;
86301
- }
86302
- }
86303
- addDataValidationRule() {
86304
- this.state.mode = "edit";
86305
- this.state.activeRule = undefined;
86306
- }
86307
- onExitEditMode() {
86308
- this.state.mode = "list";
86309
- this.state.activeRule = undefined;
86310
- }
86311
- localizeDVRule(rule) {
86312
- if (!rule)
86313
- return rule;
86314
- const locale = this.env.model.getters.getLocale();
86315
- return localizeDataValidationRule(rule, locale);
86316
- }
86317
- get validationRules() {
86318
- const sheetId = this.env.model.getters.getActiveSheetId();
86319
- return this.env.model.getters.getDataValidationRules(sheetId);
86320
- }
86321
- }
86322
-
86323
86394
  const FIND_AND_REPLACE_HIGHLIGHT_COLOR = "#8B008B";
86324
86395
  var Direction;
86325
86396
  (function (Direction) {
@@ -89197,7 +89268,18 @@ stores.inject(MyMetaStore, storeInstance);
89197
89268
  const sidePanelRegistry = new Registry$1();
89198
89269
  sidePanelRegistry.add("ConditionalFormatting", {
89199
89270
  title: _t("Conditional formatting"),
89200
- 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
+ },
89201
89283
  });
89202
89284
  sidePanelRegistry.add("ChartPanel", {
89203
89285
  title: _t("Chart"),
@@ -89234,6 +89316,13 @@ stores.inject(MyMetaStore, storeInstance);
89234
89316
  sidePanelRegistry.add("DataValidationEditor", {
89235
89317
  title: _t("Data validation"),
89236
89318
  Body: DataValidationEditor,
89319
+ computeState: (getters, props) => {
89320
+ return {
89321
+ isOpen: true,
89322
+ props,
89323
+ key: `DataValidationEditor_${props.ruleId}`,
89324
+ };
89325
+ },
89237
89326
  });
89238
89327
  sidePanelRegistry.add("MoreFormats", {
89239
89328
  title: _t("More formats"),
@@ -97985,9 +98074,9 @@ stores.inject(MyMetaStore, storeInstance);
97985
98074
  exports.tokenize = tokenize;
97986
98075
 
97987
98076
 
97988
- __info__.version = "19.1.2";
97989
- __info__.date = "2026-01-07T16:21:36.757Z";
97990
- __info__.hash = "febc3e9";
98077
+ __info__.version = "19.2.0-alpha.2";
98078
+ __info__.date = "2026-01-07T16:21:35.251Z";
98079
+ __info__.hash = "ac2fa3e";
97991
98080
 
97992
98081
 
97993
98082
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);