Skip to content

Commit

Permalink
[IMP] xlsx: support import/export of data validation rules
Browse files Browse the repository at this point in the history
This commit enhances XLSX handling capabilities by adding
support for importing and exporting data validation rules,
which helps in maintaining data integrity and consistency
when working with Excel files.

Task: 3637642
  • Loading branch information
Rachico committed Oct 14, 2024
1 parent 4a4399b commit 9012d6e
Show file tree
Hide file tree
Showing 21 changed files with 833 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/migrations/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export function createEmptySheet(sheetId: UID, name: string): SheetData {
rows: {},
merges: [],
conditionalFormats: [],
dataValidationRules: [],
figures: [],
tables: [],
isVisible: true,
Expand Down
18 changes: 18 additions & 0 deletions src/plugins/core/data_validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
CommandResult,
CoreCommand,
DataValidationRule,
ExcelWorkbookData,
Range,
Style,
UID,
Expand Down Expand Up @@ -263,6 +264,23 @@ export class DataValidationPlugin
}
}

exportForExcel(data: ExcelWorkbookData) {
if (!data.sheets) {
return;
}
for (const sheet of data.sheets) {
sheet.dataValidationRules = [];
for (const rule of this.rules[sheet.id]) {
sheet.dataValidationRules.push({
...rule,
ranges: rule.ranges.map((range) =>
this.getters.getRangeString(range, sheet.id, { useFixedReference: true })
),
});
}
}
}

