fix(sheets-data-validation): data-validation ref-range behavior (#4015)

This commit is contained in:
zhangw
2024-11-12 15:33:36 +08:00
committed by GitHub
parent 54599ce86c
commit fded2e80aa
27 changed files with 1466 additions and 268 deletions
+5 -1
View File
@@ -16,7 +16,7 @@
import type { Nullable } from './types';
import { AbsoluteRefType, type IRange, type IRectLTRB, RANGE_TYPE } from '../sheets/typedef';
import { mergeRanges, multiSubtractSingleRange } from './range';
import { mergeRanges, multiSubtractSingleRange, splitIntoGrid } from './range';
/**
* This class provides a set of methods to calculate `IRange`.
@@ -350,6 +350,10 @@ export class Rectangle {
return mergeRanges(ranges);
}
static splitIntoGrid(ranges: IRange[]): IRange[] {
return splitIntoGrid(ranges);
}
static subtractMulti(ranges1: IRange[], ranges2: IRange[]): IRange[] {
if (!ranges2.length) {
return ranges1;
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { CommandType } from '@univerjs/core';
import type { ICommand, IDataValidationRule } from '@univerjs/core';
import type { IUpdateRulePayload } from '../../types/interfaces/i-update-rule-payload';
import type { DataValidationChangeSource } from '../../models/data-validation-model';
import type { IUpdateRulePayload } from '../../types/interfaces/i-update-rule-payload';
import { CommandType } from '@univerjs/core';
import { DataValidationModel } from '../../models/data-validation-model';
export interface IAddDataValidationMutationParams {
@@ -123,6 +123,11 @@ export class DataValidationModel extends Disposable {
Object.assign(rule, getRuleOptions(payload.payload));
break;
}
case UpdateRuleType.ALL: {
Object.assign(rule, payload.payload);
break;
}
default:
break;
}
@@ -18,4 +18,5 @@ export enum UpdateRuleType {
SETTING,
RANGE,
OPTIONS,
ALL,
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { IDataValidationRuleBase, IDataValidationRuleOptions } from '@univerjs/core';
import type { IDataValidationRule, IDataValidationRuleBase, IDataValidationRuleOptions } from '@univerjs/core';
import type { UpdateRuleType } from '../enum/update-rule-type';
export interface IUpdateRuleRangePayload {
@@ -32,7 +32,13 @@ export interface IUpdateRuleOptionsPayload {
payload: Partial<IDataValidationRuleOptions>;
}
export interface IUpdateRuleAllPayload {
type: UpdateRuleType.ALL;
payload: Omit<IDataValidationRule, 'uid'>;
}
export type IUpdateRulePayload =
IUpdateRuleRangePayload
| IUpdateRuleSettingPayload
| IUpdateRuleOptionsPayload;
| IUpdateRuleOptionsPayload
| IUpdateRuleAllPayload;
@@ -126,7 +126,7 @@ export abstract class BaseDataValidator<DataType = CellValue> {
return false;
}
abstract parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult>;
abstract parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult>;
abstract validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult;
@@ -185,7 +185,7 @@ export abstract class BaseDataValidator<DataType = CellValue> {
return allowBlank;
}
const formulaInfo = await this.parseFormula(rule, unitId, subUnitId);
const formulaInfo = await this.parseFormula(rule, unitId, subUnitId, cellInfo.row, cellInfo.column);
if (!formulaInfo.isFormulaValid) {
return false;
@@ -78,6 +78,7 @@ const ColorSelect = (props: IColorSelectProps) => {
open={open}
onDropdownVisibleChange={setOpen}
dropdownStyle={{ width: 112 }}
style={{ width: 96, cursor: 'pointer' }}
className={styles.dataValidationFormulaColorSelect}
value={value}
onChange={onChange}
@@ -128,7 +129,6 @@ const Template = (props: { item: IDropdownItem; commonProps: any; style?: React.
onChange={(color) => {
onItemChange(item.id, item.label, color);
}}
/>
<Input
disabled={item.isRef}
@@ -75,16 +75,7 @@ export const DataValidationItem = (props: IDataValidationDetailProps) => {
stroke: '#49B811',
widgets: {},
},
primary: {
startColumn: range.startColumn,
endColumn: range.endColumn,
startRow: range.startRow,
endRow: range.endRow,
actualRow: range.startRow,
actualColumn: range.startColumn,
isMerged: false,
isMergedMainCell: false,
},
primary: null,
}));
}}
onMouseLeave={() => {
@@ -18,11 +18,12 @@ import type { CellValue, IAccessor, ICellData, ICommand, IDataValidationRuleBase
import type { DataValidationChangeSource, IAddDataValidationMutationParams, IRemoveDataValidationMutationParams, IUpdateDataValidationMutationParams } from '@univerjs/data-validation';
import type { ISetRangeValuesMutationParams, ISheetCommandSharedParams } from '@univerjs/sheets';
import type { RangeMutation } from '../../models/rule-matrix';
import { CommandType, DataValidationType, ICommandService, IUndoRedoService, IUniverInstanceService, ObjectMatrix, Range, sequenceExecute, Tools } from '@univerjs/core';
import { CommandType, DataValidationType, ICommandService, isFormulaString, IUndoRedoService, IUniverInstanceService, ObjectMatrix, Range, sequenceExecute, Tools } from '@univerjs/core';
import { AddDataValidationMutation, DataValidatorRegistryService, getRuleOptions, getRuleSetting, RemoveDataValidationMutation, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
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 { getStringCellValue } from '../../utils/get-cell-data-origin';
import { CHECKBOX_FORMULA_1, CHECKBOX_FORMULA_2, type CheckboxValidator } from '../../validators';
@@ -113,44 +114,17 @@ export function getDataValidationDiffMutations(
});
break;
case 'update': {
redoMutations.push({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: diff.ruleId,
payload: {
type: UpdateRuleType.RANGE,
payload: diff.newRanges,
},
source,
} as IUpdateDataValidationMutationParams,
});
undoMutations.unshift({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: diff.ruleId,
payload: {
type: UpdateRuleType.RANGE,
payload: diff.oldRanges,
},
source,
} as IUpdateDataValidationMutationParams,
});
if (diff.rule.type === DataValidationType.CUSTOM) {
if (isCustomFormulaType(diff.rule.type)) {
const originRow = diff.oldRanges[0].startRow;
const originColumn = diff.oldRanges[0].startColumn;
const newRow = diff.newRanges[0].startRow;
const newColumn = diff.newRanges[0].startColumn;
const rowDiff = newRow - originRow;
const columnDiff = newColumn - originColumn;
const newFormula = lexerTreeBuilder.moveFormulaRefOffset(diff.rule.formula1!, columnDiff, rowDiff);
const newFormula = isFormulaString(diff.rule.formula1!) ? lexerTreeBuilder.moveFormulaRefOffset(diff.rule.formula1!, columnDiff, rowDiff) : diff.rule.formula1;
const newFormula2 = isFormulaString(diff.rule.formula2!) ? lexerTreeBuilder.moveFormulaRefOffset(diff.rule.formula2!, columnDiff, rowDiff) : diff.rule.formula2;
if (newFormula !== diff.rule.formula1) {
if (newFormula !== diff.rule.formula1 || newFormula2 !== diff.rule.formula2) {
redoMutations.push({
id: UpdateDataValidationMutation.id,
params: {
@@ -158,10 +132,11 @@ export function getDataValidationDiffMutations(
subUnitId,
ruleId: diff.ruleId,
payload: {
type: UpdateRuleType.SETTING,
type: UpdateRuleType.ALL,
payload: {
...getRuleSetting(diff.rule),
formula1: newFormula,
formula2: newFormula2,
ranges: diff.newRanges,
},
},
} as IUpdateDataValidationMutationParams,
@@ -174,12 +149,44 @@ export function getDataValidationDiffMutations(
subUnitId,
ruleId: diff.ruleId,
payload: {
type: UpdateRuleType.SETTING,
payload: getRuleSetting(diff.rule),
type: UpdateRuleType.ALL,
payload: {
formula1: diff.rule.formula1,
formula2: diff.rule.formula2,
ranges: diff.oldRanges,
},
},
} as IUpdateDataValidationMutationParams,
});
}
} else {
redoMutations.push({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: diff.ruleId,
payload: {
type: UpdateRuleType.RANGE,
payload: diff.newRanges,
},
source,
} as IUpdateDataValidationMutationParams,
});
undoMutations.unshift({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: diff.ruleId,
payload: {
type: UpdateRuleType.RANGE,
payload: diff.oldRanges,
},
source,
} as IUpdateDataValidationMutationParams,
});
}
const rule = sheetDataValidationModel.getRuleById(unitId, subUnitId, diff.ruleId);
@@ -296,11 +303,15 @@ export const AddSheetDataValidationCommand: ICommand<IAddSheetDataValidationComm
const matrix = sheetDataValidationModel.getRuleObjectMatrix(unitId, subUnitId).clone();
matrix.addRule(rule);
const diffs = matrix.diff(sheetDataValidationModel.getRules(unitId, subUnitId));
const validator = sheetDataValidationModel.getValidator(rule.type);
const mutationParams: IAddDataValidationMutationParams = {
unitId,
subUnitId,
rule,
rule: {
...rule,
...validator?.normalizeFormula(rule, unitId, subUnitId),
},
};
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, diffs, accessor);
@@ -0,0 +1,194 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 { LexerTreeBuilder } from '@univerjs/engine-formula';
import { FormulaRefRangeService } from '@univerjs/sheets-formula';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
import { isCustomFormulaType } 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(LexerTreeBuilder) private _lexerTreeBuilder: LexerTreeBuilder
) {
super();
this._initRefRange();
}
private _getIdWithUnitId(unitID: string, subUnitId: string, ruleId: string) {
return `${unitID}_${subUnitId}_${ruleId}`;
}
registerRule = (unitId: string, subUnitId: string, rule: ISheetDataValidationRule) => {
if (!isCustomFormulaType(rule.type)) {
return;
}
this.register(unitId, subUnitId, rule);
};
// eslint-disable-next-line max-lines-per-function
register(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
const oldRanges = rule.ranges;
const oldFormula1 = rule.formula1;
const oldFormula2 = rule.formula2;
// eslint-disable-next-line max-lines-per-function
const disposable = this._formulaRefRangeService.registerRangeFormula(unitId, subUnitId, oldRanges, [oldFormula1 ?? '', oldFormula2 ?? ''], (res) => {
if (res.length === 0) {
return {
undos: [{
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId,
rule,
},
}],
redos: [{
id: RemoveDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: rule.uid,
},
}],
};
}
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
const first = res[0];
redos.push({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: rule.uid,
payload: {
type: UpdateRuleType.ALL,
payload: {
ranges: first.ranges,
formula1: first.formulas[0],
formula2: first.formulas[1],
},
},
} as IUpdateDataValidationMutationParams,
});
undos.push({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: rule.uid,
payload: {
type: UpdateRuleType.ALL,
payload: {
ranges: oldRanges,
formula1: oldFormula1,
formula2: oldFormula2,
},
},
},
});
for (let i = 1; i < res.length; i++) {
const item = res[i];
const id = generateRandomId();
redos.push({
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId,
rule: {
...rule,
uid: id,
formula1: item.formulas[0],
formula2: item.formulas[1],
ranges: item.ranges,
},
},
});
undos.push({
id: RemoveDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: id,
},
});
}
return {
undos,
redos,
};
});
const id = this._getIdWithUnitId(unitId, subUnitId, rule.uid);
this._disposableMap.set(id, disposable);
};
private _initRefRange() {
const allRules = this._dataValidationModel.getAll();
for (const [unitId, subUnitMap] of allRules) {
for (const [subUnitId, rules] of subUnitMap) {
for (const rule of rules) {
this.registerRule(unitId, subUnitId, rule);
}
}
}
this.disposeWithMe(
this._dataValidationModel.ruleChange$.subscribe((option) => {
const { unitId, subUnitId, rule } = option;
switch (option.type) {
case 'add': {
const rule = option.rule!;
this.registerRule(option.unitId, option.subUnitId, rule);
break;
}
case 'remove': {
const disposeSet = this._disposableMap.get(this._getIdWithUnitId(unitId, subUnitId, rule!.uid));
if (disposeSet) {
disposeSet.dispose();
}
break;
}
case 'update': {
const rule = option.rule!;
const disposeSet = this._disposableMap.get(this._getIdWithUnitId(unitId, subUnitId, rule!.uid));
if (disposeSet) {
disposeSet.dispose();
}
this.registerRule(option.unitId, option.subUnitId, rule);
break;
}
}
})
);
this.disposeWithMe(toDisposable(() => {
this._disposableMap.forEach((item) => {
item.dispose();
});
this._disposableMap.clear();
}));
}
}
@@ -17,7 +17,7 @@
import type { ISheetDataValidationRule } from '@univerjs/core';
import type { IRemoveDataValidationMutationParams, IUpdateDataValidationMutationParams } from '@univerjs/data-validation';
import type { EffectRefRangeParams } from '@univerjs/sheets';
import { DataValidationType, Disposable, Inject, Injector, isRangesEqual, toDisposable } from '@univerjs/core';
import { Disposable, Inject, Injector, isRangesEqual, toDisposable } from '@univerjs/core';
import { RemoveDataValidationMutation, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
import { handleCommonDefaultRangeChangeWithEffectRefCommands, RefRangeService } from '@univerjs/sheets';
import { FormulaRefRangeService } from '@univerjs/sheets-formula';
@@ -25,6 +25,7 @@ import { removeDataValidationUndoFactory } from '../commands/commands/data-valid
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';
export class DataValidationRefRangeController extends Disposable {
private _disposableMap: Map<string, Set<() => void>> = new Map();
@@ -46,11 +47,13 @@ export class DataValidationRefRangeController extends Disposable {
}
registerRule = (unitId: string, subUnitId: string, rule: ISheetDataValidationRule) => {
if (isCustomFormulaType(rule.type)) {
return;
}
this.register(unitId, subUnitId, rule);
this.registerFormula(unitId, subUnitId, rule);
};
// eslint-disable-next-line max-lines-per-function
registerFormula(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
const ruleId = rule.uid;
const id = this._getIdWithUnitId(unitId, subUnitId, ruleId);
@@ -106,41 +109,27 @@ export class DataValidationRefRangeController extends Disposable {
return { redos, undos };
};
if (rule.type === DataValidationType.CUSTOM) {
const currentFormula = this._dataValidationCustomFormulaService.getRuleFormulaInfo(unitId, subUnitId, ruleId);
if (currentFormula) {
const currentFormula = this._dataValidationFormulaService.getRuleFormulaInfo(unitId, subUnitId, ruleId);
if (currentFormula) {
const [formula1, formula2] = currentFormula;
if (formula1) {
const disposable = this._formulaRefRangeService.registerFormula(
unitId,
subUnitId,
currentFormula.formula,
formula1.text,
(newFormulaString) => handleFormulaChange('formula1', newFormulaString)
);
disposeSet.add(() => disposable.dispose());
}
} else {
const currentFormula = this._dataValidationFormulaService.getRuleFormulaInfo(unitId, subUnitId, ruleId);
if (currentFormula) {
const [formula1, formula2] = currentFormula;
if (formula1) {
const disposable = this._formulaRefRangeService.registerFormula(
unitId,
subUnitId,
formula1.text,
(newFormulaString) => handleFormulaChange('formula1', newFormulaString)
);
disposeSet.add(() => disposable.dispose());
}
if (formula2) {
const disposable = this._formulaRefRangeService.registerFormula(
unitId,
subUnitId,
formula2.text,
(newFormulaString) => handleFormulaChange('formula2', newFormulaString)
);
disposeSet.add(() => disposable.dispose());
}
if (formula2) {
const disposable = this._formulaRefRangeService.registerFormula(
unitId,
subUnitId,
formula2.text,
(newFormulaString) => handleFormulaChange('formula2', newFormulaString)
);
disposeSet.add(() => disposable.dispose());
}
}
}
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import type { ISheetDataValidationRule } from '@univerjs/core';
import type { DataValidationType, ISheetDataValidationRule } from '@univerjs/core';
import type { IRuleChange, IUpdateRulePayload } from '@univerjs/data-validation';
import type { IRemoveSheetMutationParams, ISheetLocation } from '@univerjs/sheets';
import { DataValidationStatus, DataValidationType, Disposable, ICommandService, Inject, IUniverInstanceService } from '@univerjs/core';
import { DataValidationStatus, Disposable, ICommandService, Inject, IUniverInstanceService } from '@univerjs/core';
import { DataValidationModel, DataValidatorRegistryService, UpdateRuleType } from '@univerjs/data-validation';
import { RemoveSheetMutation } from '@univerjs/sheets';
import { Subject } from 'rxjs';
@@ -142,10 +142,8 @@ export class SheetDataValidationModel extends Disposable {
const ruleMatrix = this._ensureRuleMatrix(unitId, subUnitId);
ruleMatrix.addRule(rule);
this._dataValidationCacheService.addRule(unitId, subUnitId, rule);
this._dataValidationFormulaService.addRule(unitId, subUnitId, rule.uid, rule.formula1, rule.formula2, rule.ranges);
if (rule.type === DataValidationType.CUSTOM) {
this._dataValidationCustomFormulaService.addRule(unitId, subUnitId, rule);
}
this._dataValidationFormulaService.addRule(unitId, subUnitId, rule);
this._dataValidationCustomFormulaService.addRule(unitId, subUnitId, rule);
}
private _addRule(unitId: string, subUnitId: string, rule: ISheetDataValidationRule | ISheetDataValidationRule[]): void {
@@ -158,41 +156,33 @@ export class SheetDataValidationModel extends Disposable {
private _updateRule(unitId: string, subUnitId: string, ruleId: string, oldRule: ISheetDataValidationRule, payload: IUpdateRulePayload) {
const ruleMatrix = this._ensureRuleMatrix(unitId, subUnitId);
const newRule = {
...oldRule,
...payload.payload,
};
if (payload.type === UpdateRuleType.RANGE) {
ruleMatrix.updateRange(ruleId, payload.payload);
this._dataValidationCacheService.updateRuleRanges(unitId, subUnitId, ruleId, payload.payload, oldRule.ranges);
if (oldRule.type === DataValidationType.CUSTOM) {
this._dataValidationCustomFormulaService.deleteByRuleId(unitId, subUnitId, ruleId);
this._dataValidationCustomFormulaService.addRule(unitId, subUnitId, {
...oldRule,
ranges: payload.payload,
});
}
} else if (payload.type === UpdateRuleType.SETTING) {
this._dataValidationCacheService.markRangeDirty(unitId, subUnitId, oldRule.ranges);
this._dataValidationFormulaService.updateRuleFormulaText(unitId, subUnitId, ruleId, payload.payload.formula1, payload.payload.formula2, oldRule.ranges);
if (oldRule.type === DataValidationType.CUSTOM) {
this._dataValidationCustomFormulaService.deleteByRuleId(unitId, subUnitId, ruleId);
this._dataValidationCustomFormulaService.addRule(unitId, subUnitId, {
...oldRule,
...payload.payload,
});
} else if (payload.payload.type === DataValidationType.CUSTOM) {
this._dataValidationCustomFormulaService.addRule(unitId, subUnitId, {
...oldRule,
...payload.payload,
});
}
} else if (payload.type === UpdateRuleType.ALL) {
ruleMatrix.updateRange(ruleId, payload.payload.ranges);
this._dataValidationCacheService.updateRuleRanges(unitId, subUnitId, ruleId, payload.payload.ranges, oldRule.ranges);
this._dataValidationCacheService.markRangeDirty(unitId, subUnitId, oldRule.ranges);
}
this._dataValidationFormulaService.removeRule(unitId, subUnitId, oldRule.uid);
this._dataValidationFormulaService.addRule(unitId, subUnitId, newRule);
this._dataValidationCustomFormulaService.deleteByRuleId(unitId, subUnitId, ruleId);
this._dataValidationCustomFormulaService.addRule(unitId, subUnitId, newRule);
}
private _removeRule(unitId: string, subUnitId: string, oldRule: ISheetDataValidationRule): void {
const ruleMatrix = this._ensureRuleMatrix(unitId, subUnitId);
ruleMatrix.removeRule(oldRule);
this._dataValidationCacheService.removeRule(unitId, subUnitId, oldRule);
if (oldRule.type === DataValidationType.CUSTOM) {
this._dataValidationCustomFormulaService.deleteByRuleId(unitId, subUnitId, oldRule.uid);
}
this._dataValidationCustomFormulaService.deleteByRuleId(unitId, subUnitId, oldRule.uid);
}
getValidator(type: DataValidationType) {
@@ -37,6 +37,7 @@ import { DATA_VALIDATION_PLUGIN_NAME } from './common/const';
import { defaultPluginConfig, PLUGIN_CONFIG_KEY } from './controllers/config.schema';
import { DataValidationController } from './controllers/dv.controller';
import { DataValidationFormulaController } from './controllers/dv-formula.controller';
import { DataValidationFormulaRefRangeController } from './controllers/dv-formula-ref-range.controller';
import { DataValidationRefRangeController } from './controllers/dv-ref-range.controller';
import { SheetDataValidationSheetController } from './controllers/dv-sheet.controller';
import { SheetDataValidationModel } from './models/sheet-data-validation-model';
@@ -71,9 +72,10 @@ export class UniverSheetsDataValidationPlugin extends Plugin {
[SheetsDataValidationValidatorService],
[SheetDataValidationModel],
[DataValidationController],
[DataValidationRefRangeController],
[DataValidationFormulaController],
[SheetDataValidationSheetController],
[DataValidationRefRangeController],
[DataValidationFormulaRefRangeController],
] as Dependency[]).forEach((dep) => {
this._injector.add(dep);
});
@@ -92,6 +94,7 @@ export class UniverSheetsDataValidationPlugin extends Plugin {
this._injector.get(DataValidationCacheService);
this._injector.get(SheetsDataValidationValidatorService);
this._injector.get(DataValidationFormulaRefRangeController);
this._injector.get(DataValidationRefRangeController);
}
@@ -15,17 +15,16 @@
*/
import type { IRange, ISheetDataValidationRule } from '@univerjs/core';
import { DataValidationType, Disposable, Inject, isFormulaString, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { Disposable, Inject, isFormulaString, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DataValidationModel } from '@univerjs/data-validation';
import { RegisterOtherFormulaService } from '@univerjs/sheets-formula';
import { getFormulaCellData } from '../utils/formula';
import { getFormulaCellData, isCustomFormulaType } from '../utils/formula';
import { DataValidationCacheService } from './dv-cache.service';
interface IFormulaData {
formula: string;
originRow: number;
originCol: number;
isTransformable: boolean;
formulaId: string;
}
@@ -38,6 +37,7 @@ export class DataValidationCustomFormulaService extends Disposable {
* Map of origin formula of rule
*/
private _ruleFormulaMap: Map<UnitId, Map<SubUnitId, Map<RuleId, IFormulaData>>> = new Map();
private _ruleFormulaMap2: Map<UnitId, Map<SubUnitId, Map<RuleId, IFormulaData>>> = new Map();
constructor(
@IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService,
@@ -65,7 +65,7 @@ export class DataValidationCustomFormulaService extends Disposable {
const ruleInfo = ruleFormulaMap.get(result.extra?.ruleId);
const rule = this._dataValidationModel.getRuleById(unitId, subUnitId, result.extra?.ruleId);
if (rule && ruleInfo && !ruleInfo.isTransformable) {
if (rule && ruleInfo) {
this._dataValidationCacheService.markRangeDirty(unitId, subUnitId, rule.ranges);
}
});
@@ -76,11 +76,15 @@ export class DataValidationCustomFormulaService extends Disposable {
private _ensureMaps(unitId: string, subUnitId: string) {
let ruleFormulaUnitMap = this._ruleFormulaMap.get(unitId);
let ruleFormulaUnitMap2 = this._ruleFormulaMap2.get(unitId);
if (!ruleFormulaUnitMap) {
ruleFormulaUnitMap = new Map();
this._ruleFormulaMap.set(unitId, ruleFormulaUnitMap);
}
if (!ruleFormulaUnitMap2) {
ruleFormulaUnitMap2 = new Map();
this._ruleFormulaMap2.set(unitId, ruleFormulaUnitMap2);
}
let ruleFormulaMap = ruleFormulaUnitMap.get(subUnitId);
@@ -88,7 +92,14 @@ export class DataValidationCustomFormulaService extends Disposable {
ruleFormulaMap = new Map();
ruleFormulaUnitMap.set(subUnitId, ruleFormulaMap);
}
return { ruleFormulaMap };
let ruleFormulaMap2 = ruleFormulaUnitMap2.get(subUnitId);
if (!ruleFormulaMap2) {
ruleFormulaMap2 = new Map();
ruleFormulaUnitMap2.set(subUnitId, ruleFormulaMap2);
}
return { ruleFormulaMap, ruleFormulaMap2 };
};
private _registerFormula(unitId: string, subUnitId: string, ruleId: string, formulaString: string, ranges: IRange[]) {
@@ -96,7 +107,7 @@ export class DataValidationCustomFormulaService extends Disposable {
};
deleteByRuleId(unitId: string, subUnitId: string, ruleId: string) {
const { ruleFormulaMap } = this._ensureMaps(unitId, subUnitId);
const { ruleFormulaMap, ruleFormulaMap2 } = this._ensureMaps(unitId, subUnitId);
const rule = this._dataValidationModel.getRuleById(unitId, subUnitId, ruleId) as ISheetDataValidationRule;
const formulaInfo = ruleFormulaMap.get(ruleId);
@@ -109,35 +120,46 @@ export class DataValidationCustomFormulaService extends Disposable {
ruleFormulaMap.delete(ruleId);
this._registerOtherFormulaService.deleteFormula(unitId, subUnitId, [current.formulaId]);
}
const current2 = ruleFormulaMap2.get(ruleId);
if (current2) {
ruleFormulaMap2.delete(ruleId);
this._registerOtherFormulaService.deleteFormula(unitId, subUnitId, [current2.formulaId]);
}
}
private _addFormulaByRange(unitId: string, subUnitId: string, ruleId: string, formula: string, ranges: IRange[]) {
const { ruleFormulaMap } = this._ensureMaps(unitId, subUnitId);
if (!formula) {
return;
}
private _addFormulaByRange(unitId: string, subUnitId: string, ruleId: string, formula: string | undefined, formula2: string | undefined, ranges: IRange[]) {
const { ruleFormulaMap, ruleFormulaMap2 } = this._ensureMaps(unitId, subUnitId);
const originRow = ranges[0].startRow;
const originCol = ranges[0].startColumn;
const formulaId = this._registerFormula(unitId, subUnitId, ruleId, formula, ranges);
if (formula && isFormulaString(formula)) {
const formulaId = this._registerFormula(unitId, subUnitId, ruleId, formula, ranges);
ruleFormulaMap.set(ruleId, {
formula,
originCol,
originRow,
formulaId,
});
}
ruleFormulaMap.set(ruleId, {
formula,
originCol,
originRow,
formulaId,
isTransformable: true,
});
if (formula2 && isFormulaString(formula2)) {
const formulaId2 = this._registerFormula(unitId, subUnitId, ruleId, formula2, ranges);
ruleFormulaMap2.set(ruleId, {
formula: formula2,
originCol,
originRow,
formulaId: formulaId2,
});
}
}
addRule(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
const { ranges, formula1, uid: ruleId, type } = rule;
if (type !== DataValidationType.CUSTOM || !formula1 || !isFormulaString(formula1)) {
return;
if (isCustomFormulaType(rule.type)) {
const { ranges, formula1, formula2, uid: ruleId } = rule;
this._addFormulaByRange(unitId, subUnitId, ruleId, formula1, formula2, ranges);
}
this._addFormulaByRange(unitId, subUnitId, ruleId, formula1, ranges);
}
async getCellFormulaValue(unitId: string, subUnitId: string, ruleId: string, row: number, column: number) {
@@ -154,6 +176,48 @@ export class DataValidationCustomFormulaService extends Disposable {
return getFormulaCellData(result?.result?.[offsetRow]?.[offsetCol]);
}
async getCellFormula2Value(unitId: string, subUnitId: string, ruleId: string, row: number, column: number) {
const { ruleFormulaMap2 } = this._ensureMaps(unitId, subUnitId);
const current = ruleFormulaMap2.get(ruleId);
if (!current) {
return Promise.resolve(undefined);
}
const result = await this._registerOtherFormulaService.getFormulaValue(unitId, subUnitId, current.formulaId);
const { originRow, originCol } = current;
const offsetRow = row - originRow;
const offsetCol = column - originCol;
return getFormulaCellData(result?.result?.[offsetRow]?.[offsetCol]);
}
getCellFormulaValueSync(unitId: string, subUnitId: string, ruleId: string, row: number, column: number) {
const { ruleFormulaMap } = this._ensureMaps(unitId, subUnitId);
const current = ruleFormulaMap.get(ruleId);
if (!current) {
return undefined;
}
const result = this._registerOtherFormulaService.getFormulaValueSync(unitId, subUnitId, current.formulaId);
const { originRow, originCol } = current;
const offsetRow = row - originRow;
const offsetCol = column - originCol;
return getFormulaCellData(result?.result?.[offsetRow]?.[offsetCol]);
}
getCellFormula2ValueSync(unitId: string, subUnitId: string, ruleId: string, row: number, column: number) {
const { ruleFormulaMap2 } = this._ensureMaps(unitId, subUnitId);
const current = ruleFormulaMap2.get(ruleId);
if (!current) {
return undefined;
}
const result = this._registerOtherFormulaService.getFormulaValueSync(unitId, subUnitId, current.formulaId);
const { originRow, originCol } = current;
const offsetRow = row - originRow;
const offsetCol = column - originCol;
return getFormulaCellData(result?.result?.[offsetRow]?.[offsetCol]);
}
getRuleFormulaInfo(unitId: string, subUnitId: string, ruleId: string) {
const { ruleFormulaMap } = this._ensureMaps(unitId, subUnitId);
@@ -14,11 +14,12 @@
* limitations under the License.
*/
import type { IRange, Nullable } from '@univerjs/core';
import type { ISheetDataValidationRule, Nullable } from '@univerjs/core';
import type { IFormulaInfo, IOtherFormulaResult } from '@univerjs/sheets-formula';
import { Disposable, Inject, isFormulaString, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DataValidationType, Disposable, Inject, isFormulaString, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DataValidationModel } from '@univerjs/data-validation';
import { RegisterOtherFormulaService } from '@univerjs/sheets-formula';
import { isCustomFormulaType } from '../utils/formula';
import { DataValidationCacheService } from './dv-cache.service';
type RuleId = string;
@@ -85,23 +86,26 @@ export class DataValidationFormulaService extends Disposable {
return this._registerOtherFormulaService.registerFormulaWithRange(unitId, subUnitId, formula, ranges, { ruleId });
}
addRule(unitId: string, subUnitId: string, ruleId: string, formula1: string | undefined, formula2: string | undefined, ranges: IRange[]) {
const isFormula1Legal = isFormulaString(formula1);
const isFormula2Legal = isFormulaString(formula2);
if (!isFormula1Legal && !isFormula2Legal) {
return;
addRule(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
if (!isCustomFormulaType(rule.type) && rule.type !== DataValidationType.CHECKBOX) {
const { formula1, formula2, uid: ruleId } = rule;
const isFormula1Legal = isFormulaString(formula1);
const isFormula2Legal = isFormulaString(formula2);
if (!isFormula1Legal && !isFormula2Legal) {
return;
}
const formulaRuleMap = this._ensureRuleFormulaMap(unitId, subUnitId);
const item: [IFormulaInfo | undefined, IFormulaInfo | undefined] = [undefined, undefined];
if (isFormula1Legal) {
const id = this._registerSingleFormula(unitId, subUnitId, formula1!, ruleId);
item[0] = { id, text: formula1! };
}
if (isFormula2Legal) {
const id = this._registerSingleFormula(unitId, subUnitId, formula2!, ruleId);
item[1] = { id, text: formula2! };
}
formulaRuleMap.set(ruleId, item);
}
const formulaRuleMap = this._ensureRuleFormulaMap(unitId, subUnitId);
const item: [IFormulaInfo | undefined, IFormulaInfo | undefined] = [undefined, undefined];
if (isFormula1Legal) {
const id = this._registerSingleFormula(unitId, subUnitId, formula1!, ruleId);
item[0] = { id, text: formula1! };
}
if (isFormula2Legal) {
const id = this._registerSingleFormula(unitId, subUnitId, formula2!, ruleId);
item[1] = { id, text: formula2! };
}
formulaRuleMap.set(ruleId, item);
}
removeRule(unitId: string, subUnitId: string, ruleId: string) {
@@ -115,42 +119,6 @@ export class DataValidationFormulaService extends Disposable {
idList.length && this._registerOtherFormulaService.deleteFormula(unitId, subUnitId, idList);
}
updateRuleFormulaText(unitId: string, subUnitId: string, ruleId: string, formula1: string | undefined, formula2: string | undefined, ranges: IRange[]) {
const formulaRuleMap = this._ensureRuleFormulaMap(unitId, subUnitId);
const item = formulaRuleMap.get(ruleId);
if (!item) {
this.addRule(unitId, subUnitId, ruleId, formula1, formula2, ranges);
return;
}
const [oldFormula1, oldFormula2] = item;
if (oldFormula1?.text !== formula1) {
oldFormula1 && this._registerOtherFormulaService.deleteFormula(unitId, subUnitId, [oldFormula1.id]);
if (isFormulaString(formula1)) {
const formulaId = this._registerSingleFormula(unitId, subUnitId, formula1!, ruleId);
item[0] = {
text: formula1!,
id: formulaId,
};
} else {
item[0] = undefined;
};
}
if (oldFormula2?.text !== formula2) {
oldFormula2 && this._registerOtherFormulaService.deleteFormula(unitId, subUnitId, [oldFormula2.id]);
if (isFormulaString(formula2)) {
const formulaId = this._registerSingleFormula(unitId, subUnitId, formula2!, ruleId);
item[1] = {
text: formula2!,
id: formulaId,
};
} else {
item[1] = undefined;
};
}
}
getRuleFormulaResult(unitId: string, subUnitId: string, ruleId: string): Promise<Nullable<[Nullable<IOtherFormulaResult>, Nullable<IOtherFormulaResult>]>> {
const ruleFormulaMap = this._ensureRuleFormulaMap(unitId, subUnitId);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { ICellData, Nullable } from '@univerjs/core';
import { DataValidationType, type ICellData, type Nullable } from '@univerjs/core';
import { ERROR_TYPE_SET } from '@univerjs/engine-formula';
export function getFormulaResult(result: Nullable<Nullable<ICellData>[][]>) {
@@ -28,3 +28,10 @@ export function getFormulaCellData(result: Nullable<Nullable<ICellData>[][]>) {
export function isLegalFormulaResult(res: string) {
return !(ERROR_TYPE_SET as Set<string>).has(res);
}
/**
* Judge if the data-validation's formula need to be offseted by ranges
*/
export function isCustomFormulaType(type: DataValidationType) {
return type !== DataValidationType.LIST && type !== DataValidationType.LIST_MULTIPLE && type !== DataValidationType.CHECKBOX && type !== DataValidationType.ANY;
}
@@ -20,9 +20,9 @@ import { DataValidationOperator, DataValidationType, isFormulaString, numfmt, To
import { BaseDataValidator } from '@univerjs/data-validation';
import dayjs from 'dayjs';
import { DateOperatorErrorTitleMap, DateOperatorNameMap, DateOperatorTitleMap } from '../common/date-text-map';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { TWO_FORMULA_OPERATOR_COUNT } from '../types/const/two-formula-operators';
import { getFormulaResult, isLegalFormulaResult } from '../utils/formula';
import { isLegalFormulaResult } from '../utils/formula';
const FORMULA1 = '{FORMULA1}';
const FORMULA2 = '{FORMULA2}';
@@ -61,32 +61,22 @@ export class DateValidator extends BaseDataValidator<number> {
];
scopes: string | string[] = ['sheet'];
private _formulaService = this.injector.get(DataValidationFormulaService);
private _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
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);
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult<number | undefined>> {
const results = await this._formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
const { formula1, formula2 } = rule;
const formulaResult1 = getFormulaResult(results?.[0]?.result?.[0][0]);
const formulaResult2 = getFormulaResult(results?.[1]?.result?.[0][0]);
const isFormulaValid = isLegalFormulaResult(String(formulaResult1)) && isLegalFormulaResult(String(formulaResult2));
return {
formula1: transformDate2SerialNumber(isFormulaString(formula1) ? formulaResult1 : formula1),
formula2: transformDate2SerialNumber(isFormulaString(formula2) ? formulaResult2 : formula2),
formula1: transformDate2SerialNumber(isFormulaString(formula1) ? formulaResult1?.v : formula1),
formula2: transformDate2SerialNumber(isFormulaString(formula2) ? formulaResult2?.v : formula2),
isFormulaValid,
};
}
parseFormulaSync(rule: IDataValidationRule, unitId: string, subUnitId: string) {
const results = this._formulaService.getRuleFormulaResultSync(unitId, subUnitId, rule.uid);
const { formula1, formula2 } = rule;
return {
formula1: transformDate2SerialNumber(isFormulaString(formula1) ? getFormulaResult(results?.[0]?.result?.[0][0]) : formula1),
formula2: transformDate2SerialNumber(isFormulaString(formula2) ? getFormulaResult(results?.[1]?.result?.[0][0]) : formula2),
};
}
override async isValidType(info: IValidatorCellInfo): Promise<boolean> {
const { interceptValue, value } = info;
if (typeof value === 'number' && typeof interceptValue === 'string') {
@@ -18,9 +18,9 @@ import type { CellValue, IDataValidationRule, Nullable } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import { DataValidationOperator, DataValidationType, isFormulaString, Tools } from '@univerjs/core';
import { BaseDataValidator } from '@univerjs/data-validation';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { TWO_FORMULA_OPERATOR_COUNT } from '../types/const/two-formula-operators';
import { getFormulaResult, isLegalFormulaResult } from '../utils/formula';
import { isLegalFormulaResult } from '../utils/formula';
export function getCellValueNumber(cellValue: CellValue) {
let str = cellValue;
@@ -36,8 +36,7 @@ export function getCellValueNumber(cellValue: CellValue) {
}
export class DecimalValidator extends BaseDataValidator<number> {
private _formulaService = this.injector.get(DataValidationFormulaService);
private _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
id: string = DataValidationType.DECIMAL;
title: string = 'dataValidation.decimal.title';
@@ -81,17 +80,16 @@ export class DecimalValidator extends BaseDataValidator<number> {
return +formula;
}
async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult> {
const formulaInfo = await this._formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult> {
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;
const formulaResult1 = getFormulaResult(formulaInfo?.[0]?.result?.[0][0]);
const formulaResult2 = getFormulaResult(formulaInfo?.[1]?.result?.[0][0]);
const isFormulaValid = isLegalFormulaResult(String(formulaResult1)) && isLegalFormulaResult(String(formulaResult2));
const info = {
formula1: this._parseNumber(isFormulaString(formula1) ? formulaResult1 : formula1),
formula2: this._parseNumber(isFormulaString(formula2) ? formulaResult2 : formula2),
formula1: this._parseNumber(isFormulaString(formula1) ? formulaResult1?.v : formula1),
formula2: this._parseNumber(isFormulaString(formula2) ? formulaResult2?.v : formula2),
isFormulaValid,
};
@@ -18,9 +18,9 @@ import type { CellValue, IDataValidationRule, IDataValidationRuleBase, Nullable
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import { DataValidationOperator, DataValidationType, isFormulaString, Tools } from '@univerjs/core';
import { BaseDataValidator, TextLengthErrorTitleMap } from '@univerjs/data-validation';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { TWO_FORMULA_OPERATOR_COUNT } from '../types/const/two-formula-operators';
import { getFormulaResult, isLegalFormulaResult } from '../utils/formula';
import { isLegalFormulaResult } from '../utils/formula';
const FORMULA1 = '{FORMULA1}';
const FORMULA2 = '{FORMULA2}';
@@ -42,7 +42,7 @@ export class TextLengthValidator extends BaseDataValidator<number> {
scopes: string | string[] = ['sheet'];
private _formulaService = this.injector.get(DataValidationFormulaService);
private _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
private _isFormulaOrInt(formula: string) {
return !Tools.isBlank(formula) && (isFormulaString(formula) || (!Number.isNaN(+formula) && Number.isInteger(+formula)));
@@ -85,17 +85,16 @@ export class TextLengthValidator extends BaseDataValidator<number> {
return !Number.isNaN(formula);
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult<any>> {
const formulaInfo = await this._formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult<any>> {
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;
const formulaResult1 = getFormulaResult(formulaInfo?.[0]?.result?.[0][0]);
const formulaResult2 = getFormulaResult(formulaInfo?.[1]?.result?.[0][0]);
const isFormulaValid = isLegalFormulaResult(String(formulaResult1)) && isLegalFormulaResult(String(formulaResult2));
return {
formula1: this._parseNumber(isFormulaString(formula1) ? formulaResult1 : formula1),
formula2: this._parseNumber(isFormulaString(formula2) ? formulaResult2 : formula2),
formula1: this._parseNumber(isFormulaString(formula1) ? formulaResult1?.v : formula1),
formula2: this._parseNumber(isFormulaString(formula2) ? formulaResult2?.v : formula2),
isFormulaValid,
};
}
@@ -18,13 +18,13 @@ import type { CellValue, IDataValidationRule, IDataValidationRuleBase, Nullable
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import { DataValidationOperator, DataValidationType, isFormulaString, Tools } from '@univerjs/core';
import { BaseDataValidator } from '@univerjs/data-validation';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { TWO_FORMULA_OPERATOR_COUNT } from '../types/const/two-formula-operators';
import { isLegalFormulaResult } from '../utils/formula';
import { getCellValueNumber } from './decimal-validator';
export class WholeValidator extends BaseDataValidator<number> {
private _formulaService = this.injector.get(DataValidationFormulaService);
private _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
id: string = DataValidationType.WHOLE;
title: string = 'dataValidation.whole.title';
@@ -68,12 +68,13 @@ export class WholeValidator extends BaseDataValidator<number> {
return +formula;
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult> {
const formulaInfo = await this._formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult> {
const res1 = await this._customFormulaService.getCellFormulaValue(unitId, subUnitId, rule.uid, row, column);
const res2 = await this._customFormulaService.getCellFormula2Value(unitId, subUnitId, rule.uid, row, column);
const { formula1, formula2 } = rule;
const formula1Result = isFormulaString(formula1) ? formulaInfo?.[0]?.result?.[0]?.[0]?.[0][0]?.v : formula1;
const formula2Result = isFormulaString(formula2) ? formulaInfo?.[1]?.result?.[0]?.[0]?.[0][0]?.v : formula2;
const formula1Result = isFormulaString(formula1) ? res1?.v : formula1;
const formula2Result = isFormulaString(formula2) ? res2?.v : formula2;
const isFormulaValid = isLegalFormulaResult(`${formula1Result}`) && isLegalFormulaResult(`${formula2Result}`);
const info = {
@@ -0,0 +1,299 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ITestBed } from './util';
import { ICommandService, type IRange } from '@univerjs/core';
import { InsertColCommand, MoveRangeCommand, RemoveColCommand } from '@univerjs/sheets';
import { beforeEach, describe, expect, it } from 'vitest';
import { FormulaRefRangeService } from '../formula-ref-range.service';
import { createCommandTestBed } from './util';
const MoveRangeCommandId = 'sheet.command.move-range';
describe('FormulaRefRangeService', () => {
let testBed: ITestBed;
beforeEach(() => {
testBed = createCommandTestBed();
});
it('transform formula string with move range', () => {
const formulaRefRangeService = testBed.get(FormulaRefRangeService);
const resFormula = formulaRefRangeService.transformFormulaByEffectCommand(
'test',
'sheet1',
'=SUM(A1)',
{
id: MoveRangeCommandId,
params: {
fromRange: {
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
},
toRange: {
startRow: 2,
endRow: 2,
startColumn: 1,
endColumn: 1,
},
},
}
);
expect(resFormula).toBe('=SUM(B3)');
});
it('not transform formula string with move range', () => {
const formulaRefRangeService = testBed.get(FormulaRefRangeService);
const resFormula = formulaRefRangeService.transformFormulaByEffectCommand(
'test',
'sheet1',
'=SUM(A1:A10)',
{
id: MoveRangeCommandId,
params: {
fromRange: {
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
},
toRange: {
startRow: 2,
endRow: 2,
startColumn: 1,
endColumn: 1,
},
},
}
);
expect(resFormula).toBe('=SUM(A1:A10)');
});
it('transform range formula string with move range', async () => {
const formulaRefRangeService = testBed.get(FormulaRefRangeService);
let newFormulas: {
formulas: string[];
ranges: IRange[];
}[] = [];
formulaRefRangeService.registerRangeFormula(
'test',
'sheet1',
[{
startRow: 5,
endRow: 10,
startColumn: 5,
endColumn: 5,
}],
['=SUM(A1)'],
(res) => {
newFormulas = res;
return {
redos: [],
undos: [],
};
}
);
await testBed.get(ICommandService).executeCommand(
MoveRangeCommand.id,
{
fromRange: {
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
},
toRange: {
startRow: 2,
endRow: 2,
startColumn: 1,
endColumn: 1,
},
}
);
expect(newFormulas).toEqual(
[
{ formulas: ['=SUM(A3)'], ranges: [{ startColumn: 5, endColumn: 5, startRow: 7, endRow: 10 }] },
{ formulas: ['=SUM(B3)'], ranges: [{ startColumn: 5, endColumn: 5, startRow: 5, endRow: 6 }] },
]
);
});
it('not transform range formula string with move range', async () => {
const formulaRefRangeService = testBed.get(FormulaRefRangeService);
let newFormulas: {
formulas: string[];
ranges: IRange[];
}[] = [];
formulaRefRangeService.registerRangeFormula(
'test',
'sheet1',
[{
startRow: 5,
endRow: 10,
startColumn: 5,
endColumn: 5,
}],
['=SUM(A1:A10)'],
(res) => {
newFormulas = res;
return {
redos: [],
undos: [],
};
}
);
await testBed.get(ICommandService).executeCommand(
MoveRangeCommand.id,
{
fromRange: {
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
},
toRange: {
startRow: 2,
endRow: 2,
startColumn: 1,
endColumn: 1,
},
}
);
expect(newFormulas).toEqual(
[
{
formulas: ['=SUM(A1:A10)'],
ranges: [{
startRow: 5,
endRow: 10,
startColumn: 5,
endColumn: 5,
}],
},
]
);
});
it('transform range formula string with delete col', async () => {
const formulaRefRangeService = testBed.get(FormulaRefRangeService);
let newFormulas: {
formulas: string[];
ranges: IRange[];
}[] = [];
formulaRefRangeService.registerRangeFormula(
'test',
'sheet1',
[{
startRow: 5,
endRow: 10,
startColumn: 5,
endColumn: 10,
}],
['=A1'],
(res) => {
newFormulas = res;
return {
redos: [],
undos: [],
};
}
);
await testBed.get(ICommandService).executeCommand(
RemoveColCommand.id,
{
range: {
startColumn: 0,
endColumn: 1,
startRow: 0,
endRow: 9999,
},
}
);
expect(newFormulas).toEqual(
[
{ formulas: ['=A1'], ranges: [{ startColumn: 5, endColumn: 8, startRow: 5, endRow: 10 }] },
{ formulas: ['=#REF!'], ranges: [{ startColumn: 3, endColumn: 4, startRow: 5, endRow: 10 }] },
]
);
});
it('transform range formula string with insert col', async () => {
const formulaRefRangeService = testBed.get(FormulaRefRangeService);
let newFormulas: {
formulas: string[];
ranges: IRange[];
}[] = [];
formulaRefRangeService.registerRangeFormula(
'test',
'sheet1',
[{
startRow: 5,
endRow: 10,
startColumn: 5,
endColumn: 10,
}],
['=A1'],
(res) => {
newFormulas = res;
return {
redos: [],
undos: [],
};
}
);
await testBed.get(ICommandService).executeCommand(
InsertColCommand.id,
{
unitId: 'test',
subUnitId: 'sheet1',
range: {
startColumn: 0,
endColumn: 1,
startRow: 0,
endRow: 9999,
},
}
);
expect(newFormulas).toEqual(
[
{
formulas: ['=C1'],
ranges: [{ startColumn: 7, endColumn: 12, startRow: 5, endRow: 10 }],
},
]
);
});
});
@@ -0,0 +1,136 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
import { ICommandService, ILogService, Inject, Injector, IUniverInstanceService, LocaleType, LogLevel, Plugin, Univer, UniverInstanceType } from '@univerjs/core';
import { DocSelectionManagerService } from '@univerjs/docs';
import { CalculateFormulaService, DefinedNamesService, FormulaCurrentConfigService, FormulaDataModel, FormulaRuntimeService, ICalculateFormulaService, IDefinedNamesService, IFormulaCurrentConfigService, IFormulaRuntimeService, LexerTreeBuilder } from '@univerjs/engine-formula';
import { DefinedNameDataController, InsertColCommand, IRefSelectionsService, MoveColsCommand, MoveRangeCommand, RangeProtectionRuleModel, RefRangeService, RemoveColCommand, SheetInterceptorService, SheetsSelectionsService, WorkbookPermissionService, WorksheetPermissionService, WorksheetProtectionPointModel, WorksheetProtectionRuleModel } from '@univerjs/sheets';
import { FormulaRefRangeService } from '../formula-ref-range.service';
const TEST_WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 1,
},
1: {
f: '=SUM(A1)',
},
},
1: {
1: {
v: 1,
},
},
},
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
export interface ITestBed {
univer: Univer;
get: Injector['get'];
sheet: Workbook;
}
export function createCommandTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
const univer = new Univer();
const injector = univer.__getInjector();
const get = injector.get.bind(injector);
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
private _formulaDataModel: FormulaDataModel | null = null;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
const injector = this._injector;
// injector.add([ICommandService, { useClass: CommandService }]);
injector.add([WorksheetPermissionService]);
injector.add([WorksheetProtectionPointModel]);
injector.add([RangeProtectionRuleModel]);
injector.add([WorkbookPermissionService]);
injector.add([WorksheetProtectionRuleModel]);
injector.add([SheetsSelectionsService]);
injector.add([SheetInterceptorService]);
injector.add([ICalculateFormulaService, { useClass: CalculateFormulaService }]);
injector.add([FormulaDataModel]);
injector.add([LexerTreeBuilder]);
injector.add([DocSelectionManagerService]);
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
injector.add([DefinedNameDataController]);
injector.add([IFormulaRuntimeService, { useClass: FormulaRuntimeService }]);
injector.add([IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }]);
injector.add([IRefSelectionsService, { useClass: SheetsSelectionsService }]);
injector.add([RefRangeService]);
injector.add([FormulaRefRangeService]);
dependencies?.forEach((d) => injector.add(d));
this._injector.get(SheetInterceptorService);
this._injector.get(WorkbookPermissionService);
this._injector.get(WorksheetPermissionService);
this._injector.get(DefinedNameDataController);
[
MoveRangeCommand,
RemoveColCommand,
InsertColCommand,
MoveColsCommand,
].forEach((command) => {
this._injector.get(ICommandService).registerCommand(command);
});
}
override onReady(): void {
this._formulaDataModel = get(FormulaDataModel);
this._formulaDataModel.initFormulaData();
}
}
univer.registerPlugin(TestPlugin);
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData || TEST_WORKBOOK_DATA_DEMO);
const univerInstanceService = injector.get(IUniverInstanceService);
univerInstanceService.focusUnit('test');
const logService = injector.get(ILogService);
logService.setLogLevel(LogLevel.SILENT); // change this to `LogLevel.VERBOSE` to debug tests via logs
return {
univer,
get,
sheet,
};
}
@@ -16,9 +16,9 @@
import type { IDisposable, IMutationInfo, IRange, Workbook } from '@univerjs/core';
import type { EffectRefRangeParams } from '@univerjs/sheets';
import { Disposable, DisposableCollection, Inject, IUniverInstanceService, moveRangeByOffset, UniverInstanceType } from '@univerjs/core';
import { AbsoluteRefType, Disposable, DisposableCollection, getIntersectRange, Inject, Injector, isFormulaString, IUniverInstanceService, moveRangeByOffset, Rectangle, UniverInstanceType } from '@univerjs/core';
import { deserializeRangeWithSheetWithCache, ErrorType, generateStringWithSequence, LexerTreeBuilder, sequenceNodeType, serializeRange, serializeRangeWithSheet, serializeRangeWithSpreadsheet } from '@univerjs/engine-formula';
import { handleDefaultRangeChangeWithEffectRefCommands, RefRangeService } from '@univerjs/sheets';
import { getSeparateEffectedRangesOnCommand, handleCommonDefaultRangeChangeWithEffectRefCommands, handleDefaultRangeChangeWithEffectRefCommands, RefRangeService } from '@univerjs/sheets';
export type FormulaChangeMap = Record<string, Record<string, Record<string, string>>>;
@@ -27,16 +27,55 @@ export type FormulaChangeCallback = (formulaString: string) => {
undos: IMutationInfo[];
};
export type RangeFormulaChangeCallback = (formulas: { formulaString: string; ranges: IRange[] }[]) => {
export type RangeFormulaChangeCallback = (infos: { formulas: string[]; ranges: IRange[] }[]) => {
redos: IMutationInfo[];
undos: IMutationInfo[];
};
function getFormulaKeyOffset(lexerTreeBuilder: LexerTreeBuilder, formulaString: string, refOffsetX: number, refOffsetY: number) {
const sequenceNodes = lexerTreeBuilder.sequenceNodesBuilder(formulaString);
if (sequenceNodes == null) {
return formulaString;
}
const newSequenceNodes: Array<{ unitId: string; sheetName: string; range: IRange }> = [];
for (let i = 0, len = sequenceNodes.length; i < len; i++) {
const node = sequenceNodes[i];
if (typeof node === 'string' || node.nodeType !== sequenceNodeType.REFERENCE) {
continue;
}
const { token } = node;
const sequenceGrid = deserializeRangeWithSheetWithCache(token);
const { sheetName, unitId: sequenceUnitId } = sequenceGrid;
let newRange: IRange = sequenceGrid.range;
if (newRange.startAbsoluteRefType === AbsoluteRefType.ALL && newRange.endAbsoluteRefType === AbsoluteRefType.ALL) {
continue;
} else {
newRange = moveRangeByOffset(newRange, refOffsetX, refOffsetY);
}
newSequenceNodes.push({
unitId: sequenceUnitId,
sheetName,
range: newRange,
});
}
return newSequenceNodes.map((item) => `${item.unitId}!${item.sheetName}!${item.range.startRow}!${item.range.endRow}!${item.range.startColumn}!${item.range.endColumn}`).join('|');
}
export class FormulaRefRangeService extends Disposable {
constructor(
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
@Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(Injector) private readonly _injector: Injector
) {
super();
}
@@ -78,7 +117,7 @@ export class FormulaRefRangeService extends Disposable {
newToken = serializeRange(finalRange);
}
} else {
newToken = ErrorType.ERROR;
newToken = ErrorType.REF;
}
return {
@@ -126,7 +165,7 @@ export class FormulaRefRangeService extends Disposable {
newToken = serializeRange(finalRange);
}
} else {
newToken = ErrorType.ERROR;
newToken = ErrorType.REF;
}
return {
@@ -165,4 +204,210 @@ export class FormulaRefRangeService extends Disposable {
return disposableCollection;
}
private _getFormulaDependcy(unitId: string, subUnitId: string, formula: string | undefined, ranges: IRange[]) {
const nodes = isFormulaString(formula) ? this._lexerTreeBuilder.sequenceNodesBuilder(formula!) : null;
const dependencyRanges: { unitId: string; subUnitId: string; ranges: IRange[] }[] = [];
nodes?.forEach((node) => {
if (typeof node === 'object' && node.nodeType === sequenceNodeType.REFERENCE) {
const gridRangeName = deserializeRangeWithSheetWithCache(node.token);
const { range, unitId: rangeUnitId, sheetName: rangeSheetName } = gridRangeName;
// ignore all absolute reference
if (range.startAbsoluteRefType === AbsoluteRefType.ALL && range.endAbsoluteRefType === AbsoluteRefType.ALL) {
return;
}
const workbook = this._univerInstanceService.getUnit<Workbook>(rangeUnitId || unitId);
const worksheet = rangeSheetName ? workbook?.getSheetBySheetName(rangeSheetName) : workbook?.getSheetBySheetId(subUnitId);
if (!worksheet) {
return;
}
const realUnitId = workbook!.getUnitId();
const realSheetId = worksheet.getSheetId();
const orginStartRow = ranges[0].startRow;
const orginStartColumn = ranges[0].startColumn;
const currentStartRow = range.startRow;
const currentStartColumn = range.startColumn;
const offsetRanges = ranges.map((range) => ({
startRow: range.startRow - orginStartRow + currentStartRow,
endRow: range.endRow - orginStartRow + currentStartRow,
startColumn: range.startColumn - orginStartColumn + currentStartColumn,
endColumn: range.endColumn - orginStartColumn + currentStartColumn,
}));
dependencyRanges.push({
unitId: realUnitId,
subUnitId: realSheetId,
ranges: offsetRanges,
});
}
});
return dependencyRanges;
}
// eslint-disable-next-line max-lines-per-function
registerRangeFormula(unitId: string, subUnitId: string, oldRanges: IRange[], formulas: string[], callback: RangeFormulaChangeCallback): IDisposable {
const disposableCollection = new DisposableCollection();
const formulaDeps = formulas.map((formula) => this._getFormulaDependcy(unitId, subUnitId, formula, oldRanges));
// eslint-disable-next-line max-lines-per-function
const handleRangeChange = (commandInfo: EffectRefRangeParams) => {
const orginStartRow = oldRanges[0].startRow;
const orginStartColumn = oldRanges[0].startColumn;
const deps = [{ unitId, subUnitId, ranges: oldRanges }, ...formulaDeps.flat()];
const matchedEffectedRanges: IRange[][] = [];
const effectedRanges = getSeparateEffectedRangesOnCommand(this._injector, commandInfo);
// 1. calculate effected ranges
for (const { unitId: depUnitId, subUnitId: depSubUnitId, ranges } of deps) {
if (depUnitId === effectedRanges.unitId && depSubUnitId === effectedRanges.subUnitId) {
const intersectedRanges: IRange[] = [];
const currentStartRow = ranges[0].startRow;
const currentStartColumn = ranges[0].startColumn;
const offsetRow = currentStartRow - orginStartRow;
const offsetColumn = currentStartColumn - orginStartColumn;
for (const range of effectedRanges.ranges) {
const intersectedRange = ranges.map((r) => getIntersectRange(range, r)).filter(Boolean) as IRange[];
if (intersectedRange.length > 0) {
intersectedRanges.push(...intersectedRange);
}
}
if (intersectedRanges.length > 0) {
matchedEffectedRanges.push(
intersectedRanges.map((range) => ({
startRow: range.startRow - offsetRow,
endRow: range.endRow - offsetRow,
startColumn: range.startColumn - offsetColumn,
endColumn: range.endColumn - offsetColumn,
}))
);
}
}
}
if (matchedEffectedRanges.length > 0) {
// 2. split effected ranges to enusre there is no overlap
const ranges = Rectangle.splitIntoGrid([...matchedEffectedRanges.flat()]);
const noEffectRanges = Rectangle.subtractMulti(oldRanges, ranges);
noEffectRanges.sort((a, b) => a.startRow - b.startRow || a.startColumn - b.startColumn);
// 3. calculate every effected range to get new range and new formula
const keyMap = new Map<string, { formulas: { newFormula: string }[]; ranges: IRange[] }[]>();
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
const currentRow = range.startRow;
const currentColumn = range.startColumn;
const offsetRow = currentRow - orginStartRow;
const offsetColumn = currentColumn - orginStartColumn;
const transformedRange = handleCommonDefaultRangeChangeWithEffectRefCommands(range, commandInfo).sort((a, b) => a.startRow - b.startRow || a.startColumn - b.startColumn);
if (!transformedRange.length) {
continue;
}
const transformedRow = transformedRange[0].startRow;
const transformedColumn = transformedRange[0].startColumn;
const transformedOffsetRow = transformedRow - orginStartRow;
const transformedOffsetColumn = transformedColumn - orginStartColumn;
const transformedFormulas = [];
for (let j = 0; j < formulas.length; j++) {
const formula = formulas[j];
const isFormulaFormulaString = isFormulaString(formula);
// 3.1 move formula ref offset to get formula string for this range before this command
const formulaString = isFormulaFormulaString ? this._lexerTreeBuilder.moveFormulaRefOffset(formula!, offsetColumn, offsetRow) : formula!;
// 3.2 transform formula by effect command
const newFormula = isFormulaFormulaString ? this.transformFormulaByEffectCommand(unitId, subUnitId, formulaString, commandInfo) : formulaString;
// 3.3 get formula key offset for this range after this command
const orginFormula = getFormulaKeyOffset(this._lexerTreeBuilder, newFormula, -transformedOffsetColumn, -transformedOffsetRow);
transformedFormulas.push({
newFormula,
orginFormula,
});
}
const item = {
formulas: transformedFormulas,
ranges: transformedRange,
key: transformedFormulas.map((item) => item.orginFormula).join('_'),
};
if (keyMap.has(item.key)) {
keyMap.get(item.key)!.push(item);
} else {
keyMap.set(item.key, [item]);
}
}
// 4. handle no effected ranges
const originKey = formulas.map((item) => getFormulaKeyOffset(this._lexerTreeBuilder, item, 0, 0)).join('_');
if (noEffectRanges.length > 0) {
const currentRow = noEffectRanges[0].startRow;
const currentColumn = noEffectRanges[0].startColumn;
const noEffectFormulas = [];
for (let i = 0; i < formulas.length; i++) {
const formula = formulas[i];
noEffectFormulas.push({
newFormula: isFormulaString(formula) ? this._lexerTreeBuilder.moveFormulaRefOffset(formula, currentColumn - orginStartColumn, currentRow - orginStartRow) : formula,
orginFormula: formula,
});
}
const item = {
formulas: noEffectFormulas,
ranges: noEffectRanges,
key: originKey,
};
if (keyMap.has(item.key)) {
keyMap.get(item.key)!.push(item);
} else {
keyMap.set(item.key, [item]);
}
}
// 5. merge effected ranges and get new formula,
// if origin formula was same, means these ranges can use same formula
const res = [];
const keys = Array.from(keyMap.keys());
for (let i = keys.length - 1; i >= 0; i--) {
const key = keys[i];
const ranges = keyMap.get(key)!.sort((a, b) => a.ranges[0].startRow - b.ranges[0].startRow || a.ranges[0].startColumn - b.ranges[0].startColumn);
const formulas = [];
for (let j = 0; j < ranges[0].formulas.length; j++) {
formulas.push(ranges[0].formulas[j].newFormula);
}
const newRanges = Rectangle.mergeRanges(ranges.map((item) => item.ranges).flat());
newRanges.sort((a, b) => a.startRow - b.startRow || a.startColumn - b.startColumn);
res.push({
formulas,
ranges: newRanges,
});
}
return callback(res);
}
return {
undos: [],
redos: [],
};
};
oldRanges.forEach((range) => {
const disposable = this._refRangeService.registerRefRange(range, handleRangeChange, unitId, subUnitId);
disposableCollection.add(disposable);
});
[...formulaDeps.flat()].forEach(({ unitId, subUnitId, ranges }) => {
ranges.forEach((range) => {
const disposable = this._refRangeService.registerRefRange(range, handleRangeChange, unitId, subUnitId);
disposableCollection.add(disposable);
});
});
return disposableCollection;
}
}
@@ -84,11 +84,13 @@ export class SheetsHyperLinkRichTextRefRangeController extends Disposable {
if (p.body?.customRanges?.some((customRange) => customRange.rangeType === CustomRangeType.HYPERLINK && this._isLegalRangeUrl(unitId, customRange.properties?.url))) {
const disposableCollection = new DisposableCollection();
let hasWatch = false;
p.body?.customRanges?.forEach((customRange) => {
if (customRange.rangeType === CustomRangeType.HYPERLINK) {
const payload = customRange.properties?.url;
const range = this._isLegalRangeUrl(unitId, payload);
if (range) {
hasWatch = true;
disposableCollection.add(this._refRangeService.watchRange(unitId, subUnitId, range, (before, after) => {
customRange.properties!.url = `#gid=${subUnitId}&range=${after ? serializeRange(after) : ERROR_RANGE}`;
}));
@@ -96,7 +98,9 @@ export class SheetsHyperLinkRichTextRefRangeController extends Disposable {
}
});
map.setValue(row, col, disposableCollection);
if (hasWatch) {
map.setValue(row, col, disposableCollection);
}
}
}
@@ -14,6 +14,11 @@
* limitations under the License.
*/
import type { IAccessor, ICommand, IRange, Worksheet } from '@univerjs/core';
import type { ISelectionWithStyle } from '../../basics';
import type { IMoveColumnsMutationParams, IMoveRowsMutationParams } from '../mutations/move-rows-cols.mutation';
import type { ISetSelectionsOperationParams } from '../operations/selection.operation';
import {
CommandType,
ErrorService,
@@ -25,8 +30,6 @@ import {
Rectangle,
sequenceExecute,
} from '@univerjs/core';
import type { IAccessor, ICommand, IRange, Worksheet } from '@univerjs/core';
import { SheetsSelectionsService } from '../../services/selections/selection-manager.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import {
@@ -38,9 +41,6 @@ import {
import { SetSelectionsOperation } from '../operations/selection.operation';
import { alignToMergedCellsBorders, getPrimaryForRange } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
import type { ISelectionWithStyle } from '../../basics';
import type { IMoveColumnsMutationParams, IMoveRowsMutationParams } from '../mutations/move-rows-cols.mutation';
import type { ISetSelectionsOperationParams } from '../operations/selection.operation';
export interface IMoveRowsCommandParams {
unitId?: string;
+1
View File
@@ -67,6 +67,7 @@ export type { EffectRefRangeParams, IOperator } from './services/ref-range/type'
export { EffectRefRangId, OperatorType } from './services/ref-range/type';
export { DefinedNameDataController } from './controllers/defined-name-data.controller';
export {
getSeparateEffectedRangesOnCommand,
handleBaseInsertRange,
handleBaseMoveRowsCols,
handleBaseRemoveRange,
+310 -18
View File
@@ -14,23 +14,12 @@
* limitations under the License.
*/
import { ObjectMatrix, queryObjectMatrix, Range, RANGE_TYPE, Rectangle } from '@univerjs/core';
import type { ICommandInfo, IMutationInfo, IRange, Nullable } from '@univerjs/core';
import { DeleteRangeMoveLeftCommand } from '../../commands/commands/delete-range-move-left.command';
import { DeleteRangeMoveUpCommand } from '../../commands/commands/delete-range-move-up.command';
import { InsertRangeMoveDownCommand } from '../../commands/commands/insert-range-move-down.command';
import { InsertRangeMoveRightCommandId } from '../../commands/commands/insert-range-move-right.command';
import { InsertColMutation, InsertRowMutation } from '../../commands/mutations/insert-row-col.mutation';
import { type IMoveRangeMutationParams, MoveRangeMutation } from '../../commands/mutations/move-range.mutation';
import { type IMoveColumnsMutationParams, type IMoveRowsMutationParams, MoveColsMutation, MoveRowsMutation } from '../../commands/mutations/move-rows-cols.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../../commands/mutations/remove-row-col.mutation';
import { RemoveSheetMutation } from '../../commands/mutations/remove-sheet.mutation';
import { EffectRefRangId, OperatorType } from './type';
import type { IAccessor, ICommandInfo, IMutationInfo, IRange, Nullable } from '@univerjs/core';
import type { IInsertColMutationParams, IInsertRowMutationParams, IRemoveColMutationParams, IRemoveRowsMutationParams, IRemoveSheetMutationParams } from '../../basics';
import type { IInsertColCommandParams, IInsertRowCommandParams } from '../../commands/commands/insert-row-col.command';
import type { IRemoveRowColCommandInterceptParams } from '../../commands/commands/remove-row-col.command';
import type { ISheetCommandSharedParams } from '../../commands/utils/interface';
import type { SheetsSelectionsService } from '../selections/selection-manager.service';
import type {
EffectRefRangeParams,
IDeleteRangeMoveLeftCommand,
@@ -46,6 +35,19 @@ import type {
IRemoveRowColCommand,
IReorderRangeCommand,
} from './type';
import { Direction, IUniverInstanceService, ObjectMatrix, queryObjectMatrix, Range, RANGE_TYPE, Rectangle } from '@univerjs/core';
import { DeleteRangeMoveLeftCommand } from '../../commands/commands/delete-range-move-left.command';
import { DeleteRangeMoveUpCommand } from '../../commands/commands/delete-range-move-up.command';
import { InsertRangeMoveDownCommand } from '../../commands/commands/insert-range-move-down.command';
import { InsertRangeMoveRightCommandId } from '../../commands/commands/insert-range-move-right.command';
import { getSheetCommandTarget } from '../../commands/commands/utils/target-util';
import { InsertColMutation, InsertRowMutation } from '../../commands/mutations/insert-row-col.mutation';
import { type IMoveRangeMutationParams, MoveRangeMutation } from '../../commands/mutations/move-range.mutation';
import { type IMoveColumnsMutationParams, type IMoveRowsMutationParams, MoveColsMutation, MoveRowsMutation } from '../../commands/mutations/move-rows-cols.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../../commands/mutations/remove-row-col.mutation';
import { RemoveSheetMutation } from '../../commands/mutations/remove-sheet.mutation';
import { SheetsSelectionsService } from '../selections/selection-manager.service';
import { EffectRefRangId, OperatorType } from './type';
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
export const handleRangeTypeInput = (range: IRange) => {
@@ -901,6 +903,84 @@ export const handleRemoveRowCommon = (param: IRemoveRowColCommandInterceptParams
return queryObjectMatrix(matrix, (value) => value === 1);
};
export const handleInsertRowCommon = (info: ICommandInfo<IInsertRowCommandParams>, targetRange: IRange) => {
const param = info.params!;
const insertRow = param.range.startRow;
const insertCount = param.range.endRow - param.range.startRow + 1;
const direction = param.direction;
// expand
if (
(direction === Direction.UP && insertRow === targetRange.startRow) ||
(direction === Direction.DOWN && insertRow - 1 === targetRange.endRow)
) {
return [
{
startRow: targetRange.startRow,
endRow: targetRange.endRow + insertCount,
startColumn: targetRange.startColumn,
endColumn: targetRange.endColumn,
},
];
}
if (targetRange.startRow >= insertRow) {
return [{
startRow: targetRange.startRow + insertCount,
endRow: targetRange.endRow + insertCount,
startColumn: targetRange.startColumn,
endColumn: targetRange.endColumn,
}];
} else if (targetRange.endRow <= insertRow) {
return [targetRange];
} else {
return [{
startRow: targetRange.startRow,
endRow: targetRange.endRow + insertCount,
startColumn: targetRange.startColumn,
endColumn: targetRange.endColumn,
}];
}
};
export const handleInsertColCommon = (info: ICommandInfo<IInsertColCommandParams>, targetRange: IRange) => {
const param = info.params!;
const insertColumn = param.range.startColumn;
const insertCount = param.range.endColumn - param.range.startColumn + 1;
const direction = param.direction;
// expand
if (
(direction === Direction.LEFT && insertColumn === targetRange.startColumn) ||
(direction === Direction.RIGHT && insertColumn - 1 === targetRange.endColumn)
) {
return [
{
startRow: targetRange.startRow,
endRow: targetRange.endRow,
startColumn: targetRange.startColumn,
endColumn: targetRange.endColumn + insertCount,
},
];
}
if (targetRange.startColumn >= insertColumn) {
return [{
startRow: targetRange.startRow,
endRow: targetRange.endRow,
startColumn: targetRange.startColumn + insertCount,
endColumn: targetRange.endColumn + insertCount,
}];
} else if (targetRange.endColumn <= insertColumn) {
return [targetRange];
} else {
return [{
startRow: targetRange.startRow,
endRow: targetRange.endRow,
startColumn: targetRange.startColumn,
endColumn: targetRange.endColumn + insertCount,
}];
}
};
export const runRefRangeMutations = (operators: IOperator[], range: IRange) => {
let result: Nullable<IRange> = { ...range };
operators.forEach((operator) => {
@@ -1037,12 +1117,10 @@ export const handleCommonDefaultRangeChangeWithEffectRefCommands = (range: IRang
return handleInsertRangeMoveRightCommon(commandInfo as IInsertRangeMoveRightCommand, range);
}
case EffectRefRangId.InsertColCommandId: {
operator = handleInsertCol(commandInfo as IInsertColCommand, range);
break;
return handleInsertColCommon(commandInfo as IInsertColCommand, range);
}
case EffectRefRangId.InsertRowCommandId: {
operator = handleInsertRow(commandInfo as IInsertRowCommand, range);
break;
return handleInsertRowCommon(commandInfo as IInsertRowCommand, range);
}
case EffectRefRangId.MoveColsCommandId: {
return handleMoveColsCommon(commandInfo as IMoveColsCommand, range);
@@ -1332,3 +1410,217 @@ export function getEffectedRangesOnMutation(mutation: IMutationInfo<MutationsAff
break;
}
}
// eslint-disable-next-line max-lines-per-function, complexity
export function getSeparateEffectedRangesOnCommand(accessor: IAccessor, command: EffectRefRangeParams): {
unitId: string;
subUnitId: string;
ranges: IRange[];
} {
const univerInstanceService = accessor.get(IUniverInstanceService);
const selectionManagerService = accessor.get(SheetsSelectionsService);
switch (command.id) {
case EffectRefRangId.MoveColsCommandId: {
const params = command.params!;
const target = getSheetCommandTarget(univerInstanceService, {
unitId: params.unitId,
subUnitId: params.subUnitId,
})!;
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [
params.fromRange,
{
...params.toRange,
startColumn: params.fromRange.startColumn < params.toRange.startColumn ? params.fromRange.endColumn + 1 : params.toRange.startColumn,
endColumn: params.fromRange.startColumn < params.toRange.startColumn ? params.toRange.endColumn - 1 : params.fromRange.startColumn - 1,
},
],
};
}
case EffectRefRangId.MoveRowsCommandId: {
const params = command.params!;
const target = getSheetCommandTarget(univerInstanceService, {
unitId: params.unitId,
subUnitId: params.subUnitId,
})!;
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [
params.fromRange,
{
...params.toRange,
startRow: params.fromRange.startRow < params.toRange.startRow ? params.fromRange.endRow + 1 : params.toRange.startRow,
endRow: params.fromRange.startRow < params.toRange.startRow ? params.toRange.endRow - 1 : params.fromRange.startRow - 1,
},
],
};
}
case EffectRefRangId.MoveRangeCommandId: {
const params = command.params!;
const target = getSheetCommandTarget(univerInstanceService)!;
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [params.fromRange, params.toRange],
};
}
case EffectRefRangId.InsertRowCommandId: {
const params = command.params!;
const range: IRange = params.range;
return {
unitId: params.unitId,
subUnitId: params.subUnitId,
ranges: [
...range.startRow > 0
? [{
...range,
startRow: range.startRow - 1,
endRow: range.endRow - 1,
}]
: [],
{
...range,
startRow: range.startRow,
endRow: Number.MAX_SAFE_INTEGER,
},
],
};
}
case EffectRefRangId.InsertColCommandId: {
const params = command.params!;
const range: IRange = params.range;
return {
unitId: params.unitId,
subUnitId: params.subUnitId,
ranges: [
...range.startColumn > 0
? [{
...range,
startColumn: range.startColumn - 1,
endColumn: range.endColumn - 1,
}]
: [],
{
...range,
startColumn: range.startColumn,
endColumn: Number.MAX_SAFE_INTEGER,
},
],
};
}
case EffectRefRangId.RemoveRowCommandId: {
const params = command.params!;
const range: IRange = params.range;
const target = getSheetCommandTarget(univerInstanceService)!;
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [
range,
{
...range,
startRow: range.endRow + 1,
endRow: Number.MAX_SAFE_INTEGER,
},
],
};
}
case EffectRefRangId.RemoveColCommandId: {
const params = command.params!;
const range: IRange = params.range;
const target = getSheetCommandTarget(univerInstanceService)!;
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [
range,
{
...range,
startColumn: range.endColumn + 1,
endColumn: Number.MAX_SAFE_INTEGER,
},
],
};
}
case EffectRefRangId.DeleteRangeMoveUpCommandId:
case EffectRefRangId.InsertRangeMoveDownCommandId: {
const params = command;
const target = getSheetCommandTarget(univerInstanceService)!;
const range = params.params?.range || selectionManagerService.getCurrentSelections()?.map((s) => s.range)?.[0];
if (!range) {
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [],
};
}
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [
range,
{
...range,
startRow: range.endRow + 1,
endRow: Number.MAX_SAFE_INTEGER,
},
],
};
}
case EffectRefRangId.DeleteRangeMoveLeftCommandId:
case EffectRefRangId.InsertRangeMoveRightCommandId: {
const params = command;
const range = params.params?.range || selectionManagerService.getCurrentSelections()?.map((s) => s.range)?.[0];
const target = getSheetCommandTarget(univerInstanceService)!;
if (!range) {
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [],
};
}
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: [
range,
{
...range,
startColumn: range.endColumn + 1,
endColumn: Number.MAX_SAFE_INTEGER,
},
],
};
}
case EffectRefRangId.ReorderRangeCommandId: {
const params = command;
const { range, order } = params.params!;
const effectRanges = [];
for (let row = range.startRow; row <= range.endRow; row++) {
if (row in order) {
effectRanges.push({
startRow: row,
endRow: row,
startColumn: range.startColumn,
endColumn: range.endColumn,
});
}
}
const target = getSheetCommandTarget(univerInstanceService)!;
return {
unitId: target.unitId,
subUnitId: target.subUnitId,
ranges: effectRanges,
};
}
}
}