@odoo/o-spreadsheet 18.1.0-alpha.4 → 18.1.0-alpha.6

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 18.1.0-alpha.4
6
- * @date 2024-11-13T15:06:47.769Z
7
- * @hash e1ad985
5
+ * @version 18.1.0-alpha.6
6
+ * @date 2024-11-28T09:06:59.527Z
7
+ * @hash 875c901
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -129,7 +129,6 @@
129
129
  const HEADER_BORDER_COLOR = "#C0C0C0";
130
130
  const CELL_BORDER_COLOR = "#E2E3E3";
131
131
  const BACKGROUND_CHART_COLOR = "#FFFFFF";
132
- const BORDER_CHART_COLOR = "#FFFFFF";
133
132
  const DISABLED_TEXT_COLOR = "#CACACA";
134
133
  const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8;
135
134
  const LINK_COLOR = "#017E84";
@@ -3235,7 +3234,6 @@
3235
3234
  (function (ClipboardMIMEType) {
3236
3235
  ClipboardMIMEType["PlainText"] = "text/plain";
3237
3236
  ClipboardMIMEType["Html"] = "text/html";
3238
- ClipboardMIMEType["OSpreadsheet"] = "web application/o-spreadsheet";
3239
3237
  })(ClipboardMIMEType || (ClipboardMIMEType = {}));
3240
3238
 
3241
3239
  function isSheetDependent(cmd) {
@@ -3499,7 +3497,9 @@
3499
3497
  CommandResult["MaxInvalidFormula"] = "MaxInvalidFormula";
3500
3498
  CommandResult["ValueUpperInvalidFormula"] = "ValueUpperInvalidFormula";
3501
3499
  CommandResult["ValueLowerInvalidFormula"] = "ValueLowerInvalidFormula";
3500
+ CommandResult["InvalidSortAnchor"] = "InvalidSortAnchor";
3502
3501
  CommandResult["InvalidSortZone"] = "InvalidSortZone";
3502
+ CommandResult["SortZoneWithArrayFormulas"] = "SortZoneWithArrayFormulas";
3503
3503
  CommandResult["WaitingSessionConfirmation"] = "WaitingSessionConfirmation";
3504
3504
  CommandResult["MergeOverlap"] = "MergeOverlap";
3505
3505
  CommandResult["TooManyHiddenElements"] = "TooManyHiddenElements";
@@ -3555,7 +3555,6 @@
3555
3555
  CommandResult["ValueCellIsInvalidFormula"] = "ValueCellIsInvalidFormula";
3556
3556
  CommandResult["InvalidDefinition"] = "InvalidDefinition";
3557
3557
  CommandResult["InvalidColor"] = "InvalidColor";
3558
- CommandResult["DataBarRangeValuesMismatch"] = "DataBarRangeValuesMismatch";
3559
3558
  })(exports.CommandResult || (exports.CommandResult = {}));
3560
3559
 
3561
3560
  const DEFAULT_LOCALES = [
@@ -4345,7 +4344,9 @@
4345
4344
  return reverseSearch ? numberOfValues - i - 1 : i;
4346
4345
  }
4347
4346
  }
4348
- return reverseSearch ? numberOfValues - closestMatchIndex - 1 : closestMatchIndex;
4347
+ return reverseSearch && closestMatchIndex !== -1
4348
+ ? numberOfValues - closestMatchIndex - 1
4349
+ : closestMatchIndex;
4349
4350
  }
4350
4351
  /**
4351
4352
  * Normalize a value.
@@ -4399,47 +4400,83 @@
4399
4400
  return true;
4400
4401
  }
4401
4402
 
4402
- function toCriterionDateNumber(dateValue) {
4403
- const today = DateTime.now();
4404
- switch (dateValue) {
4405
- case "today":
4406
- return jsDateToNumber(today);
4407
- case "yesterday":
4408
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 1)));
4409
- case "tomorrow":
4410
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() + 1)));
4411
- case "lastWeek":
4412
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 7)));
4413
- case "lastMonth":
4414
- return jsDateToNumber(DateTime.fromTimestamp(today.setMonth(today.getMonth() - 1)));
4415
- case "lastYear":
4416
- return jsDateToNumber(DateTime.fromTimestamp(today.setFullYear(today.getFullYear() - 1)));
4417
- }
4403
+ /**
4404
+ * Add the `https` prefix to the url if it's missing
4405
+ */
4406
+ function withHttps(url) {
4407
+ return !/^https?:\/\//i.test(url) ? `https://${url}` : url;
4418
4408
  }
4419
- /** Get all the dates values of a criterion converted to numbers, converting date values such as "today" to actual dates */
4420
- function getDateNumberCriterionValues(criterion, locale) {
4421
- if ("dateValue" in criterion && criterion.dateValue !== "exactDate") {
4422
- return [toCriterionDateNumber(criterion.dateValue)];
4423
- }
4424
- return criterion.values.map((value) => valueToDateNumber(value, locale));
4409
+ const urlRegistry = new Registry();
4410
+ function createWebLink(url, label) {
4411
+ url = withHttps(url);
4412
+ return {
4413
+ url,
4414
+ label: label || url,
4415
+ isExternal: true,
4416
+ isUrlEditable: true,
4417
+ };
4425
4418
  }
4426
- /** Convert the criterion values to numbers. Return undefined values if they cannot be converted to numbers. */
4427
- function getCriterionValuesAsNumber(criterion, locale) {
4428
- return criterion.values.map((value) => tryToNumber(value, locale));
4419
+ urlRegistry.add("sheet_URL", {
4420
+ match: (url) => isSheetUrl(url),
4421
+ createLink: (url, label) => {
4422
+ return {
4423
+ label,
4424
+ url,
4425
+ isExternal: false,
4426
+ isUrlEditable: false,
4427
+ };
4428
+ },
4429
+ urlRepresentation(url, getters) {
4430
+ const sheetId = parseSheetUrl(url);
4431
+ return getters.tryGetSheetName(sheetId) || _t("Invalid sheet");
4432
+ },
4433
+ open(url, env) {
4434
+ const sheetId = parseSheetUrl(url);
4435
+ const result = env.model.dispatch("ACTIVATE_SHEET", {
4436
+ sheetIdFrom: env.model.getters.getActiveSheetId(),
4437
+ sheetIdTo: sheetId,
4438
+ });
4439
+ if (result.isCancelledBecause("SheetIsHidden" /* CommandResult.SheetIsHidden */)) {
4440
+ env.notifyUser({
4441
+ type: "warning",
4442
+ sticky: false,
4443
+ text: _t("Cannot open the link because the linked sheet is hidden."),
4444
+ });
4445
+ }
4446
+ },
4447
+ sequence: 0,
4448
+ });
4449
+ const WebUrlSpec = {
4450
+ createLink: createWebLink,
4451
+ match: (url) => isWebLink(url),
4452
+ open: (url) => window.open(url, "_blank"),
4453
+ urlRepresentation: (url) => url,
4454
+ sequence: 0,
4455
+ };
4456
+ function findMatchingSpec(url) {
4457
+ return (urlRegistry
4458
+ .getAll()
4459
+ .sort((a, b) => a.sequence - b.sequence)
4460
+ .find((urlType) => urlType.match(url)) || WebUrlSpec);
4429
4461
  }
4430
-
4431
- const MAX_DELAY = 140;
4432
- const MIN_DELAY = 20;
4433
- const ACCELERATION = 0.035;
4434
- /**
4435
- * Decreasing exponential function used to determine the "speed" of edge-scrolling
4436
- * as the timeout delay.
4437
- *
4438
- * Returns a timeout delay in milliseconds.
4439
- */
4440
- function scrollDelay(value) {
4441
- // decreasing exponential from MAX_DELAY to MIN_DELAY
4442
- return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1));
4462
+ function urlRepresentation(link, getters) {
4463
+ return findMatchingSpec(link.url).urlRepresentation(link.url, getters);
4464
+ }
4465
+ function openLink(link, env) {
4466
+ findMatchingSpec(link.url).open(link.url, env);
4467
+ }
4468
+ function detectLink(value) {
4469
+ if (typeof value !== "string") {
4470
+ return undefined;
4471
+ }
4472
+ if (isMarkdownLink(value)) {
4473
+ const { label, url } = parseMarkdownLink(value);
4474
+ return findMatchingSpec(url).createLink(url, label);
4475
+ }
4476
+ else if (isWebLink(value)) {
4477
+ return createWebLink(value);
4478
+ }
4479
+ return undefined;
4443
4480
  }
4444
4481
 
4445
4482
  function tokenizeFormat(str) {
@@ -5501,6 +5538,193 @@
5501
5538
  }
5502
5539
  }
5503
5540
 
5541
+ function evaluateLiteral(literalCell, localeFormat) {
5542
+ const value = isTextFormat(localeFormat.format) ? literalCell.content : literalCell.parsedValue;
5543
+ const functionResult = { value, format: localeFormat.format };
5544
+ return createEvaluatedCell(functionResult, localeFormat.locale);
5545
+ }
5546
+ function parseLiteral(content, locale) {
5547
+ if (content.startsWith("=")) {
5548
+ throw new Error(`Cannot parse "${content}" because it's not a literal value. It's a formula`);
5549
+ }
5550
+ if (content === "") {
5551
+ return null;
5552
+ }
5553
+ if (isNumber(content, DEFAULT_LOCALE)) {
5554
+ return parseNumber(content, DEFAULT_LOCALE);
5555
+ }
5556
+ const internalDate = parseDateTime(content, locale);
5557
+ if (internalDate) {
5558
+ return internalDate.value;
5559
+ }
5560
+ if (isBoolean(content)) {
5561
+ return content.toUpperCase() === "TRUE";
5562
+ }
5563
+ return content;
5564
+ }
5565
+ function createEvaluatedCell(functionResult, locale = DEFAULT_LOCALE, cell) {
5566
+ const link = detectLink(functionResult.value);
5567
+ if (!link) {
5568
+ return _createEvaluatedCell(functionResult, locale, cell);
5569
+ }
5570
+ const value = parseLiteral(link.label, locale);
5571
+ const format = functionResult.format ||
5572
+ (typeof value === "number"
5573
+ ? detectDateFormat(link.label, locale) || detectNumberFormat(link.label)
5574
+ : undefined);
5575
+ const linkPayload = {
5576
+ value,
5577
+ format,
5578
+ };
5579
+ return {
5580
+ ..._createEvaluatedCell(linkPayload, locale, cell),
5581
+ link,
5582
+ };
5583
+ }
5584
+ function _createEvaluatedCell(functionResult, locale, cell) {
5585
+ let { value, format, message } = functionResult;
5586
+ format = cell?.format || format;
5587
+ const formattedValue = formatValue(value, { format, locale });
5588
+ if (isEvaluationError(value)) {
5589
+ return errorCell(value, message);
5590
+ }
5591
+ if (isTextFormat(format)) {
5592
+ // TO DO:
5593
+ // with the next line, the value of the cell is transformed depending on the format.
5594
+ // This shouldn't happen, by doing this, the formulas handling numbers are not able
5595
+ // to interpret the value as a number.
5596
+ return textCell(toString(value), format, formattedValue);
5597
+ }
5598
+ if (value === null) {
5599
+ return emptyCell(format);
5600
+ }
5601
+ if (typeof value === "number") {
5602
+ if (isDateTimeFormat(format || "")) {
5603
+ return dateTimeCell(value, format, formattedValue);
5604
+ }
5605
+ return numberCell(value, format, formattedValue);
5606
+ }
5607
+ if (typeof value === "boolean") {
5608
+ return booleanCell(value, format, formattedValue);
5609
+ }
5610
+ return textCell(value, format, formattedValue);
5611
+ }
5612
+ function textCell(value, format, formattedValue) {
5613
+ return {
5614
+ value,
5615
+ format,
5616
+ formattedValue,
5617
+ type: CellValueType.text,
5618
+ isAutoSummable: true,
5619
+ defaultAlign: "left",
5620
+ };
5621
+ }
5622
+ function numberCell(value, format, formattedValue) {
5623
+ return {
5624
+ value: value || 0, // necessary to avoid "-0" and NaN values,
5625
+ format,
5626
+ formattedValue,
5627
+ type: CellValueType.number,
5628
+ isAutoSummable: true,
5629
+ defaultAlign: "right",
5630
+ };
5631
+ }
5632
+ const emptyCell = memoize(function emptyCell(format) {
5633
+ return {
5634
+ value: null,
5635
+ format,
5636
+ formattedValue: "",
5637
+ type: CellValueType.empty,
5638
+ isAutoSummable: true,
5639
+ defaultAlign: "left",
5640
+ };
5641
+ });
5642
+ function dateTimeCell(value, format, formattedValue) {
5643
+ return {
5644
+ value,
5645
+ format,
5646
+ formattedValue,
5647
+ type: CellValueType.number,
5648
+ isAutoSummable: false,
5649
+ defaultAlign: "right",
5650
+ };
5651
+ }
5652
+ function booleanCell(value, format, formattedValue) {
5653
+ return {
5654
+ value,
5655
+ format,
5656
+ formattedValue,
5657
+ type: CellValueType.boolean,
5658
+ isAutoSummable: false,
5659
+ defaultAlign: "center",
5660
+ };
5661
+ }
5662
+ function errorCell(value, message) {
5663
+ return {
5664
+ value,
5665
+ formattedValue: value,
5666
+ message,
5667
+ type: CellValueType.error,
5668
+ isAutoSummable: false,
5669
+ defaultAlign: "center",
5670
+ };
5671
+ }
5672
+
5673
+ function toCriterionDateNumber(dateValue) {
5674
+ const today = DateTime.now();
5675
+ switch (dateValue) {
5676
+ case "today":
5677
+ return jsDateToNumber(today);
5678
+ case "yesterday":
5679
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 1)));
5680
+ case "tomorrow":
5681
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() + 1)));
5682
+ case "lastWeek":
5683
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 7)));
5684
+ case "lastMonth":
5685
+ return jsDateToNumber(DateTime.fromTimestamp(today.setMonth(today.getMonth() - 1)));
5686
+ case "lastYear":
5687
+ return jsDateToNumber(DateTime.fromTimestamp(today.setFullYear(today.getFullYear() - 1)));
5688
+ }
5689
+ }
5690
+ /** Get all the dates values of a criterion converted to numbers, converting date values such as "today" to actual dates */
5691
+ function getDateNumberCriterionValues(criterion, locale) {
5692
+ if ("dateValue" in criterion && criterion.dateValue !== "exactDate") {
5693
+ return [toCriterionDateNumber(criterion.dateValue)];
5694
+ }
5695
+ return criterion.values.map((value) => valueToDateNumber(value, locale));
5696
+ }
5697
+ /** Convert the criterion values to numbers. Return undefined values if they cannot be converted to numbers. */
5698
+ function getCriterionValuesAsNumber(criterion, locale) {
5699
+ return criterion.values.map((value) => tryToNumber(value, locale));
5700
+ }
5701
+ function getDateCriterionFormattedValues(values, locale) {
5702
+ return values.map((valueStr) => {
5703
+ if (valueStr.startsWith("=")) {
5704
+ return valueStr;
5705
+ }
5706
+ const value = parseLiteral(valueStr, locale);
5707
+ if (typeof value === "number") {
5708
+ return formatValue(value, { format: locale.dateFormat, locale });
5709
+ }
5710
+ return "";
5711
+ });
5712
+ }
5713
+
5714
+ const MAX_DELAY = 140;
5715
+ const MIN_DELAY = 20;
5716
+ const ACCELERATION = 0.035;
5717
+ /**
5718
+ * Decreasing exponential function used to determine the "speed" of edge-scrolling
5719
+ * as the timeout delay.
5720
+ *
5721
+ * Returns a timeout delay in milliseconds.
5722
+ */
5723
+ function scrollDelay(value) {
5724
+ // decreasing exponential from MAX_DELAY to MIN_DELAY
5725
+ return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1));
5726
+ }
5727
+
5504
5728
  class RangeImpl {
5505
5729
  getSheetSize;
5506
5730
  _zone;
@@ -6148,6 +6372,22 @@
6148
6372
  const width = content[0].length, height = content.length;
6149
6373
  return target.map((t) => splitZoneForPaste(t, width, height)).flat();
6150
6374
  }
