From bf6cd652ea2cfd8485c45ce7f90d981ead1abdc0 Mon Sep 17 00:00:00 2001 From: WEI ZHANG Date: Sat, 30 Aug 2025 08:16:39 +0800 Subject: [PATCH] feat: add data validation error handling in workbook and worksheet mixins (#5759) --- .../src/facade/f-range.ts | 93 ++++++++++++- .../src/facade/f-workbook.ts | 129 +++++++++++++++++- .../src/facade/f-worksheet.ts | 103 +++++++++++++- .../src/services/dv-validator-service.ts | 8 +- 4 files changed, 325 insertions(+), 8 deletions(-) diff --git a/packages/sheets-data-validation/src/facade/f-range.ts b/packages/sheets-data-validation/src/facade/f-range.ts index 19eb7d15b8..1628b866eb 100644 --- a/packages/sheets-data-validation/src/facade/f-range.ts +++ b/packages/sheets-data-validation/src/facade/f-range.ts @@ -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; + /** + * 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; } /** @@ -207,6 +223,79 @@ export class FRangeDataValidationMixin extends FRange implements IFRangeDataVali [this._range] ); } + + override async getDataValidationErrorAsync(): Promise { + 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 { + 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[] = []; + + for (let row = range.startRow; row <= range.endRow; row++) { + for (let col = range.startColumn; col <= range.endColumn; col++) { + promises.push((async (): Promise => { + 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); diff --git a/packages/sheets-data-validation/src/facade/f-workbook.ts b/packages/sheets-data-validation/src/facade/f-workbook.ts index 9c25bcda45..22dac205dd 100644 --- a/packages/sheets-data-validation/src/facade/f-workbook.ts +++ b/packages/sheets-data-validation/src/facade/f-workbook.ts @@ -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>>>; + /** + * 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; + /** * @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 { + 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 { + 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 { + 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[] = []; + + for (let row = range.startRow; row <= range.endRow; row++) { + for (let col = range.startColumn; col <= range.endColumn; col++) { + promises.push((async (): Promise => { + 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 diff --git a/packages/sheets-data-validation/src/facade/f-worksheet.ts b/packages/sheets-data-validation/src/facade/f-worksheet.ts index 97a036e621..c35b050b79 100644 --- a/packages/sheets-data-validation/src/facade/f-worksheet.ts +++ b/packages/sheets-data-validation/src/facade/f-worksheet.ts @@ -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; + + /** + * 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; } /** @@ -96,6 +111,90 @@ export class FWorksheetDataValidationMixin extends FWorksheet implements IFWorks } return null; } + + override async getAllDataValidationErrorAsync(): Promise { + const unitId = this._workbook.getUnitId(); + const sheetId = this._worksheet.getSheetId(); + + return this._collectValidationErrorsForSheet(unitId, sheetId); + } + + private async _collectValidationErrorsForSheet(unitId: string, sheetId: string): Promise { + 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 { + 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[] = []; + + for (let row = range.startRow; row <= range.endRow; row++) { + for (let col = range.startColumn; col <= range.endColumn; col++) { + promises.push((async (): Promise => { + 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); diff --git a/packages/sheets-data-validation/src/services/dv-validator-service.ts b/packages/sheets-data-validation/src/services/dv-validator-service.ts index c604a2c71e..4e2a13a8ef 100644 --- a/packages/sheets-data-validation/src/services/dv-validator-service.ts +++ b/packages/sheets-data-validation/src/services/dv-validator-service.ts @@ -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[] = []; Range.foreach(range, (row, col) => { promises.push(this._validatorByCell(workbook, worksheet, row, col));