feat: add data validation error handling in workbook and worksheet mixins (#5759)

This commit is contained in:
WEI ZHANG
2025-08-30 08:16:39 +08:00
committed by GitHub
parent 4a79e42981
commit bf6cd652ea
4 changed files with 325 additions and 8 deletions
@@ -14,9 +14,11 @@
* limitations under the License.
*/
import type { DataValidationStatus, Nullable } from '@univerjs/core';
import type { IDataValidationRule, IRange, Nullable } from '@univerjs/core';
import type { IAddSheetDataValidationCommandParams, IClearRangeDataValidationCommandParams } from '@univerjs/sheets-data-validation';
import { AddSheetDataValidationCommand, ClearRangeDataValidationCommand, SheetsDataValidationValidatorService } 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';
@@ -145,6 +147,20 @@ export interface IFRangeDataValidationMixin {
* ```
*/
getValidatorStatus(): Promise<DataValidationStatus[][]>;
/**
* Get data validation errors for a specific range in current worksheet.
* @returns A promise that resolves to an array of validation errors in the specified range.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getActiveSheet();
* const fRange = fWorksheet.getRange('A1:B10');
* const errors = await fRange.getDataValidationErrorAsync();
*
* console.log(errors);
* ```
*/
getDataValidationErrorAsync(): Promise<IDataValidationError[]>;
}
/**
@@ -207,6 +223,79 @@ export class FRangeDataValidationMixin extends FRange implements IFRangeDataVali
[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(FRangeDataValidationMixin);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { DataValidationStatus, IDisposable, IExecutionOptions, Nullable, ObjectMatrix } from '@univerjs/core';
import type { IDataValidationRule, IDisposable, IExecutionOptions, IRange, Nullable, ObjectMatrix } from '@univerjs/core';
import type { IRuleChange } from '@univerjs/data-validation';
import type {
IAddSheetDataValidationCommandParams,
@@ -25,7 +25,7 @@ import type {
IUpdateSheetDataValidationSettingCommandParams,
IValidStatusChange,
} from '@univerjs/sheets-data-validation';
import { toDisposable } from '@univerjs/core';
import { DataValidationStatus, toDisposable } from '@univerjs/core';
import {
AddSheetDataValidationCommand,
@@ -40,6 +40,24 @@ import {
import { FWorkbook } from '@univerjs/sheets/facade';
import { filter } from 'rxjs';
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
*/
@@ -56,6 +74,18 @@ export interface IFWorkbookDataValidationMixin {
*/
getValidatorStatus(): Promise<Record<string, ObjectMatrix<Nullable<DataValidationStatus>>>>;
/**
* Get all data validation errors for current workbook.
* @returns A promise that resolves to an array of validation errors.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const errors = await fWorkbook.getAllDataValidationError();
* console.log(errors);
* ```
*/
getAllDataValidationErrorAsync(): Promise<IDataValidationError[]>;
/**
* @deprecated Use `univerAPI.addEvent(univerAPI.Event.SheetDataValidationChanged, (event) => { ... })` instead
*/
@@ -138,6 +168,101 @@ export class FWorkbookDataValidationMixin extends FWorkbook implements IFWorkboo
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,
};
}
// region DataValidationHooks
override onDataValidationChange(
callback: (ruleChange: IRuleChange) => void
@@ -14,9 +14,11 @@
* limitations under the License.
*/
import type { DataValidationStatus, Nullable, ObjectMatrix } from '@univerjs/core';
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 { SheetsDataValidationValidatorService } from '@univerjs/sheets-data-validation';
import { SheetDataValidationModel, SheetsDataValidationValidatorService } from '@univerjs/sheets-data-validation';
import { FWorksheet } from '@univerjs/sheets/facade';
import { FDataValidation } from './f-data-validation';
@@ -65,6 +67,19 @@ export interface IFWorksheetDataValidationMixin {
* ```
*/
getDataValidation(ruleId: string): Nullable<FDataValidation>;
/**
* Get all data validation errors for current worksheet.
* @returns A promise that resolves to an array of validation errors.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getActiveSheet();
* const errors = await fWorksheet.getAllDataValidationError();
* console.log(errors);
* ```
*/
getAllDataValidationErrorAsync(): Promise<IDataValidationError[]>;
}
/**
@@ -96,6 +111,90 @@ export class FWorksheetDataValidationMixin extends FWorksheet implements IFWorks
}
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);
@@ -15,7 +15,7 @@
*/
import type { IDataValidationRule, IRange, Nullable, ObjectMatrix, Workbook, Worksheet } from '@univerjs/core';
import { bufferDebounceTime, DataValidationStatus, Disposable, Inject, IUniverInstanceService, LifecycleService, LifecycleStages, Range, Tools, UniverInstanceType } from '@univerjs/core';
import { bufferDebounceTime, DataValidationStatus, Disposable, getIntersectRange, Inject, IUniverInstanceService, LifecycleService, LifecycleStages, Range, Tools, UniverInstanceType } from '@univerjs/core';
import { bufferWhen, filter } from 'rxjs';
import { SheetDataValidationModel } from '../models/sheet-data-validation-model';
import { DataValidationCacheService } from './dv-cache.service';
@@ -121,8 +121,12 @@ export class SheetsDataValidationValidatorService extends Disposable {
if (!worksheet) {
throw new Error(`cannot find current worksheet, sheetId: ${subUnitId}`);
}
const allRules = this._sheetDataValidationModel.getRules(unitId, subUnitId);
const ruleRanges = allRules.map((i) => i.ranges).flat();
return Promise.all(ranges.map((range) => {
const intersectRanges = ranges.map((range) => ruleRanges.map((ruleRange) => getIntersectRange(range, ruleRange))).flat().filter(Boolean) as IRange[];
return Promise.all(intersectRanges.map((range) => {
const promises: Promise<DataValidationStatus>[] = [];
Range.foreach(range, (row, col) => {
promises.push(this._validatorByCell(workbook, worksheet, row, col));