6375
+ function parseOSClipboardContent(content) {
6376
+ if (!content[ClipboardMIMEType.Html]) {
6377
+ return {
6378
+ text: content[ClipboardMIMEType.PlainText],
6379
+ };
6380
+ }
6381
+ const htmlDocument = new DOMParser().parseFromString(content[ClipboardMIMEType.Html], "text/html");
6382
+ const oSheetClipboardData = htmlDocument
6383
+ .querySelector("div")
6384
+ ?.getAttribute("data-osheet-clipboard");
6385
+ const spreadsheetContent = oSheetClipboardData && JSON.parse(oSheetClipboardData);
6386
+ return {
6387
+ text: content[ClipboardMIMEType.PlainText],
6388
+ data: spreadsheetContent,
6389
+ };
6390
+ }
6151
6391
 
6152
6392
  class ClipboardHandler {
6153
6393
  getters;
@@ -6169,7 +6409,7 @@
6169
6409
  getPasteTarget(sheetId, target, content, options) {
6170
6410
  return { zones: [], sheetId };
6171
6411
  }
6172
- convertOSClipboardData(data) {
6412
+ convertTextToClipboardData(data) {
6173
6413
  return;
6174
6414
  }
6175
6415
  }
@@ -8012,7 +8252,7 @@
8012
8252
  this.dispatch("CLEAR_CELL", target);
8013
8253
  }
8014
8254
  }
8015
- convertOSClipboardData(text) {
8255
+ convertTextToClipboardData(text) {
8016
8256
  const locale = this.getters.getLocale();
8017
8257
  const copiedData = {
8018
8258
  cells: [],
@@ -8120,6 +8360,7 @@
8120
8360
 
8121
8361
  class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
8122
8362
  uuidGenerator = new UuidGenerator();
8363
+ queuedChanges = {};
8123
8364
  copy(data) {
8124
8365
  if (!data.zones.length) {
8125
8366
  return;
@@ -8141,6 +8382,7 @@
8141
8382
  return { cfRules };
8142
8383
  }
8143
8384
  paste(target, clippedContent, options) {
8385
+ this.queuedChanges = {};
8144
8386
  if (options.pasteOption === "asValue") {
8145
8387
  return;
8146
8388
  }
@@ -8152,6 +8394,7 @@
8152
8394
  else {
8153
8395
  this.pasteFromCut(sheetId, zones, clippedContent);
8154
8396
  }
8397
+ this.executeQueuedChanges();
8155
8398
  }
8156
8399
  pasteFromCut(sheetId, target, content) {
8157
8400
  const selection = target[0];
@@ -8191,34 +8434,56 @@
8191
8434
  * Add or remove cells to a given conditional formatting rule.
8192
8435
  */
8193
8436
  adaptCFRules(sheetId, cf, toAdd, toRemove) {
8194
- const newRangesXc = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
8195
- if (!newRangesXc) {
8196
- return;
8437
+ if (!this.queuedChanges[sheetId]) {
8438
+ this.queuedChanges[sheetId] = [];
8197
8439
  }
8198
- if (newRangesXc.length === 0) {
8199
- this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
8200
- return;
8440
+ const queuedChange = this.queuedChanges[sheetId].find((queued) => queued.cf.id === cf.id);
8441
+ if (!queuedChange) {
8442
+ this.queuedChanges[sheetId].push({ toAdd, toRemove, cf });
8443
+ }
8444
+ else {
8445
+ queuedChange.toAdd.push(...toAdd);
8446
+ queuedChange.toRemove.push(...toRemove);
8447
+ }
8448
+ }
8449
+ executeQueuedChanges() {
8450
+ for (const sheetId in this.queuedChanges) {
8451
+ for (const { toAdd, toRemove, cf } of this.queuedChanges[sheetId]) {
8452
+ const newRangesXc = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
8453
+ if (!newRangesXc) {
8454
+ continue;
8455
+ }
8456
+ if (newRangesXc.length === 0) {
8457
+ this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
8458
+ continue;
8459
+ }
8460
+ this.dispatch("ADD_CONDITIONAL_FORMAT", {
8461
+ cf: {
8462
+ id: cf.id,
8463
+ rule: cf.rule,
8464
+ stopIfTrue: cf.stopIfTrue,
8465
+ },
8466
+ ranges: newRangesXc,
8467
+ sheetId,
8468
+ });
8469
+ }
8201
8470
  }
8202
- this.dispatch("ADD_CONDITIONAL_FORMAT", {
8203
- cf: {
8204
- id: cf.id,
8205
- rule: cf.rule,
8206
- stopIfTrue: cf.stopIfTrue,
8207
- },
8208
- ranges: newRangesXc,
8209
- sheetId,
8210
- });
8211
8471
  }
8212
8472
  getCFToCopyTo(targetSheetId, originCF) {
8213
- const cfInTarget = this.getters
8473
+ let targetCF = this.getters
8214
8474
  .getConditionalFormats(targetSheetId)
8215
8475
  .find((cf) => cf.stopIfTrue === originCF.stopIfTrue && deepEquals(cf.rule, originCF.rule));
8216
- return cfInTarget ? cfInTarget : { ...originCF, id: this.uuidGenerator.uuidv4(), ranges: [] };
8476
+ const queuedCfs = this.queuedChanges[targetSheetId];
8477
+ if (!targetCF && queuedCfs) {
8478
+ targetCF = queuedCfs.find((queued) => queued.cf.stopIfTrue === originCF.stopIfTrue && deepEquals(queued.cf.rule, originCF.rule))?.cf;
8479
+ }
8480
+ return targetCF || { ...originCF, id: this.uuidGenerator.uuidv4(), ranges: [] };
8217
8481
  }
8218
8482
  }
8219
8483
 
8220
8484
  class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8221
8485
  uuidGenerator = new UuidGenerator();
8486
+ queuedChanges = {};
8222
8487
  copy(data) {
8223
8488
  const { rowsIndexes, columnsIndexes } = data;
8224
8489
  const sheetId = data.sheetId;
@@ -8235,6 +8500,7 @@
8235
8500
  return { dvRules };
8236
8501
  }
8237
8502
  paste(target, clippedContent, options) {
8503
+ this.queuedChanges = {};
8238
8504
  if (options.pasteOption) {
8239
8505
  return;
8240
8506
  }
@@ -8246,6 +8512,7 @@
8246
8512
  else {
8247
8513
  this.pasteFromCut(sheetId, zones, clippedContent);
8248
8514
  }
8515
+ this.executeQueuedChanges();
8249
8516
  }
8250
8517
  pasteFromCut(sheetId, target, content) {
8251
8518
  const selection = target[0];
@@ -8292,29 +8559,55 @@
8292
8559
  }
8293
8560
  }
8294
8561
  getDataValidationRuleToCopyTo(targetSheetId, originRule, newId = true) {
8295
- const ruleInTargetSheet = this.getters
8562
+ let targetRule = this.getters
8296
8563
  .getDataValidationRules(targetSheetId)
8297
8564
  .find((rule) => deepEquals(originRule.criterion, rule.criterion) &&
8298
8565
  originRule.isBlocking === rule.isBlocking);
8299
- return ruleInTargetSheet
8300
- ? ruleInTargetSheet
8301
- : { ...originRule, id: newId ? this.uuidGenerator.uuidv4() : originRule.id, ranges: [] };
8566
+ const queuedRules = this.queuedChanges[targetSheetId];
8567
+ if (!targetRule && queuedRules) {
8568
+ targetRule = queuedRules.find((queued) => deepEquals(originRule.criterion, queued.rule.criterion) &&
8569
+ originRule.isBlocking === queued.rule.isBlocking)?.rule;
8570
+ }
8571
+ return (targetRule || {
8572
+ ...originRule,
8573
+ id: newId ? this.uuidGenerator.uuidv4() : originRule.id,
8574
+ ranges: [],
8575
+ });
8302
8576
  }
8303
8577
  /**
8304
8578
  * Add or remove XCs to a given data validation rule.
8305
8579
  */
8306
8580
  adaptDataValidationRule(sheetId, rule, toAdd, toRemove) {
8307
- const dvZones = rule.ranges.map((range) => range.zone);
8308
- const newDvZones = recomputeZones([...dvZones, ...toAdd], toRemove);
8309
- if (newDvZones.length === 0) {
8310
- this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: rule.id });
8311
- return;
8581
+ if (!this.queuedChanges[sheetId]) {
8582
+ this.queuedChanges[sheetId] = [];
8583
+ }
8584
+ const queuedChange = this.queuedChanges[sheetId].find((queued) => queued.rule.id === rule.id);
8585
+ if (!queuedChange) {
8586
+ this.queuedChanges[sheetId].push({ toAdd, toRemove, rule });
8587
+ }
8588
+ else {
8589
+ queuedChange.toAdd.push(...toAdd);
8590
+ queuedChange.toRemove.push(...toRemove);
8591
+ }
8592
+ }
8593
+ executeQueuedChanges() {
8594
+ for (const sheetId in this.queuedChanges) {
8595
+ for (const { toAdd, toRemove, rule: dv } of this.queuedChanges[sheetId]) {
8596
+ // Remove the zones first in case the same position is in toAdd and toRemove
8597
+ const dvZones = dv.ranges.map((range) => range.zone);
8598
+ const withRemovedZones = recomputeZones(dvZones, toRemove);
8599
+ const newDvZones = recomputeZones([...withRemovedZones, ...toAdd], []);
8600
+ if (newDvZones.length === 0) {
8601
+ this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: dv.id });
8602
+ continue;
8603
+ }
8604
+ this.dispatch("ADD_DATA_VALIDATION_RULE", {
8605
+ rule: dv,
8606
+ ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
8607
+ sheetId,
8608
+ });
8609
+ }
8312
8610
  }
8313
- this.dispatch("ADD_DATA_VALIDATION_RULE", {
8314
- rule,
8315
- ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
8316
- sheetId,
8317
- });
8318
8611
  }
8319
8612
  }
8320
8613
 
@@ -10285,6 +10578,10 @@ stores.inject(MyMetaStore, storeInstance);
10285
10578
  description,
10286
10579
  type: types,
10287
10580
  };
10581
+ const acceptErrors = types.includes("ANY") || types.includes("RANGE");
10582
+ if (acceptErrors) {
10583
+ result.acceptErrors = true;
10584
+ }
10288
10585
  if (isOptional) {
10289
10586
  result.optional = true;
10290
10587
  }
@@ -12347,8 +12644,8 @@ stores.inject(MyMetaStore, storeInstance);
12347
12644
  const COUNT = {
12348
12645
  description: _t("The number of numeric values in dataset."),
12349
12646
  args: [
12350
- arg("value1 (number, range<number>)", _t("The first value or range to consider when counting.")),
12351
- arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when counting.")),
12647
+ arg("value1 (number, any, range<number>)", _t("The first value or range to consider when counting.")),
12648
+ arg("value2 (number, any, range<number>, repeating)", _t("Additional values or ranges to consider when counting.")),
12352
12649
  ],
12353
12650
  compute: function (...values) {
12354
12651
  return countNumbers(values, this.locale);
@@ -12737,6 +13034,7 @@ stores.inject(MyMetaStore, storeInstance);
12737
13034
  },
12738
13035
  isExported: true,
12739
13036
  };
13037
+ // CORREL
12740
13038
  // In GSheet, CORREL is just an alias to PEARSON
12741
13039
  const CORREL = PEARSON;
12742
13040
  // -----------------------------------------------------------------------------
@@ -13347,7 +13645,7 @@ stores.inject(MyMetaStore, storeInstance);
13347
13645
  }
13348
13646
  const databaseArgs = [
13349
13647
  arg("database (range)", _t("The array or range containing the data to consider, structured in such a way that the first row contains the labels for each column's values.")),
13350
- arg("field (any)", _t("Indicates which column in database contains the values to be extracted and operated on.")),
13648
+ arg("field (number, string)", _t("Indicates which column in database contains the values to be extracted and operated on.")),
13351
13649
  arg("criteria (range)", _t("An array or range containing zero or more criteria to filter the database values by before operating.")),
13352
13650
  ];
13353
13651
  // -----------------------------------------------------------------------------
@@ -14373,7 +14671,6 @@ stores.inject(MyMetaStore, storeInstance);
14373
14671
  return cellsToSort.sort(cellsSortingCriterion(sortDirection));
14374
14672
  }
14375
14673
  function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
14376
- let result = DispatchResult.Success;
14377
14674
  //several columns => bypass the contiguity check
14378
14675
  let multiColumns = zone.right > zone.left;
14379
14676
  if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
@@ -14393,49 +14690,37 @@ stores.inject(MyMetaStore, storeInstance);
14393
14690
  }
14394
14691
  }
14395
14692
  }
14396
- const { col, row } = anchor;
14397
14693
  if (multiColumns) {
14398
- result = env.model.dispatch("SORT_CELLS", { sheetId, col, row, zone, sortDirection });
14694
+ interactiveSort(env, sheetId, anchor, zone, sortDirection);
14695
+ return;
14696
+ }
14697
+ const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
14698
+ if (isEqual(contiguousZone, zone)) {
14699
+ interactiveSort(env, sheetId, anchor, zone, sortDirection);
14399
14700
  }
