refactor(data-validation): data validation validator (#4141)

This commit is contained in:
zhangw
2024-11-26 16:52:08 +08:00
committed by GitHub
parent ae027eb98f
commit ace8f93ba9
28 changed files with 205 additions and 480 deletions
+1 -1
View File
@@ -190,7 +190,7 @@ export * from './types/interfaces';
export * from './types/interfaces';
export type { ICellCustomRender, ICellRenderContext } from './types/interfaces/i-cell-custom-render';
export type { IDataValidationRule, IDataValidationRuleBase, IDataValidationRuleInfo, IDataValidationRuleOptions, ISheetDataValidationRule } from './types/interfaces/i-data-validation';
export { type IRTreeItem, RTree } from './shared/r-tree';
export { type BBox, type IRTreeItem, RBush, RTree } from './shared/r-tree';
export { type IUniverConfig, Univer } from './univer';
export { isNodeEnv } from './shared/tools';
+2
View File
@@ -361,3 +361,5 @@ export class RTree {
}
}
}
export { type BBox, RBush };
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import type { DataValidationOperator } from '../enum/data-validation-operator';
import type { DataValidationType } from '../enum/data-validation-type';
import type { DataValidationImeMode } from '../enum/data-validation-ime-mode';
import type { DataValidationErrorStyle } from '../enum/data-validation-error-style';
import type { DataValidationRenderMode } from '../enum/data-validation-render-mode';
import type { IRange } from '../../sheets/typedef';
import type { DataValidationErrorStyle } from '../enum/data-validation-error-style';
import type { DataValidationImeMode } from '../enum/data-validation-ime-mode';
import type { DataValidationOperator } from '../enum/data-validation-operator';
import type { DataValidationRenderMode } from '../enum/data-validation-render-mode';
import type { DataValidationType } from '../enum/data-validation-type';
// TODO@weird94: should be moved outside of the or package
@@ -27,7 +27,7 @@ export interface IDataValidationRuleBase {
/**
* data validation type
*/
type: DataValidationType;
type: DataValidationType | string;
allowBlank?: boolean;
/**
* data validation creteria
@@ -57,7 +57,7 @@ export class DataValidatorRegistryService {
}
}
register(validator: BaseDataValidator<any>) {
register(validator: BaseDataValidator) {
this._validatorMap.set(validator.id, validator);
if (Array.isArray(validator.scopes)) {
@@ -59,12 +59,14 @@ export interface IFormulaValidResult {
formula2?: string;
}
export abstract class BaseDataValidator<DataType = CellValue> {
export abstract class BaseDataValidator {
abstract id: string;
abstract title: string;
abstract operators: DataValidationOperator[];
abstract scopes: string[] | string;
offsetFormulaByRange = true;
// #region UI related
formulaInput: string | undefined = undefined;
@@ -127,7 +129,7 @@ export abstract class BaseDataValidator<DataType = CellValue> {
return false;
}
abstract parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult>;
abstract parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult<number | undefined>>;
abstract validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult;
@@ -142,41 +144,82 @@ export abstract class BaseDataValidator<DataType = CellValue> {
return true;
};
transform(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): IValidatorCellInfo<DataType> {
return cellInfo as IValidatorCellInfo<DataType>;
transform(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): IValidatorCellInfo<number> {
return cellInfo as IValidatorCellInfo<number>;
};
async validatorIsEqual(cellInfo: IValidatorCellInfo<DataType>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
};
async validatorIsEqual(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule) {
const { formula1 } = formula;
const { value: cellValue } = cellInfo;
if (Number.isNaN(formula1)) {
return true;
}
async validatorIsNotEqual(cellInfo: IValidatorCellInfo<DataType>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
};
return cellValue === formula1;
}
async validatorIsBetween(cellInfo: IValidatorCellInfo<DataType>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
};
async validatorIsNotEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
async validatorIsNotBetween(cellInfo: IValidatorCellInfo<DataType>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
};
return cellInfo.value !== formula1;
}
async validatorIsGreaterThan(cellInfo: IValidatorCellInfo<DataType>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
};
async validatorIsBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1, formula2 } = formula;
if (Number.isNaN(formula1) || Number.isNaN(formula2)) {
return true;
}
async validatorIsGreaterThanOrEqual(cellInfo: IValidatorCellInfo<DataType>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
};
const start = Math.min(formula1, formula2);
const end = Math.max(formula1, formula2);
return cellInfo.value >= start && cellInfo.value <= end;
}
async validatorIsLessThan(cellInfo: IValidatorCellInfo<DataType>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
};
async validatorIsNotBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1, formula2 } = formula;
if (Number.isNaN(formula1) || Number.isNaN(formula2)) {
return true;
}
const start = Math.min(formula1, formula2);
const end = Math.max(formula1, formula2);
return cellInfo.value < start || cellInfo.value > end;
}
async validatorIsLessThanOrEqual(cellInfo: IValidatorCellInfo<DataType>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
};
async validatorIsGreaterThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value > formula1;
}
async validatorIsGreaterThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value >= formula1;
}
async validatorIsLessThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value < formula1;
}
async validatorIsLessThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value <= formula1;
}
async validator(cellInfo: IValidatorCellInfo, rule: IDataValidationRule): Promise<boolean> {
const { value: cellValue, unitId, subUnitId } = cellInfo;
@@ -101,9 +101,6 @@ export class DataValidationAutoFillController extends Disposable {
redos: redoMutations,
};
};
const disabledDataVallation = [
DataValidationType.CHECKBOX,
];
const hook: ISheetAutoFillHook = {
id: DATA_VALIDATION_PLUGIN_NAME,
onBeforeFillData: (location) => {
@@ -111,7 +108,7 @@ export class DataValidationAutoFillController extends Disposable {
for (const row of sourceRange.rows) {
for (const col of sourceRange.cols) {
const dv = this._sheetDataValidationModel.getRuleByLocation(unitId, subUnitId, row, col);
if (dv && disabledDataVallation.indexOf(dv.type) > -1) {
if (dv && dv.type === DataValidationType.CHECKBOX) {
this._autoFillService.setDisableApplyType(APPLY_TYPE.SERIES, true);
return;
}
@@ -68,7 +68,6 @@
"rxjs": ">=7.0.0"
},
"dependencies": {
"@flatten-js/interval-tree": "^1.1.3",
"@univerjs/core": "workspace:*",
"@univerjs/data-validation": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
@@ -23,7 +23,7 @@ import { AddDataValidationMutation, DataValidatorRegistryService, getRuleOptions
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { getSheetCommandTarget, SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '@univerjs/sheets';
import { SheetDataValidationModel } from '../../models/sheet-data-validation-model';
import { isCustomFormulaType } from '../../utils/formula';
import { shouldOffsetFormulaByRange } from '../../utils/formula';
import { getStringCellValue } from '../../utils/get-cell-data-origin';
import { CHECKBOX_FORMULA_1, CHECKBOX_FORMULA_2, type CheckboxValidator } from '../../validators';
@@ -57,6 +57,7 @@ export function getDataValidationDiffMutations(
fillDefaultValue = true
) {
const lexerTreeBuilder = accessor.get(LexerTreeBuilder);
const validatorRegistryService = accessor.get(DataValidatorRegistryService);
const redoMutations: IMutationInfo[] = [];
const undoMutations: IMutationInfo[] = [];
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
@@ -115,7 +116,7 @@ export function getDataValidationDiffMutations(
});
break;
case 'update': {
if (isCustomFormulaType(diff.rule.type)) {
if (shouldOffsetFormulaByRange(diff.rule.type, validatorRegistryService)) {
const originRow = diff.oldRanges[0].startRow;
const originColumn = diff.oldRanges[0].startColumn;
const newRow = diff.newRanges[0].startRow;
@@ -17,17 +17,18 @@
import type { IDisposable, IMutationInfo, ISheetDataValidationRule } from '@univerjs/core';
import type { IUpdateDataValidationMutationParams } from '@univerjs/data-validation';
import { Disposable, generateRandomId, Inject, toDisposable } from '@univerjs/core';
import { AddDataValidationMutation, RemoveDataValidationMutation, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
import { AddDataValidationMutation, DataValidatorRegistryService, RemoveDataValidationMutation, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
import { FormulaRefRangeService } from '@univerjs/sheets-formula';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
import { isCustomFormulaType } from '../utils/formula';
import { shouldOffsetFormulaByRange } from '../utils/formula';
export class DataValidationFormulaRefRangeController extends Disposable {
private _disposableMap: Map<string, IDisposable> = new Map();
constructor(
@Inject(SheetDataValidationModel) private _dataValidationModel: SheetDataValidationModel,
@Inject(FormulaRefRangeService) private _formulaRefRangeService: FormulaRefRangeService
@Inject(FormulaRefRangeService) private _formulaRefRangeService: FormulaRefRangeService,
@Inject(DataValidatorRegistryService) private _validatorRegistryService: DataValidatorRegistryService
) {
super();
this._initRefRange();
@@ -38,7 +39,7 @@ export class DataValidationFormulaRefRangeController extends Disposable {
}
registerRule = (unitId: string, subUnitId: string, rule: ISheetDataValidationRule) => {
if (!isCustomFormulaType(rule.type)) {
if (!shouldOffsetFormulaByRange(rule.type, this._validatorRegistryService)) {
return;
}
@@ -18,14 +18,13 @@ import type { ISheetDataValidationRule } from '@univerjs/core';
import type { IRemoveDataValidationMutationParams, IUpdateDataValidationMutationParams } from '@univerjs/data-validation';
import type { EffectRefRangeParams } from '@univerjs/sheets';
import { Disposable, Inject, Injector, isRangesEqual, toDisposable } from '@univerjs/core';
import { RemoveDataValidationMutation, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
import { DataValidatorRegistryService, RemoveDataValidationMutation, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
import { handleCommonDefaultRangeChangeWithEffectRefCommands, RefRangeService } from '@univerjs/sheets';
import { FormulaRefRangeService } from '@univerjs/sheets-formula';
import { removeDataValidationUndoFactory } from '../commands/commands/data-validation.command';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { isCustomFormulaType } from '../utils/formula';
import { shouldOffsetFormulaByRange } from '../utils/formula';
export class DataValidationRefRangeController extends Disposable {
private _disposableMap: Map<string, Set<() => void>> = new Map();
@@ -34,9 +33,9 @@ export class DataValidationRefRangeController extends Disposable {
@Inject(SheetDataValidationModel) private _dataValidationModel: SheetDataValidationModel,
@Inject(Injector) private _injector: Injector,
@Inject(RefRangeService) private _refRangeService: RefRangeService,
@Inject(DataValidationCustomFormulaService) private _dataValidationCustomFormulaService: DataValidationCustomFormulaService,
@Inject(DataValidationFormulaService) private _dataValidationFormulaService: DataValidationFormulaService,
@Inject(FormulaRefRangeService) private _formulaRefRangeService: FormulaRefRangeService
@Inject(FormulaRefRangeService) private _formulaRefRangeService: FormulaRefRangeService,
@Inject(DataValidatorRegistryService) private _validatorRegistryService: DataValidatorRegistryService
) {
super();
this._initRefRange();
@@ -47,7 +46,7 @@ export class DataValidationRefRangeController extends Disposable {
}
registerRule = (unitId: string, subUnitId: string, rule: ISheetDataValidationRule) => {
if (isCustomFormulaType(rule.type)) {
if (shouldOffsetFormulaByRange(rule.type, this._validatorRegistryService)) {
return;
}
this.register(unitId, subUnitId, rule);
@@ -73,7 +73,7 @@ export class FDataValidationBuilder {
*
* @returns {DataValidationType} The data validation type
*/
getCriteriaType(): DataValidationType {
getCriteriaType(): DataValidationType | string {
return this._rule.type;
}
@@ -438,7 +438,7 @@ export class FDataValidationBuilder {
* operator is a DataValidationOperator enum value, formula1 is the first formula, and formula2 is the second formula.
* @return The current instance of the FDataValidationBuilder class, allowing for method chaining.
*/
withCriteriaValues(type: DataValidationType, values: [DataValidationOperator, string, string]): this {
withCriteriaValues(type: DataValidationType | string, values: [DataValidationOperator, string, string]): this {
this._rule.type = type;
this._rule.operator = values[0];
this._rule.formula1 = values[1];
@@ -46,7 +46,7 @@ export class FDataValidation {
*
* @returns The data validation type
*/
getCriteriaType(): DataValidationType {
getCriteriaType(): DataValidationType | string {
return this.rule.type;
};
@@ -14,14 +14,13 @@
* limitations under the License.
*/
import type { IRange, ISheetDataValidationRule, IUniverInstanceService, Workbook } from '@univerjs/core';
import IntervalTree from '@flatten-js/interval-tree';
import { debounce, Range, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
import type { BBox, IRange, ISheetDataValidationRule, IUniverInstanceService, Workbook } from '@univerjs/core';
interface IRuleItem {
import { debounce, Range, RBush, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
interface IRuleItem extends BBox {
ruleId: string;
startRow: number;
endRow: number;
}
export type RangeMutation = {
@@ -41,7 +40,7 @@ export type RangeMutation = {
export class RuleMatrix {
private _map: Map<string, IRange[]>;
private _tree = new Map<number, IntervalTree<string>>();
private _tree = new RBush<IRuleItem>();
private _dirty = true;
constructor(
@@ -59,33 +58,20 @@ export class RuleMatrix {
if (!this._dirty || this._disableTree) {
return;
}
const map = new Map<number, IRuleItem[]>();
this._tree.clear();
const items: IRuleItem[] = [];
this._map.forEach((ranges, ruleId) => {
ranges.forEach((range) => {
for (let col = range.startColumn; col <= range.endColumn; col++) {
let items = map.get(col);
if (!items) {
items = [];
map.set(col, items);
}
items.push({
startRow: range.startRow,
endRow: range.endRow,
ruleId,
});
}
items.push({
minX: range.startRow,
maxX: range.endRow,
minY: range.startColumn,
maxY: range.endColumn,
ruleId,
});
});
});
const treeMap = new Map<number, IntervalTree<string>>();
map.forEach((items, col) => {
const tree = new IntervalTree<string>();
items.forEach((item) => {
tree.insert([item.startRow, item.endRow], item.ruleId);
});
treeMap.set(col, tree);
});
this._tree = treeMap;
this._tree.load(items);
this._dirty = false;
};
@@ -95,13 +81,12 @@ export class RuleMatrix {
return this._univerInstanceService.getUnit<Workbook>(this._unitId, UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(this._subUnitId);
}
addRule(rule: ISheetDataValidationRule) {
private _addRule(ruleId: string, _ranges: IRange[]) {
if (!this._worksheet) {
return;
}
const ruleId = rule.uid;
const ranges = rule.ranges.map((range) => Range.transformRange(range, this._worksheet!));
const ranges = _ranges.map((range) => Range.transformRange(range, this._worksheet!));
this._map.forEach((value, key) => {
const newRanges = Rectangle.subtractMulti(value, ranges);
@@ -117,6 +102,10 @@ export class RuleMatrix {
this._debonceBuildTree();
}
addRule(rule: ISheetDataValidationRule) {
this._addRule(rule.uid, rule.ranges);
}
removeRange(_ranges: IRange[]) {
if (!this._worksheet) {
return;
@@ -134,30 +123,19 @@ export class RuleMatrix {
this._debonceBuildTree();
}
removeRule(rule: ISheetDataValidationRule) {
this._map.delete(rule.uid);
private _removeRule(ruleId: string) {
this._map.delete(ruleId);
this._dirty = true;
this._debonceBuildTree();
}
updateRange(ruleId: string, _newRanges: IRange[]) {
if (!this._worksheet) {
return;
}
this._map.delete(ruleId);
const ranges = _newRanges.map((range) => Range.transformRange(range, this._worksheet!));
removeRule(rule: ISheetDataValidationRule) {
this._removeRule(rule.uid);
}
this._map.forEach((value, key) => {
const newRanges = Rectangle.subtractMulti(value, ranges);
if (newRanges.length === 0) {
this._map.delete(key);
} else {
this._map.set(key, newRanges);
}
});
this._map.set(ruleId, ranges);
this._dirty = true;
this._debonceBuildTree();
updateRange(ruleId: string, _newRanges: IRange[]) {
this._removeRule(ruleId);
this._addRule(ruleId, _newRanges);
}
addRangeRules(rules: { id: string;ranges: IRange[] }[]) {
@@ -277,11 +255,13 @@ export class RuleMatrix {
if (this._dirty) {
this._buildTree();
}
const tree = this._tree.get(col);
if (!tree) {
return undefined;
}
const result = tree.search([row, row]);
return result.length > 0 ? result[0] : undefined;
const result = this._tree.search({
minX: row,
maxX: row,
minY: col,
maxY: col,
});
return result.length > 0 ? result[0].ruleId : undefined;
}
}
@@ -182,7 +182,7 @@ export class SheetDataValidationModel extends Disposable {
this._dataValidationCustomFormulaService.deleteByRuleId(unitId, subUnitId, oldRule.uid);
}
getValidator(type: DataValidationType) {
getValidator(type: DataValidationType | string) {
return this._dataValidatorRegistryService.getValidatorItem(type);
}
@@ -15,8 +15,9 @@
*/
import type { DataValidationStatus, IRange, ISheetDataValidationRule, Nullable } from '@univerjs/core';
import { Disposable, ICommandService, Inject, ObjectMatrix, Range } from '@univerjs/core';
import { type ISetRangeValuesMutationParams, SetRangeValuesMutation } from '@univerjs/sheets';
import type { IRemoveSheetMutationParams, ISetRangeValuesMutationParams } from '@univerjs/sheets';
import { Disposable, ICommandService, Inject, IUniverInstanceService, ObjectMatrix, Range, UniverInstanceType } from '@univerjs/core';
import { RemoveSheetMutation, SetRangeValuesMutation } from '@univerjs/sheets';
import { Subject } from 'rxjs';
export class DataValidationCacheService extends Disposable {
@@ -26,10 +27,12 @@ export class DataValidationCacheService extends Disposable {
readonly dirtyRanges$ = this._dirtyRanges$.asObservable();
constructor(
@Inject(ICommandService) private readonly _commandService: ICommandService
@Inject(ICommandService) private readonly _commandService: ICommandService,
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this._initDirtyRanges();
this._initSheetRemove();
}
private _initDirtyRanges() {
@@ -45,6 +48,21 @@ export class DataValidationCacheService extends Disposable {
}));
}
private _initSheetRemove() {
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === RemoveSheetMutation.id) {
const { unitId, subUnitId } = commandInfo.params as IRemoveSheetMutationParams;
this._cacheMatrix.get(unitId)?.delete(subUnitId);
}
}));
this.disposeWithMe(this._univerInstanceService.unitDisposed$.subscribe((univerInstance) => {
if (univerInstance.type === UniverInstanceType.UNIVER_SHEET) {
this._cacheMatrix.delete(univerInstance.getUnitId());
}
}));
}
private _ensureCache(unitId: string, subUnitId: string) {
let unitMap = this._cacheMatrix.get(unitId);
@@ -86,12 +104,6 @@ export class DataValidationCacheService extends Disposable {
this._dirtyRanges$.next({ unitId, subUnitId, ranges });
}
markCellDirty(unitId: string, subUnitId: string, row: number, col: number) {
const cache = this._ensureCache(unitId, subUnitId);
cache.setValue(row, col, undefined);
this._dirtyRanges$.next({ unitId, subUnitId, ranges: [{ startRow: row, startColumn: col, endRow: row, endColumn: col }] });
}
private _deleteRange(unitId: string, subUnitId: string, ranges: IRange[]) {
const cache = this._ensureCache(unitId, subUnitId);
ranges.forEach((range) => {
@@ -106,9 +118,4 @@ export class DataValidationCacheService extends Disposable {
const cache = this._ensureCache(unitId, subUnitId);
return cache.getValue(row, col);
}
setValue(unitId: string, subUnitId: string, row: number, col: number, value: DataValidationStatus) {
const cache = this._ensureCache(unitId, subUnitId);
return cache.setValue(row, col, value);
}
}
@@ -16,9 +16,9 @@
import type { IRange, ISheetDataValidationRule } from '@univerjs/core';
import { Disposable, Inject, isFormulaString, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DataValidationModel } from '@univerjs/data-validation';
import { DataValidationModel, DataValidatorRegistryService } from '@univerjs/data-validation';
import { RegisterOtherFormulaService } from '@univerjs/sheets-formula';
import { getFormulaCellData, isCustomFormulaType } from '../utils/formula';
import { getFormulaCellData, shouldOffsetFormulaByRange } from '../utils/formula';
import { DataValidationCacheService } from './dv-cache.service';
interface IFormulaData {
@@ -43,7 +43,8 @@ export class DataValidationCustomFormulaService extends Disposable {
@IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService,
@Inject(RegisterOtherFormulaService) private _registerOtherFormulaService: RegisterOtherFormulaService,
@Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel,
@Inject(DataValidationCacheService) private readonly _dataValidationCacheService: DataValidationCacheService
@Inject(DataValidationCacheService) private readonly _dataValidationCacheService: DataValidationCacheService,
@Inject(DataValidatorRegistryService) private readonly _validatorRegistryService: DataValidatorRegistryService
) {
super();
@@ -156,7 +157,7 @@ export class DataValidationCustomFormulaService extends Disposable {
}
addRule(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
if (isCustomFormulaType(rule.type)) {
if (shouldOffsetFormulaByRange(rule.type, this._validatorRegistryService)) {
const { ranges, formula1, formula2, uid: ruleId } = rule;
this._addFormulaByRange(unitId, subUnitId, ruleId, formula1, formula2, ranges);
}
@@ -17,9 +17,9 @@
import type { ISheetDataValidationRule, Nullable } from '@univerjs/core';
import type { IFormulaInfo, IOtherFormulaResult } from '@univerjs/sheets-formula';
import { DataValidationType, Disposable, Inject, isFormulaString, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DataValidationModel } from '@univerjs/data-validation';
import { DataValidationModel, DataValidatorRegistryService } from '@univerjs/data-validation';
import { RegisterOtherFormulaService } from '@univerjs/sheets-formula';
import { isCustomFormulaType } from '../utils/formula';
import { shouldOffsetFormulaByRange } from '../utils/formula';
import { DataValidationCacheService } from './dv-cache.service';
type RuleId = string;
@@ -33,7 +33,8 @@ export class DataValidationFormulaService extends Disposable {
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService,
@Inject(RegisterOtherFormulaService) private _registerOtherFormulaService: RegisterOtherFormulaService,
@Inject(DataValidationCacheService) private readonly _dataValidationCacheService: DataValidationCacheService,
@Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel
@Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel,
@Inject(DataValidatorRegistryService) private readonly _validatorRegistryService: DataValidatorRegistryService
) {
super();
this._initFormulaResultHandler();
@@ -87,7 +88,7 @@ export class DataValidationFormulaService extends Disposable {
}
addRule(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
if (!isCustomFormulaType(rule.type) && rule.type !== DataValidationType.CHECKBOX) {
if (!shouldOffsetFormulaByRange(rule.type, this._validatorRegistryService) && rule.type !== DataValidationType.CHECKBOX) {
const { formula1, formula2, uid: ruleId } = rule;
const isFormula1Legal = isFormulaString(formula1);
const isFormula2Legal = isFormulaString(formula2);
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { DataValidationType, type ICellData, type Nullable } from '@univerjs/core';
import type { DataValidationType, ICellData, Nullable } from '@univerjs/core';
import type { DataValidatorRegistryService } from '@univerjs/data-validation';
import { ERROR_TYPE_SET } from '@univerjs/engine-formula';
export function getFormulaResult(result: Nullable<Nullable<ICellData>[][]>) {
@@ -32,14 +33,7 @@ export function isLegalFormulaResult(res: string) {
/**
* Judge if the data-validation's formula need to be offseted by ranges
*/
export function isCustomFormulaType(type: DataValidationType) {
// types not in this list is formula type
const invalidTypes = [
DataValidationType.LIST,
DataValidationType.LIST_MULTIPLE,
DataValidationType.CHECKBOX,
DataValidationType.ANY,
];
return !invalidTypes.includes(type);
export function shouldOffsetFormulaByRange(type: DataValidationType | string, validatorRegistryService: DataValidatorRegistryService) {
const validator = validatorRegistryService.getValidatorItem(type);
return validator?.offsetFormulaByRange ?? false;
}
@@ -25,6 +25,8 @@ export class AnyValidator extends BaseDataValidator {
override operators: DataValidationOperator[] = [];
override scopes: string | string[] = ['sheet'];
override readonly offsetFormulaByRange = false;
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult> {
return {
formula1: rule.formula1,
@@ -54,6 +54,8 @@ export class CheckboxValidator extends BaseDataValidator {
override operators: DataValidationOperator[] = [];
override scopes: string | string[] = ['sheet'];
override readonly offsetFormulaByRange = false;
private _formulaService = this.injector.get(DataValidationFormulaService);
override skipDefaultFontRender = (rule: ISheetDataValidationRule, cellValue: Nullable<CellValue>, pos: { unitId: string; subUnitId: string; row: number; column: number }) => {
@@ -46,7 +46,7 @@ const transformDate2SerialNumber = (value: Nullable<CellValue>) => {
return numfmt.parseDate(dayjs(value).format('YYYY-MM-DD HH:mm:ss'))?.v as number | undefined;
};
export class DateValidator extends BaseDataValidator<number> {
export class DateValidator extends BaseDataValidator {
id: string = DataValidationType.DATE;
title: string = 'dataValidation.date.title';
operators: DataValidationOperator[] = [
@@ -156,78 +156,6 @@ export class DateValidator extends BaseDataValidator<number> {
};
}
override async validatorIsEqual(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule) {
const { formula1 } = formula;
const { value: cellValue } = cellInfo;
if (Number.isNaN(formula1)) {
return true;
}
return cellValue === formula1;
}
override async validatorIsNotEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value !== formula1;
}
override async validatorIsBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1, formula2 } = formula;
if (Number.isNaN(formula1) || Number.isNaN(formula2)) {
return true;
}
const start = Math.min(formula1, formula2);
const end = Math.max(formula1, formula2);
return cellInfo.value >= start && cellInfo.value <= end;
}
override async validatorIsNotBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1, formula2 } = formula;
if (Number.isNaN(formula1) || Number.isNaN(formula2)) {
return true;
}
const start = Math.min(formula1, formula2);
const end = Math.max(formula1, formula2);
return cellInfo.value < start || cellInfo.value > end;
}
override async validatorIsGreaterThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value > formula1;
}
override async validatorIsGreaterThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value >= formula1;
}
override async validatorIsLessThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value < formula1;
}
override async validatorIsLessThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value <= formula1;
}
override get operatorNames() {
return this.operators.map((operator) => this.localeService.t(DateOperatorNameMap[operator]));
}
@@ -40,7 +40,7 @@ export function getCellValueNumber(cellValue: CellValue) {
return +cellValue;
}
export class DecimalValidator extends BaseDataValidator<number> {
export class DecimalValidator extends BaseDataValidator {
private readonly _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
id: string = DataValidationType.DECIMAL;
private readonly _lexerTreeBuilder = this.injector.get(LexerTreeBuilder);
@@ -126,79 +126,6 @@ export class DecimalValidator extends BaseDataValidator<number> {
};
}
override async validatorIsEqual(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule) {
const { formula1 } = formula;
const { value: cellValue } = cellInfo;
if (Number.isNaN(formula1)) {
return true;
}
return cellValue === formula1;
}
override async validatorIsNotEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value !== formula1;
}
override async validatorIsBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1, formula2 } = formula;
if (Number.isNaN(formula1) || Number.isNaN(formula2)) {
return true;
}
const start = Math.min(formula1, formula2);
const end = Math.max(formula1, formula2);
return cellInfo.value >= start && cellInfo.value <= end;
}
override async validatorIsNotBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1, formula2 } = formula;
if (Number.isNaN(formula1) || Number.isNaN(formula2)) {
return true;
}
const start = Math.min(formula1, formula2);
const end = Math.max(formula1, formula2);
return cellInfo.value < start || cellInfo.value > end;
}
override async validatorIsGreaterThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value > formula1;
}
override async validatorIsGreaterThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value >= formula1;
}
override async validatorIsLessThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value < formula1;
}
override async validatorIsLessThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value <= formula1;
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase, position: ISheetLocationBase) {
if (!rule.operator) {
return this.titleStr;
@@ -20,6 +20,7 @@ import { ListValidator } from './list-validator';
export class ListMultipleValidator extends ListValidator {
override id: string = DataValidationType.LIST_MULTIPLE;
override title: string = 'dataValidation.listMultiple.title';
override readonly offsetFormulaByRange = false;
override skipDefaultFontRender = () => {
return true;
@@ -93,6 +93,8 @@ export class ListValidator extends BaseDataValidator {
private _lexer = this.injector.get(LexerTreeBuilder);
private _univerInstanceService = this.injector.get(IUniverInstanceService);
override readonly offsetFormulaByRange = false;
id: string = DataValidationType.LIST;
title: string = 'dataValidation.list.title';
operators: DataValidationOperator[] = [];
@@ -146,24 +148,25 @@ export class ListValidator extends BaseDataValidator {
return deserializeListOptions(cellString);
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult<string[] | undefined>> {
const { formula1 = '' } = rule;
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult<number | undefined>> {
const results = await this.formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
const formulaResult1 = getFormulaResult(results?.[0]?.result?.[0][0]);
const isFormulaValid = isLegalFormulaResult(String(formulaResult1));
return {
formula1: isFormulaString(formula1) ? getRuleFormulaResultSet(results?.[0]?.result?.[0][0]) : deserializeListOptions(formula1),
formula1: undefined,
formula2: undefined,
isFormulaValid,
};
}
override async isValidType(cellInfo: IValidatorCellInfo<Nullable<CellValue>>, formula: IFormulaResult<string[] | undefined>, rule: IDataValidationRule): Promise<boolean> {
const { value } = cellInfo;
const { formula1 = [] } = formula;
const { value, unitId, subUnitId } = cellInfo;
const { formula1 = '' } = rule;
const results = await this.formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
const formula1Result = isFormulaString(formula1) ? getRuleFormulaResultSet(results?.[0]?.result?.[0][0]) : deserializeListOptions(formula1);
const selected = this.parseCellValue(value!);
return selected.every((i) => formula1.includes(i));
return selected.every((i) => formula1Result.includes(i));
}
override generateRuleName() {
@@ -26,7 +26,7 @@ import { isLegalFormulaResult } from '../utils/formula';
import { FORMULA1, FORMULA2 } from './const';
import { getTransformedFormula } from './util';
export class TextLengthValidator extends BaseDataValidator<number> {
export class TextLengthValidator extends BaseDataValidator {
id: string = DataValidationType.TEXT_LENGTH;
title: string = 'dataValidation.textLength.title';
private readonly _lexerTreeBuilder = this.injector.get(LexerTreeBuilder);
@@ -50,7 +50,7 @@ export class TextLengthValidator extends BaseDataValidator<number> {
return !Tools.isBlank(formula) && (isFormulaString(formula) || (!Number.isNaN(+formula) && Number.isInteger(+formula)));
}
override validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult {
override validatorFormula(rule: IDataValidationRule, _unitId: string, _subUnitId: string): IFormulaValidResult {
const operator = rule.operator;
if (!operator) {
return {
@@ -83,11 +83,7 @@ export class TextLengthValidator extends BaseDataValidator<number> {
return +formula;
}
private _isValidFormula(formula: number) {
return !Number.isNaN(formula);
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult<any>> {
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult<number | undefined>> {
const formulaResult1 = await this._customFormulaService.getCellFormulaValue(unitId, subUnitId, rule.uid, row, column);
const formulaResult2 = await this._customFormulaService.getCellFormula2Value(unitId, subUnitId, rule.uid, row, column);
const { formula1, formula2 } = rule;
@@ -108,95 +104,11 @@ export class TextLengthValidator extends BaseDataValidator<number> {
};
}
override async isValidType(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule) {
override async isValidType(cellInfo: IValidatorCellInfo<number>, _formula: IFormulaResult, _rule: IDataValidationRule) {
const { value: cellValue } = cellInfo;
return typeof cellValue === 'string' || typeof cellValue === 'number';
}
override async validatorIsEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { formula1 } = formula;
if (!Tools.isDefine(formula1)) {
return false;
}
return cellInfo.value === formula1;
}
override async validatorIsNotEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { formula1 } = formula;
if (!Tools.isDefine(formula1)) {
return false;
}
return cellInfo.value !== formula1;
}
override async validatorIsBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { formula1, formula2 } = formula;
const { value: cellValue } = cellInfo;
if (!this._isValidFormula(formula1) || !this._isValidFormula(formula2)) {
return false;
}
const max = Math.max(formula1, formula2);
const min = Math.min(formula1, formula2);
return cellValue >= min && cellValue <= max;
}
override async validatorIsNotBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { formula1, formula2 } = formula;
const { value: cellValue } = cellInfo;
if (!this._isValidFormula(formula1) || !this._isValidFormula(formula2)) {
return false;
}
const max = Math.max(formula1, formula2);
const min = Math.min(formula1, formula2);
return cellValue < min || cellValue > max;
}
override async validatorIsGreaterThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { formula1 } = formula;
const { value: cellValue } = cellInfo;
if (!this._isValidFormula(formula1)) {
return false;
}
return cellValue > formula1;
}
override async validatorIsGreaterThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { formula1 } = formula;
const { value: cellValue } = cellInfo;
if (!this._isValidFormula(formula1)) {
return false;
}
return cellValue >= formula1;
}
override async validatorIsLessThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { formula1 } = formula;
const { value: cellValue } = cellInfo;
if (!this._isValidFormula(formula1)) {
return false;
}
return cellValue < formula1;
}
override async validatorIsLessThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { formula1 } = formula;
const { value: cellValue } = cellInfo;
if (!this._isValidFormula(formula1)) {
return false;
}
return cellValue <= formula1;
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase, pos: ISheetLocationBase) {
if (!rule.operator) {
return this.titleStr;
@@ -28,7 +28,7 @@ import { FORMULA1, FORMULA2 } from './const';
import { getCellValueNumber } from './decimal-validator';
import { getTransformedFormula } from './util';
export class WholeValidator extends BaseDataValidator<number> {
export class WholeValidator extends BaseDataValidator {
private readonly _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
private readonly _lexerTreeBuilder = this.injector.get(LexerTreeBuilder);
@@ -52,13 +52,13 @@ export class WholeValidator extends BaseDataValidator<number> {
return !Tools.isBlank(formula) && (isFormulaString(formula) || (!Number.isNaN(+formula) && Number.isInteger(+formula)));
}
override async isValidType(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule) {
override async isValidType(cellInfo: IValidatorCellInfo<CellValue>, _formula: IFormulaResult, _rule: IDataValidationRule) {
const { value: cellValue } = cellInfo;
const num = getCellValueNumber(cellValue);
return !Number.isNaN(num) && Number.isInteger(num);
}
override transform(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule) {
override transform(cellInfo: IValidatorCellInfo<CellValue>, _formula: IFormulaResult, _rule: IDataValidationRule) {
const { value: cellValue } = cellInfo;
return {
...cellInfo,
@@ -92,7 +92,7 @@ export class WholeValidator extends BaseDataValidator<number> {
return info;
}
override validatorFormula(rule: IDataValidationRuleBase, unitId: string, subUnitId: string): IFormulaValidResult {
override validatorFormula(rule: IDataValidationRuleBase, _unitId: string, _subUnitId: string): IFormulaValidResult {
const operator = rule.operator;
if (!operator) {
return {
@@ -117,80 +117,6 @@ export class WholeValidator extends BaseDataValidator<number> {
};
}
override async validatorIsEqual(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule) {
const { formula1 } = formula;
const { value: cellValue } = cellInfo;
if (Number.isNaN(formula1)) {
return true;
}
return cellValue === formula1;
}
override async validatorIsNotEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value !== formula1;
}
override async validatorIsBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1, formula2 } = formula;
if (Number.isNaN(formula1) || Number.isNaN(formula2)) {
return true;
}
const start = Math.min(formula1, formula2);
const end = Math.max(formula1, formula2);
return cellInfo.value >= start && cellInfo.value <= end;
}
override async validatorIsNotBetween(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1, formula2 } = formula;
if (Number.isNaN(formula1) || Number.isNaN(formula2)) {
return true;
}
const start = Math.min(formula1, formula2);
const end = Math.max(formula1, formula2);
return cellInfo.value < start || cellInfo.value > end;
}
override async validatorIsGreaterThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value > formula1;
}
override async validatorIsGreaterThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value >= formula1;
}
override async validatorIsLessThan(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value < formula1;
}
override async validatorIsLessThanOrEqual(cellInfo: IValidatorCellInfo<number>, formula: IFormulaResult, _rule: IDataValidationRule) {
const { formula1 } = formula;
if (Number.isNaN(formula1)) {
return true;
}
return cellInfo.value <= formula1;
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase, position: ISheetLocationBase) {
if (!rule.operator) {
return this.titleStr;
@@ -20,19 +20,21 @@ import { DocSelectionManagerService } from '@univerjs/docs';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { SheetDataValidationModel } from '@univerjs/sheets-data-validation';
const disables = new Set<string>([
DataValidationType.CHECKBOX,
DataValidationType.LIST,
DataValidationType.LIST_MULTIPLE,
]);
export const getShouldDisableCellLink = (accessor: IAccessor, worksheet: Worksheet, row: number, col: number) => {
const cell = worksheet.getCell(row, col);
if (cell?.f || cell?.si) {
return true;
}
const disables = [
DataValidationType.CHECKBOX,
DataValidationType.LIST,
DataValidationType.LIST_MULTIPLE,
];
const dataValidationModel = accessor.has(SheetDataValidationModel) ? accessor.get(SheetDataValidationModel) : null;
const rule = dataValidationModel?.getRuleByLocation(worksheet.getUnitId(), worksheet.getSheetId(), row, col);
if (rule && disables.includes(rule.type)) {
if (rule && disables.has(rule.type)) {
return true;
}
-3
View File
@@ -2044,9 +2044,6 @@ importers:
packages/sheets-data-validation:
dependencies:
'@flatten-js/interval-tree':
specifier: ^1.1.3
version: 1.1.3
'@univerjs/core':
specifier: workspace:*
version: link:../core