private checkCriterionTypeIsValid(cmd: AddDataValidationCommand): CommandResult {
return dataValidationEvaluatorRegistry.contains(cmd.rule.criterion.type)
? CommandResult.Success
Expand Down
1 change: 1 addition & 0 deletions src/plugins/core/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ export class SheetPlugin extends CorePlugin<SheetState> implements SheetState {
formats: {},
borders: {},
conditionalFormats: [],
dataValidationRules: [],
figures: [],
tables: [],
areGridLinesVisible:
Expand Down
2 changes: 1 addition & 1 deletion src/types/workbook_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface SheetData {
cols: { [key: number]: HeaderData };
rows: { [key: number]: HeaderData };
conditionalFormats: ConditionalFormat[];
dataValidationRules?: DataValidationRuleData[];
dataValidationRules: DataValidationRuleData[];
tables: TableData[];
areGridLinesVisible?: boolean;
isVisible: boolean;
Expand Down
52 changes: 52 additions & 0 deletions src/types/xlsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { ExcelFigureSize } from "./figure";
* - table column (XLSXTableCol) : §18.5.1.3 (tableColumn)
* - table style (XLSXTableStyleInfo) : §18.5.1.5 (tableStyleInfo)
* - theme color : §20.1.2.3.32 (srgbClr/sysClr)
* - data validation (XLSXDataValidation): §18.3.1.33 (dataValidations)
*
* [XLSX]: :
* - cf rule (XLSXCfRule): §2.6.2 (CT_ConditionalFormatting)
Expand Down Expand Up @@ -229,6 +230,7 @@ export interface XLSXWorksheet {
cols: XLSXColumn[];
rows: XLSXRow[];
cfs: XLSXConditionalFormat[];
dataValidations: XLSXDataValidation[];
sharedFormulas: string[];
merges: string[];
figures: XLSXFigure[];
Expand Down Expand Up @@ -442,6 +444,40 @@ export type XLSXCfOperatorType =
| "notContains"
| "notEqual";

export type XLSXDataValidationOperatorType =
| "between"
| "notBetween"
| "equal"
| "notEqual"
| "greaterThan"
| "lessThan"
| "greaterThanOrEqual"
| "lessThanOrEqual";

export type XLSXDataValidationCompatibleDecimalCriterionType =
| "isBetween"
| "isNotBetween"
| "isEqual"
| "isNotEqual"
| "isGreaterThan"
| "isGreaterOrEqualTo"
| "isLessThan"
| "isLessOrEqualTo";

export type XLSXDataValidationDateOperatorType = Exclude<
XLSXDataValidationOperatorType,
"notEqual"
>;

export type XLSXDataValidationCompatibleDateCriterionType =
| "dateIsBetween"
| "dateIsNotBetween"
| "dateIs"
| "dateIsAfter"
| "dateIsOnOrAfter"
| "dateIsBefore"
| "dateIsOnOrBefore";

export type XLSXHorizontalAlignment =
| "general"
| "left"
Expand Down Expand Up @@ -506,6 +542,22 @@ export interface XLSXCfRule {
equalAverage?: boolean;
}

export interface XLSXDataValidation {
type: string;
operator: XLSXDataValidationOperatorType;
sqref: string[];
formula1: string;
formula2?: string;
errorStyle?: string;
showErrorMessage?: boolean;
errorTitle?: string;
error?: string;
showInputMessage?: boolean;
promptTitle?: string;
prompt?: string;
allowBlank?: boolean;
}

export interface XLSXSharedFormula {
formula: string;
refCellXc: string;
Expand Down
31 changes: 31 additions & 0 deletions src/xlsx/conversion/conversion_maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
XLSXCfType,
XLSXCfValueObjectType,
XLSXChartType,
XLSXDataValidationCompatibleDateCriterionType,
XLSXDataValidationCompatibleDecimalCriterionType,
XLSXDataValidationDateOperatorType,
XLSXDataValidationOperatorType,
XLSXHorizontalAlignment,
} from "../../types/xlsx";
import { XLSXVerticalAlignment } from "./../../types/xlsx";
Expand Down Expand Up @@ -392,3 +396,30 @@ export const IMAGE_EXTENSION_TO_MIMETYPE_MAPPING = {
webp: "image/webp",
jpg: "image/jpeg",
};

export const XLSX_DV_DECIMAL_OPERATOR_TO_DV_TYPE_MAPPING: Record<
XLSXDataValidationOperatorType,
XLSXDataValidationCompatibleDecimalCriterionType
> = {
between: "isBetween",
notBetween: "isNotBetween",
equal: "isEqual",
notEqual: "isNotEqual",
greaterThan: "isGreaterThan",
greaterThanOrEqual: "isGreaterOrEqualTo",
lessThan: "isLessThan",
lessThanOrEqual: "isLessOrEqualTo",
};

export const XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING: Record<
XLSXDataValidationDateOperatorType,
XLSXDataValidationCompatibleDateCriterionType
> = {
between: "dateIsBetween",
notBetween: "dateIsNotBetween",
equal: "dateIs",
greaterThan: "dateIsAfter",
greaterThanOrEqual: "dateIsOnOrAfter",
lessThan: "dateIsBefore",
lessThanOrEqual: "dateIsOnOrBefore",
};
124 changes: 124 additions & 0 deletions src/xlsx/conversion/data_validation_conversion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { DataValidationRuleData } from "../../types";
import { XLSXDataValidation } from "../../types/xlsx";
import { WarningTypes, XLSXImportWarningManager } from "../helpers/xlsx_parser_error_manager";
import {
XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING,
XLSX_DV_DECIMAL_OPERATOR_TO_DV_TYPE_MAPPING,
} from "./conversion_maps";

export function convertDataValidationRules(
xlsxDataValidations: XLSXDataValidation[],
warningManager: XLSXImportWarningManager
): DataValidationRuleData[] {
const dvRules: DataValidationRuleData[] = [];
let dvId = 1;
for (const dv of xlsxDataValidations) {
if (!dv) {
continue;
}
switch (dv.type) {
case "time":
warningManager.generateNotSupportedWarning(WarningTypes.TimeDataValidationNotSupported);
break;
case "textLength":
warningManager.generateNotSupportedWarning(
WarningTypes.TextLengthDataValidationNotSupported
);
break;
case "whole":
warningManager.generateNotSupportedWarning(
WarningTypes.WholeNumberDataValidationNotSupported
);
break;
case "decimal":
const decimalRule = convertDecimalRule(dvId++, dv);
dvRules.push(decimalRule);
break;
case "list":
const listRule = convertListrule(dvId++, dv);
dvRules.push(listRule);
break;
case "date":
if (dv.operator === "notEqual") {
warningManager.generateNotSupportedWarning(
WarningTypes.NotEqualDateDataValidationNotSupported
);
break;
}
const dateRule = convertDateRule(dvId++, dv);
dvRules.push(dateRule);
break;
case "custom":
const customRule = convertCustomRule(dvId++, dv);
dvRules.push(customRule);
break;
}
}
return dvRules;
}

function convertDecimalRule(id: number, dv: XLSXDataValidation): DataValidationRuleData {
const values = [dv.formula1.toString()];
if (dv.formula2) {
values.push(dv.formula2.toString());
}
return {
id: id.toString(),
ranges: dv.sqref,
isBlocking: dv.errorStyle !== "warning",
criterion: {
type: XLSX_DV_DECIMAL_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator] ?? "isBetween",
values,
},
};
}

function convertListrule(id: number, dv: XLSXDataValidation): DataValidationRuleData {
const values = [dv.formula1.toString()];
if (dv.formula2) {
values.push(dv.formula2.toString());
}
return {
id: id.toString(),
ranges: dv.sqref,
isBlocking: dv.errorStyle !== "warning",
criterion: {
type: "isValueInRange",
values: dv.formula1.split(","),
displayStyle: "arrow",
},
};
}

function convertDateRule(id: number, dv: XLSXDataValidation): DataValidationRuleData {
const values = [dv.formula1.toString()];
if (dv.formula2 && dv.formula2 !== "undefined") {
values.push(dv.formula2.toString());
}
let criterion = {
type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator] ?? "dateIsBetween",
values: values,
};
if (!dv.formula2 || dv.formula2 === "undefined") {
// @ts-ignore
criterion.dateValue = "exactDate";
}
return {
id: id.toString(),
ranges: dv.sqref,
isBlocking: dv.errorStyle !== "warning",
criterion: criterion,
};
}

function convertCustomRule(id: number, dv: XLSXDataValidation): DataValidationRuleData {
return {
id: id.toString(),
ranges: dv.sqref,
isBlocking: dv.errorStyle !== "warning",
criterion: {
type: "customFormula",
values: [`=${dv.formula1.toString()}`],
},
};
}
2 changes: 2 additions & 0 deletions src/xlsx/conversion/sheet_conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { convertHeightFromExcel, convertWidthFromExcel } from "../helpers/conten
import { WarningTypes, XLSXImportWarningManager } from "../helpers/xlsx_parser_error_manager";
import { convertConditionalFormats } from "./cf_conversion";
import { convertColor } from "./color_conversion";
import { convertDataValidationRules } from "./data_validation_conversion";
import { convertFigures } from "./figure_conversion";
import { convertFormulasContent } from "./formula_conversion";

Expand Down Expand Up @@ -52,6 +53,7 @@ export function convertSheets(
cols: convertCols(sheet, sheetDims[0], colHeaderGroups),
rows: convertRows(sheet, sheetDims[1], rowHeaderGroups),
conditionalFormats: convertConditionalFormats(sheet.cfs, data.dxfs, warningManager),
dataValidationRules: convertDataValidationRules(sheet.dataValidations, warningManager),
figures: convertFigures(sheet),
isVisible: sheet.isVisible,
panes: sheetOptions
Expand Down
59 changes: 59 additions & 0 deletions src/xlsx/extraction/data_validation_extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
XLSXDataValidation,
XLSXDataValidationOperatorType,
XLSXFileStructure,
XLSXImportFile,
XLSXTheme,
} from "../../types/xlsx";
import { XLSXImportWarningManager } from "../helpers/xlsx_parser_error_manager";
import { XlsxBaseExtractor } from "./base_extractor";

export class XlsxDataValidationExtractor extends XlsxBaseExtractor {
theme?: XLSXTheme;

constructor(
sheetFile: XLSXImportFile,
xlsxStructure: XLSXFileStructure,
warningManager: XLSXImportWarningManager,
theme: XLSXTheme | undefined
) {
super(sheetFile, xlsxStructure, warningManager);
this.theme = theme;
}

public extractDataValidations(): XLSXDataValidation[] {
const dataValidations = this.mapOnElements(
{ parent: this.rootFile.file.xml, query: "worksheet > dataValidations > dataValidation" },
(dvElement): XLSXDataValidation => {
return {
type: this.extractAttr(dvElement, "type", { required: true }).asString(),
operator: this.extractAttr(
dvElement,
"operator"
)?.asString() as XLSXDataValidationOperatorType,
sqref: this.extractAttr(dvElement, "sqref", { required: true }).asString().split(" "),
errorStyle: this.extractAttr(dvElement, "errorStyle")?.asString(),
formula1: this.extractDataValidationFormula(dvElement, 1)[0],
formula2: this.extractDataValidationFormula(dvElement, 2)[0],
showErrorMessage: this.extractAttr(dvElement, "showErrorMessage")?.asBool(),
errorTitle: this.extractAttr(dvElement, "errorTitle")?.asString(),
error: this.extractAttr(dvElement, "error")?.asString(),
showInputMessage: this.extractAttr(dvElement, "showInputMessage")?.asBool(),
promptTitle: this.extractAttr(dvElement, "promptTitle")?.asString(),
prompt: this.extractAttr(dvElement, "prompt")?.asString(),
allowBlank: this.extractAttr(dvElement, "allowBlank")?.asBool(),
};
}
);
return dataValidations;
}

private extractDataValidationFormula(dvElement: Element, index: number): string[] {
return this.mapOnElements(
{ parent: dvElement, query: `formula${index}` },
(cfFormulaElements): string => {
return this.extractTextContent(cfFormulaElements, { required: true });
}
);
}
}
Loading

0 comments on commit 9012d6e

Please sign in to comment.