14400
14701
  else {
14401
- // check contiguity
14402
- const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
14403
- if (isEqual(contiguousZone, zone)) {
14404
- // merge as it is
14405
- result = env.model.dispatch("SORT_CELLS", {
14406
- sheetId,
14407
- col,
14408
- row,
14409
- zone,
14410
- sortDirection,
14411
- });
14412
- }
14413
- else {
14414
- env.askConfirmation(_t("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => {
14415
- zone = contiguousZone;
14416
- result = env.model.dispatch("SORT_CELLS", {
14417
- sheetId,
14418
- col,
14419
- row,
14420
- zone,
14421
- sortDirection,
14422
- });
14423
- }, () => {
14424
- result = env.model.dispatch("SORT_CELLS", {
14425
- sheetId,
14426
- col,
14427
- row,
14428
- zone,
14429
- sortDirection,
14430
- });
14431
- });
14432
- }
14702
+ env.askConfirmation(_t("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => interactiveSort(env, sheetId, anchor, contiguousZone, sortDirection), () => interactiveSort(env, sheetId, anchor, zone, sortDirection));
14433
14703
  }
14704
+ }
14705
+ function interactiveSort(env, sheetId, anchor, zone, sortDirection, sortOptions) {
14706
+ const result = env.model.dispatch("SORT_CELLS", {
14707
+ sheetId,
14708
+ col: anchor.col,
14709
+ row: anchor.row,
14710
+ zone,
14711
+ sortDirection,
14712
+ sortOptions,
14713
+ });
14434
14714
  if (result.isCancelledBecause("InvalidSortZone" /* CommandResult.InvalidSortZone */)) {
14435
14715
  const { col, row } = anchor;
14436
14716
  env.model.selection.selectZone({ cell: { col, row }, zone });
14437
14717
  env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
14438
14718
  }
14719
+ if (result.isCancelledBecause("SortZoneWithArrayFormulas" /* CommandResult.SortZoneWithArrayFormulas */)) {
14720
+ const { col, row } = anchor;
14721
+ env.model.selection.selectZone({ cell: { col, row }, zone });
14722
+ env.raiseError(_t("Cannot sort a zone with array formulas."));
14723
+ }
14439
14724
  }
14440
14725
 
14441
14726
  function sortMatrix(matrix, locale, ...criteria) {
@@ -17729,7 +18014,7 @@ stores.inject(MyMetaStore, storeInstance);
17729
18014
  args: [
17730
18015
  arg("condition1 (boolean)", _t("The first condition to be evaluated. This can be a boolean, a number, an array, or a reference to any of those.")),
17731
18016
  arg("value1 (any)", _t("The returned value if condition1 is TRUE.")),
17732
- arg("condition2 (boolean, repeating)", _t("Additional conditions to be evaluated if the previous ones are FALSE.")),
18017
+ arg("condition2 (boolean, any, repeating)", _t("Additional conditions to be evaluated if the previous ones are FALSE.")),
17733
18018
  arg("value2 (any, repeating)", _t("Additional values to be returned if their corresponding conditions are TRUE.")),
17734
18019
  ],
17735
18020
  compute: function (...values) {
@@ -17968,7 +18253,7 @@ stores.inject(MyMetaStore, storeInstance);
17968
18253
  const HLOOKUP = {
17969
18254
  description: _t("Horizontal lookup"),
17970
18255
  args: [
17971
- arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18256
+ arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
17972
18257
  arg("range (range)", _t("The range to consider for the search. The first row in the range is searched for the key specified in search_key.")),
17973
18258
  arg("index (number)", _t("The row index of the value to be returned, where the first row in range is numbered 1.")),
17974
18259
  arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the row to be searched (the first row of the specified range) is sorted, in which case the closest match for search_key will be returned.")),
@@ -17976,9 +18261,6 @@ stores.inject(MyMetaStore, storeInstance);
17976
18261
  compute: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {
17977
18262
  const _index = Math.trunc(toNumber(index?.value, this.locale));
17978
18263
  assert(() => 1 <= _index && _index <= range[0].length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
17979
- if (searchKey && isEvaluationError(searchKey.value)) {
17980
- return searchKey;
17981
- }
17982
18264
  const getValueFromRange = (range, index) => range[index][0].value;
17983
18265
  const _isSorted = toBoolean(isSorted.value);
17984
18266
  const colIndex = _isSorted
@@ -18075,7 +18357,7 @@ stores.inject(MyMetaStore, storeInstance);
18075
18357
  const LOOKUP = {
18076
18358
  description: _t("Look up a value."),
18077
18359
  args: [
18078
- arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18360
+ arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18079
18361
  arg("search_array (range)", _t("One method of using this function is to provide a single sorted row or column search_array to look through for the search_key with a second argument result_range. The other way is to combine these two arguments into one search_array where the first row or column is searched and a value is returned from the last row or column in the array. If search_key is not found, a non-exact match may be returned.")),
18080
18362
  arg("result_range (range, optional)", _t("The range from which to return a result. The value returned corresponds to the location where search_key is found in search_range. This range must be only a single row or column and should not be used if using the search_result_array method.")),
18081
18363
  ],
@@ -18115,7 +18397,7 @@ stores.inject(MyMetaStore, storeInstance);
18115
18397
  const MATCH = {
18116
18398
  description: _t("Position of item in range that matches value."),
18117
18399
  args: [
18118
- arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18400
+ arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18119
18401
  arg("range (any, range)", _t("The one-dimensional array to be searched.")),
18120
18402
  arg(`search_type (number, default=${DEFAULT_SEARCH_TYPE})`, _t("The search method. 1 (default) finds the largest value less than or equal to search_key when range is sorted in ascending order. 0 finds the exact value when range is unsorted. -1 finds the smallest value greater than or equal to search_key when range is sorted in descending order.")),
18121
18403
  ],
@@ -18190,7 +18472,7 @@ stores.inject(MyMetaStore, storeInstance);
18190
18472
  const VLOOKUP = {
18191
18473
  description: _t("Vertical lookup."),
18192
18474
  args: [
18193
- arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18475
+ arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18194
18476
  arg("range (any, range)", _t("The range to consider for the search. The first column in the range is searched for the key specified in search_key.")),
18195
18477
  arg("index (number)", _t("The column index of the value to be returned, where the first column in range is numbered 1.")),
18196
18478
  arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the column to be searched (the first column of the specified range) is sorted, in which case the closest match for search_key will be returned.")),
@@ -18198,9 +18480,6 @@ stores.inject(MyMetaStore, storeInstance);
18198
18480
  compute: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {
18199
18481
  const _index = Math.trunc(toNumber(index?.value, this.locale));
18200
18482
  assert(() => 1 <= _index && _index <= range.length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
18201
- if (searchKey && isEvaluationError(searchKey.value)) {
18202
- return searchKey;
18203
- }
18204
18483
  const getValueFromRange = (range, index) => range[0][index].value;
18205
18484
  const _isSorted = toBoolean(isSorted.value);
18206
18485
  const rowIndex = _isSorted
@@ -18226,7 +18505,7 @@ stores.inject(MyMetaStore, storeInstance);
18226
18505
  const XLOOKUP = {
18227
18506
  description: _t("Search a range for a match and return the corresponding item from a second range."),
18228
18507
  args: [
18229
- arg("search_key (any)", _t("The value to search for.")),
18508
+ arg("search_key (string,number,boolean)", _t("The value to search for.")),
18230
18509
  arg("lookup_range (any, range)", _t("The range to consider for the search. Should be a single column or a single row.")),
18231
18510
  arg("return_range (any, range)", _t("The range containing the return value. Should have the same dimensions as lookup_range.")),
18232
18511
  arg("if_not_found (any, optional)", _t("If a valid match is not found, return this value.")),
@@ -18251,9 +18530,6 @@ stores.inject(MyMetaStore, storeInstance);
18251
18530
  assert(() => lookupDirection === "col"
18252
18531
  ? returnRange[0].length === lookupRange[0].length
18253
18532
  : returnRange.length === lookupRange.length, _t("return_range should have the same dimensions as lookup_range."));
18254
- if (searchKey && isEvaluationError(searchKey.value)) {
18255
- return [[searchKey]];
18256
- }
18257
18533
  const getElement = lookupDirection === "col"
18258
18534
  ? (range, index) => range[0][index].value
18259
18535
  : (range, index) => range[index][0].value;
@@ -18278,13 +18554,14 @@ stores.inject(MyMetaStore, storeInstance);
18278
18554
  //--------------------------------------------------------------------------
18279
18555
  // Pivot functions
18280
18556
  //--------------------------------------------------------------------------
18557
+ // PIVOT.VALUE
18281
18558
  const PIVOT_VALUE = {
18282
18559
  description: _t("Get the value from a pivot."),
18283
18560
  args: [
18284
- arg("pivot_id (string)", _t("ID of the pivot.")),
18561
+ arg("pivot_id (number,string)", _t("ID of the pivot.")),
18285
18562
  arg("measure_name (string)", _t("Name of the measure.")),
18286
18563
  arg("domain_field_name (string,optional,repeating)", _t("Field name.")),
18287
- arg("domain_value (string,optional,repeating)", _t("Value.")),
18564
+ arg("domain_value (number,string,boolean,optional,repeating)", _t("Value.")),
18288
18565
  ],
18289
18566
  compute: function (formulaId, measureName, ...domainArgs) {
18290
18567
  const _pivotFormulaId = toString(formulaId);
@@ -18311,12 +18588,13 @@ stores.inject(MyMetaStore, storeInstance);
18311
18588
  return pivot.getPivotCellValueAndFormat(_measure, domain);
18312
18589
  },
18313
18590
  };
18591
+ // PIVOT.HEADER
18314
18592
  const PIVOT_HEADER = {
18315
18593
  description: _t("Get the header of a pivot."),
18316
18594
  args: [
18317
- arg("pivot_id (string)", _t("ID of the pivot.")),
18595
+ arg("pivot_id (number,string)", _t("ID of the pivot.")),
18318
18596
  arg("domain_field_name (string,optional,repeating)", _t("Field name.")),
18319
- arg("domain_value (string,optional,repeating)", _t("Value.")),
18597
+ arg("domain_value (number,string,value,optional,repeating)", _t("Value.")),
18320
18598
  ],
18321
18599
  compute: function (pivotId, ...domainArgs) {
18322
18600
  const _pivotFormulaId = toString(pivotId);
@@ -18563,8 +18841,8 @@ stores.inject(MyMetaStore, storeInstance);
18563
18841
  const EQ = {
18564
18842
  description: _t("Equal."),
18565
18843
  args: [
18566
- arg("value1 (any)", _t("The first value.")),
18567
- arg("value2 (any)", _t("The value to test against value1 for equality.")),
18844
+ arg("value1 (string, number, boolean)", _t("The first value.")),
18845
+ arg("value2 (string, number, boolean)", _t("The value to test against value1 for equality.")),
18568
18846
  ],
18569
18847
  compute: function (value1, value2) {
18570
18848
  if (isEvaluationError(value1?.value)) {
@@ -18615,8 +18893,8 @@ stores.inject(MyMetaStore, storeInstance);
18615
18893
  const GT = {
18616
18894
  description: _t("Strictly greater than."),
18617
18895
  args: [
18618
- arg("value1 (any)", _t("The value to test as being greater than value2.")),
18619
- arg("value2 (any)", _t("The second value.")),
18896
+ arg("value1 (number, string, boolean)", _t("The value to test as being greater than value2.")),
18897
+ arg("value2 (number, string, boolean)", _t("The second value.")),
18620
18898
  ],
18621
18899
  compute: function (value1, value2) {
18622
18900
  return applyRelationalOperator(value1, value2, (v1, v2) => {
@@ -18630,8 +18908,8 @@ stores.inject(MyMetaStore, storeInstance);
18630
18908
  const GTE = {
18631
18909
  description: _t("Greater than or equal to."),
18632
18910
  args: [
18633
- arg("value1 (any)", _t("The value to test as being greater than or equal to value2.")),
18634
- arg("value2 (any)", _t("The second value.")),
18911
+ arg("value1 (number, string, boolean)", _t("The value to test as being greater than or equal to value2.")),
18912
+ arg("value2 (number, string, boolean)", _t("The second value.")),
18635
18913
  ],
18636
18914
  compute: function (value1, value2) {
18637
18915
  return applyRelationalOperator(value1, value2, (v1, v2) => {
@@ -18645,8 +18923,8 @@ stores.inject(MyMetaStore, storeInstance);
18645
18923
  const LT = {
18646
18924
  description: _t("Less than."),
18647
18925
  args: [
18648
- arg("value1 (any)", _t("The value to test as being less than value2.")),
18649
- arg("value2 (any)", _t("The second value.")),
18926
+ arg("value1 (number, string, boolean)", _t("The value to test as being less than value2.")),
18927
+ arg("value2 (number, string, boolean)", _t("The second value.")),
18650
18928
  ],
18651
18929
  compute: function (value1, value2) {
18652
18930
  const result = GTE.compute.bind(this)(value1, value2);
@@ -18662,8 +18940,8 @@ stores.inject(MyMetaStore, storeInstance);
18662
18940
  const LTE = {
18663
18941
  description: _t("Less than or equal to."),
18664
18942
  args: [
18665
- arg("value1 (any)", _t("The value to test as being less than or equal to value2.")),
18666
- arg("value2 (any)", _t("The second value.")),
18943
+ arg("value1 (number, string, boolean)", _t("The value to test as being less than or equal to value2.")),
18944
+ arg("value2 (number, string, boolean)", _t("The second value.")),
18667
18945
  ],
18668
18946
  compute: function (value1, value2) {
18669
18947
  const result = GT.compute.bind(this)(value1, value2);
@@ -18711,8 +18989,8 @@ stores.inject(MyMetaStore, storeInstance);
18711
18989
  const NE = {
18712
18990
  description: _t("Not equal."),
18713
18991
  args: [
18714
- arg("value1 (any)", _t("The first value.")),
18715
- arg("value2 (any)", _t("The value to test against value1 for inequality.")),
18992
+ arg("value1 (string, number, boolean)", _t("The first value.")),
18993
+ arg("value2 (string, number, boolean)", _t("The value to test against value1 for inequality.")),
18716
18994
  ],
18717
18995
  compute: function (value1, value2) {
18718
18996
  const result = EQ.compute.bind(this)(value1, value2);
@@ -19630,6 +19908,16 @@ stores.inject(MyMetaStore, storeInstance);
19630
19908
  });
19631
19909
  }
19632
19910
  function errorHandlingCompute(...args) {
19911
+ for (let i = 0; i < args.length; i++) {
19912
+ const arg = args[i];
19913
+ const argDefinition = descr.args[descr.getArgToFocus(i + 1) - 1];
19914
+ // Early exit if the argument is an error and the function does not accept errors
19915
+ // We only check scalar arguments, not matrix arguments for performance reasons.
19916
+ // Casting helpers are responsible for handling errors in matrix arguments.
19917
+ if (!argDefinition.acceptErrors && !isMatrix(arg) && isEvaluationError(arg?.value)) {
19918
+ return arg;
19919
+ }
19920
+ }
19633
19921
  try {
19634
19922
  return computeFunctionToObject.apply(this, args);
19635
19923
  }
@@ -19638,6 +19926,9 @@ stores.inject(MyMetaStore, storeInstance);
19638
19926
  }
19639
19927
  }
19640
19928
  function computeFunctionToObject(...args) {
19929
+ if (this.debug) {
19930
+ debugger;
19931
+ }
19641
19932
  const result = descr.compute.apply(this, args);
19642
19933
  if (!isMatrix(result)) {
19643
19934
  if (typeof result === "object" && result !== null && "value" in result) {
@@ -20747,6 +21038,7 @@ stores.inject(MyMetaStore, storeInstance);
20747
21038
  }
20748
21039
  if (ast.debug) {
20749
21040
  code.append("debugger;");
21041
+ code.append(`ctx["debug"] = true;`);
20750
21042
  }
20751
21043
  switch (ast.type) {
20752
21044
  case "BOOLEAN":
@@ -21345,217 +21637,6 @@ stores.inject(MyMetaStore, storeInstance);
21345
21637
  },
21346
21638
  });
21347
21639
 
21348
- /**
21349
- * Add the `https` prefix to the url if it's missing
21350
- */
21351
- function withHttps(url) {
21352
- return !/^https?:\/\//i.test(url) ? `https://${url}` : url;
21353
- }
21354
- const urlRegistry = new Registry();
21355
- function createWebLink(url, label) {
21356
- url = withHttps(url);
21357
- return {
21358
- url,
21359
- label: label || url,
21360
- isExternal: true,
21361
- isUrlEditable: true,
21362
- };
21363
- }
21364
- urlRegistry.add("sheet_URL", {
21365
- match: (url) => isSheetUrl(url),
21366
- createLink: (url, label) => {
21367
- return {
21368
- label,
21369
- url,
21370
- isExternal: false,
21371
- isUrlEditable: false,
21372
- };
21373
- },
21374
- urlRepresentation(url, getters) {
21375
- const sheetId = parseSheetUrl(url);
21376
- return getters.tryGetSheetName(sheetId) || _t("Invalid sheet");
21377
- },
21378
- open(url, env) {
21379
- const sheetId = parseSheetUrl(url);
21380
- const result = env.model.dispatch("ACTIVATE_SHEET", {
21381
- sheetIdFrom: env.model.getters.getActiveSheetId(),
21382
- sheetIdTo: sheetId,
21383
- });
21384
- if (result.isCancelledBecause("SheetIsHidden" /* CommandResult.SheetIsHidden */)) {
21385
- env.notifyUser({
21386
- type: "warning",
21387
- sticky: false,
21388
- text: _t("Cannot open the link because the linked sheet is hidden."),
21389
- });
21390
- }
21391
- },
21392
- sequence: 0,
21393
- });
21394
- const WebUrlSpec = {
21395
- createLink: createWebLink,
21396
- match: (url) => isWebLink(url),
21397
- open: (url) => window.open(url, "_blank"),
21398
- urlRepresentation: (url) => url,
21399
- sequence: 0,
21400
- };
21401
- function findMatchingSpec(url) {
21402
- return (urlRegistry
21403
- .getAll()
21404
- .sort((a, b) => a.sequence - b.sequence)
21405
- .find((urlType) => urlType.match(url)) || WebUrlSpec);
21406
- }
21407
- function urlRepresentation(link, getters) {
21408
- return findMatchingSpec(link.url).urlRepresentation(link.url, getters);
21409
- }
21410
- function openLink(link, env) {
21411
- findMatchingSpec(link.url).open(link.url, env);
21412
- }
21413
- function detectLink(value) {
21414
- if (typeof value !== "string") {
21415
- return undefined;
21416
- }
21417
- if (isMarkdownLink(value)) {
21418
- const { label, url } = parseMarkdownLink(value);
21419
- return findMatchingSpec(url).createLink(url, label);
21420
- }
21421
- else if (isWebLink(value)) {
21422
- return createWebLink(value);
21423
- }
21424
- return undefined;
21425
- }
21426
-
21427
- function evaluateLiteral(literalCell, localeFormat) {
21428
- const value = isTextFormat(localeFormat.format) ? literalCell.content : literalCell.parsedValue;
21429
- const functionResult = { value, format: localeFormat.format };
21430
- return createEvaluatedCell(functionResult, localeFormat.locale);
21431
- }
21432
- function parseLiteral(content, locale) {
21433
- if (content.startsWith("=")) {
21434
- throw new Error(`Cannot parse "${content}" because it's not a literal value. It's a formula`);
21435
- }
21436
- if (content === "") {
21437
- return null;
21438
- }
21439
- if (isNumber(content, DEFAULT_LOCALE)) {
21440
- return parseNumber(content, DEFAULT_LOCALE);
21441
- }
21442
- const internalDate = parseDateTime(content, locale);
21443
- if (internalDate) {
21444
- return internalDate.value;
21445
- }
21446
- if (isBoolean(content)) {
21447
- return content.toUpperCase() === "TRUE";
21448
- }
21449
- return content;
21450
- }
21451
- function createEvaluatedCell(functionResult, locale = DEFAULT_LOCALE, cell) {
21452
- const link = detectLink(functionResult.value);
21453
- if (!link) {
21454
- return _createEvaluatedCell(functionResult, locale, cell);
21455
- }
21456
- const value = parseLiteral(link.label, locale);
21457
- const format = functionResult.format ||
21458
- (typeof value === "number"
21459
- ? detectDateFormat(link.label, locale) || detectNumberFormat(link.label)
21460
- : undefined);
21461
- const linkPayload = {
21462
- value,
21463
- format,
21464
- };
21465
- return {
21466
- ..._createEvaluatedCell(linkPayload, locale, cell),
21467
- link,
21468
- };
21469
- }
21470
- function _createEvaluatedCell(functionResult, locale, cell) {
21471
- let { value, format, message } = functionResult;
21472
- format = cell?.format || format;
21473
- const formattedValue = formatValue(value, { format, locale });
21474
- if (isEvaluationError(value)) {
21475
- return errorCell(value, message);
21476
- }
21477
- if (isTextFormat(format)) {
21478
- // TO DO:
21479
- // with the next line, the value of the cell is transformed depending on the format.
21480
- // This shouldn't happen, by doing this, the formulas handling numbers are not able
21481
- // to interpret the value as a number.
21482
- return textCell(toString(value), format, formattedValue);
21483
- }
21484
- if (value === null) {
21485
- return emptyCell(format);
21486
- }
21487
- if (typeof value === "number") {
21488
- if (isDateTimeFormat(format || "")) {
21489
- return dateTimeCell(value, format, formattedValue);
21490
- }
21491
- return numberCell(value, format, formattedValue);
21492
- }
21493
- if (typeof value === "boolean") {
21494
- return booleanCell(value, format, formattedValue);
21495
- }
21496
- return textCell(value, format, formattedValue);
21497
- }
21498
- function textCell(value, format, formattedValue) {
21499
- return {
21500
- value,
21501
- format,
21502
- formattedValue,
21503
- type: CellValueType.text,
21504
- isAutoSummable: true,
21505
- defaultAlign: "left",
21506
- };
21507
- }
21508
- function numberCell(value, format, formattedValue) {
21509
- return {
21510
- value: value || 0, // necessary to avoid "-0" and NaN values,
21511
- format,
21512
- formattedValue,
21513
- type: CellValueType.number,
21514
- isAutoSummable: true,
21515
- defaultAlign: "right",
21516
- };
21517
- }
21518
- const emptyCell = memoize(function emptyCell(format) {
21519
- return {
21520
- value: null,
21521
- format,
21522
- formattedValue: "",
21523
- type: CellValueType.empty,
21524
- isAutoSummable: true,
21525
- defaultAlign: "left",
21526
- };
21527
- });
21528
- function dateTimeCell(value, format, formattedValue) {
21529
- return {
21530
- value,
21531
- format,
21532
- formattedValue,
21533
- type: CellValueType.number,
21534
- isAutoSummable: false,
21535
- defaultAlign: "right",
21536
- };
21537
- }
21538
- function booleanCell(value, format, formattedValue) {
21539
- return {
21540
- value,
21541
- format,
21542
- formattedValue,
21543
- type: CellValueType.boolean,
21544
- isAutoSummable: false,
21545
- defaultAlign: "center",
21546
- };
21547
- }
21548
- function errorCell(value, message) {
21549
- return {
21550
- value,
21551
- formattedValue: value,
21552
- message,
21553
- type: CellValueType.error,
21554
- isAutoSummable: false,
21555
- defaultAlign: "center",
21556
- };
21557
- }
21558
-
21559
21640
  /**
21560
21641
  * An AutofillModifierImplementation is used to describe how to handle a
21561
21642
  * AutofillModifier.
@@ -22920,6 +23001,10 @@ stores.inject(MyMetaStore, storeInstance);
22920
23001
  WarningTypes["CfIconSetEmptyIconNotSupported"] = "IconSets with empty icons";
22921
23002
  WarningTypes["BadlyFormattedHyperlink"] = "Badly formatted hyperlink";
22922
23003
  WarningTypes["NumFmtIdNotSupported"] = "Number format";
23004
+ WarningTypes["TimeDataValidationNotSupported"] = "Time data validation rules";
23005
+ WarningTypes["TextLengthDataValidationNotSupported"] = "Text length data validation rules";
23006
+ WarningTypes["WholeNumberDataValidationNotSupported"] = "Whole number data validation rules";
23007
+ WarningTypes["NotEqualDateDataValidationNotSupported"] = "Not equal date data validation rules";
22923
23008
  })(WarningTypes || (WarningTypes = {}));
22924
23009
  class XLSXImportWarningManager {
22925
23010
  _parsingWarnings = new Set();
@@ -23298,6 +23383,25 @@ stores.inject(MyMetaStore, storeInstance);
23298
23383
  webp: "image/webp",
23299
23384
  jpg: "image/jpeg",
23300
23385
  };
23386
+ const XLSX_DV_DECIMAL_OPERATOR_MAPPING = {
23387
+ between: "isBetween",
23388
+ notBetween: "isNotBetween",
23389
+ equal: "isEqual",
23390
+ notEqual: "isNotEqual",
23391
+ greaterThan: "isGreaterThan",
23392
+ greaterThanOrEqual: "isGreaterOrEqualTo",
23393
+ lessThan: "isLessThan",
23394
+ lessThanOrEqual: "isLessOrEqualTo",
23395
+ };
23396
+ const XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING = {
23397
+ between: "dateIsBetween",
23398
+ notBetween: "dateIsNotBetween",
23399
+ equal: "dateIs",
23400
+ greaterThan: "dateIsAfter",
23401
+ greaterThanOrEqual: "dateIsOnOrAfter",
23402
+ lessThan: "dateIsBefore",
23403
+ lessThanOrEqual: "dateIsOnOrBefore",
23404
+ };
23301
23405
 
23302
23406
  /**
23303
23407
  * Most of the functions could stay private, but are exported for testing purposes
@@ -24079,6 +24183,20 @@ stores.inject(MyMetaStore, storeInstance);
24079
24183
  }
24080
24184
  return position / HEIGHT_FACTOR;
24081
24185
  }
24186
+ /**
24187
+ * Convert the o-spreadsheet data validation decimal
24188
+ * criterion type to the corresponding excel operator.
24189
+ */
24190
+ function convertDecimalCriterionTypeToExcelOperator(operator) {
24191
+ return Object.keys(XLSX_DV_DECIMAL_OPERATOR_MAPPING).find((key) => XLSX_DV_DECIMAL_OPERATOR_MAPPING[key] === operator);
24192
+ }
24193
+ /**
24194
+ * Convert the o-spreadsheet data validation date
24195
+ * criterion type to the corresponding excel operator.
24196
+ */
24197
+ function convertDateCriterionTypeToExcelOperator(operator) {
24198
+ return Object.keys(XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING).find((key) => XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[key] === operator);
24199
+ }
24082
24200
 
24083
24201
  function convertFigures(sheetData) {
24084
24202
  let id = 1;
@@ -24188,6 +24306,112 @@ stores.inject(MyMetaStore, storeInstance);
24188
24306
  };
24189
24307
  }
24190
24308
 
24309
+ function convertDataValidationRules(xlsxDataValidations, warningManager) {
24310
+ const dvRules = [];
24311
+ let dvId = 1;
24312
+ for (const dv of xlsxDataValidations) {
24313
+ if (!dv) {
24314
+ continue;
24315
+ }
24316
+ switch (dv.type) {
24317
+ case "time":
24318
+ warningManager.generateNotSupportedWarning(WarningTypes.TimeDataValidationNotSupported);
24319
+ break;
24320
+ case "textLength":
24321
+ warningManager.generateNotSupportedWarning(WarningTypes.TextLengthDataValidationNotSupported);
24322
+ break;
24323
+ case "whole":
24324
+ warningManager.generateNotSupportedWarning(WarningTypes.WholeNumberDataValidationNotSupported);
24325
+ break;
24326
+ case "decimal":
24327
+ const decimalRule = convertDecimalRule(dvId++, dv);
24328
+ dvRules.push(decimalRule);
24329
+ break;
24330
+ case "list":
24331
+ const listRule = convertListrule(dvId++, dv);
24332
+ dvRules.push(listRule);
24333
+ break;
24334
+ case "date":
24335
+ if (dv.operator === "notEqual") {
24336
+ warningManager.generateNotSupportedWarning(WarningTypes.NotEqualDateDataValidationNotSupported);
24337
+ break;
24338
+ }
24339
+ const dateRule = convertDateRule(dvId++, dv);
24340
+ dvRules.push(dateRule);
24341
+ break;
24342
+ case "custom":
24343
+ const customRule = convertCustomRule(dvId++, dv);
24344
+ dvRules.push(customRule);
24345
+ break;
24346
+ }
24347
+ }
24348
+ return dvRules;
24349
+ }
24350
+ function convertDecimalRule(id, dv) {
24351
+ const values = [dv.formula1.toString()];
24352
+ if (dv.formula2) {
24353
+ values.push(dv.formula2.toString());
24354
+ }
24355
+ return {
24356
+ id: id.toString(),
24357
+ ranges: dv.sqref,
24358
+ isBlocking: dv.errorStyle !== "warning",
24359
+ criterion: {
24360
+ type: XLSX_DV_DECIMAL_OPERATOR_MAPPING[dv.operator],
24361
+ values,
24362
+ },
24363
+ };
24364
+ }
24365
+ function convertListrule(id, dv) {
24366
+ const formula1 = dv.formula1.toString();
24367
+ const isRangeRule = rangeReference.test(formula1);
24368
+ return {
24369
+ id: id.toString(),
24370
+ ranges: dv.sqref,
24371
+ isBlocking: dv.errorStyle !== "warning",
24372
+ criterion: {
24373
+ type: isRangeRule ? "isValueInRange" : "isValueInList",
24374
+ values: isRangeRule ? [formula1] : formula1.replaceAll('"', "").split(","),
24375
+ displayStyle: "arrow",
24376
+ },
24377
+ };
24378
+ }
24379
+ function convertDateRule(id, dv) {
24380
+ let criterion;
24381
+ const values = [dv.formula1.toString()];
24382
+ if (dv.formula2) {
24383
+ values.push(dv.formula2.toString());
24384
+ criterion = {
24385
+ type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
24386
+ values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
24387
+ };
24388
+ }
24389
+ else {
24390
+ criterion = {
24391
+ type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
24392
+ values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
24393
+ dateValue: "exactDate",
24394
+ };
24395
+ }
24396
+ return {
24397
+ id: id.toString(),
24398
+ ranges: dv.sqref,
24399
+ isBlocking: dv.errorStyle !== "warning",
24400
+ criterion: criterion,
24401
+ };
24402
+ }
24403
+ function convertCustomRule(id, dv) {
24404
+ return {
24405
+ id: id.toString(),
24406
+ ranges: dv.sqref,
24407
+ isBlocking: dv.errorStyle !== "warning",
24408
+ criterion: {
24409
+ type: "customFormula",
24410
+ values: [`=${dv.formula1.toString()}`],
24411
+ },
24412
+ };
24413
+ }
24414
+
24191
24415
  /**
24192
24416
  * Match external reference (ex. '[1]Sheet 3'!$B$4)
24193
24417
  *
@@ -24308,6 +24532,7 @@ stores.inject(MyMetaStore, storeInstance);
24308
24532
  cols: convertCols(sheet, sheetDims[0], colHeaderGroups),
24309
24533
  rows: convertRows(sheet, sheetDims[1], rowHeaderGroups),
24310
24534
  conditionalFormats: convertConditionalFormats(sheet.cfs, data.dxfs, warningManager),
24535
+ dataValidationRules: convertDataValidationRules(sheet.dataValidations, warningManager),
24311
24536
  figures: convertFigures(sheet),
24312
24537
  isVisible: sheet.isVisible,
24313
24538
  panes: sheetOptions
@@ -25587,6 +25812,41 @@ stores.inject(MyMetaStore, storeInstance);
25587
25812
  }
25588
25813
  }
25589
25814
 
25815
+ class XlsxDataValidationExtractor extends XlsxBaseExtractor {
25816
+ theme;
25817
+ constructor(sheetFile, xlsxStructure, warningManager, theme) {
25818
+ super(sheetFile, xlsxStructure, warningManager);
25819
+ this.theme = theme;
25820
+ }
25821
+ extractDataValidations() {
25822
+ const dataValidations = this.mapOnElements({ parent: this.rootFile.file.xml, query: "worksheet > dataValidations > dataValidation" }, (dvElement) => {
25823
+ return {
25824
+ type: this.extractAttr(dvElement, "type", { required: true }).asString(),
25825
+ operator: this.extractAttr(dvElement, "operator", {
25826
+ default: "between",
25827
+ })?.asString(),
25828
+ sqref: this.extractAttr(dvElement, "sqref", { required: true }).asString().split(" "),
25829
+ errorStyle: this.extractAttr(dvElement, "errorStyle")?.asString(),
25830
+ formula1: this.extractDataValidationFormula(dvElement, 1)[0],
25831
+ formula2: this.extractDataValidationFormula(dvElement, 2)[0],
25832
+ showErrorMessage: this.extractAttr(dvElement, "showErrorMessage")?.asBool(),
25833
+ errorTitle: this.extractAttr(dvElement, "errorTitle")?.asString(),
25834
+ error: this.extractAttr(dvElement, "error")?.asString(),
25835
+ showInputMessage: this.extractAttr(dvElement, "showInputMessage")?.asBool(),
25836
+ promptTitle: this.extractAttr(dvElement, "promptTitle")?.asString(),
25837
+ prompt: this.extractAttr(dvElement, "prompt")?.asString(),
25838
+ allowBlank: this.extractAttr(dvElement, "allowBlank")?.asBool(),
25839
+ };
25840
+ });
25841
+ return dataValidations;
25842
+ }
25843
+ extractDataValidationFormula(dvElement, index) {
25844
+ return this.mapOnElements({ parent: dvElement, query: `formula${index}` }, (cfFormulaElements) => {
25845
+ return this.extractTextContent(cfFormulaElements, { required: true });
25846
+ });
25847
+ }
25848
+ }
25849
+
25590
25850
  class XlsxChartExtractor extends XlsxBaseExtractor {
25591
25851
  extractChart() {
25592
25852
  return this.mapOnElements({ parent: this.rootFile.file.xml, query: "c:chartSpace" }, (rootChartElement) => {
@@ -25954,6 +26214,7 @@ stores.inject(MyMetaStore, storeInstance);
25954
26214
  sharedFormulas: this.extractSharedFormulas(sheetElement),
25955
26215
  merges: this.extractMerges(sheetElement),
25956
26216
  cfs: this.extractConditionalFormats(),
26217
+ dataValidations: this.extractDataValidations(),
25957
26218
  figures: this.extractFigures(sheetElement),
25958
26219
  hyperlinks: this.extractHyperLinks(sheetElement),
25959
26220
  tables: this.extractTables(sheetElement),
@@ -26025,6 +26286,9 @@ stores.inject(MyMetaStore, storeInstance);
26025
26286
  extractConditionalFormats() {
26026
26287
  return new XlsxCfExtractor(this.rootFile, this.xlsxFileStructure, this.warningManager, this.theme).extractConditionalFormattings();
26027
26288
  }
26289
+ extractDataValidations() {
26290
+ return new XlsxDataValidationExtractor(this.rootFile, this.xlsxFileStructure, this.warningManager, this.theme).extractDataValidations();
26291
+ }
26028
26292
  extractFigures(worksheet) {
26029
26293
  const figures = this.mapOnElements({ parent: worksheet, query: "drawing" }, (drawingElement) => {
26030
26294
  const drawingId = this.extractAttr(drawingElement, "r:id", { required: true })?.asString();
@@ -27332,6 +27596,7 @@ stores.inject(MyMetaStore, storeInstance);
27332
27596
  rows: {},
27333
27597
  merges: [],
27334
27598
  conditionalFormats: [],
27599
+ dataValidationRules: [],
27335
27600
  figures: [],
27336
27601
  tables: [],
27337
27602
  isVisible: true,
@@ -27406,17 +27671,15 @@ stores.inject(MyMetaStore, storeInstance);
27406
27671
  });
27407
27672
  }
27408
27673
  catch (error) {
27409
- const parsedSpreadsheetContent = clipboardContent[ClipboardMIMEType.OSpreadsheet]
27410
- ? JSON.parse(clipboardContent[ClipboardMIMEType.OSpreadsheet])
27411
- : {};
27412
- if (parsedSpreadsheetContent.version && parsedSpreadsheetContent.version !== CURRENT_VERSION) {
27674
+ const parsedSpreadsheetContent = clipboardContent.data;
27675
+ if (parsedSpreadsheetContent?.version !== CURRENT_VERSION) {
27413
27676
  env.raiseError(_t("An unexpected error occurred while pasting content.\
27414
27677
  This is probably due to a spreadsheet version mismatch."));
27415
27678
  }
27416
27679
  result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
27417
27680
  target,
27418
27681
  clipboardContent: {
27419
- [ClipboardMIMEType.PlainText]: clipboardContent[ClipboardMIMEType.PlainText],
27682
+ text: clipboardContent.text,
27420
27683
  },
27421
27684
  pasteOption,
27422
27685
  });
@@ -27445,7 +27708,6 @@ stores.inject(MyMetaStore, storeInstance);
27445
27708
  ["ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */]: _t("Invalid lower inflection point formula"),
27446
27709
  ["EmptyRange" /* CommandResult.EmptyRange */]: _t("A range needs to be defined"),
27447
27710
  ["ValueCellIsInvalidFormula" /* CommandResult.ValueCellIsInvalidFormula */]: _t("At least one of the provided values is an invalid formula"),
27448
- ["DataBarRangeValuesMismatch" /* CommandResult.DataBarRangeValuesMismatch */]: _t("All the ranges and the range values must have the same size"),
27449
27711
  Unexpected: _t("The rule is invalid for an unknown reason"),
27450
27712
  },
27451
27713
  ColorScale: _t("Color scale"),
@@ -27541,6 +27803,13 @@ stores.inject(MyMetaStore, storeInstance);
27541
27803
  numberValue: _t("The value must be a number"),
27542
27804
  dateValue: _t("The value must be a date"),
27543
27805
  validRange: _t("The value must be a valid range"),
27806
+ validFormula: _t("The formula must be valid"),
27807
+ },
27808
+ Errors: {
27809
+ ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid."),
27810
+ ["InvalidDataValidationCriterionValue" /* CommandResult.InvalidDataValidationCriterionValue */]: _t("One or more of the provided criteria values are invalid. Please review and correct them."),
27811
+ ["InvalidNumberOfCriterionValues" /* CommandResult.InvalidNumberOfCriterionValues */]: _t("One or more of the provided criteria values are missing."),
27812
+ Unexpected: _t("The rule is invalid for an unknown reason."),
27544
27813
  },
27545
27814
  };
27546
27815
  const TableTerms = {
@@ -27751,7 +28020,7 @@ stores.inject(MyMetaStore, storeInstance);
27751
28020
  labels.length > dataSetsValues[0].data.length) {
27752
28021
  labels.shift();
27753
28022
  }
27754
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
28023
+ ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
27755
28024
  if (definition.aggregated) {
27756
28025
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
27757
28026
  }
@@ -27806,7 +28075,7 @@ stores.inject(MyMetaStore, storeInstance);
27806
28075
  labels.length > dataSetsValues[0].data.length) {
27807
28076
  labels.shift();
27808
28077
  }
27809
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
28078
+ ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
27810
28079
  if (axisType === "time") {
27811
28080
  ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues));
27812
28081
  }
@@ -27823,7 +28092,7 @@ stores.inject(MyMetaStore, storeInstance);
27823
28092
  if (definition.cumulative) {
27824
28093
  let accumulator = 0;
27825
28094
  data = data.map((value) => {
27826
- if (!isNaN(value)) {
28095
+ if (!isNaN(parseFloat(value))) {
27827
28096
  accumulator += parseFloat(value);
27828
28097
  return accumulator;
27829
28098
  }
@@ -27856,11 +28125,11 @@ stores.inject(MyMetaStore, storeInstance);
27856
28125
  labels.length > dataSetsValues[0].data.length) {
27857
28126
  labels.shift();
27858
28127
  }
27859
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
28128
+ ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
27860
28129
  if (definition.aggregated) {
27861
28130
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
27862
28131
  }
27863
- ({ dataSetsValues, labels } = filterNegativeValues(labels, dataSetsValues));
28132
+ ({ dataSetsValues, labels } = keepOnlyPositiveValues(labels, dataSetsValues));
27864
28133
  const dataSetFormat = getChartDatasetFormat(getters, dataSets, "left");
27865
28134
  return {
27866
28135
  dataSetsValues,
@@ -27878,7 +28147,7 @@ stores.inject(MyMetaStore, storeInstance);
27878
28147
  labels.length > dataSetsValues[0].data.length) {
27879
28148
  labels.shift();
27880
28149
  }
27881
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
28150
+ ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
27882
28151
  if (definition.aggregated) {
27883
28152
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
27884
28153
  }
@@ -28057,26 +28326,16 @@ stores.inject(MyMetaStore, storeInstance);
28057
28326
  }
28058
28327
  return isInstalled;
28059
28328
  }
28060
- function filterNegativeValues(labels, datasets) {
28061
- const dataPointsIndexes = labels.reduce((indexes, label, i) => {
28062
- const shouldKeep = datasets.some((dataset) => {
28063
- const dataPoint = dataset.data[i];
28064
- return typeof dataPoint !== "number" || dataPoint >= 0;
28065
- });
28066
- if (shouldKeep) {
28067
- indexes.push(i);
28068
- }
28069
- return indexes;
28070
- }, []);
28071
- const filteredLabels = dataPointsIndexes.map((i) => labels[i] || "");
28072
- const filteredDatasets = datasets.map((dataset) => ({
28073
- ...dataset,
28074
- data: dataPointsIndexes.map((i) => {
28075
- const dataPoint = dataset.data[i];
28076
- return typeof dataPoint !== "number" || dataPoint >= 0 ? dataPoint : 0;
28077
- }),
28078
- }));
28079
- return { labels: filteredLabels, dataSetsValues: filteredDatasets };
28329
+ function keepOnlyPositiveValues(labels, datasets) {
28330
+ const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
28331
+ const filteredIndexes = range(0, numberOfDataPoints).filter((i) => datasets.some((ds) => typeof ds.data[i] === "number" && ds.data[i] > 0));
28332
+ return {
28333
+ labels: filteredIndexes.map((i) => labels[i] || ""),
28334
+ dataSetsValues: datasets.map((ds) => ({
28335
+ ...ds,
28336
+ data: filteredIndexes.map((i) => typeof ds.data[i] === "number" && ds.data[i] > 0 ? ds.data[i] : null),
28337
+ })),
28338
+ };
28080
28339
  }
28081
28340
  function fixEmptyLabelsForDateCharts(labels, dataSetsValues) {
28082
28341
  if (labels.length === 0 || labels.every((label) => !label)) {
@@ -28109,18 +28368,23 @@ stores.inject(MyMetaStore, storeInstance);
28109
28368
  }
28110
28369
  return [];
28111
28370
  }
28112
- function filterEmptyDataPoints(labels, datasets) {
28371
+ /**
28372
+ * Filter the data points that:
28373
+ * - have neither a label nor a value
28374
+ * - have no label and a non-numeric value
28375
+ */
28376
+ function filterInvalidDataPoints(labels, datasets) {
28113
28377
  const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
28114
28378
  const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
28115
28379
  const label = labels[dataPointIndex];
28116
28380
  const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);
28117
- return label || values.some((value) => value === 0 || Boolean(value));
28381
+ return label || values.some((value) => typeof value === "number");
28118
28382
  });
28119
28383
  return {
28120
28384
  labels: dataPointsIndexes.map((i) => labels[i] || ""),
28121
28385
  dataSetsValues: datasets.map((dataset) => ({
28122
28386
  ...dataset,
28123
- data: dataPointsIndexes.map((i) => dataset.data[i]),
28387
+ data: dataPointsIndexes.map((i) => typeof dataset.data[i] === "number" ? dataset.data[i] : null),
28124
28388
  })),
28125
28389
  };
28126
28390
  }
@@ -28263,8 +28527,8 @@ stores.inject(MyMetaStore, storeInstance);
28263
28527
  const dataset = {
28264
28528
  label,
28265
28529
  data,
28266
- borderColor: BORDER_CHART_COLOR,
28267
- borderWidth: 1,
28530
+ borderColor: definition.background || BACKGROUND_CHART_COLOR,
28531
+ borderWidth: definition.stacked ? 1 : 0,
28268
28532
  backgroundColor,
28269
28533
  yAxisID: definition.horizontal ? "y" : definition.dataSets?.[index].yAxisId || "y",
28270
28534
  xAxisID: "x",
@@ -28817,20 +29081,27 @@ stores.inject(MyMetaStore, storeInstance);
28817
29081
  return scales;
28818
29082
  }
28819
29083
  function getPyramidChartScales(definition, args) {
29084
+ const { dataSetsValues } = args;
28820
29085
  const scales = getBarChartScales(definition, args);
28821
29086
  const scalesXCallback = scales.x.ticks.callback;
28822
29087
  scales.x.ticks.callback = (value) => scalesXCallback(Math.abs(value));
29088
+ const maxValue = Math.max(...dataSetsValues.map((dataSet) => Math.max(...dataSet.data.map(Math.abs))));
29089
+ scales.x.suggestedMin = -maxValue;
29090
+ scales.x.suggestedMax = maxValue;
28823
29091
  return scales;
28824
29092
  }
28825
29093
  function getRadarChartScales(definition, args) {
28826
- const { locale, axisFormats } = args;
29094
+ const { locale, axisFormats, dataSetsValues } = args;
29095
+ const minValue = Math.min(...dataSetsValues.map((ds) => Math.min(...ds.data.filter((x) => !isNaN(x)))));
28827
29096
  return {
28828
29097
  r: {
29098
+ beginAtZero: true,
28829
29099
  ticks: {
28830
29100
  callback: formatTickValue({ format: axisFormats?.r, locale }),
28831
29101
  backdropColor: definition.background || "#FFFFFF",
28832
29102
  },
28833
29103
  pointLabels: { color: chartFontColor(definition.background) },
29104
+ suggestedMin: minValue < 0 ? minValue - 1 : 0,
28834
29105
  },
28835
29106
  };
28836
29107
  }
@@ -31931,14 +32202,9 @@ stores.inject(MyMetaStore, storeInstance);
31931
32202
  }
31932
32203
  const sheetId = this.env.model.getters.getActiveSheetId();
31933
32204
  const contentZone = { ...tableZone, top: tableZone.top + 1 };
31934
- this.env.model.dispatch("SORT_CELLS", {
31935
- sheetId,
31936
- col: filterPosition.col,
31937
- row: contentZone.top,
31938
- zone: contentZone,
31939
- sortDirection,
31940
- sortOptions: { emptyCellAsZero: true, sortHeaders: true },
31941
- });
32205
+ const sortAnchor = { col: filterPosition.col, row: contentZone.top };
32206
+ const sortOptions = { emptyCellAsZero: true, sortHeaders: true };
32207
+ interactiveSort(this.env, sheetId, sortAnchor, contentZone, sortDirection, sortOptions);
31942
32208
  this.props.onClosed?.();
31943
32209
  }
31944
32210
  }
@@ -32936,13 +33202,11 @@ stores.inject(MyMetaStore, storeInstance);
32936
33202
  const osClipboard = await env.clipboard.read();
32937
33203
  switch (osClipboard.status) {
32938
33204
  case "ok":
32939
- const htmlDocument = new DOMParser().parseFromString(osClipboard.content[ClipboardMIMEType.Html] ?? "<div></div>", "text/html");
32940
- const osClipboardSpreadsheetContent = osClipboard.content[ClipboardMIMEType.OSpreadsheet] || "{}";
32941
- const clipboardId = JSON.parse(osClipboardSpreadsheetContent).clipboardId ??
32942
- htmlDocument.querySelector("div")?.getAttribute("data-clipboard-id");
33205
+ const clipboardContent = parseOSClipboardContent(osClipboard.content);
33206
+ const clipboardId = clipboardContent.data?.clipboardId;
32943
33207
  const target = env.model.getters.getSelectedZones();
32944
33208
  if (env.model.getters.getClipboardId() !== clipboardId) {
32945
- interactivePasteFromOS(env, target, osClipboard.content, pasteOption);
33209
+ interactivePasteFromOS(env, target, clipboardContent, pasteOption);
32946
33210
  }
32947
33211
  else {
32948
33212
  interactivePaste(env, target, pasteOption);
@@ -33956,6 +34220,7 @@ stores.inject(MyMetaStore, storeInstance);
33956
34220
  const pivotId = env.model.getters.getPivotIdFromPosition(position);
33957
34221
  return (pivotId && env.model.getters.isExistingPivot(pivotId)) || false;
33958
34222
  },
34223
+ isReadonlyAllowed: true,
33959
34224
  icon: "o-spreadsheet-Icon.PIVOT",
33960
34225
  };
33961
34226
  const FIX_FORMULAS = {
@@ -35673,6 +35938,7 @@ stores.inject(MyMetaStore, storeInstance);
35673
35938
  id: `item_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,
35674
35939
  name: env.model.getters.getPivotDisplayName(pivotId),
35675
35940
  sequence: sequence + index,
35941
+ isReadonlyAllowed: true,
35676
35942
  execute: (env) => env.openSidePanel("PivotSidePanel", { pivotId }),
35677
35943
  onStartHover: (env) => env.getStore(HighlightStore).register(highlightProvider),
35678
35944
  onStopHover: (env) => env.getStore(HighlightStore).unRegister(highlightProvider),
@@ -36162,7 +36428,7 @@ stores.inject(MyMetaStore, storeInstance);
36162
36428
  flex-grow: 0;
36163
36429
  }
36164
36430
 
36165
- /** Make the character a bit bigger
36431
+ /* Make the character a bit bigger
36166
36432
  compared to its neighbor INPUT box */
36167
36433
  .o-remove-selection {
36168
36434
  font-size: calc(100% + 4px);
@@ -36767,7 +37033,7 @@ stores.inject(MyMetaStore, storeInstance);
36767
37033
  css /* scss */ `
36768
37034
  .o-color-picker {
36769
37035
  padding: ${PICKER_PADDING}px 0;
36770
- /** FIXME: this is useless, overiden by the popover container */
37036
+ /* FIXME: this is useless, overiden by the popover container */
36771
37037
  box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
36772
37038
  background-color: white;
36773
37039
  line-height: 1.2;
@@ -36910,7 +37176,7 @@ stores.inject(MyMetaStore, storeInstance);
36910
37176
  margin-right: 2px;
36911
37177
  }
36912
37178
  .o-wrong-color {
36913
- /** FIXME bootstrap class instead? */
37179
+ /* FIXME bootstrap class instead? */
36914
37180
  outline-color: red;
36915
37181
  border-color: red;
36916
37182
  &:focus {
@@ -38954,10 +39220,10 @@ stores.inject(MyMetaStore, storeInstance);
38954
39220
  }
38955
39221
 
38956
39222
  .fa-stack {
38957
- // reset stack size which is doubled by default
38958
- width: 1em;
38959
- height: 1em;
38960
- line-height: 1em;
39223
+ /* reset stack size which is doubled by default */
39224
+ width: ${CLOSE_ICON_RADIUS * 2}px;
39225
+ height: ${CLOSE_ICON_RADIUS * 2}px;
39226
+ line-height: ${CLOSE_ICON_RADIUS * 2}px;
38961
39227
  }
38962
39228
 
38963
39229
  .force-open-assistant {
@@ -38974,7 +39240,7 @@ stores.inject(MyMetaStore, storeInstance);
38974
39240
  margin: 1px 4px;
38975
39241
 
38976
39242
  .o-semi-bold {
38977
- /** FIXME: to remove in favor of Bootstrap
39243
+ /* FIXME: to remove in favor of Bootstrap
38978
39244
  * 'fw-semibold' when we upgrade to Bootstrap 5.2
38979
39245
  */
38980
39246
  font-weight: 600 !important;
@@ -40210,7 +40476,7 @@ stores.inject(MyMetaStore, storeInstance);
40210
40476
  .o-threshold-value {
40211
40477
  flex-grow: 1;
40212
40478
  flex-basis: 60%;
40213
- min-width: 0px; // input overflows in Firefox otherwise
40479
+ min-width: 0px; /* input overflows in Firefox otherwise */
40214
40480
  }
40215
40481
  .o-threshold-value input:disabled {
40216
40482
  background-color: #edebed;
@@ -41029,7 +41295,7 @@ stores.inject(MyMetaStore, storeInstance);
41029
41295
  name: _t("Date is"),
41030
41296
  getPreview: (criterion, getters) => {
41031
41297
  return criterion.dateValue === "exactDate"
41032
- ? _t("Date is %s", getDateCriterionFormattedValues(criterion, getters)[0])
41298
+ ? _t("Date is %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41033
41299
  : _t("Date is %s", DVTerms.DateIs[criterion.dateValue]);
41034
41300
  },
41035
41301
  });
@@ -41054,7 +41320,7 @@ stores.inject(MyMetaStore, storeInstance);
41054
41320
  name: _t("Date is before"),
41055
41321
  getPreview: (criterion, getters) => {
41056
41322
  return criterion.dateValue === "exactDate"
41057
- ? _t("Date is before %s", getDateCriterionFormattedValues(criterion, getters)[0])
41323
+ ? _t("Date is before %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41058
41324
  : _t("Date is before %s", DVTerms.DateIsBefore[criterion.dateValue]);
41059
41325
  },
41060
41326
  });
@@ -41079,7 +41345,7 @@ stores.inject(MyMetaStore, storeInstance);
41079
41345
  name: _t("Date is on or before"),
41080
41346
  getPreview: (criterion, getters) => {
41081
41347
  return criterion.dateValue === "exactDate"
41082
- ? _t("Date is on or before %s", getDateCriterionFormattedValues(criterion, getters)[0])
41348
+ ? _t("Date is on or before %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41083
41349
  : _t("Date is on or before %s", DVTerms.DateIsBefore[criterion.dateValue]);
41084
41350
  },
41085
41351
  });
@@ -41104,7 +41370,7 @@ stores.inject(MyMetaStore, storeInstance);
41104
41370
  name: _t("Date is after"),
41105
41371
  getPreview: (criterion, getters) => {
41106
41372
  return criterion.dateValue === "exactDate"
41107
- ? _t("Date is after %s", getDateCriterionFormattedValues(criterion, getters)[0])
41373
+ ? _t("Date is after %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41108
41374
  : _t("Date is after %s", DVTerms.DateIsBefore[criterion.dateValue]);
41109
41375
  },
41110
41376
  });
@@ -41129,7 +41395,7 @@ stores.inject(MyMetaStore, storeInstance);
41129
41395
  name: _t("Date is on or after"),
41130
41396
  getPreview: (criterion, getters) => {
41131
41397
  return criterion.dateValue === "exactDate"
41132
- ? _t("Date is on or after %s", getDateCriterionFormattedValues(criterion, getters)[0])
41398
+ ? _t("Date is on or after %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41133
41399
  : _t("Date is on or after %s", DVTerms.DateIsBefore[criterion.dateValue]);
41134
41400
  },
41135
41401
  });
@@ -41155,7 +41421,7 @@ stores.inject(MyMetaStore, storeInstance);
41155
41421
  numberOfValues: () => 2,
41156
41422
  name: _t("Date is between"),
41157
41423
  getPreview: (criterion, getters) => {
41158
- const values = getDateCriterionFormattedValues(criterion, getters);
41424
+ const values = getDateCriterionFormattedValues(criterion.values, getters.getLocale());
41159
41425
  return _t("Date is between %s and %s", values[0], values[1]);
41160
41426
  },
41161
41427
  });
@@ -41181,7 +41447,7 @@ stores.inject(MyMetaStore, storeInstance);
41181
41447
  numberOfValues: () => 2,
41182
41448
  name: _t("Date is not between"),
41183
41449
  getPreview: (criterion, getters) => {
41184
- const values = getDateCriterionFormattedValues(criterion, getters);
41450
+ const values = getDateCriterionFormattedValues(criterion.values, getters.getLocale());
41185
41451
  return _t("Date is not between %s and %s", values[0], values[1]);
41186
41452
  },
41187
41453
  });
@@ -41464,19 +41730,6 @@ stores.inject(MyMetaStore, storeInstance);
41464
41730
  const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
41465
41731
  return valueAsNumber !== undefined;
41466
41732
  }
41467
- function getDateCriterionFormattedValues(criterion, getters) {
41468
- const locale = getters.getLocale();
41469
- return criterion.values.map((valueStr) => {
41470
- if (valueStr.startsWith("=")) {
41471
- return valueStr;
41472
- }
41473
- const value = parseLiteral(valueStr, locale);
41474
- if (typeof value === "number") {
41475
- return formatValue(value, { format: locale.dateFormat, locale });
41476
- }
41477
- return "";
41478
- });
41479
- }
41480
41733
 
41481
41734
  /** This component looks like a select input, but on click it opens a Menu with the items given as props instead of a dropdown */
41482
41735
  class SelectMenu extends owl.Component {
@@ -41558,6 +41811,7 @@ stores.inject(MyMetaStore, storeInstance);
41558
41811
  focused: false,
41559
41812
  onBlur: () => { },
41560
41813
  };
41814
+ static components = { StandaloneComposer: StandaloneComposer };
41561
41815
  inputRef = owl.useRef("input");
41562
41816
  setup() {
41563
41817
  owl.useEffect(() => {
@@ -41569,10 +41823,6 @@ stores.inject(MyMetaStore, storeInstance);
41569
41823
  state = owl.useState({
41570
41824
  shouldDisplayError: !!this.props.value, // Don't display error if user inputted nothing yet
41571
41825
  });
41572
- onValueChanged(ev) {
41573
- this.state.shouldDisplayError = true;
41574
- this.props.onValueChanged(ev.target.value);
41575
- }
41576
41826
  get placeholder() {
41577
41827
  const evaluator = dataValidationEvaluatorRegistry.get(this.props.criterionType);
41578
41828
  if (evaluator.allowedValues === "onlyFormulas") {
@@ -41583,6 +41833,27 @@ stores.inject(MyMetaStore, storeInstance);
41583
41833
  }
41584
41834
  return _t("Value or formula");
41585
41835
  }
41836
+ get allowedValues() {
41837
+ const evaluator = dataValidationEvaluatorRegistry.get(this.props.criterionType);
41838
+ return evaluator.allowedValues ?? "any";
41839
+ }
41840
+ onInputValueChanged(ev) {
41841
+ this.state.shouldDisplayError = true;
41842
+ this.props.onValueChanged(ev.target.value);
41843
+ }
41844
+ onChangeComposerValue(str) {
41845
+ this.state.shouldDisplayError = true;
41846
+ this.props.onValueChanged(str);
41847
+ }
41848
+ getDataValidationRuleInputComposerProps() {
41849
+ return {
41850
+ onConfirm: (str) => this.onChangeComposerValue(str),
41851
+ composerContent: this.props.value,
41852
+ placeholder: this.placeholder,
41853
+ class: "o-sidePanel-composer",
41854
+ defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
41855
+ };
41856
+ }
41586
41857
  get errorMessage() {
41587
41858
  if (!this.state.shouldDisplayError) {
41588
41859
  return undefined;
@@ -41918,13 +42189,13 @@ stores.inject(MyMetaStore, storeInstance);
41918
42189
 
41919
42190
  class DataValidationEditor extends owl.Component {
41920
42191
  static template = "o-spreadsheet-DataValidationEditor";
41921
- static components = { SelectionInput, SelectMenu, Section };
42192
+ static components = { SelectionInput, SelectMenu, Section, ValidationMessages };
41922
42193
  static props = {
41923
42194
  rule: { type: Object, optional: true },
41924
42195
  onExit: Function,
41925
42196
  onCloseSidePanel: { type: Function, optional: true },
41926
42197
  };
41927
- state = owl.useState({ rule: this.defaultDataValidationRule });
42198
+ state = owl.useState({ rule: this.defaultDataValidationRule, errors: [] });
41928
42199
  setup() {
41929
42200
  if (this.props.rule) {
41930
42201
  const sheetId = this.env.model.getters.getActiveSheetId();
@@ -41949,15 +42220,15 @@ stores.inject(MyMetaStore, storeInstance);
41949
42220
  this.state.rule.isBlocking = isBlocking === "true";
41950
42221
  }
41951
42222
  onSave() {
41952
- if (!this.canSave) {
41953
- return;
42223
+ if (this.state.rule) {
42224
+ const result = this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
42225
+ if (!result.isSuccessful) {
42226
+ this.state.errors = result.reasons;
42227
+ }
42228
+ else {
42229
+ this.props.onExit();
42230
+ }
41954
42231
  }
41955
- this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
41956
- this.props.onExit();
41957
- }
41958
- get canSave() {
41959
- return this.env.model.canDispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload)
41960
- .isSuccessful;
41961
42232
  }
41962
42233
  get dispatchPayload() {
41963
42234
  const rule = { ...this.state.rule, ranges: undefined };
@@ -41998,6 +42269,9 @@ stores.inject(MyMetaStore, storeInstance);
41998
42269
  get criterionComponent() {
41999
42270
  return dataValidationPanelCriteriaRegistry.get(this.state.rule.criterion.type).component;
42000
42271
  }
42272
+ get errorMessages() {
42273
+ return this.state.errors.map((error) => DVTerms.Errors[error] || DVTerms.Errors.Unexpected);
42274
+ }
42001
42275
  }
42002
42276
 
42003
42277
  css /* scss */ `
@@ -42009,7 +42283,7 @@ stores.inject(MyMetaStore, storeInstance);
42009
42283
  border-bottom: 1px solid ${FIGURE_BORDER_COLOR};
42010
42284
 
42011
42285
  .o-dv-container {
42012
- min-width: 0; // otherwise flex won't shrink correctly
42286
+ min-width: 0; /* otherwise flex won't shrink correctly */
42013
42287
  }
42014
42288
 
42015
42289
  .o-dv-preview-description {
@@ -45027,6 +45301,9 @@ stores.inject(MyMetaStore, storeInstance);
45027
45301
  PivotLayoutConfigurator,
45028
45302
  Section,
45029
45303
  };
45304
+ setup() {
45305
+ useHighlights(this);
45306
+ }
45030
45307
  get sidePanelEditor() {
45031
45308
  const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);
45032
45309
  if (!pivot) {
@@ -45034,6 +45311,9 @@ stores.inject(MyMetaStore, storeInstance);
45034
45311
  }
45035
45312
  return pivotSidePanelRegistry.get(pivot.type).editor;
45036
45313
  }
45314
+ get highlights() {
45315
+ return getPivotHighlights(this.env.model.getters, this.props.pivotId);
45316
+ }
45037
45317
  }
45038
45318
 
45039
45319
  css /* scss */ `
@@ -45703,7 +45983,7 @@ stores.inject(MyMetaStore, storeInstance);
45703
45983
 
45704
45984
  css /* scss */ `
45705
45985
  .o-table-style-popover {
45706
- /** 7 tables preview + padding by line */
45986
+ /* 7 tables preview + padding by line */
45707
45987
  width: calc((66px + 4px * 2) * 7 + 1.5rem * 2);
45708
45988
  background: #fff;
45709
45989
  font-size: 14px;
@@ -47255,7 +47535,7 @@ stores.inject(MyMetaStore, storeInstance);
47255
47535
  box-sizing: border-box !important;
47256
47536
  accent-color: #808080;
47257
47537
  margin: ${MARGIN}px;
47258
- /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
47538
+ /* required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
47259
47539
  position: absolute;
47260
47540
  }
47261
47541
  `;
@@ -47602,7 +47882,7 @@ stores.inject(MyMetaStore, storeInstance);
47602
47882
  }
47603
47883
  }
47604
47884
  .o-figure-container {
47605
- -webkit-user-select: none; // safari
47885
+ -webkit-user-select: none; /* safari */
47606
47886
  user-select: none;
47607
47887
  }
47608
47888
  `;
@@ -50675,25 +50955,20 @@ stores.inject(MyMetaStore, storeInstance);
50675
50955
  if (!clipboardData) {
50676
50956
  return;
50677
50957
  }
50678
- const clipboardDataTextContent = clipboardData?.getData(ClipboardMIMEType.PlainText);
50679
- const clipboardDataHtmlContent = clipboardData?.getData(ClipboardMIMEType.Html);
50680
- const htmlDocument = new DOMParser().parseFromString(clipboardDataHtmlContent ?? "<div></div>", "text/html");
50681
- const osClipboardSpreadsheetContent = clipboardData.getData(ClipboardMIMEType.OSpreadsheet) || "{}";
50958
+ const osClipboard = {
50959
+ content: {
50960
+ [ClipboardMIMEType.PlainText]: clipboardData?.getData(ClipboardMIMEType.PlainText),
50961
+ [ClipboardMIMEType.Html]: clipboardData?.getData(ClipboardMIMEType.Html),
50962
+ },
50963
+ };
50682
50964
  const target = this.env.model.getters.getSelectedZones();
50683
50965
  const isCutOperation = this.env.model.getters.isCutOperation();
50684
- const clipboardId = JSON.parse(osClipboardSpreadsheetContent).clipboardId ??
50685
- htmlDocument.querySelector("div")?.getAttribute("data-clipboard-id");
50966
+ const clipboardContent = parseOSClipboardContent(osClipboard.content);
50967
+ const clipboardId = clipboardContent.data?.clipboardId;
50686
50968
  if (this.env.model.getters.getClipboardId() === clipboardId) {
50687
50969
  interactivePaste(this.env, target);
50688
50970
  }
50689
50971
  else {
50690
- const clipboardContent = {
50691
- [ClipboardMIMEType.PlainText]: clipboardDataTextContent,
50692
- [ClipboardMIMEType.Html]: clipboardDataHtmlContent,
50693
- };
50694
- if (osClipboardSpreadsheetContent !== "{}") {
50695
- clipboardContent[ClipboardMIMEType.OSpreadsheet] = osClipboardSpreadsheetContent;
50696
- }
50697
50972
  interactivePasteFromOS(this.env, target, clipboardContent);
50698
50973
  }
50699
50974
  if (isCutOperation) {
@@ -52513,8 +52788,9 @@ stores.inject(MyMetaStore, storeInstance);
52513
52788
  if (replaceIndex > -1) {
52514
52789
  currentRanges = rules[replaceIndex].ranges.map(toUnboundedZone);
52515
52790
  }
52516
- currentRanges = currentRanges.concat(toAdd);
52517
- return recomputeZones(currentRanges, toRemove).map((zone) => this.getters.getRangeDataFromZone(sheetId, zone));
52791
+ // Remove the zones first in case the same position is in toAdd and toRemove
52792
+ const withRemovedZones = recomputeZones(currentRanges, toRemove);
52793
+ return recomputeZones([...toAdd, ...withRemovedZones], []).map((zone) => this.getters.getRangeDataFromZone(sheetId, zone));
52518
52794
  }
52519
52795
  // ---------------------------------------------------------------------------
52520
52796
  // Private
@@ -52618,8 +52894,6 @@ stores.inject(MyMetaStore, storeInstance);
52618
52894
  case "IconSetRule": {
52619
52895
  return this.checkValidations(rule, this.chainValidations(this.checkInflectionPoints(this.checkNaN), this.checkLowerBiggerThanUpper), this.chainValidations(this.checkInflectionPoints(this.checkFormulaCompilation)));
52620
52896
  }
52621
- case "DataBarRule":
52622
- return this.checkDataBarRangeValues(rule, cmd.ranges, cmd.sheetId);
52623
52897
  }
52624
52898
  return "Success" /* CommandResult.Success */;
52625
52899
  }
@@ -52750,18 +53024,6 @@ stores.inject(MyMetaStore, storeInstance);
52750
53024
  }
52751
53025
  return "Success" /* CommandResult.Success */;
52752
53026
  }
52753
- checkDataBarRangeValues(rule, ranges, sheetId) {
52754
- if (rule.rangeValues) {
52755
- const { numberOfCols, numberOfRows } = zoneToDimension(this.getters.getRangeFromSheetXC(sheetId, rule.rangeValues).zone);
52756
- for (const range of ranges) {
52757
- const dimensions = zoneToDimension(this.getters.getRangeFromRangeData(range).zone);
52758
- if (numberOfCols !== dimensions.numberOfCols || numberOfRows !== dimensions.numberOfRows) {
52759
- return "DataBarRangeValuesMismatch" /* CommandResult.DataBarRangeValuesMismatch */;
52760
- }
52761
- }
52762
- }
52763
- return "Success" /* CommandResult.Success */;
52764
- }
52765
53027
  removeConditionalFormatting(id, sheet) {
52766
53028
  const cfIndex = this.cfRules[sheet].findIndex((s) => s.id === id);
52767
53029
  if (cfIndex !== -1) {
@@ -52825,7 +53087,7 @@ stores.inject(MyMetaStore, storeInstance);
52825
53087
  allowDispatch(cmd) {
52826
53088
  switch (cmd.type) {
52827
53089
  case "ADD_DATA_VALIDATION_RULE":
52828
- return this.checkValidations(cmd, this.chainValidations(this.checkEmptyRange, this.checkCriterionTypeIsValid, this.checkCriterionHasValidNumberOfValues, this.checkCriterionValuesAreValid));
53090
+ return this.checkValidations(cmd, this.chainValidations(this.checkEmptyRange, this.checkValidRange, this.checkCriterionTypeIsValid, this.checkCriterionHasValidNumberOfValues, this.checkCriterionValuesAreValid));
52829
53091
  case "REMOVE_DATA_VALIDATION_RULE":
52830
53092
  if (!this.rules[cmd.sheetId].find((rule) => rule.id === cmd.id)) {
52831
53093
  return "UnknownDataValidationRule" /* CommandResult.UnknownDataValidationRule */;
@@ -52984,6 +53246,20 @@ stores.inject(MyMetaStore, storeInstance);
52984
53246
  }
52985
53247
  }
52986
53248
  }
53249
+ exportForExcel(data) {
53250
+ if (!data.sheets) {
53251
+ return;
53252
+ }
53253
+ for (const sheet of data.sheets) {
53254
+ sheet.dataValidationRules = [];
53255
+ for (const rule of this.rules[sheet.id]) {
53256
+ sheet.dataValidationRules.push({
53257
+ ...rule,
53258
+ ranges: rule.ranges.map((range) => this.getters.getRangeString(range, sheet.id, { useFixedReference: true })),
53259
+ });
53260
+ }
53261
+ }
53262
+ }
52987
53263
  checkCriterionTypeIsValid(cmd) {
52988
53264
  return dataValidationEvaluatorRegistry.contains(cmd.rule.criterion.type)
52989
53265
  ? "Success" /* CommandResult.Success */
@@ -53002,21 +53278,28 @@ stores.inject(MyMetaStore, storeInstance);
53002
53278
  checkCriterionValuesAreValid(cmd) {
53003
53279
  const criterion = cmd.rule.criterion;
53004
53280
  const evaluator = dataValidationEvaluatorRegistry.get(criterion.type);
53005
- if (criterion.values.some((value) => {
53006
- if (value.startsWith("=")) {
53007
- return evaluator.allowedValues === "onlyLiterals";
53008
- }
53009
- else if (evaluator.allowedValues === "onlyFormulas") {
53281
+ const isInvalid = (value) => {
53282
+ if (evaluator.allowedValues === "onlyFormulas" && !value.startsWith("=")) {
53010
53283
  return true;
53011
53284
  }
53012
- else {
53013
- return !evaluator.isCriterionValueValid(value);
53285
+ if (value.startsWith("=")) {
53286
+ return evaluator.allowedValues === "onlyLiterals" || compile(value).isBadExpression;
53014
53287
  }
53015
- })) {
53288
+ return !evaluator.isCriterionValueValid(value);
53289
+ };
53290
+ if (criterion.values.some(isInvalid)) {
53016
53291
  return "InvalidDataValidationCriterionValue" /* CommandResult.InvalidDataValidationCriterionValue */;
53017
53292
  }
53018
53293
  return "Success" /* CommandResult.Success */;
53019
53294
  }
53295
+ checkValidRange(cmd) {
53296
+ const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
53297
+ const stringRanges = ranges.map((range) => this.getters.getRangeString(range, cmd.sheetId));
53298
+ if (stringRanges.some((xc) => !this.getters.isRangeValid(xc))) {
53299
+ return "InvalidRange" /* CommandResult.InvalidRange */;
53300
+ }
53301
+ return "Success" /* CommandResult.Success */;
53302
+ }
53020
53303
  }
53021
53304
 
53022
53305
  class FigurePlugin extends CorePlugin {
@@ -54751,6 +55034,7 @@ stores.inject(MyMetaStore, storeInstance);
54751
55034
  formats: {},
54752
55035
  borders: {},
54753
55036
  conditionalFormats: [],
55037
+ dataValidationRules: [],
54754
55038
  figures: [],
54755
55039
  tables: [],
54756
55040
  areGridLinesVisible: sheet.areGridLinesVisible === undefined ? true : sheet.areGridLinesVisible,
@@ -58120,6 +58404,9 @@ stores.inject(MyMetaStore, storeInstance);
58120
58404
  }
58121
58405
  for (let i = 0; i < positions.length; ++i) {
58122
58406
  const position = positions[i];
58407
+ if (this.nextPositionsToUpdate.has(position)) {
58408
+ continue;
58409
+ }
58123
58410
  const evaluatedCell = this.computeCell(position);
58124
58411
  if (evaluatedCell !== EMPTY_CELL) {
58125
58412
  this.evaluatedCells.set(position, evaluatedCell);
@@ -58127,6 +58414,9 @@ stores.inject(MyMetaStore, storeInstance);
58127
58414
  }
58128
58415
  onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
58129
58416
  }
58417
+ if (currentIteration >= MAX_ITERATION) {
58418
+ console.warn("Maximum iteration reached while evaluating cells");
58419
+ }
58130
58420
  }
58131
58421
  computeCell(position) {
58132
58422
  const evaluation = this.evaluatedCells.get(position);
@@ -58161,7 +58451,6 @@ stores.inject(MyMetaStore, storeInstance);
58161
58451
  }
58162
58452
  finally {
58163
58453
  this.cellsBeingComputed.delete(cellId);
58164
- this.nextPositionsToUpdate.delete(position);
58165
58454
  }
58166
58455
  }
58167
58456
  computeAndSave(position) {
@@ -58197,6 +58486,7 @@ stores.inject(MyMetaStore, storeInstance);
58197
58486
  invalidatePositionsDependingOnSpread(sheetId, resultZone) {
58198
58487
  // the result matrix is split in 2 zones to exclude the array formula position
58199
58488
  const invalidatedPositions = this.formulaDependencies().getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
58489
+ invalidatedPositions.delete({ sheetId, col: resultZone.left, row: resultZone.top });
58200
58490
  this.nextPositionsToUpdate.addMany(invalidatedPositions);
58201
58491
  }
58202
58492
  assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
@@ -59138,8 +59428,12 @@ stores.inject(MyMetaStore, storeInstance);
59138
59428
  const zoneOfValues = rangeValues.zone;
59139
59429
  for (let row = zone.top; row <= zone.bottom; row++) {
59140
59430
  for (let col = zone.left; col <= zone.right; col++) {
59141
- const cell = this.getEvaluatedCellInZone(sheetId, zone, col, row, zoneOfValues);
59142
- if (cell.type !== CellValueType.number || cell.value <= 0) {
59431
+ const targetCol = col - zone.left + zoneOfValues.left;
59432
+ const targetRow = row - zone.top + zoneOfValues.top;
59433
+ const cell = this.getters.getEvaluatedCell({ sheetId, col: targetCol, row: targetRow });
59434
+ if (!isInside(targetCol, targetRow, zoneOfValues) ||
59435
+ cell.type !== CellValueType.number ||
59436
+ cell.value <= 0) {
59143
59437
  // values negatives or 0 are ignored
59144
59438
  continue;
59145
59439
  }
@@ -59152,11 +59446,6 @@ stores.inject(MyMetaStore, storeInstance);
59152
59446
  }
59153
59447
  }
59154
59448
  }
59155
- getEvaluatedCellInZone(sheetId, zone, col, row, targetZone) {
59156
- const targetCol = col - zone.left + targetZone.left;
59157
- const targetRow = row - zone.top + targetZone.top;
59158
- return this.getters.getEvaluatedCell({ sheetId, col: targetCol, row: targetRow });
59159
- }
59160
59449
  /** Compute the color scale for the given range and CF rule, and apply in in the given computedStyle object */
59161
59450
  applyColorScale(sheetId, range, rule, computedStyle) {
59162
59451
  const minValue = this.parsePoint(sheetId, range, rule.minimum, "min");
@@ -59320,25 +59609,11 @@ stores.inject(MyMetaStore, storeInstance);
59320
59609
  }
59321
59610
  switch (cmd.type) {
59322
59611
  case "ADD_DATA_VALIDATION_RULE":
59323
- const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
59324
- if (cmd.rule.criterion.type === "isBoolean") {
59325
- this.setContentToBooleanCells({ ...cmd.rule, ranges });
59326
- }
59327
- delete this.validationResults[cmd.sheetId];
59328
- break;
59329
59612
  case "REMOVE_DATA_VALIDATION_RULE":
59330
59613
  delete this.validationResults[cmd.sheetId];
59331
59614
  break;
59332
59615
  }
59333
59616
  }
59334
- setContentToBooleanCells(rule) {
59335
- for (const position of getCellPositionsInRanges(rule.ranges)) {
59336
- const evaluatedCell = this.getters.getEvaluatedCell(position);
59337
- if (evaluatedCell.type !== CellValueType.boolean) {
59338
- this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
59339
- }
59340
- }
59341
- }
59342
59617
  isDataValidationInvalid(cellPosition) {
59343
59618
  return !this.getValidationResultForCell(cellPosition).isValid;
59344
59619
  }
@@ -59354,9 +59629,10 @@ stores.inject(MyMetaStore, storeInstance);
59354
59629
  getDataValidationInvalidCriterionValueMessage(criterionType, value) {
59355
59630
  const evaluator = dataValidationEvaluatorRegistry.get(criterionType);
59356
59631
  if (value.startsWith("=")) {
59357
- return evaluator.allowedValues === "onlyLiterals"
59358
- ? _t("The value must not be a formula")
59359
- : undefined;
59632
+ if (evaluator.allowedValues === "onlyLiterals") {
59633
+ return _t("The value must not be a formula");
59634
+ }
59635
+ return this.isValidFormula(value) ? undefined : DVTerms.CriterionError.validFormula;
59360
59636
  }
59361
59637
  else if (evaluator.allowedValues === "onlyFormulas") {
59362
59638
  return _t("The value must be a formula");
@@ -59382,6 +59658,9 @@ stores.inject(MyMetaStore, storeInstance);
59382
59658
  const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
59383
59659
  return error ? { error, rule, isValid: false } : VALID_RESULT;
59384
59660
  }
59661
+ isValidFormula(value) {
59662
+ return !compile(value).isBadExpression;
59663
+ }
59385
59664
  getValidationResultForCell(cellPosition) {
59386
59665
  const { col, row, sheetId } = cellPosition;
59387
59666
  if (!this.validationResults[sheetId]) {
@@ -61817,7 +62096,7 @@ stores.inject(MyMetaStore, storeInstance);
61817
62096
  * Notify the server that the user client left the collaborative session
61818
62097
  */
61819
62098
  async leave(data) {
61820
- if (Object.keys(this.clients).length === 1 && this.processedRevisions.size) {
62099
+ if (data && Object.keys(this.clients).length === 1 && this.processedRevisions.size) {
61821
62100
  await this.snapshot(data());
61822
62101
  }
61823
62102
  delete this.clients[this.clientId];
@@ -62744,9 +63023,9 @@ stores.inject(MyMetaStore, storeInstance);
62744
63023
  switch (cmd.type) {
62745
63024
  case "SORT_CELLS":
62746
63025
  if (!isInside(cmd.col, cmd.row, cmd.zone)) {
62747
- throw new Error(_t("The anchor must be part of the provided zone"));
63026
+ return "InvalidSortAnchor" /* CommandResult.InvalidSortAnchor */;
62748
63027
  }
62749
- return this.checkValidations(cmd, this.checkMerge, this.checkMergeSizes);
63028
+ return this.checkValidations(cmd, this.checkMerge, this.checkMergeSizes, this.checkArrayFormulaInSortZone);
62750
63029
  }
62751
63030
  return "Success" /* CommandResult.Success */;
62752
63031
  }
@@ -62787,6 +63066,10 @@ stores.inject(MyMetaStore, storeInstance);
62787
63066
  }
62788
63067
  return "Success" /* CommandResult.Success */;
62789
63068
  }
63069
+ checkArrayFormulaInSortZone({ sheetId, zone }) {
63070
+ const arrayFormulaInZone = positions(zone).some(({ col, row }) => this.getters.getArrayFormulaSpreadingOn({ sheetId, col, row }));
63071
+ return arrayFormulaInZone ? "SortZoneWithArrayFormulas" /* CommandResult.SortZoneWithArrayFormulas */ : "Success" /* CommandResult.Success */;
63072
+ }
62790
63073
  /**
62791
63074
  * This function evaluates if the top row of a provided zone can be considered as a `header`
62792
63075
  * by checking the following criteria:
@@ -63369,6 +63652,42 @@ stores.inject(MyMetaStore, storeInstance);
63369
63652
  }
63370
63653
  }
63371
63654
 
63655
+ class DataValidationInsertionPlugin extends UIPlugin {
63656
+ handle(cmd) {
63657
+ switch (cmd.type) {
63658
+ case "ADD_DATA_VALIDATION_RULE":
63659
+ if (cmd.rule.criterion.type === "isBoolean") {
63660
+ const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
63661
+ for (const position of getCellPositionsInRanges(ranges)) {
63662
+ const cell = this.getters.getCell(position);
63663
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
63664
+ if (!cell?.content) {
63665
+ this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
63666
+ // In this case, a cell has been updated in the core plugin but
63667
+ // not yet evaluated. This can occur after a paste operation.
63668
+ }
63669
+ else if (cell?.content && evaluatedCell.type === CellValueType.empty) {
63670
+ let value;
63671
+ if (cell.content.startsWith("=")) {
63672
+ const result = this.getters.evaluateFormula(position.sheetId, cell.content);
63673
+ value = (isMatrix(result) ? result[0][0] : result)?.toString();
63674
+ }
63675
+ else {
63676
+ value = cell.content;
63677
+ }
63678
+ if (!value || !isBoolean(value)) {
63679
+ this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
63680
+ }
63681
+ }
63682
+ else if (evaluatedCell.type !== CellValueType.boolean) {
63683
+ this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
63684
+ }
63685
+ }
63686
+ }
63687
+ }
63688
+ }
63689
+ }
63690
+
63372
63691
  const genericRepeatsTransforms = [
63373
63692
  repeatSheetDependantCommand,
63374
63693
  repeatTargetDependantCommand,
@@ -64022,7 +64341,7 @@ stores.inject(MyMetaStore, storeInstance);
64022
64341
  const zones = this.getters.getSelectedZones();
64023
64342
  return this.isCutAllowedOn(zones);
64024
64343
  case "PASTE_FROM_OS_CLIPBOARD": {
64025
- const copiedData = this.convertOSClipboardData(cmd.clipboardContent[ClipboardMIMEType.PlainText] ?? "");
64344
+ const copiedData = this.convertTextToClipboardData(cmd.clipboardContent.text ?? "");
64026
64345
  const pasteOption = cmd.pasteOption;
64027
64346
  return this.isPasteAllowed(cmd.target, copiedData, { pasteOption, isCutOperation: false });
64028
64347
  }
@@ -64075,12 +64394,9 @@ stores.inject(MyMetaStore, storeInstance);
64075
64394
  break;
64076
64395
  case "PASTE_FROM_OS_CLIPBOARD": {
64077
64396
  this._isCutOperation = false;
64078
- if (cmd.clipboardContent[ClipboardMIMEType.OSpreadsheet]) {
64079
- this.copiedData = JSON.parse(cmd.clipboardContent[ClipboardMIMEType.OSpreadsheet]);
64080
- }
64081
- else {
64082
- this.copiedData = this.convertOSClipboardData(cmd.clipboardContent[ClipboardMIMEType.PlainText] ?? "");
64083
- }
64397
+ this.copiedData =
64398
+ cmd.clipboardContent.data ||
64399
+ this.convertTextToClipboardData(cmd.clipboardContent.text ?? "");
64084
64400
  const pasteOption = cmd.pasteOption;
64085
64401
  this.paste(cmd.target, this.copiedData, {
64086
64402
  pasteOption,
@@ -64211,11 +64527,11 @@ stores.inject(MyMetaStore, storeInstance);
64211
64527
  }
64212
64528
  }
64213
64529
  }
64214
- convertOSClipboardData(clipboardData) {
64530
+ convertTextToClipboardData(clipboardData) {
64215
64531
  const handlers = this.selectClipboardHandlers({ figureId: true }).concat(this.selectClipboardHandlers({}));
64216
64532
  let copiedData = {};
64217
64533
  for (const { handlerName, handler } of handlers) {
64218
- const data = handler.convertOSClipboardData(clipboardData);
64534
+ const data = handler.convertTextToClipboardData(clipboardData);
64219
64535
  copiedData[handlerName] = data;
64220
64536
  const minimalKeys = ["sheetId", "cells", "zones", "figureId"];
64221
64537
  for (const key of minimalKeys) {
@@ -64384,21 +64700,20 @@ stores.inject(MyMetaStore, storeInstance);
64384
64700
  return {
64385
64701
  [ClipboardMIMEType.PlainText]: this.getPlainTextContent(),
64386
64702
  [ClipboardMIMEType.Html]: this.getHTMLContent(),
64387
- [ClipboardMIMEType.OSpreadsheet]: this.getSerializedGridData(),
64388
64703
  };
64389
64704
  }
64390
- getSerializedGridData() {
64705
+ getSheetData() {
64391
64706
  const data = {
64392
64707
  version: CURRENT_VERSION,
64393
64708
  clipboardId: this.clipboardId,
64394
64709
  };
64395
64710
  if (this.copiedData && "figureId" in this.copiedData) {
64396
- return JSON.stringify(data);
64711
+ return data;
64397
64712
  }
64398
- return JSON.stringify({
64713
+ return {
64399
64714
  ...data,
64400
64715
  ...this.copiedData,
64401
- });
64716
+ };
64402
64717
  }
64403
64718
  getPlainTextContent() {
64404
64719
  if (!this.copiedData?.cells) {
@@ -64415,31 +64730,36 @@ stores.inject(MyMetaStore, storeInstance);
64415
64730
  .join("\n") || "\t");
64416
64731
  }
64417
64732
  getHTMLContent() {
64418
- if (!this.copiedData?.cells) {
64419
- return `<div data-clipboard-id="${this.clipboardId}">\t</div>`;
64733
+ let innerHTML = "";
64734
+ const cells = this.copiedData?.cells;
64735
+ if (!cells) {
64736
+ innerHTML = "\t";
64420
64737
  }
64421
- const cells = this.copiedData.cells;
64422
- if (cells.length === 1 && cells[0].length === 1) {
64423
- return `<div data-clipboard-id="${this.clipboardId}">${this.getters.getCellText(cells[0][0].position)}</div>`;
64738
+ else if (cells.length === 1 && cells[0].length === 1) {
64739
+ innerHTML = `${this.getters.getCellText(cells[0][0].position)}`;
64424
64740
  }
64425
- if (!cells[0][0]) {
64741
+ else if (!cells[0][0]) {
64426
64742
  return "";
64427
64743
  }
64428
- let htmlTable = `<div data-clipboard-id="${this.clipboardId}"><table border="1" style="border-collapse:collapse">`;
64429
- for (const row of cells) {
64430
- htmlTable += "<tr>";
64431
- for (const cell of row) {
64432
- if (!cell) {
64433
- continue;
64744
+ else {
64745
+ let htmlTable = `<table border="1" style="border-collapse:collapse">`;
64746
+ for (const row of cells) {
64747
+ htmlTable += "<tr>";
64748
+ for (const cell of row) {
64749
+ if (!cell) {
64750
+ continue;
64751
+ }
64752
+ const cssStyle = cssPropertiesToCss(cellStyleToCss(this.getters.getCellComputedStyle(cell.position)));
64753
+ const cellText = this.getters.getCellText(cell.position);
64754
+ htmlTable += `<td style="${cssStyle}">` + xmlEscape(cellText) + "</td>";
64434
64755
  }
64435
- const cssStyle = cssPropertiesToCss(cellStyleToCss(this.getters.getCellComputedStyle(cell.position)));
64436
- const cellText = this.getters.getCellText(cell.position);
64437
- htmlTable += `<td style="${cssStyle}">` + xmlEscape(cellText) + "</td>";
64756
+ htmlTable += "</tr>";
64438
64757
  }
64439
- htmlTable += "</tr>";
64758
+ htmlTable += "</table>";
64759
+ innerHTML = htmlTable;
64440
64760
  }
64441
- htmlTable += "</table></div>";
64442
- return htmlTable;
64761
+ const serializedData = JSON.stringify(this.getSheetData());
64762
+ return `<div data-osheet-clipboard='${xmlEscape(serializedData)}'>${innerHTML}</div>`;
64443
64763
  }
64444
64764
  isCutOperation() {
64445
64765
  return this._isCutOperation ?? false;
@@ -66419,7 +66739,8 @@ stores.inject(MyMetaStore, storeInstance);
66419
66739
  .add("history", HistoryPlugin)
66420
66740
  .add("data_cleanup", DataCleanupPlugin)
66421
66741
  .add("table_autofill", TableAutofillPlugin)
66422
- .add("table_ui_resize", TableResizeUI);
66742
+ .add("table_ui_resize", TableResizeUI)
66743
+ .add("datavalidation_insert", DataValidationInsertionPlugin);
66423
66744
  // Plugins which have a state, but which should not be shared in collaborative
66424
66745
  const statefulUIPluginRegistry = new Registry()
66425
66746
  .add("selection", GridSelectionPlugin)
@@ -67645,7 +67966,7 @@ stores.inject(MyMetaStore, storeInstance);
67645
67966
 
67646
67967
  .o-header-group-main-pane {
67647
67968
  &.o-group-rows {
67648
- margin-top: -2px; // Counteract o-header-group-frozen-pane-border offset
67969
+ margin-top: -2px; /* Counteract o-header-group-frozen-pane-border offset */
67649
67970
  }
67650
67971
  &.o-group-columns {
67651
67972
  margin-left: -2px;
@@ -68678,10 +68999,6 @@ stores.inject(MyMetaStore, storeInstance);
68678
68999
  [ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),
68679
69000
  [ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),
68680
69001
  };
68681
- const spreadsheetData = content[ClipboardMIMEType.OSpreadsheet];
68682
- if (spreadsheetData) {
68683
- clipboardItemData[ClipboardMIMEType.OSpreadsheet] = this.getBlob(content, ClipboardMIMEType.OSpreadsheet);
68684
- }
68685
69002
  return [new ClipboardItem(clipboardItemData)];
68686
69003
  }
68687
69004
  getBlob(clipboardContent, type) {
@@ -68723,7 +69040,7 @@ stores.inject(MyMetaStore, storeInstance);
68723
69040
  *:before,
68724
69041
  *:after {
68725
69042
  box-sizing: content-box;
68726
- /** rtl not supported ATM */
69043
+ /* rtl not supported ATM */
68727
69044
  direction: ltr;
68728
69045
  }
68729
69046
  .o-separator {
@@ -71628,6 +71945,124 @@ stores.inject(MyMetaStore, storeInstance);
71628
71945
  }
71629
71946
  }
71630
71947
 
71948
+ function addDataValidationRules(dataValidationRules) {
71949
+ const dvRulesCount = dataValidationRules.length;
71950
+ if (dvRulesCount === 0) {
71951
+ return [];
71952
+ }
71953
+ const dvNodes = [new XMLString(`<dataValidations count="${dvRulesCount}">`)];
71954
+ for (const dvRule of dataValidationRules) {
71955
+ switch (dvRule.criterion.type) {
71956
+ case "dateIs":
71957
+ case "dateIsBefore":
71958
+ case "dateIsOnOrBefore":
71959
+ case "dateIsAfter":
71960
+ case "dateIsOnOrAfter":
71961
+ case "dateIsBetween":
71962
+ case "dateIsNotBetween":
71963
+ dvNodes.push(addDateRule(dvRule));
71964
+ break;
71965
+ case "isEqual":
71966
+ case "isNotEqual":
71967
+ case "isGreaterThan":
71968
+ case "isGreaterOrEqualTo":
71969
+ case "isLessThan":
71970
+ case "isLessOrEqualTo":
71971
+ case "isBetween":
71972
+ case "isNotBetween":
71973
+ dvNodes.push(addDecimalRule(dvRule));
71974
+ break;
71975
+ case "isValueInRange":
71976
+ case "isValueInList":
71977
+ dvNodes.push(addListRule(dvRule));
71978
+ break;
71979
+ case "customFormula":
71980
+ dvNodes.push(addCustomFormulaRule(dvRule));
71981
+ break;
71982
+ default:
71983
+ console.warn(`Data validation ${dvRule.criterion.type} is not supported in xlsx.`);
71984
+ break;
71985
+ }
71986
+ }
71987
+ dvNodes.push(new XMLString("</dataValidations>"));
71988
+ return dvNodes;
71989
+ }
71990
+ function addDateRule(dvRule) {
71991
+ const rule = dvRule.criterion;
71992
+ const formula1 = adaptFormulaToExcel(rule.values[0]);
71993
+ const formula2 = rule.values[1] ? adaptFormulaToExcel(rule.values[1]) : undefined;
71994
+ const operator = convertDateCriterionTypeToExcelOperator(dvRule.criterion.type);
71995
+ const attributes = commonDataValidationAttributes(dvRule);
71996
+ attributes.push(["type", "date"], ["operator", operator]);
71997
+ if (formula2) {
71998
+ return escapeXml /*xml*/ `
71999
+ <dataValidation ${formatAttributes(attributes)}>
72000
+ <formula1>${toNumber(formula1, DEFAULT_LOCALE)}</formula1>
72001
+ <formula2>${toNumber(formula2, DEFAULT_LOCALE)}</formula2>
72002
+ </dataValidation>
72003
+ `;
72004
+ }
72005
+ return escapeXml /*xml*/ `
72006
+ <dataValidation ${formatAttributes(attributes)}>
72007
+ <formula1>${toNumber(formula1, DEFAULT_LOCALE)}</formula1>
72008
+ </dataValidation>
72009
+ `;
72010
+ }
72011
+ function addDecimalRule(dvRule) {
72012
+ const rule = dvRule.criterion;
72013
+ const formula1 = adaptFormulaToExcel(rule.values[0]);
72014
+ const formula2 = rule.values[1] ? adaptFormulaToExcel(rule.values[1]) : undefined;
72015
+ const operator = convertDecimalCriterionTypeToExcelOperator(dvRule.criterion.type);
72016
+ const attributes = commonDataValidationAttributes(dvRule);
72017
+ attributes.push(["type", "decimal"], ["operator", operator]);
72018
+ if (formula2) {
72019
+ return escapeXml /*xml*/ `
72020
+ <dataValidation ${formatAttributes(attributes)}>
72021
+ <formula1>${formula1}</formula1>
72022
+ <formula2>${formula2}</formula2>
72023
+ </dataValidation>
72024
+ `;
72025
+ }
72026
+ return escapeXml /*xml*/ `
72027
+ <dataValidation ${formatAttributes(attributes)}>
72028
+ <formula1>${formula1}</formula1>
72029
+ </dataValidation>
72030
+ `;
72031
+ }
72032
+ function addListRule(dvRule) {
72033
+ const rule = dvRule.criterion;
72034
+ const formula1 = dvRule.criterion.type === "isValueInRange"
72035
+ ? adaptFormulaToExcel(rule.values[0])
72036
+ : `"${rule.values.join(",")}"`;
72037
+ const attributes = commonDataValidationAttributes(dvRule);
72038
+ attributes.push(["type", "list"]);
72039
+ return escapeXml /*xml*/ `
72040
+ <dataValidation ${formatAttributes(attributes)}>
72041
+ <formula1>${formula1}</formula1>
72042
+ </dataValidation>
72043
+ `;
72044
+ }
72045
+ function addCustomFormulaRule(dvRule) {
72046
+ const rule = dvRule.criterion;
72047
+ const formula1 = adaptFormulaToExcel(rule.values[0]);
72048
+ const attributes = commonDataValidationAttributes(dvRule);
72049
+ attributes.push(["type", "custom"]);
72050
+ return escapeXml /*xml*/ `
72051
+ <dataValidation ${formatAttributes(attributes)}>
72052
+ <formula1>${formula1}</formula1>
72053
+ </dataValidation>
72054
+ `;
72055
+ }
72056
+ function commonDataValidationAttributes(dvRule) {
72057
+ return [
72058
+ ["allowBlank", "1"],
72059
+ ["showInputMessage", "1"],
72060
+ ["showErrorMessage", "1"],
72061
+ ["errorStyle", !dvRule.isBlocking ? "warning" : ""],
72062
+ ["sqref", dvRule.ranges.join(" ")],
72063
+ ];
72064
+ }
72065
+
71631
72066
  function createDrawing(drawingRelIds, sheet, figures) {
71632
72067
  const namespaces = [
71633
72068
  ["xmlns:xdr", NAMESPACE.drawing],
@@ -72412,6 +72847,7 @@ stores.inject(MyMetaStore, storeInstance);
72412
72847
  ${addRows(construct, data, sheet)}
72413
72848
  ${addMerges(sheet.merges)}
72414
72849
  ${joinXmlNodes(addConditionalFormatting(construct.dxfs, sheet.conditionalFormats))}
72850
+ ${joinXmlNodes(addDataValidationRules(sheet.dataValidationRules))}
72415
72851
  ${addHyperlinks(construct, data, sheetIndex)}
72416
72852
  ${drawingNode}
72417
72853
  ${tablesNode}
@@ -72735,7 +73171,8 @@ stores.inject(MyMetaStore, storeInstance);
72735
73171
  this.session.join(this.config.client);
72736
73172
  }
72737
73173
  async leaveSession() {
72738
- await this.session.leave(lazy(() => this.exportData()));
73174
+ const snapshot = this.getters.isReadonly() ? undefined : lazy(() => this.exportData());
73175
+ await this.session.leave(snapshot);
72739
73176
  }
72740
73177
  setupUiPlugin(Plugin) {
72741
73178
  const plugin = new Plugin(this.uiPluginConfig);
@@ -73234,6 +73671,9 @@ stores.inject(MyMetaStore, storeInstance);
73234
73671
  GaugeChartDesignPanel,
73235
73672
  ScorecardChartConfigPanel,
73236
73673
  ScorecardChartDesignPanel,
73674
+ RadarChartDesignPanel,
73675
+ WaterfallChartDesignPanel,
73676
+ ComboChartDesignPanel,
73237
73677
  ChartTypePicker,
73238
73678
  FigureComponent,
73239
73679
  Menu,
@@ -73287,6 +73727,7 @@ stores.inject(MyMetaStore, storeInstance);
73287
73727
  DEFAULT_LOCALE,
73288
73728
  HIGHLIGHT_COLOR,
73289
73729
  PIVOT_TABLE_CONFIG,
73730
+ ChartTerms,
73290
73731
  };
73291
73732
  const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
73292
73733
 
@@ -73337,9 +73778,9 @@ stores.inject(MyMetaStore, storeInstance);
73337
73778
  exports.tokenize = tokenize;
73338
73779
 
73339
73780
 
73340
- __info__.version = "18.1.0-alpha.4";
73341
- __info__.date = "2024-11-13T15:06:47.769Z";
73342
- __info__.hash = "e1ad985";
73781
+ __info__.version = "18.1.0-alpha.6";
73782
+ __info__.date = "2024-11-28T09:06:59.527Z";
73783
+ __info__.hash = "875c901";
73343
73784
 
73344
73785
 
73345
73786
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);