snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { Injector, Workbook } from '@univerjs/core';
import type { FUniver } from '@univerjs/core/facade';
import { DataValidationErrorStyle, DataValidationOperator, DataValidationType, ICommandService } from '@univerjs/core';
import {
AddDataValidationMutation,
RemoveDataValidationMutation,
UpdateDataValidationMutation,
UpdateRuleType,
} from '@univerjs/data-validation';
import { SetRangeValuesMutation } from '@univerjs/sheets';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createFacadeTestBed } from '../../../facade/__tests__/create-test-bed';
import { SheetDataValidationModel } from '../../../models/sheet-data-validation-model';
import {
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
getDataValidationDiffMutations,
RemoveSheetAllDataValidationCommand,
RemoveSheetDataValidationCommand,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
} from '../data-validation.command';
const unitId = 'test';
const subUnitId = 'sheet1';
describe('sheet data validation commands', () => {
let univerAPI: FUniver;
let workbook: Workbook;
let injector: Injector;
let commandService: ICommandService;
let model: SheetDataValidationModel;
let dispose: () => void;
beforeEach(() => {
const testBed = createFacadeTestBed();
univerAPI = testBed.univerAPI;
workbook = testBed.sheet as Workbook;
injector = testBed.injector;
commandService = testBed.get(ICommandService);
model = testBed.get(SheetDataValidationModel);
dispose = () => testBed.univer.dispose();
const commands = [
SetRangeValuesMutation,
AddSheetDataValidationCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
UpdateSheetDataValidationOptionsCommand,
ClearRangeDataValidationCommand,
RemoveSheetDataValidationCommand,
RemoveSheetAllDataValidationCommand,
];
for (const command of commands) {
commandService.registerCommand(command);
}
});
afterEach(() => {
dispose();
});
it('adds checkbox validation to the selected range', async () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const rule = univerAPI.newDataValidation()
.requireCheckbox('Yes', 'No')
.build();
activeSheet.getRange(0, 0, 2, 1).setDataValidation(rule);
const rules = model.getRules(unitId, subUnitId);
expect(rules).toHaveLength(1);
expect(rules[0].type).toBe(DataValidationType.CHECKBOX);
expect(rules[0].formula1).toBe('Yes');
expect(rules[0].formula2).toBe('No');
expect(rules[0].ranges).toEqual([{ unitId, sheetId: subUnitId, startRow: 0, endRow: 1, startColumn: 0, endColumn: 0 }]);
});
it('moves a validation rule to a new range and applies checkbox defaults there', async () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const rule = univerAPI.newDataValidation()
.requireCheckbox('TRUE', 'FALSE')
.build();
activeSheet.getRange(0, 0, 1, 1).setDataValidation(rule);
const ruleId = model.getRules(unitId, subUnitId)[0].uid;
const result = await commandService.executeCommand(UpdateSheetDataValidationRangeCommand.id, {
unitId,
subUnitId,
ruleId,
ranges: [{ startRow: 2, endRow: 3, startColumn: 1, endColumn: 1 }],
});
const updatedRule = model.getRuleById(unitId, subUnitId, ruleId);
const worksheet = workbook.getSheetBySheetId(subUnitId);
expect(result).toBe(true);
expect(updatedRule?.ranges).toEqual([{ startRow: 2, endRow: 3, startColumn: 1, endColumn: 1 }]);
expect(worksheet?.getCellRaw(2, 1)?.v).toBe(0);
expect(worksheet?.getCellRaw(3, 1)?.v).toBe(0);
});
it('updates validation settings and options on an existing rule', async () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const rule = univerAPI.newDataValidation()
.requireNumberBetween(1, 10)
.build();
activeSheet.getRange(0, 1, 2, 1).setDataValidation(rule);
const ruleId = model.getRules(unitId, subUnitId)[0].uid;
const settingResult = await commandService.executeCommand(UpdateSheetDataValidationSettingCommand.id, {
unitId,
subUnitId,
ruleId,
setting: {
type: DataValidationType.DECIMAL,
operator: univerAPI.Enum.DataValidationOperator.GREATER_THAN,
formula1: '5',
},
});
const optionsResult = await commandService.executeCommand(UpdateSheetDataValidationOptionsCommand.id, {
unitId,
subUnitId,
ruleId,
options: {
errorStyle: DataValidationErrorStyle.STOP,
showErrorMessage: true,
error: 'Value must be greater than five',
},
});
const updatedRule = model.getRuleById(unitId, subUnitId, ruleId);
expect(settingResult).toBe(true);
expect(optionsResult).toBe(true);
expect(updatedRule?.operator).toBe(univerAPI.Enum.DataValidationOperator.GREATER_THAN);
expect(updatedRule?.formula1).toBe('5');
expect(updatedRule?.errorStyle).toBe(DataValidationErrorStyle.STOP);
expect(updatedRule?.error).toBe('Value must be greater than five');
});
it('updates checkbox settings and rewrites existing checkbox values', async () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const rule = univerAPI.newDataValidation()
.requireCheckbox('Y', 'N')
.build();
const worksheet = workbook.getSheetBySheetId(subUnitId);
activeSheet.getRange(0, 2, 2, 1).setDataValidation(rule);
worksheet?.getCellMatrix().setValue(0, 2, { v: 'N' });
worksheet?.getCellMatrix().setValue(1, 2, { v: 'Y' });
const ruleId = model.getRules(unitId, subUnitId)[0].uid;
const result = await commandService.executeCommand(UpdateSheetDataValidationSettingCommand.id, {
unitId,
subUnitId,
ruleId,
setting: {
type: DataValidationType.CHECKBOX,
formula1: 'Done',
formula2: 'Todo',
},
});
const updatedRule = model.getRuleById(unitId, subUnitId, ruleId);
expect(result).toBe(true);
expect(updatedRule?.formula1).toBe('Done');
expect(updatedRule?.formula2).toBe('Todo');
expect(worksheet?.getCellRaw(0, 2)?.v).toBe('Todo');
expect(worksheet?.getCellRaw(1, 2)?.v).toBe('Done');
});
it('clears validation from a range without removing unrelated rules', async () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const firstRule = univerAPI.newDataValidation().requireCheckbox().build();
const secondRule = univerAPI.newDataValidation().requireNumberEqualTo(1).build();
activeSheet.getRange(0, 0, 1, 1).setDataValidation(firstRule);
activeSheet.getRange(4, 4, 1, 1).setDataValidation(secondRule);
const result = await commandService.executeCommand(ClearRangeDataValidationCommand.id, {
unitId,
subUnitId,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
});
const rules = model.getRules(unitId, subUnitId);
expect(result).toBe(true);
expect(rules).toHaveLength(1);
expect(rules[0].type).toBe(DataValidationType.DECIMAL);
expect(rules[0].ranges).toEqual([{ unitId, sheetId: subUnitId, startRow: 4, endRow: 4, startColumn: 4, endColumn: 4 }]);
});
it('clears only requested validation types from a range', async () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const checkbox = univerAPI.newDataValidation().requireCheckbox().build();
const dropdown = univerAPI.newDataValidation().requireValueInList(['A', 'B']).build();
activeSheet.getRange(0, 0, 1, 1).setDataValidation(checkbox);
activeSheet.getRange(0, 1, 1, 1).setDataValidation(dropdown);
const result = await commandService.executeCommand(ClearRangeDataValidationCommand.id, {
unitId,
subUnitId,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 1 }],
types: [DataValidationType.CHECKBOX],
});
const rules = model.getRules(unitId, subUnitId);
expect(result).toBe(true);
expect(rules).toHaveLength(1);
expect(rules[0].type).toBe(DataValidationType.LIST);
expect(rules[0].ranges).toEqual([
{ unitId, sheetId: subUnitId, startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 },
]);
});
it('removes one rule or all rules from the sheet', async () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const firstRule = univerAPI.newDataValidation().requireCheckbox().build();
const secondRule = univerAPI.newDataValidation().requireNumberEqualTo(1).build();
activeSheet.getRange(0, 0, 1, 1).setDataValidation(firstRule);
activeSheet.getRange(4, 4, 1, 1).setDataValidation(secondRule);
const ruleId = model.getRules(unitId, subUnitId)[0].uid;
const removeOneResult = await commandService.executeCommand(RemoveSheetDataValidationCommand.id, {
unitId,
subUnitId,
ruleId,
});
const removeAllResult = await commandService.executeCommand(RemoveSheetAllDataValidationCommand.id, {
unitId,
subUnitId,
});
expect(removeOneResult).toBe(true);
expect(removeAllResult).toBe(true);
expect(model.getRules(unitId, subUnitId)).toEqual([]);
});
it('builds diff mutations for validation rule add and formula-offset range changes', () => {
const checkboxRule = {
uid: 'checkbox-diff',
type: DataValidationType.CHECKBOX,
formula1: '1',
formula2: '0',
ranges: [{ startRow: 6, endRow: 6, startColumn: 0, endColumn: 0 }],
};
const customFormulaRule = {
uid: 'formula-diff',
type: DataValidationType.CUSTOM,
operator: DataValidationOperator.BETWEEN,
formula1: '=A1>0',
formula2: '=B1<10',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
};
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, [
{
type: 'add',
rule: checkboxRule,
},
{
type: 'update',
ruleId: customFormulaRule.uid,
rule: customFormulaRule,
oldRanges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
newRanges: [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }],
},
] as never, injector);
let hasAddRule = false;
let hasFormulaUpdate = false;
let hasDefaultCellValue = false;
let hasUndoRemove = false;
for (const mutation of redoMutations) {
if (mutation.id === AddDataValidationMutation.id) {
hasAddRule = true;
}
if (mutation.id === UpdateDataValidationMutation.id && (mutation.params as never as { payload: { type: UpdateRuleType } }).payload.type === UpdateRuleType.ALL) {
hasFormulaUpdate = true;
}
if (mutation.id === SetRangeValuesMutation.id && (mutation.params as never as { cellValue: Record<number, Record<number, { v: unknown }>> }).cellValue[6]?.[0]?.v === '0') {
hasDefaultCellValue = true;
}
}
for (const mutation of undoMutations) {
if (mutation.id === RemoveDataValidationMutation.id) {
hasUndoRemove = true;
}
}
expect(hasAddRule).toBe(true);
expect(hasFormulaUpdate).toBe(true);
expect(hasDefaultCellValue).toBe(true);
expect(hasUndoRemove).toBe(true);
});
it('returns no diff mutations when the target sheet is missing', () => {
const result = getDataValidationDiffMutations(unitId, 'missing-sheet', [
{
type: 'add',
rule: {
uid: 'missing-target-rule',
type: DataValidationType.CHECKBOX,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
},
},
] as never, injector);
expect(result.redoMutations).toEqual([]);
expect(result.undoMutations).toEqual([]);
});
});

View File

@@ -0,0 +1,769 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 {
CellValue,
IAccessor,
ICellData,
ICommand,
IDataValidationRuleBase,
IDataValidationRuleOptions,
IMutationInfo,
Injector,
IRange,
ISheetDataValidationRule,
Nullable,
} from '@univerjs/core';
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 type { CheckboxValidator } from '../../validators';
import {
CommandType,
DataValidationType,
ICommandService,
isFormulaString,
isRangesEqual,
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 { shouldOffsetFormulaByRange } from '../../utils/formula';
import { getStringCellValue } from '../../utils/get-cell-data-origin';
import { CHECKBOX_FORMULA_1, CHECKBOX_FORMULA_2 } from '../../validators';
export interface IUpdateSheetDataValidationRangeCommandParams {
unitId: string;
subUnitId: string;
ruleId: string;
ranges: IRange[];
}
function isBlankCell(cellData: Nullable<ICellData>) {
if (!cellData) {
return true;
}
if (!cellData.p) {
return Tools.isBlank(cellData.v);
}
const dataStream = (cellData.p.body?.dataStream ?? '').slice(0, -2).trim();
return !dataStream;
}
// eslint-disable-next-line max-lines-per-function
export function getDataValidationDiffMutations(
unitId: string,
subUnitId: string,
diffs: RangeMutation[],
accessor: IAccessor,
source: DataValidationChangeSource = 'command',
fillDefaultValue = true
) {
const lexerTreeBuilder = accessor.get(LexerTreeBuilder);
const validatorRegistryService = accessor.get(DataValidatorRegistryService);
const redoMutations: IMutationInfo[] = [];
const undoMutations: IMutationInfo[] = [];
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId });
if (!target) {
return {
redoMutations,
undoMutations,
};
}
const { worksheet } = target;
const redoMatrix = new ObjectMatrix<ICellData>();
let setRangeValue = false;
function setRangesDefaultValue(ranges: IRange[], defaultValue: CellValue) {
if (!fillDefaultValue) {
return;
}
ranges.forEach((range) => {
Range.foreach(range, (row, column) => {
const cellData = worksheet.getCellRaw(row, column);
const value = getStringCellValue(cellData);
if ((isBlankCell(cellData) || value === defaultValue) && !cellData?.p) {
setRangeValue = true;
redoMatrix.setValue(row, column, {
v: defaultValue,
p: null,
});
}
});
});
}
// eslint-disable-next-line max-lines-per-function
diffs.forEach((diff) => {
switch (diff.type) {
case 'delete':
redoMutations.push({
id: RemoveDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: diff.rule.uid,
source,
},
});
undoMutations.unshift({
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId,
rule: diff.rule,
index: diff.index,
source,
},
});
break;
case 'update': {
if (shouldOffsetFormulaByRange(diff.rule.type, validatorRegistryService)) {
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 = 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 || newFormula2 !== diff.rule.formula2 || !isRangesEqual(diff.newRanges, diff.oldRanges)) {
redoMutations.push({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: diff.ruleId,
payload: {
type: UpdateRuleType.ALL,
payload: {
formula1: newFormula,
formula2: newFormula2,
ranges: diff.newRanges,
},
},
} as IUpdateDataValidationMutationParams,
});
undoMutations.unshift({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: diff.ruleId,
payload: {
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,
});
}
} 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);
if (rule && rule.type === DataValidationType.CHECKBOX) {
const validator = sheetDataValidationModel.getValidator(DataValidationType.CHECKBOX) as CheckboxValidator;
const formula = validator.parseFormulaSync(rule, unitId, subUnitId);
setRangesDefaultValue(diff.newRanges, formula.formula2!);
}
break;
}
case 'add': {
redoMutations.push({
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId,
rule: diff.rule,
source,
} as IAddDataValidationMutationParams,
});
undoMutations.unshift({
id: RemoveDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: diff.rule.uid,
source,
},
});
if (diff.rule.type === DataValidationType.CHECKBOX) {
const validator = sheetDataValidationModel.getValidator(DataValidationType.CHECKBOX) as CheckboxValidator;
const formula = validator.parseFormulaSync(diff.rule, unitId, subUnitId);
setRangesDefaultValue(diff.rule.ranges, formula.originFormula2!);
}
break;
}
default:
break;
}
});
if (setRangeValue) {
const redoSetRangeValues = {
id: SetRangeValuesMutation.id,
params: {
unitId,
subUnitId,
cellValue: redoMatrix.clone(),
} as ISetRangeValuesMutationParams,
};
const undoSetRangeValues = {
id: SetRangeValuesMutation.id,
params: SetRangeValuesUndoMutationFactory(accessor, redoSetRangeValues.params),
};
redoMutations.push(redoSetRangeValues);
undoMutations.push(undoSetRangeValues);
}
return {
redoMutations,
undoMutations,
};
}
export const UpdateSheetDataValidationRangeCommand: ICommand<IUpdateSheetDataValidationRangeCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.updateDataValidationRuleRange',
handler(accessor, params) {
if (!params) {
return false;
}
const { unitId, subUnitId, ranges, ruleId } = params;
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const currentRule = sheetDataValidationModel.getRuleById(unitId, subUnitId, ruleId);
if (!currentRule) {
return false;
}
const matrix = sheetDataValidationModel.getRuleObjectMatrix(unitId, subUnitId).clone();
matrix.updateRange(ruleId, ranges);
const diffs = matrix.diff(sheetDataValidationModel.getRules(unitId, subUnitId));
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, diffs, accessor);
undoRedoService.pushUndoRedo({
undoMutations,
redoMutations,
unitID: unitId,
});
sequenceExecute(redoMutations, commandService);
return true;
},
};
export interface IAddSheetDataValidationCommandParams {
unitId: string;
subUnitId: string;
rule: ISheetDataValidationRule;
}
export const AddSheetDataValidationCommand: ICommand<IAddSheetDataValidationCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.addDataValidation',
handler(accessor, params) {
if (!params) {
return false;
}
const { unitId, subUnitId, rule } = params;
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
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,
...validator?.normalizeFormula(rule, unitId, subUnitId),
},
};
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, diffs, accessor);
redoMutations.push({
id: AddDataValidationMutation.id,
params: mutationParams,
});
undoMutations.unshift({
id: RemoveDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: rule.uid,
},
});
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations,
undoMutations,
});
sequenceExecute(redoMutations, commandService);
return true;
},
};
export interface IUpdateSheetDataValidationSettingCommandParams extends ISheetCommandSharedParams {
ruleId: string;
setting: IDataValidationRuleBase;
}
export const UpdateSheetDataValidationSettingCommand: ICommand<IUpdateSheetDataValidationSettingCommandParams> = {
type: CommandType.COMMAND,
id: 'sheets.command.update-data-validation-setting',
// eslint-disable-next-line max-lines-per-function
handler(accessor, params) {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const redoUndoService = accessor.get(IUndoRedoService);
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
const dataValidatorRegistryService = accessor.get(DataValidatorRegistryService);
const { unitId, subUnitId, ruleId, setting } = params;
const validator = dataValidatorRegistryService.getValidatorItem(setting.type);
if (!validator) {
return false;
}
const rule = sheetDataValidationModel.getRuleById(unitId, subUnitId, ruleId);
if (!rule) {
return false;
}
const newRule = { ...rule, ...setting };
if (!validator.validatorFormula(newRule, unitId, subUnitId).success) {
return false;
}
const mutationParams: IUpdateDataValidationMutationParams = {
unitId,
subUnitId,
ruleId,
payload: {
type: UpdateRuleType.SETTING,
payload: {
...setting,
...validator.normalizeFormula(newRule, unitId, subUnitId),
},
},
};
const redoMutations: IMutationInfo[] = [{
id: UpdateDataValidationMutation.id,
params: mutationParams,
}];
const undoMutationParams: IUpdateDataValidationMutationParams = {
unitId,
subUnitId,
ruleId,
payload: {
type: UpdateRuleType.SETTING,
payload: getRuleSetting(rule),
},
};
const undoMutations: IMutationInfo[] = [{
id: UpdateDataValidationMutation.id,
params: undoMutationParams,
}];
if (setting.type === DataValidationType.CHECKBOX) {
const ranges = rule.ranges as IRange[];
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId });
if (target) {
const redoMatrix = new ObjectMatrix<ICellData>();
const { worksheet } = target;
const { formula2: oldFormula2 = CHECKBOX_FORMULA_2, formula1: oldFormula1 = CHECKBOX_FORMULA_1 } = rule;
const { formula2 = CHECKBOX_FORMULA_2, formula1 = CHECKBOX_FORMULA_1 } = setting;
let setted = false;
ranges.forEach((range) => {
Range.foreach(range, (row, column) => {
const cellData = worksheet.getCellRaw(row, column);
const value = getStringCellValue(cellData);
if ((isBlankCell(cellData) || value === String(oldFormula2)) && !cellData?.p) {
redoMatrix.setValue(row, column, {
v: formula2,
p: null,
});
setted = true;
} else if (value === String(oldFormula1) && !cellData?.p) {
redoMatrix.setValue(row, column, {
v: formula1,
p: null,
});
setted = true;
}
});
});
if (setted) {
const redoSetRangeValues = {
id: SetRangeValuesMutation.id,
params: {
unitId,
subUnitId,
cellValue: redoMatrix.clone(),
} as ISetRangeValuesMutationParams,
};
const undoSetRangeValues = {
id: SetRangeValuesMutation.id,
params: SetRangeValuesUndoMutationFactory(accessor, redoSetRangeValues.params),
};
redoMutations.push(redoSetRangeValues);
undoMutations.push(undoSetRangeValues);
}
}
}
const res = sequenceExecute(redoMutations, commandService);
if (res.result) {
redoUndoService.pushUndoRedo({
unitID: unitId,
redoMutations,
undoMutations,
});
return true;
}
return false;
},
};
export interface IUpdateSheetDataValidationOptionsCommandParams extends ISheetCommandSharedParams {
ruleId: string;
options: IDataValidationRuleOptions;
}
export const UpdateSheetDataValidationOptionsCommand: ICommand<IUpdateSheetDataValidationOptionsCommandParams> = {
type: CommandType.COMMAND,
id: 'sheets.command.update-data-validation-options',
handler(accessor, params) {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const redoUndoService = accessor.get(IUndoRedoService);
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
const { unitId, subUnitId, ruleId, options } = params;
const rule = sheetDataValidationModel.getRuleById(unitId, subUnitId, ruleId);
if (!rule) {
return false;
}
const mutationParams: IUpdateDataValidationMutationParams = {
unitId,
subUnitId,
ruleId,
payload: {
type: UpdateRuleType.OPTIONS,
payload: options,
},
};
const redoMutations: IMutationInfo[] = [{
id: UpdateDataValidationMutation.id,
params: mutationParams,
}];
const undoMutationParams: IUpdateDataValidationMutationParams = {
unitId,
subUnitId,
ruleId,
payload: {
type: UpdateRuleType.OPTIONS,
payload: getRuleOptions(rule),
},
};
const undoMutations: IMutationInfo[] = [{
id: UpdateDataValidationMutation.id,
params: undoMutationParams,
}];
redoUndoService.pushUndoRedo({
unitID: unitId,
redoMutations,
undoMutations,
});
commandService.executeCommand(UpdateDataValidationMutation.id, mutationParams);
return true;
},
};
export interface IClearRangeDataValidationCommandParams {
unitId: string;
subUnitId: string;
ranges: IRange[];
types?: DataValidationType[];
}
export const ClearRangeDataValidationCommand: ICommand<IClearRangeDataValidationCommandParams> = {
type: CommandType.COMMAND,
id: 'sheets.command.clear-range-data-validation',
handler(accessor, params) {
if (!params) {
return false;
}
const { unitId, subUnitId, ranges, types } = params;
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId });
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
if (!target) return false;
const undoRedoService = accessor.get(IUndoRedoService);
const matrix = sheetDataValidationModel.getRuleObjectMatrix(unitId, subUnitId).clone();
const ruleIds = types
? new Set(
sheetDataValidationModel
.getRules(unitId, subUnitId)
.filter((rule) => types.includes(rule.type as DataValidationType))
.map((rule) => rule.uid)
)
: undefined;
matrix.removeRange(ranges, ruleIds);
const diffs = matrix.diff(sheetDataValidationModel.getRules(unitId, subUnitId));
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, diffs, accessor);
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations,
undoMutations,
});
return sequenceExecute(redoMutations, commandService).result;
},
};
export interface IRemoveSheetAllDataValidationCommandParams extends ISheetCommandSharedParams {
}
export const RemoveSheetAllDataValidationCommand: ICommand<IRemoveSheetAllDataValidationCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.remove-all-data-validation',
handler(accessor, params) {
if (!params) {
return false;
}
const { unitId, subUnitId } = params;
const commandService = accessor.get(ICommandService);
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
const undoRedoService = accessor.get(IUndoRedoService);
const currentRules = [...sheetDataValidationModel.getRules(unitId, subUnitId)];
const redoParams: IRemoveDataValidationMutationParams = {
unitId,
subUnitId,
ruleId: currentRules.map((rule) => rule.uid),
};
const redoMutations: IMutationInfo[] = [{
id: RemoveDataValidationMutation.id,
params: redoParams,
}];
const undoMutations: IMutationInfo[] = [{
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId,
rule: currentRules,
},
}];
undoRedoService.pushUndoRedo({
redoMutations,
undoMutations,
unitID: unitId,
});
commandService.executeCommand(RemoveDataValidationMutation.id, redoParams);
return true;
},
};
export interface IRemoveSheetDataValidationCommandParams extends ISheetCommandSharedParams {
ruleId: string;
}
export const removeDataValidationUndoFactory = (accessor: Injector, redoParams: IRemoveDataValidationMutationParams) => {
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
const { unitId, subUnitId, ruleId, source } = redoParams;
if (Array.isArray(ruleId)) {
const rules = ruleId.map((id) => sheetDataValidationModel.getRuleById(unitId, subUnitId, id)).filter(Boolean) as ISheetDataValidationRule[];
return [{
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId,
rule: rules,
source,
} as IAddDataValidationMutationParams,
}];
}
const undoMutations: IMutationInfo[] = [{
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId,
rule: {
...sheetDataValidationModel.getRuleById(unitId, subUnitId, ruleId),
},
index: sheetDataValidationModel.getRuleIndex(unitId, subUnitId, ruleId),
} as IAddDataValidationMutationParams,
}];
return undoMutations;
};
export const RemoveSheetDataValidationCommand: ICommand<IRemoveSheetDataValidationCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.remove-data-validation-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { unitId, subUnitId, ruleId } = params;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetDataValidationModel = accessor.get(SheetDataValidationModel);
const redoMutations: IMutationInfo[] = [{
id: RemoveDataValidationMutation.id,
params,
}];
const undoMutations: IMutationInfo[] = [{
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId,
rule: {
...sheetDataValidationModel.getRuleById(unitId, subUnitId, ruleId),
},
index: sheetDataValidationModel.getRuleIndex(unitId, subUnitId, ruleId),
} as IAddDataValidationMutationParams,
}];
undoRedoService.pushUndoRedo({
undoMutations,
redoMutations,
unitID: params.unitId,
});
commandService.executeCommand(RemoveDataValidationMutation.id, params);
return true;
},
};

View File

@@ -0,0 +1,19 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export const DATA_VALIDATION_PLUGIN_NAME = 'SHEET_DATA_VALIDATION_PLUGIN';
export const DROP_DOWN_COLORS = [];

View File

@@ -0,0 +1,63 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationOperator } from '@univerjs/core';
export const DateOperatorNameMap: Record<DataValidationOperator, string> = {
[DataValidationOperator.BETWEEN]: 'sheets-data-validation.date.operators.between',
[DataValidationOperator.EQUAL]: 'sheets-data-validation.date.operators.equal',
[DataValidationOperator.GREATER_THAN]: 'sheets-data-validation.date.operators.greaterThan',
[DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'sheets-data-validation.date.operators.greaterThanOrEqual',
[DataValidationOperator.LESS_THAN]: 'sheets-data-validation.date.operators.lessThan',
[DataValidationOperator.LESS_THAN_OR_EQUAL]: 'sheets-data-validation.date.operators.lessThanOrEqual',
[DataValidationOperator.NOT_BETWEEN]: 'sheets-data-validation.date.operators.notBetween',
[DataValidationOperator.NOT_EQUAL]: 'sheets-data-validation.date.operators.notEqual',
};
export const DateOperatorTextMap: Record<DataValidationOperator, string> = {
[DataValidationOperator.BETWEEN]: 'sheets-data-validation.date.operators.between',
[DataValidationOperator.EQUAL]: 'sheets-data-validation.date.operators.equal',
[DataValidationOperator.GREATER_THAN]: 'sheets-data-validation.date.operators.greaterThan',
[DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'sheets-data-validation.date.operators.greaterThanOrEqual',
[DataValidationOperator.LESS_THAN]: 'sheets-data-validation.date.operators.lessThan',
[DataValidationOperator.LESS_THAN_OR_EQUAL]: 'sheets-data-validation.date.operators.lessThanOrEqual',
[DataValidationOperator.NOT_BETWEEN]: 'sheets-data-validation.date.operators.notBetween',
[DataValidationOperator.NOT_EQUAL]: 'sheets-data-validation.date.operators.notEqual',
};
export const DateOperatorTitleMap: Record<DataValidationOperator | 'NONE', string> = {
[DataValidationOperator.BETWEEN]: 'sheets-data-validation.date.ruleName.between',
[DataValidationOperator.EQUAL]: 'sheets-data-validation.date.ruleName.equal',
[DataValidationOperator.GREATER_THAN]: 'sheets-data-validation.date.ruleName.greaterThan',
[DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'sheets-data-validation.date.ruleName.greaterThanOrEqual',
[DataValidationOperator.LESS_THAN]: 'sheets-data-validation.date.ruleName.lessThan',
[DataValidationOperator.LESS_THAN_OR_EQUAL]: 'sheets-data-validation.date.ruleName.lessThanOrEqual',
[DataValidationOperator.NOT_BETWEEN]: 'sheets-data-validation.date.ruleName.notBetween',
[DataValidationOperator.NOT_EQUAL]: 'sheets-data-validation.date.ruleName.notEqual',
NONE: 'sheets-data-validation.date.ruleName.legal',
};
export const DateOperatorErrorTitleMap: Record<DataValidationOperator | 'NONE', string> = {
[DataValidationOperator.BETWEEN]: 'sheets-data-validation.date.errorMsg.between',
[DataValidationOperator.EQUAL]: 'sheets-data-validation.date.errorMsg.equal',
[DataValidationOperator.GREATER_THAN]: 'sheets-data-validation.date.errorMsg.greaterThan',
[DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'sheets-data-validation.date.errorMsg.greaterThanOrEqual',
[DataValidationOperator.LESS_THAN]: 'sheets-data-validation.date.errorMsg.lessThan',
[DataValidationOperator.LESS_THAN_OR_EQUAL]: 'sheets-data-validation.date.errorMsg.lessThanOrEqual',
[DataValidationOperator.NOT_BETWEEN]: 'sheets-data-validation.date.errorMsg.notBetween',
[DataValidationOperator.NOT_EQUAL]: 'sheets-data-validation.date.errorMsg.notEqual',
NONE: 'sheets-data-validation.date.errorMsg.legal',
};

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export const SHEETS_DATA_VALIDATION_PLUGIN_CONFIG_KEY = 'sheets-data-validation.config';
export const configSymbol = Symbol(SHEETS_DATA_VALIDATION_PLUGIN_CONFIG_KEY);
export interface IUniverSheetsDataValidationConfig {
}
export const defaultPluginConfig: IUniverSheetsDataValidationConfig = {};

View File

@@ -0,0 +1,199 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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, DataValidatorRegistryService, RemoveDataValidationMutation, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
import { FormulaRefRangeService } from '@univerjs/sheets-formula';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
import { shouldOffsetFormulaByRange } from '../utils/formula';
export class DataValidationFormulaRefRangeController extends Disposable {
private _disposableMap: Map<string, IDisposable> = new Map();
constructor(
@Inject(SheetDataValidationModel) private _dataValidationModel: SheetDataValidationModel,
@Inject(FormulaRefRangeService) private _formulaRefRangeService: FormulaRefRangeService,
@Inject(DataValidatorRegistryService) private _validatorRegistryService: DataValidatorRegistryService
) {
super();
this._initRefRange();
}
private _getIdWithUnitId(unitID: string, subUnitId: string, ruleId: string) {
return `${unitID}_${subUnitId}_${ruleId}`;
}
registerRule = (unitId: string, subUnitId: string, rule: ISheetDataValidationRule) => {
if (!shouldOffsetFormulaByRange(rule.type, this._validatorRegistryService)) {
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,
source: 'patched',
},
}],
redos: [{
id: RemoveDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: rule.uid,
source: 'patched',
},
}],
};
}
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],
},
},
source: 'patched',
} as IUpdateDataValidationMutationParams,
});
undos.push({
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: rule.uid,
payload: {
type: UpdateRuleType.ALL,
payload: {
ranges: oldRanges,
formula1: oldFormula1,
formula2: oldFormula2,
},
},
source: 'patched',
},
});
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,
},
source: 'patched',
},
});
undos.push({
id: RemoveDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: id,
source: 'patched',
},
});
}
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();
}));
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ICellDataForSheetInterceptor, Nullable, Workbook, Worksheet } from '@univerjs/core';
import type { ICellPermission } from '@univerjs/sheets';
import { Disposable, Inject, IPermissionService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { deserializeRangeWithSheetWithCache, LexerTreeBuilder } from '@univerjs/engine-formula';
import { UnitAction } from '@univerjs/protocol';
import { WorksheetViewPermission } from '@univerjs/sheets';
export class DataValidationFormulaController extends Disposable {
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@IPermissionService private readonly _permissionService: IPermissionService,
@Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder
) {
super();
}
getFormulaRefCheck(formulaString: string) {
const sequenceNodes = this._lexerTreeBuilder.sequenceNodesBuilder(formulaString);
if (!sequenceNodes) {
return true;
}
for (let i = 0; i < sequenceNodes.length; i++) {
const node = sequenceNodes[i];
if (typeof node === 'string') {
continue;
}
const { token } = node;
const sequenceGrid = deserializeRangeWithSheetWithCache(token);
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
let targetSheet: Nullable<Worksheet> = workbook.getActiveSheet();
const unitId = workbook.getUnitId();
if (sequenceGrid.sheetName) {
targetSheet = workbook.getSheetBySheetName(sequenceGrid.sheetName);
if (!targetSheet) {
return false;
}
const subUnitId = targetSheet?.getSheetId();
const viewPermission = this._permissionService.getPermissionPoint(new WorksheetViewPermission(unitId, subUnitId).id);
if (!viewPermission) return false;
}
if (!targetSheet) {
return false;
}
const { startRow, endRow, startColumn, endColumn } = sequenceGrid.range;
for (let i = startRow; i <= endRow; i++) {
for (let j = startColumn; j <= endColumn; j++) {
const permission = (targetSheet.getCell(i, j) as (ICellDataForSheetInterceptor & { selectionProtection: ICellPermission[] }))?.selectionProtection?.[0];
if (permission?.[UnitAction.View] === false) {
return false;
}
}
}
}
return true;
}
}

View File

@@ -0,0 +1,244 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ISheetDataValidationRule } from '@univerjs/core';
import type { IRemoveDataValidationMutationParams, IUpdateDataValidationMutationParams } from '@univerjs/data-validation';
import type { EffectRefRangeParams } from '@univerjs/sheets';
import { Disposable, Inject, Injector, isRangesEqual, toDisposable } from '@univerjs/core';
import { DataValidatorRegistryService, RemoveDataValidationMutation, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
import { handleCommonDefaultRangeChangeWithEffectRefCommands, RefRangeService } from '@univerjs/sheets';
import { FormulaRefRangeService } from '@univerjs/sheets-formula';
import { removeDataValidationUndoFactory } from '../commands/commands/data-validation.command';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { shouldOffsetFormulaByRange } from '../utils/formula';
export class DataValidationRefRangeController extends Disposable {
private _disposableMap: Map<string, Set<() => void>> = new Map();
constructor(
@Inject(SheetDataValidationModel) private _dataValidationModel: SheetDataValidationModel,
@Inject(Injector) private _injector: Injector,
@Inject(RefRangeService) private _refRangeService: RefRangeService,
@Inject(DataValidationFormulaService) private _dataValidationFormulaService: DataValidationFormulaService,
@Inject(FormulaRefRangeService) private _formulaRefRangeService: FormulaRefRangeService,
@Inject(DataValidatorRegistryService) private _validatorRegistryService: DataValidatorRegistryService
) {
super();
this._initRefRange();
}
private _getIdWithUnitId(unitID: string, subUnitId: string, ruleId: string) {
return `${unitID}_${subUnitId}_${ruleId}`;
}
registerRule = (unitId: string, subUnitId: string, rule: ISheetDataValidationRule) => {
if (shouldOffsetFormulaByRange(rule.type, this._validatorRegistryService)) {
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);
const disposeSet = this._disposableMap.get(id) ?? new Set();
const handleFormulaChange = (type: 'formula1' | 'formula2', formulaString: string) => {
const oldRule = this._dataValidationModel.getRuleById(unitId, subUnitId, ruleId);
if (!oldRule) {
return { redos: [], undos: [] };
}
const oldFormula = oldRule[type];
if (!oldFormula || oldFormula === formulaString) {
return { redos: [], undos: [] };
}
const redoParams: IUpdateDataValidationMutationParams = {
unitId,
subUnitId,
ruleId: rule.uid,
payload: {
type: UpdateRuleType.SETTING,
payload: {
type: oldRule.type,
formula1: oldRule.formula1,
formula2: oldRule.formula2,
[type]: formulaString,
},
},
source: 'patched',
};
const undoParams: IUpdateDataValidationMutationParams = {
unitId,
subUnitId,
ruleId: rule.uid,
payload: {
type: UpdateRuleType.SETTING,
payload: {
type: oldRule.type,
formula1: oldRule.formula1,
formula2: oldRule.formula2,
},
},
source: 'patched',
};
const redos = [
{
id: UpdateDataValidationMutation.id,
params: redoParams,
},
];
const undos = [
{
id: UpdateDataValidationMutation.id,
params: undoParams,
},
];
return { redos, undos };
};
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());
}
}
}
register(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
const handleRangeChange = (commandInfo: EffectRefRangeParams) => {
const oldRanges = [...rule.ranges];
const resultRangesOrigin = oldRanges.map((range) => {
return handleCommonDefaultRangeChangeWithEffectRefCommands(range, commandInfo);
}).filter((range) => !!range);
const resultRanges = resultRangesOrigin.flat();
const isEqual = isRangesEqual(resultRanges, oldRanges);
if (isEqual) {
return { redos: [], undos: [] };
}
if (resultRanges.length) {
const redoParams: IUpdateDataValidationMutationParams = {
unitId,
subUnitId,
ruleId: rule.uid,
payload: {
type: UpdateRuleType.RANGE,
payload: resultRanges,
},
source: 'patched',
};
// in ref-range case, there won't be any overlap about rule ranges
const redos = [{ id: UpdateDataValidationMutation.id, params: redoParams }];
const undos = [{
id: UpdateDataValidationMutation.id,
params: {
unitId,
subUnitId,
ruleId: rule.uid,
payload: {
type: UpdateRuleType.RANGE,
payload: oldRanges,
},
source: 'patched',
},
}];
return { redos, undos };
} else {
const redoParams: IRemoveDataValidationMutationParams = { unitId, subUnitId, ruleId: rule.uid };
const redos = [{ id: RemoveDataValidationMutation.id, params: redoParams }];
const undos = removeDataValidationUndoFactory(this._injector, redoParams);
return { redos, undos };
}
};
const disposeList: (() => void)[] = [];
rule.ranges.forEach((range) => {
const disposable = this._refRangeService.registerRefRange(range, handleRangeChange, unitId, subUnitId);
disposeList.push(() => disposable.dispose());
});
const id = this._getIdWithUnitId(unitId, subUnitId, rule.uid);
const current = this._disposableMap.get(id) ?? new Set();
current.add(() => disposeList.forEach((dispose) => dispose()));
this._disposableMap.set(id, current);
};
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.forEach((dispose) => dispose());
}
break;
}
case 'update': {
const rule = option.rule!;
const disposeSet = this._disposableMap.get(this._getIdWithUnitId(unitId, subUnitId, rule!.uid));
if (disposeSet) {
disposeSet.forEach((dispose) => dispose());
}
this.registerRule(option.unitId, option.subUnitId, rule);
break;
}
}
})
);
this.disposeWithMe(toDisposable(() => {
this._disposableMap.forEach((item) => {
item.forEach((dispose) => dispose());
});
this._disposableMap.clear();
}));
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { Workbook } from '@univerjs/core';
import type { IAddDataValidationMutationParams, IRemoveDataValidationMutationParams } from '@univerjs/data-validation';
import type { ICopySheetCommandParams, IRemoveSheetCommandParams } from '@univerjs/sheets';
import { Disposable, generateRandomId, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { AddDataValidationMutation, RemoveDataValidationMutation } from '@univerjs/data-validation';
import { CopySheetCommand, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
export class SheetDataValidationSheetController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(SheetDataValidationModel) private readonly _sheetDataValidationModel: SheetDataValidationModel
) {
super();
this._initSheetChange();
}
// eslint-disable-next-line max-lines-per-function
private _initSheetChange() {
this.disposeWithMe(
this._sheetInterceptorService.interceptCommand({
// eslint-disable-next-line max-lines-per-function
getMutations: (commandInfo) => {
if (commandInfo.id === RemoveSheetCommand.id) {
const params = commandInfo.params as IRemoveSheetCommandParams;
const unitId = params.unitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return { redos: [], undos: [] };
}
const subUnitId = params.subUnitId || workbook.getActiveSheet()?.getSheetId();
if (!subUnitId) {
return { redos: [], undos: [] };
}
const rules = this._sheetDataValidationModel.getRules(unitId, subUnitId);
if (rules.length === 0) {
return { redos: [], undos: [] };
}
const ids = rules.map((i) => i.uid);
const redoParams: IRemoveDataValidationMutationParams = {
unitId,
subUnitId,
ruleId: ids,
source: 'patched',
};
const undoParams: IAddDataValidationMutationParams = {
unitId,
subUnitId,
rule: [...rules],
source: 'patched',
};
return {
redos: [{
id: RemoveDataValidationMutation.id,
params: redoParams,
}],
undos: [{
id: AddDataValidationMutation.id,
params: undoParams,
}],
};
} else if (commandInfo.id === CopySheetCommand.id) {
const params = commandInfo.params as ICopySheetCommandParams & { targetSubUnitId: string };
const { unitId, subUnitId, targetSubUnitId } = params;
if (!unitId || !subUnitId || !targetSubUnitId) {
return { redos: [], undos: [] };
}
const rules = this._sheetDataValidationModel.getRules(unitId, subUnitId);
if (rules.length === 0) {
return { redos: [], undos: [] };
}
const newRules = rules.map((rule) => {
const newRule = { ...rule, uid: generateRandomId(6) }; // Clear uid to let the system generate a new one
return newRule;
});
return {
redos: [
{
id: AddDataValidationMutation.id,
params: {
unitId,
subUnitId: targetSubUnitId,
rule: newRules,
source: 'patched',
},
},
],
undos: [
{
id: RemoveDataValidationMutation.id,
params: {
unitId,
subUnitId: targetSubUnitId,
ruleId: newRules.map((r) => r.uid),
source: 'patched',
},
},
],
};
}
return { redos: [], undos: [] };
},
})
);
}
}

View File

@@ -0,0 +1,100 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { Workbook } from '@univerjs/core';
import { Inject, Injector, IUniverInstanceService, RxDisposable, toDisposable, UniverInstanceType } from '@univerjs/core';
import { DataValidatorRegistryService } from '@univerjs/data-validation';
import { ClearSelectionAllCommand, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
import { getDataValidationDiffMutations } from '../commands/commands/data-validation.command';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
import { CheckboxValidator, DateValidator, DecimalValidator, ListValidator, TextLengthValidator } from '../validators';
import { AnyValidator } from '../validators/any-validator';
import { CustomFormulaValidator } from '../validators/custom-validator';
import { ListMultipleValidator } from '../validators/list-multiple-validator';
import { WholeValidator } from '../validators/whole-validator';
export class DataValidationController extends RxDisposable {
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(DataValidatorRegistryService) private readonly _dataValidatorRegistryService: DataValidatorRegistryService,
@Inject(Injector) private readonly _injector: Injector,
@Inject(SheetsSelectionsService) private _selectionManagerService: SheetsSelectionsService,
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(SheetDataValidationModel) private readonly _sheetDataValidationModel: SheetDataValidationModel
) {
super();
this._init();
}
private _init() {
this._registerValidators();
this._initCommandInterceptor();
}
private _registerValidators(): void {
([
AnyValidator,
DecimalValidator,
WholeValidator,
TextLengthValidator,
DateValidator,
CheckboxValidator,
ListValidator,
ListMultipleValidator,
CustomFormulaValidator,
]).forEach((Validator) => {
const validator = this._injector.createInstance(Validator as typeof ListValidator);
this.disposeWithMe(this._dataValidatorRegistryService.register(validator));
this.disposeWithMe(toDisposable(() => this._injector.delete(Validator as typeof ListValidator)));
});
}
private _initCommandInterceptor() {
this._sheetInterceptorService.interceptCommand({
getMutations: (commandInfo) => {
if (commandInfo.id === ClearSelectionAllCommand.id) {
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const unitId = workbook.getUnitId();
const worksheet = workbook.getActiveSheet();
if (!worksheet) {
throw new Error('No active sheet found');
}
const subUnitId = worksheet.getSheetId();
const selections = this._selectionManagerService.getCurrentSelections()?.map((s) => s.range);
const ruleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(unitId, subUnitId).clone();
if (selections) {
ruleMatrix.removeRange(selections);
}
const diffs = ruleMatrix.diff(this._sheetDataValidationModel.getRules(unitId, subUnitId));
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, diffs, this._injector, 'patched');
return {
undos: undoMutations,
redos: redoMutations,
};
}
return {
undos: [],
redos: [],
};
},
});
}
}

View File

@@ -0,0 +1,222 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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, UnitModel } from '@univerjs/core';
import {
ILogService,
Inject,
Injector,
IUniverInstanceService,
LocaleService,
LocaleType,
LogLevel,
Plugin,
set,
ThemeService,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
import {
ActiveDirtyManagerService,
DefinedNamesService,
DescriptionService,
FormulaCalculationSessionController,
FormulaCalculationSessionService,
FormulaDataModel,
FunctionService,
IActiveDirtyManagerService,
IDefinedNamesService,
IDescriptionService,
IFunctionService,
IRegisterFunctionService,
ISheetRowFilteredService,
ISuperTableService,
LexerTreeBuilder,
RegisterFunctionService,
RegisterOtherFormulaService,
SheetRowFilteredService,
SuperTableService,
} from '@univerjs/engine-formula';
import {
RefRangeService,
SheetInterceptorService,
SheetSkeletonService,
SheetsSelectionsService,
} from '@univerjs/sheets';
import {
DataValidationCacheService,
DataValidationCustomFormulaService,
DataValidationFormulaService,
DataValidationListCacheService,
SheetDataValidationModel,
SheetsDataValidationValidatorService,
} from '@univerjs/sheets-data-validation';
import enUS from '@univerjs/sheets/locale/en-US';
import zhCN from '@univerjs/sheets/locale/zh-CN';
import { DataValidationController } from '../../controllers/dv.controller';
import '@univerjs/sheets/facade';
import '@univerjs/sheets-data-validation/facade';
function getTestWorkbookDataDemo(): IWorkbookData {
return {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
cellData: {
0: {
3: {
f: '=SUM(A1)',
si: '3e4r5t',
},
4: {
v: 123,
t: 2,
},
},
1: {
3: {
f: '=SUM(A2)',
si: 'OSPtzm',
},
},
2: {
3: {
si: 'OSPtzm',
},
},
3: {
3: {
si: 'OSPtzm',
},
},
},
rowCount: 100,
columnCount: 100,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
}
export interface ITestBed {
univer: Univer;
get: Injector['get'];
sheet: UnitModel<IWorkbookData>;
univerAPI: FUniver;
injector: Injector;
}
export function createFacadeTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
const univer = new Univer();
const injector = univer.__getInjector();
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
const injector = this._injector;
injector.add([SheetsSelectionsService]);
injector.add([SheetInterceptorService]);
injector.add([SheetSkeletonService]);
injector.add([FormulaDataModel]);
injector.add([LexerTreeBuilder]);
injector.add([RefRangeService]);
injector.add([IFunctionService, { useClass: FunctionService }]);
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
injector.add([ISuperTableService, { useClass: SuperTableService }]);
injector.add([IDescriptionService, { useClass: DescriptionService }]);
injector.add([IRegisterFunctionService, { useClass: RegisterFunctionService }]);
injector.add([FormulaCalculationSessionService]);
injector.add([FormulaCalculationSessionController]);
// register feature modules
([
// data validation
[DataValidationCacheService],
[DataValidationListCacheService],
[DataValidationFormulaService],
[DataValidationCustomFormulaService],
[RegisterOtherFormulaService],
[IActiveDirtyManagerService, { useClass: ActiveDirtyManagerService }],
[ISheetRowFilteredService, { useClass: SheetRowFilteredService }],
[SheetsDataValidationValidatorService],
[SheetDataValidationModel],
[DataValidationController],
] as Dependency[]).forEach((d) => {
injector.add(d);
});
dependencies?.forEach((d) => injector.add(d));
this._injector.get(SheetInterceptorService);
this._injector.get(FormulaCalculationSessionController);
this._injector.get(DataValidationController);
}
override onReady(): void {
}
}
// load i18n
injector.get(LocaleService).load({ zhCN, enUS });
// load theme service
const themeService = injector.get(ThemeService);
const theme = themeService.getCurrentTheme();
const newTheme = set(theme, 'gray.1000', '#35322b');
themeService.setTheme(newTheme);
// register builtin plugins
// note that UI plugins are not registered here, because the unit test environment does not have a UI
univer.registerPlugin(UniverDataValidationPlugin);
univer.registerPlugin(TestPlugin);
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData || getTestWorkbookDataDemo());
const univerInstanceService = injector.get(IUniverInstanceService);
univerInstanceService.focusUnit('test');
// set log level
const logService = injector.get(ILogService);
logService.setLogLevel(LogLevel.SILENT); // NOTE: change this to `LogLevel.VERBOSE` to debug tests via logs
// init data validation
const univerAPI = FUniver.newAPI(injector);
return {
univer,
get: injector.get.bind(injector),
sheet,
univerAPI,
injector,
};
}

View File

@@ -0,0 +1,193 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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, UnitModel } from '@univerjs/core';
import {
ILogService,
Inject,
Injector,
IUniverInstanceService,
LocaleService,
LocaleType,
LogLevel,
Plugin,
set,
ThemeService,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
import {
ActiveDirtyManagerService,
DefinedNamesService,
FormulaDataModel,
IActiveDirtyManagerService,
IDefinedNamesService,
ISheetRowFilteredService,
LexerTreeBuilder,
RegisterOtherFormulaService,
SheetRowFilteredService,
} from '@univerjs/engine-formula';
import {
RefRangeService,
SheetInterceptorService,
SheetSkeletonService,
SheetsSelectionsService,
} from '@univerjs/sheets';
import {
DataValidationCacheService,
DataValidationCustomFormulaService,
DataValidationFormulaService,
DataValidationListCacheService,
SheetDataValidationModel,
SheetsDataValidationValidatorService,
} from '@univerjs/sheets-data-validation';
import enUS from '@univerjs/sheets/locale/en-US';
import zhCN from '@univerjs/sheets/locale/zh-CN';
import '@univerjs/sheets/facade';
import '@univerjs/sheets-data-validation/facade';
function getTestWorkbookDataDemo(): IWorkbookData {
return {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
cellData: {
0: {
0: {
v: 1,
},
},
1: {
0: {
v: 2,
},
},
2: {
0: {
v: 3,
},
},
3: {
0: {
v: 4,
},
},
},
rowCount: 100,
columnCount: 100,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
}
export interface ITestBed {
univer: Univer;
get: Injector['get'];
sheet: UnitModel<IWorkbookData>;
univerAPI: FUniver;
injector: Injector;
}
export function createWorksheetTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
const univer = new Univer();
const injector = univer.__getInjector();
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
const injector = this._injector;
injector.add([SheetsSelectionsService]);
injector.add([SheetInterceptorService]);
injector.add([SheetSkeletonService]);
injector.add([FormulaDataModel]);
injector.add([LexerTreeBuilder]);
injector.add([RefRangeService]);
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
// register feature modules
([
// data validation
[DataValidationCacheService],
[DataValidationFormulaService],
[DataValidationListCacheService],
[DataValidationCustomFormulaService],
[RegisterOtherFormulaService],
[IActiveDirtyManagerService, { useClass: ActiveDirtyManagerService }],
[ISheetRowFilteredService, { useClass: SheetRowFilteredService }],
[SheetsDataValidationValidatorService],
[SheetDataValidationModel],
] as Dependency[]).forEach((d) => {
injector.add(d);
});
dependencies?.forEach((d) => injector.add(d));
}
override onReady(): void {
}
}
// load i18n
injector.get(LocaleService).load({ zhCN, enUS });
// load theme service
const themeService = injector.get(ThemeService);
const theme = themeService.getCurrentTheme();
const newTheme = set(theme, 'gray.1000', '#35322b');
themeService.setTheme(newTheme);
// register builtin plugins
// note that UI plugins are not registered here, because the unit test environment does not have a UI
univer.registerPlugin(TestPlugin);
univer.registerPlugin(UniverDataValidationPlugin);
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData || getTestWorkbookDataDemo());
const univerInstanceService = injector.get(IUniverInstanceService);
univerInstanceService.focusUnit('test');
// set log level
const logService = injector.get(ILogService);
logService.setLogLevel(LogLevel.SILENT); // NOTE: change this to `LogLevel.VERBOSE` to debug tests via logs
const univerAPI = FUniver.newAPI(injector);
return {
univer,
get: injector.get.bind(injector),
sheet,
univerAPI,
injector,
};
}

View File

@@ -0,0 +1,272 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { FUniver } from '@univerjs/core/facade';
import { DataValidationErrorStyle, DataValidationOperator, DataValidationType } from '@univerjs/core';
import { deserializeListOptions } from '@univerjs/sheets';
import { FDataValidationBuilder } from '@univerjs/sheets-data-validation/facade/f-data-validation-builder.js';
import { beforeEach, describe, expect, it } from 'vitest';
import { createFacadeTestBed } from './create-test-bed';
describe('Test FDataValidationBuilder', () => {
let univerAPI: FUniver;
beforeEach(() => {
const testBed = createFacadeTestBed();
univerAPI = testBed.univerAPI;
});
it('should build checkbox', () => {
const builder = new FDataValidationBuilder();
builder.requireCheckbox('1', '2');
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.CHECKBOX);
expect(builder.getCriteriaValues()).toEqual([undefined, '1', '2']);
});
it('should build number equal', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberEqualTo(1);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DECIMAL);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.EQUAL, '1', undefined]);
});
it('should build number greater than', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberGreaterThan(1);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DECIMAL);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.GREATER_THAN, '1', undefined]);
});
it('should build number greater than or equal', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberGreaterThanOrEqualTo(1);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DECIMAL);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.GREATER_THAN_OR_EQUAL, '1', undefined]);
});
it('should build number less than', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberLessThan(1);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DECIMAL);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.LESS_THAN, '1', undefined]);
});
it('should build number less than or equal to', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberLessThanOrEqualTo(1);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DECIMAL);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.LESS_THAN_OR_EQUAL, '1', undefined]);
});
it('should build number between', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberBetween(1, 2);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DECIMAL);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.BETWEEN, '1', '2']);
});
it('should build number not between', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberNotBetween(1, 2);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DECIMAL);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.NOT_BETWEEN, '1', '2']);
});
it('should build int equal', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberEqualTo(1, true);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.WHOLE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.EQUAL, '1', undefined]);
});
it('should build int greater than', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberGreaterThan(1, true);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.WHOLE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.GREATER_THAN, '1', undefined]);
});
it('should build int greater than or equal', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberGreaterThanOrEqualTo(1, true);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.WHOLE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.GREATER_THAN_OR_EQUAL, '1', undefined]);
});
it('should build int less than', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberLessThan(1, true);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.WHOLE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.LESS_THAN, '1', undefined]);
});
it('should build int less than or equal to', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberLessThanOrEqualTo(1, true);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.WHOLE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.LESS_THAN_OR_EQUAL, '1', undefined]);
});
it('should build int between', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberBetween(1, 2, true);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.WHOLE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.BETWEEN, '1', '2']);
});
it('should build int not between', () => {
const builder = new FDataValidationBuilder();
builder.requireNumberNotBetween(1, 2, true);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.WHOLE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.NOT_BETWEEN, '1', '2']);
});
it('should build decimal and whole number not equal rules', () => {
const decimalBuilder = new FDataValidationBuilder();
decimalBuilder.requireNumberNotEqualTo(10);
expect(decimalBuilder.getCriteriaType()).toBe(DataValidationType.DECIMAL);
expect(decimalBuilder.getCriteriaValues()).toEqual([DataValidationOperator.NOT_EQUAL, '10', undefined]);
const wholeBuilder = new FDataValidationBuilder();
wholeBuilder.requireNumberNotEqualTo(7, true);
expect(wholeBuilder.getCriteriaType()).toBe(DataValidationType.WHOLE);
expect(wholeBuilder.getCriteriaValues()).toEqual([DataValidationOperator.NOT_EQUAL, '7', undefined]);
});
it('should build date equal', () => {
const builder = new FDataValidationBuilder();
builder.requireDateEqualTo(new Date('2020-01-01'));
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DATE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.EQUAL, new Date('2020-01-01').toLocaleDateString(), undefined]);
});
it('should build date after', () => {
const builder = new FDataValidationBuilder();
builder.requireDateAfter(new Date('2020-01-01'));
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DATE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.GREATER_THAN, new Date('2020-01-01').toLocaleDateString(), undefined]);
});
it('should build date before', () => {
const builder = new FDataValidationBuilder();
builder.requireDateBefore(new Date('2020-01-01'));
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DATE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.LESS_THAN, new Date('2020-01-01').toLocaleDateString(), undefined]);
});
it('should build date on or after', () => {
const builder = new FDataValidationBuilder();
builder.requireDateOnOrAfter(new Date('2020-01-01'));
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DATE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.GREATER_THAN_OR_EQUAL, new Date('2020-01-01').toLocaleDateString(), undefined]);
});
it('should build date on or before', () => {
const builder = new FDataValidationBuilder();
builder.requireDateOnOrBefore(new Date('2020-01-01'));
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DATE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.LESS_THAN_OR_EQUAL, new Date('2020-01-01').toLocaleDateString(), undefined]);
});
it('should build date between', () => {
const builder = new FDataValidationBuilder();
builder.requireDateBetween(new Date('2020-01-01'), new Date('2020-01-02'));
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DATE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.BETWEEN, new Date('2020-01-01').toLocaleDateString(), new Date('2020-01-02').toLocaleDateString()]);
});
it('should build date not between', () => {
const builder = new FDataValidationBuilder();
builder.requireDateNotBetween(new Date('2020-01-01'), new Date('2020-01-02'));
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.DATE);
expect(builder.getCriteriaValues()).toEqual([DataValidationOperator.NOT_BETWEEN, new Date('2020-01-01').toLocaleDateString(), new Date('2020-01-02').toLocaleDateString()]);
});
it('should build value in list', () => {
const builder = new FDataValidationBuilder();
builder.requireValueInList(['1', '2', '3']);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.LIST);
expect(builder.getCriteriaValues()).toEqual([undefined, '["1","2","3"]', undefined]);
});
it('should build value in list-multiple', () => {
const builder = new FDataValidationBuilder();
builder.requireValueInList(['1', '2', '3'], true);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.LIST_MULTIPLE);
expect(builder.getCriteriaValues()).toEqual([undefined, '["1","2","3"]', undefined]);
});
it('should keep commas inside list values', () => {
const builder = new FDataValidationBuilder();
builder.requireValueInList(['Part 1 (blue)', 'Part 2 (green, short)']);
const [, formula1] = builder.getCriteriaValues();
expect(deserializeListOptions(formula1!)).toEqual(['Part 1 (blue)', 'Part 2 (green, short)']);
});
it('should deserialize legacy comma separated list values', () => {
expect(deserializeListOptions('1,2,3')).toEqual(['1', '2', '3']);
});
it('should build value in range', () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
// // A1 sets the number
const range1 = activeSheet!.getRange(0, 0, 1, 1)!;
const builder = new FDataValidationBuilder();
builder.requireValueInRange(range1);
expect(builder.build().getCriteriaType()).toEqual(DataValidationType.LIST);
expect(builder.getCriteriaValues()).toEqual([undefined, '=[test]sheet1!A1', undefined]);
});
it('copies configured rules and keeps user-facing validation options', () => {
const builder = new FDataValidationBuilder();
builder
.requireValueInList(['Approved', 'Rejected'])
.setAllowInvalid(false)
.setAllowBlank(false)
.setOptions({
error: 'Choose an approval state',
showErrorMessage: true,
});
expect(builder.getCriteriaType()).toBe(DataValidationType.LIST);
expect(builder.getHelpText()).toBe('Choose an approval state');
expect(builder.getAllowInvalid()).toBe(false);
const rule = builder.build().rule;
expect(rule).toMatchObject({
errorStyle: DataValidationErrorStyle.STOP,
allowBlank: false,
error: 'Choose an approval state',
showErrorMessage: true,
});
const copiedRule = builder.copy().build().rule;
expect(copiedRule).toMatchObject({
type: rule.type,
formula1: rule.formula1,
errorStyle: DataValidationErrorStyle.STOP,
allowBlank: false,
error: 'Choose an approval state',
});
expect(copiedRule.uid).not.toBe(rule.uid);
});
});

View File

@@ -0,0 +1,216 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { Injector, Univer } from '@univerjs/core';
import type { FUniver } from '@univerjs/core/facade';
import {
DataValidationErrorStyle,
DataValidationOperator,
DataValidationStatus,
DataValidationType,
ICommandService,
} from '@univerjs/core';
import {
FormulaExecuteStageType,
OtherFormulaMarkDirty,
RemoveOtherFormulaMutation,
SetFormulaCalculationNotificationMutation,
SetFormulaCalculationResultMutation,
SetFormulaCalculationStartMutation,
SetOtherFormulaMutation,
} from '@univerjs/engine-formula';
import {
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
DataValidationCustomFormulaService,
RemoveSheetDataValidationCommand,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
} from '@univerjs/sheets-data-validation';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFacadeTestBed } from './create-test-bed';
import '@univerjs/sheets-formula/facade';
describe('Test FRange', () => {
let get: Injector['get'];
let commandService: ICommandService;
let univer: Univer;
let univerAPI: FUniver;
beforeEach(() => {
const testBed = createFacadeTestBed();
get = testBed.get;
univer = testBed.univer;
univerAPI = testBed.univerAPI;
commandService = get(ICommandService);
[
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
RemoveSheetDataValidationCommand,
OtherFormulaMarkDirty,
RemoveOtherFormulaMutation,
SetOtherFormulaMutation,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
].forEach((command) => commandService.registerCommand(command));
commandService.registerCommand(SetFormulaCalculationStartMutation);
commandService.registerCommand(SetFormulaCalculationNotificationMutation);
commandService.registerCommand(SetFormulaCalculationResultMutation);
vi.stubGlobal('requestIdleCallback', ((callback: IdleRequestCallback) => {
callback({ didTimeout: false, timeRemaining: () => 16 } as IdleDeadline);
return 1;
}) as typeof requestIdleCallback);
});
afterEach(() => {
univer.dispose();
vi.unstubAllGlobals();
});
it('Range set data validation', async () => {
const activeSheet = univerAPI.getActiveWorkbook()?.getActiveSheet();
if (!activeSheet) {
throw new Error('Active sheet not found');
}
const range = activeSheet.getRange(0, 0, 10, 10);
const range2 = activeSheet.getRange(11, 11, 2, 2);
await range.setDataValidation(univerAPI.newDataValidation().requireCheckbox().build());
await range2?.setDataValidation(univerAPI.newDataValidation().requireNumberEqualTo(1).build());
const range3 = activeSheet.getRange(0, 0, 100, 100);
expect(range.getDataValidation()).toBeTruthy();
expect(range.getDataValidation()?.rule.ranges).toEqual([{
unitId: univerAPI.getActiveWorkbook()?.getId(),
sheetId: activeSheet.getSheetId(),
startRow: 0,
endRow: 9,
startColumn: 0,
endColumn: 9,
}]);
expect(range.getDataValidation()?.getCriteriaType()).toEqual(DataValidationType.CHECKBOX);
expect(range.getDataValidations().length).toEqual(1);
expect(range3?.getDataValidations().length).toEqual(2);
expect(activeSheet?.getDataValidations().length).toEqual(2);
});
it('manages an applied validation rule from a range facade', async () => {
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const sourceRange = activeSheet.getRange(0, 0, 2, 1);
const targetRange = activeSheet.getRange(3, 0, 2, 1);
const rule = univerAPI.newDataValidation()
.requireNumberBetween(1, 10)
.setOptions({
errorStyle: DataValidationErrorStyle.STOP,
showErrorMessage: true,
error: 'Use a number from 1 to 10',
})
.build();
sourceRange.setDataValidation(rule);
const appliedRule = sourceRange.getDataValidation()!;
expect(appliedRule.getApplied()).toBe(true);
expect(appliedRule.getAllowInvalid()).toBe(false);
expect(appliedRule.getCriteriaType()).toBe(DataValidationType.DECIMAL);
expect(appliedRule.getCriteriaValues()).toEqual([DataValidationOperator.BETWEEN, '1', '10']);
expect(appliedRule.getHelpText()).toBe('Use a number from 1 to 10');
expect(appliedRule.getUnitId()).toBe(univerAPI.getActiveWorkbook()!.getId());
expect(appliedRule.getSheetId()).toBe(activeSheet.getSheetId());
expect(appliedRule.getRanges().map((range) => range.getA1Notation())).toEqual(['A1:A2']);
expect(appliedRule.copy().getCriteriaValues()).toEqual([DataValidationOperator.BETWEEN, '1', '10']);
appliedRule
.setCriteria(DataValidationType.DECIMAL, [DataValidationOperator.GREATER_THAN, '2', undefined as never], false)
.setOptions({ error: 'Use a whole number greater than 2', showErrorMessage: false })
.setRanges([targetRange]);
const movedRule = targetRange.getDataValidation()!;
expect(sourceRange.getDataValidation()).toBeUndefined();
expect(movedRule.getCriteriaType()).toBe(DataValidationType.DECIMAL);
expect(movedRule.getCriteriaValues()).toEqual([DataValidationOperator.GREATER_THAN, '2', undefined]);
expect(movedRule.getHelpText()).toBe('Use a whole number greater than 2');
expect(movedRule.getRanges().map((range) => range.getA1Notation())).toEqual(['A4:A5']);
expect(await targetRange.getValidatorStatus()).toEqual([
[DataValidationStatus.INVALID, DataValidationStatus.INVALID],
]);
expect(movedRule.delete()).toBe(true);
expect(targetRange.getDataValidation()).toBeUndefined();
targetRange.setDataValidation(rule);
expect(targetRange.getDataValidations()).toHaveLength(1);
targetRange.setDataValidation(null);
expect(targetRange.getDataValidations()).toEqual([]);
});
it('resolves onCalculationResultApplied after data-validation custom formula results are emitted', async () => {
const workbook = univerAPI.getActiveWorkbook()!;
const activeSheet = workbook.getActiveSheet();
const unitId = workbook.getId();
const subUnitId = activeSheet.getSheetId();
const range = activeSheet.getRange(0, 0, 1, 1);
const formula = univerAPI.getFormula();
const rule = univerAPI.newDataValidation()
.requireFormulaSatisfied('=A1>0')
.build();
await range.setDataValidation(rule);
const customFormulaService = get(DataValidationCustomFormulaService);
const registeredFormula = customFormulaService.getRuleFormulaInfo(unitId, subUnitId, rule.rule.uid);
expect(registeredFormula?.formula).toBe('=A1>0');
const waitForResult = formula.onCalculationResultApplied();
await commandService.executeCommand(SetFormulaCalculationStartMutation.id, {}, { onlyLocal: true });
await commandService.executeCommand(SetFormulaCalculationNotificationMutation.id, {
stageInfo: {
stage: FormulaExecuteStageType.START_CALCULATION,
completedFormulasCount: 0,
completedArrayFormulasCount: 0,
formulaCycleIndex: 0,
totalArrayFormulasToCalculate: 0,
totalFormulasToCalculate: 1,
},
});
await commandService.executeCommand(SetFormulaCalculationResultMutation.id, {
unitData: {},
unitOtherData: {
[unitId]: {
[subUnitId]: {
[registeredFormula!.formulaId]: {
0: {
0: [[{ v: true }]],
},
},
},
},
},
});
await expect(waitForResult).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,159 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationOperator, DataValidationStatus, DataValidationType, ICommandService } from '@univerjs/core';
import {
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
RemoveSheetAllDataValidationCommand,
RemoveSheetDataValidationCommand,
SheetDataValidationModel,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
} from '@univerjs/sheets-data-validation';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createFacadeTestBed } from './create-test-bed';
describe('FUniver sheets data validation events', () => {
let testBed: ReturnType<typeof createFacadeTestBed>;
let commandService: ICommandService;
beforeEach(() => {
testBed = createFacadeTestBed();
commandService = testBed.get(ICommandService);
[
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
RemoveSheetAllDataValidationCommand,
RemoveSheetDataValidationCommand,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
].forEach((command) => commandService.registerCommand(command));
});
afterEach(() => {
testBed.univer.dispose();
});
it('fires before and changed events around validation rule lifecycle commands', () => {
const { univerAPI } = testBed;
const workbook = univerAPI.getActiveWorkbook()!;
const worksheet = workbook.getActiveSheet();
const range = worksheet.getRange(0, 0, 1, 1);
const movedRange = worksheet.getRange(2, 0, 1, 1);
const logs: string[] = [];
univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationAdd, ({ rule, worksheet }) => {
logs.push(`before-add:${worksheet.getSheetId()}:${rule.type}`);
});
univerAPI.addEvent(univerAPI.Event.SheetDataValidationChanged, ({ changeType, rule }) => {
logs.push(`changed:${changeType}:${rule.getCriteriaType()}`);
});
univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationCriteriaUpdate, ({ newCriteria }) => {
logs.push(`before-criteria:${newCriteria.type}:${newCriteria.formula1}`);
});
univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationOptionsUpdate, ({ newOptions }) => {
logs.push(`before-options:${newOptions.error}`);
});
univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationRangeUpdate, ({ newRanges }) => {
logs.push(`before-range:${newRanges[0].startRow}`);
});
univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationDelete, ({ ruleId }) => {
logs.push(`before-delete:${ruleId}`);
});
univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationDeleteAll, ({ rules }) => {
logs.push(`before-delete-all:${rules.length}`);
});
const rule = univerAPI.newDataValidation()
.requireNumberBetween(1, 5)
.build();
range.setDataValidation(rule);
const appliedRule = range.getDataValidation()!;
const ruleId = appliedRule.rule.uid;
appliedRule.setCriteria(DataValidationType.DECIMAL, [DataValidationOperator.GREATER_THAN, '2', undefined as never]);
appliedRule.setOptions({ error: 'Use a whole number greater than 2' });
appliedRule.setRanges([movedRange]);
expect(movedRange.getDataValidation()?.delete()).toBe(true);
movedRange.setDataValidation(univerAPI.newDataValidation().requireCheckbox().build());
expect(commandService.syncExecuteCommand(RemoveSheetAllDataValidationCommand.id, {
unitId: workbook.getId(),
subUnitId: worksheet.getSheetId(),
})).toBe(true);
expect(logs).toEqual([
`before-add:${worksheet.getSheetId()}:${DataValidationType.DECIMAL}`,
`changed:add:${DataValidationType.DECIMAL}`,
`before-criteria:${DataValidationType.DECIMAL}:2`,
`changed:update:${DataValidationType.DECIMAL}`,
'before-options:Use a whole number greater than 2',
`changed:update:${DataValidationType.DECIMAL}`,
'before-range:2',
`changed:update:${DataValidationType.DECIMAL}`,
`before-delete:${ruleId}`,
`changed:remove:${DataValidationType.DECIMAL}`,
`before-add:${worksheet.getSheetId()}:${DataValidationType.CHECKBOX}`,
`changed:add:${DataValidationType.CHECKBOX}`,
'before-delete-all:1',
`changed:remove:${DataValidationType.CHECKBOX}`,
]);
});
it('cancels validation commands from before events', () => {
const { univerAPI } = testBed;
const range = univerAPI.getActiveWorkbook()!.getActiveSheet().getRange(0, 0, 1, 1);
univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationAdd, (event) => {
event.cancel = true;
});
range.setDataValidation(univerAPI.newDataValidation().requireCheckbox().build());
expect(range.getDataValidation()).toBeUndefined();
});
it('fires validator status changes for cells whose validation result changes', async () => {
const { get, univerAPI } = testBed;
const workbook = univerAPI.getActiveWorkbook()!;
const worksheet = workbook.getActiveSheet();
const range = worksheet.getRange(0, 0, 1, 1);
const statuses: string[] = [];
range.setDataValidation(univerAPI.newDataValidation().requireNumberBetween(1, 5).build());
const rule = range.getDataValidation()!;
univerAPI.addEvent(univerAPI.Event.SheetDataValidatorStatusChanged, ({ column, row, rule, status }) => {
statuses.push(`${row}:${column}:${rule.getCriteriaType()}:${status}`);
});
get(SheetDataValidationModel).validator(rule.rule, {
unitId: workbook.getId(),
subUnitId: worksheet.getSheetId(),
workbook: workbook.getWorkbook(),
worksheet: worksheet.getSheet(),
row: 0,
col: 0,
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(statuses).toEqual([`0:0:${DataValidationType.DECIMAL}:${DataValidationStatus.VALID}`]);
});
});

View File

@@ -0,0 +1,183 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { Injector, IWorkbookData } from '@univerjs/core';
import type { FUniver } from '@univerjs/core/facade';
import {
DataValidationStatus,
ICommandService,
LocaleType,
} from '@univerjs/core';
import {
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
RemoveSheetAllDataValidationCommand,
RemoveSheetDataValidationCommand,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
} from '@univerjs/sheets-data-validation';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createFacadeTestBed } from './create-test-bed';
function createWorkbookData(): IWorkbookData {
return {
id: 'test',
appVersion: '3.0.0-alpha',
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: ['sheet1'],
styles: {},
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
rowCount: 100,
columnCount: 100,
cellData: {
0: { 0: { v: 1, t: 2 } },
1: { 0: { v: 2, t: 2 } },
2: { 0: { v: 3, t: 2 } },
3: { 0: { v: 4, t: 2 } },
},
},
},
};
}
describe('Test FWorksheet data validation facade', () => {
let get: Injector['get'];
let univerAPI: FUniver;
let univer: ReturnType<typeof createFacadeTestBed>['univer'];
beforeEach(() => {
const testBed = createFacadeTestBed(createWorkbookData());
univer = testBed.univer;
get = testBed.get;
univerAPI = testBed.univerAPI;
const commandService = get(ICommandService);
[
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
RemoveSheetAllDataValidationCommand,
RemoveSheetDataValidationCommand,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
].forEach((command) => commandService.registerCommand(command));
});
afterEach(() => {
univer.dispose();
});
it('reads worksheet rules, validator status, and validation errors', async () => {
const workbook = univerAPI.getActiveWorkbook()!;
const worksheet = workbook.getActiveSheet();
const range = worksheet.getRange(0, 0, 4, 1);
const rule = univerAPI.newDataValidation()
.requireNumberBetween(1, 2)
.setOptions({
showErrorMessage: true,
error: 'Only values from 1 to 2 are valid',
})
.build();
range.setDataValidation(rule);
const rules = worksheet.getDataValidations();
expect(rules).toHaveLength(1);
expect(worksheet.getDataValidation(rules[0].rule.uid)?.getCriteriaValues()).toEqual(
rules[0].getCriteriaValues()
);
expect(worksheet.getDataValidation('missing-rule')).toBeNull();
const worksheetStatus = await worksheet.getValidatorStatusAsync();
expect(worksheetStatus.getValue(2, 0)).toBe(DataValidationStatus.INVALID);
expect(worksheetStatus.getValue(3, 0)).toBe(DataValidationStatus.INVALID);
await expect(range.getDataValidationErrorAsync()).resolves.toMatchObject([
{
sheetName: 'sheet1',
row: 2,
column: 0,
ruleId: rules[0].rule.uid,
inputValue: 3,
},
{
sheetName: 'sheet1',
row: 3,
column: 0,
ruleId: rules[0].rule.uid,
inputValue: 4,
},
]);
await expect(worksheet.getAllDataValidationErrorAsync()).resolves.toMatchObject([
{
sheetName: 'sheet1',
row: 2,
column: 0,
inputValue: 3,
},
{
sheetName: 'sheet1',
row: 3,
column: 0,
inputValue: 4,
},
]);
});
it('aggregates validation status and errors at workbook level', async () => {
const workbook = univerAPI.getActiveWorkbook()!;
const worksheet = workbook.getActiveSheet();
const rule = univerAPI.newDataValidation()
.requireNumberLessThanOrEqualTo(2)
.build();
worksheet.getRange(0, 0, 4, 1).setDataValidation(rule);
const status = await workbook.getValidatorStatus();
expect(status.sheet1.getValue(2, 0)).toBe(DataValidationStatus.INVALID);
expect(status.sheet1.getValue(3, 0)).toBe(DataValidationStatus.INVALID);
await expect(workbook.getAllDataValidationErrorAsync()).resolves.toMatchObject([
{
sheetName: 'sheet1',
row: 2,
column: 0,
inputValue: 3,
},
{
sheetName: 'sheet1',
row: 3,
column: 0,
inputValue: 4,
},
]);
});
it('returns empty error lists when no rule is applied', async () => {
const workbook = univerAPI.getActiveWorkbook()!;
const worksheet = workbook.getActiveSheet();
await expect(worksheet.getAllDataValidationErrorAsync()).resolves.toEqual([]);
await expect(workbook.getAllDataValidationErrorAsync()).resolves.toEqual([]);
await expect(worksheet.getRange(0, 0, 1, 1).getDataValidationErrorAsync()).resolves.toEqual([]);
});
});

View File

@@ -0,0 +1,963 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IDataValidationRule, IDataValidationRuleOptions } from '@univerjs/core';
import type { FRange } from '@univerjs/sheets/facade';
import { DataValidationErrorStyle, DataValidationOperator, DataValidationType, generateRandomId } from '@univerjs/core';
import { serializeRangeToRefString } from '@univerjs/engine-formula';
import { serializeListOptions } from '@univerjs/sheets';
import { FDataValidation } from './f-data-validation';
/**
* Builder for data validation rules. use {@link FUniver} `univerAPI.newDataValidation()` to create a new builder.
* @example
* ```typescript
* // Set the data validation for cell A1 to require a value from B1:B10
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('B1:B2');
* fRange.setValues([
* ['Yes'],
* ['No']
* ]);
*
* const rule = univerAPI.newDataValidation()
* .requireValueInRange(fRange)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a value from the list'
* })
* .build();
* const cell = fWorksheet.getRange('A1');
* cell.setDataValidation(rule);
* ```
* @hideconstructor
*/
export class FDataValidationBuilder {
private _rule: IDataValidationRule;
constructor(rule?: IDataValidationRule) {
this._rule = rule ?? {
uid: generateRandomId(),
ranges: undefined,
type: DataValidationType.CUSTOM,
};
}
/**
* Builds an FDataValidation instance based on the _rule property of the current class
* @returns {FDataValidation} A new instance of the FDataValidation class
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number between 1 and 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a number between 1 and 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
build(): FDataValidation {
return new FDataValidation(this._rule);
}
/**
* Creates a duplicate of the current DataValidationBuilder object
* @returns {FDataValidationBuilder} A new instance of the DataValidationBuilder class
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number between 1 and 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const builder = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a number between 1 and 10'
* });
* fRange.setDataValidation(builder.build());
*
* // Copy the builder applied to the new range F1:G10
* const newRange = fWorksheet.getRange('F1:G10');
* const copyBuilder = builder.copy();
* newRange.setDataValidation(copyBuilder.build());
* ```
*/
copy(): FDataValidationBuilder {
return new FDataValidationBuilder({
...this._rule,
uid: generateRandomId(),
});
}
/**
* Determines whether invalid data is allowed
* @returns {boolean} True if invalid data is allowed, False otherwise
* @example
* ```typescript
* const builder = univerAPI.newDataValidation().requireNumberBetween(1, 10);
* console.log(builder.getAllowInvalid());
* ```
*/
getAllowInvalid(): boolean {
return this._rule.errorStyle !== DataValidationErrorStyle.STOP;
}
/**
* Gets the data validation type of the rule
* @returns {DataValidationType | string} The data validation type
* @example
* ```typescript
* const builder = univerAPI.newDataValidation();
* console.log(builder.getCriteriaType()); // custom
*
* builder.requireNumberBetween(1, 10);
* console.log(builder.getCriteriaType()); // decimal
*
* builder.requireValueInList(['Yes', 'No']);
* console.log(builder.getCriteriaType()); // list
* ```
*/
getCriteriaType(): DataValidationType | string {
return this._rule.type;
}
/**
* Gets the values used for criteria evaluation
* @returns {[string | undefined, string | undefined, string | undefined]} An array containing the operator, formula1, and formula2 values
* @example
* ```typescript
* const builder = univerAPI.newDataValidation().requireNumberBetween(1, 10);
* const [operator, formula1, formula2] = builder.getCriteriaValues();
* console.log(operator, formula1, formula2); // between 1 10
*
* builder.requireValueInList(['Yes', 'No']);
* console.log(builder.getCriteriaValues()); // undefined Yes,No undefined
* ```
*/
getCriteriaValues(): [string | undefined, string | undefined, string | undefined] {
return [this._rule.operator, this._rule.formula1, this._rule.formula2];
}
/**
* Gets the help text information, which is used to provide users with guidance and support
* @returns {string | undefined} Returns the help text information. If there is no error message, it returns an undefined value
* @example
* ```typescript
* const builder = univerAPI.newDataValidation().setOptions({
* showErrorMessage: true,
* error: 'Please enter a valid value'
* });
* console.log(builder.getHelpText()); // 'Please enter a valid value'
* ```
*/
getHelpText(): string | undefined {
return this._rule.error;
}
/**
* Sets the data validation rule to require that the input is a boolean value; this value is rendered as a checkbox.
* @param {string} [checkedValue] - The value assigned to a checked box.
* @param {string} [uncheckedValue] - The value assigned to an unchecked box.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set the data validation for cell A1:A10 to require a checkbox with default 1 and 0 values
* const fRange = fWorksheet.getRange('A1:A10');
* const rule = univerAPI.newDataValidation()
* .requireCheckbox()
* .build();
* fRange.setDataValidation(rule);
*
* // Set the data validation for cell B1:B10 to require a checkbox with 'Yes' and 'No' values
* const fRange2 = fWorksheet.getRange('B1:B10');
* const rule2 = univerAPI.newDataValidation()
* .requireCheckbox('Yes', 'No')
* .build();
* fRange2.setDataValidation(rule2);
* ```
*/
requireCheckbox(checkedValue?: string, uncheckedValue?: string): FDataValidationBuilder {
this._rule.type = DataValidationType.CHECKBOX;
this._rule.formula1 = checkedValue;
this._rule.formula2 = uncheckedValue;
return this;
}
/**
* Set the data validation type to DATE and configure the validation rules to be after a specific date.
* @param {Date} date - The latest unacceptable date.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some date values in the range A1:B2
* const fRange = fWorksheet.getRange('A1:B2');
* fRange.setValues([
* ['2024-01-01', '2024-12-31'],
* ['2025-01-01', '2025-12-31']
* ]);
*
* // Create a data validation rule that requires a date after 2025-01-01
* const rule = univerAPI.newDataValidation()
* .requireDateAfter(new Date('2025-01-01'))
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validation status of the range
* const status = await fRange.getValidatorStatus();
* console.log(status); // [['invalid', 'invalid', 'invalid', 'valid']]
* ```
*/
requireDateAfter(date: Date): FDataValidationBuilder {
this._rule.type = DataValidationType.DATE;
this._rule.formula1 = date.toLocaleDateString();
this._rule.operator = DataValidationOperator.GREATER_THAN;
return this;
}
/**
* Set the data validation type to DATE and configure the validation rules to be before a specific date.
* @param {Date} date - The earliest unacceptable date.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some date values in the range A1:B2
* const fRange = fWorksheet.getRange('A1:B2');
* fRange.setValues([
* ['2024-01-01', '2024-12-31'],
* ['2025-01-01', '2025-12-31']
* ]);
*
* // Create a data validation rule that requires a date before 2025-01-01
* const rule = univerAPI.newDataValidation()
* .requireDateBefore(new Date('2025-01-01'))
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validation status of the range
* const status = await fRange.getValidatorStatus();
* console.log(status); // [['valid', 'valid', 'invalid', 'invalid']]
* ```
*/
requireDateBefore(date: Date): FDataValidationBuilder {
this._rule.type = DataValidationType.DATE;
this._rule.formula1 = date.toLocaleDateString();
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.LESS_THAN;
return this;
}
/**
* Set the data validation type to DATE and configure the validation rules to be within a specific date range.
* @param {Date} start - The earliest acceptable date.
* @param {Date} end - The latest acceptable date.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some date values in the range A1:B2
* const fRange = fWorksheet.getRange('A1:B2');
* fRange.setValues([
* ['2024-01-01', '2024-12-31'],
* ['2025-01-01', '2025-12-31']
* ]);
*
* // Create a data validation rule that requires a date between 2024-06-01 and 2025-06-01
* const rule = univerAPI.newDataValidation()
* .requireDateBetween(new Date('2024-06-01'), new Date('2025-06-01'))
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validation status of the range
* const status = await fRange.getValidatorStatus();
* console.log(status); // [['invalid', 'valid', 'valid', 'invalid']]
* ```
*/
requireDateBetween(start: Date, end: Date): FDataValidationBuilder {
this._rule.type = DataValidationType.DATE;
this._rule.formula1 = start.toLocaleDateString();
this._rule.formula2 = end.toLocaleDateString();
this._rule.operator = DataValidationOperator.BETWEEN;
return this;
}
/**
* Set the data validation type to DATE and configure the validation rules to be equal to a specific date.
* @param {Date} date - The sole acceptable date.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some date values in the range A1:B2
* const fRange = fWorksheet.getRange('A1:B2');
* fRange.setValues([
* ['2024-01-01', '2024-12-31'],
* ['2025-01-01', '2025-12-31']
* ]);
*
* // Create a data validation rule that requires a date equal to 2025-01-01
* const rule = univerAPI.newDataValidation()
* .requireDateEqualTo(new Date('2025-01-01'))
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validation status of the cell A2
* const status = await fWorksheet.getRange('A2').getValidatorStatus();
* console.log(status?.[0]?.[0]); // 'valid'
*
* // Get the validation status of the cell B2
* const status2 = await fWorksheet.getRange('B2').getValidatorStatus();
* console.log(status2?.[0]?.[0]); // 'invalid'
* ```
*/
requireDateEqualTo(date: Date): FDataValidationBuilder {
this._rule.type = DataValidationType.DATE;
this._rule.formula1 = date.toLocaleDateString();
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.EQUAL;
return this;
}
/**
* Set the data validation type to DATE and configure the validation rules to be not within a specific date range.
* @param {Date} start - The earliest unacceptable date.
* @param {Date} end - The latest unacceptable date.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some date values in the range A1:B2
* const fRange = fWorksheet.getRange('A1:B2');
* fRange.setValues([
* ['2024-01-01', '2024-12-31'],
* ['2025-01-01', '2025-12-31']
* ]);
*
* // Create a data validation rule that requires a date not between 2024-06-01 and 2025-06-01
* const rule = univerAPI.newDataValidation()
* .requireDateNotBetween(new Date('2024-06-01'), new Date('2025-06-01'))
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validation status of the range
* const status = await fRange.getValidatorStatus();
* console.log(status); // [['valid', 'invalid', 'invalid', 'valid']]
* ```
*/
requireDateNotBetween(start: Date, end: Date): FDataValidationBuilder {
this._rule.type = DataValidationType.DATE;
this._rule.formula1 = start.toLocaleDateString();
this._rule.formula2 = end.toLocaleDateString();
this._rule.operator = DataValidationOperator.NOT_BETWEEN;
return this;
}
/**
* Set the data validation type to DATE and configure the validation rules to be on or after a specific date.
* @param {Date} date - The earliest acceptable date.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some date values in the range A1:B2
* const fRange = fWorksheet.getRange('A1:B2');
* fRange.setValues([
* ['2024-01-01', '2024-12-31'],
* ['2025-01-01', '2025-12-31']
* ]);
*
* // Create a data validation rule that requires a date on or after 2025-01-01
* const rule = univerAPI.newDataValidation()
* .requireDateOnOrAfter(new Date('2025-01-01'))
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validation status of the range
* const status = await fRange.getValidatorStatus();
* console.log(status); // [['invalid', 'invalid', 'valid', 'valid']]
* ```
*/
requireDateOnOrAfter(date: Date): FDataValidationBuilder {
this._rule.type = DataValidationType.DATE;
this._rule.formula1 = date.toLocaleDateString();
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.GREATER_THAN_OR_EQUAL;
return this;
}
/**
* Set the data validation type to DATE and configure the validation rules to be on or before a specific date.
* @param {Date} date - The latest acceptable date.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some date values in the range A1:B2
* const fRange = fWorksheet.getRange('A1:B2');
* fRange.setValues([
* ['2024-01-01', '2024-12-31'],
* ['2025-01-01', '2025-12-31']
* ]);
*
* // Create a data validation rule that requires a date on or before 2025-01-01
* const rule = univerAPI.newDataValidation()
* .requireDateOnOrBefore(new Date('2025-01-01'))
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validation status of the range
* const status = await fRange.getValidatorStatus();
* console.log(status); // [['valid', 'valid', 'valid', 'invalid']]
* ```
*/
requireDateOnOrBefore(date: Date): FDataValidationBuilder {
this._rule.type = DataValidationType.DATE;
this._rule.formula1 = date.toLocaleDateString();
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.LESS_THAN_OR_EQUAL;
return this;
}
/**
* Sets the data validation rule to require that the given formula evaluates to `true`.
* @param {string} formula - The formula string that needs to be satisfied, formula result should be TRUE or FALSE, and references range will relative offset.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some values in the range A1:B2 and C1:D2
* const cell = fWorksheet.getRange('A1:B2');
* cell.setValues([
* [4, 3],
* [2, 1]
* ]);
* const fRange = fWorksheet.getRange('C1:D2');
* fRange.setValues([
* [1, 2],
* [3, 4]
* ]);
*
* // Create a data validation rule that requires the formula '=A1>2' to be satisfied
* const rule = univerAPI.newDataValidation()
* .requireFormulaSatisfied('=A1>2')
* .setOptions({
* showErrorMessage: true,
* error: 'Please enter a value equal to A1'
* })
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validation status of the range
* const status = await fRange.getValidatorStatus();
* console.log(status); // [['valid', 'valid', 'invalid', 'invalid']]
* ```
*/
requireFormulaSatisfied(formula: string): FDataValidationBuilder {
this._rule.type = DataValidationType.CUSTOM;
this._rule.formula1 = formula;
this._rule.formula2 = undefined;
return this;
}
/**
* Sets the data validation rule to require a number that falls between, or is either of, two specified numbers.
* @param {number} start - The lowest acceptable value.
* @param {number} end - The highest acceptable value.
* @param {boolean} [isInteger] - Indicates whether the required number is an integer.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number between 1 and 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a number between 1 and 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireNumberBetween(start: number, end: number, isInteger?: boolean): FDataValidationBuilder {
this._rule.formula1 = `${start}`;
this._rule.formula2 = `${end}`;
this._rule.operator = DataValidationOperator.BETWEEN;
this._rule.type = isInteger ? DataValidationType.WHOLE : DataValidationType.DECIMAL;
return this;
}
/**
* Sets the data validation rule to require a number equal to the given value.
* @param {number} num - The sole acceptable value.
* @param {boolean} [isInteger] - Indicates whether the required number is an integer.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number equal to 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberEqualTo(10)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a number equal to 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireNumberEqualTo(num: number, isInteger?: boolean): FDataValidationBuilder {
this._rule.formula1 = `${num}`;
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.EQUAL;
this._rule.type = isInteger ? DataValidationType.WHOLE : DataValidationType.DECIMAL;
return this;
}
/**
* Sets the data validation rule to require a number greater than the given value.
* @param {number} num - The highest unacceptable value.
* @param {boolean} [isInteger] - Indicates whether the required number is an integer.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number greater than 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberGreaterThan(10)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a number greater than 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireNumberGreaterThan(num: number, isInteger?: boolean): FDataValidationBuilder {
this._rule.formula1 = `${num}`;
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.GREATER_THAN;
this._rule.type = isInteger ? DataValidationType.WHOLE : DataValidationType.DECIMAL;
return this;
}
/**
* Sets the data validation rule to require a number greater than or equal to the given value.
* @param {number} num - The lowest acceptable value.
* @param {boolean} [isInteger] - Indicates whether the required number is an integer.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number greater than 10 or equal to 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberGreaterThanOrEqualTo(10)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a number greater than 10 or equal to 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireNumberGreaterThanOrEqualTo(num: number, isInteger?: boolean): FDataValidationBuilder {
this._rule.formula1 = `${num}`;
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.GREATER_THAN_OR_EQUAL;
this._rule.type = isInteger ? DataValidationType.WHOLE : DataValidationType.DECIMAL;
return this;
}
/**
* Sets the data validation rule to require a number less than the given value.
* @param {number} num - The lowest unacceptable value.
* @param {boolean} [isInteger] - Indicates whether the required number is an integer.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number less than 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberLessThan(10)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a number less than 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireNumberLessThan(num: number, isInteger?: boolean): FDataValidationBuilder {
this._rule.formula1 = `${num}`;
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.LESS_THAN;
this._rule.type = isInteger ? DataValidationType.WHOLE : DataValidationType.DECIMAL;
return this;
}
/**
* Sets the data validation rule to require a number less than or equal to the given value.
* @param {number} num - The highest acceptable value.
* @param {boolean} [isInteger] - Indicates whether the required number is an integer.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number less than 10 or equal to 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberLessThanOrEqualTo(10)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a number less than 10 or equal to 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireNumberLessThanOrEqualTo(num: number, isInteger?: boolean): FDataValidationBuilder {
this._rule.formula1 = `${num}`;
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.LESS_THAN_OR_EQUAL;
this._rule.type = isInteger ? DataValidationType.WHOLE : DataValidationType.DECIMAL;
return this;
}
/**
* Sets the data validation rule to require a number that does not fall between, and is neither of, two specified numbers.
* @param {number} start - The lowest unacceptable value.
* @param {number} end - The highest unacceptable value.
* @param {boolean} [isInteger] - Optional parameter, indicating whether the number to be verified is an integer.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number not between 1 and 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberNotBetween(1, 10)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a number not between 1 and 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireNumberNotBetween(start: number, end: number, isInteger?: boolean): FDataValidationBuilder {
this._rule.formula1 = `${start}`;
this._rule.formula2 = `${end}`;
this._rule.operator = DataValidationOperator.NOT_BETWEEN;
this._rule.type = isInteger ? DataValidationType.WHOLE : DataValidationType.DECIMAL;
return this;
}
/**
* Sets the data validation rule to require a number not equal to the given value.
* @param {number} num - The sole unacceptable value.
* @param {boolean} [isInteger] - Indicates whether the required number is an integer.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number not equal to 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberNotEqualTo(10)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a number not equal to 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireNumberNotEqualTo(num: number, isInteger?: boolean): FDataValidationBuilder {
this._rule.formula1 = `${num}`;
this._rule.formula2 = undefined;
this._rule.operator = DataValidationOperator.NOT_EQUAL;
this._rule.type = isInteger ? DataValidationType.WHOLE : DataValidationType.DECIMAL;
return this;
}
/**
* Sets a data validation rule that requires the user to enter a value from a list of specific values.
* The list can be displayed in a dropdown, and the user can choose multiple values according to the settings.
* @param {string[]} values - An array of acceptable values.
* @param {boolean} [multiple] - Optional parameter indicating whether the user can select multiple values.
* @param {boolean} [showDropdown] - Optional parameter indicating whether to display the list in a dropdown.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires the user to enter a value from the list ['Yes', 'No'] for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireValueInList(['Yes', 'No'])
* .setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a value from the list'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
requireValueInList(values: string[], multiple?: boolean, showDropdown?: boolean): FDataValidationBuilder {
this._rule.type = multiple ? DataValidationType.LIST_MULTIPLE : DataValidationType.LIST;
this._rule.formula1 = serializeListOptions(values);
this._rule.formula2 = undefined;
this._rule.showDropDown = showDropdown ?? true;
return this;
}
/**
* Sets a data validation rule that requires the user to enter a value within a specific range.
* The range is defined by an FRange object, which contains the unit ID, sheet name, and cell range.
* @param {FRange} range - An FRange object representing the range of values that the user can enter.
* @param {boolean} [multiple] - Optional parameter indicating whether the user can select multiple values.
* @param {boolean} [showDropdown] - Optional parameter indicating whether to display the list in a dropdown.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set the values in the range B1:B2
* const fRange = fWorksheet.getRange('B1:B2');
* fRange.setValues([
* ['Yes'],
* ['No']
* ]);
*
* // Create a new data validation rule that requires the user to enter a value from the range B1:B2 for the range A1:A10
* const rule = univerAPI.newDataValidation()
* .requireValueInRange(fRange)
* .setOptions({
* allowBlank: false,
* showErrorMessage: true,
* error: 'Please enter a value from the list'
* })
* .build();
* const cell = fWorksheet.getRange('A1');
* cell.setDataValidation(rule);
* ```
*/
requireValueInRange(range: FRange, multiple?: boolean, showDropdown?: boolean): FDataValidationBuilder {
this._rule.type = multiple ? DataValidationType.LIST_MULTIPLE : DataValidationType.LIST;
this._rule.formula1 = `=${serializeRangeToRefString({
unitId: range.getUnitId(),
sheetName: range.getSheetName(),
range: range.getRange(),
})}`;
this._rule.formula2 = undefined;
this._rule.showDropDown = showDropdown ?? true;
return this;
}
/**
* Sets whether to allow invalid data and configures the error style.
* If invalid data is not allowed, the error style will be set to STOP, indicating that data entry must stop upon encountering an error.
* If invalid data is allowed, the error style will be set to WARNING, indicating that a warning will be displayed when invalid data is entered, but data entry can continue.
* @param {boolean} allowInvalidData - Whether to allow invalid data.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set the data validation for cell A1:B2 to allow invalid data, so A1:B2 will display a warning when invalid data is entered
* const fRange = fWorksheet.getRange('A1:B2');
* const rule = univerAPI.newDataValidation()
* .requireValueInList(['Yes', 'No'])
* .setAllowInvalid(true)
* .build();
* fRange.setDataValidation(rule);
*
* // Set the data validation for cell C1:D2 to not allow invalid data, so C1:D2 will stop data entry when invalid data is entered
* const fRange2 = fWorksheet.getRange('C1:D2');
* const rule2 = univerAPI.newDataValidation()
* .requireValueInList(['Yes', 'No'])
* .setAllowInvalid(false)
* .build();
* fRange2.setDataValidation(rule2);
* ```
*/
setAllowInvalid(allowInvalidData: boolean): FDataValidationBuilder {
this._rule.errorStyle = !allowInvalidData ? DataValidationErrorStyle.STOP : DataValidationErrorStyle.WARNING;
return this;
}
/**
* Sets whether to allow blank values.
* @param {boolean} allowBlank - Whether to allow blank values.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* // Assume current sheet is empty data
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set the data validation for cell A1:B2 to allow blank values
* const fRange = fWorksheet.getRange('A1:B2');
* const rule = univerAPI.newDataValidation()
* .requireValueInList(['Yes', 'No'])
* .setAllowBlank(true)
* .build();
* fRange.setDataValidation(rule);
*
* // Set the data validation for cell C1:D2 to not allow blank values
* const fRange2 = fWorksheet.getRange('C1:D2');
* const rule2 = univerAPI.newDataValidation()
* .requireValueInList(['Yes', 'No'])
* .setAllowBlank(false)
* .build();
* fRange2.setDataValidation(rule2);
* ```
*/
setAllowBlank(allowBlank: boolean): FDataValidationBuilder {
this._rule.allowBlank = allowBlank;
return this;
}
/**
* Sets the options for the data validation rule.
* @param {Partial<IDataValidationRuleOptions>} options - The options to set for the data validation rule.
* @returns {FDataValidationBuilder} The current instance for method chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires the user to enter a value from the list ['Yes', 'No'] for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireValueInList(['Yes', 'No'])
* .setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a value from the list'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
setOptions(options: Partial<IDataValidationRuleOptions>): this {
Object.assign(this._rule, options);
return this;
}
}

View File

@@ -0,0 +1,439 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 {
DataValidationOperator,
DataValidationType,
IDataValidationRule,
IDataValidationRuleOptions,
Injector,
IRange,
Workbook,
Worksheet,
} from '@univerjs/core';
import type {
IRemoveSheetDataValidationCommandParams,
IUpdateSheetDataValidationOptionsCommandParams,
IUpdateSheetDataValidationRangeCommandParams,
IUpdateSheetDataValidationSettingCommandParams,
} from '@univerjs/sheets-data-validation';
import { DataValidationErrorStyle, ICommandService, IUniverInstanceService } from '@univerjs/core';
import { DataValidationModel, getRuleOptions } from '@univerjs/data-validation';
import {
RemoveSheetDataValidationCommand,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
} from '@univerjs/sheets-data-validation';
import { FRange } from '@univerjs/sheets/facade';
import { FDataValidationBuilder } from './f-data-validation-builder';
/**
* @hideconstructor
*/
export class FDataValidation {
rule: IDataValidationRule;
private _worksheet: Worksheet | undefined;
private _injector: Injector | undefined;
constructor(rule: IDataValidationRule, worksheet?: Worksheet, _injector?: Injector) {
this._injector = _injector;
this.rule = rule;
this._worksheet = worksheet;
}
/**
* Gets whether invalid data is allowed based on the error style value
* @returns {boolean} true if invalid data is allowed, false otherwise
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const rules = fWorksheet.getDataValidations();
* rules.forEach((rule) => {
* console.log(rule, rule.getAllowInvalid());
* });
* ```
*/
getAllowInvalid(): boolean {
return this.rule.errorStyle !== DataValidationErrorStyle.STOP;
};
/**
* Gets the data validation type of the rule
* @returns {DataValidationType | string} The data validation type
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const rules = fWorksheet.getDataValidations();
* rules.forEach((rule) => {
* console.log(rule, rule.getCriteriaType());
* });
* ```
*/
getCriteriaType(): DataValidationType | string {
return this.rule.type;
};
/**
* Gets the values used for criteria evaluation
* @returns {[string | undefined, string | undefined, string | undefined]} An array containing the operator, formula1, and formula2 values
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const rules = fWorksheet.getDataValidations();
* rules.forEach((rule) => {
* console.log(rule);
* const criteriaValues = rule.getCriteriaValues();
* const [operator, formula1, formula2] = criteriaValues;
* console.log(operator, formula1, formula2);
* });
* ```
*/
getCriteriaValues(): [string | undefined, string | undefined, string | undefined] {
return [this.rule.operator, this.rule.formula1, this.rule.formula2];
}
/**
* Gets the help text information, which is used to provide users with guidance and support
* @returns {string | undefined} Returns the help text information. If there is no error message, it returns an undefined value
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a number between 1 and 10'
* })
* .build();
* fRange.setDataValidation(rule);
* console.log(fRange.getDataValidation().getHelpText()); // 'Please enter a number between 1 and 10'
* ```
*/
getHelpText(): string | undefined {
return this.rule.error;
};
/**
* Creates a new instance of FDataValidationBuilder using the current rule object
* @returns {FDataValidationBuilder} A new FDataValidationBuilder instance with the same rule configuration
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a number between 1 and 10'
* })
* .build();
* fRange.setDataValidation(rule);
*
* const builder = fRange.getDataValidation().copy();
* const newRule = builder
* .requireNumberBetween(1, 5)
* .setOptions({
* error: 'Please enter a number between 1 and 5'
* })
* .build();
* fRange.setDataValidation(newRule);
* ```
*/
copy(): FDataValidationBuilder {
return new FDataValidationBuilder(this.rule);
}
/**
* Gets whether the data validation rule is applied to the worksheet
* @returns {boolean} true if the rule is applied, false otherwise
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const rules = fWorksheet.getDataValidations();
* rules.forEach((rule) => {
* console.log(rule, rule.getApplied());
* });
*
* const fRange = fWorksheet.getRange('A1:B10');
* console.log(fRange.getDataValidation()?.getApplied());
* ```
*/
getApplied(): boolean {
if (!this._worksheet) {
return false;
}
const dataValidationModel = this._injector!.get(DataValidationModel);
const currentRule = dataValidationModel.getRuleById(this._worksheet.getUnitId(), this._worksheet.getSheetId(), this.rule.uid);
if (currentRule && currentRule.ranges.length) {
return true;
}
return false;
}
/**
* Gets the ranges to which the data validation rule is applied
* @returns {FRange[]} An array of FRange objects representing the ranges to which the data validation rule is applied
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const rules = fWorksheet.getDataValidations();
* rules.forEach((rule) => {
* console.log(rule);
* const ranges = rule.getRanges();
* ranges.forEach((range) => {
* console.log(range.getA1Notation());
* });
* });
* ```
*/
getRanges(): FRange[] {
if (!this.getApplied()) {
return [];
}
const workbook = this._injector!.get(IUniverInstanceService).getUnit<Workbook>(this._worksheet!.getUnitId())!;
return this.rule.ranges.map((range: IRange) => this._injector!.createInstance(FRange, workbook, this._worksheet!, range));
}
/**
* Gets the unit ID of the workbook
* @returns {string | undefined} The unit ID of the workbook
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1:B10');
* console.log(fRange.getDataValidation().getUnitId());
* ```
*/
getUnitId(): string | undefined {
return this._worksheet?.getUnitId();
}
/**
* Gets the sheet ID of the worksheet
* @returns {string | undefined} The sheet ID of the worksheet
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1:B10');
* console.log(fRange.getDataValidation().getSheetId());
* ```
*/
getSheetId(): string | undefined {
return this._worksheet?.getSheetId();
}
/**
* Set Criteria for the data validation rule
* @param {DataValidationType} type - The type of data validation criteria
* @param {[DataValidationOperator, string, string]} values - An array containing the operator, formula1, and formula2 values
* @param {boolean} [allowBlank] - Whether to allow blank values
* @returns {FDataValidation} The current instance for method chaining
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number equal to 20 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberEqualTo(20)
* .build();
* fRange.setDataValidation(rule);
*
* // Change the rule criteria to require a number between 1 and 10
* fRange.getDataValidation().setCriteria(
* univerAPI.Enum.DataValidationType.DECIMAL,
* [univerAPI.Enum.DataValidationOperator.BETWEEN, '1', '10']
* );
* ```
*/
setCriteria(type: DataValidationType, values: [DataValidationOperator, string, string], allowBlank = true): FDataValidation {
if (this.getApplied()) {
const commandService = this._injector!.get(ICommandService);
const res = commandService.syncExecuteCommand(UpdateSheetDataValidationSettingCommand.id, {
unitId: this.getUnitId(),
subUnitId: this.getSheetId(),
ruleId: this.rule.uid,
setting: {
operator: values[0],
formula1: values[1],
formula2: values[2],
type: this.rule.type,
allowBlank,
},
} as IUpdateSheetDataValidationSettingCommandParams);
if (!res) {
throw new Error('setCriteria failed');
}
}
this.rule.operator = values[0];
this.rule.formula1 = values[1];
this.rule.formula2 = values[2];
this.rule.type = type;
this.rule.allowBlank = allowBlank;
return this;
}
/**
* Set the options for the data validation rule
* @param {Partial<IDataValidationRuleOptions>} options - The options to set for the data validation rule
* @returns {FDataValidation} The current instance for method chaining
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number equal to 20 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberEqualTo(20)
* .build();
* fRange.setDataValidation(rule);
*
* // Supplement the rule with additional options
* fRange.getDataValidation().setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a valid value'
* });
* ```
*/
setOptions(options: Partial<IDataValidationRuleOptions>): FDataValidation {
if (this.getApplied()) {
const commandService = this._injector!.get(ICommandService);
const res = commandService.syncExecuteCommand(UpdateSheetDataValidationOptionsCommand.id, {
unitId: this.getUnitId(),
subUnitId: this.getSheetId(),
ruleId: this.rule.uid,
options: {
...getRuleOptions(this.rule),
...options,
},
} as IUpdateSheetDataValidationOptionsCommandParams);
if (!res) {
throw new Error('setOptions failed');
}
}
Object.assign(this.rule, options);
return this;
}
/**
* Set the ranges to the data validation rule
* @param {FRange[]} ranges - New ranges array
* @returns {FDataValidation} The current instance for method chaining
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number equal to 20 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberEqualTo(20)
* .build();
* fRange.setDataValidation(rule);
*
* // Change the range to C1:D10
* const newRuleRange = fWorksheet.getRange('C1:D10');
* fRange.getDataValidation().setRanges([newRuleRange]);
* ```
*/
setRanges(ranges: FRange[]): FDataValidation {
if (this.getApplied()) {
const commandService = this._injector!.get(ICommandService);
const res = commandService.syncExecuteCommand(UpdateSheetDataValidationRangeCommand.id, {
unitId: this.getUnitId(),
subUnitId: this.getSheetId(),
ruleId: this.rule.uid,
ranges: ranges.map((range) => range.getRange()),
} as IUpdateSheetDataValidationRangeCommandParams);
if (!res) {
throw new Error('setRanges failed');
}
}
this.rule.ranges = ranges.map((range) => range.getRange());
return this;
}
/**
* Delete the data validation rule from the worksheet
* @returns {boolean} true if the rule is deleted successfully, false otherwise
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number equal to 20 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberEqualTo(20)
* .build();
* fRange.setDataValidation(rule);
*
* // Delete the data validation rule
* fRange.getDataValidation().delete();
* ```
*/
delete(): boolean {
if (!this.getApplied()) {
return false;
}
const commandService = this._injector!.get(ICommandService);
return commandService.syncExecuteCommand(RemoveSheetDataValidationCommand.id, {
unitId: this.getUnitId(),
subUnitId: this.getSheetId(),
ruleId: this.rule.uid,
} as IRemoveSheetDataValidationCommandParams);
}
}

View File

@@ -0,0 +1,378 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationStatus, IDataValidationRule, IDataValidationRuleBase, IDataValidationRuleOptions, IRange, ISheetDataValidationRule } from '@univerjs/core';
import type { IEventBase } from '@univerjs/core/facade';
import type { DataValidationChangeType, IRuleChange } from '@univerjs/data-validation';
import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade';
import type { FDataValidation } from './f-data-validation';
import { FEventName } from '@univerjs/core/facade';
/**
* Event interface triggered when a data validation rule is changed
* @interface ISheetDataValidationChangedEventParams
* @augments {IEventBase}
*/
export interface ISheetDataValidationChangedEventParams extends IEventBase {
/** The source of the rule change */
origin: IRuleChange;
/** The worksheet containing the validation rule */
worksheet: FWorksheet;
/** The workbook instance */
workbook: FWorkbook;
/** Type of change made to the validation rule */
changeType: DataValidationChangeType;
/** The previous validation rule, if it exists */
oldRule?: IDataValidationRule;
/** The new or modified validation rule */
rule: FDataValidation;
}
/**
* Event interface triggered when a data validation status changes
* @interface ISheetDataValidatorStatusChangedEventParams
* @augments {IEventBase}
*/
export interface ISheetDataValidatorStatusChangedEventParams extends IEventBase {
/** The worksheet containing the validation */
worksheet: FWorksheet;
/** The workbook instance */
workbook: FWorkbook;
/** Row index of the validated cell */
row: number;
/** Column index of the validated cell */
column: number;
/** Current validation status */
status: DataValidationStatus;
/** The validation rule that was checked */
rule: FDataValidation;
}
/**
* Event interface triggered before adding a new data validation rule
* @interface IBeforeSheetDataValidationAddEventParams
* @augments {IEventBase}
*/
export interface IBeforeSheetDataValidationAddEventParams extends IEventBase {
/** The worksheet to add the validation to */
worksheet: FWorksheet;
/** The workbook instance */
workbook: FWorkbook;
/** The validation rule to be added */
rule: ISheetDataValidationRule;
}
/**
* Event interface triggered before deleting a data validation rule
* @interface IBeforeSheetDataValidationDeleteEventParams
* @augments {IEventBase}
*/
export interface IBeforeSheetDataValidationDeleteEventParams extends IEventBase {
/** The worksheet containing the validation */
worksheet: FWorksheet;
/** The workbook instance */
workbook: FWorkbook;
/** Unique identifier of the rule to be deleted */
ruleId: string;
/** The validation rule to be deleted */
rule: FDataValidation;
}
/**
* Event interface triggered before updating a data validation rule's criteria
* @interface IBeforeSheetDataValidationCriteriaUpdateEventParams
* @augments {IEventBase}
*/
export interface IBeforeSheetDataValidationCriteriaUpdateEventParams extends IEventBase {
/** The worksheet containing the validation */
worksheet: FWorksheet;
/** The workbook instance */
workbook: FWorkbook;
/** Unique identifier of the rule to be updated */
ruleId: string;
/** The current validation rule */
rule: FDataValidation;
/** The new criteria to be applied */
newCriteria: IDataValidationRuleBase;
}
/**
* Event interface triggered before updating a data validation rule's ranges
* @interface IBeforeSheetDataValidationRangeUpdateEventParams
* @augments {IEventBase}
*/
export interface IBeforeSheetDataValidationRangeUpdateEventParams extends IEventBase {
/** The worksheet containing the validation */
worksheet: FWorksheet;
/** The workbook instance */
workbook: FWorkbook;
/** Unique identifier of the rule to be updated */
ruleId: string;
/** The current validation rule */
rule: FDataValidation;
/** The new ranges to be applied */
newRanges: IRange[];
}
/**
* Event interface triggered before updating a data validation rule's options
* @interface IBeforeSheetDataValidationOptionsUpdateEventParams
* @augments {IEventBase}
*/
export interface IBeforeSheetDataValidationOptionsUpdateEventParams extends IEventBase {
/** The worksheet containing the validation */
worksheet: FWorksheet;
/** The workbook instance */
workbook: FWorkbook;
/** Unique identifier of the rule to be updated */
ruleId: string;
/** The current validation rule */
rule: FDataValidation;
/** The new options to be applied */
newOptions: IDataValidationRuleOptions;
}
/**
* Event interface triggered before deleting all data validation rules
* @interface IBeforeSheetDataValidationDeleteAllEventParams
* @augments {IEventBase}
*/
export interface IBeforeSheetDataValidationDeleteAllEventParams extends IEventBase {
/** The worksheet containing the validations */
worksheet: FWorksheet;
/** The workbook instance */
workbook: FWorkbook;
/** Array of all validation rules to be deleted */
rules: FDataValidation[];
}
/**
* @ignore
*/
export interface IDataValidationEventParamConfig {
SheetDataValidationChanged: ISheetDataValidationChangedEventParams;
SheetDataValidatorStatusChanged: ISheetDataValidatorStatusChangedEventParams;
BeforeSheetDataValidationAdd: IBeforeSheetDataValidationAddEventParams;
BeforeSheetDataValidationDelete: IBeforeSheetDataValidationDeleteEventParams;
BeforeSheetDataValidationDeleteAll: IBeforeSheetDataValidationDeleteAllEventParams;
BeforeSheetDataValidationCriteriaUpdate: IBeforeSheetDataValidationCriteriaUpdateEventParams;
BeforeSheetDataValidationRangeUpdate: IBeforeSheetDataValidationRangeUpdateEventParams;
BeforeSheetDataValidationOptionsUpdate: IBeforeSheetDataValidationOptionsUpdateEventParams;
}
/**
* @ignore
*/
interface IFSheetsDataValidationEventNameMixin {
/**
* Event fired when a rule is added, deleted, or modified
* @see {@link ISheetDataValidationChangedEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetDataValidationChanged, (params) => {
* const { origin, worksheet, workbook, changeType, oldRule, rule } = params;
* console.log(params);
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly SheetDataValidationChanged: 'SheetDataValidationChanged';
/**
* Event fired when a cell validator status is changed
* @see {@link ISheetDataValidatorStatusChangedEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetDataValidatorStatusChanged, (params) => {
* const { worksheet, workbook, row, column, status, rule } = params;
* console.log(params);
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly SheetDataValidatorStatusChanged: 'SheetDataValidatorStatusChanged';
/**
* Event fired before a rule is added
* @see {@link IBeforeSheetDataValidationAddEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationAdd, (params) => {
* const { worksheet, workbook, rule } = params;
* console.log(params);
*
* // Cancel the data validation rule addition operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetDataValidationAdd: 'BeforeSheetDataValidationAdd';
/**
* Event fired before a rule is deleted
* @see {@link IBeforeSheetDataValidationDeleteEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationDelete, (params) => {
* const { worksheet, workbook, ruleId, rule } = params;
* console.log(params);
*
* // Cancel the data validation rule deletion operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetDataValidationDelete: 'BeforeSheetDataValidationDelete';
/**
* Event fired before all rules are deleted
* @see {@link IBeforeSheetDataValidationDeleteAllEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationDeleteAll, (params) => {
* const { worksheet, workbook, rules } = params;
* console.log(params);
*
* // Cancel the data validation rule deletion operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetDataValidationDeleteAll: 'BeforeSheetDataValidationDeleteAll';
/**
* Event fired before the criteria of a rule are updated
* @see {@link IBeforeSheetDataValidationCriteriaUpdateEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationCriteriaUpdate, (params) => {
* const { worksheet, workbook, ruleId, rule, newCriteria } = params;
* console.log(params);
*
* // Cancel the data validation rule criteria update operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetDataValidationCriteriaUpdate: 'BeforeSheetDataValidationCriteriaUpdate';
/**
* Event fired before the range of a rule is updated
* @see {@link IBeforeSheetDataValidationRangeUpdateEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationRangeUpdate, (params) => {
* const { worksheet, workbook, ruleId, rule, newRanges } = params;
* console.log(params);
*
* // Cancel the data validation rule range update operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetDataValidationRangeUpdate: 'BeforeSheetDataValidationRangeUpdate';
/**
* Event fired before the options of a rule are updated
* @see {@link IBeforeSheetDataValidationOptionsUpdateEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetDataValidationOptionsUpdate, (params) => {
* const { worksheet, workbook, ruleId, rule, newOptions } = params;
* console.log(params);
*
* // Cancel the data validation rule options update operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetDataValidationOptionsUpdate: 'BeforeSheetDataValidationOptionsUpdate';
}
/**
* @ignore
*/
export class FSheetsDataValidationEventNameMixin extends FEventName implements IFSheetsDataValidationEventNameMixin {
override get SheetDataValidationChanged(): 'SheetDataValidationChanged' {
return 'SheetDataValidationChanged';
}
override get SheetDataValidatorStatusChanged(): 'SheetDataValidatorStatusChanged' {
return 'SheetDataValidatorStatusChanged';
}
override get BeforeSheetDataValidationAdd(): 'BeforeSheetDataValidationAdd' {
return 'BeforeSheetDataValidationAdd';
}
override get BeforeSheetDataValidationDelete(): 'BeforeSheetDataValidationDelete' {
return 'BeforeSheetDataValidationDelete';
}
override get BeforeSheetDataValidationDeleteAll(): 'BeforeSheetDataValidationDeleteAll' {
return 'BeforeSheetDataValidationDeleteAll';
}
override get BeforeSheetDataValidationCriteriaUpdate(): 'BeforeSheetDataValidationCriteriaUpdate' {
return 'BeforeSheetDataValidationCriteriaUpdate';
}
override get BeforeSheetDataValidationRangeUpdate(): 'BeforeSheetDataValidationRangeUpdate' {
return 'BeforeSheetDataValidationRangeUpdate';
}
override get BeforeSheetDataValidationOptionsUpdate(): 'BeforeSheetDataValidationOptionsUpdate' {
return 'BeforeSheetDataValidationOptionsUpdate';
}
}
/**
* @ignore
*/
export interface ISheetsDataValidationEventParamConfig {
SheetDataValidationChanged: ISheetDataValidationChangedEventParams;
SheetDataValidatorStatusChanged: ISheetDataValidatorStatusChangedEventParams;
BeforeSheetDataValidationAdd: IBeforeSheetDataValidationAddEventParams;
BeforeSheetDataValidationDelete: IBeforeSheetDataValidationDeleteEventParams;
BeforeSheetDataValidationDeleteAll: IBeforeSheetDataValidationDeleteAllEventParams;
BeforeSheetDataValidationCriteriaUpdate: IBeforeSheetDataValidationCriteriaUpdateEventParams;
BeforeSheetDataValidationRangeUpdate: IBeforeSheetDataValidationRangeUpdateEventParams;
BeforeSheetDataValidationOptionsUpdate: IBeforeSheetDataValidationOptionsUpdateEventParams;
}
FEventName.extend(FSheetsDataValidationEventNameMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FEventName extends IFSheetsDataValidationEventNameMixin {
}
interface IEventParamConfig extends ISheetsDataValidationEventParamConfig {
}
}

View File

@@ -0,0 +1,309 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IDataValidationRule, IRange, Nullable } from '@univerjs/core';
import type { IAddSheetDataValidationCommandParams, IClearRangeDataValidationCommandParams } from '@univerjs/sheets-data-validation';
import type { IDataValidationError } from './f-workbook';
import { DataValidationStatus } from '@univerjs/core';
import { AddSheetDataValidationCommand, ClearRangeDataValidationCommand, SheetDataValidationModel, SheetsDataValidationValidatorService } from '@univerjs/sheets-data-validation';
import { FRange } from '@univerjs/sheets/facade';
import { FDataValidation } from './f-data-validation';
/**
* @ignore
*/
export interface IFRangeSheetsDataValidationMixin {
/**
* Set a data validation rule to current range. if rule is null, clear data validation rule.
* @param {Nullable<FDataValidation>} rule data validation rule, built by `univerAPI.newDataValidation()`
* @returns current range
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a data validation rule that requires a number between 1 and 10 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a number between 1 and 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
setDataValidation(rule: Nullable<FDataValidation>): FRange;
/**
* Get first data validation rule in current range.
* @returns {Nullable<FDataValidation>} data validation rule
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a data validation rule that requires a number equal to 20 for the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberEqualTo(20)
* .build();
* fRange.setDataValidation(rule);
* console.log(fRange.getDataValidation().getCriteriaValues());
*
* // Change the rule criteria to require a number between 1 and 10
* fRange.getDataValidation().setCriteria(
* univerAPI.Enum.DataValidationType.DECIMAL,
* [univerAPI.Enum.DataValidationOperator.BETWEEN, '1', '10']
* );
*
* // Print the new rule criteria values
* console.log(fRange.getDataValidation().getCriteriaValues());
* ```
*/
getDataValidation(): Nullable<FDataValidation>;
/**
* Get all data validation rules in current range.
* @returns {FDataValidation[]} all data validation rules
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a data validation rule that requires a number equal to 20 for the range A1:B10
* const fRange1 = fWorksheet.getRange('A1:B10');
* const rule1 = univerAPI.newDataValidation()
* .requireNumberEqualTo(20)
* .build();
* fRange1.setDataValidation(rule1);
*
* // Create a data validation rule that requires a number between 1 and 10 for the range C1:D10
* const fRange2 = fWorksheet.getRange('C1:D10');
* const rule2 = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .build();
* fRange2.setDataValidation(rule2);
*
* // Get all data validation rules in the range A1:D10
* const range = fWorksheet.getRange('A1:D10');
* const rules = range.getDataValidations();
* console.log(rules.length); // 2
* ```
*/
getDataValidations(): FDataValidation[];
/**
* Get data validation validator status for current range.
* @returns {Promise<DataValidationStatus[][]>} matrix of validator status
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Set some values in the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* fRange.setValues([
* [1, 2],
* [3, 4],
* [5, 6],
* [7, 8],
* [9, 10],
* [11, 12],
* [13, 14],
* [15, 16],
* [17, 18],
* [19, 20]
* ]);
*
* // Create a data validation rule that requires a number between 1 and 10 for the range A1:B10
* const rule = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .build();
* fRange.setDataValidation(rule);
*
* // Get the validator status for the cell B2
* const status = await fWorksheet.getRange('B2').getValidatorStatus();
* console.log(status?.[0]?.[0]); // 'valid'
*
* // Get the validator status for the cell B10
* const status2 = await fWorksheet.getRange('B10').getValidatorStatus();
* console.log(status2?.[0]?.[0]); // 'invalid'
* ```
*/
getValidatorStatus(): Promise<DataValidationStatus[][]>;
/**
* Get data validation errors for a specific range in current worksheet.
* @returns {Promise<IDataValidationError[]>} A promise that resolves to an array of validation errors in the specified range.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1:B10');
* const errors = await fRange.getDataValidationErrorAsync();
*
* console.log(errors);
* ```
*/
getDataValidationErrorAsync(): Promise<IDataValidationError[]>;
}
/**
* @ignore
*/
export class FRangeSheetsDataValidationMixin extends FRange implements IFRangeSheetsDataValidationMixin {
override setDataValidation(rule: Nullable<FDataValidation>): FRange {
if (!rule) {
this._commandService.syncExecuteCommand(ClearRangeDataValidationCommand.id, {
unitId: this._workbook.getUnitId(),
subUnitId: this._worksheet.getSheetId(),
ranges: [this._range],
} as IClearRangeDataValidationCommandParams);
return this;
}
const params: IAddSheetDataValidationCommandParams = {
unitId: this._workbook.getUnitId(),
subUnitId: this._worksheet.getSheetId(),
rule: {
...rule.rule,
ranges: [this._range],
},
};
this._commandService.syncExecuteCommand(AddSheetDataValidationCommand.id, params);
return this;
}
override getDataValidation(): Nullable<FDataValidation> {
const validatorService = this._injector.get(SheetsDataValidationValidatorService);
const rule = validatorService.getDataValidation(
this._workbook.getUnitId(),
this._worksheet.getSheetId(),
[this._range]
);
if (rule) {
return new FDataValidation(rule, this._worksheet, this._injector);
}
return rule;
}
override getDataValidations(): FDataValidation[] {
const validatorService = this._injector.get(SheetsDataValidationValidatorService);
return validatorService.getDataValidations(
this._workbook.getUnitId(),
this._worksheet.getSheetId(),
[this._range]
).map((rule) => new FDataValidation(rule, this._worksheet, this._injector));
}
override async getValidatorStatus(): Promise<DataValidationStatus[][]> {
const validatorService = this._injector.get(SheetsDataValidationValidatorService);
return validatorService.validatorRanges(
this._workbook.getUnitId(),
this._worksheet.getSheetId(),
[this._range]
);
}
override async getDataValidationErrorAsync(): Promise<IDataValidationError[]> {
const unitId = this._workbook.getUnitId();
const sheetId = this._worksheet.getSheetId();
return this._collectValidationErrorsForRange(unitId, sheetId, [this._range]);
}
private async _collectValidationErrorsForRange(unitId: string, sheetId: string, ranges: IRange[]): Promise<IDataValidationError[]> {
if (!ranges.length) {
return [];
}
const validatorService = this._injector.get(SheetsDataValidationValidatorService);
const worksheet = this._worksheet;
const sheetName = worksheet.getName();
const errors: IDataValidationError[] = [];
for (const range of ranges) {
const promises: Promise<void>[] = [];
for (let row = range.startRow; row <= range.endRow; row++) {
for (let col = range.startColumn; col <= range.endColumn; col++) {
promises.push((async (): Promise<void> => {
try {
const status = await validatorService.validatorCell(unitId, sheetId, row, col);
// Only collect errors (non-VALID status)
if (status !== DataValidationStatus.VALID) {
const dataValidationModel = this._injector.get(SheetDataValidationModel);
const rule = dataValidationModel.getRuleByLocation(unitId, sheetId, row, col);
if (rule) {
const cellValue = worksheet.getCell(row, col)?.v || null;
const error = this._createDataValidationError(
sheetName,
row,
col,
rule,
cellValue
);
errors.push(error);
}
}
} catch (e) {
// Skip cells that can't be validated
console.warn(`Failed to validate cell [${row}, ${col}]:`, e);
}
})());
}
}
await Promise.all(promises);
}
return errors;
}
private _createDataValidationError(
sheetName: string,
row: number,
column: number,
rule: IDataValidationRule,
inputValue: string | number | boolean | null
): IDataValidationError {
return {
sheetName,
row,
column,
ruleId: rule.uid,
inputValue,
rule,
};
}
}
FRange.extend(FRangeSheetsDataValidationMixin);
declare module '@univerjs/sheets/facade' {
interface FRange extends IFRangeSheetsDataValidationMixin { }
}

View File

@@ -0,0 +1,334 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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, Injector } from '@univerjs/core';
import type {
IAddSheetDataValidationCommandParams,
IRemoveSheetAllDataValidationCommandParams,
IRemoveSheetDataValidationCommandParams,
IUpdateSheetDataValidationOptionsCommandParams,
IUpdateSheetDataValidationRangeCommandParams,
IUpdateSheetDataValidationSettingCommandParams,
} from '@univerjs/sheets-data-validation';
import type {
IBeforeSheetDataValidationAddEventParams,
IBeforeSheetDataValidationCriteriaUpdateEventParams,
IBeforeSheetDataValidationDeleteAllEventParams,
IBeforeSheetDataValidationDeleteEventParams,
IBeforeSheetDataValidationOptionsUpdateEventParams,
IBeforeSheetDataValidationRangeUpdateEventParams,
ISheetDataValidationChangedEventParams,
ISheetDataValidatorStatusChangedEventParams,
} from './f-event';
import { CanceledError, ICommandService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import {
AddSheetDataValidationCommand,
RemoveSheetAllDataValidationCommand,
RemoveSheetDataValidationCommand,
SheetDataValidationModel,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
} from '@univerjs/sheets-data-validation';
import { FDataValidation } from './f-data-validation';
import { FDataValidationBuilder } from './f-data-validation-builder';
/**
* @ignore
*/
export interface IFUniverSheetsDataValidationMixin {
/**
* Creates a new instance of FDataValidationBuilder
* @returns {FDataValidationBuilder} A new instance of the FDataValidationBuilder class
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a new data validation rule that requires a number between 1 and 10 fot the range A1:B10
* const fRange = fWorksheet.getRange('A1:B10');
* const rule = univerAPI.newDataValidation()
* .requireNumberBetween(1, 10)
* .setOptions({
* allowBlank: true,
* showErrorMessage: true,
* error: 'Please enter a number between 1 and 10'
* })
* .build();
* fRange.setDataValidation(rule);
* ```
*/
newDataValidation(): FDataValidationBuilder;
}
export class FUniverSheetsDataValidationMixin extends FUniver implements IFUniverSheetsDataValidationMixin {
override newDataValidation(): FDataValidationBuilder {
return new FDataValidationBuilder();
}
/**
* @ignore
*/
// eslint-disable-next-line max-lines-per-function
override _initialize(injector: Injector): void {
const commandService = injector.get(ICommandService);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetDataValidationChanged,
() => {
if (!injector.has(SheetDataValidationModel)) return { dispose: () => {} } as IDisposable;
const sheetDataValidationModel = injector.get(SheetDataValidationModel);
return sheetDataValidationModel.ruleChange$.subscribe((ruleChange) => {
const { unitId, subUnitId, rule, oldRule, type } = ruleChange;
const target = this.getSheetCommandTarget({ unitId, subUnitId });
if (!target) return;
const { workbook, worksheet } = target;
const fRule = new FDataValidation(rule, worksheet.getSheet(), this._injector);
const eventParams: ISheetDataValidationChangedEventParams = {
origin: ruleChange,
worksheet,
workbook,
changeType: type,
oldRule,
rule: fRule,
};
this.fireEvent(this.Event.SheetDataValidationChanged, eventParams);
});
}
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetDataValidatorStatusChanged,
() => {
if (!injector.has(SheetDataValidationModel)) return { dispose: () => {} } as IDisposable;
const sheetDataValidationModel = injector.get(SheetDataValidationModel);
return sheetDataValidationModel.validStatusChange$.subscribe((statusChange) => {
const { unitId, subUnitId, ruleId, status, row, col } = statusChange;
const target = this.getSheetCommandTarget({ unitId, subUnitId });
if (!target) return;
const { workbook, worksheet } = target;
const rule = worksheet.getDataValidation(ruleId);
if (!rule) return;
const eventParams: ISheetDataValidatorStatusChangedEventParams = {
workbook,
worksheet,
row,
column: col,
rule,
status,
};
this.fireEvent(this.Event.SheetDataValidatorStatusChanged, eventParams);
});
}
)
);
// Register handlers for before command events
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetDataValidationAdd,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id === AddSheetDataValidationCommand.id) {
const params = commandInfo.params as IAddSheetDataValidationCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const { rule } = params;
const eventParams: IBeforeSheetDataValidationAddEventParams = {
worksheet,
workbook,
rule,
};
this.fireEvent(this.Event.BeforeSheetDataValidationAdd, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetDataValidationCriteriaUpdate,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id === UpdateSheetDataValidationSettingCommand.id) {
const params = commandInfo.params as IUpdateSheetDataValidationSettingCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const { ruleId, setting: newCriteria } = params;
const rule = worksheet.getDataValidation(ruleId);
if (!rule) return;
const eventParams: IBeforeSheetDataValidationCriteriaUpdateEventParams = {
worksheet,
workbook,
rule,
ruleId,
newCriteria,
};
this.fireEvent(this.Event.BeforeSheetDataValidationCriteriaUpdate, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetDataValidationRangeUpdate,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id === UpdateSheetDataValidationRangeCommand.id) {
const params = commandInfo.params as IUpdateSheetDataValidationRangeCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const { ruleId, ranges: newRanges } = params;
const rule = worksheet.getDataValidation(ruleId);
if (!rule) return;
const eventParams: IBeforeSheetDataValidationRangeUpdateEventParams = {
worksheet,
workbook,
rule,
ruleId,
newRanges,
};
this.fireEvent(this.Event.BeforeSheetDataValidationRangeUpdate, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetDataValidationOptionsUpdate,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id === UpdateSheetDataValidationOptionsCommand.id) {
const params = commandInfo.params as IUpdateSheetDataValidationOptionsCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const { ruleId, options: newOptions } = params;
const rule = worksheet.getDataValidation(ruleId);
if (!rule) return;
const eventParams: IBeforeSheetDataValidationOptionsUpdateEventParams = {
worksheet,
workbook,
rule,
ruleId,
newOptions,
};
this.fireEvent(this.Event.BeforeSheetDataValidationOptionsUpdate, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetDataValidationDelete,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id === RemoveSheetDataValidationCommand.id) {
const params = commandInfo.params as IRemoveSheetDataValidationCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const { ruleId } = params;
const rule = worksheet.getDataValidation(ruleId);
if (!rule) return;
const eventParams: IBeforeSheetDataValidationDeleteEventParams = {
worksheet,
workbook,
rule,
ruleId,
};
this.fireEvent(this.Event.BeforeSheetDataValidationDelete, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetDataValidationDeleteAll,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id === RemoveSheetAllDataValidationCommand.id) {
const params = commandInfo.params as IRemoveSheetAllDataValidationCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const rules = worksheet.getDataValidations();
const eventParams: IBeforeSheetDataValidationDeleteAllEventParams = {
worksheet,
workbook,
rules,
};
this.fireEvent(this.Event.BeforeSheetDataValidationDeleteAll, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
}
})
)
);
}
}
FUniver.extend(FUniverSheetsDataValidationMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FUniver extends IFUniverSheetsDataValidationMixin {}
}

View File

@@ -0,0 +1,186 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IDataValidationRule, IRange, Nullable, ObjectMatrix } from '@univerjs/core';
import { DataValidationStatus } from '@univerjs/core';
import { SheetDataValidationModel, SheetsDataValidationValidatorService } from '@univerjs/sheets-data-validation';
import { FWorkbook } from '@univerjs/sheets/facade';
export interface IDataValidationError {
sheetName: string;
/** The row of the cell that triggered the error */
row: number;
column: number;
/** The ID of the rule that triggered the error */
ruleId: string;
/** The input value that triggered the error */
inputValue: string | number | boolean | null;
/** The rule content snapshot (optional, to avoid tracing back to the rule after modification) */
rule?: IDataValidationRule;
}
/**
* @ignore
*/
export interface IFWorkbookSheetsDataValidationMixin {
/**
* Get data validation validator status for current workbook.
* @returns {Promise<Record<string, ObjectMatrix<Nullable<DataValidationStatus>>>>} A promise resolving to an object keyed by sheet ID, with a validation-status matrix for each sheet.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const status = await fWorkbook.getValidatorStatus();
* console.log(status);
* ```
*/
getValidatorStatus(): Promise<Record<string, ObjectMatrix<Nullable<DataValidationStatus>>>>;
/**
* Get all data validation errors for current workbook.
* @returns {Promise<IDataValidationError[]>} A promise that resolves to an array of validation errors.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const errors = await fWorkbook.getAllDataValidationErrorAsync();
* console.log(errors);
* ```
*/
getAllDataValidationErrorAsync(): Promise<IDataValidationError[]>;
}
/**
* @ignore
*/
export class FWorkbookSheetsDataValidationMixin extends FWorkbook implements IFWorkbookSheetsDataValidationMixin {
declare _dataValidationModel: SheetDataValidationModel;
override _initialize(): void {
Object.defineProperty(this, '_dataValidationModel', {
get() {
return this._injector.get(SheetDataValidationModel);
},
});
}
override getValidatorStatus(): Promise<Record<string, ObjectMatrix<Nullable<DataValidationStatus>>>> {
const validatorService = this._injector.get(SheetsDataValidationValidatorService);
return validatorService.validatorWorkbook(this._workbook.getUnitId());
}
override async getAllDataValidationErrorAsync(): Promise<IDataValidationError[]> {
const unitId = this._workbook.getUnitId();
const sheetIds = this._dataValidationModel.getSubUnitIds(unitId);
const allErrors: IDataValidationError[] = [];
for (const sheetId of sheetIds) {
const sheetErrors = await this._collectValidationErrorsForSheet(unitId, sheetId);
allErrors.push(...sheetErrors);
}
return allErrors;
}
private async _collectValidationErrorsForSheet(unitId: string, sheetId: string): Promise<IDataValidationError[]> {
const rules = this._dataValidationModel.getRules(unitId, sheetId);
if (!rules.length) {
return [];
}
const allRanges = rules.flatMap((rule) => rule.ranges);
return this._collectValidationErrorsForRange(unitId, sheetId, allRanges);
}
private async _collectValidationErrorsForRange(unitId: string, sheetId: string, ranges: IRange[]): Promise<IDataValidationError[]> {
if (!ranges.length) {
return [];
}
const validatorService = this._injector.get(SheetsDataValidationValidatorService);
const workbook = this._workbook;
const worksheet = workbook.getSheetBySheetId(sheetId);
if (!worksheet) {
throw new Error(`Cannot find worksheet with sheetId: ${sheetId}`);
}
const sheetName = worksheet.getName();
const errors: IDataValidationError[] = [];
for (const range of ranges) {
const promises: Promise<void>[] = [];
for (let row = range.startRow; row <= range.endRow; row++) {
for (let col = range.startColumn; col <= range.endColumn; col++) {
promises.push((async (): Promise<void> => {
try {
const status = await validatorService.validatorCell(unitId, sheetId, row, col);
// Only collect errors (non-VALID status)
if (status !== DataValidationStatus.VALID) {
const rule = this._dataValidationModel.getRuleByLocation(unitId, sheetId, row, col);
if (rule) {
const cellValue = worksheet.getCell(row, col)?.v || null;
const error = this._createDataValidationError(
sheetName,
row,
col,
rule,
cellValue
);
errors.push(error);
}
}
} catch (e) {
// Skip cells that can't be validated
console.warn(`Failed to validate cell [${row}, ${col}]:`, e);
}
})());
}
}
await Promise.all(promises);
}
return errors;
}
private _createDataValidationError(
sheetName: string,
row: number,
column: number,
rule: IDataValidationRule,
inputValue: string | number | boolean | null
): IDataValidationError {
return {
sheetName,
row,
column,
ruleId: rule.uid,
inputValue,
rule,
};
}
}
FWorkbook.extend(FWorkbookSheetsDataValidationMixin);
declare module '@univerjs/sheets/facade' {
interface FWorkbook extends IFWorkbookSheetsDataValidationMixin { }
}

View File

@@ -0,0 +1,201 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IDataValidationRule, IRange, Nullable, ObjectMatrix } from '@univerjs/core';
import type { IDataValidationError } from './f-workbook';
import { DataValidationStatus } from '@univerjs/core';
import { DataValidationModel } from '@univerjs/data-validation';
import { SheetDataValidationModel, SheetsDataValidationValidatorService } from '@univerjs/sheets-data-validation';
import { FWorksheet } from '@univerjs/sheets/facade';
import { FDataValidation } from './f-data-validation';
/**
* @ignore
*/
export interface IFWorksheetDataValidationMixin {
/**
* Get all data validation rules in current sheet.
* @returns {FDataValidation[]} All data validation rules
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const rules = fWorksheet.getDataValidations();
* console.log(rules);
* ```
*/
getDataValidations(): FDataValidation[];
/**
* Get data validation validator status for current sheet.
* @returns {Promise<ObjectMatrix<Nullable<DataValidationStatus>>>} matrix of validator status
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const status = await fWorksheet.getValidatorStatusAsync();
* console.log(status);
* ```
*/
getValidatorStatusAsync(): Promise<ObjectMatrix<Nullable<DataValidationStatus>>>;
/**
* get data validation rule by rule id
* @param ruleId - the rule id
* @returns {Nullable<FDataValidation>} data validation rule
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const rules = fWorksheet.getDataValidations();
* console.log(fWorksheet.getDataValidation(rules[0]?.rule.uid));
* ```
*/
getDataValidation(ruleId: string): Nullable<FDataValidation>;
/**
* Get all data validation errors for current worksheet.
* @returns {Promise<IDataValidationError[]>} A promise that resolves to an array of validation errors.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const errors = await fWorksheet.getAllDataValidationErrorAsync();
* console.log(errors);
* ```
*/
getAllDataValidationErrorAsync(): Promise<IDataValidationError[]>;
}
/**
* @ignore
*/
export class FWorksheetDataValidationMixin extends FWorksheet implements IFWorksheetDataValidationMixin {
override getDataValidations(): FDataValidation[] {
const dataValidationModel = this._injector.get(DataValidationModel);
return dataValidationModel.getRules(this._workbook.getUnitId(), this._worksheet.getSheetId()).map((rule) => new FDataValidation(rule, this._worksheet, this._injector));
}
override getValidatorStatusAsync(): Promise<ObjectMatrix<Nullable<DataValidationStatus>>> {
const validatorService = this._injector.get(SheetsDataValidationValidatorService);
return validatorService.validatorWorksheet(
this._workbook.getUnitId(),
this._worksheet.getSheetId()
);
}
override getDataValidation(ruleId: string): Nullable<FDataValidation> {
const dataValidationModel = this._injector.get(DataValidationModel);
const rule = dataValidationModel.getRuleById(this._workbook.getUnitId(), this._worksheet.getSheetId(), ruleId);
if (rule) {
return new FDataValidation(rule, this._worksheet, this._injector);
}
return null;
}
override async getAllDataValidationErrorAsync(): Promise<IDataValidationError[]> {
const unitId = this._workbook.getUnitId();
const sheetId = this._worksheet.getSheetId();
return this._collectValidationErrorsForSheet(unitId, sheetId);
}
private async _collectValidationErrorsForSheet(unitId: string, sheetId: string): Promise<IDataValidationError[]> {
const dataValidationModel = this._injector.get(DataValidationModel);
const rules = dataValidationModel.getRules(unitId, sheetId);
if (!rules.length) {
return [];
}
const allRanges = rules.flatMap((rule) => rule.ranges);
return this._collectValidationErrorsForRange(unitId, sheetId, allRanges);
}
private async _collectValidationErrorsForRange(unitId: string, sheetId: string, ranges: IRange[]): Promise<IDataValidationError[]> {
if (!ranges.length) {
return [];
}
const validatorService = this._injector.get(SheetsDataValidationValidatorService);
const worksheet = this._worksheet;
const sheetName = worksheet.getName();
const errors: IDataValidationError[] = [];
for (const range of ranges) {
const promises: Promise<void>[] = [];
for (let row = range.startRow; row <= range.endRow; row++) {
for (let col = range.startColumn; col <= range.endColumn; col++) {
promises.push((async (): Promise<void> => {
try {
const status = await validatorService.validatorCell(unitId, sheetId, row, col);
// Only collect errors (non-VALID status)
if (status !== DataValidationStatus.VALID) {
const dataValidationModel = this._injector.get(SheetDataValidationModel);
const rule = dataValidationModel.getRuleByLocation(unitId, sheetId, row, col);
if (rule) {
const cellValue = worksheet.getCell(row, col)?.v || null;
const error = this._createDataValidationError(
sheetName,
row,
col,
rule,
cellValue
);
errors.push(error);
}
}
} catch (e) {
// Skip cells that can't be validated
console.warn(`Failed to validate cell [${row}, ${col}]:`, e);
}
})());
}
}
await Promise.all(promises);
}
return errors;
}
private _createDataValidationError(
sheetName: string,
row: number,
column: number,
rule: IDataValidationRule,
inputValue: string | number | boolean | null
): IDataValidationError {
return {
sheetName,
row,
column,
ruleId: rule.uid,
inputValue,
rule,
};
}
}
FWorksheet.extend(FWorksheetDataValidationMixin);
declare module '@univerjs/sheets/facade' {
interface FWorksheet extends IFWorksheetDataValidationMixin {}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 './f-range';
import './f-univer';
import './f-workbook';
import './f-worksheet';
import './f-event';
export { FDataValidation } from './f-data-validation';
export { FDataValidationBuilder } from './f-data-validation-builder';
export type * from './f-event';
export type * from './f-range';
export type * from './f-univer';
export type * from './f-workbook';
export type * from './f-worksheet';

View File

@@ -0,0 +1,67 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export {
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
getDataValidationDiffMutations,
RemoveSheetAllDataValidationCommand,
RemoveSheetDataValidationCommand,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
} from './commands/commands/data-validation.command';
export type {
IAddSheetDataValidationCommandParams,
IClearRangeDataValidationCommandParams,
IRemoveSheetAllDataValidationCommandParams,
IRemoveSheetDataValidationCommandParams,
IUpdateSheetDataValidationOptionsCommandParams,
IUpdateSheetDataValidationRangeCommandParams,
IUpdateSheetDataValidationSettingCommandParams,
} from './commands/commands/data-validation.command';
export { DATA_VALIDATION_PLUGIN_NAME } from './common/const';
export type { IUniverSheetsDataValidationConfig } from './config/config';
export { DataValidationFormulaController } from './controllers/dv-formula.controller';
export { SheetDataValidationModel } from './models/sheet-data-validation-model';
export type { IValidStatusChange } from './models/sheet-data-validation-model';
export { UniverSheetsDataValidationPlugin } from './plugin';
export { DataValidationCacheService } from './services/dv-cache.service';
export { DataValidationCustomFormulaService } from './services/dv-custom-formula.service';
export { DataValidationFormulaService } from './services/dv-formula.service';
export { DataValidationListCacheService } from './services/dv-list-cache.service';
export type { IListCacheItem } from './services/dv-list-cache.service';
export { SheetsDataValidationValidatorService } from './services/dv-validator.service';
export { createDefaultNewRule } from './utils/create';
export { getFormulaCellData, getFormulaResult } from './utils/formula';
export { isLegalFormulaResult } from './utils/formula';
export { getCellValueOrigin } from './utils/get-cell-data-origin';
export {
CHECKBOX_FORMULA_1,
CHECKBOX_FORMULA_2,
CheckboxValidator,
transformCheckboxValue,
} from './validators/checkbox-validator';
export { DateValidator } from './validators/date-validator';
export { getCellValueNumber } from './validators/decimal-validator';
export { ListMultipleValidator } from './validators/list-multiple-validator';
export { ListValidator } from './validators/list-validator';
export { getDataValidationCellValue } from './validators/util';
export { getTransformedFormula } from './validators/util';
export const CUSTOM_FORMULA_INPUT_NAME = 'sheets-data-validation.custom-formula-input';
export const BASE_FORMULA_INPUT_NAME = 'sheets-data-validation.formula-input';
export const LIST_FORMULA_INPUT_NAME = 'sheets-data-validation.list-formula-input';
export const CHECKBOX_FORMULA_INPUT_NAME = 'sheets-data-validation.checkbox-formula-input';

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'بين',
greaterThan: 'أكبر من',
greaterThanOrEqual: 'أكبر من أو يساوي',
lessThan: 'أقل من',
lessThanOrEqual: 'أقل من أو يساوي',
equal: 'يساوي',
notEqual: 'لا يساوي',
notBetween: 'ليس بين',
},
ruleName: {
between: 'بين {FORMULA1} و{FORMULA2}',
greaterThan: 'أكبر من {FORMULA1}',
greaterThanOrEqual: 'أكبر من أو يساوي {FORMULA1}',
lessThan: 'أقل من {FORMULA1}',
lessThanOrEqual: 'أقل من أو يساوي {FORMULA1}',
equal: 'يساوي {FORMULA1}',
notEqual: 'لا يساوي {FORMULA1}',
notBetween: 'ليس بين {FORMULA1} و{FORMULA2}',
legal: 'نوع قانوني {TYPE}',
},
errorMsg: {
between: 'يجب أن تكون القيمة بين {FORMULA1} و{FORMULA2}',
greaterThan: 'يجب أن تكون القيمة أكبر من {FORMULA1}',
greaterThanOrEqual: 'يجب أن تكون القيمة أكبر من أو تساوي {FORMULA1}',
lessThan: 'يجب أن تكون القيمة أقل من {FORMULA1}',
lessThanOrEqual: 'يجب أن تكون القيمة أقل من أو تساوي {FORMULA1}',
equal: 'يجب أن تكون القيمة تساوي {FORMULA1}',
notEqual: 'يجب ألا تكون القيمة تساوي {FORMULA1}',
notBetween: 'يجب ألا تكون القيمة بين {FORMULA1} و{FORMULA2}',
legal: 'يجب أن تكون القيمة من نوع قانوني {TYPE}',
},
date: {
operators: {
between: 'بين',
greaterThan: 'بعد',
greaterThanOrEqual: 'في أو بعد',
lessThan: 'قبل',
lessThanOrEqual: 'في أو قبل',
equal: 'يساوي',
notEqual: 'لا يساوي',
notBetween: 'ليس بين',
},
ruleName: {
between: 'بين {FORMULA1} و{FORMULA2}',
greaterThan: 'بعد {FORMULA1}',
greaterThanOrEqual: 'في أو بعد {FORMULA1}',
lessThan: 'قبل {FORMULA1}',
lessThanOrEqual: 'في أو قبل {FORMULA1}',
equal: 'في {FORMULA1}',
notEqual: 'ليس في {FORMULA1}',
notBetween: 'ليس بين {FORMULA1} و{FORMULA2}',
legal: 'تاريخ قانوني',
},
errorMsg: {
between: 'يجب أن تكون القيمة تاريخًا قانونيًا وبين {FORMULA1} و{FORMULA2}',
greaterThan: 'يجب أن تكون القيمة تاريخًا قانونيًا وبعد {FORMULA1}',
greaterThanOrEqual: 'يجب أن تكون القيمة تاريخًا قانونيًا وفي أو بعد {FORMULA1}',
lessThan: 'يجب أن تكون القيمة تاريخًا قانونيًا وقبل {FORMULA1}',
lessThanOrEqual: 'يجب أن تكون القيمة تاريخًا قانونيًا وفي أو قبل {FORMULA1}',
equal: 'يجب أن تكون القيمة تاريخًا قانونيًا وفي {FORMULA1}',
notEqual: 'يجب أن تكون القيمة تاريخًا قانونيًا وليس في {FORMULA1}',
notBetween: 'يجب أن تكون القيمة تاريخًا قانونيًا وليس بين {FORMULA1} و{FORMULA2}',
legal: 'يجب أن تكون القيمة تاريخًا قانونيًا',
},
title: 'تاريخ',
},
textLength: {
errorMsg: {
between: 'يجب أن يكون طول النص بين {FORMULA1} و{FORMULA2}',
greaterThan: 'يجب أن يكون طول النص أكبر من {FORMULA1}',
greaterThanOrEqual: 'يجب أن يكون طول النص أكبر من أو يساوي {FORMULA1}',
lessThan: 'يجب أن يكون طول النص أقل من {FORMULA1}',
lessThanOrEqual: 'يجب أن يكون طول النص أقل من أو يساوي {FORMULA1}',
equal: 'يجب أن يكون طول النص يساوي {FORMULA1}',
notEqual: 'يجب ألا يكون طول النص يساوي {FORMULA1}',
notBetween: 'يجب ألا يكون طول النص بين {FORMULA1} و{FORMULA2}',
},
title: 'طول النص',
},
custom: {
ruleName: 'الصيغة المخصصة هي {FORMULA1}',
title: 'صيغة مخصصة',
error: 'محتويات هذه الخلية تنتهك قاعدة التحقق',
},
validFail: {
value: 'يرجى إدخال قيمة',
number: 'يرجى إدخال رقم أو صيغة',
formula: 'يرجى إدخال صيغة',
date: 'يرجى إدخال تاريخ أو صيغة',
list: 'يرجى إدخال خيارات',
listInvalid: 'يجب أن يكون مصدر القائمة قائمة محددة بفواصل أو مرجعًا لصف أو عمود واحد',
checkboxEqual: 'أدخل قيمًا مختلفة لمحتويات الخلية المحددة وغير المحددة.',
listIntersects: 'لا يمكن أن يتقاطع النطاق المحدد مع نطاق القواعد',
primitive: 'غير مسموح بالصيغ لقيم المحدد وغير المحدد المخصصة.',
},
any: {
title: 'أي قيمة',
error: 'محتوى هذه الخلية ينتهك قاعدة التحقق',
},
list: {
title: 'قائمة منسدلة',
name: 'القيمة تحتوي على واحدة من النطاق',
error: 'يجب أن يكون الإدخال ضمن النطاق المحدد',
},
listMultiple: {
title: 'قائمة منسدلة - متعددة',
},
decimal: {
title: 'رقم',
},
whole: {
title: 'عدد صحيح',
},
checkbox: {
title: 'خانة اختيار',
error: 'محتويات هذه الخلية تنتهك قاعدة التحقق',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'entre',
greaterThan: 'més gran que',
greaterThanOrEqual: 'més gran o igual que',
lessThan: 'menys que',
lessThanOrEqual: 'menys o igual que',
equal: 'igual',
notEqual: 'no igual',
notBetween: 'no entre',
},
ruleName: {
between: 'Està entre {FORMULA1} i {FORMULA2}',
greaterThan: 'És més gran que {FORMULA1}',
greaterThanOrEqual: 'És més gran o igual que {FORMULA1}',
lessThan: 'És menys que {FORMULA1}',
lessThanOrEqual: 'És menys o igual que {FORMULA1}',
equal: 'És igual a {FORMULA1}',
notEqual: 'No és igual a {FORMULA1}',
notBetween: 'No està entre {FORMULA1} i {FORMULA2}',
legal: 'És un {TYPE} legal',
},
errorMsg: {
between: 'El valor ha d’estar entre {FORMULA1} i {FORMULA2}',
greaterThan: 'El valor ha de ser més gran que {FORMULA1}',
greaterThanOrEqual: 'El valor ha de ser més gran o igual que {FORMULA1}',
lessThan: 'El valor ha de ser menys que {FORMULA1}',
lessThanOrEqual: 'El valor ha de ser menys o igual que {FORMULA1}',
equal: 'El valor ha de ser igual a {FORMULA1}',
notEqual: 'El valor no ha de ser igual a {FORMULA1}',
notBetween: 'El valor no ha d’estar entre {FORMULA1} i {FORMULA2}',
legal: 'El valor ha de ser un {TYPE} legal',
},
date: {
operators: {
between: 'entre',
greaterThan: 'després de',
greaterThanOrEqual: 'en o després de',
lessThan: 'abans de',
lessThanOrEqual: 'en o abans de',
equal: 'igual',
notEqual: 'no igual',
notBetween: 'no entre',
},
ruleName: {
between: 'està entre {FORMULA1} i {FORMULA2}',
greaterThan: 'és després de {FORMULA1}',
greaterThanOrEqual: 'és en o després de {FORMULA1}',
lessThan: 'és abans de {FORMULA1}',
lessThanOrEqual: 'és en o abans de {FORMULA1}',
equal: 'és {FORMULA1}',
notEqual: 'no és {FORMULA1}',
notBetween: 'no està entre {FORMULA1} i {FORMULA2}',
legal: 'és una data legal',
},
errorMsg: {
between: 'El valor ha de ser una data legal i estar entre {FORMULA1} i {FORMULA2}',
greaterThan: 'El valor ha de ser una data legal i després de {FORMULA1}',
greaterThanOrEqual: 'El valor ha de ser una data legal i en o després de {FORMULA1}',
lessThan: 'El valor ha de ser una data legal i abans de {FORMULA1}',
lessThanOrEqual: 'El valor ha de ser una data legal i en o abans de {FORMULA1}',
equal: 'El valor ha de ser una data legal i {FORMULA1}',
notEqual: 'El valor ha de ser una data legal i no {FORMULA1}',
notBetween: 'El valor ha de ser una data legal i no estar entre {FORMULA1} i {FORMULA2}',
legal: 'El valor ha de ser una data legal',
},
title: 'Data',
},
textLength: {
errorMsg: {
between: 'La longitud del text ha d’estar entre {FORMULA1} i {FORMULA2}',
greaterThan: 'La longitud del text ha de ser més gran que {FORMULA1}',
greaterThanOrEqual: 'La longitud del text ha de ser més gran o igual que {FORMULA1}',
lessThan: 'La longitud del text ha de ser menys que {FORMULA1}',
lessThanOrEqual: 'La longitud del text ha de ser menys o igual que {FORMULA1}',
equal: 'La longitud del text ha de ser {FORMULA1}',
notEqual: 'La longitud del text no ha de ser {FORMULA1}',
notBetween: 'La longitud del text no ha d’estar entre {FORMULA1} i {FORMULA2}',
},
title: 'Longitud del text',
},
custom: {
ruleName: 'La fórmula personalitzada és {FORMULA1}',
title: 'Fórmula personalitzada',
error: 'El contingut d’aquesta cel·la incompleix la seva regla de validació',
},
validFail: {
value: 'Si us plau, introdueix un valor',
number: 'Si us plau, introdueix un número o una fórmula',
formula: 'Si us plau, introdueix una fórmula',
date: 'Si us plau, introdueix una data o una fórmula',
list: 'Si us plau, introdueix opcions',
listInvalid: 'La font de la llista ha de ser una llista delimitada o una referència a una sola fila o columna',
checkboxEqual: 'Introdueix valors diferents per als continguts de cel·la marcats i desmarcats.',
listIntersects: 'L’interval seleccionat no pot creuar-se amb l’àmbit de les regles',
primitive: 'No es permeten fórmules per a valors personalitzats marcats i desmarcats.',
},
any: {
title: 'Qualsevol valor',
error: 'El contingut d’aquesta cel·la incompleix la regla de validació',
},
list: {
title: 'Desplegable',
name: 'El valor conté un del rang',
error: 'L’entrada ha d’estar dins del rang especificat',
},
listMultiple: {
title: 'Desplegable-múltiple',
},
decimal: {
title: 'Número',
},
whole: {
title: 'Enter',
},
checkbox: {
title: 'Casella de selecció',
error: 'El contingut d’aquesta cel·la incompleix la seva regla de validació',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'zwischen',
greaterThan: 'größer als',
greaterThanOrEqual: 'größer als oder gleich',
lessThan: 'kleiner als',
lessThanOrEqual: 'kleiner als oder gleich',
equal: 'gleich',
notEqual: 'ungleich',
notBetween: 'nicht zwischen',
},
ruleName: {
between: 'Ist zwischen {FORMULA1} und {FORMULA2}',
greaterThan: 'Ist größer als {FORMULA1}',
greaterThanOrEqual: 'Ist größer als oder gleich {FORMULA1}',
lessThan: 'Ist kleiner als {FORMULA1}',
lessThanOrEqual: 'Ist kleiner als oder gleich {FORMULA1}',
equal: 'Ist gleich {FORMULA1}',
notEqual: 'Ist ungleich {FORMULA1}',
notBetween: 'Ist nicht zwischen {FORMULA1} und {FORMULA2}',
legal: 'Ist ein gültiger {TYPE}',
},
errorMsg: {
between: 'Wert muss zwischen {FORMULA1} und {FORMULA2} liegen',
greaterThan: 'Wert muss größer als {FORMULA1} sein',
greaterThanOrEqual: 'Wert muss größer als oder gleich {FORMULA1} sein',
lessThan: 'Wert muss kleiner als {FORMULA1} sein',
lessThanOrEqual: 'Wert muss kleiner als oder gleich {FORMULA1} sein',
equal: 'Wert muss gleich {FORMULA1} sein',
notEqual: 'Wert muss ungleich {FORMULA1} sein',
notBetween: 'Wert muss nicht zwischen {FORMULA1} und {FORMULA2} liegen',
legal: 'Wert muss ein gültiger {TYPE} sein',
},
date: {
operators: {
between: 'zwischen',
greaterThan: 'nach',
greaterThanOrEqual: 'am oder nach',
lessThan: 'vor',
lessThanOrEqual: 'am oder vor',
equal: 'gleich',
notEqual: 'ungleich',
notBetween: 'nicht zwischen',
},
ruleName: {
between: 'ist zwischen {FORMULA1} und {FORMULA2}',
greaterThan: 'ist nach {FORMULA1}',
greaterThanOrEqual: 'ist am oder nach {FORMULA1}',
lessThan: 'ist vor {FORMULA1}',
lessThanOrEqual: 'ist am oder vor {FORMULA1}',
equal: 'ist {FORMULA1}',
notEqual: 'ist nicht {FORMULA1}',
notBetween: 'ist nicht zwischen {FORMULA1} und {FORMULA2}',
legal: 'ist ein gültiges Datum',
},
errorMsg: {
between: 'Wert muss ein gültiges Datum sein und zwischen {FORMULA1} und {FORMULA2} liegen',
greaterThan: 'Wert muss ein gültiges Datum sein und nach {FORMULA1} liegen',
greaterThanOrEqual: 'Wert muss ein gültiges Datum sein und am oder nach {FORMULA1} liegen',
lessThan: 'Wert muss ein gültiges Datum sein und vor {FORMULA1} liegen',
lessThanOrEqual: 'Wert muss ein gültiges Datum sein und am oder vor {FORMULA1} liegen',
equal: 'Wert muss ein gültiges Datum sein und {FORMULA1} entsprechen',
notEqual: 'Wert muss ein gültiges Datum sein und nicht {FORMULA1} entsprechen',
notBetween: 'Wert muss ein gültiges Datum sein und nicht zwischen {FORMULA1} und {FORMULA2} liegen',
legal: 'Wert muss ein gültiges Datum sein',
},
title: 'Datum',
},
textLength: {
errorMsg: {
between: 'Textlänge muss zwischen {FORMULA1} und {FORMULA2} liegen',
greaterThan: 'Textlänge muss größer als {FORMULA1} sein',
greaterThanOrEqual: 'Textlänge muss größer als oder gleich {FORMULA1} sein',
lessThan: 'Textlänge muss kleiner als {FORMULA1} sein',
lessThanOrEqual: 'Textlänge muss kleiner als oder gleich {FORMULA1} sein',
equal: 'Textlänge muss {FORMULA1} sein',
notEqual: 'Textlänge muss ungleich {FORMULA1} sein',
notBetween: 'Textlänge muss nicht zwischen {FORMULA1} und {FORMULA2} liegen',
},
title: 'Textlänge',
},
custom: {
ruleName: 'Benutzerdefinierte Formel ist {FORMULA1}',
title: 'Benutzerdefinierte Formel',
error: 'Der Inhalt dieser Zelle verstößt gegen die Überprüfungsregel',
},
validFail: {
value: 'Bitte geben Sie einen Wert ein',
number: 'Bitte geben Sie eine Zahl oder eine Formel ein',
formula: 'Bitte geben Sie eine Formel ein',
date: 'Bitte geben Sie ein Datum oder eine Formel ein',
list: 'Bitte geben Sie Optionen ein',
listInvalid: 'Die Listenquelle muss eine durch Trennzeichen getrennte Liste oder ein Bezug auf eine einzelne Zeile oder Spalte sein',
checkboxEqual: 'Geben Sie unterschiedliche Werte für angekreuzte und nicht angekreuzte Zelleninhalte ein.',
listIntersects: 'Der ausgewählte Bereich darf sich nicht mit dem Geltungsbereich der Regeln überschneiden',
primitive: 'Formeln sind für benutzerdefinierte angekreuzte und nicht angekreuzte Werte nicht zulässig.',
},
any: {
title: 'Beliebiger Wert',
error: 'Der Inhalt dieser Zelle verstößt gegen die Überprüfungsregel',
},
list: {
title: 'Dropdown',
name: 'Wert enthält einen aus dem Bereich',
error: 'Eingabe muss im angegebenen Bereich liegen',
},
listMultiple: {
title: 'Dropdown-Mehrfachauswahl',
},
decimal: {
title: 'Zahl',
},
whole: {
title: 'Ganze Zahl',
},
checkbox: {
title: 'Kontrollkästchen',
error: 'Der Inhalt dieser Zelle verstößt gegen die Überprüfungsregel',
},
},
};
export default locale;

View File

@@ -0,0 +1,140 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
const locale = {
'sheets-data-validation': {
operators: {
between: 'between',
greaterThan: 'greater than',
greaterThanOrEqual: 'greater than or equal',
lessThan: 'less than',
lessThanOrEqual: 'less than or equal',
equal: 'equal',
notEqual: 'not equal',
notBetween: 'not between',
},
ruleName: {
between: 'Is between {FORMULA1} and {FORMULA2}',
greaterThan: 'Is greater than {FORMULA1}',
greaterThanOrEqual: 'Is greater than or equal to {FORMULA1}',
lessThan: 'Is less than {FORMULA1}',
lessThanOrEqual: 'Is less than or equal to {FORMULA1}',
equal: 'Is equal to {FORMULA1}',
notEqual: 'Is not equal to {FORMULA1}',
notBetween: 'Is not between {FORMULA1} and {FORMULA2}',
legal: 'Is a legal {TYPE}',
},
errorMsg: {
between: 'Value must be between {FORMULA1} and {FORMULA2}',
greaterThan: 'Value must be greater than {FORMULA1}',
greaterThanOrEqual: 'Value must be greater than or equal to {FORMULA1}',
lessThan: 'Value must be less than {FORMULA1}',
lessThanOrEqual: 'Value must be less than or equal to {FORMULA1}',
equal: 'Value must be equal to {FORMULA1}',
notEqual: 'Value must be not equal to {FORMULA1}',
notBetween: 'Value must be not between {FORMULA1} and {FORMULA2}',
legal: 'Value must be a legal {TYPE}',
},
date: {
operators: {
between: 'between',
greaterThan: 'after',
greaterThanOrEqual: 'on or after',
lessThan: 'before',
lessThanOrEqual: 'on or before',
equal: 'equal',
notEqual: 'not equal',
notBetween: 'not between',
},
ruleName: {
between: 'is between {FORMULA1} and {FORMULA2}',
greaterThan: 'is after {FORMULA1}',
greaterThanOrEqual: 'is on or after {FORMULA1}',
lessThan: 'is before {FORMULA1}',
lessThanOrEqual: 'is on or before {FORMULA1}',
equal: 'is {FORMULA1}',
notEqual: 'is not {FORMULA1}',
notBetween: 'is not between {FORMULA1} and {FORMULA2}',
legal: 'is a legal date',
},
errorMsg: {
between: 'Value must be a legal date and between {FORMULA1} and {FORMULA2}',
greaterThan: 'Value must be a legal date and after {FORMULA1}',
greaterThanOrEqual: 'Value must be a legal date and on or after {FORMULA1}',
lessThan: 'Value must be a legal date and before {FORMULA1}',
lessThanOrEqual: 'Value must be a legal date and on or before {FORMULA1}',
equal: 'Value must be a legal date and {FORMULA1}',
notEqual: 'Value must be a legal date and not {FORMULA1}',
notBetween: 'Value must be a legal date and not between {FORMULA1} and {FORMULA2}',
legal: 'Value must be a legal date',
},
title: 'Date',
},
textLength: {
errorMsg: {
between: 'Text length must be between {FORMULA1} and {FORMULA2}',
greaterThan: 'Text length must be after {FORMULA1}',
greaterThanOrEqual: 'Text length must be on or after {FORMULA1}',
lessThan: 'Text length must be before {FORMULA1}',
lessThanOrEqual: 'Text length must be on or before {FORMULA1}',
equal: 'Text length must be {FORMULA1}',
notEqual: 'Text length must be not {FORMULA1}',
notBetween: 'Text length must be not between {FORMULA1} and {FORMULA2}',
},
title: 'Text length',
},
custom: {
ruleName: 'Custom formula is {FORMULA1}',
title: 'Custom formula',
error: 'This cell\'s contents violate its validation rule',
},
validFail: {
value: 'Please input a value',
number: 'Please input number or formula',
formula: 'Please input formula',
date: 'Please input date or formula',
list: 'Please input options',
listInvalid: 'The list source must be a delimited list or a reference to a single row or column',
checkboxEqual: 'Enter different values for ticked and unticked cell contents.',
listIntersects: 'The selected range cannot intersect with the scope of the rules',
primitive: 'Formulas are not permitted for custom ticked and unticked values.',
},
any: {
title: 'Any value',
error: 'The content of this cell violates the validation rule',
},
list: {
title: 'Dropdown',
name: 'Value contains one from range',
error: 'Input must fall within specified range',
},
listMultiple: {
title: 'Dropdown-Multiple',
},
decimal: {
title: 'Number',
},
whole: {
title: 'Integer',
},
checkbox: {
title: 'Checkbox',
error: 'This cell\'s contents violate its validation rule',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'entre',
greaterThan: 'mayor que',
greaterThanOrEqual: 'mayor o igual que',
lessThan: 'menor que',
lessThanOrEqual: 'menor o igual que',
equal: 'igual',
notEqual: 'no igual',
notBetween: 'no entre',
},
ruleName: {
between: 'Está entre {FORMULA1} y {FORMULA2}',
greaterThan: 'Es mayor que {FORMULA1}',
greaterThanOrEqual: 'Es mayor o igual que {FORMULA1}',
lessThan: 'Es menor que {FORMULA1}',
lessThanOrEqual: 'Es menor o igual que {FORMULA1}',
equal: 'Es igual a {FORMULA1}',
notEqual: 'No es igual a {FORMULA1}',
notBetween: 'No está entre {FORMULA1} y {FORMULA2}',
legal: 'Es un {TYPE} legal',
},
errorMsg: {
between: 'El valor debe estar entre {FORMULA1} y {FORMULA2}',
greaterThan: 'El valor debe ser mayor que {FORMULA1}',
greaterThanOrEqual: 'El valor debe ser mayor o igual que {FORMULA1}',
lessThan: 'El valor debe ser menor que {FORMULA1}',
lessThanOrEqual: 'El valor debe ser menor o igual que {FORMULA1}',
equal: 'El valor debe ser igual a {FORMULA1}',
notEqual: 'El valor no debe ser igual a {FORMULA1}',
notBetween: 'El valor no debe estar entre {FORMULA1} y {FORMULA2}',
legal: 'El valor debe ser un {TYPE} legal',
},
date: {
operators: {
between: 'entre',
greaterThan: 'después de',
greaterThanOrEqual: 'en o después de',
lessThan: 'antes de',
lessThanOrEqual: 'en o antes de',
equal: 'igual',
notEqual: 'no igual',
notBetween: 'no entre',
},
ruleName: {
between: 'está entre {FORMULA1} y {FORMULA2}',
greaterThan: 'es después de {FORMULA1}',
greaterThanOrEqual: 'es en o después de {FORMULA1}',
lessThan: 'es antes de {FORMULA1}',
lessThanOrEqual: 'es en o antes de {FORMULA1}',
equal: 'es {FORMULA1}',
notEqual: 'no es {FORMULA1}',
notBetween: 'no está entre {FORMULA1} y {FORMULA2}',
legal: 'es una fecha legal',
},
errorMsg: {
between: 'El valor debe ser una fecha legal y estar entre {FORMULA1} y {FORMULA2}',
greaterThan: 'El valor debe ser una fecha legal y después de {FORMULA1}',
greaterThanOrEqual: 'El valor debe ser una fecha legal y en o después de {FORMULA1}',
lessThan: 'El valor debe ser una fecha legal y antes de {FORMULA1}',
lessThanOrEqual: 'El valor debe ser una fecha legal y en o antes de {FORMULA1}',
equal: 'El valor debe ser una fecha legal y {FORMULA1}',
notEqual: 'El valor debe ser una fecha legal y no {FORMULA1}',
notBetween: 'El valor debe ser una fecha legal y no estar entre {FORMULA1} y {FORMULA2}',
legal: 'El valor debe ser una fecha legal',
},
title: 'Fecha',
},
textLength: {
errorMsg: {
between: 'La longitud del texto debe estar entre {FORMULA1} y {FORMULA2}',
greaterThan: 'La longitud del texto debe ser mayor que {FORMULA1}',
greaterThanOrEqual: 'La longitud del texto debe ser mayor o igual que {FORMULA1}',
lessThan: 'La longitud del texto debe ser menor que {FORMULA1}',
lessThanOrEqual: 'La longitud del texto debe ser menor o igual que {FORMULA1}',
equal: 'La longitud del texto debe ser {FORMULA1}',
notEqual: 'La longitud del texto no debe ser {FORMULA1}',
notBetween: 'La longitud del texto no debe estar entre {FORMULA1} y {FORMULA2}',
},
title: 'Longitud del texto',
},
custom: {
ruleName: 'La fórmula personalizada es {FORMULA1}',
title: 'Fórmula personalizada',
error: 'El contenido de esta celda viola su regla de validación',
},
validFail: {
value: 'Por favor, introduce un valor',
number: 'Por favor, introduce un número o fórmula',
formula: 'Por favor, introduce una fórmula',
date: 'Por favor, introduce una fecha o fórmula',
list: 'Por favor, introduce opciones',
listInvalid: 'La fuente de la lista debe ser una lista delimitada o una referencia a una sola fila o columna',
checkboxEqual: 'Introduce valores diferentes para los contenidos de celda marcados y desmarcados.',
listIntersects: 'El rango seleccionado no puede cruzarse con el ámbito de las reglas',
primitive: 'No se permiten fórmulas para valores personalizados marcados y desmarcados.',
},
any: {
title: 'Cualquier valor',
error: 'El contenido de esta celda viola la regla de validación',
},
list: {
title: 'Desplegable',
name: 'El valor contiene uno del rango',
error: 'La entrada debe estar dentro del rango especificado',
},
listMultiple: {
title: 'Desplegable-múltiple',
},
decimal: {
title: 'Número',
},
whole: {
title: 'Entero',
},
checkbox: {
title: 'Casilla de verificación',
error: 'El contenido de esta celda viola su regla de validación',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'بین',
greaterThan: 'بزرگتر از',
greaterThanOrEqual: 'بزرگتر از یا برابر با',
lessThan: 'کوچکتر از',
lessThanOrEqual: 'کوچکتر از یا برابر با',
equal: 'برابر با',
notEqual: 'نابرابر با',
notBetween: 'بین نیست',
},
ruleName: {
between: 'بین {FORMULA1} و {FORMULA2} است',
greaterThan: 'بزرگتر از {FORMULA1} است',
greaterThanOrEqual: 'بزرگتر از یا برابر با {FORMULA1} است',
lessThan: 'کوچکتر از {FORMULA1} است',
lessThanOrEqual: 'کوچکتر از یا برابر با {FORMULA1} است',
equal: 'برابر است با {FORMULA1}',
notEqual: 'برابر نیست با {FORMULA1}',
notBetween: 'بین {FORMULA1} و {FORMULA2} نیست',
legal: 'یک {TYPE} قانونی است',
},
errorMsg: {
between: 'مقدار باید بین {FORMULA1} و {FORMULA2} باشد',
greaterThan: 'مقدار باید بزرگتر از {FORMULA1} باشد',
greaterThanOrEqual: 'مقدار باید بزرگتر از یا برابر با {FORMULA1} باشد',
lessThan: 'مقدار باید کوچکتر از {FORMULA1} باشد',
lessThanOrEqual: 'مقدار باید کوچکتر از یا برابر با {FORMULA1} باشد',
equal: 'مقدار باید برابر با {FORMULA1} باشد',
notEqual: 'مقدار باید برابر نیست با {FORMULA1}',
notBetween: 'مقدار باید بین {FORMULA1} و {FORMULA2} نباشد',
legal: 'مقدار باید یک {TYPE} قانونی باشد',
},
date: {
operators: {
between: 'بین',
greaterThan: 'بعد از',
greaterThanOrEqual: 'روی یا بعد از',
lessThan: 'قبل از',
lessThanOrEqual: 'روی یا قبل از',
equal: 'برابر است با',
notEqual: 'برابر نیست با',
notBetween: 'بین ... و ... نیست',
},
ruleName: {
between: 'بین {FORMULA1} و {FORMULA2} است',
greaterThan: 'بعد از {FORMULA1} است',
greaterThanOrEqual: 'روی یا بعد از {FORMULA1} است',
lessThan: 'قبل از {FORMULA1} است',
lessThanOrEqual: 'روی یا قبل از {FORMULA1} است',
equal: 'برابر است با {FORMULA1}',
notEqual: 'برابر نیست با {FORMULA1}',
notBetween: 'بین {FORMULA1} و {FORMULA2} نیست',
legal: 'یک تاریخ قانونی است',
},
errorMsg: {
between: 'مقدار باید یک تاریخ معتبر و بین {FORMULA1} و {FORMULA2} باشد',
greaterThan: 'مقدار باید یک تاریخ معتبر و بعد از {FORMULA1} باشد',
greaterThanOrEqual: 'مقدار باید یک تاریخ معتبر و روی یا بعد از {FORMULA1} باشد',
lessThan: 'مقدار باید یک تاریخ معتبر و قبل از {FORMULA1} باشد',
lessThanOrEqual: 'مقدار باید یک تاریخ معتبر و روی یا قبل از {FORMULA1} باشد',
equal: 'مقدار باید یک تاریخ معتبر و {FORMULA1} باشد',
notEqual: 'مقدار باید یک تاریخ معتبر و نه {FORMULA1} باشد',
notBetween: 'مقدار باید یک تاریخ معتبر و بین {FORMULA1} و {FORMULA2} نباشد',
legal: 'مقدار باید یک تاریخ قانونی باشد',
},
title: 'تاریخ',
},
textLength: {
errorMsg: {
between: 'طول متن باید بین {FORMULA1} و {FORMULA2} باشد',
greaterThan: 'طول متن باید بعد از {FORMULA1} باشد',
greaterThanOrEqual: 'طول متن باید روی یا بعد از {FORMULA1} باشد',
lessThan: 'طول متن باید قبل از {FORMULA1} باشد',
lessThanOrEqual: 'طول متن باید روی یا قبل از {FORMULA1} باشد',
equal: 'طول متن باید {FORMULA1} باشد',
notEqual: 'طول متن باید {FORMULA1} نباشد',
notBetween: 'طول متن باید بین {FORMULA1} و {FORMULA2} نباشد',
},
title: 'طول متن',
},
custom: {
ruleName: 'فرمول سفارشی {FORMULA1}',
title: 'فرمول سفارشی ',
error: 'محتوای این سلول با قانون اعتبارسنجی آن نقض می‌شود',
},
validFail: {
value: 'لطفا یک مقدار وارد کنید',
number: 'لطفا عدد یا فرمول را وارد کنید',
formula: 'لطفا فرمول را وارد کنید',
date: 'لطفا تاریخ یا فرمول را وارد کنید',
list: 'لطفا گزینه‌ها را وارد کنید',
listInvalid: 'منبع لیست باید یک لیست جدا شده یا یک مرجع به یک سطر یا ستون منفرد باشد',
checkboxEqual: 'برای محتوای سلول‌های علامت‌دار و علامت‌گذاری نشده، مقادیر متفاوتی وارد کنید.',
listIntersects: 'محدوده انتخاب شده نمی‌تواند با دامنه قوانین تداخل داشته باشد',
primitive: 'فرمول‌ها برای مقادیر سفارشی علامت‌دار و علامت‌گذاری نشده مجاز نیستند.',
},
any: {
title: 'هر مقدار',
error: 'محتوای این سلول نقض قانون اعتبارسنجی است',
},
list: {
title: 'لیست کشویی',
name: 'مقدار شامل یکی از محدوده است',
error: 'ورود باید در محدوده مشخص شده قرار گیرد',
},
listMultiple: {
title: 'لیست کشویی چندگانه',
},
decimal: {
title: 'عدد',
},
whole: {
title: 'عدد صحیح',
},
checkbox: {
title: 'کادر انتخاب',
error: 'محتوای این سلول با قانون اعتبارسنجی آن نقض می‌شود',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'entre',
greaterThan: 'supérieur à',
greaterThanOrEqual: 'supérieur ou égal à',
lessThan: 'inférieur à',
lessThanOrEqual: 'inférieur ou égal à',
equal: 'égal à',
notEqual: 'différent de',
notBetween: 'pas entre',
},
ruleName: {
between: 'est entre {FORMULA1} et {FORMULA2}',
greaterThan: 'est supérieur à {FORMULA1}',
greaterThanOrEqual: 'est supérieur ou égal à {FORMULA1}',
lessThan: 'est inférieur à {FORMULA1}',
lessThanOrEqual: 'est inférieur ou égal à {FORMULA1}',
equal: 'est égal à {FORMULA1}',
notEqual: 'est différent de {FORMULA1}',
notBetween: 'n\'est pas entre {FORMULA1} et {FORMULA2}',
legal: 'est un {TYPE} légal',
},
errorMsg: {
between: 'La valeur doit être entre {FORMULA1} et {FORMULA2}',
greaterThan: 'La valeur doit être supérieure à {FORMULA1}',
greaterThanOrEqual: 'La valeur doit être supérieure ou égale à {FORMULA1}',
lessThan: 'La valeur doit être inférieure à {FORMULA1}',
lessThanOrEqual: 'La valeur doit être inférieure ou égale à {FORMULA1}',
equal: 'La valeur doit être égale à {FORMULA1}',
notEqual: 'La valeur doit être différente de {FORMULA1}',
notBetween: 'La valeur ne doit pas être entre {FORMULA1} et {FORMULA2}',
legal: 'La valeur doit être un {TYPE} légal',
},
date: {
operators: {
between: 'entre',
greaterThan: 'après',
greaterThanOrEqual: 'le ou après',
lessThan: 'avant',
lessThanOrEqual: 'le ou avant',
equal: 'égal à',
notEqual: 'différent de',
notBetween: 'pas entre',
},
ruleName: {
between: 'est entre {FORMULA1} et {FORMULA2}',
greaterThan: 'est après {FORMULA1}',
greaterThanOrEqual: 'est le ou après {FORMULA1}',
lessThan: 'est avant {FORMULA1}',
lessThanOrEqual: 'est le ou avant {FORMULA1}',
equal: 'est {FORMULA1}',
notEqual: 'n\'est pas {FORMULA1}',
notBetween: 'n\'est pas entre {FORMULA1} et {FORMULA2}',
legal: 'est une date légale',
},
errorMsg: {
between: 'La valeur doit être une date légale et entre {FORMULA1} et {FORMULA2}',
greaterThan: 'La valeur doit être une date légale et après {FORMULA1}',
greaterThanOrEqual: 'La valeur doit être une date légale et le ou après {FORMULA1}',
lessThan: 'La valeur doit être une date légale et avant {FORMULA1}',
lessThanOrEqual: 'La valeur doit être une date légale et le ou avant {FORMULA1}',
equal: 'La valeur doit être une date légale et {FORMULA1}',
notEqual: 'La valeur doit être une date légale et non {FORMULA1}',
notBetween: 'La valeur doit être une date légale et non entre {FORMULA1} et {FORMULA2}',
legal: 'La valeur doit être une date légale',
},
title: 'Date',
},
textLength: {
errorMsg: {
between: 'La longueur du texte doit être entre {FORMULA1} et {FORMULA2}',
greaterThan: 'La longueur du texte doit être supérieure à {FORMULA1}',
greaterThanOrEqual: 'La longueur du texte doit être supérieure ou égale à {FORMULA1}',
lessThan: 'La longueur du texte doit être inférieure à {FORMULA1}',
lessThanOrEqual: 'La longueur du texte doit être inférieure ou égale à {FORMULA1}',
equal: 'La longueur du texte doit être {FORMULA1}',
notEqual: 'La longueur du texte doit être différente de {FORMULA1}',
notBetween: 'La longueur du texte ne doit pas être entre {FORMULA1} et {FORMULA2}',
},
title: 'Longueur du texte',
},
custom: {
ruleName: 'La formule personnalisée est {FORMULA1}',
title: 'Formule personnalisée',
error: 'Le contenu de cette cellule viole sa règle de validation',
},
validFail: {
value: 'Veuillez entrer une valeur',
number: 'Veuillez entrer un nombre ou une formule',
formula: 'Veuillez entrer une formule',
date: 'Veuillez entrer une date ou une formule',
list: 'Veuillez entrer des options',
listInvalid: 'La source de la liste doit être une liste délimitée ou une référence à une seule ligne ou colonne',
checkboxEqual: 'Entrez des valeurs différentes pour les contenus des cellules cochées et décochées.',
listIntersects: 'La plage sélectionnée ne peut pas croiser le champ des règles',
primitive: 'Les formules ne sont pas autorisées pour les valeurs personnalisées cochées et décochées.',
},
any: {
title: 'N\'importe quelle valeur',
error: 'Le contenu de cette cellule viole la règle de validation',
},
list: {
title: 'Liste déroulante',
name: 'La valeur contient une de la plage',
error: 'L\'entrée doit être dans la plage spécifiée',
},
listMultiple: {
title: 'Liste déroulante-Multiple',
},
decimal: {
title: 'Nombre',
},
whole: {
title: 'Entier',
},
checkbox: {
title: 'Case à cocher',
error: 'Le contenu de cette cellule viole sa règle de validation',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'antara',
greaterThan: 'lebih besar dari',
greaterThanOrEqual: 'lebih besar dari atau sama dengan',
lessThan: 'kurang dari',
lessThanOrEqual: 'kurang dari atau sama dengan',
equal: 'sama dengan',
notEqual: 'tidak sama dengan',
notBetween: 'tidak antara',
},
ruleName: {
between: 'Di antara {FORMULA1} dan {FORMULA2}',
greaterThan: 'Lebih besar dari {FORMULA1}',
greaterThanOrEqual: 'Lebih besar dari atau sama dengan {FORMULA1}',
lessThan: 'Kurang dari {FORMULA1}',
lessThanOrEqual: 'Kurang dari atau sama dengan {FORMULA1}',
equal: 'Sama dengan {FORMULA1}',
notEqual: 'Tidak sama dengan {FORMULA1}',
notBetween: 'Tidak di antara {FORMULA1} dan {FORMULA2}',
legal: 'Tipe {TYPE} yang sah',
},
errorMsg: {
between: 'Nilai harus di antara {FORMULA1} dan {FORMULA2}',
greaterThan: 'Nilai harus lebih besar dari {FORMULA1}',
greaterThanOrEqual: 'Nilai harus lebih besar dari atau sama dengan {FORMULA1}',
lessThan: 'Nilai harus kurang dari {FORMULA1}',
lessThanOrEqual: 'Nilai harus kurang dari atau sama dengan {FORMULA1}',
equal: 'Nilai harus sama dengan {FORMULA1}',
notEqual: 'Nilai harus tidak sama dengan {FORMULA1}',
notBetween: 'Nilai harus tidak di antara {FORMULA1} dan {FORMULA2}',
legal: 'Nilai harus tipe {TYPE} yang sah',
},
date: {
operators: {
between: 'antara',
greaterThan: 'setelah',
greaterThanOrEqual: 'pada atau setelah',
lessThan: 'sebelum',
lessThanOrEqual: 'pada atau sebelum',
equal: 'sama dengan',
notEqual: 'tidak sama dengan',
notBetween: 'tidak antara',
},
ruleName: {
between: 'antara {FORMULA1} dan {FORMULA2}',
greaterThan: 'setelah {FORMULA1}',
greaterThanOrEqual: 'pada atau setelah {FORMULA1}',
lessThan: 'sebelum {FORMULA1}',
lessThanOrEqual: 'pada atau sebelum {FORMULA1}',
equal: 'adalah {FORMULA1}',
notEqual: 'bukan {FORMULA1}',
notBetween: 'tidak antara {FORMULA1} dan {FORMULA2}',
legal: 'tanggal yang sah',
},
errorMsg: {
between: 'Nilai harus tanggal yang sah dan di antara {FORMULA1} dan {FORMULA2}',
greaterThan: 'Nilai harus tanggal yang sah dan setelah {FORMULA1}',
greaterThanOrEqual: 'Nilai harus tanggal yang sah dan pada atau setelah {FORMULA1}',
lessThan: 'Nilai harus tanggal yang sah dan sebelum {FORMULA1}',
lessThanOrEqual: 'Nilai harus tanggal yang sah dan pada atau sebelum {FORMULA1}',
equal: 'Nilai harus tanggal yang sah dan {FORMULA1}',
notEqual: 'Nilai harus tanggal yang sah dan bukan {FORMULA1}',
notBetween: 'Nilai harus tanggal yang sah dan tidak antara {FORMULA1} dan {FORMULA2}',
legal: 'Nilai harus tanggal yang sah',
},
title: 'Tanggal',
},
textLength: {
errorMsg: {
between: 'Panjang teks harus di antara {FORMULA1} dan {FORMULA2}',
greaterThan: 'Panjang teks harus lebih besar dari {FORMULA1}',
greaterThanOrEqual: 'Panjang teks harus lebih besar dari atau sama dengan {FORMULA1}',
lessThan: 'Panjang teks harus kurang dari {FORMULA1}',
lessThanOrEqual: 'Panjang teks harus kurang dari atau sama dengan {FORMULA1}',
equal: 'Panjang teks harus {FORMULA1}',
notEqual: 'Panjang teks harus tidak sama dengan {FORMULA1}',
notBetween: 'Panjang teks harus tidak di antara {FORMULA1} dan {FORMULA2}',
},
title: 'Panjang teks',
},
custom: {
ruleName: 'Rumus kustom adalah {FORMULA1}',
title: 'Rumus kustom',
error: 'Konten sel ini melanggar aturan validasinya',
},
validFail: {
value: 'Harap masukkan nilai',
number: 'Harap masukkan angka atau rumus',
formula: 'Harap masukkan rumus',
date: 'Harap masukkan tanggal atau rumus',
list: 'Harap masukkan opsi',
listInvalid: 'Sumber daftar harus berupa daftar yang dipisahkan atau referensi ke satu baris atau kolom',
checkboxEqual: 'Masukkan nilai yang berbeda untuk konten sel yang dicentang dan tidak dicentang',
listIntersects: 'Rentang yang dipilih tidak boleh berpotongan dengan cakupan aturan',
primitive: 'Rumus tidak diizinkan untuk nilai kustom yang dicentang dan tidak dicentang',
},
any: {
title: 'Nilai apa saja',
error: 'Konten sel ini melanggar aturan validasi',
},
list: {
title: 'Dropdown',
name: 'Nilai mengandung satu dari rentang',
error: 'Input harus berada dalam rentang yang ditentukan',
},
listMultiple: {
title: 'Dropdown-Multiple',
},
decimal: {
title: 'Angka',
},
whole: {
title: 'Bilangan bulat',
},
checkbox: {
title: 'Kotak centang',
error: 'Konten sel ini melanggar aturan validasinya',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'tra',
greaterThan: 'maggiore di',
greaterThanOrEqual: 'maggiore o uguale a',
lessThan: 'minore di',
lessThanOrEqual: 'minore o uguale a',
equal: 'uguale a',
notEqual: 'diverso da',
notBetween: 'non tra',
},
ruleName: {
between: 'È tra {FORMULA1} e {FORMULA2}',
greaterThan: 'È maggiore di {FORMULA1}',
greaterThanOrEqual: 'È maggiore o uguale a {FORMULA1}',
lessThan: 'È minore di {FORMULA1}',
lessThanOrEqual: 'È minore o uguale a {FORMULA1}',
equal: 'È uguale a {FORMULA1}',
notEqual: 'È diverso da {FORMULA1}',
notBetween: 'Non è tra {FORMULA1} e {FORMULA2}',
legal: 'È un {TYPE} valido',
},
errorMsg: {
between: 'Il valore deve essere tra {FORMULA1} e {FORMULA2}',
greaterThan: 'Il valore deve essere maggiore di {FORMULA1}',
greaterThanOrEqual: 'Il valore deve essere maggiore o uguale a {FORMULA1}',
lessThan: 'Il valore deve essere minore di {FORMULA1}',
lessThanOrEqual: 'Il valore deve essere minore o uguale a {FORMULA1}',
equal: 'Il valore deve essere uguale a {FORMULA1}',
notEqual: 'Il valore deve essere diverso da {FORMULA1}',
notBetween: 'Il valore non deve essere tra {FORMULA1} e {FORMULA2}',
legal: 'Il valore deve essere un {TYPE} valido',
},
date: {
operators: {
between: 'tra',
greaterThan: 'dopo',
greaterThanOrEqual: 'il giorno o dopo',
lessThan: 'prima',
lessThanOrEqual: 'il giorno o prima',
equal: 'uguale',
notEqual: 'diverso',
notBetween: 'non tra',
},
ruleName: {
between: 'è tra {FORMULA1} e {FORMULA2}',
greaterThan: 'è dopo {FORMULA1}',
greaterThanOrEqual: 'è il giorno o dopo {FORMULA1}',
lessThan: 'è prima di {FORMULA1}',
lessThanOrEqual: 'è il giorno o prima di {FORMULA1}',
equal: 'è {FORMULA1}',
notEqual: 'non è {FORMULA1}',
notBetween: 'non è tra {FORMULA1} e {FORMULA2}',
legal: 'è una data valida',
},
errorMsg: {
between: 'Il valore deve essere una data valida e tra {FORMULA1} e {FORMULA2}',
greaterThan: 'Il valore deve essere una data valida e dopo {FORMULA1}',
greaterThanOrEqual: 'Il valore deve essere una data valida e il giorno o dopo {FORMULA1}',
lessThan: 'Il valore deve essere una data valida e prima di {FORMULA1}',
lessThanOrEqual: 'Il valore deve essere una data valida e il giorno o prima di {FORMULA1}',
equal: 'Il valore deve essere una data valida e {FORMULA1}',
notEqual: 'Il valore deve essere una data valida e non {FORMULA1}',
notBetween: 'Il valore deve essere una data valida e non tra {FORMULA1} e {FORMULA2}',
legal: 'Il valore deve essere una data valida',
},
title: 'Data',
},
textLength: {
errorMsg: {
between: 'La lunghezza del testo deve essere tra {FORMULA1} e {FORMULA2}',
greaterThan: 'La lunghezza del testo deve essere maggiore di {FORMULA1}',
greaterThanOrEqual: 'La lunghezza del testo deve essere maggiore o uguale a {FORMULA1}',
lessThan: 'La lunghezza del testo deve essere minore di {FORMULA1}',
lessThanOrEqual: 'La lunghezza del testo deve essere minore o uguale a {FORMULA1}',
equal: 'La lunghezza del testo deve essere {FORMULA1}',
notEqual: 'La lunghezza del testo deve essere diversa da {FORMULA1}',
notBetween: 'La lunghezza del testo non deve essere tra {FORMULA1} e {FORMULA2}',
},
title: 'Lunghezza testo',
},
custom: {
ruleName: 'La formula personalizzata è {FORMULA1}',
title: 'Formula personalizzata',
error: 'Il contenuto di questa cella viola la regola di convalida',
},
validFail: {
value: 'Inserire un valore',
number: 'Inserire numero o formula',
formula: 'Inserire formula',
date: 'Inserire data o formula',
list: 'Inserire opzioni',
listInvalid: 'L\'origine dell\'elenco deve essere un elenco delimitato o un riferimento a una singola riga o colonna',
checkboxEqual: 'Inserire valori diversi per i contenuti delle celle selezionate e non selezionate.',
listIntersects: 'L\'intervallo selezionato non può intersecarsi con l\'ambito delle regole',
primitive: 'Le formule non sono consentite per i valori personalizzati di selezionato e non selezionato.',
},
any: {
title: 'Qualsiasi valore',
error: 'Il contenuto di questa cella viola la regola di convalida',
},
list: {
title: 'Menu a discesa',
name: 'Il valore contiene uno dall\'intervallo',
error: 'L\'input deve rientrare nell\'intervallo specificato',
},
listMultiple: {
title: 'Menu a discesa - Multiplo',
},
decimal: {
title: 'Numero',
},
whole: {
title: 'Numero intero',
},
checkbox: {
title: 'Casella di controllo',
error: 'Il contenuto di questa cella viola la regola di convalida',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: '次の値の間',
greaterThan: '次の値より大きい',
greaterThanOrEqual: '次の値以上',
lessThan: '次の値より小さい',
lessThanOrEqual: '次の値以下',
equal: '次の値に等しい',
notEqual: '次の値に等しくない',
notBetween: '次の値の間以外',
},
ruleName: {
between: '{FORMULA1} と {FORMULA2} の間',
greaterThan: '{FORMULA1} より大きい',
greaterThanOrEqual: '{FORMULA1} 以上',
lessThan: '{FORMULA1} より小さい',
lessThanOrEqual: '{FORMULA1} 以下',
equal: '{FORMULA1} に等しい',
notEqual: '{FORMULA1} に等しくない',
notBetween: '{FORMULA1} と {FORMULA2} の間以外',
legal: '有効な {TYPE} 形式',
},
errorMsg: {
between: '値は {FORMULA1} と {FORMULA2} の間である必要があります',
greaterThan: '値は {FORMULA1} より大きい必要があります',
greaterThanOrEqual: '値は {FORMULA1} 以上である必要があります',
lessThan: '値は {FORMULA1} より小さい必要があります',
lessThanOrEqual: '値は {FORMULA1} 以下である必要があります',
equal: '値は {FORMULA1} に等しい必要があります',
notEqual: '値は {FORMULA1} に等しくない必要があります',
notBetween: '値は {FORMULA1} と {FORMULA2} の間以外である必要があります',
legal: '値は有効な {TYPE} 形式である必要があります',
},
date: {
operators: {
between: '次の値の間',
greaterThan: '次の値より大きい',
greaterThanOrEqual: '次の値以上',
lessThan: '次の値より小さい',
lessThanOrEqual: '次の値以下',
equal: '次の値に等しい',
notEqual: '次の値に等しくない',
notBetween: '次の値の間以外',
},
ruleName: {
between: '{FORMULA1} と {FORMULA2} の間',
greaterThan: '{FORMULA1} より後',
greaterThanOrEqual: '{FORMULA1} 以降',
lessThan: '{FORMULA1} より前',
lessThanOrEqual: '{FORMULA1} 以前',
equal: '{FORMULA1} に等しい',
notEqual: '{FORMULA1} に等しくない',
notBetween: '{FORMULA1} と {FORMULA2} の間以外',
legal: '有効な日付',
},
errorMsg: {
between: '値は有効な日付で、{FORMULA1} と {FORMULA2} の間である必要があります',
greaterThan: '値は有効な日付で、{FORMULA1} より後である必要があります',
greaterThanOrEqual: '値は有効な日付で、{FORMULA1} 以降である必要があります',
lessThan: '値は有効な日付で、{FORMULA1} より前である必要があります',
lessThanOrEqual: '値は有効な日付で、{FORMULA1} 以前である必要があります',
equal: '値は有効な日付で、{FORMULA1} に等しい必要があります',
notEqual: '値は有効な日付で、{FORMULA1} に等しくない必要があります',
notBetween: '値は有効な日付で、{FORMULA1} と {FORMULA2} の間以外である必要があります',
legal: '値は有効な日付である必要があります',
},
title: '日付',
},
textLength: {
errorMsg: {
between: '文字列の長さは {FORMULA1} と {FORMULA2} の間である必要があります',
greaterThan: '文字列の長さは {FORMULA1} より長い必要があります',
greaterThanOrEqual: '文字列の長さは {FORMULA1} 以上である必要があります',
lessThan: '文字列の長さは {FORMULA1} より短い必要があります',
lessThanOrEqual: '文字列の長さは {FORMULA1} 以下である必要があります',
equal: '文字列の長さは {FORMULA1} に等しい必要があります',
notEqual: '文字列の長さは {FORMULA1} に等しくない必要があります',
notBetween: '文字列の長さは {FORMULA1} と {FORMULA2} の間以外である必要があります',
},
title: '文字列の長さ',
},
custom: {
ruleName: 'ユーザー設定数式: {FORMULA1}',
title: 'ユーザー設定',
error: 'このセルの内容は入力規則に違反しています',
},
validFail: {
value: '値を入力してください',
number: '数値または数式を入力してください',
formula: '数式を入力してください',
date: '日付または数式を入力してください',
list: 'リストのオプションを入力してください',
listInvalid: 'リスト元の値は、区切りリスト、または単一の行または列の参照である必要があります',
checkboxEqual: 'チェックされた値とチェックされていない値には異なる値を入力してください',
listIntersects: '選択した範囲はルール範囲と重なることはできません',
primitive: 'カスタムの選択/未選択値には数式を使用できません',
},
any: {
title: 'すべての値',
error: 'このセルの内容は入力規則に違反しています',
},
list: {
title: 'リスト',
name: 'リスト範囲内の値',
error: '入力値は指定された範囲内にある必要があります',
},
listMultiple: {
title: '複数選択リスト',
},
decimal: {
title: '小数点数',
},
whole: {
title: '整数',
},
checkbox: {
title: 'チェックボックス',
error: 'このセルの内容は入力規則に違反しています',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: '사이',
greaterThan: '초과',
greaterThanOrEqual: '이상',
lessThan: '미만',
lessThanOrEqual: '이하',
equal: '같음',
notEqual: '같지 않음',
notBetween: '사이가 아님',
},
ruleName: {
between: '{FORMULA1}와 {FORMULA2} 사이',
greaterThan: '{FORMULA1} 초과',
greaterThanOrEqual: '{FORMULA1} 이상',
lessThan: '{FORMULA1} 미만',
lessThanOrEqual: '{FORMULA1} 이하',
equal: '{FORMULA1}와 같음',
notEqual: '{FORMULA1}와 다름',
notBetween: '{FORMULA1}와 {FORMULA2} 사이 아님',
legal: '유효한 {TYPE} 형식',
},
errorMsg: {
between: '값은 {FORMULA1}와 {FORMULA2} 사이여야 합니다',
greaterThan: '값은 {FORMULA1} 초과여야 합니다',
greaterThanOrEqual: '값은 {FORMULA1} 이상이어야 합니다',
lessThan: '값은 {FORMULA1} 미만이어야 합니다',
lessThanOrEqual: '값은 {FORMULA1} 이하이어야 합니다',
equal: '값은 {FORMULA1}와 같아야 합니다',
notEqual: '값은 {FORMULA1}와 달라야 합니다',
notBetween: '값은 {FORMULA1}와 {FORMULA2} 사이가 아니어야 합니다',
legal: '값은 유효한 {TYPE} 형식이어야 합니다',
},
date: {
operators: {
between: '사이',
greaterThan: '이후',
greaterThanOrEqual: '이거나 이후',
lessThan: '이전',
lessThanOrEqual: '이거나 이전',
equal: '같음',
notEqual: '다름',
notBetween: '사이가 아님',
},
ruleName: {
between: '{FORMULA1}와 {FORMULA2} 사이',
greaterThan: '{FORMULA1} 이후',
greaterThanOrEqual: '{FORMULA1} 이거나 이후',
lessThan: '{FORMULA1} 이전',
lessThanOrEqual: '{FORMULA1} 이거나 이전',
equal: '{FORMULA1}와 같음',
notEqual: '{FORMULA1}와 다름',
notBetween: '{FORMULA1}와 {FORMULA2} 사이 아님',
legal: '유효한 날짜',
},
errorMsg: {
between: '값은 유효한 날짜이며 {FORMULA1}와 {FORMULA2} 사이여야 합니다',
greaterThan: '값은 유효한 날짜이며 {FORMULA1} 이후여야 합니다',
greaterThanOrEqual: '값은 유효한 날짜이며 {FORMULA1} 이거나 이후여야 합니다',
lessThan: '값은 유효한 날짜이며 {FORMULA1} 이전이어야 합니다',
lessThanOrEqual: '값은 유효한 날짜이며 {FORMULA1} 이거나 이전이어야 합니다',
equal: '값은 유효한 날짜이며 {FORMULA1}와 같아야 합니다',
notEqual: '값은 유효한 날짜이며 {FORMULA1}와 달라야 합니다',
notBetween: '값은 유효한 날짜이며 {FORMULA1}와 {FORMULA2} 사이가 아니어야 합니다',
legal: '값은 유효한 날짜여야 합니다',
},
title: '날짜',
},
textLength: {
errorMsg: {
between: '텍스트 길이는 {FORMULA1}와 {FORMULA2} 사이여야 합니다',
greaterThan: '텍스트 길이는 {FORMULA1} 초과여야 합니다',
greaterThanOrEqual: '텍스트 길이는 {FORMULA1} 이상이어야 합니다',
lessThan: '텍스트 길이는 {FORMULA1} 미만이어야 합니다',
lessThanOrEqual: '텍스트 길이는 {FORMULA1} 이하이어야 합니다',
equal: '텍스트 길이는 {FORMULA1}와 같아야 합니다',
notEqual: '텍스트 길이는 {FORMULA1}와 달라야 합니다',
notBetween: '텍스트 길이는 {FORMULA1}와 {FORMULA2} 사이가 아니어야 합니다',
},
title: '텍스트 길이',
},
custom: {
ruleName: '사용자 지정 수식: {FORMULA1}',
title: '사용자 지정 수식',
error: '이 셀의 내용이 유효성 검사 규칙을 위반했습니다',
},
validFail: {
value: '값을 입력하세요',
number: '숫자 또는 수식을 입력하세요',
formula: '수식을 입력하세요',
date: '날짜 또는 수식을 입력하세요',
list: '목록 옵션을 입력하세요',
listInvalid: '목록 소스는 구분된 목록이거나 단일 행 또는 열 참조여야 합니다',
checkboxEqual: '선택된 값과 선택되지 않은 값에 서로 다른 값을 입력하세요',
listIntersects: '선택한 범위가 규칙 범위와 겹칠 수 없습니다',
primitive: '사용자 지정 선택/미선택 값에는 수식을 사용할 수 없습니다',
},
any: {
title: '모든 값',
error: '이 셀의 내용이 유효성 검사 규칙을 위반했습니다',
},
list: {
title: '목록',
name: '목록 범위 내 값',
error: '입력 값이 지정된 범위 내에 있어야 합니다',
},
listMultiple: {
title: '복수 선택 목록',
},
decimal: {
title: '숫자',
},
whole: {
title: '정수',
},
checkbox: {
title: '체크박스',
error: '이 셀의 내용이 유효성 검사 규칙을 위반했습니다',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'między',
greaterThan: 'większe niż',
greaterThanOrEqual: 'większe lub równe',
lessThan: 'mniejsze niż',
lessThanOrEqual: 'mniejsze lub równe',
equal: 'równe',
notEqual: 'różne od',
notBetween: 'nie między',
},
ruleName: {
between: 'Jest między {FORMULA1} a {FORMULA2}',
greaterThan: 'Jest większe niż {FORMULA1}',
greaterThanOrEqual: 'Jest większe lub równe {FORMULA1}',
lessThan: 'Jest mniejsze niż {FORMULA1}',
lessThanOrEqual: 'Jest mniejsze lub równe {FORMULA1}',
equal: 'Jest równe {FORMULA1}',
notEqual: 'Jest różne od {FORMULA1}',
notBetween: 'Nie jest między {FORMULA1} a {FORMULA2}',
legal: 'Jest prawidłowym {TYPE}',
},
errorMsg: {
between: 'Wartość musi być między {FORMULA1} a {FORMULA2}',
greaterThan: 'Wartość musi być większa niż {FORMULA1}',
greaterThanOrEqual: 'Wartość musi być większa lub równa {FORMULA1}',
lessThan: 'Wartość musi być mniejsza niż {FORMULA1}',
lessThanOrEqual: 'Wartość musi być mniejsza lub równa {FORMULA1}',
equal: 'Wartość musi być równa {FORMULA1}',
notEqual: 'Wartość musi być różna od {FORMULA1}',
notBetween: 'Wartość nie może być między {FORMULA1} a {FORMULA2}',
legal: 'Wartość musi być prawidłowym {TYPE}',
},
date: {
operators: {
between: 'między',
greaterThan: 'po',
greaterThanOrEqual: 'w dniu lub po',
lessThan: 'przed',
lessThanOrEqual: 'w dniu lub przed',
equal: 'równe',
notEqual: 'różne od',
notBetween: 'nie między',
},
ruleName: {
between: 'jest między {FORMULA1} a {FORMULA2}',
greaterThan: 'jest po {FORMULA1}',
greaterThanOrEqual: 'jest w dniu {FORMULA1} lub po',
lessThan: 'jest przed {FORMULA1}',
lessThanOrEqual: 'jest w dniu {FORMULA1} lub przed',
equal: 'jest {FORMULA1}',
notEqual: 'nie jest {FORMULA1}',
notBetween: 'nie jest między {FORMULA1} a {FORMULA2}',
legal: 'jest prawidłową datą',
},
errorMsg: {
between: 'Wartość musi być prawidłową datą i między {FORMULA1} a {FORMULA2}',
greaterThan: 'Wartość musi być prawidłową datą i po {FORMULA1}',
greaterThanOrEqual: 'Wartość musi być prawidłową datą i w dniu {FORMULA1} lub po',
lessThan: 'Wartość musi być prawidłową datą i przed {FORMULA1}',
lessThanOrEqual: 'Wartość musi być prawidłową datą i w dniu {FORMULA1} lub przed',
equal: 'Wartość musi być prawidłową datą i {FORMULA1}',
notEqual: 'Wartość musi być prawidłową datą i nie być {FORMULA1}',
notBetween: 'Wartość musi być prawidłową datą i nie być między {FORMULA1} a {FORMULA2}',
legal: 'Wartość musi być prawidłową datą',
},
title: 'Data',
},
textLength: {
errorMsg: {
between: 'Długość tekstu musi być między {FORMULA1} a {FORMULA2}',
greaterThan: 'Długość tekstu musi być większa niż {FORMULA1}',
greaterThanOrEqual: 'Długość tekstu musi być większa lub równa {FORMULA1}',
lessThan: 'Długość tekstu musi być mniejsza niż {FORMULA1}',
lessThanOrEqual: 'Długość tekstu musi być mniejsza lub równa {FORMULA1}',
equal: 'Długość tekstu musi być równa {FORMULA1}',
notEqual: 'Długość tekstu musi być różna od {FORMULA1}',
notBetween: 'Długość tekstu nie może być między {FORMULA1} a {FORMULA2}',
},
title: 'Długość tekstu',
},
custom: {
ruleName: 'Formuła niestandardowa to {FORMULA1}',
title: 'Formuła niestandardowa',
error: 'Zawartość tej komórki narusza regułę sprawdzania poprawności',
},
validFail: {
value: 'Wprowadź wartość',
number: 'Wprowadź liczbę lub formułę',
formula: 'Wprowadź formułę',
date: 'Wprowadź datę lub formułę',
list: 'Wprowadź opcje',
listInvalid: 'Źródło listy musi być rozdzieloną listą lub odwołaniem do pojedynczego wiersza lub kolumny',
checkboxEqual: 'Wprowadź różne wartości dla zaznaczonej i niezaznaczonej zawartości komórki.',
listIntersects: 'Wybrany zakres nie może przecinać się z zakresem reguł',
primitive: 'Formuły nie są dozwolone dla niestandardowych wartości zaznaczonych i niezaznaczonych.',
},
any: {
title: 'Dowolna wartość',
error: 'Zawartość tej komórki narusza regułę sprawdzania poprawności',
},
list: {
title: 'Lista rozwijana',
name: 'Wartość zawiera element z zakresu',
error: 'Wartość musi należeć do określonego zakresu',
},
listMultiple: {
title: 'Lista rozwijana — wielokrotny wybór',
},
decimal: {
title: 'Liczba',
},
whole: {
title: 'Liczba całkowita',
},
checkbox: {
title: 'Pole wyboru',
error: 'Zawartość tej komórki narusza regułę sprawdzania poprawności',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'entre',
greaterThan: 'maior que',
greaterThanOrEqual: 'maior ou igual',
lessThan: 'menor que',
lessThanOrEqual: 'menor ou igual',
equal: 'igual',
notEqual: 'diferente',
notBetween: 'não está entre',
},
ruleName: {
between: 'Está entre {FORMULA1} e {FORMULA2}',
greaterThan: 'É maior que {FORMULA1}',
greaterThanOrEqual: 'É maior ou igual a {FORMULA1}',
lessThan: 'É menor que {FORMULA1}',
lessThanOrEqual: 'É menor ou igual a {FORMULA1}',
equal: 'É igual a {FORMULA1}',
notEqual: 'É diferente de {FORMULA1}',
notBetween: 'Não está entre {FORMULA1} e {FORMULA2}',
legal: 'É um {TYPE} válido',
},
errorMsg: {
between: 'O valor deve estar entre {FORMULA1} e {FORMULA2}',
greaterThan: 'O valor deve ser maior que {FORMULA1}',
greaterThanOrEqual: 'O valor deve ser maior ou igual a {FORMULA1}',
lessThan: 'O valor deve ser menor que {FORMULA1}',
lessThanOrEqual: 'O valor deve ser menor ou igual a {FORMULA1}',
equal: 'O valor deve ser igual a {FORMULA1}',
notEqual: 'O valor deve ser diferente de {FORMULA1}',
notBetween: 'O valor não deve estar entre {FORMULA1} e {FORMULA2}',
legal: 'O valor deve ser um {TYPE} válido',
},
date: {
operators: {
between: 'entre',
greaterThan: 'depois de',
greaterThanOrEqual: 'em ou depois de',
lessThan: 'antes de',
lessThanOrEqual: 'em ou antes de',
equal: 'igual',
notEqual: 'diferente',
notBetween: 'não está entre',
},
ruleName: {
between: 'está entre {FORMULA1} e {FORMULA2}',
greaterThan: 'é depois de {FORMULA1}',
greaterThanOrEqual: 'é em ou depois de {FORMULA1}',
lessThan: 'é antes de {FORMULA1}',
lessThanOrEqual: 'é em ou antes de {FORMULA1}',
equal: 'é {FORMULA1}',
notEqual: 'não é {FORMULA1}',
notBetween: 'não está entre {FORMULA1} e {FORMULA2}',
legal: 'é uma data válida',
},
errorMsg: {
between: 'O valor deve ser uma data válida e estar entre {FORMULA1} e {FORMULA2}',
greaterThan: 'O valor deve ser uma data válida e ser depois de {FORMULA1}',
greaterThanOrEqual: 'O valor deve ser uma data válida e ser em ou depois de {FORMULA1}',
lessThan: 'O valor deve ser uma data válida e ser antes de {FORMULA1}',
lessThanOrEqual: 'O valor deve ser uma data válida e ser em ou antes de {FORMULA1}',
equal: 'O valor deve ser uma data válida e {FORMULA1}',
notEqual: 'O valor deve ser uma data válida e não ser {FORMULA1}',
notBetween: 'O valor deve ser uma data válida e não estar entre {FORMULA1} e {FORMULA2}',
legal: 'O valor deve ser uma data válida',
},
title: 'Data',
},
textLength: {
errorMsg: {
between: 'O comprimento do texto deve estar entre {FORMULA1} e {FORMULA2}',
greaterThan: 'O comprimento do texto deve ser maior que {FORMULA1}',
greaterThanOrEqual: 'O comprimento do texto deve ser maior ou igual a {FORMULA1}',
lessThan: 'O comprimento do texto deve ser menor que {FORMULA1}',
lessThanOrEqual: 'O comprimento do texto deve ser menor ou igual a {FORMULA1}',
equal: 'O comprimento do texto deve ser {FORMULA1}',
notEqual: 'O comprimento do texto não deve ser {FORMULA1}',
notBetween: 'O comprimento do texto não deve estar entre {FORMULA1} e {FORMULA2}',
},
title: 'Comprimento do texto',
},
custom: {
ruleName: 'A fórmula personalizada é {FORMULA1}',
title: 'Fórmula personalizada',
error: 'O conteúdo desta célula viola sua regra de validação',
},
validFail: {
value: 'Por favor, insira um valor',
number: 'Por favor, insira um número ou fórmula',
formula: 'Por favor, insira uma fórmula',
date: 'Por favor, insira uma data ou fórmula',
list: 'Por favor, insira opções',
listInvalid: 'A fonte da lista deve ser uma lista delimitada ou uma referência a uma única linha ou coluna',
checkboxEqual: 'Insira valores diferentes para o conteúdo das células marcadas e desmarcadas.',
listIntersects: 'O intervalo selecionado não pode intersectar com o escopo das regras',
primitive: 'Fórmulas não são permitidas para valores personalizados de marcado e desmarcado.',
},
any: {
title: 'Qualquer valor',
error: 'O conteúdo desta célula viola a regra de validação',
},
list: {
title: 'Lista suspensa',
name: 'O valor contém um do intervalo',
error: 'A entrada deve estar dentro do intervalo especificado',
},
listMultiple: {
title: 'Lista suspensa - Múltipla',
},
decimal: {
title: 'Número',
},
whole: {
title: 'Número inteiro',
},
checkbox: {
title: 'Caixa de seleção',
error: 'O conteúdo desta célula viola sua regra de validação',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'между',
greaterThan: 'больше чем',
greaterThanOrEqual: 'больше или равно',
lessThan: 'меньше чем',
lessThanOrEqual: 'меньше или равно',
equal: 'равно',
notEqual: 'не равно',
notBetween: 'не между',
},
ruleName: {
between: 'между {FORMULA1} и {FORMULA2}',
greaterThan: 'больше чем {FORMULA1}',
greaterThanOrEqual: 'больше или равно {FORMULA1}',
lessThan: 'меньше чем {FORMULA1}',
lessThanOrEqual: 'меньше или равно {FORMULA1}',
equal: 'равно {FORMULA1}',
notEqual: 'не равно {FORMULA1}',
notBetween: 'не между {FORMULA1} и {FORMULA2}',
legal: 'является допустимым {TYPE}',
},
errorMsg: {
between: 'Значение должно быть между {FORMULA1} и {FORMULA2}',
greaterThan: 'Значение должно быть больше {FORMULA1}',
greaterThanOrEqual: 'Значение должно быть больше или равно {FORMULA1}',
lessThan: 'Значение должно быть меньше {FORMULA1}',
lessThanOrEqual: 'Значение должно быть меньше или равно {FORMULA1}',
equal: 'Значение должно быть равно {FORMULA1}',
notEqual: 'Значение должно быть не равно {FORMULA1}',
notBetween: 'Значение должно быть не между {FORMULA1} и {FORMULA2}',
legal: 'Значение должно быть допустимым {TYPE}',
},
date: {
operators: {
between: 'между',
greaterThan: 'после',
greaterThanOrEqual: 'в или после',
lessThan: 'до',
lessThanOrEqual: 'в или до',
equal: 'равно',
notEqual: 'не равно',
notBetween: 'не между',
},
ruleName: {
between: 'между {FORMULA1} и {FORMULA2}',
greaterThan: 'после {FORMULA1}',
greaterThanOrEqual: 'в или после {FORMULA1}',
lessThan: 'до {FORMULA1}',
lessThanOrEqual: 'в или до {FORMULA1}',
equal: 'равно {FORMULA1}',
notEqual: 'не равно {FORMULA1}',
notBetween: 'не между {FORMULA1} и {FORMULA2}',
legal: 'является допустимой датой',
},
errorMsg: {
between: 'Значение должно быть между {FORMULA1} и {FORMULA2}',
greaterThan: 'Значение должно быть после {FORMULA1}',
greaterThanOrEqual: 'Значение должно быть в или после {FORMULA1}',
lessThan: 'Значение должно быть до {FORMULA1}',
lessThanOrEqual: 'Значение должно быть в или до {FORMULA1}',
equal: 'Значение должно быть {FORMULA1}',
notEqual: 'Значение должно быть не {FORMULA1}',
notBetween: 'Значение должно быть не между {FORMULA1} и {FORMULA2}',
legal: 'Значение должно быть допустимой датой',
},
title: 'Дата',
},
textLength: {
errorMsg: {
between: 'Длина текста должна быть между {FORMULA1} и {FORMULA2}',
greaterThan: 'Длина текста должна быть больше {FORMULA1}',
greaterThanOrEqual: 'Длина текста должна быть больше или равна {FORMULA1}',
lessThan: 'Длина текста должна быть меньше {FORMULA1}',
lessThanOrEqual: 'Длина текста должна быть меньше или равна {FORMULA1}',
equal: 'Длина текста должна быть равна {FORMULA1}',
notEqual: 'Длина текста должна быть не равна {FORMULA1}',
notBetween: 'Длина текста должна быть не между {FORMULA1} и {FORMULA2}',
},
title: 'Длина текста',
},
custom: {
ruleName: 'Пользовательская формула {FORMULA1}',
title: 'Пользовательская формула',
error: 'Содержимое этой ячейки нарушает правило проверки',
},
validFail: {
value: 'Пожалуйста, введите значение',
number: 'Пожалуйста, введите число или формулу',
formula: 'Пожалуйста, введите формулу',
date: 'Пожалуйста, введите дату или формулу',
list: 'Пожалуйста, введите параметры',
listInvalid: 'Источник списка должен быть разделенным списком или ссылкой на одну строку или столбец',
checkboxEqual: 'Для выбранных и не выбранных ячеек введите разные значения',
listIntersects: 'Выбранный диапазон не может пересекаться с диапазоном правила.',
primitive: 'Формулы не разрешены для пользовательских отмеченных и неотмеченных значений.',
},
any: {
title: 'Любое значение',
error: 'Содержимое этой ячейки нарушает правило проверки',
},
list: {
title: 'Выпадающий список',
name: 'Значение содержит одно из диапазона',
error: 'Ввод должен соответствовать указанному диапазону',
},
listMultiple: {
title: 'Выпадающий список - Множественный',
},
decimal: {
title: 'Число',
},
whole: {
title: 'Целое число',
},
checkbox: {
title: 'Флажок',
error: 'Содержимое этой ячейки нарушает правило проверки',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'medzi',
greaterThan: 'väčšie ako',
greaterThanOrEqual: 'väčšie alebo rovné',
lessThan: 'menšie ako',
lessThanOrEqual: 'menšie alebo rovné',
equal: 'rovné',
notEqual: 'nerovné',
notBetween: 'nie medzi',
},
ruleName: {
between: 'Je medzi {FORMULA1} a {FORMULA2}',
greaterThan: 'Je väčšie ako {FORMULA1}',
greaterThanOrEqual: 'Je väčšie alebo rovné {FORMULA1}',
lessThan: 'Je menšie ako {FORMULA1}',
lessThanOrEqual: 'Je menšie alebo rovné {FORMULA1}',
equal: 'Je rovné {FORMULA1}',
notEqual: 'Nie je rovné {FORMULA1}',
notBetween: 'Nie je medzi {FORMULA1} a {FORMULA2}',
legal: 'Je platný {TYPE}',
},
errorMsg: {
between: 'Hodnota musí byť medzi {FORMULA1} a {FORMULA2}',
greaterThan: 'Hodnota musí byť väčšia ako {FORMULA1}',
greaterThanOrEqual: 'Hodnota musí byť väčšia alebo rovná {FORMULA1}',
lessThan: 'Hodnota musí byť menšia ako {FORMULA1}',
lessThanOrEqual: 'Hodnota musí byť menšia alebo rovná {FORMULA1}',
equal: 'Hodnota musí byť rovná {FORMULA1}',
notEqual: 'Hodnota nesmie byť rovná {FORMULA1}',
notBetween: 'Hodnota nesmie byť medzi {FORMULA1} a {FORMULA2}',
legal: 'Hodnota musí byť platný {TYPE}',
},
date: {
operators: {
between: 'medzi',
greaterThan: 'po',
greaterThanOrEqual: 'v alebo po',
lessThan: 'pred',
lessThanOrEqual: 'v alebo pred',
equal: 'rovné',
notEqual: 'nerovné',
notBetween: 'nie medzi',
},
ruleName: {
between: 'je medzi {FORMULA1} a {FORMULA2}',
greaterThan: 'je po {FORMULA1}',
greaterThanOrEqual: 'je v alebo po {FORMULA1}',
lessThan: 'je pred {FORMULA1}',
lessThanOrEqual: 'je v alebo pred {FORMULA1}',
equal: 'je {FORMULA1}',
notEqual: 'nie je {FORMULA1}',
notBetween: 'nie je medzi {FORMULA1} a {FORMULA2}',
legal: 'je platný dátum',
},
errorMsg: {
between: 'Hodnota musí byť platný dátum a medzi {FORMULA1} a {FORMULA2}',
greaterThan: 'Hodnota musí byť platný dátum a po {FORMULA1}',
greaterThanOrEqual: 'Hodnota musí byť platný dátum a v alebo po {FORMULA1}',
lessThan: 'Hodnota musí byť platný dátum a pred {FORMULA1}',
lessThanOrEqual: 'Hodnota musí byť platný dátum a v alebo pred {FORMULA1}',
equal: 'Hodnota musí byť platný dátum a {FORMULA1}',
notEqual: 'Hodnota musí byť platný dátum a nie {FORMULA1}',
notBetween: 'Hodnota musí byť platný dátum a nie medzi {FORMULA1} a {FORMULA2}',
legal: 'Hodnota musí byť platný dátum',
},
title: 'Dátum',
},
textLength: {
errorMsg: {
between: 'Dĺžka textu musí byť medzi {FORMULA1} a {FORMULA2}',
greaterThan: 'Dĺžka textu musí byť po {FORMULA1}',
greaterThanOrEqual: 'Dĺžka textu musí byť v alebo po {FORMULA1}',
lessThan: 'Dĺžka textu musí byť pred {FORMULA1}',
lessThanOrEqual: 'Dĺžka textu musí byť v alebo pred {FORMULA1}',
equal: 'Dĺžka textu musí byť {FORMULA1}',
notEqual: 'Dĺžka textu nesmie byť {FORMULA1}',
notBetween: 'Dĺžka textu nesmie byť medzi {FORMULA1} a {FORMULA2}',
},
title: 'Dĺžka textu',
},
custom: {
ruleName: 'Vlastný vzorec je {FORMULA1}',
title: 'Vlastný vzorec',
error: 'Obsah tejto bunky porušuje pravidlo overenia',
},
validFail: {
value: 'Zadajte hodnotu',
number: 'Zadajte číslo alebo vzorec',
formula: 'Zadajte vzorec',
date: 'Zadajte dátum alebo vzorec',
list: 'Zadajte možnosti',
listInvalid: 'Zdroj zoznamu musí byť zoznam oddelený oddeľovačmi alebo odkaz na jeden riadok alebo stĺpec',
checkboxEqual: 'Zadajte odlišné hodnoty pre označený a neoznačený obsah bunky.',
listIntersects: 'Vybraný rozsah sa nemôže prekrývať s rozsahom pravidiel',
primitive: 'Vzorce nie sú povolené pre vlastné hodnoty označené/neoznačené.',
},
any: {
title: 'Ľubovoľná hodnota',
error: 'Obsah tejto bunky porušuje pravidlo overenia',
},
list: {
title: 'Rozbaľovací zoznam',
name: 'Hodnota obsahuje jednu z rozsahu',
error: 'Vstup musí spadať do zadaného rozsahu',
},
listMultiple: {
title: 'Rozbaľovací zoznam – viacnásobný',
},
decimal: {
title: 'Číslo',
},
whole: {
title: 'Celé číslo',
},
checkbox: {
title: 'Začiarkavacie políčko',
error: 'Obsah tejto bunky porušuje pravidlo overenia',
},
},
};
export default locale;

View File

@@ -0,0 +1,20 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { LocaleLeafKeys } from '@univerjs/core';
import type enUS from './en-US';
export type LocaleKey = LocaleLeafKeys<typeof enUS>;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: 'Giữa',
greaterThan: 'Lớn hơn',
greaterThanOrEqual: 'Lớn hơn hoặc bằng',
lessThan: 'Nhỏ hơn',
lessThanOrEqual: 'Nhỏ hơn hoặc bằng',
equal: 'Bằng',
notEqual: 'Không bằng',
notBetween: 'Không nằm giữa',
},
ruleName: {
between: 'Giữa {FORMULA1} và {FORMULA2}',
greaterThan: 'Lớn hơn {FORMULA1}',
greaterThanOrEqual: 'Lớn hơn hoặc bằng {FORMULA1}',
lessThan: 'Nhỏ hơn {FORMULA1}',
lessThanOrEqual: 'Nhỏ hơn hoặc bằng {FORMULA1}',
equal: 'Bằng {FORMULA1}',
notEqual: 'Không bằng {FORMULA1}',
notBetween: 'Không nằm giữa {FORMULA1} và {FORMULA2}',
legal: 'là một {TYPE} hợp lệ',
},
errorMsg: {
between: 'Giá trị phải nằm giữa {FORMULA1} và {FORMULA2}',
greaterThan: 'Giá trị phải lớn hơn {FORMULA1}',
greaterThanOrEqual: 'Giá trị phải lớn hơn hoặc bằng {FORMULA1}',
lessThan: 'Giá trị phải nhỏ hơn {FORMULA1}',
lessThanOrEqual: 'Giá trị phải nhỏ hơn hoặc bằng {FORMULA1}',
equal: 'Giá trị phải bằng {FORMULA1}',
notEqual: 'Giá trị phải không bằng {FORMULA1}',
notBetween: 'Giá trị phải không nằm giữa {FORMULA1} và {FORMULA2}',
legal: 'Giá trị phải là một {TYPE} hợp lệ',
},
date: {
operators: {
between: 'Giữa',
greaterThan: 'Sau',
greaterThanOrEqual: 'Sau hoặc bằng',
lessThan: 'Trước',
lessThanOrEqual: 'Trước hoặc bằng',
equal: 'Bằng',
notEqual: 'Không bằng',
notBetween: 'Không nằm giữa',
},
ruleName: {
between: 'Giữa {FORMULA1} và {FORMULA2}',
greaterThan: 'Sau {FORMULA1}',
greaterThanOrEqual: 'Sau hoặc bằng {FORMULA1}',
lessThan: 'Trước {FORMULA1}',
lessThanOrEqual: 'Trước hoặc bằng {FORMULA1}',
equal: 'Bằng {FORMULA1}',
notEqual: 'Không bằng {FORMULA1}',
notBetween: 'Không nằm giữa {FORMULA1} và {FORMULA2}',
legal: 'là một ngày hợp lệ',
},
errorMsg: {
between: 'Ngày phải nằm giữa {FORMULA1} và {FORMULA2}',
greaterThan: 'Ngày phải sau {FORMULA1}',
greaterThanOrEqual: 'Ngày phải sau hoặc bằng {FORMULA1}',
lessThan: 'Ngày phải trước {FORMULA1}',
lessThanOrEqual: 'Ngày phải trước hoặc bằng {FORMULA1}',
equal: 'Ngày phải bằng {FORMULA1}',
notEqual: 'Ngày phải không bằng {FORMULA1}',
notBetween: 'Ngày phải không nằm giữa {FORMULA1} và {FORMULA2}',
legal: 'Giá trị phải là một ngày hợp lệ',
},
title: 'Ngày',
},
textLength: {
errorMsg: {
between: 'Độ dài văn bản phải nằm giữa {FORMULA1} và {FORMULA2}',
greaterThan: 'Độ dài văn bản phải lớn hơn {FORMULA1}',
greaterThanOrEqual: 'Độ dài văn bản phải lớn hơn hoặc bằng {FORMULA1}',
lessThan: 'Độ dài văn bản phải nhỏ hơn {FORMULA1}',
lessThanOrEqual: 'Độ dài văn bản phải nhỏ hơn hoặc bằng {FORMULA1}',
equal: 'Độ dài văn bản phải bằng {FORMULA1}',
notEqual: 'Độ dài văn bản phải không bằng {FORMULA1}',
notBetween: 'Độ dài văn bản phải không nằm giữa {FORMULA1} và {FORMULA2}',
},
title: 'Độ dài văn bản',
},
custom: {
ruleName: 'Công thức tùy chỉnh {FORMULA1}',
title: 'Công thức tùy chỉnh',
error: 'Nội dung của ô này vi phạm quy tắc xác thực',
},
validFail: {
value: 'Vui lòng nhập một giá trị hợp lệ',
number: 'Vui lòng nhập một số hợp lệ hoặc công thức',
formula: 'Vui lòng nhập một công thức hợp lệ',
date: 'Vui lòng nhập một ngày hợp lệ hoặc công thức',
list: 'Vui lòng nhập ít nhất một tùy chọn hợp lệ',
listInvalid: 'Nguồn danh sách phải là một danh sách phân tách hoặc một tham chiếu đến một hàng hoặc cột đơn.',
checkboxEqual: 'Nhập các giá trị khác nhau cho các ô được chọn và không được chọn.',
listIntersects: 'Phạm vi đã chọn không thể giao với phạm vi quy tắc',
primitive: 'Các giá trị tùy chỉnh cho ô được chọn và không được chọn không được phép sử dụng công thức.',
},
any: {
title: 'Bất kỳ giá trị',
error: 'Nội dung của ô này vi phạm quy tắc xác thực',
},
list: {
title: 'Danh sách thả xuống',
name: 'Giá trị phải nằm trong danh sách',
error: 'Đầu vào phải nằm trong phạm vi chỉ định',
},
listMultiple: {
title: 'Danh sách thả xuống - Chọn nhiều',
},
decimal: {
title: 'Số thập phân',
},
whole: {
title: 'Số nguyên',
},
checkbox: {
title: 'Hộp kiểm',
error: 'Nội dung của ô này vi phạm quy tắc xác thực',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: '介于',
greaterThan: '大于',
greaterThanOrEqual: '大于或等于',
lessThan: '小于',
lessThanOrEqual: '小于或等于',
equal: '等于',
notEqual: '不等于',
notBetween: '未介于',
},
ruleName: {
between: '介于 {FORMULA1} 和 {FORMULA2} 之间',
greaterThan: '大于 {FORMULA1}',
greaterThanOrEqual: '大于或等于 {FORMULA1}',
lessThan: '小于 {FORMULA1}',
lessThanOrEqual: '小于或等于 {FORMULA1}',
equal: '等于 {FORMULA1}',
notEqual: '不等于 {FORMULA1}',
notBetween: '在 {FORMULA1} 和 {FORMULA2} 范围之外',
legal: '是一个合法的 {TYPE}',
},
errorMsg: {
between: '值必须介于 {FORMULA1} 和 {FORMULA2} 之间',
greaterThan: '值必须大于 {FORMULA1}',
greaterThanOrEqual: '值必须大于或等于 {FORMULA1}',
lessThan: '值必须小于 {FORMULA1}',
lessThanOrEqual: '值必须小于或等于 {FORMULA1}',
equal: '值必须等于 {FORMULA1}',
notEqual: '值必须不等于 {FORMULA1}',
notBetween: '值必须在 {FORMULA1} 和 {FORMULA2} 范围之外',
legal: '值必须是一个合法的 {TYPE}',
},
date: {
operators: {
between: '介于',
greaterThan: '晚于',
greaterThanOrEqual: '晚于或等于',
lessThan: '早于',
lessThanOrEqual: '早于或等于',
equal: '等于',
notEqual: '不等于',
notBetween: '未介于',
},
ruleName: {
between: '介于 {FORMULA1} 和 {FORMULA2} 之间',
greaterThan: '晚于 {FORMULA1}',
greaterThanOrEqual: '晚于或等于 {FORMULA1}',
lessThan: '早于 {FORMULA1}',
lessThanOrEqual: '早于或等于 {FORMULA1}',
equal: '等于 {FORMULA1}',
notEqual: '不等于 {FORMULA1}',
notBetween: '在 {FORMULA1} 和 {FORMULA2} 范围之外',
legal: '是一个合法的日期',
},
errorMsg: {
between: '必须为有效日期且介于 {FORMULA1} 和 {FORMULA2} 之间',
greaterThan: '必须为有效日期且晚于 {FORMULA1}',
greaterThanOrEqual: '必须为有效日期且晚于或等于 {FORMULA1}',
lessThan: '必须为有效日期且早于 {FORMULA1}',
lessThanOrEqual: '必须为有效日期且早于或等于 {FORMULA1}',
equal: '必须为有效日期且等于 {FORMULA1}',
notEqual: '必须为有效日期且不等于 {FORMULA1}',
notBetween: '必须为有效日期且在 {FORMULA1} 和 {FORMULA2} 范围之外',
legal: '值必须是一个合法的日期',
},
title: '日期',
},
textLength: {
errorMsg: {
between: '文本长度必须介于 {FORMULA1} 和 {FORMULA2} 之间',
greaterThan: '文本长度必须大于 {FORMULA1}',
greaterThanOrEqual: '文本长度必须大于或等于 {FORMULA1}',
lessThan: '文本长度必须小于 {FORMULA1}',
lessThanOrEqual: '文本长度必须小于或等于 {FORMULA1}',
equal: '文本长度必须等于 {FORMULA1}',
notEqual: '文本长度必须不等于 {FORMULA1}',
notBetween: '文本长度必须在 {FORMULA1} 和 {FORMULA2} 范围之外',
},
title: '文本长度',
},
custom: {
ruleName: '自定义公式 {FORMULA1}',
title: '自定义公式',
error: '此单元格的内容违反了验证规则',
},
validFail: {
value: '请输入一个合法值',
number: '请输入合法的数字或公式',
formula: '请输入合法的公式',
date: '请输入合法的日期或公式',
list: '请输入至少一个合法选项',
listInvalid: '列表源必须是分隔列表或对单行或列的引用。',
checkboxEqual: '为勾选和未勾选的单元格内容输入不同的值。',
listIntersects: '所选范围不能和规则范围相交',
primitive: '自定义勾选和未勾选值不允许使用公式。',
},
any: {
title: '任意值',
error: '此单元格的内容违反了验证规则',
},
list: {
title: '下拉菜单',
name: '值必须是列表中的值',
error: '输入必须在指定的范围内',
},
listMultiple: {
title: '下拉菜单-多选',
},
decimal: {
title: '数字',
},
whole: {
title: '整数',
},
checkbox: {
title: '复选框',
error: '此单元格的内容违反了验证规则',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: '介於',
greaterThan: '大於',
greaterThanOrEqual: '大於或等於',
lessThan: '小於',
lessThanOrEqual: '小於或等於',
equal: '等於',
notEqual: '不等於',
notBetween: '未介於',
},
ruleName: {
between: '介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '大於 {FORMULA1}',
greaterThanOrEqual: '大於或等於 {FORMULA1}',
lessThan: '小於 {FORMULA1}',
lessThanOrEqual: '小於或等於 {FORMULA1}',
equal: '等於 {FORMULA1}',
notEqual: '不等於 {FORMULA1}',
notBetween: '在 {FORMULA1} 和 {FORMULA2} 範圍之外',
legal: '是一個合法的 {TYPE}',
},
errorMsg: {
between: '值必須介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '值必須大於 {FORMULA1}',
greaterThanOrEqual: '值必須大於或等於 {FORMULA1}',
lessThan: '值必須小於 {FORMULA1}',
lessThanOrEqual: '值必須小於或等於 {FORMULA1}',
equal: '值必須等於 {FORMULA1}',
notEqual: '值必須不等於 {FORMULA1}',
notBetween: '值必須在 {FORMULA1} 和 {FORMULA2} 範圍之外',
legal: '值必須是一個合法的 {TYPE}',
},
date: {
operators: {
between: '介於',
greaterThan: '晚於',
greaterThanOrEqual: '晚於或等於',
lessThan: '早於',
lessThanOrEqual: '早於或等於',
equal: '等於',
notEqual: '不等於',
notBetween: '未介於',
},
ruleName: {
between: '介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '晚於 {FORMULA1}',
greaterThanOrEqual: '晚於或等於 {FORMULA1}',
lessThan: '早於 {FORMULA1}',
lessThanOrEqual: '早於或等於 {FORMULA1}',
equal: '等於 {FORMULA1}',
notEqual: '不等於 {FORMULA1}',
notBetween: '在 {FORMULA1} 和 {FORMULA2} 範圍之外',
legal: '是一個合法的日期',
},
errorMsg: {
between: '日期必須介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '日期必須晚於 {FORMULA1}',
greaterThanOrEqual: '日期必須晚於或等於 {FORMULA1}',
lessThan: '日期必須早於 {FORMULA1}',
lessThanOrEqual: '早於或等於 {FORMULA1}',
equal: '日期必須等於 {FORMULA1}',
notEqual: '日期必須不等於 {FORMULA1}',
notBetween: '日期必須在 {FORMULA1} 和 {FORMULA2} 範圍之外',
legal: '值必須是一個合法的日期',
},
title: '日期',
},
textLength: {
errorMsg: {
between: '文字長度必須介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '文字長度必須大於 {FORMULA1}',
greaterThanOrEqual: '文字長度必須大於或等於 {FORMULA1}',
lessThan: '文字長度必須小於 {FORMULA1}',
lessThanOrEqual: '文字長度必須小於或等於 {FORMULA1}',
equal: '文字長度必須等於 {FORMULA1}',
notEqual: '文字長度必須不等於 {FORMULA1}',
notBetween: '文字長度必須在 {FORMULA1} 和 {FORMULA2} 範圍之外',
},
title: '文字長度',
},
custom: {
ruleName: '自訂公式 {FORMULA1}',
title: '自訂公式',
error: '此儲存格的內容違反了驗證規則',
},
validFail: {
value: '請輸入一個合法值',
number: '請輸入合法的數字或公式',
formula: '請輸入合法的公式',
date: '請輸入合法的日期或公式',
list: '請輸入至少一個合法選項',
listInvalid: '清單來源必須是分隔清單或單列或欄的參考。 ',
checkboxEqual: '為勾選和未勾選的儲存格內容輸入不同的值。 ',
listIntersects: '所選範圍不能和規則範圍相交',
primitive: '自訂勾選和未勾選值不允許使用公式。 ',
},
any: {
title: '任意值',
error: '此儲存格的內容違反了驗證規則',
},
list: {
title: '下拉選單',
name: '值必須是列表中的值',
error: '輸入必須在指定的範圍內',
},
listMultiple: {
title: '下拉式選單-多選',
},
decimal: {
title: '數字',
},
whole: {
title: '整數',
},
checkbox: {
title: '複選框',
error: '此儲存格的內容違反了驗證規則',
},
},
};
export default locale;

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 enUS from './en-US';
const locale: typeof enUS = {
'sheets-data-validation': {
operators: {
between: '介於',
greaterThan: '大於',
greaterThanOrEqual: '大於或等於',
lessThan: '小於',
lessThanOrEqual: '小於或等於',
equal: '等於',
notEqual: '不等於',
notBetween: '未介於',
},
ruleName: {
between: '介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '大於 {FORMULA1}',
greaterThanOrEqual: '大於或等於 {FORMULA1}',
lessThan: '小於 {FORMULA1}',
lessThanOrEqual: '小於或等於 {FORMULA1}',
equal: '等於 {FORMULA1}',
notEqual: '不等於 {FORMULA1}',
notBetween: '在 {FORMULA1} 和 {FORMULA2} 範圍之外',
legal: '是一個合法的 {TYPE}',
},
errorMsg: {
between: '值必須介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '值必須大於 {FORMULA1}',
greaterThanOrEqual: '值必須大於或等於 {FORMULA1}',
lessThan: '值必須小於 {FORMULA1}',
lessThanOrEqual: '值必須小於或等於 {FORMULA1}',
equal: '值必須等於 {FORMULA1}',
notEqual: '值必須不等於 {FORMULA1}',
notBetween: '值必須在 {FORMULA1} 和 {FORMULA2} 範圍之外',
legal: '值必須是一個合法的 {TYPE}',
},
date: {
operators: {
between: '介於',
greaterThan: '晚於',
greaterThanOrEqual: '晚於或等於',
lessThan: '早於',
lessThanOrEqual: '早於或等於',
equal: '等於',
notEqual: '不等於',
notBetween: '未介於',
},
ruleName: {
between: '介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '晚於 {FORMULA1}',
greaterThanOrEqual: '晚於或等於 {FORMULA1}',
lessThan: '早於 {FORMULA1}',
lessThanOrEqual: '早於或等於 {FORMULA1}',
equal: '等於 {FORMULA1}',
notEqual: '不等於 {FORMULA1}',
notBetween: '在 {FORMULA1} 和 {FORMULA2} 範圍之外',
legal: '是一個合法的日期',
},
errorMsg: {
between: '日期必須介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '日期必須晚於 {FORMULA1}',
greaterThanOrEqual: '日期必須晚於或等於 {FORMULA1}',
lessThan: '日期必須早於 {FORMULA1}',
lessThanOrEqual: '早於或等於 {FORMULA1}',
equal: '日期必須等於 {FORMULA1}',
notEqual: '日期必須不等於 {FORMULA1}',
notBetween: '日期必須在 {FORMULA1} 和 {FORMULA2} 範圍之外',
legal: '值必須是一個合法的日期',
},
title: '日期',
},
textLength: {
errorMsg: {
between: '文字長度必須介於 {FORMULA1} 和 {FORMULA2} 之間',
greaterThan: '文字長度必須大於 {FORMULA1}',
greaterThanOrEqual: '文字長度必須大於或等於 {FORMULA1}',
lessThan: '文字長度必須小於 {FORMULA1}',
lessThanOrEqual: '文字長度必須小於或等於 {FORMULA1}',
equal: '文字長度必須等於 {FORMULA1}',
notEqual: '文字長度必須不等於 {FORMULA1}',
notBetween: '文字長度必須在 {FORMULA1} 和 {FORMULA2} 範圍之外',
},
title: '文字長度',
},
custom: {
ruleName: '自訂公式 {FORMULA1}',
title: '自訂公式',
error: '此儲存格的內容違反了驗證規則',
},
validFail: {
value: '請輸入一個合法值',
number: '請輸入合法的數字或公式',
formula: '請輸入合法的公式',
date: '請輸入合法的日期或公式',
list: '請輸入至少一個合法選項',
listInvalid: '清單來源必須是分隔清單或單列或欄的參考。 ',
checkboxEqual: '為勾選和未勾選的儲存格內容輸入不同的值。 ',
listIntersects: '所選範圍不能和規則範圍相交',
primitive: '自訂勾選和未勾選值不允許使用公式。 ',
},
any: {
title: '任意值',
error: '此儲存格的內容違反了驗證規則',
},
list: {
title: '下拉選單',
name: '值必須是列表中的值',
error: '輸入必須在指定的範圍內',
},
listMultiple: {
title: '下拉式選單-多選',
},
decimal: {
title: '數字',
},
whole: {
title: '整數',
},
checkbox: {
title: '複選框',
error: '此儲存格的內容違反了驗證規則',
},
},
};
export default locale;

View File

@@ -0,0 +1,145 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IRange, ISheetDataValidationRule, IUniverInstanceService, Workbook, Worksheet } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { RuleMatrix } from '../rule-matrix';
function createRule(uid: string, ranges: IRange[]): ISheetDataValidationRule {
return {
uid,
type: 'decimal',
ranges,
} as ISheetDataValidationRule;
}
function createMatrix(initial: Map<string, IRange[]> = new Map()) {
const worksheet = {
getMaxColumns: () => 20,
getMaxRows: () => 20,
} as unknown as Worksheet;
const workbook = {
getSheetBySheetId: () => worksheet,
} as unknown as Workbook;
const univerInstanceService = {
getUnit: () => workbook,
} as unknown as IUniverInstanceService;
return new RuleMatrix(initial, 'unit-1', 'sheet-1', univerInstanceService);
}
describe('RuleMatrix', () => {
it('overwrites overlapped cells with the latest rule and supports lookups', () => {
const matrix = createMatrix();
matrix.addRule(createRule('rule-1', [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }]));
matrix.addRule(createRule('rule-2', [{ startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 }]));
expect(matrix.getValue(0, 0)).toBe('rule-1');
expect(matrix.getValue(1, 1)).toBe('rule-2');
expect(matrix.getValue(2, 2)).toBe('rule-2');
expect(matrix.getValue(5, 5)).toBeUndefined();
});
it('updates, removes, and merges rule ranges correctly', () => {
const matrix = createMatrix(new Map([
['rule-1', [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }]],
['rule-2', [{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 }]],
]));
matrix.updateRange('rule-1', [{ startRow: 3, endRow: 3, startColumn: 3, endColumn: 3 }]);
expect(matrix.getValue(0, 0)).toBeUndefined();
expect(matrix.getValue(3, 3)).toBe('rule-1');
matrix.addRangeRules([
{
id: 'rule-1',
ranges: [
{ startRow: 4, endRow: 4, startColumn: 4, endColumn: 4 },
{ startRow: 4, endRow: 4, startColumn: 5, endColumn: 5 },
],
},
]);
expect(matrix.getValue(4, 4)).toBe('rule-1');
expect(matrix.getValue(4, 5)).toBe('rule-1');
matrix.removeRange([{ startRow: 4, endRow: 4, startColumn: 5, endColumn: 5 }]);
expect(matrix.getValue(4, 4)).toBe('rule-1');
expect(matrix.getValue(4, 5)).toBeUndefined();
matrix.removeRule(createRule('rule-2', []));
expect(matrix.getValue(2, 2)).toBeUndefined();
});
it('reports update, delete, add mutations and clones independently', () => {
const ranges1 = [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }];
const ranges2 = [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }];
const matrix = createMatrix(new Map([
['rule-1', ranges1],
['rule-2', ranges2],
]));
const cloned = matrix.clone();
cloned.updateRange('rule-1', [{ startRow: 3, endRow: 3, startColumn: 3, endColumn: 3 }]);
expect(matrix.getValue(0, 0)).toBe('rule-1');
const baseRules = [
createRule('rule-1', ranges1),
createRule('rule-2', ranges2),
];
cloned.removeRule(createRule('rule-2', ranges2));
expect(cloned.diff(baseRules)).toEqual([
{
type: 'update',
ruleId: 'rule-1',
oldRanges: ranges1,
newRanges: [{ startRow: 3, endRow: 3, startColumn: 3, endColumn: 3 }],
rule: baseRules[0],
},
{
type: 'delete',
rule: baseRules[1],
index: 1,
},
]);
expect(cloned.diffWithAddition(baseRules, [createRule('rule-3', [{ startRow: 6, endRow: 6, startColumn: 6, endColumn: 6 }])].values())).toEqual([
{
type: 'update',
ruleId: 'rule-1',
oldRanges: ranges1,
newRanges: [{ startRow: 3, endRow: 3, startColumn: 3, endColumn: 3 }],
rule: baseRules[0],
},
{
type: 'delete',
rule: baseRules[1],
index: 1,
},
{
type: 'add',
rule: {
uid: 'rule-3',
type: 'decimal',
ranges: [],
},
},
]);
});
});

View File

@@ -0,0 +1,270 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { BBox, IRange, ISheetDataValidationRule, IUniverInstanceService, Workbook } from '@univerjs/core';
import { debounce, Range, RBush, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
interface IRuleItem extends BBox {
ruleId: string;
}
export type RangeMutation = {
type: 'update';
ruleId: string;
oldRanges: IRange[];
newRanges: IRange[];
rule: ISheetDataValidationRule;
} | {
type: 'delete';
rule: ISheetDataValidationRule;
index: number;
} | {
type: 'add';
rule: ISheetDataValidationRule;
};
export class RuleMatrix {
private _map: Map<string, IRange[]>;
private _tree = new RBush<IRuleItem>();
private _dirty = true;
constructor(
value: Map<string, IRange[]>,
private _unitId: string,
private _subUnitId: string,
private _univerInstanceService: IUniverInstanceService,
private _disableTree = false
) {
this._map = value;
this._buildTree();
}
private _buildTree = () => {
if (!this._dirty || this._disableTree) {
return;
}
this._tree.clear();
const items: IRuleItem[] = [];
this._map.forEach((ranges, ruleId) => {
ranges.forEach((range) => {
items.push({
minX: range.startRow,
maxX: range.endRow,
minY: range.startColumn,
maxY: range.endColumn,
ruleId,
});
});
});
this._tree.load(items);
this._dirty = false;
};
private _debonceBuildTree = debounce(this._buildTree, 0);
get _worksheet() {
return this._univerInstanceService.getUnit<Workbook>(this._unitId, UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(this._subUnitId);
}
private _addRule(ruleId: string, _ranges: IRange[]) {
if (!this._worksheet) {
return;
}
const ranges = Rectangle.mergeRanges(_ranges.map((range) => Range.transformRange(range, this._worksheet!)));
this._map.forEach((value, key) => {
const newRanges = Rectangle.subtractMulti(value, ranges);
if (newRanges.length === 0) {
this._map.delete(key);
} else {
this._map.set(key, newRanges);
}
});
this._dirty = true;
this._map.set(ruleId, ranges);
this._debonceBuildTree();
}
addRule(rule: ISheetDataValidationRule) {
this._addRule(rule.uid, rule.ranges);
}
removeRange(_ranges: IRange[], ruleIds?: ReadonlySet<string>) {
if (!this._worksheet) {
return;
}
const ranges = _ranges.map((range) => Range.transformRange(range, this._worksheet!));
this._map.forEach((value, key) => {
if (ruleIds && !ruleIds.has(key)) {
return;
}
const newRanges = Rectangle.subtractMulti(value, ranges);
if (newRanges.length === 0) {
this._map.delete(key);
} else {
this._map.set(key, newRanges);
}
});
this._dirty = true;
this._debonceBuildTree();
}
private _removeRule(ruleId: string) {
this._map.delete(ruleId);
this._dirty = true;
this._debonceBuildTree();
}
removeRule(rule: ISheetDataValidationRule) {
this._removeRule(rule.uid);
}
updateRange(ruleId: string, _newRanges: IRange[]) {
this._removeRule(ruleId);
this._addRule(ruleId, _newRanges);
}
addRangeRules(rules: { id: string; ranges: IRange[] }[]) {
rules.forEach(({ id: ruleId, ranges }) => {
if (!ranges.length) {
return;
}
let current = this._map.get(ruleId);
if (!current) {
current = ranges;
this._map.set(ruleId, current);
} else {
this._map.set(ruleId, Rectangle.mergeRanges([...current, ...ranges]));
current = this._map.get(ruleId)!;
}
this._map.forEach((value, key) => {
if (key === ruleId) {
return;
}
const newRanges = Rectangle.subtractMulti(value, ranges);
if (newRanges.length === 0) {
this._map.delete(key);
} else {
this._map.set(key, newRanges);
}
});
});
this._dirty = true;
this._debonceBuildTree();
}
diff(rules: ISheetDataValidationRule[]) {
const mutations: RangeMutation[] = [];
let deleteIndex = 0;
rules.forEach((rule, index) => {
const newRanges = this._map.get(rule.uid) ?? [];
const oldRanges = rule.ranges;
if (newRanges.length !== 0 && (newRanges.length !== oldRanges.length || newRanges.some((range, i) => !Rectangle.equals(range, oldRanges[i])))) {
mutations.push({
type: 'update',
ruleId: rule.uid,
oldRanges,
newRanges: Rectangle.sort(newRanges),
rule,
});
}
if (newRanges.length === 0) {
mutations.push({
type: 'delete',
rule,
index: index - deleteIndex,
});
deleteIndex++;
}
});
return mutations;
}
diffWithAddition(rules: ISheetDataValidationRule[], additionRules: IterableIterator<ISheetDataValidationRule>) {
const mutations: RangeMutation[] = [];
let deleteIndex = 0;
rules.forEach((rule, index) => {
const newRanges = this._map.get(rule.uid) ?? [];
const oldRanges = rule.ranges;
if (newRanges.length !== 0 && (newRanges.length !== oldRanges.length || newRanges.some((range, i) => !Rectangle.equals(range, oldRanges[i])))) {
mutations.push({
type: 'update',
ruleId: rule.uid,
oldRanges,
newRanges: Rectangle.sort(newRanges),
rule,
});
}
if (newRanges.length === 0) {
mutations.push({
type: 'delete',
rule,
index: index - deleteIndex,
});
deleteIndex++;
}
});
Array.from(additionRules).forEach((rule) => {
const newRanges = this._map.get(rule.uid) ?? [];
mutations.push({
type: 'add',
rule: {
...rule,
ranges: Rectangle.sort(newRanges),
},
});
});
return mutations;
}
clone() {
return new RuleMatrix(
new Map(Tools.deepClone(Array.from(this._map.entries()))),
this._unitId,
this._subUnitId,
this._univerInstanceService,
// disable tree on cloned matrix, cause there is no need to search
true
);
}
getValue(row: number, col: number): string | undefined {
if (this._dirty) {
this._buildTree();
}
const result = this._tree.search({
minX: row,
maxX: row,
minY: col,
maxY: col,
});
return result.length > 0 ? result[0].ruleId : undefined;
}
}

View File

@@ -0,0 +1,297 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationType, ISheetDataValidationRule } from '@univerjs/core';
import type { IRuleChange, IUpdateRulePayload } from '@univerjs/data-validation';
import type { IRemoveSheetMutationParams, ISheetLocation } from '@univerjs/sheets';
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';
import { DataValidationCacheService } from '../services/dv-cache.service';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { getCellValueOrigin } from '../utils/get-cell-data-origin';
import { RuleMatrix } from './rule-matrix';
export interface IValidStatusChange {
unitId: string;
subUnitId: string;
row: number;
col: number;
ruleId: string;
status: DataValidationStatus;
}
export class SheetDataValidationModel extends Disposable {
private readonly _ruleMatrixMap = new Map<string, Map<string, RuleMatrix>>();
private readonly _validStatusChange$ = new Subject<IValidStatusChange>();
private readonly _ruleChange$ = new Subject<IRuleChange>();
readonly ruleChange$ = this._ruleChange$.asObservable();
readonly validStatusChange$ = this._validStatusChange$.asObservable();
constructor(
@Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(DataValidatorRegistryService) private _dataValidatorRegistryService: DataValidatorRegistryService,
@Inject(DataValidationCacheService) private _dataValidationCacheService: DataValidationCacheService,
@Inject(DataValidationFormulaService) private _dataValidationFormulaService: DataValidationFormulaService,
@Inject(DataValidationCustomFormulaService) private _dataValidationCustomFormulaService: DataValidationCustomFormulaService,
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._initRuleUpdateListener();
this.disposeWithMe(() => {
this._ruleChange$.complete();
this._validStatusChange$.complete();
});
this._initUniverInstanceListener();
}
private _initUniverInstanceListener() {
this.disposeWithMe(
this._univerInstanceService.unitDisposed$.subscribe((unit) => {
this._ruleMatrixMap.delete(unit.getUnitId());
})
);
this.disposeWithMe(
this._commandService.onCommandExecuted((command) => {
if (command.id === RemoveSheetMutation.id) {
const { unitId, subUnitId } = command.params as IRemoveSheetMutationParams;
const subUnitMap = this._ruleMatrixMap.get(unitId);
if (subUnitMap) {
subUnitMap.delete(subUnitId);
}
}
})
);
}
private _initRuleUpdateListener() {
const allRules = this._dataValidationModel.getAll();
for (const [unitId, subUnitMap] of allRules) {
for (const [subUnitId, rules] of subUnitMap) {
for (const rule of rules) {
this._addRule(unitId, subUnitId, rule);
this._ruleChange$.next({
type: 'add',
unitId,
subUnitId,
rule,
source: 'patched',
});
}
}
}
this.disposeWithMe(
this._dataValidationModel.ruleChange$.subscribe((ruleChange) => {
switch (ruleChange.type) {
case 'add':
this._addRule(ruleChange.unitId, ruleChange.subUnitId, ruleChange.rule);
break;
case 'update':
this._updateRule(ruleChange.unitId, ruleChange.subUnitId, ruleChange.rule.uid, ruleChange.oldRule!, ruleChange.updatePayload!);
break;
case 'remove':
this._removeRule(ruleChange.unitId, ruleChange.subUnitId, ruleChange.rule);
break;
}
this._ruleChange$.next(ruleChange);
})
);
}
private _ensureRuleMatrix(unitId: string, subUnitId: string) {
let unitMap = this._ruleMatrixMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._ruleMatrixMap.set(unitId, unitMap);
}
let matrix = unitMap.get(subUnitId);
if (!matrix) {
matrix = new RuleMatrix(new Map(), unitId, subUnitId, this._univerInstanceService);
unitMap.set(subUnitId, matrix);
}
return matrix;
}
private _addRuleSideEffect(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
const ruleMatrix = this._ensureRuleMatrix(unitId, subUnitId);
ruleMatrix.addRule(rule);
this._dataValidationCacheService.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 {
const rules = Array.isArray(rule) ? rule : [rule];
rules.forEach((item) => {
this._addRuleSideEffect(unitId, subUnitId, item);
});
}
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);
} else if (payload.type === UpdateRuleType.ALL) {
ruleMatrix.updateRange(ruleId, payload.payload.ranges);
}
this._dataValidationCacheService.removeRule(unitId, subUnitId, oldRule);
this._dataValidationCacheService.addRule(unitId, subUnitId, newRule);
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);
this._dataValidationCustomFormulaService.deleteByRuleId(unitId, subUnitId, oldRule.uid);
}
getValidator(type: DataValidationType | string) {
return this._dataValidatorRegistryService.getValidatorItem(type);
}
getRuleIdByLocation(unitId: string, subUnitId: string, row: number, col: number): string | undefined {
const ruleMatrix = this._ensureRuleMatrix(unitId, subUnitId);
return ruleMatrix.getValue(row, col)!;
}
getRuleByLocation(unitId: string, subUnitId: string, row: number, col: number): ISheetDataValidationRule | undefined {
const ruleId = this.getRuleIdByLocation(unitId, subUnitId, row, col);
if (!ruleId) {
return undefined;
}
return this._dataValidationModel.getRuleById(unitId, subUnitId, ruleId);
}
validator(rule: ISheetDataValidationRule, pos: ISheetLocation, _onCompete?: (status: DataValidationStatus, changed: boolean) => void): DataValidationStatus {
const { col, row, unitId, subUnitId, worksheet } = pos;
const onCompete = (status: DataValidationStatus, changed: boolean) => {
if (_onCompete) {
_onCompete(status, changed);
}
if (changed) {
this._validStatusChange$.next({
unitId,
subUnitId,
ruleId: rule.uid,
status,
row,
col,
});
}
};
const cell = worksheet.getCellValueOnly(row, col);
const validator = this.getValidator(rule.type);
const cellRaw = worksheet.getCellRaw(row, col);
const cellValue = getCellValueOrigin(cellRaw);
if (validator) {
const cache = this._dataValidationCacheService.ensureCache(unitId, subUnitId);
const current = cache.getValue(row, col);
if (current === null || current === undefined) {
cache.setValue(row, col, DataValidationStatus.VALIDATING);
validator.validator(
{
value: cellValue,
unitId,
subUnitId,
row,
column: col,
worksheet: pos.worksheet,
workbook: pos.workbook,
interceptValue: getCellValueOrigin(cell),
t: cellRaw?.t,
},
rule
).then((status) => {
const realStatus = status ? DataValidationStatus.VALID : DataValidationStatus.INVALID;
const now = cache.getValue(row, col);
if (realStatus === DataValidationStatus.VALID) {
cache.realDeleteValue(row, col);
} else {
cache.setValue(row, col, realStatus);
}
onCompete(realStatus, current !== now);
});
return DataValidationStatus.VALIDATING;
}
onCompete(current ?? DataValidationStatus.VALID, false);
return current ?? DataValidationStatus.VALID;
} else {
onCompete(DataValidationStatus.VALID, false);
return DataValidationStatus.VALID;
}
}
getRuleObjectMatrix(unitId: string, subUnitId: string) {
return this._ensureRuleMatrix(unitId, subUnitId);
}
getRuleById(unitId: string, subUnitId: string, ruleId: string): ISheetDataValidationRule | undefined {
return this._dataValidationModel.getRuleById(unitId, subUnitId, ruleId);
}
getRuleIndex(unitId: string, subUnitId: string, ruleId: string) {
return this._dataValidationModel.getRuleIndex(unitId, subUnitId, ruleId);
}
getRules(unitId: string, subUnitId: string): ISheetDataValidationRule[] {
return [...this._dataValidationModel.getRules(unitId, subUnitId)];
}
getUnitRules(unitId: string): [string, ISheetDataValidationRule[]][] {
return this._dataValidationModel.getUnitRules(unitId);
}
deleteUnitRules(unitId: string) {
return this._dataValidationModel.deleteUnitRules(unitId);
}
getSubUnitIds(unitId: string) {
return this._dataValidationModel.getSubUnitIds(unitId);
}
getAll() {
return this._dataValidationModel.getAll();
}
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 } from '@univerjs/core';
import type { IUniverSheetsDataValidationConfig } from './config/config';
import {
DependentOn,
ICommandService,
IConfigService,
Inject,
Injector,
merge,
Plugin,
UniverInstanceType,
} from '@univerjs/core';
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
import pkg from '../package.json';
import {
AddSheetDataValidationCommand,
ClearRangeDataValidationCommand,
RemoveSheetAllDataValidationCommand,
RemoveSheetDataValidationCommand,
UpdateSheetDataValidationOptionsCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
} from './commands/commands/data-validation.command';
import { DATA_VALIDATION_PLUGIN_NAME } from './common/const';
import { defaultPluginConfig, SHEETS_DATA_VALIDATION_PLUGIN_CONFIG_KEY } from './config/config';
import { DataValidationFormulaRefRangeController } from './controllers/dv-formula-ref-range.controller';
import { DataValidationFormulaController } from './controllers/dv-formula.controller';
import { DataValidationRefRangeController } from './controllers/dv-ref-range.controller';
import { SheetDataValidationSheetController } from './controllers/dv-sheet.controller';
import { DataValidationController } from './controllers/dv.controller';
import { SheetDataValidationModel } from './models/sheet-data-validation-model';
import { DataValidationCacheService } from './services/dv-cache.service';
import { DataValidationCustomFormulaService } from './services/dv-custom-formula.service';
import { DataValidationFormulaService } from './services/dv-formula.service';
import { DataValidationListCacheService } from './services/dv-list-cache.service';
import { SheetsDataValidationValidatorService } from './services/dv-validator.service';
@DependentOn(
UniverDataValidationPlugin,
UniverFormulaEnginePlugin,
UniverSheetsPlugin,
UniverSheetsFormulaPlugin
)
export class UniverSheetsDataValidationPlugin extends Plugin {
static override pluginName = DATA_VALIDATION_PLUGIN_NAME;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: Partial<IUniverSheetsDataValidationConfig> = defaultPluginConfig,
@Inject(Injector) protected _injector: Injector,
@ICommandService private readonly _commandService: ICommandService,
@IConfigService private readonly _configService: IConfigService
) {
super();
// Manage the plugin configuration.
const { ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
this._configService.setConfig(SHEETS_DATA_VALIDATION_PLUGIN_CONFIG_KEY, rest);
}
override onStarting() {
([
[DataValidationCacheService],
[DataValidationListCacheService],
[DataValidationFormulaService],
[DataValidationCustomFormulaService],
[SheetsDataValidationValidatorService],
[SheetDataValidationModel],
[DataValidationController],
[DataValidationFormulaController],
[SheetDataValidationSheetController],
[DataValidationRefRangeController],
[DataValidationFormulaRefRangeController],
] as Dependency[]).forEach((dep) => {
this._injector.add(dep);
});
[
AddSheetDataValidationCommand,
UpdateSheetDataValidationRangeCommand,
UpdateSheetDataValidationSettingCommand,
UpdateSheetDataValidationOptionsCommand,
RemoveSheetDataValidationCommand,
RemoveSheetAllDataValidationCommand,
ClearRangeDataValidationCommand,
].forEach((command) => {
this._commandService.registerCommand(command);
});
this._injector.get(DataValidationCacheService);
this._injector.get(SheetsDataValidationValidatorService);
this._injector.get(DataValidationController);
this._injector.get(DataValidationFormulaRefRangeController);
this._injector.get(DataValidationRefRangeController);
}
override onReady(): void {
this._injector.get(SheetDataValidationSheetController);
}
override onRendered(): void {
this._injector.get(DataValidationFormulaController);
}
}

View File

@@ -0,0 +1,134 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ICommandService, IRange, IUniverInstanceService, ObjectMatrix } from '@univerjs/core';
import type { DataValidationModel } from '@univerjs/data-validation';
import { RemoveSheetMutation, SetRangeValuesMutation } from '@univerjs/sheets';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { DataValidationCacheService } from '../dv-cache.service';
function createRule(ranges: IRange[]) {
return { uid: 'rule-1', ranges } as any;
}
function createService(ruleRanges: IRange[] = [{ startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 }]) {
const commandCallbacks: Array<(info: { id: string; params: unknown }, options?: { onlyLocal?: boolean }) => void> = [];
const unitDisposed$ = new Subject<any>();
const commandService = {
onCommandExecuted: vi.fn((callback) => {
commandCallbacks.push(callback);
return { dispose: vi.fn() };
}),
} as unknown as ICommandService;
const univerInstanceService = {
unitDisposed$,
} as unknown as IUniverInstanceService;
const dataValidationModel = {
getRules: vi.fn(() => [createRule(ruleRanges)]),
} as unknown as DataValidationModel;
return {
commandCallbacks,
unitDisposed$,
service: new DataValidationCacheService(commandService, univerInstanceService, dataValidationModel),
};
}
describe('DataValidationCacheService', () => {
it('creates caches, marks dirty ranges, and removes cached values by rule ranges', () => {
const { service } = createService();
const dirty: Array<{ unitId: string; subUnitId: string; ranges: IRange[]; isSetRange?: boolean }> = [];
const sub = service.dirtyRanges$.subscribe((value) => dirty.push(value));
const cache = service.ensureCache('unit-1', 'sheet-1');
cache.setValue(1, 1, 2 as never);
cache.setValue(2, 2, 3 as never);
service.markRangeDirty('unit-1', 'sheet-1', [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }], true);
expect(service.getValue('unit-1', 'sheet-1', 1, 1)).toBeUndefined();
service.addRule('unit-1', 'sheet-1', createRule([{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 }]));
service.removeRule('unit-1', 'sheet-1', createRule([{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 }]));
expect(service.getValue('unit-1', 'sheet-1', 2, 2)).toBeUndefined();
expect(dirty).toEqual([
{
unitId: 'unit-1',
subUnitId: 'sheet-1',
ranges: [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }],
isSetRange: true,
},
{
unitId: 'unit-1',
subUnitId: 'sheet-1',
ranges: [{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 }],
},
{
unitId: 'unit-1',
subUnitId: 'sheet-1',
ranges: [{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 }],
},
]);
sub.unsubscribe();
service.dispose();
});
it('reacts to set-range, remove-sheet, and unit-dispose events', () => {
const { service, commandCallbacks, unitDisposed$ } = createService();
const cache = service.ensureCache('unit-1', 'sheet-1');
cache.setValue(1, 1, 1 as never);
cache.setValue(5, 5, 1 as never);
commandCallbacks[0](
{
id: SetRangeValuesMutation.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
cellValue: {
0: { 0: { v: 'x' } },
1: { 1: { v: 'y' } },
},
},
},
undefined
);
expect(service.getValue('unit-1', 'sheet-1', 1, 1)).toBeUndefined();
expect(service.getValue('unit-1', 'sheet-1', 5, 5)).toBe(1);
commandCallbacks[1]({
id: RemoveSheetMutation.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
},
});
const recreated = service.ensureCache('unit-1', 'sheet-1');
expect(recreated).not.toBe(cache);
const unitCache = service.ensureCache('unit-1', 'sheet-2');
unitCache.setValue(0, 0, 9 as never);
unitDisposed$.next({ type: 2, getUnitId: () => 'unit-1' });
expect(service.ensureCache('unit-1', 'sheet-2')).not.toBe(unitCache as ObjectMatrix<any>);
service.dispose();
});
});

View File

@@ -0,0 +1,133 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IUniverInstanceService } from '@univerjs/core';
import type { DataValidationModel, DataValidatorRegistryService } from '@univerjs/data-validation';
import type { RegisterOtherFormulaService } from '@univerjs/engine-formula';
import type { DataValidationCacheService } from '../dv-cache.service';
import { DataValidationType, UniverInstanceType } from '@univerjs/core';
import { OtherFormulaBizType } from '@univerjs/engine-formula';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { DataValidationCustomFormulaService } from '../dv-custom-formula.service';
describe('DataValidationCustomFormulaService', () => {
it('registers offset formulas, reads cell results, marks dirty ranges, and deletes formula ids', async () => {
const formulaResult$ = new Subject<any>();
const dirtyRanges$ = new Subject<any>();
const registerOtherFormulaService = {
formulaResult$,
registerFormulaWithRange: vi.fn((unitId: string, subUnitId: string, formula: string, _ranges, extra) => `${extra.ruleId}-${formula}`),
deleteFormula: vi.fn(),
getFormulaValue: vi.fn(async () => ({ result: [[[[{ v: 1 }]], [[{ v: 2 }]]], [[[{ v: 3 }]], [[{ v: 4 }]]]] })),
getFormulaValueSync: vi.fn(() => ({ result: [[[[{ v: 1 }]], [[{ v: 2 }]]], [[[{ v: 3 }]], [[{ v: 4 }]]]] })),
markFormulaDirty: vi.fn(),
} as unknown as RegisterOtherFormulaService;
const rule = {
uid: 'rule-1',
type: DataValidationType.DATE,
formula1: '=A1',
formula2: '=B1',
ranges: [{ startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 }],
} as any;
const dataValidationModel = {
getRuleById: vi.fn(() => rule),
getRules: vi.fn(() => [rule]),
} as unknown as DataValidationModel;
const cacheService = {
dirtyRanges$,
markRangeDirty: vi.fn(),
} as unknown as DataValidationCacheService;
const service = new DataValidationCustomFormulaService(
{ getUnitType: vi.fn(() => UniverInstanceType.UNIVER_SHEET) } as unknown as IUniverInstanceService,
registerOtherFormulaService,
dataValidationModel,
cacheService,
{ getValidatorItem: vi.fn(() => ({ offsetFormulaByRange: true })) } as unknown as DataValidatorRegistryService
);
service.addRule('unit-1', 'sheet-1', rule);
expect(registerOtherFormulaService.registerFormulaWithRange).toHaveBeenCalledWith(
'unit-1',
'sheet-1',
'=A1',
rule.ranges,
{ ruleId: 'rule-1' },
OtherFormulaBizType.DATA_VALIDATION_CUSTOM,
'rule-1'
);
expect(service.getRuleFormulaInfo('unit-1', 'sheet-1', 'rule-1')).toEqual({
formula: '=A1',
originCol: 1,
originRow: 1,
formulaId: 'rule-1-=A1',
});
await expect(service.getCellFormulaValue('unit-1', 'sheet-1', 'rule-1', 2, 2)).resolves.toEqual({ v: 4 });
await expect(service.getCellFormula2Value('unit-1', 'sheet-1', 'rule-1', 1, 2)).resolves.toEqual({ v: 2 });
expect(service.getCellFormulaValueSync('unit-1', 'sheet-1', 'rule-1', 1, 1)).toEqual({ v: 1 });
expect(service.getCellFormula2ValueSync('unit-1', 'sheet-1', 'rule-1', 2, 1)).toEqual({ v: 3 });
formulaResult$.next({
'unit-1': {
'sheet-1': [{ extra: { ruleId: 'rule-1' } }],
},
});
expect(cacheService.markRangeDirty).toHaveBeenCalledWith('unit-1', 'sheet-1', rule.ranges);
dirtyRanges$.next({ unitId: 'unit-1', subUnitId: 'sheet-1', ranges: [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }], isSetRange: true });
expect(registerOtherFormulaService.markFormulaDirty).toHaveBeenCalledWith('unit-1', 'sheet-1', 'rule-1-=A1');
expect(registerOtherFormulaService.markFormulaDirty).toHaveBeenCalledWith('unit-1', 'sheet-1', 'rule-1-=B1');
service.deleteByRuleId('unit-1', 'sheet-1', 'rule-1');
expect(registerOtherFormulaService.deleteFormula).toHaveBeenCalledWith('unit-1', 'sheet-1', ['rule-1-=A1']);
expect(registerOtherFormulaService.deleteFormula).toHaveBeenCalledWith('unit-1', 'sheet-1', ['rule-1-=B1']);
service.dispose();
});
it('skips non-offset rules and returns undefined for missing formulas', async () => {
const service = new DataValidationCustomFormulaService(
{ getUnitType: vi.fn(() => UniverInstanceType.UNIVER_DOC) } as unknown as IUniverInstanceService,
{
formulaResult$: new Subject<any>(),
registerFormulaWithRange: vi.fn(),
deleteFormula: vi.fn(),
getFormulaValue: vi.fn(),
getFormulaValueSync: vi.fn(),
markFormulaDirty: vi.fn(),
} as unknown as RegisterOtherFormulaService,
{
getRuleById: vi.fn(() => null),
getRules: vi.fn(() => []),
} as unknown as DataValidationModel,
{
dirtyRanges$: new Subject<any>(),
markRangeDirty: vi.fn(),
} as unknown as DataValidationCacheService,
{ getValidatorItem: vi.fn(() => ({ offsetFormulaByRange: false })) } as unknown as DataValidatorRegistryService
);
service.addRule('unit-1', 'sheet-1', { uid: 'rule-2', type: DataValidationType.DECIMAL, formula1: '=A1', ranges: [] } as any);
await expect(service.getCellFormulaValue('unit-1', 'sheet-1', 'missing', 0, 0)).resolves.toBeUndefined();
await expect(service.getCellFormula2Value('unit-1', 'sheet-1', 'missing', 0, 0)).resolves.toBeUndefined();
expect(service.getCellFormulaValueSync('unit-1', 'sheet-1', 'missing', 0, 0)).toBeUndefined();
expect(service.getCellFormula2ValueSync('unit-1', 'sheet-1', 'missing', 0, 0)).toBeUndefined();
service.deleteByRuleId('unit-1', 'sheet-1', 'missing');
});
});

View File

@@ -0,0 +1,148 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IUniverInstanceService } from '@univerjs/core';
import type { DataValidationModel, DataValidatorRegistryService } from '@univerjs/data-validation';
import type { RegisterOtherFormulaService } from '@univerjs/engine-formula';
import type { DataValidationCacheService } from '../dv-cache.service';
import type { DataValidationListCacheService } from '../dv-list-cache.service';
import { DataValidationType, UniverInstanceType } from '@univerjs/core';
import { OtherFormulaBizType } from '@univerjs/engine-formula';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { DataValidationFormulaService } from '../dv-formula.service';
describe('DataValidationFormulaService', () => {
it('registers formula rules, fetches results, removes them, and reacts to formula result changes', async () => {
const formulaResult$ = new Subject<any>();
const registerOtherFormulaService = {
formulaResult$,
registerFormulaWithRange: vi.fn((unitId: string, subUnitId: string, formula: string, _ranges, extra) => `${extra.ruleId}-${formula}`),
deleteFormula: vi.fn(),
getFormulaValue: vi.fn(async (_unitId: string, _subUnitId: string, id: string) => ({ id })),
getFormulaValueSync: vi.fn((_unitId: string, _subUnitId: string, id: string) => ({ id })),
} as unknown as RegisterOtherFormulaService;
const cacheService = {
markRangeDirty: vi.fn(),
} as unknown as DataValidationCacheService;
const listCacheService = {
markRuleDirty: vi.fn(),
} as unknown as DataValidationListCacheService;
const rule = {
uid: 'rule-1',
type: DataValidationType.DECIMAL,
formula1: '=A1',
formula2: '=B1',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
} as any;
const dataValidationModel = {
getRuleById: vi.fn(() => rule),
} as unknown as DataValidationModel;
const validatorRegistryService = {
getValidatorItem: vi.fn(() => ({ offsetFormulaByRange: false })),
} as unknown as DataValidatorRegistryService;
const instanceService = {
getUnitType: vi.fn(() => UniverInstanceType.UNIVER_SHEET),
} as unknown as IUniverInstanceService;
const service = new DataValidationFormulaService(
instanceService,
registerOtherFormulaService,
cacheService,
dataValidationModel,
validatorRegistryService,
listCacheService
);
service.addRule('unit-1', 'sheet-1', rule);
expect(registerOtherFormulaService.registerFormulaWithRange).toHaveBeenCalledWith(
'unit-1',
'sheet-1',
'=A1',
[{ startColumn: 0, endColumn: 0, startRow: 0, endRow: 0 }],
{ ruleId: 'rule-1' },
OtherFormulaBizType.DATA_VALIDATION,
'rule-1'
);
expect(service.getRuleFormulaInfo('unit-1', 'sheet-1', 'rule-1')).toEqual([
{ id: 'rule-1-=A1', text: '=A1' },
{ id: 'rule-1-=B1', text: '=B1' },
]);
await expect(service.getRuleFormulaResult('unit-1', 'sheet-1', 'rule-1')).resolves.toEqual([
{ id: 'rule-1-=A1' },
{ id: 'rule-1-=B1' },
]);
expect(service.getRuleFormulaResultSync('unit-1', 'sheet-1', 'rule-1')).toEqual([
{ id: 'rule-1-=A1' },
{ id: 'rule-1-=B1' },
]);
formulaResult$.next({
'unit-1': {
'sheet-1': [
{ extra: { ruleId: 'rule-1' } },
],
},
});
expect(listCacheService.markRuleDirty).toHaveBeenCalledWith('unit-1', 'sheet-1', 'rule-1');
expect(cacheService.markRangeDirty).toHaveBeenCalledWith('unit-1', 'sheet-1', rule.ranges);
service.removeRule('unit-1', 'sheet-1', 'rule-1');
expect(registerOtherFormulaService.deleteFormula).toHaveBeenCalledWith('unit-1', 'sheet-1', ['rule-1-=A1', 'rule-1-=B1']);
});
it('skips registration for non-formula rules, checkbox rules, and missing entries', async () => {
const formulaResult$ = new Subject<any>();
const registerOtherFormulaService = {
formulaResult$,
registerFormulaWithRange: vi.fn(),
deleteFormula: vi.fn(),
getFormulaValue: vi.fn(),
getFormulaValueSync: vi.fn(),
} as unknown as RegisterOtherFormulaService;
const service = new DataValidationFormulaService(
{ getUnitType: vi.fn(() => UniverInstanceType.UNIVER_DOC) } as unknown as IUniverInstanceService,
registerOtherFormulaService,
{ markRangeDirty: vi.fn() } as unknown as DataValidationCacheService,
{ getRuleById: vi.fn(() => null) } as unknown as DataValidationModel,
{ getValidatorItem: vi.fn(() => ({ offsetFormulaByRange: true })) } as unknown as DataValidatorRegistryService,
{ markRuleDirty: vi.fn() } as unknown as DataValidationListCacheService
);
service.addRule('unit-1', 'sheet-1', {
uid: 'rule-2',
type: DataValidationType.DECIMAL,
formula1: '1',
formula2: '2',
} as any);
service.addRule('unit-1', 'sheet-1', {
uid: 'rule-3',
type: DataValidationType.CHECKBOX,
formula1: '=A1',
formula2: '=B1',
} as any);
expect(registerOtherFormulaService.registerFormulaWithRange).not.toHaveBeenCalled();
await expect(service.getRuleFormulaResult('unit-1', 'sheet-1', 'missing')).resolves.toBeNull();
expect(service.getRuleFormulaResultSync('unit-1', 'sheet-1', 'missing')).toBeUndefined();
service.removeRule('unit-1', 'sheet-1', 'missing');
expect(registerOtherFormulaService.deleteFormula).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,98 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { Injector } from '@univerjs/core';
import type { DataValidationModel } from '@univerjs/data-validation';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { DataValidationFormulaService } from '../dv-formula.service';
import { DataValidationListCacheService } from '../dv-list-cache.service';
describe('DataValidationListCacheService', () => {
it('computes, caches, invalidates, and clears list items', () => {
const ruleChange$ = new Subject<any>();
const formulaService = {
getRuleFormulaResultSync: vi.fn(() => [
{ result: [[[[{ v: 'A' }, { v: 'B' }, { v: '#VALUE!' }, { v: 1, s: { n: { pattern: '0.00' } } }]]]] },
]),
};
const injector = {
get(token: unknown) {
if (token === DataValidationFormulaService) {
return formulaService;
}
throw new Error(`Unknown token: ${String(token)}`);
},
} as unknown as Injector;
const dataValidationModel = {
ruleChange$,
} as unknown as DataValidationModel;
const service = new DataValidationListCacheService(injector, dataValidationModel);
const rule = {
uid: 'rule-1',
formula1: '=A1',
formula2: '#f00,#0f0,#00f',
} as any;
const first = service.getOrCompute('unit-1', 'sheet-1', rule);
expect(first.list).toEqual(['A', 'B', '1.00']);
expect(first.listWithColor).toEqual([
{ label: 'A', color: '#f00' },
{ label: 'B', color: '#0f0' },
{ label: '1.00', color: '#00f' },
]);
expect(first.colorMap).toEqual({ A: '#f00', B: '#0f0', '1.00': '#00f' });
expect(first.set.has('A')).toBe(true);
expect(service.getOrCompute('unit-1', 'sheet-1', rule)).toBe(first);
expect(formulaService.getRuleFormulaResultSync).toHaveBeenCalledTimes(1);
service.markRuleDirty('unit-1', 'sheet-1', 'rule-1');
expect(service.getCache('unit-1', 'sheet-1', 'rule-1')).toBeUndefined();
service.setCache('unit-1', 'sheet-1', 'rule-2', first);
expect(service.getCache('unit-1', 'sheet-1', 'rule-2')).toBe(first);
ruleChange$.next({ type: 'remove', unitId: 'unit-1', subUnitId: 'sheet-1', rule: { uid: 'rule-2' } });
expect(service.getCache('unit-1', 'sheet-1', 'rule-2')).toBeUndefined();
service.clear();
expect(service.getCache('unit-1', 'sheet-1', 'rule-1')).toBeUndefined();
});
it('supports non-formula lists and update invalidation', () => {
const ruleChange$ = new Subject<any>();
const service = new DataValidationListCacheService(
{
get() {
return {
getRuleFormulaResultSync: vi.fn(),
};
},
} as unknown as Injector,
{ ruleChange$ } as unknown as DataValidationModel
);
const item = service.computeAndCache('unit-1', 'sheet-1', { uid: 'rule-3', formula1: 'x,y,,z', formula2: '' } as any, null);
expect(item.list).toEqual(['x', 'y', 'z']);
expect(item.colorMap).toEqual({});
ruleChange$.next({ type: 'update', unitId: 'unit-1', subUnitId: 'sheet-1', rule: { uid: 'rule-3' } });
expect(service.getCache('unit-1', 'sheet-1', 'rule-3')).toBeUndefined();
});
});

View File

@@ -0,0 +1,174 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 {
IDataValidationRule,
IRange,
IUniverInstanceService,
LifecycleService,
Nullable,
Workbook,
Worksheet,
} from '@univerjs/core';
import type { ISheetLocation } from '@univerjs/sheets';
import type { SheetDataValidationModel } from '../../models/sheet-data-validation-model';
import type { DataValidationCacheService } from '../dv-cache.service';
import { DataValidationStatus, LifecycleStages, ObjectMatrix } from '@univerjs/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SheetsDataValidationValidatorService } from '../dv-validator.service';
function createRule(uid: string, ranges: IRange[]): IDataValidationRule {
return { uid, ranges } as IDataValidationRule;
}
function createService() {
const dirtyRanges$ = new Subject<{ unitId: string; subUnitId: string; ranges: IRange[] }>();
const lifecycle$ = new BehaviorSubject(LifecycleStages.Rendered);
let lifecycleSubscribeCount = 0;
const lifecycleSubject = lifecycle$ as any;
const originalSubscribe = lifecycleSubject._subscribe.bind(lifecycleSubject);
lifecycleSubject._subscribe = (...args: unknown[]) => {
lifecycleSubscribeCount++;
return originalSubscribe(...args);
};
const worksheet = {
getSheetId: () => 'sheet-1',
getMergedCell: vi.fn(() => null),
getCell: vi.fn((row: number, col: number) => ({ v: `${row},${col}` })),
getMaxColumns: () => 20,
getMaxRows: () => 20,
} as unknown as Worksheet;
const workbook = {
getUnitId: () => 'unit-1',
getActiveSheet: () => worksheet,
getSheetBySheetId: vi.fn((sheetId: string) => (sheetId === 'sheet-1' ? worksheet : null)),
} as unknown as Workbook;
const cache = new ObjectMatrix<Nullable<DataValidationStatus>>();
const rule = createRule('rule-1', [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }]);
const model = {
getRuleByLocation: vi.fn(() => rule),
validator: vi.fn((_rule: IDataValidationRule, _pos: ISheetLocation, onComplete?: (status: DataValidationStatus, changed: boolean) => void) => {
onComplete?.(DataValidationStatus.INVALID, true);
return DataValidationStatus.INVALID;
}),
getRules: vi.fn(() => [rule]),
getSubUnitIds: vi.fn(() => ['sheet-1']),
getRuleObjectMatrix: vi.fn(() => ({
getValue: vi.fn((row: number, col: number) => (row === 0 && col === 0 ? 'rule-1' : undefined)),
})),
getRuleById: vi.fn((_, __, id: string) => (id === 'rule-1' ? rule : undefined)),
} as unknown as SheetDataValidationModel;
const cacheService = {
dirtyRanges$,
getValue: vi.fn(() => DataValidationStatus.INVALID),
ensureCache: vi.fn(() => cache),
} as unknown as DataValidationCacheService;
const univerInstanceService = {
getUnit: vi.fn((unitId: string) => (unitId === 'unit-1' ? workbook : null)),
getCurrentUnitOfType: vi.fn(() => workbook),
} as unknown as IUniverInstanceService;
const lifecycleService = {
lifecycle$,
stage: LifecycleStages.Rendered,
} as unknown as LifecycleService;
return {
dirtyRanges$,
lifecycle$,
worksheet,
workbook,
cache,
model,
cacheService,
univerInstanceService,
lifecycleService,
getLifecycleSubscribeCount: () => lifecycleSubscribeCount,
service: new SheetsDataValidationValidatorService(univerInstanceService, model, cacheService, lifecycleService),
};
}
describe('SheetsDataValidationValidatorService', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.stubGlobal('requestIdleCallback', (callback: IdleRequestCallback) => {
callback({ didTimeout: false, timeRemaining: () => 1 } as IdleDeadline);
return 1;
});
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
it('does not recursively reopen dirty range buffer when created after render', () => {
const { service, getLifecycleSubscribeCount } = createService();
expect(getLifecycleSubscribeCount()).toBe(1);
service.dispose();
});
it('validates single cells, merged cells, ranges, worksheets, and workbooks', async () => {
const { service, worksheet, model, cacheService } = createService();
await expect(service.validatorCell('missing', 'sheet-1', 0, 0)).rejects.toThrow('cannot find current workbook');
await expect(service.validatorCell('unit-1', 'missing', 0, 0)).rejects.toThrow('cannot find current worksheet');
worksheet.getMergedCell = vi.fn(() => ({ startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }));
expect(await service.validatorCell('unit-1', 'sheet-1', 1, 1)).toBe(DataValidationStatus.INVALID);
expect(model.getRuleByLocation).toHaveBeenCalledWith('unit-1', 'sheet-1', 0, 0);
model.validator = vi.fn((_rule: IDataValidationRule, _pos: ISheetLocation, onComplete?: (status: DataValidationStatus, changed: boolean) => void) => {
onComplete?.(DataValidationStatus.VALIDATING, true);
return DataValidationStatus.VALIDATING;
});
const rangeStatuses = await service.validatorRanges('unit-1', 'sheet-1', [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }]);
expect(rangeStatuses).toEqual([[DataValidationStatus.INVALID]]);
expect(await service.validatorRanges('unit-1', 'sheet-1', [])).toEqual([]);
expect(await service.validatorWorksheet('unit-1', 'sheet-1')).toBe(cacheService.ensureCache('unit-1', 'sheet-1'));
expect(await service.validatorWorkbook('unit-1')).toEqual({
'sheet-1': cacheService.ensureCache('unit-1', 'sheet-1'),
});
});
it('returns matched data validations and reacts to dirty ranges after render', async () => {
const { service, dirtyRanges$, lifecycle$, model } = createService();
const validatorRangesSpy = vi.spyOn(service, 'validatorRanges').mockResolvedValue([]);
expect(service.getDataValidations('unit-1', 'sheet-1', [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }])).toEqual([
createRule('rule-1', [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }]),
]);
expect(service.getDataValidation('unit-1', 'sheet-1', [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }])?.uid).toBe('rule-1');
dirtyRanges$.next({
unitId: 'unit-1',
subUnitId: 'sheet-1',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
});
lifecycle$.next(LifecycleStages.Rendered);
await vi.runAllTimersAsync();
expect(validatorRangesSpy).toHaveBeenCalledWith('unit-1', 'sheet-1', [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }]);
model.getRuleById = vi.fn(() => undefined);
expect(service.getDataValidations('unit-1', 'sheet-1', [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }])).toEqual([]);
service.dispose();
});
});

View File

@@ -0,0 +1,149 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationStatus, IRange, ISheetDataValidationRule, Nullable } from '@univerjs/core';
import type { IRemoveSheetMutationParams, ISetRangeValuesMutationParams } from '@univerjs/sheets';
import {
Disposable,
getIntersectRange,
ICommandService,
Inject,
IUniverInstanceService,
ObjectMatrix,
Range,
UniverInstanceType,
} from '@univerjs/core';
import { DataValidationModel } from '@univerjs/data-validation';
import { RemoveSheetMutation, SetRangeValuesMutation } from '@univerjs/sheets';
import { Subject } from 'rxjs';
export class DataValidationCacheService extends Disposable {
private _cacheMatrix: Map<string, Map<string, ObjectMatrix<Nullable<DataValidationStatus>>>> = new Map();
private _dirtyRanges$ = new Subject<{ unitId: string; subUnitId: string; ranges: IRange[]; isSetRange?: boolean }>();
readonly dirtyRanges$ = this._dirtyRanges$.asObservable();
constructor(
@Inject(ICommandService) private readonly _commandService: ICommandService,
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
@Inject(DataValidationModel) private readonly _sheetDataValidationModel: DataValidationModel
) {
super();
this._initDirtyRanges();
this._initSheetRemove();
}
private _initDirtyRanges() {
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo, options) => {
if (commandInfo.id === SetRangeValuesMutation.id && !options?.onlyLocal) {
const { cellValue, unitId, subUnitId } = commandInfo.params as ISetRangeValuesMutationParams;
if (cellValue) {
const range = new ObjectMatrix(cellValue).getDataRange();
if (range.endRow === -1) return;
const rules = this._sheetDataValidationModel.getRules(unitId, subUnitId);
const ranges: IRange[] = [];
for (const rule of rules) {
ranges.push(...rule.ranges);
}
const intersectsRanges: IRange[] = [];
for (const ruleRange of ranges) {
const intersect = getIntersectRange(ruleRange, range);
if (intersect) {
intersectsRanges.push(intersect);
}
}
if (intersectsRanges.length) {
this.markRangeDirty(unitId, subUnitId, intersectsRanges, true);
}
}
}
}));
}
private _initSheetRemove() {
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === RemoveSheetMutation.id) {
const { unitId, subUnitId } = commandInfo.params as IRemoveSheetMutationParams;
this._cacheMatrix.get(unitId)?.delete(subUnitId);
}
}));
this.disposeWithMe(this._univerInstanceService.unitDisposed$.subscribe((univerInstance) => {
if (univerInstance.type === UniverInstanceType.UNIVER_SHEET) {
this._cacheMatrix.delete(univerInstance.getUnitId());
}
}));
}
private _ensureCache(unitId: string, subUnitId: string) {
let unitMap = this._cacheMatrix.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._cacheMatrix.set(unitId, unitMap);
}
let cacheMatrix = unitMap.get(subUnitId);
if (!cacheMatrix) {
cacheMatrix = new ObjectMatrix();
unitMap.set(subUnitId, cacheMatrix);
}
return cacheMatrix;
}
ensureCache(unitId: string, subUnitId: string) {
return this._ensureCache(unitId, subUnitId);
}
addRule(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
this.markRangeDirty(unitId, subUnitId, rule.ranges);
}
removeRule(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
this._deleteRange(unitId, subUnitId, rule.ranges);
}
markRangeDirty(unitId: string, subUnitId: string, ranges: IRange[], isSetRange?: boolean) {
const cache = this._ensureCache(unitId, subUnitId);
ranges.forEach((range) => {
Range.foreach(range, (row, col) => {
if (cache.getValue(row, col) !== undefined) {
cache.setValue(row, col, undefined);
}
});
});
this._dirtyRanges$.next({ unitId, subUnitId, ranges, isSetRange });
}
private _deleteRange(unitId: string, subUnitId: string, ranges: IRange[]) {
const cache = this._ensureCache(unitId, subUnitId);
ranges.forEach((range) => {
Range.foreach(range, (row, col) => {
cache.realDeleteValue(row, col);
});
});
this._dirtyRanges$.next({ unitId, subUnitId, ranges });
}
getValue(unitId: string, subUnitId: string, row: number, col: number) {
const cache = this._ensureCache(unitId, subUnitId);
return cache.getValue(row, col);
}
}

View File

@@ -0,0 +1,264 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IRange, ISheetDataValidationRule } from '@univerjs/core';
import { Disposable, Inject, isFormulaString, IUniverInstanceService, Rectangle, UniverInstanceType } from '@univerjs/core';
import { DataValidationModel, DataValidatorRegistryService } from '@univerjs/data-validation';
import { OtherFormulaBizType, RegisterOtherFormulaService } from '@univerjs/engine-formula';
import { getFormulaCellData, shouldOffsetFormulaByRange } from '../utils/formula';
import { DataValidationCacheService } from './dv-cache.service';
interface IFormulaData {
formula: string;
originRow: number;
originCol: number;
formulaId: string;
}
type RuleId = string;
type UnitId = string;
type SubUnitId = string;
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,
@Inject(RegisterOtherFormulaService) private _registerOtherFormulaService: RegisterOtherFormulaService,
@Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel,
@Inject(DataValidationCacheService) private readonly _dataValidationCacheService: DataValidationCacheService,
@Inject(DataValidatorRegistryService) private readonly _validatorRegistryService: DataValidatorRegistryService
) {
super();
this._initFormulaResultHandler();
this._initDirtyRanges();
}
override dispose(): void {
super.dispose();
this._ruleFormulaMap.clear();
this._ruleFormulaMap2.clear();
}
private _initFormulaResultHandler() {
this.disposeWithMe(this._registerOtherFormulaService.formulaResult$.subscribe((resultMap) => {
for (const unitId in resultMap) {
const unitMap = resultMap[unitId];
const type = this._instanceSrv.getUnitType(unitId);
if (type !== UniverInstanceType.UNIVER_SHEET) continue;
for (const subUnitId in unitMap) {
const results = unitMap[subUnitId];
const { ruleFormulaMap } = this._ensureMaps(unitId, subUnitId);
results.forEach((result) => {
const ruleInfo = ruleFormulaMap.get(result.extra?.ruleId);
const rule = this._dataValidationModel.getRuleById(unitId, subUnitId, result.extra?.ruleId);
if (rule && ruleInfo) {
this._dataValidationCacheService.markRangeDirty(unitId, subUnitId, rule.ranges);
}
});
}
}
}));
}
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);
if (!ruleFormulaMap) {
ruleFormulaMap = new Map();
ruleFormulaUnitMap.set(subUnitId, 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[]) {
return this._registerOtherFormulaService.registerFormulaWithRange(unitId, subUnitId, formulaString, ranges, { ruleId }, OtherFormulaBizType.DATA_VALIDATION_CUSTOM, ruleId);
};
private _handleDirtyRanges(unitId: string, subUnitId: string, ranges: IRange[]) {
const rules = this._dataValidationModel.getRules(unitId, subUnitId);
rules.forEach((rule) => {
const ruleRanges = rule.ranges as IRange[];
const hasOverLap = Rectangle.doAnyRangesIntersect(ruleRanges, ranges);
if (hasOverLap) {
this.makeRuleDirty(unitId, subUnitId, rule.uid);
}
});
}
private _initDirtyRanges() {
this.disposeWithMe(this._dataValidationCacheService.dirtyRanges$.subscribe((data) => {
if (data.isSetRange) {
this._handleDirtyRanges(data.unitId, data.subUnitId, data.ranges);
}
}));
}
deleteByRuleId(unitId: string, subUnitId: string, ruleId: string) {
const { ruleFormulaMap, ruleFormulaMap2 } = this._ensureMaps(unitId, subUnitId);
const rule = this._dataValidationModel.getRuleById(unitId, subUnitId, ruleId) as ISheetDataValidationRule;
const formulaInfo = ruleFormulaMap.get(ruleId);
if (!rule || !formulaInfo) {
return;
}
const current = ruleFormulaMap.get(ruleId);
if (current) {
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 | undefined, formula2: string | undefined, ranges: IRange[]) {
const { ruleFormulaMap, ruleFormulaMap2 } = this._ensureMaps(unitId, subUnitId);
const originRow = ranges[0].startRow;
const originCol = ranges[0].startColumn;
if (formula && isFormulaString(formula)) {
const formulaId = this._registerFormula(unitId, subUnitId, ruleId, formula, ranges);
ruleFormulaMap.set(ruleId, {
formula,
originCol,
originRow,
formulaId,
});
}
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) {
if (shouldOffsetFormulaByRange(rule.type, this._validatorRegistryService)) {
const { ranges, formula1, formula2, uid: ruleId } = rule;
this._addFormulaByRange(unitId, subUnitId, ruleId, formula1, formula2, ranges);
}
}
async getCellFormulaValue(unitId: string, subUnitId: string, ruleId: string, row: number, column: number) {
const { ruleFormulaMap } = this._ensureMaps(unitId, subUnitId);
const current = ruleFormulaMap.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]);
}
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);
return ruleFormulaMap.get(ruleId);
}
makeRuleDirty(unitId: string, subUnitId: string, ruleId: string) {
const formula1 = this._ruleFormulaMap.get(unitId)?.get(subUnitId)?.get(ruleId);
const formula2 = this._ruleFormulaMap2.get(unitId)?.get(subUnitId)?.get(ruleId);
if (formula1) {
this._registerOtherFormulaService.markFormulaDirty(unitId, subUnitId, formula1.formulaId);
}
if (formula2) {
this._registerOtherFormulaService.markFormulaDirty(unitId, subUnitId, formula2.formulaId);
}
}
}

View File

@@ -0,0 +1,165 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ISheetDataValidationRule, Nullable } from '@univerjs/core';
import type { IFormulaInfo, IOtherFormulaResult } from '@univerjs/engine-formula';
import { DataValidationType, Disposable, Inject, isFormulaString, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DataValidationModel, DataValidatorRegistryService } from '@univerjs/data-validation';
import { OtherFormulaBizType, RegisterOtherFormulaService } from '@univerjs/engine-formula';
import { shouldOffsetFormulaByRange } from '../utils/formula';
import { DataValidationCacheService } from './dv-cache.service';
import { DataValidationListCacheService } from './dv-list-cache.service';
type RuleId = string;
type UnitId = string;
type SubUnitId = string;
export class DataValidationFormulaService extends Disposable {
private _formulaRuleMap: Map<UnitId, Map<SubUnitId, Map<RuleId, [IFormulaInfo | undefined, IFormulaInfo | undefined]>>> = new Map();
constructor(
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService,
@Inject(RegisterOtherFormulaService) private _registerOtherFormulaService: RegisterOtherFormulaService,
@Inject(DataValidationCacheService) private readonly _dataValidationCacheService: DataValidationCacheService,
@Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel,
@Inject(DataValidatorRegistryService) private readonly _validatorRegistryService: DataValidatorRegistryService,
@Inject(DataValidationListCacheService) private readonly _listCacheService: DataValidationListCacheService
) {
super();
this._initFormulaResultHandler();
}
private _initFormulaResultHandler() {
this.disposeWithMe(this._registerOtherFormulaService.formulaResult$.subscribe((resultMap) => {
for (const unitId in resultMap) {
const unitMap = resultMap[unitId];
const type = this._instanceService.getUnitType(unitId);
if (type !== UniverInstanceType.UNIVER_SHEET) continue;
for (const subUnitId in unitMap) {
const results = unitMap[subUnitId];
const formulaMap = this._ensureRuleFormulaMap(unitId, subUnitId);
results.forEach((result) => {
const ruleId = result.extra?.ruleId;
if (ruleId && formulaMap.get(ruleId)) {
const rule = this._dataValidationModel.getRuleById(unitId, subUnitId, ruleId);
if (rule) {
// Mark list cache dirty when formula result changes
this._listCacheService.markRuleDirty(unitId, subUnitId, ruleId);
this._dataValidationCacheService.markRangeDirty(unitId, subUnitId, rule.ranges);
}
}
});
}
}
}));
}
private _ensureRuleFormulaMap(unitId: string, subUnitId: string) {
let unitMap = this._formulaRuleMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._formulaRuleMap.set(unitId, unitMap);
}
let subUnitMap = unitMap.get(subUnitId);
if (!subUnitMap) {
subUnitMap = new Map();
unitMap.set(subUnitId, subUnitMap);
}
return subUnitMap;
}
private _registerSingleFormula(unitId: string, subUnitId: string, formula: string, ruleId: string) {
const ranges = [{ startColumn: 0, endColumn: 0, startRow: 0, endRow: 0 }];
return this._registerOtherFormulaService.registerFormulaWithRange(unitId, subUnitId, formula, ranges, { ruleId }, OtherFormulaBizType.DATA_VALIDATION, ruleId);
}
addRule(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
if (!shouldOffsetFormulaByRange(rule.type, this._validatorRegistryService) && 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);
}
}
removeRule(unitId: string, subUnitId: string, ruleId: string) {
const formulaRuleMap = this._ensureRuleFormulaMap(unitId, subUnitId);
const item = formulaRuleMap.get(ruleId);
if (!item) {
return;
}
const [formula1, formula2] = item;
const idList = [formula1?.id, formula2?.id].filter(Boolean) as string[];
idList.length && this._registerOtherFormulaService.deleteFormula(unitId, subUnitId, idList);
}
getRuleFormulaResult(unitId: string, subUnitId: string, ruleId: string): Promise<Nullable<[Nullable<IOtherFormulaResult>, Nullable<IOtherFormulaResult>]>> {
const ruleFormulaMap = this._ensureRuleFormulaMap(unitId, subUnitId);
const formulaInfo = ruleFormulaMap.get(ruleId);
if (!formulaInfo) {
return Promise.resolve(null);
}
const getResult = async (info: IFormulaInfo | undefined) => info && this._registerOtherFormulaService.getFormulaValue(unitId, subUnitId, info.id);
return Promise.all([
getResult(formulaInfo[0]),
getResult(formulaInfo[1]),
]);
}
getRuleFormulaResultSync(unitId: string, subUnitId: string, ruleId: string) {
const ruleFormulaMap = this._ensureRuleFormulaMap(unitId, subUnitId);
const formulaInfo = ruleFormulaMap.get(ruleId);
if (!formulaInfo) {
return undefined;
}
return formulaInfo.map((i) => {
if (i) {
return this._registerOtherFormulaService.getFormulaValueSync(unitId, subUnitId, i.id);
}
return undefined;
});
}
getRuleFormulaInfo(unitId: string, subUnitId: string, ruleId: string) {
const ruleFormulaMap = this._ensureRuleFormulaMap(unitId, subUnitId);
return ruleFormulaMap.get(ruleId);
}
}

View File

@@ -0,0 +1,194 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ICellData, IDataValidationRule, Nullable } from '@univerjs/core';
import type { IOtherFormulaResult } from '@univerjs/engine-formula';
import { Disposable, Inject, Injector, isFormulaString, numfmt } from '@univerjs/core';
import { DataValidationModel } from '@univerjs/data-validation';
import { deserializeListOptions } from '@univerjs/sheets';
import { isLegalFormulaResult } from '../utils/formula';
import { getCellValueOrigin } from '../utils/get-cell-data-origin';
import { DataValidationFormulaService } from './dv-formula.service';
export interface IListCacheItem {
list: string[];
listWithColor: Array<{ label: string; color: string }>;
colorMap: Record<string, string>;
set: Set<string>;
}
type UnitId = string;
type SubUnitId = string;
type RuleId = string;
/**
* Service for caching data validation list results.
* Cache is invalidated when formula results change (through markRuleDirty).
*/
export class DataValidationListCacheService extends Disposable {
// Cache structure: unitId -> subUnitId -> ruleId -> cache item
private _cache: Map<UnitId, Map<SubUnitId, Map<RuleId, IListCacheItem>>> = new Map();
constructor(
@Inject(Injector) private readonly _injector: Injector,
@Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel
) {
super();
this._initRuleChangeListener();
}
private _initRuleChangeListener(): void {
this.disposeWithMe(
this._dataValidationModel.ruleChange$.subscribe((change) => {
if (change.type === 'remove' || change.type === 'update') {
this.markRuleDirty(change.unitId, change.subUnitId, change.rule.uid);
}
})
);
}
/**
* Get cached list data or compute and cache it if not exists.
*/
getOrCompute(
unitId: string,
subUnitId: string,
rule: IDataValidationRule
): IListCacheItem {
// Check cache first
const cached = this.getCache(unitId, subUnitId, rule.uid);
if (cached) {
return cached;
}
const formulaService = this._injector.get(DataValidationFormulaService);
// Compute and cache
const results = formulaService.getRuleFormulaResultSync(unitId, subUnitId, rule.uid);
return this.computeAndCache(unitId, subUnitId, rule, results);
}
private _ensureCache(unitId: string, subUnitId: string): Map<RuleId, IListCacheItem> {
let unitMap = this._cache.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._cache.set(unitId, unitMap);
}
let subUnitMap = unitMap.get(subUnitId);
if (!subUnitMap) {
subUnitMap = new Map();
unitMap.set(subUnitId, subUnitMap);
}
return subUnitMap;
}
/**
* Get cached list data for a rule. Returns undefined if not cached.
*/
getCache(unitId: string, subUnitId: string, ruleId: string): IListCacheItem | undefined {
return this._cache.get(unitId)?.get(subUnitId)?.get(ruleId);
}
/**
* Set cache for a rule.
*/
setCache(unitId: string, subUnitId: string, ruleId: string, item: IListCacheItem): void {
const cache = this._ensureCache(unitId, subUnitId);
cache.set(ruleId, item);
}
/**
* Mark a rule's cache as dirty (invalidate it).
* Called when formula results change.
*/
markRuleDirty(unitId: string, subUnitId: string, ruleId: string): void {
this._cache.get(unitId)?.get(subUnitId)?.delete(ruleId);
}
/**
* Clear all caches.
*/
clear(): void {
this._cache.clear();
}
/**
* Compute list data from formula result and cache it.
*/
computeAndCache(
unitId: string,
subUnitId: string,
rule: IDataValidationRule,
formulaResult: Nullable<Nullable<IOtherFormulaResult>[]>
): IListCacheItem {
const { formula1 = '', formula2 = '' } = rule;
// Compute list
const list = isFormulaString(formula1)
? this._getRuleFormulaResultSet(formulaResult?.[0]?.result?.[0]?.[0])
: deserializeListOptions(formula1);
// Compute listWithColor and colorMap
const colorList = formula2.split(',');
const listWithColor = list.map((label, i) => ({ label, color: colorList[i] || '' }));
const colorMap: Record<string, string> = {};
for (const item of listWithColor) {
if (item.color) {
colorMap[item.label] = item.color;
}
}
const set = new Set(list);
const cacheItem: IListCacheItem = { list, listWithColor, colorMap, set };
this.setCache(unitId, subUnitId, rule.uid, cacheItem);
return cacheItem;
}
/**
* Extract string list from formula result cells.
*/
private _getRuleFormulaResultSet(result: Nullable<Nullable<ICellData>[][]>): string[] {
if (!result) {
return [];
}
const resultSet = new Set<string>();
for (let i = 0, rowLen = result.length; i < rowLen; i++) {
const row = result[i];
if (!row) continue;
for (let j = 0, colLen = row.length; j < colLen; j++) {
const cell = row[j];
const value = getCellValueOrigin(cell);
if (value !== null && value !== undefined) {
// Handle number format
if (typeof value !== 'string' && typeof cell?.s === 'object' && cell.s?.n?.pattern) {
resultSet.add(numfmt.format(cell.s.n.pattern, value, { throws: false }));
continue;
}
const valueStr = typeof value === 'string' ? value : String(value);
if (isLegalFormulaResult(valueStr)) {
resultSet.add(valueStr);
}
}
}
}
return [...resultSet];
}
}

View File

@@ -0,0 +1,260 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IDataValidationRule, IRange, Nullable, ObjectMatrix, Workbook, Worksheet } from '@univerjs/core';
import { bufferDebounceTime, DataValidationStatus, Disposable, getIntersectRange, Inject, IUniverInstanceService, LifecycleService, LifecycleStages, Range, Tools, UniverInstanceType } from '@univerjs/core';
import { bufferWhen, filter, skip } from 'rxjs';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
import { DataValidationCacheService } from './dv-cache.service';
export class SheetsDataValidationValidatorService extends Disposable {
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(SheetDataValidationModel) private readonly _sheetDataValidationModel: SheetDataValidationModel,
@Inject(DataValidationCacheService) private readonly _dataValidationCacheService: DataValidationCacheService,
@Inject(LifecycleService) private readonly _lifecycleService: LifecycleService
) {
super();
this._initRecalculate();
}
private _initRecalculate() {
const handleDirtyRanges = (ranges: { unitId: string; subUnitId: string; ranges: IRange[] }[]) => {
if (ranges.length === 0) {
return;
}
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const worksheet = workbook?.getActiveSheet();
const map: Record<string, Record<string, IRange[]>> = {};
ranges.flat().forEach((range) => {
if (!map[range.unitId]) {
map[range.unitId] = {};
}
if (!map[range.unitId][range.subUnitId]) {
map[range.unitId][range.subUnitId] = [];
}
const workbook = this._univerInstanceService.getUnit<Workbook>(range.unitId, UniverInstanceType.UNIVER_SHEET);
const worksheet = workbook?.getSheetBySheetId(range.subUnitId);
if (!worksheet) {
return;
}
map[range.unitId][range.subUnitId].push(...range.ranges.map((range) => Range.transformRange(range, worksheet)));
});
Object.entries(map).forEach(([unitId, subUnitMap]) => {
Object.entries(subUnitMap).forEach(([subUnitId, ranges]) => {
if (workbook?.getUnitId() === unitId && worksheet?.getSheetId() === subUnitId) {
this.validatorRanges(unitId, subUnitId, ranges);
} else {
requestIdleCallback(() => {
this.validatorRanges(unitId, subUnitId, ranges);
});
}
});
});
};
this.disposeWithMe(this._dataValidationCacheService.dirtyRanges$.pipe(bufferWhen(() => this._lifecycleService.lifecycle$.pipe(skip(1), filter((stage) => stage === LifecycleStages.Rendered)))).subscribe(handleDirtyRanges));
this.disposeWithMe(this._dataValidationCacheService.dirtyRanges$.pipe(filter(() => this._lifecycleService.stage >= LifecycleStages.Rendered), bufferDebounceTime(20)).subscribe(handleDirtyRanges));
}
private async _validatorByCell(workbook: Workbook, worksheet: Worksheet, row: number, col: number) {
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
if (!Tools.isDefine(row) || !Tools.isDefine(col)) {
throw new Error(`row or col is not defined, row: ${row}, col: ${col}`);
}
let _row = row;
let _col = col;
const mergedCell = worksheet.getMergedCell(row, col);
if (mergedCell) {
_row = mergedCell.startRow;
_col = mergedCell.startColumn;
}
const rule = this._sheetDataValidationModel.getRuleByLocation(unitId, subUnitId, _row, _col);
if (!rule) {
return DataValidationStatus.VALID;
}
return new Promise<DataValidationStatus>((resolve) => {
this._sheetDataValidationModel.validator(rule, { unitId, subUnitId, row: _row, col: _col, worksheet, workbook }, (status) => {
resolve(status);
});
});
}
async validatorCell(unitId: string, subUnitId: string, row: number, col: number) {
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
throw new Error(`cannot find current workbook, unitId: ${unitId}`);
}
const worksheet = workbook.getSheetBySheetId(subUnitId);
if (!worksheet) {
throw new Error(`cannot find current worksheet, sheetId: ${subUnitId}`);
}
return this._validatorByCell(workbook, worksheet, row, col);
}
async validatorRanges(unitId: string, subUnitId: string, ranges: IRange[]) {
if (!ranges.length) {
return Promise.resolve([]);
}
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
throw new Error(`cannot find current workbook, unitId: ${unitId}`);
}
const worksheet = workbook.getSheetBySheetId(subUnitId);
if (!worksheet) {
throw new Error(`cannot find current worksheet, sheetId: ${subUnitId}`);
}
const allRules = this._sheetDataValidationModel.getRules(unitId, subUnitId);
const ruleRanges: IRange[] = [];
for (const rule of allRules) {
ruleRanges.push(...rule.ranges);
}
const intersectRanges: IRange[] = [];
for (const range of ranges) {
for (const ruleRange of ruleRanges) {
const intersect = getIntersectRange(range, ruleRange);
if (intersect) {
intersectRanges.push(intersect);
}
}
}
const mergeCells: Array<{
resultRowIndex: number;
resultColIndex: number;
row: number;
col: number;
}> = [];
const result = await Promise.all(
intersectRanges.map((range, resultRowIndex) => {
const promises: Promise<DataValidationStatus>[] = [];
for (let row = range.startRow; row <= range.endRow; row++) {
for (let col = range.startColumn; col <= range.endColumn; col++) {
promises.push(this._validatorByCell(workbook, worksheet, row, col));
const mergedCell = worksheet.getMergedCell(row, col);
if (mergedCell) {
mergeCells.push({
resultRowIndex,
resultColIndex: promises.length - 1,
row: mergedCell.startRow,
col: mergedCell.startColumn,
});
}
}
}
return Promise.all(promises);
})
);
/**
* If this range has merged cells, the validation status of merged cells should be the same as the main cell, so we need to update the status of merged cells here after all validations are done.
* Because during the validation process, merged cells are all marked as VALIDATING status in cache.
*/
if (mergeCells.length) {
mergeCells.forEach(({ resultRowIndex, resultColIndex, row, col }) => {
if (result[resultRowIndex][resultColIndex] === DataValidationStatus.VALIDATING) {
result[resultRowIndex][resultColIndex] = this._dataValidationCacheService.getValue(unitId, subUnitId, row, col) ?? DataValidationStatus.VALID;
}
});
}
return result;
}
async validatorWorksheet(unitId: string, subUnitId: string) {
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
throw new Error(`cannot find current workbook, unitId: ${unitId}`);
}
const worksheet = workbook.getSheetBySheetId(subUnitId);
if (!worksheet) {
throw new Error(`cannot find current worksheet, sheetId: ${subUnitId}`);
}
const rules = this._sheetDataValidationModel.getRules(unitId, subUnitId);
await Promise.all(
rules.map((rule) => {
return Promise.all(
rule.ranges.map((range) => {
const promises: Promise<DataValidationStatus>[] = [];
Range.foreach(range, (row, col) => {
promises.push(this._validatorByCell(workbook, worksheet, row, col));
});
return Promise.all(promises);
})
);
})
);
return this._dataValidationCacheService.ensureCache(unitId, subUnitId);
}
async validatorWorkbook(unitId: string) {
const sheetIds = this._sheetDataValidationModel.getSubUnitIds(unitId);
const results = await Promise.all(sheetIds.map((id) => this.validatorWorksheet(unitId, id)));
const map: Record<string, ObjectMatrix<Nullable<DataValidationStatus>>> = {};
results.forEach((result, i) => {
map[sheetIds[i]] = result;
});
return map;
}
getDataValidations(unitId: string, subUnitId: string, ranges: IRange[]): IDataValidationRule[] {
const ruleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(unitId, subUnitId);
const ruleIdSet = new Set<string>();
ranges.forEach((range) => {
Range.foreach(range, (row, col) => {
const ruleId = ruleMatrix.getValue(row, col);
if (ruleId) {
ruleIdSet.add(ruleId);
}
});
});
const rules: IDataValidationRule[] = [];
for (const id of ruleIdSet) {
const rule = this._sheetDataValidationModel.getRuleById(unitId, subUnitId, id);
if (rule) {
rules.push(rule);
}
}
return rules;
}
getDataValidation(unitId: string, subUnitId: string, ranges: IRange[]): Nullable<IDataValidationRule> {
return this.getDataValidations(unitId, subUnitId, ranges)[0];
}
}

View File

@@ -0,0 +1,18 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export * from './operator-text-map';
export * from './two-formula-operators';

View File

@@ -0,0 +1,63 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationOperator } from '@univerjs/core';
export const OperatorTextMap: Record<DataValidationOperator, string> = {
[DataValidationOperator.BETWEEN]: 'sheets-data-validation.operators.between',
[DataValidationOperator.EQUAL]: 'sheets-data-validation.operators.equal',
[DataValidationOperator.GREATER_THAN]: 'sheets-data-validation.operators.greaterThan',
[DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'sheets-data-validation.operators.greaterThanOrEqual',
[DataValidationOperator.LESS_THAN]: 'sheets-data-validation.operators.lessThan',
[DataValidationOperator.LESS_THAN_OR_EQUAL]: 'sheets-data-validation.operators.lessThanOrEqual',
[DataValidationOperator.NOT_BETWEEN]: 'sheets-data-validation.operators.notBetween',
[DataValidationOperator.NOT_EQUAL]: 'sheets-data-validation.operators.notEqual',
};
export const OperatorTitleMap: Record<DataValidationOperator | 'NONE', string> = {
[DataValidationOperator.BETWEEN]: 'sheets-data-validation.ruleName.between',
[DataValidationOperator.EQUAL]: 'sheets-data-validation.ruleName.equal',
[DataValidationOperator.GREATER_THAN]: 'sheets-data-validation.ruleName.greaterThan',
[DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'sheets-data-validation.ruleName.greaterThanOrEqual',
[DataValidationOperator.LESS_THAN]: 'sheets-data-validation.ruleName.lessThan',
[DataValidationOperator.LESS_THAN_OR_EQUAL]: 'sheets-data-validation.ruleName.lessThanOrEqual',
[DataValidationOperator.NOT_BETWEEN]: 'sheets-data-validation.ruleName.notBetween',
[DataValidationOperator.NOT_EQUAL]: 'sheets-data-validation.ruleName.notEqual',
NONE: 'sheets-data-validation.ruleName.legal',
};
export const OperatorErrorTitleMap: Record<DataValidationOperator | 'NONE', string> = {
[DataValidationOperator.BETWEEN]: 'sheets-data-validation.errorMsg.between',
[DataValidationOperator.EQUAL]: 'sheets-data-validation.errorMsg.equal',
[DataValidationOperator.GREATER_THAN]: 'sheets-data-validation.errorMsg.greaterThan',
[DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'sheets-data-validation.errorMsg.greaterThanOrEqual',
[DataValidationOperator.LESS_THAN]: 'sheets-data-validation.errorMsg.lessThan',
[DataValidationOperator.LESS_THAN_OR_EQUAL]: 'sheets-data-validation.errorMsg.lessThanOrEqual',
[DataValidationOperator.NOT_BETWEEN]: 'sheets-data-validation.errorMsg.notBetween',
[DataValidationOperator.NOT_EQUAL]: 'sheets-data-validation.errorMsg.notEqual',
NONE: 'sheets-data-validation.errorMsg.legal',
};
export const TextLengthErrorTitleMap: Record<DataValidationOperator, string> = {
[DataValidationOperator.BETWEEN]: 'sheets-data-validation.textLength.errorMsg.between',
[DataValidationOperator.EQUAL]: 'sheets-data-validation.textLength.errorMsg.equal',
[DataValidationOperator.GREATER_THAN]: 'sheets-data-validation.textLength.errorMsg.greaterThan',
[DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'sheets-data-validation.textLength.errorMsg.greaterThanOrEqual',
[DataValidationOperator.LESS_THAN]: 'sheets-data-validation.textLength.errorMsg.lessThan',
[DataValidationOperator.LESS_THAN_OR_EQUAL]: 'sheets-data-validation.textLength.errorMsg.lessThanOrEqual',
[DataValidationOperator.NOT_BETWEEN]: 'sheets-data-validation.textLength.errorMsg.notBetween',
[DataValidationOperator.NOT_EQUAL]: 'sheets-data-validation.textLength.errorMsg.notEqual',
};

View File

@@ -0,0 +1,22 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationOperator } from '@univerjs/core';
export const TWO_FORMULA_OPERATOR_COUNT: DataValidationOperator[] = [
DataValidationOperator.BETWEEN,
DataValidationOperator.NOT_BETWEEN,
];

View File

@@ -0,0 +1,18 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export * from './const';
export * from './interface';

View File

@@ -0,0 +1,20 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export interface IUpdateDataValidationFormulaPayload {
type: 'formula1' | 'formula2';
formulaString: string;
}

View File

@@ -0,0 +1,17 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export * from './i-update-rule-formula';

View File

@@ -0,0 +1,47 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IAccessor } from '@univerjs/core';
import { DataValidationOperator, DataValidationType } from '@univerjs/core';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { describe, expect, it, vi } from 'vitest';
import { createDefaultNewRule } from '../create';
describe('createDefaultNewRule', () => {
it('uses the current selection ranges and default decimal criteria', () => {
const accessor = {
get(token: unknown) {
if (token === SheetsSelectionsService) {
return {
getCurrentSelections: vi.fn(() => [
{ range: { startRow: 1, endRow: 2, startColumn: 3, endColumn: 4 } },
]),
};
}
throw new Error(`Unknown token: ${String(token)}`);
},
} as IAccessor;
const rule = createDefaultNewRule(accessor);
expect(rule.uid).toHaveLength(6);
expect(rule.type).toBe(DataValidationType.DECIMAL);
expect(rule.operator).toBe(DataValidationOperator.EQUAL);
expect(rule.formula1).toBe('100');
expect(rule.ranges).toEqual([{ startRow: 1, endRow: 2, startColumn: 3, endColumn: 4 }]);
});
});

View File

@@ -0,0 +1,34 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IAccessor } from '@univerjs/core';
import { DataValidationOperator, DataValidationType, generateRandomId } from '@univerjs/core';
import { SheetsSelectionsService } from '@univerjs/sheets';
export function createDefaultNewRule(accessor: IAccessor) {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const currentRanges = selectionManagerService.getCurrentSelections().map((s) => s.range);
const uid = generateRandomId(6);
const rule = {
uid,
type: DataValidationType.DECIMAL,
operator: DataValidationOperator.EQUAL,
formula1: '100',
ranges: currentRanges ?? [{ startColumn: 0, endColumn: 0, startRow: 0, endRow: 0 }],
};
return rule;
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationType, ICellData, Nullable } from '@univerjs/core';
import type { DataValidatorRegistryService } from '@univerjs/data-validation';
import { ERROR_TYPE_SET } from '@univerjs/engine-formula';
export function getFormulaResult(result: Nullable<Nullable<ICellData>[][]>) {
return result?.[0]?.[0]?.v;
}
export function getFormulaCellData(result: Nullable<Nullable<ICellData>[][]>) {
return result?.[0]?.[0];
}
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 shouldOffsetFormulaByRange(type: DataValidationType | string, validatorRegistryService: DataValidatorRegistryService) {
const validator = validatorRegistryService.getValidatorItem(type);
return validator?.offsetFormulaByRange ?? false;
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ICellData, Nullable } from '@univerjs/core';
import { getOriginCellValue } from '@univerjs/core';
export function getCellValueOrigin(cell: Nullable<ICellData>) {
return getOriginCellValue(cell);
}
export function getStringCellValue(cell: Nullable<ICellData>) {
return String(getCellValueOrigin(cell) ?? '');
}

View File

@@ -0,0 +1,309 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ICellData, IWorkbookData, Workbook } from '@univerjs/core';
import {
CellValueType,
DataValidationRenderMode,
DataValidationType,
IUniverInstanceService,
LocaleService,
LocaleType,
Univer,
UniverInstanceType,
WrapStrategy,
} from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DataValidationFormulaService } from '../../services/dv-formula.service';
import { DataValidationListCacheService } from '../../services/dv-list-cache.service';
import { getRuleFormulaResultSet, isValidListFormula, ListValidator } from '../list-validator';
import { getDataValidationCellValue, getSheetRangeValueSet, getTransformedFormula } from '../util';
const univers: Univer[] = [];
function createContext() {
const univer = new Univer();
univers.push(univer);
const injector = univer.__getInjector();
const localeService = injector.get(LocaleService);
localeService.load({ [LocaleType.ZH_CN]: {} } as never);
const formulaService = {
getRuleFormulaResult: vi.fn(async () => ([{
result: [[[
[{ v: 'Blue' }, { v: 7, t: CellValueType.NUMBER, s: { n: { pattern: '0.00' } } }],
[{ v: null }, { v: 'Red' }],
]]],
}])),
};
const listCacheService = {
getOrCompute: vi.fn(() => ({
list: ['Red', 'Green', 'Blue'],
listWithColor: [{ label: 'Red', color: '#ff0000' }, { label: 'Green', color: '#00ff00' }],
colorMap: { Red: '#ff0000', Green: '#00ff00' },
})),
};
const lexerTreeBuilder = new LexerTreeBuilder();
injector.add([DataValidationFormulaService, { useValue: formulaService as never }]);
injector.add([DataValidationListCacheService, { useValue: listCacheService as never }]);
injector.add([LexerTreeBuilder, { useValue: lexerTreeBuilder }]);
injector.add([ListValidator]);
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, {
id: 'unit1',
appVersion: '3.0.0-alpha',
locale: LocaleType.EN_US,
name: 'Test workbook',
styles: {},
sheetOrder: ['sheet1'],
sheets: {
sheet1: {
id: 'sheet1',
name: 'Sheet1',
cellData: {
0: { 0: { v: 'Red' }, 1: { v: 'Green,Blue' } },
1: { 0: { v: '' }, 1: { v: 10 } },
},
},
},
});
const univerInstanceService = injector.get(IUniverInstanceService);
return {
workbook,
localeService,
formulaService,
listCacheService,
lexerTreeBuilder,
univerInstanceService,
injector,
validator: injector.get(ListValidator),
};
}
describe('list-validator helpers', () => {
afterEach(() => {
univers.splice(0).forEach((univer) => univer.dispose());
vi.restoreAllMocks();
});
it('collects formatted values and skips empty cells', () => {
const result = getRuleFormulaResultSet([
[
{
v: 1234,
s: {
n: {
pattern: '#,##0.00',
},
},
} as ICellData,
{
v: 'Alpha',
} as ICellData,
null,
],
[
{
v: undefined,
} as ICellData,
{
v: false,
} as ICellData,
],
]);
expect(result).toEqual(['1,234.00', 'Alpha', '', 'false']);
});
it('accepts direct references and supported helper formulas only', () => {
const lexer = new LexerTreeBuilder();
expect(isValidListFormula('Red,Green,Blue', lexer)).toBe(true);
expect(isValidListFormula('=A1:A3', lexer)).toBe(true);
expect(isValidListFormula('=IF(A1>0,B1:B3,C1:C3)', lexer)).toBe(true);
expect(isValidListFormula('=OFFSET(A1,0,0,2,1)', lexer)).toBe(true);
expect(isValidListFormula('=SUM(A1:A3)', lexer)).toBe(false);
lexer.dispose();
});
it('validates list formulas and rejects self-intersecting reference lists', () => {
const { validator } = createContext();
expect(validator.validatorFormula({ formula1: '' } as any, 'unit1', 'sheet1')).toEqual({
success: false,
formula1: 'sheets-data-validation.validFail.list',
});
expect(validator.validatorFormula({ formula1: '=SUM(A1:A2)' } as any, 'unit1', 'sheet1')).toEqual({
success: false,
formula1: 'sheets-data-validation.validFail.listInvalid',
});
expect(validator.validatorFormula({
formula1: '=A1:A2',
ranges: [{ startRow: 0, startColumn: 0, endRow: 1, endColumn: 0 }],
} as any, 'unit1', 'sheet1')).toEqual({
success: false,
formula1: 'sheets-data-validation.validFail.listIntersects',
});
expect(validator.validatorFormula({
formula1: '=Sheet2!A1:A2',
ranges: [{ startRow: 0, startColumn: 0, endRow: 1, endColumn: 0 }],
} as any, 'unit1', 'sheet1')).toEqual({
success: true,
formula1: undefined,
});
});
it('applies list option colors and validates list/list-multiple selections', async () => {
const { validator, listCacheService } = createContext();
const rule = {
uid: 'rule-list',
type: DataValidationType.LIST,
renderMode: DataValidationRenderMode.ARROW,
formula1: 'Red,Green,Blue',
} as any;
expect(validator.skipDefaultFontRender(rule)).toBe(true);
expect(validator.skipDefaultFontRender({ ...rule, renderMode: DataValidationRenderMode.TEXT })).toBe(false);
expect(validator.getExtraStyle(rule, 'Red', { style: { tb: WrapStrategy.OVERFLOW } as any })).toEqual({
bg: { rgb: '#ff0000' },
tb: WrapStrategy.CLIP,
});
expect(validator.getExtraStyle({ ...rule, renderMode: undefined }, 'Missing', { style: {} as any })).toEqual({
tb: WrapStrategy.WRAP,
});
expect(validator.parseCellValue('Red,Green,Blue')).toEqual(['Red', 'Green', 'Blue']);
await expect(validator.isValidType(
{ value: 'Green', unitId: 'unit1', subUnitId: 'sheet1' } as any,
{} as any,
rule
)).resolves.toBe(true);
await expect(validator.isValidType(
{ value: 'Missing', unitId: 'unit1', subUnitId: 'sheet1' } as any,
{} as any,
rule
)).resolves.toBe(false);
await expect(validator.isValidType(
{ value: 'Red,Blue', unitId: 'unit1', subUnitId: 'sheet1' } as any,
{} as any,
{ ...rule, type: DataValidationType.LIST_MULTIPLE }
)).resolves.toBe(true);
await expect(validator.isValidType(
{ value: undefined, unitId: 'unit1', subUnitId: 'sheet1' } as any,
{} as any,
rule
)).resolves.toBe(true);
expect(listCacheService.getOrCompute).toHaveBeenCalled();
});
it('reads cached and async list options for the active workbook', async () => {
const { validator, formulaService, listCacheService } = createContext();
const formulaRule = {
uid: 'formula-list',
type: DataValidationType.LIST,
formula1: '=Sheet1!A1:B2',
} as any;
expect(validator.getList(formulaRule, 'unit1', 'sheet1')).toEqual(['Red', 'Green', 'Blue']);
expect(validator.getListWithColor(formulaRule, 'unit1', 'sheet1')).toEqual([
{ label: 'Red', color: '#ff0000' },
{ label: 'Green', color: '#00ff00' },
]);
expect(validator.getListWithColorMap(formulaRule, 'unit1', 'sheet1')).toEqual({
Red: '#ff0000',
Green: '#00ff00',
});
await expect(validator.getListAsync(formulaRule, 'unit1', 'sheet1')).resolves.toEqual(['Blue', '7.00', 'Red']);
await expect(validator.getListAsync({ ...formulaRule, formula1: 'A,B' }, 'unit1', 'sheet1')).resolves.toEqual(['A', 'B']);
await expect(validator.parseFormula(formulaRule, 'unit1', 'sheet1')).resolves.toEqual({
formula1: undefined,
formula2: undefined,
isFormulaValid: true,
});
expect(validator.generateRuleName()).toBe('sheets-data-validation.list.name');
expect(validator.generateRuleErrorMessage()).toBe('sheets-data-validation.list.error');
expect(formulaService.getRuleFormulaResult).toHaveBeenCalled();
expect(listCacheService.getOrCompute).toHaveBeenCalled();
});
it('returns empty lists when no workbook can be resolved', async () => {
const testBed = createContext();
testBed.univerInstanceService.disposeUnit('unit1');
expect(testBed.validator.getList({ formula1: 'A,B' } as any, 'missing', 'sheet1')).toEqual([]);
expect(testBed.validator.getListWithColor({ formula1: 'A,B' } as any, 'missing', 'sheet1')).toEqual([]);
expect(testBed.validator.getListWithColorMap({ formula1: 'A,B' } as any, 'missing', 'sheet1')).toEqual({});
await expect(testBed.validator.getListAsync({ formula1: 'A,B' } as any, 'missing', 'sheet1')).resolves.toEqual([]);
});
it('falls back to the active worksheet when the requested worksheet is missing', async () => {
const { validator, workbook } = createContext();
const rule = { formula1: 'A,B' } as any;
expect(workbook.getSheetBySheetId('missing')).toBeUndefined();
expect(validator.getListWithColor(rule, 'unit1', 'missing')).toEqual([
{ label: 'Red', color: '#ff0000' },
{ label: 'Green', color: '#00ff00' },
]);
expect(validator.getListWithColorMap(rule, 'unit1', 'missing')).toEqual({
Red: '#ff0000',
Green: '#00ff00',
});
await expect(validator.getListAsync(rule, 'unit1', 'missing')).resolves.toEqual(['A', 'B']);
});
it('extracts values from sheet ranges and single cells', () => {
const { univerInstanceService } = createContext();
expect(getSheetRangeValueSet({
unitId: 'unit1',
sheetName: 'Sheet1',
range: { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 },
}, univerInstanceService as any, 'unit1', 'sheet1')).toEqual(['Red', 'Green,Blue', '10']);
expect(getDataValidationCellValue(null)).toBe('');
expect(getDataValidationCellValue({ v: 12 })).toBe('12');
expect(getDataValidationCellValue({ v: true })).toBe('true');
});
it('offsets formula references relative to the validated cell position', () => {
const lexer = {
moveFormulaRefOffset: vi.fn((formula: string, col: number, row: number) => `${formula}:${col}:${row}`),
};
expect(getTransformedFormula(lexer as any, {
formula1: '=A1',
formula2: 'B',
ranges: [{ startRow: 2, startColumn: 3, endRow: 2, endColumn: 3 }],
} as any, {
row: 5,
col: 7,
} as any)).toEqual({
transformedFormula1: '=A1:4:3',
transformedFormula2: 'B',
});
expect(lexer.moveFormulaRefOffset).toHaveBeenCalledWith('=A1', 4, 3, true);
});
it('collects legal formula results from mixed formula result cells', () => {
expect(getRuleFormulaResultSet([
[{ v: '#VALUE!' }, { v: 'OK' }],
[{ v: 3, s: { n: { pattern: '0.0' } } } as any],
] as any)).toEqual(['OK', '3.0']);
});
});

View File

@@ -0,0 +1,263 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { Injector, LocaleService } from '@univerjs/core';
import { CellValueType, DataValidationOperator, DataValidationType, DateSystem, LocaleType, WrapStrategy } from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { describe, expect, it, vi } from 'vitest';
import { DataValidationCustomFormulaService } from '../../services/dv-custom-formula.service';
import { DataValidationFormulaService } from '../../services/dv-formula.service';
import { DataValidationListCacheService } from '../../services/dv-list-cache.service';
import { AnyValidator } from '../any-validator';
import { CheckboxValidator, transformCheckboxValue } from '../checkbox-validator';
import { CustomFormulaValidator } from '../custom-validator';
import { DateValidator } from '../date-validator';
import { DecimalValidator, getCellValueNumber } from '../decimal-validator';
import { ListMultipleValidator } from '../list-multiple-validator';
import { ListValidator } from '../list-validator';
import { TextLengthValidator } from '../text-length-validator';
import { WholeValidator } from '../whole-validator';
function createContext() {
const localeService = {
t: vi.fn((key: string) => key),
} as unknown as LocaleService;
const customFormulaService = {
getCellFormulaValue: vi.fn(async () => ({ v: '12' })),
getCellFormula2Value: vi.fn(async () => ({ v: '34' })),
};
const formulaService = {
getRuleFormulaResult: vi.fn(async () => ([{ result: [[[[{ v: true }]]]] }, { result: [[[[{ v: false }]]]] }])),
getRuleFormulaResultSync: vi.fn(() => ([{ result: [[[[{ v: true }]]]] }, { result: [[[[{ v: false }]]]] }])),
};
const univerInstanceService = {
getUnit: vi.fn(() => ({
getDateSystem: (): DateSystem => DateSystem.Date1900,
getSnapshot: () => ({ locale: LocaleType.EN_US }),
getSheetBySheetId: vi.fn(() => ({
getName: () => 'sheet-1',
})),
})),
};
const listCacheService = {
getOrCompute: vi.fn(() => ({ list: ['a', 'b'] })),
};
const lexerTreeBuilder = {
moveFormulaRefOffset: vi.fn((formula: string, col: number, row: number) => `${formula}:${col},${row}`),
checkIfAddBracket: vi.fn(() => 0),
sequenceNodesBuilder: vi.fn(() => [{ nodeType: 'FUNCTION', token: 'IF' }]),
};
const injector = {
get(token: unknown) {
if (token === DataValidationCustomFormulaService) {
return customFormulaService;
}
if (token === DataValidationFormulaService) {
return formulaService;
}
if (token === DataValidationListCacheService) {
return listCacheService;
}
if (token === LexerTreeBuilder) {
return lexerTreeBuilder;
}
if (String(token) === 'univer.current') {
return univerInstanceService;
}
throw new Error(`Unknown token: ${String(token)}`);
},
} as unknown as Injector;
return {
localeService,
customFormulaService,
formulaService,
listCacheService,
lexerTreeBuilder,
injector,
univerInstanceService,
};
}
describe('validators', () => {
it('covers decimal, whole, and text-length validation branches', async () => {
const { localeService, injector, customFormulaService, lexerTreeBuilder } = createContext();
const decimal = new DecimalValidator(localeService, injector);
const whole = new WholeValidator(localeService, injector);
const textLength = new TextLengthValidator(localeService, injector);
customFormulaService.getCellFormulaValue.mockResolvedValueOnce({ v: '12.5' });
customFormulaService.getCellFormula2Value.mockResolvedValueOnce({ v: '#VALUE!' });
expect(getCellValueNumber('12.5')).toBe(12.5);
expect(await decimal.isValidType({ value: '12.5' } as never, {} as never, {} as never)).toBe(true);
expect(decimal.transform({ value: '12.5' } as never, {} as never, {} as never).value).toBe(12.5);
await expect(decimal.parseFormula({ uid: 'rule-1', formula1: '=A1', formula2: '=B1' } as never, 'u', 's', 0, 0)).resolves.toEqual({
formula1: 12.5,
formula2: Number.NaN,
isFormulaValid: false,
});
expect(decimal.validatorFormula({ operator: DataValidationOperator.BETWEEN, formula1: '1', formula2: 'x' } as never, 'u', 's')).toEqual({
success: false,
formula1: undefined,
formula2: 'sheets-data-validation.validFail.number',
});
expect(decimal.validatorFormula({ operator: DataValidationOperator.EQUAL, formula1: 'x' } as never, 'u', 's')).toEqual({
success: false,
formula1: 'sheets-data-validation.validFail.number',
});
expect(decimal.generateRuleErrorMessage({ operator: DataValidationOperator.EQUAL, formula1: '=A1', ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }] } as never, { row: 2, col: 3 } as never)).toBe('sheets-data-validation.errorMsg.equal');
expect(await whole.isValidType({ value: 3 } as never, {} as never, {} as never)).toBe(true);
expect(await whole.isValidType({ value: 3.2 } as never, {} as never, {} as never)).toBe(false);
expect(await whole.parseFormula({ uid: 'rule-2', formula1: '4', formula2: null } as never, 'u', 's', 0, 0)).toEqual({
formula1: 4,
formula2: Number.NaN,
isFormulaValid: true,
});
expect(whole.validatorFormula({ operator: DataValidationOperator.BETWEEN, formula1: '1', formula2: '2' } as never, 'u', 's')).toEqual({
success: true,
formula1: undefined,
formula2: undefined,
});
expect(textLength.validatorFormula({} as never, 'u', 's')).toEqual({ success: false });
expect(await textLength.parseFormula({ uid: 'rule-3', formula1: '3', formula2: '4' } as never, 'u', 's', 0, 0)).toEqual({
formula1: 3,
formula2: 4,
isFormulaValid: true,
});
expect(textLength.transform({ value: 1234 } as never, {} as never, {} as never).value).toBe(4);
expect(await textLength.isValidType({ value: true } as never, {} as never, {} as never)).toBe(false);
expect(await textLength.isValidType({ value: 'abcd' } as never, {} as never, {} as never)).toBe(true);
expect(textLength.generateRuleErrorMessage({ operator: DataValidationOperator.EQUAL, formula1: '=A1', ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }] } as never, { row: 1, col: 1 } as never)).toBe('sheets-data-validation.textLength.errorMsg.equal');
expect(lexerTreeBuilder.moveFormulaRefOffset).toHaveBeenCalled();
});
it('covers date validation parsing, normalization, and messages', async () => {
const { localeService, injector, customFormulaService, lexerTreeBuilder, univerInstanceService } = createContext();
const date = new DateValidator(localeService, injector);
customFormulaService.getCellFormulaValue.mockResolvedValueOnce({ v: '2024-01-02' });
customFormulaService.getCellFormula2Value.mockResolvedValueOnce({ v: '2024-01-05' });
const parsed = await date.parseFormula({ uid: 'rule-1', formula1: '=A1', formula2: '=B1' } as never, 'u', 's', 0, 0);
expect(parsed.formula1).toBeTypeOf('number');
expect(parsed.formula2).toBeTypeOf('number');
expect(parsed.isFormulaValid).toBe(true);
expect(await date.isValidType({ value: 45293, interceptValue: '2024-01-02' } as never)).toBe(true);
expect(await date.isValidType({ value: 'ignored', interceptValue: 'not-a-date' } as never)).toBe(false);
expect(date.validatorFormula({ operator: DataValidationOperator.BETWEEN, formula1: '2024-01-01', formula2: '' } as never, 'u', 's')).toEqual({
success: false,
formula1: undefined,
formula2: 'sheets-data-validation.validFail.date',
});
expect(date.normalizeFormula({ formula1: '45293', formula2: 'invalid', bizInfo: { showTime: false } } as never, 'u', 's')).toEqual({
formula1: '2024-01-02',
formula2: '',
});
univerInstanceService.getUnit.mockReturnValue({
getDateSystem: (): DateSystem => DateSystem.Date1904,
getSnapshot: () => ({ locale: LocaleType.EN_US }),
getSheetBySheetId: vi.fn(() => ({
getName: () => 'sheet-1',
})),
});
expect(date.normalizeFormula({ formula1: '1', bizInfo: { showTime: false } } as never, 'u', 's').formula1).toBe('1904-01-02');
expect(date.transform({ value: '2024-01-02' } as never, {} as never, {} as never).value).toBeTypeOf('number');
expect(date.generateRuleName({ operator: DataValidationOperator.EQUAL, formula1: '2024-01-02' } as never)).toContain('sheets-data-validation.date.title');
expect(date.generateRuleErrorMessage({ operator: DataValidationOperator.EQUAL, formula1: '=A1', ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }] } as never, { row: 0, col: 0 } as never)).toBe('sheets-data-validation.date.errorMsg.equal');
expect(lexerTreeBuilder.moveFormulaRefOffset).toHaveBeenCalled();
});
it('covers checkbox, custom, any, and list-multiple behaviors', async () => {
const { localeService, injector, formulaService, lexerTreeBuilder } = createContext();
const checkbox = new CheckboxValidator(localeService, injector);
const custom = new CustomFormulaValidator(localeService, injector);
const any = new AnyValidator(localeService, injector);
const listMultiple = new ListMultipleValidator(localeService, injector);
expect(transformCheckboxValue(true)).toBe('1');
expect(transformCheckboxValue(false)).toBe('0');
expect(transformCheckboxValue('other')).toBe('other');
expect(checkbox.validatorFormula({ formula1: '', formula2: '' } as never, 'u', 's')).toEqual({ success: true });
expect(checkbox.validatorFormula({ formula1: '1', formula2: '1' } as never, 'u', 's')).toEqual({
success: false,
formula1: 'sheets-data-validation.validFail.checkboxEqual',
formula2: 'sheets-data-validation.validFail.checkboxEqual',
});
expect(await checkbox.parseFormula({ uid: 'rule-1', formula1: '=A1', formula2: '=B1' } as never, 'u', 's')).toEqual({
formula1: '1',
formula2: '0',
originFormula1: true,
originFormula2: false,
isFormulaValid: true,
});
expect(checkbox.parseFormulaSync({ uid: 'rule-1', formula1: '=A1', formula2: '=B1' } as never, 'u', 's')).toEqual({
formula1: '1',
formula2: '0',
originFormula1: true,
originFormula2: false,
isFormulaValid: true,
});
expect(checkbox.skipDefaultFontRender!({ uid: 'rule-1', formula1: '=A1', formula2: '=B1' } as never, '0', { unitId: 'u', subUnitId: 's', row: 0, column: 0 })).toBe(true);
expect(await checkbox.isValidType({ value: false, unitId: 'u', subUnitId: 's' } as never, {} as never, { uid: 'rule-1', formula1: '=A1', formula2: '=B1' } as never)).toBe(true);
expect(checkbox.getExtraStyle({} as never, null)).toEqual({ tb: WrapStrategy.CLIP });
expect(checkbox.generateRuleErrorMessage({} as never)).toBe('sheets-data-validation.checkbox.error');
lexerTreeBuilder.checkIfAddBracket.mockReturnValueOnce(1);
expect(custom.validatorFormula({ formula1: 'A1' } as never, 'u', 's')).toEqual({
success: false,
formula1: 'sheets-data-validation.validFail.formula',
});
lexerTreeBuilder.checkIfAddBracket.mockReturnValueOnce(0);
expect(custom.validatorFormula({ formula1: '=A1' } as never, 'u', 's')).toEqual({
success: true,
formula1: '',
});
await expect(custom.parseFormula({} as never, 'u', 's')).resolves.toEqual({ formula1: undefined, formula2: undefined, isFormulaValid: true });
(injector.get(DataValidationCustomFormulaService) as any).getCellFormulaValue.mockResolvedValueOnce({ v: true, t: CellValueType.BOOLEAN });
expect(await custom.isValidType({ unitId: 'u', subUnitId: 's', row: 0, column: 0 } as never, {} as never, { uid: 'rule-2' } as never)).toBe(true);
(injector.get(DataValidationCustomFormulaService) as any).getCellFormulaValue.mockResolvedValueOnce({ v: '#VALUE!' });
expect(await custom.isValidType({ unitId: 'u', subUnitId: 's', row: 0, column: 0 } as never, {} as never, { uid: 'rule-2' } as never)).toBe(false);
expect(custom.generateRuleName({ formula1: '=A1' } as never)).toBe('sheets-data-validation.custom.ruleName');
await expect(any.parseFormula({ formula1: '1', formula2: '2' } as never, 'u', 's')).resolves.toEqual({ formula1: '1', formula2: '2', isFormulaValid: true });
expect(any.validatorFormula({} as never, 'u', 's')).toEqual({ success: true });
expect(await any.isValidType({} as never, {} as never, {} as never)).toBe(true);
expect(any.generateRuleErrorMessage({} as never)).toBe('sheets-data-validation.any.error');
expect(listMultiple.id).toBe('listMultiple');
expect(listMultiple.title).toBe('sheets-data-validation.listMultiple.title');
expect(listMultiple.offsetFormulaByRange).toBe(false);
expect(listMultiple.skipDefaultFontRender!()).toBe(true);
expect(formulaService.getRuleFormulaResult).toHaveBeenCalled();
});
it('treats comma-containing single list values as one selected value', async () => {
const { localeService, injector } = createContext();
const list = new ListValidator(localeService, injector);
await expect(list.isValidType(
{ value: '1,2', unitId: 'u', subUnitId: 's' } as never,
{} as never,
{ type: DataValidationType.LIST, formula1: '["1,2","3"]' } as never
)).resolves.toBe(true);
});
});

View File

@@ -0,0 +1,53 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { CellValue, DataValidationOperator, IDataValidationRule, IDataValidationRuleBase } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import type { LocaleKey } from '../locale/types';
import { DataValidationType } from '@univerjs/core';
import { BaseSheetValidator } from './base-sheet-validator';
export class AnyValidator extends BaseSheetValidator {
override id: string = DataValidationType.ANY;
override title: string = 'sheets-data-validation.any.title';
override operators: DataValidationOperator[] = [];
override scopes: string | string[] = ['sheet'];
order = 0;
override readonly offsetFormulaByRange = false;
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult> {
return {
formula1: rule.formula1,
formula2: rule.formula2,
isFormulaValid: true,
};
}
override validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult {
return {
success: true,
};
}
override async isValidType(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
return true;
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase): string {
return this.localeService.t<LocaleKey>('sheets-data-validation.any.error');
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IDataValidationRuleBase } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { BaseDataValidator } from '@univerjs/data-validation';
import { OperatorErrorTitleMap, OperatorTextMap, OperatorTitleMap } from '../types/const/operator-text-map';
export const FORMULA1 = '{FORMULA1}';
export const FORMULA2 = '{FORMULA2}';
export const TYPE = '{TYPE}';
export abstract class BaseSheetValidator extends BaseDataValidator {
override get operatorNames() {
return this.operators.map((operator) => this.localeService.t(OperatorTextMap[operator]));
}
override generateRuleName(rule: IDataValidationRuleBase): string {
if (!rule.operator) {
return this.localeService.t(OperatorTitleMap.NONE).replace(TYPE, this.titleStr);
}
const ruleName = this.localeService.t(OperatorTitleMap[rule.operator]).replace(FORMULA1, rule.formula1 ?? '').replace(FORMULA2, rule.formula2 ?? '');
return `${this.titleStr} ${ruleName}`;
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase, _position: ISheetLocationBase) {
if (!rule.operator) {
return this.localeService.t(OperatorErrorTitleMap.NONE).replace(TYPE, this.titleStr);
}
const errorMsg = this.localeService.t(OperatorErrorTitleMap[rule.operator]).replace(FORMULA1, rule.formula1 ?? '').replace(FORMULA2, rule.formula2 ?? '');
return `${errorMsg}`;
}
}

View File

@@ -0,0 +1,169 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 {
CellValue,
DataValidationOperator,
IDataValidationRule,
IDataValidationRuleBase,
ISheetDataValidationRule,
LocaleService,
Nullable,
} from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import type { LocaleKey } from '../locale/types';
import { DataValidationType, isFormulaString, Tools, WrapStrategy } from '@univerjs/core';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { getFormulaResult, isLegalFormulaResult } from '../utils/formula';
import { BaseSheetValidator } from './base-sheet-validator';
export const CHECKBOX_FORMULA_1 = 1;
export const CHECKBOX_FORMULA_2 = 0;
interface ICheckboxFormulaResult extends IFormulaResult {
originFormula1: Nullable<CellValue>;
originFormula2: Nullable<CellValue>;
}
function getFailMessage(formula: string | undefined, localeService: LocaleService) {
if (Tools.isBlank(formula)) {
return localeService.t<LocaleKey>('sheets-data-validation.validFail.value');
}
if (isFormulaString(formula)) {
return localeService.t<LocaleKey>('sheets-data-validation.validFail.primitive');
}
return '';
}
export const transformCheckboxValue = (value: Nullable<CellValue>) =>
Tools.isDefine(value) && String(value).toLowerCase() === 'true'
? '1'
: String(value).toLowerCase() === 'false'
? '0'
: value;
export class CheckboxValidator extends BaseSheetValidator {
override id: string = DataValidationType.CHECKBOX;
override title: string = 'sheets-data-validation.checkbox.title';
override operators: DataValidationOperator[] = [];
override scopes: string | string[] = ['sheet'];
order = 41;
override readonly offsetFormulaByRange = false;
private _formulaService = this.injector.get(DataValidationFormulaService);
override skipDefaultFontRender = (rule: ISheetDataValidationRule, cellValue: Nullable<CellValue>, pos: { unitId: string; subUnitId: string; row: number; column: number }) => {
const { unitId, subUnitId } = pos;
const { formula1, formula2 } = this.parseFormulaSync(rule, unitId, subUnitId);
const valueStr = `${cellValue ?? ''}`;
const res = !valueStr || (valueStr === (`${formula1}`) || valueStr === `${formula2}`);
return res;
};
override validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult {
const { formula1, formula2 } = rule;
const isEqual = formula1 === formula2;
if (Tools.isBlank(formula1) && Tools.isBlank(formula2)) {
return {
success: true,
};
}
if (isEqual) {
return {
success: false,
formula1: this.localeService.t<LocaleKey>('sheets-data-validation.validFail.checkboxEqual'),
formula2: this.localeService.t<LocaleKey>('sheets-data-validation.validFail.checkboxEqual'),
};
}
const error1 = getFailMessage(formula1, this.localeService);
const error2 = getFailMessage(formula2, this.localeService);
return {
success: (!error1 && !error2),
formula1: error1,
formula2: error2,
};
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<ICheckboxFormulaResult> {
const { formula1 = CHECKBOX_FORMULA_1, formula2 = CHECKBOX_FORMULA_2 } = rule;
const results = await this._formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
const originFormula1 = isFormulaString(formula1) ? getFormulaResult(results?.[0]?.result?.[0][0]) : formula1;
const originFormula2 = isFormulaString(formula2) ? getFormulaResult(results?.[1]?.result?.[0][0]) : formula2;
const isFormulaValid = isLegalFormulaResult(String(originFormula1)) && isLegalFormulaResult(String(originFormula2));
return {
formula1: transformCheckboxValue(originFormula1),
formula2: transformCheckboxValue(originFormula2),
originFormula1,
originFormula2,
isFormulaValid,
};
}
override getExtraStyle(rule: IDataValidationRule, value: Nullable<CellValue>) {
return {
tb: WrapStrategy.CLIP,
};
}
parseFormulaSync(rule: IDataValidationRule, unitId: string, subUnitId: string): ICheckboxFormulaResult {
const { formula1 = CHECKBOX_FORMULA_1, formula2 = CHECKBOX_FORMULA_2 } = rule;
const results = this._formulaService.getRuleFormulaResultSync(unitId, subUnitId, rule.uid);
const originFormula1 = isFormulaString(formula1) ? getFormulaResult(results?.[0]?.result?.[0][0]) : formula1;
const originFormula2 = isFormulaString(formula2) ? getFormulaResult(results?.[1]?.result?.[0][0]) : formula2;
const isFormulaValid = isLegalFormulaResult(String(originFormula1)) && isLegalFormulaResult(String(originFormula2)); // TODO@Dushusir Handling return type errors, not sure if this is correct
return {
formula1: transformCheckboxValue(originFormula1),
formula2: transformCheckboxValue(originFormula2),
originFormula1,
originFormula2,
isFormulaValid,
};
}
override async isValidType(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule): Promise<boolean> {
const { value, unitId, subUnitId } = cellInfo;
const { formula1, formula2, originFormula1, originFormula2 } = await this.parseFormula(rule, unitId, subUnitId);
if (!Tools.isDefine(formula1) || !Tools.isDefine(formula2)) {
return true;
}
return Tools.isDefine(value) && (
String(value) === String(formula1)
|| String(value) === String(formula2)
|| String(value) === String(originFormula1 ?? '')
|| String(value) === String(originFormula2 ?? '')
);
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase): string {
return this.localeService.t<LocaleKey>('sheets-data-validation.checkbox.error');
}
override generateRuleName(rule: IDataValidationRuleBase): string {
return this.titleStr;
}
}

View File

@@ -0,0 +1,18 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export const FORMULA1 = '{FORMULA1}';
export const FORMULA2 = '{FORMULA2}';

View File

@@ -0,0 +1,95 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { CellValue, DataValidationOperator, IDataValidationRule, IDataValidationRuleBase } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import type { LocaleKey } from '../locale/types';
import { CellValueType, DataValidationType, isFormulaString, Tools } from '@univerjs/core';
import { LexerTreeBuilder, operatorToken } from '@univerjs/engine-formula';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { isLegalFormulaResult } from '../utils/formula';
import { BaseSheetValidator } from './base-sheet-validator';
export class CustomFormulaValidator extends BaseSheetValidator {
override id: string = DataValidationType.CUSTOM;
override title: string = 'sheets-data-validation.custom.title';
override operators: DataValidationOperator[] = [];
override scopes: string | string[] = ['sheet'];
order = 60;
private readonly _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
private readonly _lexerTreeBuilder = this.injector.get(LexerTreeBuilder);
override validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult {
const success = isFormulaString(rule.formula1);
const formulaText = rule.formula1 ?? '';
const result = this._lexerTreeBuilder.checkIfAddBracket(formulaText);
const valid = result === 0 && formulaText.startsWith(operatorToken.EQUALS);
return {
success: success && valid,
formula1: success && valid ? '' : this.localeService.t<LocaleKey>('sheets-data-validation.validFail.formula'),
};
}
override async parseFormula(_rule: IDataValidationRule, _unitId: string, _subUnitId: string): Promise<IFormulaResult> {
return {
formula1: undefined,
formula2: undefined,
isFormulaValid: true,
};
}
override async isValidType(cellInfo: IValidatorCellInfo<CellValue>, _formula: IFormulaResult, _rule: IDataValidationRule): Promise<boolean> {
const { column, row, unitId, subUnitId } = cellInfo;
const cellData = await this._customFormulaService.getCellFormulaValue(unitId, subUnitId, _rule.uid, row, column);
const formulaResult = cellData?.v;
if (!isLegalFormulaResult(String(formulaResult))) {
return false;
}
if (Tools.isDefine(formulaResult) && formulaResult !== '') {
if (cellData!.t === CellValueType.BOOLEAN) {
return Boolean(formulaResult);
}
if (typeof formulaResult === 'boolean') {
return formulaResult;
}
if (typeof formulaResult === 'number') {
return Boolean(formulaResult);
}
if (typeof formulaResult === 'string') {
return isLegalFormulaResult(formulaResult);
}
return Boolean(formulaResult);
}
return false;
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase): string {
return this.localeService.t<LocaleKey>('sheets-data-validation.custom.error');
}
override generateRuleName(rule: IDataValidationRuleBase): string {
return (this.localeService.t<LocaleKey>('sheets-data-validation.custom.ruleName')).replace('{FORMULA1}', rule.formula1 ?? '');
}
}

View File

@@ -0,0 +1,192 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { CellValue, IDataValidationRule, IDataValidationRuleBase, ISheetDataValidationRule, Nullable, ParseOptions, Workbook } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { LocaleKey } from '../locale/types';
import { DataValidationOperator, DataValidationType, DateSystem, excelSerialToDateTimeParts, getNumfmtLocaleTag, isFormulaString, IUniverInstanceService, numfmt, Tools, UniverInstanceType } from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { DateOperatorErrorTitleMap, DateOperatorNameMap, DateOperatorTitleMap } from '../common/date-text-map';
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 { BaseSheetValidator } from './base-sheet-validator';
import { FORMULA1, FORMULA2 } from './const';
import { getTransformedFormula } from './util';
const transformDate2SerialNumber = (value: Nullable<CellValue>, options: ParseOptions = {}): number | undefined => {
if (value === undefined || value === null || typeof value === 'boolean') {
return undefined;
}
if (typeof value === 'number' || !Number.isNaN(+value)) {
return +value;
}
// transform date string to serial number
return numfmt.parseDate(value, options)?.v as number | undefined;
};
function pad(value: number): string {
return String(value).padStart(2, '0');
}
export class DateValidator extends BaseSheetValidator {
id: string = DataValidationType.DATE;
title: string = 'sheets-data-validation.date.title';
order = 40;
operators: DataValidationOperator[] = [
DataValidationOperator.BETWEEN,
DataValidationOperator.EQUAL,
DataValidationOperator.GREATER_THAN,
DataValidationOperator.GREATER_THAN_OR_EQUAL,
DataValidationOperator.LESS_THAN,
DataValidationOperator.LESS_THAN_OR_EQUAL,
DataValidationOperator.NOT_BETWEEN,
DataValidationOperator.NOT_EQUAL,
];
scopes: string | string[] = ['sheet'];
private readonly _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
private readonly _lexerTreeBuilder = this.injector.get(LexerTreeBuilder);
private readonly _univerInstanceService = this.injector.get(IUniverInstanceService);
private _getParseOptions(unitId?: string, workbook?: Workbook): ParseOptions {
const targetWorkbook = workbook ?? (unitId
? this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET)
: undefined);
return targetWorkbook
? { locale: getNumfmtLocaleTag(targetWorkbook.getSnapshot().locale), dateSystem: targetWorkbook.getDateSystem() }
: { dateSystem: DateSystem.Date1900 };
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult<number | undefined>> {
const formulaResult1 = await this._customFormulaService.getCellFormulaValue(unitId, subUnitId, rule.uid, row, column);
const formulaResult2 = await this._customFormulaService.getCellFormula2Value(unitId, subUnitId, rule.uid, row, column);
const { formula1, formula2 } = rule;
const isFormulaValid = isLegalFormulaResult(String(formulaResult1?.v)) && isLegalFormulaResult(String(formulaResult2?.v));
return {
formula1: transformDate2SerialNumber(isFormulaString(formula1) ? formulaResult1?.v : formula1, this._getParseOptions(unitId)),
formula2: transformDate2SerialNumber(isFormulaString(formula2) ? formulaResult2?.v : formula2, this._getParseOptions(unitId)),
isFormulaValid,
};
}
override async isValidType(info: IValidatorCellInfo): Promise<boolean> {
const { interceptValue, value } = info;
const options = this._getParseOptions(info.unitId, info.workbook);
if (typeof value === 'number' && typeof interceptValue === 'string') {
return Boolean(numfmt.parseDate(interceptValue, options));
}
if (typeof interceptValue === 'string') {
return Boolean(numfmt.parseDate(interceptValue, options));
}
return false;
}
private _validatorSingleFormula(formula: string | undefined, options: ParseOptions) {
return !Tools.isBlank(formula) && (isFormulaString(formula) || !Number.isNaN(+formula!) || Boolean(formula && numfmt.parseDate(formula, options)));
}
override validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult {
const operator = rule.operator;
if (!operator) {
return {
success: true,
};
}
const options = this._getParseOptions(unitId);
const formula1Success = this._validatorSingleFormula(rule.formula1, options);
const errorMsg = this.localeService.t<LocaleKey>('sheets-data-validation.validFail.date');
const isTwoFormula = TWO_FORMULA_OPERATOR_COUNT.includes(operator);
if (isTwoFormula) {
const formula2Success = this._validatorSingleFormula(rule.formula2, options);
return {
success: formula1Success && formula2Success,
formula1: formula1Success ? undefined : errorMsg,
formula2: formula2Success ? undefined : errorMsg,
};
}
return {
success: formula1Success,
formula1: formula1Success ? undefined : errorMsg,
};
}
override normalizeFormula(rule: IDataValidationRule, unitId: string, _subUnitId: string): { formula1: string | undefined; formula2: string | undefined } {
const { formula1, formula2, bizInfo } = rule;
const options = this._getParseOptions(unitId);
const normlizeSingleFormula = (formula: string | undefined) => {
if (!formula) {
return formula;
}
const serial = !Number.isNaN(+formula) ? +formula : numfmt.parseDate(formula, options)?.v;
if (typeof serial !== 'number') {
return '';
}
const date = excelSerialToDateTimeParts(serial, { dateSystem: options.dateSystem ?? DateSystem.Date1900 });
if (!date) return '';
const dateText = `${String(date.year).padStart(4, '0')}-${pad(date.month)}-${pad(date.day)}`;
return bizInfo?.showTime ? `${dateText} ${pad(date.hours)}:${pad(date.minutes)}:${pad(date.seconds)}` : dateText;
};
return {
formula1: isFormulaString(formula1) ? formula1 : normlizeSingleFormula(`${formula1}`),
formula2: isFormulaString(formula2) ? formula2 : normlizeSingleFormula(`${formula2}`),
};
}
override transform(cellInfo: IValidatorCellInfo<CellValue>, _formula: IFormulaResult, _rule: IDataValidationRule): IValidatorCellInfo<number> {
const { value } = cellInfo;
return {
...cellInfo,
value: transformDate2SerialNumber(value, this._getParseOptions(cellInfo.unitId, cellInfo.workbook))!,
};
}
override get operatorNames() {
return this.operators.map((operator) => this.localeService.t(DateOperatorNameMap[operator]));
}
override generateRuleName(rule: IDataValidationRuleBase): string {
if (!rule.operator) {
return this.localeService.t(DateOperatorTitleMap.NONE);
}
const ruleName = this.localeService.t(DateOperatorTitleMap[rule.operator]).replace(FORMULA1, rule.formula1 ?? '').replace(FORMULA2, rule.formula2 ?? '');
return `${this.titleStr} ${ruleName}`;
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase, pos: ISheetLocationBase) {
if (!rule.operator) {
return this.titleStr;
}
const { transformedFormula1, transformedFormula2 } = getTransformedFormula(this._lexerTreeBuilder, rule as ISheetDataValidationRule, pos);
const errorMsg = this.localeService.t(DateOperatorErrorTitleMap[rule.operator]).replace(FORMULA1, transformedFormula1 ?? '').replace(FORMULA2, transformedFormula2 ?? '');
return `${errorMsg}`;
}
}

View File

@@ -0,0 +1,131 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { CellValue, IDataValidationRule, IDataValidationRuleBase, ISheetDataValidationRule, Nullable } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { LocaleKey } from '../locale/types';
import { DataValidationOperator, DataValidationType, isFormulaString, Tools } from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { OperatorErrorTitleMap } from '../types';
import { TWO_FORMULA_OPERATOR_COUNT } from '../types/const/two-formula-operators';
import { isLegalFormulaResult } from '../utils/formula';
import { BaseSheetValidator } from './base-sheet-validator';
import { FORMULA1, FORMULA2 } from './const';
import { getTransformedFormula } from './util';
export function getCellValueNumber(cellValue: CellValue) {
return +cellValue;
}
export class DecimalValidator extends BaseSheetValidator {
private readonly _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
id: string = DataValidationType.DECIMAL;
private readonly _lexerTreeBuilder = this.injector.get(LexerTreeBuilder);
title: string = 'sheets-data-validation.decimal.title';
order = 20;
operators: DataValidationOperator[] = [
DataValidationOperator.BETWEEN,
DataValidationOperator.EQUAL,
DataValidationOperator.GREATER_THAN,
DataValidationOperator.GREATER_THAN_OR_EQUAL,
DataValidationOperator.LESS_THAN,
DataValidationOperator.LESS_THAN_OR_EQUAL,
DataValidationOperator.NOT_BETWEEN,
DataValidationOperator.NOT_EQUAL,
];
scopes: string | string[] = ['sheet'];
private _isFormulaOrNumber(formula: string) {
return !Tools.isBlank(formula) && (isFormulaString(formula) || !Number.isNaN(+formula));
}
override async isValidType(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule) {
const { value: cellValue } = cellInfo;
return !Number.isNaN(getCellValueNumber(cellValue));
}
override transform(cellInfo: IValidatorCellInfo<CellValue>, formula: IFormulaResult, rule: IDataValidationRule) {
const { value: cellValue } = cellInfo;
return {
...cellInfo,
value: getCellValueNumber(cellValue),
};
}
private _parseNumber(formula: Nullable<string | number | boolean>) {
if (formula === undefined || formula === null) {
return Number.NaN;
}
return +formula;
}
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 isFormulaValid = isLegalFormulaResult(String(formulaResult1?.v)) && isLegalFormulaResult(String(formulaResult2?.v));
const info = {
formula1: this._parseNumber(isFormulaString(formula1) ? formulaResult1?.v : formula1),
formula2: this._parseNumber(isFormulaString(formula2) ? formulaResult2?.v : formula2),
isFormulaValid,
};
return info;
}
override validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult {
const operator = rule.operator;
if (!operator) {
return {
success: true,
};
}
const formula1Success = Tools.isDefine(rule.formula1) && this._isFormulaOrNumber(rule.formula1);
const formula2Success = Tools.isDefine(rule.formula2) && this._isFormulaOrNumber(rule.formula2);
const isTwoFormula = TWO_FORMULA_OPERATOR_COUNT.includes(operator);
const errorMsg = this.localeService.t<LocaleKey>('sheets-data-validation.validFail.number');
if (isTwoFormula) {
return {
success: formula1Success && formula2Success,
formula1: formula1Success ? undefined : errorMsg,
formula2: formula2Success ? undefined : errorMsg,
};
}
return {
success: formula1Success,
formula1: formula1Success ? '' : errorMsg,
};
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase, position: ISheetLocationBase) {
if (!rule.operator) {
return this.localeService.t(OperatorErrorTitleMap.NONE).replace('{TYPE}', this.titleStr);
}
const { transformedFormula1, transformedFormula2 } = getTransformedFormula(this._lexerTreeBuilder, rule as ISheetDataValidationRule, position);
const errorMsg = this.localeService.t(OperatorErrorTitleMap[rule.operator]).replace(FORMULA1, transformedFormula1 ?? '').replace(FORMULA2, transformedFormula2 ?? '');
return `${errorMsg}`;
}
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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.
*/
export { CHECKBOX_FORMULA_1, CHECKBOX_FORMULA_2, CheckboxValidator } from './checkbox-validator';
export { DateValidator } from './date-validator';
export { DecimalValidator } from './decimal-validator';
export { ListValidator } from './list-validator';
export { TextLengthValidator } from './text-length-validator';

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DataValidationType } from '@univerjs/core';
import { ListValidator } from './list-validator';
export class ListMultipleValidator extends ListValidator {
override id: string = DataValidationType.LIST_MULTIPLE;
override title: string = 'sheets-data-validation.listMultiple.title';
override readonly offsetFormulaByRange = false;
override skipDefaultFontRender = () => {
return true;
};
}

View File

@@ -0,0 +1,279 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 {
CellValue,
DataValidationOperator,
ICellData,
IDataValidationRule,
IRange,
ISheetDataValidationRule,
IStyleData,
Nullable,
Workbook,
} from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import type { LocaleKey } from '../locale/types';
import {
DataValidationRenderMode,
DataValidationType,
isFormulaString,
IUniverInstanceService,
numfmt,
Rectangle,
Tools,
UniverInstanceType,
WrapStrategy,
} from '@univerjs/core';
import {
deserializeRangeWithSheet,
isReferenceString,
LexerTreeBuilder,
sequenceNodeType,
} from '@univerjs/engine-formula';
import { deserializeListOptions } from '@univerjs/sheets';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { DataValidationListCacheService } from '../services/dv-list-cache.service';
import { getFormulaResult, isLegalFormulaResult } from '../utils/formula';
import { getCellValueOrigin } from '../utils/get-cell-data-origin';
import { BaseSheetValidator } from './base-sheet-validator';
// Keep getRuleFormulaResultSet for backward compatibility (used in isValidType)
export function getRuleFormulaResultSet(result: Nullable<Nullable<ICellData>[][]>): string[] {
if (!result) {
return [];
}
const resultSet = new Set<string>();
for (let i = 0, rowLen = result.length; i < rowLen; i++) {
const row = result[i];
if (!row) continue;
for (let j = 0, colLen = row.length; j < colLen; j++) {
const cell = row[j];
const value = getCellValueOrigin(cell);
if (value !== null && value !== undefined) {
if (typeof value !== 'string' && typeof cell?.s === 'object' && cell.s?.n?.pattern) {
resultSet.add(numfmt.format(cell.s.n.pattern, value, { throws: false }));
continue;
}
const valueStr = typeof value === 'string' ? value : String(value);
if (isLegalFormulaResult(valueStr)) {
resultSet.add(valueStr);
}
}
}
}
return [...resultSet];
}
const supportedFormula = [
'if',
'indirect',
'choose',
'offset',
];
// 1. must have REFERENCE or DEFINED_NAME node.
// 2. only support some formula
export function isValidListFormula(formula: string, lexer: LexerTreeBuilder) {
if (!isFormulaString(formula)) {
return true;
}
const isRefString = isReferenceString(formula.slice(1));
if (isRefString) {
return true;
}
const nodes = lexer.sequenceNodesBuilder(formula);
return (nodes) && nodes.some((node) => typeof node === 'object' && node.nodeType === sequenceNodeType.FUNCTION && supportedFormula.indexOf(node.token.toLowerCase()) > -1);
}
function isRuleIntersects(rule: IDataValidationRule, sheetName: string) {
const { formula1 = '', ranges } = rule;
const isRefString = isReferenceString(formula1.slice(1));
if (isRefString) {
const refRange = deserializeRangeWithSheet(formula1.slice(1));
if ((!refRange.sheetName || refRange.sheetName === sheetName) && ranges.some((range: IRange) => Rectangle.intersects(range, refRange.range))) {
return true;
}
}
return false;
}
export class ListValidator extends BaseSheetValidator {
protected formulaService = this.injector.get(DataValidationFormulaService);
private _lexer = this.injector.get(LexerTreeBuilder);
private _univerInstanceService = this.injector.get(IUniverInstanceService);
private _listCacheService = this.injector.get(DataValidationListCacheService);
order = 50;
override readonly offsetFormulaByRange = false;
id: string = DataValidationType.LIST;
title: string = 'sheets-data-validation.list.title';
operators: DataValidationOperator[] = [];
scopes: string | string[] = ['sheet'];
override skipDefaultFontRender = (rule: ISheetDataValidationRule) => {
return rule.renderMode !== DataValidationRenderMode.TEXT;
};
override validatorFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): IFormulaValidResult {
const success = !Tools.isBlank(rule.formula1);
const valid = isValidListFormula(rule.formula1 ?? '', this._lexer);
const sheetName = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(subUnitId)?.getName();
const isIntersects = isRuleIntersects(rule, sheetName ?? '');
return {
success: Boolean(success && valid && !isIntersects),
formula1: success
? valid
? !isIntersects ?
undefined :
this.localeService.t<LocaleKey>('sheets-data-validation.validFail.listIntersects') :
this.localeService.t<LocaleKey>('sheets-data-validation.validFail.listInvalid')
: this.localeService.t<LocaleKey>('sheets-data-validation.validFail.list'),
};
}
override getExtraStyle(rule: IDataValidationRule, value: Nullable<CellValue>, { style: defaultStyle }: { style: IStyleData }): Nullable<IStyleData> {
const tb = (defaultStyle.tb !== WrapStrategy.OVERFLOW ? defaultStyle.tb : WrapStrategy.CLIP) ?? WrapStrategy.WRAP;
if (rule.type === DataValidationType.LIST && (rule.renderMode === DataValidationRenderMode.ARROW || rule.renderMode === DataValidationRenderMode.TEXT)) {
const colorMap = this.getListWithColorMap(rule);
const valueStr = `${value ?? ''}`;
const color = colorMap[valueStr];
if (color) {
return {
bg: {
rgb: color,
},
tb,
};
}
}
return {
tb,
};
}
parseCellValue(cellValue: CellValue) {
const cellString = cellValue.toString();
return deserializeListOptions(cellString);
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult<number | undefined>> {
const results = await this.formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
const formulaResult1 = getFormulaResult(results?.[0]?.result?.[0][0]);
const isFormulaValid = isLegalFormulaResult(String(formulaResult1));
return {
formula1: undefined,
formula2: undefined,
isFormulaValid,
};
}
override async isValidType(cellInfo: IValidatorCellInfo<Nullable<CellValue>>, formula: IFormulaResult<string[] | undefined>, rule: IDataValidationRule): Promise<boolean> {
const { value, unitId, subUnitId } = cellInfo;
const { formula1 = '' } = rule;
const formula1Result = isFormulaString(formula1)
? this._listCacheService.getOrCompute(
unitId,
subUnitId,
rule
).list
: deserializeListOptions(formula1);
const selected = rule.type === DataValidationType.LIST
? (value === undefined || value === null || value === '' ? [] : [`${value}`])
: this.parseCellValue(value!);
return selected.every((i) => formula1Result.includes(i));
}
override generateRuleName() {
return this.localeService.t<LocaleKey>('sheets-data-validation.list.name');
}
override generateRuleErrorMessage(): string {
return this.localeService.t<LocaleKey>('sheets-data-validation.list.error');
}
private _getUnitAndSubUnit(currentUnitId?: string, currentSubUnitId?: string): { unitId: string; subUnitId: string } | null {
const workbook = (currentUnitId ? this._univerInstanceService.getUnit<Workbook>(currentUnitId, UniverInstanceType.UNIVER_SHEET) : undefined)
?? this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) return null;
const worksheet = (currentSubUnitId ? workbook.getSheetBySheetId(currentSubUnitId) : undefined) ?? workbook.getActiveSheet();
if (!worksheet) return null;
return {
unitId: workbook.getUnitId(),
subUnitId: worksheet.getSheetId(),
};
}
getList(rule: IDataValidationRule, currentUnitId?: string, currentSubUnitId?: string): string[] {
const location = this._getUnitAndSubUnit(currentUnitId, currentSubUnitId);
if (!location) return [];
const { unitId, subUnitId } = location;
return this._listCacheService.getOrCompute(
unitId,
subUnitId,
rule
).list;
}
async getListAsync(rule: IDataValidationRule, currentUnitId?: string, currentSubUnitId?: string) {
const { formula1 = '' } = rule;
const location = this._getUnitAndSubUnit(currentUnitId, currentSubUnitId);
if (!location) return [];
const { unitId, subUnitId } = location;
const results = await this.formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
return isFormulaString(formula1) ? getRuleFormulaResultSet(results?.[0]?.result?.[0][0]) : deserializeListOptions(formula1);
}
getListWithColor(rule: IDataValidationRule, currentUnitId?: string, currentSubUnitId?: string): Array<{ label: string; color: string }> {
const location = this._getUnitAndSubUnit(currentUnitId, currentSubUnitId);
if (!location) return [];
const { unitId, subUnitId } = location;
return this._listCacheService.getOrCompute(
unitId,
subUnitId,
rule
).listWithColor;
}
getListWithColorMap(rule: IDataValidationRule, currentUnitId?: string, currentSubUnitId?: string): Record<string, string> {
const location = this._getUnitAndSubUnit(currentUnitId, currentSubUnitId);
if (!location) return {};
const { unitId, subUnitId } = location;
return this._listCacheService.getOrCompute(
unitId,
subUnitId,
rule
).colorMap;
}
}

View File

@@ -0,0 +1,124 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { CellValue, IDataValidationRule, IDataValidationRuleBase, ISheetDataValidationRule, Nullable } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { LocaleKey } from '../locale/types';
import { DataValidationOperator, DataValidationType, isFormulaString, Tools } from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { TextLengthErrorTitleMap } from '../types/const/operator-text-map';
import { TWO_FORMULA_OPERATOR_COUNT } from '../types/const/two-formula-operators';
import { isLegalFormulaResult } from '../utils/formula';
import { BaseSheetValidator } from './base-sheet-validator';
import { FORMULA1, FORMULA2 } from './const';
import { getTransformedFormula } from './util';
export class TextLengthValidator extends BaseSheetValidator {
id: string = DataValidationType.TEXT_LENGTH;
title: string = 'sheets-data-validation.textLength.title';
private readonly _lexerTreeBuilder = this.injector.get(LexerTreeBuilder);
order = 30;
operators: DataValidationOperator[] = [
DataValidationOperator.BETWEEN,
DataValidationOperator.EQUAL,
DataValidationOperator.GREATER_THAN,
DataValidationOperator.GREATER_THAN_OR_EQUAL,
DataValidationOperator.LESS_THAN,
DataValidationOperator.LESS_THAN_OR_EQUAL,
DataValidationOperator.NOT_BETWEEN,
DataValidationOperator.NOT_EQUAL,
];
scopes: string | string[] = ['sheet'];
private readonly _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
private _isFormulaOrInt(formula: string) {
return !Tools.isBlank(formula) && (isFormulaString(formula) || (!Number.isNaN(+formula) && Number.isInteger(+formula)));
}
override validatorFormula(rule: IDataValidationRule, _unitId: string, _subUnitId: string): IFormulaValidResult {
const operator = rule.operator;
if (!operator) {
return {
success: false,
};
}
const formula1Success = Tools.isDefine(rule.formula1) && this._isFormulaOrInt(rule.formula1);
const formula2Success = Tools.isDefine(rule.formula2) && this._isFormulaOrInt(rule.formula2);
const isTwoFormula = TWO_FORMULA_OPERATOR_COUNT.includes(operator);
const errorMsg = this.localeService.t<LocaleKey>('sheets-data-validation.validFail.number');
if (isTwoFormula) {
return {
success: formula1Success && formula2Success,
formula1: formula1Success ? undefined : errorMsg,
formula2: formula2Success ? undefined : errorMsg,
};
}
return {
success: formula1Success,
formula1: errorMsg,
};
}
private _parseNumber(formula: Nullable<string | number | boolean>) {
if (formula === undefined || formula === null) {
return Number.NaN;
}
return +formula;
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string, row: number, column: number): Promise<IFormulaResult<number | undefined>> {
const formulaResult1 = await this._customFormulaService.getCellFormulaValue(unitId, subUnitId, rule.uid, row, column);
const formulaResult2 = await this._customFormulaService.getCellFormula2Value(unitId, subUnitId, rule.uid, row, column);
const { formula1, formula2 } = rule;
const isFormulaValid = isLegalFormulaResult(String(formulaResult1?.v)) && isLegalFormulaResult(String(formulaResult2?.v));
return {
formula1: this._parseNumber(isFormulaString(formula1) ? formulaResult1?.v : formula1),
formula2: this._parseNumber(isFormulaString(formula2) ? formulaResult2?.v : formula2),
isFormulaValid,
};
}
override transform(cellInfo: IValidatorCellInfo<CellValue>, _formula: IFormulaResult, _rule: IDataValidationRule) {
return {
...cellInfo,
value: cellInfo.value.toString().length,
};
}
override async isValidType(cellInfo: IValidatorCellInfo<number>, _formula: IFormulaResult, _rule: IDataValidationRule) {
const { value: cellValue } = cellInfo;
return typeof cellValue === 'string' || typeof cellValue === 'number';
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase, pos: ISheetLocationBase) {
if (!rule.operator) {
return this.titleStr;
}
const { transformedFormula1, transformedFormula2 } = getTransformedFormula(this._lexerTreeBuilder, rule as ISheetDataValidationRule, pos);
const errorMsg = this.localeService.t(TextLengthErrorTitleMap[rule.operator]).replace(FORMULA1, transformedFormula1 ?? '').replace(FORMULA2, transformedFormula2 ?? '');
return `${errorMsg}`;
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ICellData, ISheetDataValidationRule, IUnitRangeName, IUniverInstanceService, Nullable, Workbook } from '@univerjs/core';
import type { LexerTreeBuilder } from '@univerjs/engine-formula';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { isFormulaString, Range, UniverInstanceType } from '@univerjs/core';
import { deserializeListOptions } from '@univerjs/sheets';
import { getCellValueOrigin } from '../utils/get-cell-data-origin';
export function getSheetRangeValueSet(grid: IUnitRangeName, univerInstanceService: IUniverInstanceService, currUnitId: string, currSubUnitId: string) {
const set = new Set<string>();
const unitId = grid.unitId || currUnitId;
const workbook = univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET) ?? univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getSheetBySheetName(grid.sheetName) ?? workbook.getSheetBySheetId(currSubUnitId) ?? workbook.getActiveSheet();
Range.foreach(grid.range, (row, col) => {
const data = worksheet?.getCellRaw(row, col);
if (!data) {
return;
}
const cellValue = getCellValueOrigin(data);
if (cellValue === undefined || cellValue === null || cellValue === '') {
return;
}
const list = deserializeListOptions(`${cellValue}`);
list.forEach((item) => {
if (item) {
set.add(`${cellValue}`);
}
});
});
return Array.from(set);
}
export function getDataValidationCellValue(cellData: Nullable<ICellData>) {
const cellValue = getCellValueOrigin(cellData);
if (cellValue === undefined || cellValue === null) {
return '';
}
return cellValue.toString();
}
export function getTransformedFormula(lexerTreeBuilder: LexerTreeBuilder, rule: ISheetDataValidationRule, position: ISheetLocationBase) {
const { formula1, formula2 } = rule;
const originStartRow = rule.ranges[0].startRow;
const originStartColumn = rule.ranges[0].startColumn;
const offsetRow = position.row - originStartRow;
const offsetColumn = position.col - originStartColumn;
const transformedFormula1 = isFormulaString(formula1) ? lexerTreeBuilder.moveFormulaRefOffset(formula1!, offsetColumn, offsetRow, true) : formula1;
const transformedFormula2 = isFormulaString(formula2) ? lexerTreeBuilder.moveFormulaRefOffset(formula2!, offsetColumn, offsetRow, true) : formula2;
return {
transformedFormula1,
transformedFormula2,
};
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { CellValue, IDataValidationRule, IDataValidationRuleBase, ISheetDataValidationRule, Nullable } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { LocaleKey } from '../locale/types';
import { DataValidationOperator, DataValidationType, isFormulaString, Tools } from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { OperatorErrorTitleMap } from '../types';
import { TWO_FORMULA_OPERATOR_COUNT } from '../types/const/two-formula-operators';
import { isLegalFormulaResult } from '../utils/formula';
import { BaseSheetValidator } from './base-sheet-validator';
import { FORMULA1, FORMULA2 } from './const';
import { getCellValueNumber } from './decimal-validator';
import { getTransformedFormula } from './util';
export class WholeValidator extends BaseSheetValidator {
private readonly _customFormulaService = this.injector.get(DataValidationCustomFormulaService);
private readonly _lexerTreeBuilder = this.injector.get(LexerTreeBuilder);
id: string = DataValidationType.WHOLE;
title: LocaleKey = 'sheets-data-validation.whole.title';
order = 10;
operators: DataValidationOperator[] = [
DataValidationOperator.BETWEEN,
DataValidationOperator.EQUAL,
DataValidationOperator.GREATER_THAN,
DataValidationOperator.GREATER_THAN_OR_EQUAL,
DataValidationOperator.LESS_THAN,
DataValidationOperator.LESS_THAN_OR_EQUAL,
DataValidationOperator.NOT_BETWEEN,
DataValidationOperator.NOT_EQUAL,
];
scopes: string | string[] = ['sheet'];
private _isFormulaOrInt(formula: string) {
return !Tools.isBlank(formula) && (isFormulaString(formula) || (!Number.isNaN(+formula) && Number.isInteger(+formula)));
}
override async isValidType(cellInfo: IValidatorCellInfo<CellValue>, _formula: IFormulaResult, _rule: IDataValidationRule) {
const { value: cellValue } = cellInfo;
const num = getCellValueNumber(cellValue);
return !Number.isNaN(num) && Number.isInteger(num);
}
override transform(cellInfo: IValidatorCellInfo<CellValue>, _formula: IFormulaResult, _rule: IDataValidationRule) {
const { value: cellValue } = cellInfo;
return {
...cellInfo,
value: getCellValueNumber(cellValue),
};
}
private _parseNumber(formula: Nullable<string | number | boolean>) {
if (formula === undefined || formula === null) {
return Number.NaN;
}
return +formula;
}
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) ? res1?.v : formula1;
const formula2Result = isFormulaString(formula2) ? res2?.v : formula2;
const isFormulaValid = isLegalFormulaResult(`${formula1Result}`) && isLegalFormulaResult(`${formula2Result}`);
const info = {
formula1: this._parseNumber(formula1Result),
formula2: this._parseNumber(formula2Result),
isFormulaValid,
};
return info;
}
override validatorFormula(rule: IDataValidationRuleBase, _unitId: string, _subUnitId: string): IFormulaValidResult {
const operator = rule.operator;
if (!operator) {
return {
success: true,
};
}
const formula1Success = Tools.isDefine(rule.formula1) && this._isFormulaOrInt(rule.formula1);
const formula2Success = Tools.isDefine(rule.formula2) && this._isFormulaOrInt(rule.formula2);
const isTwoFormula = TWO_FORMULA_OPERATOR_COUNT.includes(operator);
const errorMsg = this.localeService.t<LocaleKey>('sheets-data-validation.validFail.number');
if (isTwoFormula) {
return {
success: formula1Success && formula2Success,
formula1: formula1Success ? undefined : errorMsg,
formula2: formula2Success ? undefined : errorMsg,
};
}
return {
success: formula1Success,
formula1: errorMsg,
};
}
override generateRuleErrorMessage(rule: IDataValidationRuleBase, position: ISheetLocationBase) {
if (!rule.operator) {
return this.localeService.t(OperatorErrorTitleMap.NONE).replace('{TYPE}', this.titleStr);
}
const { transformedFormula1, transformedFormula2 } = getTransformedFormula(this._lexerTreeBuilder, rule as ISheetDataValidationRule, position);
const errorMsg = this.localeService.t(OperatorErrorTitleMap[rule.operator]).replace(FORMULA1, transformedFormula1 ?? '').replace(FORMULA2, transformedFormula2 ?? '');
return `${errorMsg}`;
}
}