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,106 @@
/**
* 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 { IHoverCellPosition } from '@univerjs/sheets-ui';
import type { Subject } from 'rxjs';
import {
DataValidationOperator,
DataValidationStatus,
DataValidationType,
IUniverInstanceService,
LocaleService,
} from '@univerjs/core';
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
import { IRefSelectionsService, RefSelectionsService, SetRangeValuesMutation } from '@univerjs/sheets';
import { AddSheetDataValidationCommand } from '@univerjs/sheets-data-validation';
import {
CellAlertManagerService,
CellPopupManagerService,
HoverManagerService,
SheetCanvasPopManagerService,
} from '@univerjs/sheets-ui';
import { CanvasPopupService, ICanvasPopupService } from '@univerjs/ui';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createDvUiTestBed } from '../../__tests__/create-dv-ui-test-bed';
import enUS from '../../locale/en-US';
import { DataValidationAlertController } from '../dv-alert.controller';
afterEach(() => {
vi.useRealTimers();
});
describe('DataValidationAlertController', () => {
it.each(['active', 'inactive', 'disposed', 'switched-after-open'] as const)('handles delayed validation hover for a %s worksheet', async (state) => {
vi.useFakeTimers();
const { univer, injector, workbook, commandService, dataValidationModel } = createDvUiTestBed();
try {
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
injector.add([ICanvasPopupService, { useClass: CanvasPopupService }]);
injector.add([IRefSelectionsService, { useClass: RefSelectionsService }]);
injector.add([SheetCanvasPopManagerService]);
injector.add([CellPopupManagerService]);
injector.add([CellAlertManagerService]);
injector.add([HoverManagerService]);
injector.add([DataValidationAlertController]);
injector.get(LocaleService).load({ enUS });
injector.get(DataValidationAlertController);
const rule = {
uid: 'delayed-alert-rule',
type: DataValidationType.DECIMAL,
operator: DataValidationOperator.GREATER_THAN,
formula1: '111',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
};
commandService.syncExecuteCommand(SetRangeValuesMutation.id, {
unitId: 'test',
subUnitId: 'sheet1',
cellValue: { 0: { 0: { v: 1 } } },
});
commandService.syncExecuteCommand(AddSheetDataValidationCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
rule,
});
const location = { unitId: 'test', subUnitId: 'sheet1', row: 0, col: 0 };
const validation = { ...location, workbook, worksheet: workbook.getActiveSheet() };
dataValidationModel.validator(rule, validation);
await vi.advanceTimersByTimeAsync(0);
expect(dataValidationModel.validator(rule, validation)).toBe(DataValidationStatus.INVALID);
const hover = injector.get(HoverManagerService);
const input = hover as unknown as { _currentCell$: Subject<IHoverCellPosition> };
input._currentCell$.next({ location } as IHoverCellPosition);
if (state === 'inactive') {
workbook.setActiveSheet(workbook.getSheetBySheetId('sheet2')!);
} else if (state === 'disposed') {
injector.get(IUniverInstanceService).disposeUnit(workbook.getUnitId());
}
await vi.advanceTimersByTimeAsync(120);
const alerts = injector.get(CellAlertManagerService);
expect(alerts.currentAlert.size).toBe(state === 'active' || state === 'switched-after-open' ? 1 : 0);
if (state === 'switched-after-open') {
workbook.setActiveSheet(workbook.getSheetBySheetId('sheet2')!);
expect(alerts.currentAlert.size).toBe(0);
}
if (state === 'active') {
hover.triggerScroll();
await vi.advanceTimersByTimeAsync(120);
expect(alerts.currentAlert.size).toBe(0);
}
} finally {
univer.dispose();
}
});
});

View File

@@ -0,0 +1,135 @@
/**
* 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 { ISheetAutoFillHook } from '@univerjs/sheets';
import { DataValidationType, Direction, Range } from '@univerjs/core';
import { AddDataValidationMutation } from '@univerjs/data-validation';
import { AUTO_FILL_APPLY_TYPE } from '@univerjs/sheets';
import { DATA_VALIDATION_PLUGIN_NAME } from '@univerjs/sheets-data-validation';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createDvUiTestBed } from '../../__tests__/create-dv-ui-test-bed';
import { DataValidationAutoFillController } from '../dv-auto-fill.controller';
describe('DataValidationAutoFillController', () => {
let testBed: ReturnType<typeof createDvUiTestBed>;
let hook: ISheetAutoFillHook;
beforeEach(() => {
testBed = createDvUiTestBed();
testBed.injector.add([DataValidationAutoFillController]);
testBed.injector.get(DataValidationAutoFillController);
const registeredHook = testBed.autoFillService.getAllHooks().find((item) => item.id === DATA_VALIDATION_PLUGIN_NAME);
if (!registeredHook) {
throw new Error('Data validation autofill hook was not registered');
}
hook = registeredHook;
});
afterEach(() => {
vi.restoreAllMocks();
testBed.univer.dispose();
});
it('disables series fill for checkbox rules without querying each source cell', async () => {
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
uid: 'checkbox-rule',
type: DataValidationType.CHECKBOX,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
},
});
vi.spyOn(testBed.dataValidationModel, 'getRuleByLocation').mockImplementation(() => {
throw new Error('must not query validation by cell');
});
hook.onBeforeFillData?.({
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
source: {
rows: Array.from({ length: 100_000 }, (_, index) => index),
cols: Array.from({ length: 1_000 }, (_, index) => index),
},
target: { rows: [100_000], cols: [0] },
}, Direction.DOWN);
expect(testBed.autoFillService.menu.find((item) => item.value === AUTO_FILL_APPLY_TYPE.SERIES)?.disable).toBe(true);
});
it('ignores checkbox rules that only cover filtered-out source rows', async () => {
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
uid: 'filtered-checkbox-rule',
type: DataValidationType.CHECKBOX,
ranges: [{ startRow: 1, endRow: 1, startColumn: 0, endColumn: 0 }],
},
});
vi.spyOn(testBed.dataValidationModel, 'getRuleByLocation').mockImplementation(() => {
throw new Error('must not query validation by cell');
});
hook.onBeforeFillData?.({
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
source: { rows: [0, 2], cols: [0] },
target: { rows: [3], cols: [0] },
}, Direction.DOWN);
expect(testBed.autoFillService.menu.find((item) => item.value === AUTO_FILL_APPLY_TYPE.SERIES)?.disable).toBe(false);
});
it('executes and undoes real validation mutations without enumerating autofill cells', async () => {
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
uid: 'decimal-rule',
type: DataValidationType.DECIMAL,
formula1: '1',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
},
});
const foreach = vi.spyOn(Range, 'foreach').mockImplementation(() => {
throw new Error('must not enumerate cells');
});
const location = {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
source: { rows: [0], cols: [0] },
target: { rows: [1], cols: [0] },
};
const result = hook.onFillData?.(location, Direction.DOWN, AUTO_FILL_APPLY_TYPE.COPY);
if (!result) {
throw new Error('Data validation autofill did not return mutations');
}
foreach.mockRestore();
for (const mutation of result.redos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 0, 0)?.uid).toBe('decimal-rule');
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 1, 0)?.uid).toBe('decimal-rule');
for (const mutation of result.undos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 0, 0)?.uid).toBe('decimal-rule');
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 1, 0)).toBeUndefined();
});
});

View File

@@ -0,0 +1,62 @@
/**
* 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 { AddSheetDataValidationCommand, UpdateSheetDataValidationRangeCommand } from '@univerjs/sheets-data-validation';
import { describe, expect, it, vi } from 'vitest';
import { DataValidationPermissionController } from '../dv-permission.controller';
describe('DataValidationPermissionController', () => {
it('blocks add/update commands when range permissions fail', () => {
let beforeCommandExecutedHandler: ((command: { id: string; params: unknown }) => void) | undefined;
const permissionCheckWithRanges = vi.fn()
.mockReturnValueOnce(false)
.mockReturnValueOnce(false);
const blockExecuteWithoutPermission = vi.fn();
const controller = new DataValidationPermissionController(
{ t: (key: string) => key } as never,
{
beforeCommandExecuted: vi.fn((handler) => {
beforeCommandExecutedHandler = handler;
return { dispose: vi.fn() };
}),
} as never,
{ permissionCheckWithRanges, blockExecuteWithoutPermission } as never
);
expect(controller).toBeTruthy();
beforeCommandExecutedHandler!({
id: AddSheetDataValidationCommand.id,
params: {
unitId: 'book-1',
subUnitId: 'sheet-1',
rule: { ranges: [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }] },
},
});
beforeCommandExecutedHandler!({
id: UpdateSheetDataValidationRangeCommand.id,
params: {
unitId: 'book-1',
subUnitId: 'sheet-1',
ranges: [{ startRow: 2, endRow: 3, startColumn: 0, endColumn: 1 }],
},
});
expect(permissionCheckWithRanges).toHaveBeenCalledTimes(2);
expect(blockExecuteWithoutPermission).toHaveBeenCalledTimes(2);
expect(blockExecuteWithoutPermission).toHaveBeenCalledWith('sheets-data-validation-ui.permission.dialog.setStyleErr');
});
});

View File

@@ -0,0 +1,93 @@
/**
* 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 { LocaleService } from '@univerjs/core';
import type { DataValidatorRegistryService } from '@univerjs/data-validation';
import type { SheetInterceptorService } from '@univerjs/sheets';
import type { SheetDataValidationModel, SheetsDataValidationValidatorService } from '@univerjs/sheets-data-validation';
import type { IDialogService } from '@univerjs/ui';
import { DataValidationErrorStyle, DataValidationStatus } from '@univerjs/core';
import { VALIDATE_CELL } from '@univerjs/sheets';
import { describe, expect, it, vi } from 'vitest';
import { DataValidationRejectInputController } from '../dv-reject-input.controller';
interface IInterceptContext {
row: number;
col: number;
unitId: string;
subUnitId: string;
}
describe('DataValidationRejectInputController', () => {
it('intercepts invalid STOP rules and opens a reject dialog, while allowing valid cases through', async () => {
let interceptedHandler:
| ((lastResult: Promise<boolean>, context: IInterceptContext, next: (value: Promise<boolean>) => Promise<boolean>) => Promise<boolean>)
| undefined;
const open = vi.fn();
const close = vi.fn();
const getRuleById = vi.fn(() => ({ type: 'list', errorStyle: DataValidationErrorStyle.STOP }));
const model = {
getRuleIdByLocation: vi.fn(() => 'rule-1'),
getRuleById,
};
const validator = { getRuleFinalError: vi.fn(() => 'Invalid value') };
const controller = new DataValidationRejectInputController(
{
writeCellInterceptor: {
intercept: vi.fn((name, config) => {
expect(name).toBe(VALIDATE_CELL);
interceptedHandler = config.handler;
return { dispose: vi.fn() };
}),
},
} as unknown as SheetInterceptorService,
model as unknown as SheetDataValidationModel,
{ getValidatorItem: vi.fn(() => validator) } as unknown as DataValidatorRegistryService,
{ open, close } as unknown as IDialogService,
{ t: (key: string) => key } as LocaleService,
{
validatorCell: vi.fn()
.mockResolvedValueOnce(DataValidationStatus.INVALID)
.mockResolvedValueOnce(DataValidationStatus.VALID),
} as unknown as SheetsDataValidationValidatorService
);
expect(controller).toBeTruthy();
expect(interceptedHandler).toBeDefined();
const next = (value: Promise<boolean>) => value;
const context: IInterceptContext = { row: 1, col: 2, unitId: 'book-1', subUnitId: 'sheet-1' };
await expect(interceptedHandler!(Promise.resolve(false), context, next)).resolves.toBe(false);
expect(open).not.toHaveBeenCalled();
await expect(interceptedHandler!(Promise.resolve(true), context, next)).resolves.toBe(false);
expect(open).toHaveBeenCalledWith(expect.objectContaining({
id: 'reject-input-dialog',
children: { title: 'Invalid value' },
}));
await expect(interceptedHandler!(Promise.resolve(true), context, next)).resolves.toBe(true);
getRuleById.mockReturnValue({ type: 'list', errorStyle: DataValidationErrorStyle.WARNING });
await expect(interceptedHandler!(Promise.resolve(true), context, next)).resolves.toBe(true);
controller.showReject('Bad input');
expect(open).toHaveBeenLastCalledWith(expect.objectContaining({
children: { title: 'Bad input' },
}));
});
});

View File

@@ -0,0 +1,277 @@
/**
* 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 { Ctor, ICommandInfo, IDisposable, IWorkbookData, Workbook } from '@univerjs/core';
import type { ISetWorksheetRowAutoHeightMutationParams } from '@univerjs/sheets';
import {
DataValidationRenderMode,
DataValidationStatus,
DataValidationType,
ICommandService,
ILogService,
InterceptorEffectEnum,
IUniverInstanceService,
LocaleService,
LocaleType,
LogLevel,
Tools,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { AddDataValidationMutation, DataValidatorRegistryService } from '@univerjs/data-validation';
import { DocSelectionManagerService } from '@univerjs/docs';
import { EditorService, IEditorService } from '@univerjs/docs-ui';
import {
CanvasColorService,
ICanvasColorService,
IRenderManagerService,
RenderManagerService,
} from '@univerjs/engine-render';
import {
INTERCEPTOR_POINT,
IRefSelectionsService,
SetWorksheetRowAutoHeightMutation,
SheetInterceptorService,
SheetsSelectionsService,
UniverSheetsPlugin,
} from '@univerjs/sheets';
import { DataValidationCacheService, UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation';
import {
AutoHeightController,
EditorBridgeService,
IEditorBridgeService,
ISheetCellDropdownManagerService,
SheetCanvasPopManagerService,
SheetCellDropdownManagerService,
SheetSkeletonManagerService,
} from '@univerjs/sheets-ui';
import { CanvasPopupService, ICanvasPopupService, IMenuManagerService, MenuManagerService } from '@univerjs/ui';
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import enUS from '../../locale/en-US';
import { DataValidationDropdownManagerService } from '../../services/dropdown-manager.service';
import { SheetsDataValidationRenderController } from '../dv-render.controller';
import { SheetsDataValidationMobileRenderController } from '../mobile/dv-render.controller';
import { SheetsDataValidationUIController } from '../ui.controller';
vi.hoisted(() => {
vi.stubGlobal('Path2D', class {});
});
afterAll(() => vi.unstubAllGlobals());
const controllers: Ctor<IDisposable>[] = [SheetsDataValidationRenderController, SheetsDataValidationMobileRenderController];
for (const Controller of controllers) {
describe(Controller.name, () => {
let univer: Univer;
let peer: Workbook;
let first: Workbook;
let second: Workbook;
beforeEach(() => {
vi.useFakeTimers();
// Only the unavailable Canvas backend is replaced; controllers and providers are real.
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(function (this: HTMLCanvasElement) {
return {
canvas: this,
setTransform: vi.fn(),
clearRect: vi.fn(),
measureText: (text: string) => ({
width: text.length * 7,
fontBoundingBoxAscent: 10,
fontBoundingBoxDescent: 4,
actualBoundingBoxAscent: 10,
actualBoundingBoxDescent: 4,
}),
} as unknown as CanvasRenderingContext2D;
});
univer = new Univer();
univer.registerPlugin(UniverSheetsPlugin, { notExecuteFormula: true });
univer.registerPlugin(UniverSheetsDataValidationPlugin);
peer = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookSnapshot('peer', 70));
const injector = univer.__getInjector();
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
injector.get(LocaleService).load({ enUS });
injector.get(LocaleService).setLocale(LocaleType.EN_US);
injector.add([ICanvasColorService, { useClass: CanvasColorService }]);
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
injector.add([AutoHeightController]);
injector.add([DocSelectionManagerService]);
injector.add([IEditorService, { useClass: EditorService }]);
injector.add([IEditorBridgeService, { useClass: EditorBridgeService }]);
injector.add([IRefSelectionsService, { useClass: SheetsSelectionsService }]);
injector.add([ICanvasPopupService, { useClass: CanvasPopupService }]);
injector.add([SheetCanvasPopManagerService]);
injector.add([ISheetCellDropdownManagerService, { useClass: SheetCellDropdownManagerService }]);
injector.add([DataValidationDropdownManagerService]);
injector.add([IMenuManagerService, { useClass: MenuManagerService }]);
injector.add([Controller]);
injector.add([SheetsDataValidationUIController]);
const instances = injector.get(IUniverInstanceService);
first = instances.createUnit<IWorkbookData, Workbook>(
UniverInstanceType.UNIVER_SHEET,
workbookSnapshot('first', 50),
{ makeCurrent: false }
);
second = instances.createUnit<IWorkbookData, Workbook>(
UniverInstanceType.UNIVER_SHEET,
workbookSnapshot('second', 60),
{ makeCurrent: false }
);
const renders = injector.get(IRenderManagerService);
renders.registerRenderModule(UniverInstanceType.UNIVER_SHEET, [SheetSkeletonManagerService]);
for (const workbook of [peer, first, second]) {
renders.createRender(workbook.getUnitId());
}
injector.get(SheetsDataValidationUIController);
injector.get(Controller);
});
afterEach(() => {
vi.clearAllTimers();
univer.dispose();
vi.restoreAllMocks();
vi.useRealTimers();
});
it('keeps each workbook and worksheet in a shared batch isolated after focus changes', async () => {
const injector = univer.__getInjector();
const instances = injector.get(IUniverInstanceService);
instances.setCurrentUnitForType('first');
const peerBefore = Tools.deepClone(peer.getSnapshot());
const heights: ISetWorksheetRowAutoHeightMutationParams[] = [];
const commands = injector.get(ICommandService);
const listener = commands.onCommandExecuted((command: ICommandInfo) => {
if (command.id === SetWorksheetRowAutoHeightMutation.id) {
heights.push(command.params as ISetWorksheetRowAutoHeightMutationParams);
}
});
addRule(univer, 'first', 'sheet1', 1, DataValidationType.LIST);
addRule(univer, 'first', 'sheet1', 2, DataValidationType.LIST_MULTIPLE);
addRule(univer, 'first', 'sheet2', 3, DataValidationType.LIST);
addRule(univer, 'second', 'sheet1', 4, DataValidationType.LIST_MULTIPLE);
instances.setCurrentUnitForType('peer');
instances.focusUnit('peer');
expect(first.getSheetBySheetId('sheet1')!.getRowHeight(1)).toBe(100);
await vi.advanceTimersByTimeAsync(100);
expect(first.getSheetBySheetId('sheet1')!.getRowHeight(1)).toBe(50);
expect(first.getSheetBySheetId('sheet1')!.getRowHeight(2)).toBe(50);
expect(first.getSheetBySheetId('sheet2')!.getRowHeight(3)).toBe(55);
expect(second.getSheetBySheetId('sheet1')!.getRowHeight(4)).toBe(60);
expect(first.getSheetBySheetId('sheet1')!.getRowHeight(4)).toBe(100);
expect(peer.getSnapshot()).toEqual(peerBefore);
expect(heights).toHaveLength(3);
expect(heights).toEqual(expect.arrayContaining([
{ unitId: 'first', subUnitId: 'sheet1', rowsAutoHeightInfo: [{ row: 1, autoHeight: 50 }, { row: 2, autoHeight: 50 }] },
{ unitId: 'first', subUnitId: 'sheet2', rowsAutoHeightInfo: [{ row: 3, autoHeight: 55 }] },
{ unitId: 'second', subUnitId: 'sheet1', rowsAutoHeightInfo: [{ row: 4, autoHeight: 60 }] },
]));
listener.dispose();
});
it('cancels buffered and future work when its controller is disposed', async () => {
const injector = univer.__getInjector();
injector.get(IUniverInstanceService).setCurrentUnitForType('first');
addRule(univer, 'first', 'sheet1', 1, DataValidationType.LIST);
injector.get(Controller).dispose();
addRule(univer, 'first', 'sheet1', 2, DataValidationType.LIST);
const before = Tools.deepClone(first.getSnapshot());
await vi.advanceTimersByTimeAsync(200);
expect(first.getSnapshot()).toEqual(before);
});
it('ignores patched changes and non-list rules without suppressing command-origin list changes', async () => {
addRule(univer, 'first', 'sheet1', 1, DataValidationType.LIST, 'patched');
addRule(univer, 'first', 'sheet1', 2, DataValidationType.WHOLE);
addRule(univer, 'first', 'sheet1', 3, DataValidationType.LIST);
await vi.advanceTimersByTimeAsync(100);
const sheet = first.getSheetBySheetId('sheet1')!;
expect([sheet.getRowHeight(1), sheet.getRowHeight(2), sheet.getRowHeight(3)]).toEqual([100, 100, 50]);
});
it('discards a disposed target without changing the current workbook', async () => {
addRule(univer, 'first', 'sheet1', 1, DataValidationType.LIST);
const before = Tools.deepClone(peer.getSnapshot());
univer.__getInjector().get(IUniverInstanceService).disposeUnit('first');
await vi.advanceTimersByTimeAsync(100);
expect(peer.getSnapshot()).toEqual(before);
});
it('preserves invalid markers, custom rendering, and list layout through real cell interceptors', () => {
const injector = univer.__getInjector();
addRule(univer, 'peer', 'sheet1', 1, DataValidationType.LIST, 'patched');
injector.get(DataValidationCacheService).ensureCache('peer', 'sheet1').setValue(1, 0, DataValidationStatus.INVALID);
const worksheet = peer.getSheetBySheetId('sheet1')!;
const raw = { v: 'bad', markers: { bl: { size: 1, color: '#112233' } } };
const cell = injector.get(SheetInterceptorService).fetchThroughInterceptors(
INTERCEPTOR_POINT.CELL_CONTENT,
InterceptorEffectEnum.Style
)(raw, { row: 1, col: 0, unitId: 'peer', subUnitId: 'sheet1', rawData: raw, workbook: peer, worksheet })!;
const validator = injector.get(DataValidatorRegistryService).getValidatorItem(DataValidationType.LIST)!;
expect(cell).not.toBe(raw);
expect(cell.markers?.bl).toEqual(raw.markers.bl);
expect(cell.markers?.tr?.size).toBeGreaterThan(0);
expect(cell.customRender).toContain(validator.canvasRender);
expect(cell.fontRenderExtension?.isSkip).toBe(true);
expect(cell.coverable).toBe(false);
expect(cell.interceptorAutoHeight?.()).toBeGreaterThan(0);
expect(cell.interceptorAutoWidth?.()).toBeGreaterThan(0);
});
});
}
function addRule(
univer: Univer,
unitId: string,
subUnitId: string,
row: number,
type: DataValidationType,
source: 'command' | 'patched' = 'command'
) {
expect(univer.__getInjector().get(ICommandService).syncExecuteCommand(AddDataValidationMutation.id, {
unitId,
subUnitId,
source,
rule: {
uid: `rule-${row}`,
type,
formula1: 'red,blue',
ranges: [{ startRow: row, endRow: row, startColumn: 0, endColumn: 0 }],
renderMode: DataValidationRenderMode.CUSTOM,
},
})).toBe(true);
}
function workbookSnapshot(id: string, defaultRowHeight: number): IWorkbookData {
return {
id,
appVersion: '3.0.0-alpha',
locale: LocaleType.EN_US,
name: id,
styles: {},
sheetOrder: ['sheet1', 'sheet2'],
sheets: Object.fromEntries(['sheet1', 'sheet2'].map((sheetId, index) => [sheetId, {
id: sheetId,
name: sheetId,
rowCount: 6,
columnCount: 3,
defaultRowHeight: defaultRowHeight + index * 5,
rowData: { 1: { ah: 100 }, 2: { ah: 100 }, 3: { ah: 100 }, 4: { ah: 100 } },
}])),
};
}

View File

@@ -0,0 +1,52 @@
/**
* 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 { awaitTime } from '@univerjs/core';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { SheetsDataValidationReRenderController } from '../dv-rerender.controller';
describe('SheetsDataValidationReRenderController', () => {
it('marks affected skeletons dirty and forces a rerender when validation status changes', async () => {
const validStatusChange$ = new Subject<{ subUnitId: string }>();
const makeDirty1 = vi.fn();
const makeDirty2 = vi.fn();
const makeForceDirty = vi.fn();
const controller = new SheetsDataValidationReRenderController(
{ mainComponent: { makeForceDirty } } as never,
{ validStatusChange$ } as never,
{
getSkeletonParam: vi.fn((sheetId: string) => {
if (sheetId === 'sheet-1') return { skeleton: { makeDirty: makeDirty1 } };
if (sheetId === 'sheet-2') return { skeleton: { makeDirty: makeDirty2 } };
return undefined;
}),
} as never
);
validStatusChange$.next({ subUnitId: 'sheet-1' });
validStatusChange$.next({ subUnitId: 'sheet-2' });
validStatusChange$.next({ subUnitId: 'sheet-1' });
expect(controller).toBeTruthy();
await awaitTime(30);
expect(makeDirty1).toHaveBeenCalledTimes(1);
expect(makeDirty2).toHaveBeenCalledTimes(1);
expect(makeForceDirty).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,58 @@
/**
* 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 { Disposable, Inject } from '@univerjs/core';
import { CheckboxIcon, DataValidationIcon, DatePickerIcon, DropdownListIcon } from '@univerjs/icons';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { DATA_VALIDATION_PANEL } from '../commands/operations/data-validation.operation';
import { DataValidationPanel } from '../views/components';
import { DateShowTimeOption } from '../views/components/DateShowTimeOption';
import { DROPDOWN_PRESETS_COMPONENT, DropdownPresets } from '../views/components/DropdownPresets';
import { FORMULA_INPUTS } from '../views/components/formula-input';
import { ListRenderModeInput } from '../views/components/ListRenderModeInput';
export class ComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
@Inject(IconManager) private readonly _iconManager: IconManager
) {
super();
this._registerIcons();
this._registerComponents();
}
private _registerIcons(): void {
this.disposeWithMe(this._iconManager.register({
CheckboxIcon,
DataValidationIcon,
DatePickerIcon,
DropdownListIcon,
}));
}
private _registerComponents(): void {
([
[DATA_VALIDATION_PANEL, DataValidationPanel],
[ListRenderModeInput.componentKey, ListRenderModeInput],
[DateShowTimeOption.componentKey, DateShowTimeOption],
[DROPDOWN_PRESETS_COMPONENT, DropdownPresets],
...FORMULA_INPUTS,
] as const).forEach(([key, component]) => {
this.disposeWithMe(this._componentManager.register(key, component));
});
}
}

View File

@@ -0,0 +1,106 @@
/**
* 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 { BaseDataValidator } from '@univerjs/data-validation';
import type { LocaleKey } from '../locale/types';
import {
DataValidationStatus,
Disposable,
Inject,
IUniverInstanceService,
LocaleService,
UniverInstanceType,
} from '@univerjs/core';
import { SheetDataValidationModel } from '@univerjs/sheets-data-validation';
import { CellAlertManagerService, CellAlertType, HoverManagerService } from '@univerjs/sheets-ui';
import { debounceTime, of, switchMap } from 'rxjs';
const ALERT_KEY = 'SHEET_DATA_VALIDATION_ALERT';
export class DataValidationAlertController extends Disposable {
constructor(
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService,
@Inject(CellAlertManagerService) private readonly _cellAlertManagerService: CellAlertManagerService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(LocaleService) private readonly _localeService: LocaleService,
@Inject(SheetDataValidationModel) private readonly _dataValidationModel: SheetDataValidationModel
) {
super();
this._init();
}
private _init() {
this._initCellAlertPopup();
}
private _initCellAlertPopup() {
this.disposeWithMe(this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET).pipe(
switchMap((workbook) => workbook?.activeSheet$ ?? of(null))
).subscribe(() => this._cellAlertManagerService.removeAlert(ALERT_KEY)));
this.disposeWithMe(this._hoverManagerService.currentCell$.pipe(debounceTime(100)).subscribe((cellPos) => {
if (cellPos) {
const workbook = this._univerInstanceService.getUnit<Workbook>(cellPos.location.unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook || workbook.getActiveSheet()?.getSheetId() !== cellPos.location.subUnitId) {
this._cellAlertManagerService.removeAlert(ALERT_KEY);
return;
}
const worksheet = workbook.getSheetBySheetId(cellPos.location.subUnitId);
if (!worksheet) return;
const rule = this._dataValidationModel.getRuleByLocation(cellPos.location.unitId, cellPos.location.subUnitId, cellPos.location.row, cellPos.location.col);
if (!rule) {
this._cellAlertManagerService.removeAlert(ALERT_KEY);
return;
}
const validStatus = this._dataValidationModel.validator(rule, { ...cellPos.location, workbook, worksheet });
if (validStatus === DataValidationStatus.INVALID) {
const currentAlert = this._cellAlertManagerService.currentAlert.get(ALERT_KEY);
const currentLoc = currentAlert?.alert?.location;
if (
currentLoc &&
currentLoc.row === cellPos.location.row &&
currentLoc.col === cellPos.location.col &&
currentLoc.subUnitId === cellPos.location.subUnitId &&
currentLoc.unitId === cellPos.location.unitId
) {
this._cellAlertManagerService.removeAlert(ALERT_KEY);
return;
}
const validator = this._dataValidationModel.getValidator(rule.type) as BaseDataValidator;
if (!validator) {
this._cellAlertManagerService.removeAlert(ALERT_KEY);
return;
}
this._cellAlertManagerService.showAlert({
type: CellAlertType.ERROR,
title: this._localeService.t<LocaleKey>('sheets-data-validation-ui.error.title'),
message: validator?.getRuleFinalError(rule, cellPos.location),
location: cellPos.location,
width: 200,
height: 74,
key: ALERT_KEY,
});
return;
}
}
this._cellAlertManagerService.removeAlert(ALERT_KEY);
}));
}
}

View File

@@ -0,0 +1,120 @@
/**
* 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 } from '@univerjs/core';
import type { IAutoFillLocation, ISheetAutoFillHook } from '@univerjs/sheets';
import { DataValidationType, Disposable, getIntersectRange, Inject, Injector, Rectangle } from '@univerjs/core';
import { AUTO_FILL_APPLY_TYPE, AutoFillTools, IAutoFillService } from '@univerjs/sheets';
import { DATA_VALIDATION_PLUGIN_NAME, getDataValidationDiffMutations, SheetDataValidationModel } from '@univerjs/sheets-data-validation';
import { virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
// TODO: adjust imports
export class DataValidationAutoFillController extends Disposable {
constructor(
@IAutoFillService private readonly _autoFillService: IAutoFillService,
@Inject(SheetDataValidationModel) private readonly _sheetDataValidationModel: SheetDataValidationModel,
@Inject(Injector) private readonly _injector: Injector
) {
super();
this._initAutoFill();
}
// eslint-disable-next-line max-lines-per-function
private _initAutoFill() {
const noopReturnFunc = () => ({ redos: [], undos: [] });
const generalApplyFunc = (location: IAutoFillLocation, applyType: AUTO_FILL_APPLY_TYPE) => {
const { source: sourceRange, target: targetRange, unitId, subUnitId } = location;
const ruleMatrixCopy = this._sheetDataValidationModel.getRuleObjectMatrix(unitId, subUnitId).clone();
const virtualRange = virtualizeDiscreteRanges([sourceRange, targetRange]);
const [vSourceRange, vTargetRange] = virtualRange.ranges;
const { mapRange, projectRange } = virtualRange;
const repeats = AutoFillTools.getAutoFillRepeatRange(vSourceRange, vTargetRange);
const additionsByRuleId = new Map<string, IRange[]>();
additionsByRuleId.set('', repeats.flatMap((repeat) => mapRange(Rectangle.getPositionRange(repeat.relativeRange, {
startRow: repeat.repeatStartCell.row,
endRow: repeat.repeatStartCell.row,
startColumn: repeat.repeatStartCell.col,
endColumn: repeat.repeatStartCell.col,
}))));
this._sheetDataValidationModel.getRules(unitId, subUnitId).forEach((rule) => {
const relativeSourceRanges = rule.ranges.flatMap((range) => {
const projected = projectRange(range);
const intersected = projected && getIntersectRange(projected, vSourceRange);
return intersected
? [Rectangle.getRelativeRange(intersected, vSourceRange)]
: [];
});
const targetRanges = repeats.flatMap((repeat) => relativeSourceRanges.flatMap((sourceRange) => {
const copiedRange = getIntersectRange(sourceRange, repeat.relativeRange);
if (!copiedRange) {
return [];
}
return mapRange(Rectangle.getPositionRange(copiedRange, {
startRow: repeat.repeatStartCell.row,
endRow: repeat.repeatStartCell.row,
startColumn: repeat.repeatStartCell.col,
endColumn: repeat.repeatStartCell.col,
}));
}));
if (targetRanges.length) {
additionsByRuleId.set(rule.uid, targetRanges);
}
});
const additions = Array.from(additionsByRuleId, ([id, ranges]) => ({
id,
ranges: ranges.length > 1 ? Rectangle.mergeRanges(ranges) : ranges,
}));
ruleMatrixCopy.addRangeRules(additions);
const diffs = ruleMatrixCopy.diff(this._sheetDataValidationModel.getRules(unitId, subUnitId));
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, diffs, this._injector, 'patched', applyType === AUTO_FILL_APPLY_TYPE.ONLY_FORMAT);
return {
undos: undoMutations,
redos: redoMutations,
};
};
const hook: ISheetAutoFillHook = {
id: DATA_VALIDATION_PLUGIN_NAME,
onBeforeFillData: (location) => {
const { source: sourceRange, unitId, subUnitId } = location;
const { projectRange } = virtualizeDiscreteRanges([sourceRange]);
const hasCheckbox = this._sheetDataValidationModel.getRules(unitId, subUnitId).some((rule) => (
rule.type === DataValidationType.CHECKBOX && rule.ranges.some((range) => projectRange(range) !== null)
));
if (hasCheckbox) {
this._autoFillService.setDisableApplyType(AUTO_FILL_APPLY_TYPE.SERIES, true);
}
},
onFillData: (location, direction, applyType) => {
if (
applyType === AUTO_FILL_APPLY_TYPE.COPY ||
applyType === AUTO_FILL_APPLY_TYPE.ONLY_FORMAT ||
applyType === AUTO_FILL_APPLY_TYPE.SERIES
) {
return generalApplyFunc(location, applyType);
}
return noopReturnFunc();
},
onAfterFillData: () => {
},
};
this.disposeWithMe(this._autoFillService.addHook(hook));
}
}

View File

@@ -0,0 +1,191 @@
/**
* 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, Nullable } from '@univerjs/core';
import type { ICopyPastePayload, IPasteHookValueType, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
import { Disposable, Inject, Injector, IUniverInstanceService, Rectangle } from '@univerjs/core';
import { getSheetCommandTarget, rangeToDiscreteRange } from '@univerjs/sheets';
import { DATA_VALIDATION_PLUGIN_NAME, getDataValidationDiffMutations, SheetDataValidationModel } from '@univerjs/sheets-data-validation';
import { COPY_TYPE, getRepeatRange, ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE, virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
interface ICopyInfoType {
rules: Map<string, IRange[]>;
unitId: string;
subUnitId: string;
}
const specialPastes: IPasteHookValueType[] = [
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE,
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA,
];
export class DataValidationCopyPasteController extends Disposable {
private _copyInfo: Nullable<ICopyInfoType>;
constructor(
@ISheetClipboardService private _sheetClipboardService: ISheetClipboardService,
@Inject(SheetDataValidationModel) private _sheetDataValidationModel: SheetDataValidationModel,
@Inject(Injector) private _injector: Injector,
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService
) {
super();
this._initCopyPaste();
}
private _initCopyPaste() {
this._sheetClipboardService.addClipboardHook({
id: DATA_VALIDATION_PLUGIN_NAME,
onBeforeCopy: (unitId, subUnitId, range) => this._collect(unitId, subUnitId, range),
onPasteCells: (pasteFrom, pasteTo, data, payload) => {
if (!pasteFrom || !this._copyInfo || specialPastes.includes(payload.pasteType)) {
return { redos: [], undos: [] };
}
return this._generateMutations(pasteFrom, pasteTo, payload);
},
});
}
private _collect(unitId: string, subUnitId: string, range: IRange) {
const rules = new Map<string, IRange[]>();
this._copyInfo = {
unitId,
subUnitId,
rules,
};
const discreteRange = this._injector.invoke((accessor) => {
return rangeToDiscreteRange(range, accessor, unitId, subUnitId);
});
if (!discreteRange) {
return;
}
const { projectRange } = virtualizeDiscreteRanges([discreteRange]);
this._sheetDataValidationModel.getRules(unitId, subUnitId).forEach((rule) => {
const projectedRanges = rule.ranges.flatMap((ruleRange) => {
const projected = projectRange(ruleRange);
return projected ? [projected] : [];
});
if (projectedRanges.length) {
rules.set(rule.uid, projectedRanges.length > 1 ? Rectangle.mergeRanges(projectedRanges) : projectedRanges);
}
});
}
// eslint-disable-next-line max-lines-per-function
private _generateMutations(pasteFrom: ISheetDiscreteRangeLocation, pasteTo: ISheetDiscreteRangeLocation, payload: ICopyPastePayload) {
const copyInfo = this._copyInfo;
if (!copyInfo) {
return { redos: [], undos: [] };
}
const { unitId: copyUnitId, subUnitId: copySubUnitId, range: copyRange } = pasteFrom;
const { unitId: pastedUnitId, subUnitId: pastedSubUnitId, range: pastedRange } = pasteTo;
const { copyType = COPY_TYPE.COPY } = payload;
const target = getSheetCommandTarget(this._univerInstanceService, { unitId: pastedUnitId, subUnitId: pastedSubUnitId });
if (!target) {
return { redos: [], undos: [] };
}
// If it is cut and paste in the same worksheet, do not need to handle the data validation rules, because the move range had handle the ref range of data validation rules, to see dv-formula-ref-range.controller.ts.
if (copyType === COPY_TYPE.CUT && pastedUnitId === copyUnitId && pastedSubUnitId === copySubUnitId) {
this._copyInfo = null;
return { redos: [], undos: [] };
}
const sourceVirtualRange = virtualizeDiscreteRanges([copyRange]).ranges[0];
const targetVirtualization = virtualizeDiscreteRanges([pastedRange]);
const targetVirtualRange = targetVirtualization.ranges[0];
const repeatRange = getRepeatRange(sourceVirtualRange, targetVirtualRange, true);
const clearTargetRanges = targetVirtualization.mapRange(targetVirtualRange);
const getTargetRanges = (sourceRanges: IRange[]) => repeatRange.flatMap(({ startRange }) => sourceRanges.flatMap((sourceRange) => (
targetVirtualization.mapRange(Rectangle.getPositionRange(sourceRange, startRange))
)));
if (pastedUnitId !== copyUnitId || pastedSubUnitId !== copySubUnitId) {
const ruleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(pastedUnitId, pastedSubUnitId).clone();
const additionRules: Map<string, ISheetDataValidationRule> = new Map();
const additions = [{ id: '', ranges: clearTargetRanges }];
copyInfo.rules.forEach((ranges, ruleId) => {
const transformedRuleId = `${copySubUnitId}-${ruleId}`;
const oldRule = this._sheetDataValidationModel.getRuleById(copyUnitId, copySubUnitId, ruleId);
if (!this._sheetDataValidationModel.getRuleById(pastedUnitId, pastedSubUnitId, transformedRuleId) && oldRule) {
additionRules.set(transformedRuleId, { ...oldRule, uid: transformedRuleId });
}
additions.push({ id: transformedRuleId, ranges: getTargetRanges(ranges) });
});
ruleMatrix.addRangeRules(additions);
const { redoMutations, undoMutations } = getDataValidationDiffMutations(
pastedUnitId,
pastedSubUnitId,
ruleMatrix.diffWithAddition(this._sheetDataValidationModel.getRules(pastedUnitId, pastedSubUnitId), additionRules.values()),
this._injector,
'patched',
false
);
if (copyType === COPY_TYPE.CUT) {
// Delete rules in copy range
const copySheetRuleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(copyUnitId, copySubUnitId).clone();
copySheetRuleMatrix.addRangeRules([
{
id: '',
ranges: virtualizeDiscreteRanges([copyRange]).mapRange(sourceVirtualRange),
},
]);
const { redoMutations: cutRedos, undoMutations: cutUndos } = getDataValidationDiffMutations(
copyUnitId,
copySubUnitId,
copySheetRuleMatrix.diff(this._sheetDataValidationModel.getRules(copyUnitId, copySubUnitId)),
this._injector,
'patched',
false
);
redoMutations.push(...cutRedos);
undoMutations.push(...cutUndos);
}
return {
redos: redoMutations,
undos: undoMutations,
};
} else {
const ruleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(copyUnitId, copySubUnitId).clone();
const additions = [
{ id: '', ranges: clearTargetRanges },
...Array.from(copyInfo.rules, ([id, ranges]) => ({ id, ranges: getTargetRanges(ranges) })),
];
ruleMatrix.addRangeRules(additions);
const { redoMutations, undoMutations } = getDataValidationDiffMutations(
pastedUnitId,
pastedSubUnitId,
ruleMatrix.diff(this._sheetDataValidationModel.getRules(copyUnitId, copySubUnitId)),
this._injector,
'patched',
false
);
return {
redos: redoMutations,
undos: undoMutations,
};
}
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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 { ICommandInfo } from '@univerjs/core';
import type {
IAddSheetDataValidationCommandParams,
IUpdateSheetDataValidationRangeCommandParams,
} from '@univerjs/sheets-data-validation';
import type { LocaleKey } from '../locale/types';
import { Disposable, ICommandService, Inject, LocaleService } from '@univerjs/core';
import {
RangeProtectionPermissionEditPoint,
SheetPermissionCheckController,
WorkbookEditablePermission,
WorksheetEditPermission,
WorksheetSetCellStylePermission,
} from '@univerjs/sheets';
import { AddSheetDataValidationCommand, UpdateSheetDataValidationRangeCommand } from '@univerjs/sheets-data-validation';
export class DataValidationPermissionController extends Disposable {
constructor(
@Inject(LocaleService) private _localeService: LocaleService,
@ICommandService private readonly _commandService: ICommandService,
@Inject(SheetPermissionCheckController) private readonly _sheetPermissionCheckController: SheetPermissionCheckController
) {
super();
this._commandExecutedListener();
}
private _commandExecutedListener() {
this.disposeWithMe(
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
if (command.id === AddSheetDataValidationCommand.id) {
const { unitId, subUnitId, rule: { ranges } } = command.params as IAddSheetDataValidationCommandParams;
const permission = this._sheetPermissionCheckController.permissionCheckWithRanges({
workbookTypes: [WorkbookEditablePermission],
rangeTypes: [RangeProtectionPermissionEditPoint],
worksheetTypes: [WorksheetEditPermission, WorksheetSetCellStylePermission],
}, ranges, unitId, subUnitId);
if (!permission) {
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-data-validation-ui.permission.dialog.setStyleErr'));
}
}
if (command.id === UpdateSheetDataValidationRangeCommand.id) {
const { unitId, subUnitId, ranges } = command.params as IUpdateSheetDataValidationRangeCommandParams;
const permission = this._sheetPermissionCheckController.permissionCheckWithRanges({
workbookTypes: [WorkbookEditablePermission],
rangeTypes: [RangeProtectionPermissionEditPoint],
worksheetTypes: [WorksheetEditPermission, WorksheetSetCellStylePermission],
}, ranges, unitId, subUnitId);
if (!permission) {
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-data-validation-ui.permission.dialog.setStyleErr'));
}
}
})
);
}
}

View File

@@ -0,0 +1,120 @@
/**
* 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 { LocaleKey } from '../locale/types';
import { DataValidationErrorStyle, DataValidationStatus, Disposable, Inject, LocaleService } from '@univerjs/core';
import { DataValidatorRegistryService } from '@univerjs/data-validation';
import { Button } from '@univerjs/design';
import { SheetInterceptorService, VALIDATE_CELL } from '@univerjs/sheets';
import { SheetDataValidationModel, SheetsDataValidationValidatorService } from '@univerjs/sheets-data-validation';
import { IDialogService } from '@univerjs/ui';
import { createElement } from 'react';
export class DataValidationRejectInputController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(SheetDataValidationModel) private readonly _dataValidationModel: SheetDataValidationModel,
@Inject(DataValidatorRegistryService) private readonly _dataValidatorRegistryService: DataValidatorRegistryService,
@IDialogService private readonly _dialogService: IDialogService,
@Inject(LocaleService) private readonly _localeService: LocaleService,
@Inject(SheetsDataValidationValidatorService) private readonly _sheetsDataValidationValidatorService: SheetsDataValidationValidatorService
) {
super();
this._initEditorBridgeInterceptor();
}
private _initEditorBridgeInterceptor() {
this.disposeWithMe(this._sheetInterceptorService.writeCellInterceptor.intercept(
VALIDATE_CELL,
{
handler: async (lastResult, context, next) => {
const cell = await lastResult;
const { row, col, unitId, subUnitId } = context;
const ruleId = this._dataValidationModel.getRuleIdByLocation(unitId, subUnitId, row, col);
const rule = ruleId ? this._dataValidationModel.getRuleById(unitId, subUnitId, ruleId) : undefined;
if (cell === false) {
return next(Promise.resolve(false))!;
}
if (!rule || rule.errorStyle !== DataValidationErrorStyle.STOP) {
return next(Promise.resolve(true))!;
}
const validator = this._dataValidatorRegistryService.getValidatorItem(rule.type);
if (!validator) {
return next(Promise.resolve(true))!;
}
const res = await this._sheetsDataValidationValidatorService.validatorCell(unitId, subUnitId, row, col);
if (res === DataValidationStatus.VALID) {
return next(Promise.resolve(true))!;
}
this._dialogService.open({
width: 368,
title: {
title: this._localeService.t<LocaleKey>('sheets-data-validation-ui.alert.title'),
},
id: 'reject-input-dialog',
children: {
title: validator.getRuleFinalError(rule, { row, col, unitId, subUnitId }),
},
footer: {
title: createElement(
Button,
{
variant: 'primary',
onClick: () => this._dialogService.close('reject-input-dialog'),
},
this._localeService.t<LocaleKey>('sheets-data-validation-ui.alert.ok')
),
},
onClose: () => {
this._dialogService.close('reject-input-dialog');
},
});
return next(Promise.resolve(false))!; // Add explicit return for invalid data
},
}
));
}
showReject(title: string) {
this._dialogService.open({
width: 368,
title: {
title: this._localeService.t<LocaleKey>('sheets-data-validation-ui.alert.title'),
},
id: 'reject-input-dialog',
children: {
title,
},
footer: {
title: createElement(
Button,
{
variant: 'primary',
onClick: () => this._dialogService.close('reject-input-dialog'),
},
this._localeService.t<LocaleKey>('sheets-data-validation-ui.alert.ok')
),
},
onClose: () => {
this._dialogService.close('reject-input-dialog');
},
});
}
}

View File

@@ -0,0 +1,302 @@
/**
* 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, ICellRenderContext, IRange, Nullable, Workbook } from '@univerjs/core';
import type { IRuleChange } from '@univerjs/data-validation';
import {
DataValidationStatus,
DataValidationType,
ICommandService,
Inject,
InterceptorEffectEnum,
IUniverInstanceService,
Optional,
RxDisposable,
sequenceExecute,
UniverInstanceType,
} from '@univerjs/core';
import { DataValidatorRegistryService } from '@univerjs/data-validation';
import { IRenderManagerService } from '@univerjs/engine-render';
import { InterceptCellContentPriority, INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
import { DataValidationCacheService, getCellValueOrigin, SheetDataValidationModel } from '@univerjs/sheets-data-validation';
import { AutoHeightController, IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { IMenuManagerService } from '@univerjs/ui';
import { bufferTime, filter, takeUntil } from 'rxjs';
import { menuSchema } from '../menu/schema';
import { DataValidationDropdownManagerService } from '../services/dropdown-manager.service';
const INVALID_MARK = {
tr: {
size: 6,
color: '#fe4b4b',
},
};
export class SheetsDataValidationRenderController extends RxDisposable {
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(AutoHeightController) private readonly _autoHeightController: AutoHeightController,
@Inject(DataValidationDropdownManagerService) private readonly _dropdownManagerService: DataValidationDropdownManagerService,
@Inject(SheetDataValidationModel) private readonly _sheetDataValidationModel: SheetDataValidationModel,
@Inject(DataValidatorRegistryService) private readonly _dataValidatorRegistryService: DataValidatorRegistryService,
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(DataValidationCacheService) private readonly _dataValidationCacheService: DataValidationCacheService,
@Optional(IEditorBridgeService) private readonly _editorBridgeService?: IEditorBridgeService
) {
super();
this._initMenu();
this._initDropdown();
this._initViewModelIntercept();
this._initAutoHeight();
}
private _initMenu() {
this._menuManagerService.mergeMenu(menuSchema);
}
private _initDropdown() {
if (!this._editorBridgeService) {
return;
}
this.disposeWithMe(this._editorBridgeService.visible$.subscribe((visible) => {
if (!visible.visible) {
if (this._dropdownManagerService.activeDropdown?.trigger === 'editor-bridge') {
this._dropdownManagerService.hideDropdown();
}
return;
}
const state = this._editorBridgeService!.getEditCellState();
if (state) {
const { unitId, sheetId, row, column } = state;
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return;
}
const rule = this._sheetDataValidationModel.getRuleByLocation(unitId, sheetId, row, column);
if (!rule) {
return;
}
const validator = this._dataValidatorRegistryService.getValidatorItem(rule.type);
if (!validator?.dropdownType) {
return;
}
const worksheet = workbook.getActiveSheet();
if (!worksheet) return;
const activeDropdown = this._dropdownManagerService.activeDropdown;
const currLoc = activeDropdown?.location;
if (
currLoc &&
currLoc.unitId === unitId &&
currLoc.subUnitId === sheetId &&
currLoc.row === row &&
currLoc.col === column
) {
return;
}
this._dropdownManagerService.showDropdown(
{
location: {
unitId,
subUnitId: sheetId,
row,
col: column,
workbook,
worksheet,
},
trigger: 'editor-bridge',
closeOnOutSide: false,
}
);
}
}));
}
// eslint-disable-next-line max-lines-per-function
private _initViewModelIntercept() {
this.disposeWithMe(
this._sheetInterceptorService.intercept(
INTERCEPTOR_POINT.CELL_CONTENT,
{
effect: InterceptorEffectEnum.Style,
// must be after numfmt
priority: InterceptCellContentPriority.DATA_VALIDATION,
// eslint-disable-next-line max-lines-per-function, complexity
handler: (cell, pos, next) => {
const { row, col, unitId, subUnitId, workbook, worksheet } = pos;
const ruleId = this._sheetDataValidationModel.getRuleIdByLocation(unitId, subUnitId, row, col);
if (!ruleId) {
return next(cell);
}
const rule = this._sheetDataValidationModel.getRuleById(unitId, subUnitId, ruleId);
if (!rule) {
return next(cell);
}
const validStatus = this._dataValidationCacheService.getValue(unitId, subUnitId, row, col) ?? DataValidationStatus.VALID;
const validator = this._dataValidatorRegistryService.getValidatorItem(rule.type);
const cellOrigin = pos.rawData;
let cache: Nullable<CellValue>;
const cellValue = {
get value() {
if (cache !== undefined) {
return cache;
}
cache = getCellValueOrigin(cellOrigin) ?? null;
return cache;
},
};
const valueStr = {
get value() {
return `${cellValue.value ?? ''}`;
},
};
if (!cell || cell === pos.rawData) {
cell = { ...pos.rawData };
}
cell.markers = {
...cell?.markers,
...validStatus === DataValidationStatus.INVALID ? INVALID_MARK : null,
};
cell.customRender = [
...(cell?.customRender ?? []),
...(validator?.canvasRender ? [validator.canvasRender] : []),
];
cell.fontRenderExtension = {
...cell?.fontRenderExtension,
isSkip: cell?.fontRenderExtension?.isSkip || validator?.skipDefaultFontRender?.(rule, cellValue.value, pos),
};
cell.interceptorStyle = {
...cell?.interceptorStyle,
...validator?.getExtraStyle(rule, valueStr.value, {
get style() {
const styleMap = workbook.getStyles();
return (typeof cell?.s === 'string' ? styleMap.get(cell?.s) : cell?.s) || {};
},
}, row, col),
};
cell.interceptorAutoHeight = () => {
const skeleton = this._renderManagerService.getRenderUnitById(unitId)
?.with(SheetSkeletonManagerService)
.getSkeletonParam(subUnitId)
?.skeleton;
if (!skeleton) {
return undefined;
}
const mergeCell = skeleton.worksheet.getMergedCell(row, col);
const info: ICellRenderContext = {
data: cell,
style: workbook.getStyles().getStyleByCell(cell),
primaryWithCoord: skeleton.getCellWithCoordByIndex(mergeCell?.startRow ?? row, mergeCell?.startColumn ?? col),
unitId,
subUnitId,
row,
col,
workbook,
worksheet,
};
return validator?.canvasRender?.calcCellAutoHeight?.(info);
};
cell.interceptorAutoWidth = () => {
const skeleton = this._renderManagerService.getRenderUnitById(unitId)
?.with(SheetSkeletonManagerService)
.getSkeletonParam(subUnitId)
?.skeleton;
if (!skeleton) {
return undefined;
}
const mergeCell = skeleton.worksheet.getMergedCell(row, col);
const info: ICellRenderContext = {
data: cell,
style: workbook.getStyles().getStyleByCell(cell),
primaryWithCoord: skeleton.getCellWithCoordByIndex(mergeCell?.startRow ?? row, mergeCell?.startColumn ?? col),
unitId,
subUnitId,
row,
col,
workbook,
worksheet,
};
return validator?.canvasRender?.calcCellAutoWidth?.(info);
};
cell.coverable = (cell?.coverable ?? true) && !(rule.type === DataValidationType.LIST || rule.type === DataValidationType.LIST_MULTIPLE);
return next(cell);
},
}
)
);
}
private _initAutoHeight() {
this._sheetDataValidationModel.ruleChange$
.pipe(
// patched data-validation change don't need to re-calc row height
// re-calc of row height will be triggered precisely by the origin command
filter((change) => change.source === 'command'),
bufferTime(100),
takeUntil(this.dispose$)
)
.subscribe((infos) => {
recalculateAutoHeight(infos, this._autoHeightController, this._commandService);
});
}
}
export function recalculateAutoHeight(
infos: IRuleChange[],
autoHeightController: AutoHeightController,
commandService: ICommandService
) {
const rangesByUnit = new Map<string, Map<string, IRange[]>>();
for (const { unitId, subUnitId, rule } of infos) {
if (rule.type !== DataValidationType.LIST && rule.type !== DataValidationType.LIST_MULTIPLE) {
continue;
}
if (!rule.ranges?.length) {
continue;
}
let rangesBySheet = rangesByUnit.get(unitId);
if (!rangesBySheet) {
rangesBySheet = new Map();
rangesByUnit.set(unitId, rangesBySheet);
}
const ranges = rangesBySheet.get(subUnitId) ?? [];
ranges.push(...rule.ranges);
rangesBySheet.set(subUnitId, ranges);
}
for (const [unitId, rangesBySheet] of rangesByUnit) {
for (const [subUnitId, ranges] of rangesBySheet) {
const mutations = autoHeightController.getUndoRedoParamsOfAutoHeight(ranges, subUnitId, undefined, unitId);
sequenceExecute(mutations.redos, commandService);
}
}
}

View File

@@ -0,0 +1,51 @@
/**
* 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 { IRenderContext, IRenderModule } from '@univerjs/engine-render';
import type { IValidStatusChange } from '@univerjs/sheets-data-validation';
import { bufferDebounceTime, Disposable, Inject } from '@univerjs/core';
import { SheetDataValidationModel } from '@univerjs/sheets-data-validation';
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
export class SheetsDataValidationReRenderController extends Disposable implements IRenderModule {
constructor(
private readonly _context: IRenderContext<Workbook>,
@Inject(SheetDataValidationModel) private readonly _sheetDataValidationModel: SheetDataValidationModel,
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService
) {
super();
this._initSkeletonChange();
}
private _initSkeletonChange() {
const reRender = (values: IValidStatusChange[]) => {
if (!values.length) {
return;
}
const sheetIds = new Set<string>();
values.forEach((value) => {
sheetIds.add(value.subUnitId);
});
sheetIds.forEach((sheetId) => {
this._sheetSkeletonManagerService.getSkeletonParam(sheetId)?.skeleton.makeDirty(true);
});
this._context.mainComponent?.makeForceDirty();
};
this.disposeWithMe(this._sheetDataValidationModel.validStatusChange$.pipe(bufferDebounceTime(16)).subscribe(reRender));
}
}

View File

@@ -0,0 +1,37 @@
/**
* 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 { Disposable, Inject } from '@univerjs/core';
import { CheckboxIcon, DataValidationIcon, DatePickerIcon, DropdownListIcon } from '@univerjs/icons';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { DROPDOWN_PRESETS_COMPONENT, DropdownPresets } from '../../views/components/DropdownPresets';
export class MobileComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) componentManager: ComponentManager,
@Inject(IconManager) iconManager: IconManager
) {
super();
this.disposeWithMe(componentManager.register(DROPDOWN_PRESETS_COMPONENT, DropdownPresets));
this.disposeWithMe(iconManager.register({
CheckboxIcon,
DataValidationIcon,
DatePickerIcon,
DropdownListIcon,
}));
}
}

View File

@@ -0,0 +1,176 @@
/**
* 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 { ICellRenderContext } from '@univerjs/core';
import {
DataValidationStatus,
DataValidationType,
ICommandService,
Inject,
InterceptorEffectEnum,
RxDisposable,
} from '@univerjs/core';
import { DataValidatorRegistryService } from '@univerjs/data-validation';
import { IRenderManagerService } from '@univerjs/engine-render';
import { InterceptCellContentPriority, INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
import { DataValidationCacheService, getCellValueOrigin, SheetDataValidationModel } from '@univerjs/sheets-data-validation';
import { AutoHeightController, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { bufferTime, filter, takeUntil } from 'rxjs';
import { recalculateAutoHeight } from '../dv-render.controller';
const INVALID_MARK = {
tr: {
size: 6,
color: '#fe4b4b',
},
};
// The mobile version does not provide the ability to change data validation model.
export class SheetsDataValidationMobileRenderController extends RxDisposable {
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@Inject(AutoHeightController) private readonly _autoHeightController: AutoHeightController,
@Inject(DataValidatorRegistryService) private readonly _dataValidatorRegistryService: DataValidatorRegistryService,
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(SheetDataValidationModel) private readonly _sheetDataValidationModel: SheetDataValidationModel,
@Inject(DataValidationCacheService) private readonly _dataValidationCacheService: DataValidationCacheService
) {
super();
this._initViewModelIntercept();
this._initAutoHeight();
}
// eslint-disable-next-line max-lines-per-function
private _initViewModelIntercept() {
this.disposeWithMe(
this._sheetInterceptorService.intercept(
INTERCEPTOR_POINT.CELL_CONTENT,
{
effect: InterceptorEffectEnum.Style,
// must be after numfmt
priority: InterceptCellContentPriority.DATA_VALIDATION,
// eslint-disable-next-line complexity, max-lines-per-function
handler: (cell, pos, next) => {
const { row, col, unitId, subUnitId, workbook, worksheet } = pos;
const ruleId = this._sheetDataValidationModel.getRuleIdByLocation(unitId, subUnitId, row, col);
if (!ruleId) {
return next(cell);
}
const rule = this._sheetDataValidationModel.getRuleById(unitId, subUnitId, ruleId);
if (!rule) {
return next(cell);
}
const validStatus = this._dataValidationCacheService.getValue(unitId, subUnitId, row, col) ?? DataValidationStatus.VALID;
const validator = this._dataValidatorRegistryService.getValidatorItem(rule.type);
const cellOrigin = worksheet.getCellRaw(row, col);
const cellValue = getCellValueOrigin(cellOrigin);
const valueStr = `${cellValue ?? ''}`;
if (!cell || cell === pos.rawData) {
cell = { ...pos.rawData };
}
cell.markers = {
...cell?.markers,
...validStatus === DataValidationStatus.INVALID ? INVALID_MARK : null,
};
cell.customRender = [
...(cell?.customRender ?? []),
...(validator?.canvasRender ? [validator.canvasRender] : []),
];
cell.fontRenderExtension = {
...cell?.fontRenderExtension,
isSkip: cell?.fontRenderExtension?.isSkip || validator?.skipDefaultFontRender?.(rule, cellValue, pos),
};
cell.interceptorStyle = {
...cell?.interceptorStyle,
...validator?.getExtraStyle(rule, valueStr, {
get style() {
const styleMap = workbook.getStyles();
return (typeof cell?.s === 'string' ? styleMap.get(cell?.s) : cell?.s) || {};
},
}, row, col),
};
cell.interceptorAutoHeight = () => {
const skeleton = this._renderManagerService.getRenderUnitById(unitId)
?.with(SheetSkeletonManagerService)
.getSkeletonParam(subUnitId)
?.skeleton;
if (!skeleton) {
return undefined;
}
const mergeCell = skeleton.worksheet.getMergedCell(row, col);
const info: ICellRenderContext = {
data: cell,
style: workbook.getStyles().getStyleByCell(cell),
primaryWithCoord: skeleton.getCellWithCoordByIndex(mergeCell?.startRow ?? row, mergeCell?.startColumn ?? col),
unitId,
subUnitId,
row,
col,
workbook,
worksheet,
};
return validator?.canvasRender?.calcCellAutoHeight?.(info);
};
cell.interceptorAutoWidth = () => {
const skeleton = this._renderManagerService.getRenderUnitById(unitId)
?.with(SheetSkeletonManagerService)
.getSkeletonParam(subUnitId)
?.skeleton;
if (!skeleton) {
return undefined;
}
const mergeCell = skeleton.worksheet.getMergedCell(row, col);
const info: ICellRenderContext = {
data: cell,
style: workbook.getStyles().getStyleByCell(cell),
primaryWithCoord: skeleton.getCellWithCoordByIndex(mergeCell?.startRow ?? row, mergeCell?.startColumn ?? col),
unitId,
subUnitId,
row,
col,
workbook,
worksheet,
};
return validator?.canvasRender?.calcCellAutoWidth?.(info);
};
cell.coverable = (cell?.coverable ?? true) && !(rule.type === DataValidationType.LIST || rule.type === DataValidationType.LIST_MULTIPLE);
return next(cell);
},
}
)
);
}
private _initAutoHeight() {
this._sheetDataValidationModel.ruleChange$
.pipe(
filter((change) => change.source === 'command'),
bufferTime(16),
takeUntil(this.dispose$)
)
.subscribe((infos) => {
recalculateAutoHeight(infos, this._autoHeightController, this._commandService);
});
}
}

View File

@@ -0,0 +1,61 @@
/**
* 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 { Ctor } from '@univerjs/core';
import type { BaseSheetDataValidatorView } from '../views/validator-views/sheet-validator-view';
import { Inject, Injector, RxDisposable } from '@univerjs/core';
import { DataValidatorRegistryService } from '@univerjs/data-validation';
import { CheckboxValidatorView } from '../views/validator-views/checkbox-validator-view';
import { CustomFormulaValidatorView } from '../views/validator-views/custom-validator-view';
import { DateValidatorView } from '../views/validator-views/date-validator-view';
import { DecimalValidatorView } from '../views/validator-views/decimal-validator-view';
import { ListMultipleValidatorView } from '../views/validator-views/list-multiple-view';
import { ListValidatorView } from '../views/validator-views/list-validator-view';
import { TextLengthValidatorView } from '../views/validator-views/text-length-validator.view';
import { WholeValidatorView } from '../views/validator-views/whole-validator-view';
export class SheetsDataValidationUIController extends RxDisposable {
constructor(
@Inject(Injector) private readonly _injector: Injector,
@Inject(DataValidatorRegistryService) private readonly _dataValidatorRegistryService: DataValidatorRegistryService
) {
super();
this._registerValidatorViews();
}
private _registerValidatorViews(): void {
([
DecimalValidatorView,
WholeValidatorView,
TextLengthValidatorView,
DateValidatorView,
CheckboxValidatorView,
ListValidatorView,
ListMultipleValidatorView,
CustomFormulaValidatorView,
] as Ctor<BaseSheetDataValidatorView>[]).forEach((v) => {
const view = this._injector.createInstance(v);
const validator = this._dataValidatorRegistryService.getValidatorItem(view.id);
if (validator) {
validator.formulaInput = view.formulaInput;
validator.canvasRender = view.canvasRender;
validator.dropdownType = view.dropdownType;
validator.optionsInput = view.optionsInput;
}
});
}
}