mirror of
https://github.com/dream-num/univer.git
synced 2026-09-24 15:32:18 +08:00
feat: add data validation error handling in workbook and worksheet mixins (#5759)
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user