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