diff --git a/packages/core/src/shared/rectangle.ts b/packages/core/src/shared/rectangle.ts index f9894b807e..a9450852d4 100644 --- a/packages/core/src/shared/rectangle.ts +++ b/packages/core/src/shared/rectangle.ts @@ -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; diff --git a/packages/data-validation/src/commands/mutations/data-validation.mutation.ts b/packages/data-validation/src/commands/mutations/data-validation.mutation.ts index 8a1735b301..f88d943215 100644 --- a/packages/data-validation/src/commands/mutations/data-validation.mutation.ts +++ b/packages/data-validation/src/commands/mutations/data-validation.mutation.ts @@ -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 { diff --git a/packages/data-validation/src/models/data-validation-model.ts b/packages/data-validation/src/models/data-validation-model.ts index 95159c28cb..c63b289dbb 100644 --- a/packages/data-validation/src/models/data-validation-model.ts +++ b/packages/data-validation/src/models/data-validation-model.ts @@ -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; } diff --git a/packages/data-validation/src/types/enum/update-rule-type.ts b/packages/data-validation/src/types/enum/update-rule-type.ts index a92a537861..7a0e1289d6 100644 --- a/packages/data-validation/src/types/enum/update-rule-type.ts +++ b/packages/data-validation/src/types/enum/update-rule-type.ts @@ -18,4 +18,5 @@ export enum UpdateRuleType { SETTING, RANGE, OPTIONS, + ALL, } diff --git a/packages/data-validation/src/types/interfaces/i-update-rule-payload.ts b/packages/data-validation/src/types/interfaces/i-update-rule-payload.ts index 4a35826787..ea6b2d2c3c 100644 --- a/packages/data-validation/src/types/interfaces/i-update-rule-payload.ts +++ b/packages/data-validation/src/types/interfaces/i-update-rule-payload.ts @@ -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; } +export interface IUpdateRuleAllPayload { + type: UpdateRuleType.ALL; + payload: Omit; +} + export type IUpdateRulePayload = IUpdateRuleRangePayload | IUpdateRuleSettingPayload - | IUpdateRuleOptionsPayload; + | IUpdateRuleOptionsPayload + | IUpdateRuleAllPayload; diff --git a/packages/data-validation/src/validators/base-data-validator.ts b/packages/data-validation/src/validators/base-data-validator.ts index 61e5ad885b..216516dc67 100644 --- a/packages/data-validation/src/validators/base-data-validator.ts +++ b/packages/data-validation/src/validators/base-data-validator.ts @@ -126,7 +126,7 @@ export abstract class BaseDataValidator { return false; } - abstract parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise; + abstract parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise; abstract validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult; @@ -185,7 +185,7 @@ export abstract class BaseDataValidator { 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; diff --git a/packages/sheets-data-validation-ui/src/views/components/formula-input/list-formula-input.tsx b/packages/sheets-data-validation-ui/src/views/components/formula-input/list-formula-input.tsx index 5e0532106f..8e61cd7db1 100644 --- a/packages/sheets-data-validation-ui/src/views/components/formula-input/list-formula-input.tsx +++ b/packages/sheets-data-validation-ui/src/views/components/formula-input/list-formula-input.tsx @@ -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); }} - /> { 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={() => { diff --git a/packages/sheets-data-validation/src/commands/commands/data-validation.command.ts b/packages/sheets-data-validation/src/commands/commands/data-validation.command.ts index 2c7c365843..0c37a4e9c4 100644 --- a/packages/sheets-data-validation/src/commands/commands/data-validation.command.ts +++ b/packages/sheets-data-validation/src/commands/commands/data-validation.command.ts @@ -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 = 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(); + })); + } +} diff --git a/packages/sheets-data-validation/src/controllers/dv-ref-range.controller.ts b/packages/sheets-data-validation/src/controllers/dv-ref-range.controller.ts index c9b1c56d4b..28c5caa90b 100644 --- a/packages/sheets-data-validation/src/controllers/dv-ref-range.controller.ts +++ b/packages/sheets-data-validation/src/controllers/dv-ref-range.controller.ts @@ -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 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()); } } } diff --git a/packages/sheets-data-validation/src/models/sheet-data-validation-model.ts b/packages/sheets-data-validation/src/models/sheet-data-validation-model.ts index ac444726fd..82ecdb514b 100644 --- a/packages/sheets-data-validation/src/models/sheet-data-validation-model.ts +++ b/packages/sheets-data-validation/src/models/sheet-data-validation-model.ts @@ -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) { diff --git a/packages/sheets-data-validation/src/plugin.ts b/packages/sheets-data-validation/src/plugin.ts index c258736cf5..42045163db 100644 --- a/packages/sheets-data-validation/src/plugin.ts +++ b/packages/sheets-data-validation/src/plugin.ts @@ -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); } diff --git a/packages/sheets-data-validation/src/services/dv-custom-formula.service.ts b/packages/sheets-data-validation/src/services/dv-custom-formula.service.ts index 6a79223311..5ed5187ed2 100644 --- a/packages/sheets-data-validation/src/services/dv-custom-formula.service.ts +++ b/packages/sheets-data-validation/src/services/dv-custom-formula.service.ts @@ -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>> = new Map(); + private _ruleFormulaMap2: Map>> = 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); diff --git a/packages/sheets-data-validation/src/services/dv-formula.service.ts b/packages/sheets-data-validation/src/services/dv-formula.service.ts index f874cf0657..a90d7a370d 100644 --- a/packages/sheets-data-validation/src/services/dv-formula.service.ts +++ b/packages/sheets-data-validation/src/services/dv-formula.service.ts @@ -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]>> { const ruleFormulaMap = this._ensureRuleFormulaMap(unitId, subUnitId); diff --git a/packages/sheets-data-validation/src/utils/formula.ts b/packages/sheets-data-validation/src/utils/formula.ts index d9f9acaacc..72ae10b038 100644 --- a/packages/sheets-data-validation/src/utils/formula.ts +++ b/packages/sheets-data-validation/src/utils/formula.ts @@ -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[][]>) { @@ -28,3 +28,10 @@ export function getFormulaCellData(result: Nullable[][]>) { export function isLegalFormulaResult(res: string) { return !(ERROR_TYPE_SET as Set).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; +} diff --git a/packages/sheets-data-validation/src/validators/date-validator.ts b/packages/sheets-data-validation/src/validators/date-validator.ts index 33061afb10..1e9f64245c 100644 --- a/packages/sheets-data-validation/src/validators/date-validator.ts +++ b/packages/sheets-data-validation/src/validators/date-validator.ts @@ -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 { ]; 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> { + 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> { - 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 { const { interceptValue, value } = info; if (typeof value === 'number' && typeof interceptValue === 'string') { diff --git a/packages/sheets-data-validation/src/validators/decimal-validator.ts b/packages/sheets-data-validation/src/validators/decimal-validator.ts index a2a6e229d1..314c761774 100644 --- a/packages/sheets-data-validation/src/validators/decimal-validator.ts +++ b/packages/sheets-data-validation/src/validators/decimal-validator.ts @@ -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 { - 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 { return +formula; } - async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise { - const formulaInfo = await this._formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid); + async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise { + 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, }; diff --git a/packages/sheets-data-validation/src/validators/text-length-validator.ts b/packages/sheets-data-validation/src/validators/text-length-validator.ts index 44d0822487..fb669a9884 100644 --- a/packages/sheets-data-validation/src/validators/text-length-validator.ts +++ b/packages/sheets-data-validation/src/validators/text-length-validator.ts @@ -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 { 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 { return !Number.isNaN(formula); } - override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise> { - const formulaInfo = await this._formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid); + override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise> { + 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, }; } diff --git a/packages/sheets-data-validation/src/validators/whole-validator.ts b/packages/sheets-data-validation/src/validators/whole-validator.ts index 42c7311b07..5ec15841cd 100644 --- a/packages/sheets-data-validation/src/validators/whole-validator.ts +++ b/packages/sheets-data-validation/src/validators/whole-validator.ts @@ -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 { - 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 { return +formula; } - override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise { - const formulaInfo = await this._formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid); + override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise { + 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 = { diff --git a/packages/sheets-formula/src/services/__test__/formula-ref.spec.ts b/packages/sheets-formula/src/services/__test__/formula-ref.spec.ts new file mode 100644 index 0000000000..1ec9fbcd32 --- /dev/null +++ b/packages/sheets-formula/src/services/__test__/formula-ref.spec.ts @@ -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 }], + }, + ] + ); + }); +}); diff --git a/packages/sheets-formula/src/services/__test__/util.ts b/packages/sheets-formula/src/services/__test__/util.ts new file mode 100644 index 0000000000..ec7e45612b --- /dev/null +++ b/packages/sheets-formula/src/services/__test__/util.ts @@ -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(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, + }; +} diff --git a/packages/sheets-formula/src/services/formula-ref-range.service.ts b/packages/sheets-formula/src/services/formula-ref-range.service.ts index 8383638d35..f3ee678248 100644 --- a/packages/sheets-formula/src/services/formula-ref-range.service.ts +++ b/packages/sheets-formula/src/services/formula-ref-range.service.ts @@ -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>>; @@ -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(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(); + 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; + } } diff --git a/packages/sheets-hyper-link/src/controllers/rich-text-ref-range.controller.ts b/packages/sheets-hyper-link/src/controllers/rich-text-ref-range.controller.ts index 3ea2303032..4aaa475cc2 100644 --- a/packages/sheets-hyper-link/src/controllers/rich-text-ref-range.controller.ts +++ b/packages/sheets-hyper-link/src/controllers/rich-text-ref-range.controller.ts @@ -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); + } } } diff --git a/packages/sheets/src/commands/commands/move-rows-cols.command.ts b/packages/sheets/src/commands/commands/move-rows-cols.command.ts index 569bacd8ad..42a025dff1 100644 --- a/packages/sheets/src/commands/commands/move-rows-cols.command.ts +++ b/packages/sheets/src/commands/commands/move-rows-cols.command.ts @@ -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; diff --git a/packages/sheets/src/index.ts b/packages/sheets/src/index.ts index ce4f73bfd7..ff6018c8f7 100644 --- a/packages/sheets/src/index.ts +++ b/packages/sheets/src/index.ts @@ -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, diff --git a/packages/sheets/src/services/ref-range/util.ts b/packages/sheets/src/services/ref-range/util.ts index ff3035851a..034d5807c4 100644 --- a/packages/sheets/src/services/ref-range/util.ts +++ b/packages/sheets/src/services/ref-range/util.ts @@ -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, 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, 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 = { ...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 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, + }; + } + } +}