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,46 @@
# @univerjs/sheets-conditional-formatting-ui
[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-conditional-formatting-ui?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-conditional-formatting-ui)
[![license](https://img.shields.io/npm/l/@univerjs/sheets-conditional-formatting-ui?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-conditional-formatting-ui)
[![downloads](https://img.shields.io/npm/dm/@univerjs/sheets-conditional-formatting-ui?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-conditional-formatting-ui)
`@univerjs/sheets-conditional-formatting-ui` adds the conditional formatting panels, menus, and interaction UI for Univer Sheets.
## Package Overview
| Package | UMD global | CSS | Locales | Facade entry |
| --- | --- | :---: | :---: | :---: |
| `@univerjs/sheets-conditional-formatting-ui` | `UniverSheetsConditionalFormattingUi` | Yes | Yes | No |
## Installation
```sh
pnpm add @univerjs/sheets-conditional-formatting-ui
# or
npm install @univerjs/sheets-conditional-formatting-ui
```
Keep all `@univerjs/*` packages on the same version.
## Usage
```ts
import '@univerjs/sheets-conditional-formatting-ui/lib/index.css';
import EnUS from '@univerjs/sheets-conditional-formatting-ui/locale/en-US';
import { UniverSheetsConditionalFormattingUIPlugin } from '@univerjs/sheets-conditional-formatting-ui';
univer.registerPlugin(UniverSheetsConditionalFormattingUIPlugin);
// Merge EnUS into your Univer locale map when this package contributes UI text.
```
Exported plugin classes:
- `UniverSheetsConditionalFormattingUIPlugin`
- `UniverSheetsConditionalFormattingMobileUIPlugin`
## Resources
- [Documentation](https://docs.univer.ai)
- [NPM package](https://npmjs.com/package/@univerjs/sheets-conditional-formatting-ui)
- [GitHub repository](https://github.com/dream-num/univer)

View File

@@ -0,0 +1,98 @@
{
"name": "@univerjs/sheets-conditional-formatting-ui",
"version": "1.0.0-rc.0",
"private": false,
"description": "Conditional formatting UI for Univer Sheets.",
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
"license": "Apache-2.0",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/univer"
},
"homepage": "https://univer.ai",
"repository": {
"type": "git",
"url": "https://github.com/dream-num/univer"
},
"bugs": {
"url": "https://github.com/dream-num/univer/issues"
},
"keywords": [
"univer",
"sheets",
"conditional-formatting",
"ui",
"plugin"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*",
"./locale/*": "./src/locale/*.ts"
},
"main": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/es/index.js",
"module": "./lib/es/index.js",
"exports": {
".": {
"import": "./lib/es/index.js",
"require": "./lib/cjs/index.js",
"types": "./lib/types/index.d.ts"
},
"./*": {
"import": "./lib/es/*",
"require": "./lib/cjs/*",
"types": "./lib/types/index.d.ts"
},
"./locale/*": {
"import": "./lib/es/locale/*.js",
"require": "./lib/cjs/locale/*.js",
"types": "./lib/types/locale/*.d.ts"
},
"./lib/*": "./lib/*"
}
},
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"build:bundle": "univer-cli build",
"build:types": "tsc -p tsconfig.node.json",
"build": "pnpm run build:bundle && pnpm run build:types"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "1.38.0",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-conditional-formatting": "workspace:*",
"@univerjs/sheets-formula": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"postcss": "^8.5.28",
"react": "18.3.1",
"rxjs": "^7.8.2",
"tailwindcss": "3.4.18",
"typescript": "^6.0.3",
"vitest": "^5.0.0"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@univerjs-infra/shared/postcss';

View File

@@ -0,0 +1,380 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency, IDisposable, IRange, IWorkbookData, Nullable, Workbook } from '@univerjs/core';
import type { ISelectionWithStyle } from '@univerjs/sheets';
import {
BooleanNumber,
Disposable,
ICommandService,
ILogService,
Inject,
Injector,
IUniverInstanceService,
LocaleService,
LocaleType,
LogLevel,
Plugin,
RANGE_TYPE,
toDisposable,
Tools,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import {
ActiveDirtyManagerService,
IActiveDirtyManagerService,
OtherFormulaMarkDirty,
RegisterOtherFormulaService,
RemoveOtherFormulaMutation,
SetOtherFormulaMutation,
} from '@univerjs/engine-formula';
import {
BorderStyleManagerService,
RangeProtectionRuleModel,
SetRangeValuesCommand,
SetRangeValuesMutation,
SheetInterceptorService,
SheetLazyExecuteScheduleService,
SheetSkeletonService,
SheetsSelectionsService,
WorkbookPermissionService,
WorksheetPermissionService,
WorksheetProtectionPointModel,
WorksheetProtectionRuleModel,
} from '@univerjs/sheets';
import {
AddConditionalRuleMutation,
ClearRangeCfCommand,
ClearWorksheetCfCommand,
ConditionalFormattingFormulaService,
ConditionalFormattingRangeIndexModel,
ConditionalFormattingRangeTransformService,
ConditionalFormattingRuleModel,
ConditionalFormattingViewModel,
DeleteConditionalRuleMutation,
MoveConditionalRuleMutation,
SetConditionalRuleMutation,
} from '@univerjs/sheets-conditional-formatting';
import { ISheetClipboardService } from '@univerjs/sheets-ui';
import { ComponentManager, ISidebarService } from '@univerjs/ui';
import { BehaviorSubject, Subject } from 'rxjs';
const TEST_WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
},
1: {
v: 'A2',
},
},
},
columnData: {
1: {
hd: BooleanNumber.FALSE,
},
},
rowData: {
1: {
hd: BooleanNumber.FALSE,
},
},
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
class TestSidebarService extends Disposable {
private _sidebarOptions = { id: '', visible: false } as any;
readonly sidebarOptions$ = new BehaviorSubject(this._sidebarOptions);
readonly scrollEvent$ = new Subject<Event>();
private _container?: HTMLElement;
private _width?: number;
get visible(): boolean {
return Boolean(this._sidebarOptions.visible);
}
get options() {
return this._sidebarOptions;
}
get width(): number | undefined {
return this._width;
}
setWidth(value: number): void {
this._width = value;
}
open(params: any): IDisposable {
this._sidebarOptions = {
...params,
visible: true,
};
this.sidebarOptions$.next(this._sidebarOptions);
params.onOpen?.();
return toDisposable(() => {
this.close(params.id);
});
}
close(id?: string): void {
if (id && this._sidebarOptions.id !== id) {
return;
}
this._sidebarOptions = {
...this._sidebarOptions,
visible: false,
};
this.sidebarOptions$.next(this._sidebarOptions);
this._sidebarOptions.onClose?.();
}
getContainer() {
return this._container;
}
setContainer(element?: HTMLElement): void {
this._container = element;
}
}
class TestSheetClipboardService extends Disposable {
private _clipboardHooks: any[] = [];
private readonly _showMenu$ = new BehaviorSubject(false);
readonly showMenu$ = this._showMenu$.asObservable();
private readonly _pasteOptionsCache$ = new BehaviorSubject<any>(null);
readonly pasteOptionsCache$ = this._pasteOptionsCache$.asObservable();
setShowMenu(show: boolean): void {
this._showMenu$.next(show);
}
getPasteMenuVisible(): boolean {
return this._showMenu$.getValue();
}
getPasteOptionsCache(): any {
return this._pasteOptionsCache$.getValue();
}
updatePasteOptionsCache(cache: any): void {
this._pasteOptionsCache$.next(cache);
}
async copy(): Promise<boolean> {
return false;
}
async cut(): Promise<boolean> {
return false;
}
async paste(): Promise<boolean> {
return false;
}
async pasteByCopyId(): Promise<boolean> {
return false;
}
async legacyPaste(): Promise<boolean> {
return false;
}
rePasteWithPasteType(): boolean {
return false;
}
disposePasteOptionsCache(): void {
this._pasteOptionsCache$.next(null);
}
generateCopyContent(_workbookId: string, _worksheetId: string, _range: IRange): Nullable<unknown> {
return null;
}
copyContentCache(): any {
return null;
}
addClipboardHook(hook: any): IDisposable {
this._clipboardHooks.push(hook);
return toDisposable(() => {
const hooks = [];
for (const item of this._clipboardHooks) {
if (item !== hook) {
hooks.push(item);
}
}
this._clipboardHooks = hooks;
});
}
getClipboardHooks(): any[] {
return this._clipboardHooks;
}
removeMarkSelection(): void {
// Mark selections are not created in these UI command tests.
}
override dispose(): void {
super.dispose();
this._showMenu$.complete();
this._pasteOptionsCache$.complete();
this._clipboardHooks = [];
}
}
function createLocalSheetsTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]) {
const univer = new Univer();
const injector = univer.__getInjector();
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
const injector = this._injector;
injector.add([WorksheetPermissionService]);
injector.add([WorksheetProtectionPointModel]);
injector.add([RangeProtectionRuleModel]);
injector.add([WorkbookPermissionService]);
injector.add([WorksheetProtectionRuleModel]);
injector.add([SheetsSelectionsService]);
injector.add([BorderStyleManagerService]);
injector.add([SheetInterceptorService]);
injector.add([SheetSkeletonService]);
injector.add([SheetLazyExecuteScheduleService]);
dependencies?.forEach((dependency) => injector.add(dependency));
injector.get(SheetInterceptorService);
injector.get(SheetSkeletonService);
injector.get(WorkbookPermissionService);
injector.get(WorksheetPermissionService);
}
}
univer.registerPlugin(TestPlugin);
const sheet = univer.createUnit<IWorkbookData, Workbook>(
UniverInstanceType.UNIVER_SHEET,
Tools.deepClone(workbookData || TEST_WORKBOOK_DATA_DEMO)
);
const univerInstanceService = injector.get(IUniverInstanceService);
univerInstanceService.focusUnit('test');
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
injector.get(LocaleService).load({
[LocaleType.EN_US]: {},
});
return {
univer,
injector,
get: injector.get.bind(injector),
sheet,
};
}
export function createCfUiTestBed() {
const testBed = createLocalSheetsTestBed();
const { univer, get, injector } = testBed;
const commandService = get(ICommandService);
const selectionsService = get(SheetsSelectionsService);
injector.add([IActiveDirtyManagerService, { useClass: ActiveDirtyManagerService }]);
injector.add([RegisterOtherFormulaService]);
injector.add([ConditionalFormattingRuleModel]);
injector.add([ConditionalFormattingRangeIndexModel]);
injector.add([ConditionalFormattingFormulaService]);
injector.add([ConditionalFormattingRangeTransformService]);
injector.add([ConditionalFormattingViewModel]);
injector.add([ComponentManager]);
injector.add([ISidebarService, { useClass: TestSidebarService as never }]);
injector.add([ISheetClipboardService, { useClass: TestSheetClipboardService as never }]);
[
AddConditionalRuleMutation,
DeleteConditionalRuleMutation,
SetConditionalRuleMutation,
MoveConditionalRuleMutation,
ClearRangeCfCommand,
ClearWorksheetCfCommand,
OtherFormulaMarkDirty,
RemoveOtherFormulaMutation,
SetRangeValuesCommand,
SetRangeValuesMutation,
SetOtherFormulaMutation,
].forEach((command) => commandService.registerCommand(command));
const setSelection = (range: ISelectionWithStyle['range']) => {
selectionsService.addSelections([{
range: {
...range,
rangeType: RANGE_TYPE.NORMAL,
},
primary: null,
style: null,
}]);
};
return {
univer,
get,
injector,
commandService,
selectionsService,
ruleModel: injector.get(ConditionalFormattingRuleModel),
viewModel: injector.get(ConditionalFormattingViewModel),
componentManager: injector.get(ComponentManager),
sidebarService: injector.get(ISidebarService) as unknown as TestSidebarService,
sidebarOptions$: (injector.get(ISidebarService) as unknown as TestSidebarService).sidebarOptions$,
sheetClipboardService: injector.get(ISheetClipboardService) as unknown as TestSheetClipboardService,
getClipboardHook: () => (injector.get(ISheetClipboardService) as unknown as TestSheetClipboardService).getClipboardHooks()[0] as
| {
onBeforeCopy: (unitId: string, subUnitId: string, range: ISelectionWithStyle['range']) => void;
onPasteCells: (...args: any[]) => { redos: Array<{ id: string; params: unknown }>; undos: Array<{ id: string; params: unknown }> };
}
| undefined,
unitId: 'test',
subUnitId: 'sheet1',
setSelection,
};
}

View File

@@ -0,0 +1,417 @@
/**
* 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 {
CFNumberOperator,
CFRuleType,
CFSubRuleType,
CFTextOperator,
CFTimePeriodOperator,
CFValueType,
ClearRangeCfCommand,
IIconSetType,
} from '@univerjs/sheets-conditional-formatting';
import { afterEach, describe, expect, it } from 'vitest';
import { createCfUiTestBed } from '../../../__tests__/create-cf-ui-test-bed';
import { AddAverageCfCommand } from '../add-average-cf.command';
import { AddColorScaleConditionalRuleCommand } from '../add-color-scale-cf.command';
import { AddDataBarConditionalRuleCommand } from '../add-data-bar-cf.command';
import { AddDuplicateValuesCfCommand } from '../add-duplicate-values-cf.command';
import { AddIconSetConditionalRuleCommand } from '../add-icon-set-cf.command';
import { AddNumberCfCommand } from '../add-number-cf.command';
import { AddRankCfCommand } from '../add-rank-cf.command';
import { AddTextCfCommand } from '../add-text-cf.command';
import { AddTimePeriodCfCommand } from '../add-time-period-cf.command';
import { AddUniqueValuesCfCommand } from '../add-unique-values-cf.command';
const range: IRange = {
startRow: 1,
endRow: 3,
startColumn: 2,
endColumn: 4,
};
describe('conditional formatting add commands', () => {
afterEach(() => {
// each test disposes its own univer instance
});
it('adds number highlight rules into the real rule model', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddNumberCfCommand);
expect(await testBed.commandService.executeCommand(AddNumberCfCommand.id, {
ranges: [range],
operator: CFNumberOperator.between,
style: { bg: { rgb: '#aaf' } },
value: [10, 20],
stopIfTrue: true,
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(1);
expect(rules?.[0]).toMatchObject({
ranges: [range],
stopIfTrue: true,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: CFNumberOperator.between,
style: { bg: { rgb: '#aaf' } },
value: [10, 20],
},
});
testBed.univer.dispose();
});
it('adds number highlight rules with a single comparison value', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddNumberCfCommand);
expect(await testBed.commandService.executeCommand(AddNumberCfCommand.id, {
ranges: [range],
operator: CFNumberOperator.greaterThan,
style: { bg: { rgb: '#afa' } },
value: 100,
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(1);
expect(rules?.[0]).toMatchObject({
ranges: [range],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: CFNumberOperator.greaterThan,
style: { bg: { rgb: '#afa' } },
value: 100,
},
});
testBed.univer.dispose();
});
it('adds color scale rules into the real rule model', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddColorScaleConditionalRuleCommand);
expect(await testBed.commandService.executeCommand(AddColorScaleConditionalRuleCommand.id, {
ranges: [range],
config: [{ index: 0, color: '#f00' }, { index: 1, color: '#0f0' }],
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(1);
expect(rules?.[0]).toMatchObject({
ranges: [range],
rule: {
type: CFRuleType.colorScale,
config: [{ index: 0, color: '#f00' }, { index: 1, color: '#0f0' }],
},
});
testBed.univer.dispose();
});
it('adds data bar rules into the real rule model', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddDataBarConditionalRuleCommand);
expect(await testBed.commandService.executeCommand(AddDataBarConditionalRuleCommand.id, {
ranges: [range],
min: { type: 'min' },
max: { type: 'max' },
nativeColor: '#ddd',
positiveColor: '#0f0',
isGradient: true,
isShowValue: false,
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(1);
expect(rules?.[0]).toMatchObject({
ranges: [range],
rule: {
type: CFRuleType.dataBar,
isShowValue: false,
config: {
min: { type: 'min' },
max: { type: 'max' },
nativeColor: '#ddd',
positiveColor: '#0f0',
isGradient: true,
},
},
});
testBed.univer.dispose();
});
it('adds a data bar to the current selection when ranges are omitted', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddDataBarConditionalRuleCommand);
testBed.setSelection(range);
expect(await testBed.commandService.executeCommand(AddDataBarConditionalRuleCommand.id, {
min: { type: CFValueType.min },
max: { type: CFValueType.max },
nativeColor: '#ddd',
positiveColor: '#0f0',
isGradient: false,
isShowValue: true,
})).toBe(true);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId)?.[0].ranges).toMatchObject([range]);
testBed.univer.dispose();
});
it('adds an icon set to the current selection', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddIconSetConditionalRuleCommand);
testBed.setSelection(range);
const config = [
{
iconType: IIconSetType.threeStars,
iconId: '0',
operator: CFNumberOperator.greaterThanOrEqual,
value: { type: CFValueType.percent, value: 67 },
},
{
iconType: IIconSetType.threeStars,
iconId: '1',
operator: CFNumberOperator.greaterThanOrEqual,
value: { type: CFValueType.percent, value: 33 },
},
{
iconType: IIconSetType.threeStars,
iconId: '2',
operator: CFNumberOperator.lessThan,
value: { type: CFValueType.percent, value: Number.MAX_SAFE_INTEGER },
},
];
expect(await testBed.commandService.executeCommand(AddIconSetConditionalRuleCommand.id, {
config,
isShowValue: true,
})).toBe(true);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId)?.[0]).toMatchObject({
ranges: [range],
rule: {
type: CFRuleType.iconSet,
config,
isShowValue: true,
},
});
testBed.univer.dispose();
});
it('clears only the requested conditional formatting type', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddDataBarConditionalRuleCommand);
testBed.commandService.registerCommand(AddIconSetConditionalRuleCommand);
await testBed.commandService.executeCommand(AddDataBarConditionalRuleCommand.id, {
ranges: [range],
min: { type: CFValueType.min },
max: { type: CFValueType.max },
nativeColor: '#ddd',
positiveColor: '#0f0',
isGradient: false,
isShowValue: true,
});
await testBed.commandService.executeCommand(AddIconSetConditionalRuleCommand.id, {
ranges: [range],
config: [{
iconType: IIconSetType.threeStars,
iconId: '0',
operator: CFNumberOperator.greaterThanOrEqual,
value: { type: CFValueType.num, value: 0 },
}],
isShowValue: true,
});
expect(testBed.commandService.syncExecuteCommand(ClearRangeCfCommand.id, {
ranges: [range],
types: [CFRuleType.dataBar],
})).toBe(true);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId)).toMatchObject([{
rule: { type: CFRuleType.iconSet },
}]);
testBed.univer.dispose();
});
it('adds rank highlight rules for bottom percent scenarios', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddRankCfCommand);
expect(await testBed.commandService.executeCommand(AddRankCfCommand.id, {
ranges: [range],
isPercent: true,
isBottom: true,
value: 10,
style: { bg: { rgb: '#fed' } },
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(1);
expect(rules?.[0]).toMatchObject({
ranges: [range],
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.rank,
isPercent: true,
isBottom: true,
value: 10,
style: { bg: { rgb: '#fed' } },
},
});
testBed.univer.dispose();
});
it('adds average highlight rules for above-average cells', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddAverageCfCommand);
expect(await testBed.commandService.executeCommand(AddAverageCfCommand.id, {
ranges: [range],
operator: CFNumberOperator.greaterThan,
style: { cl: { rgb: '#080' } },
stopIfTrue: true,
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(1);
expect(rules?.[0]).toMatchObject({
ranges: [range],
stopIfTrue: true,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.average,
operator: CFNumberOperator.greaterThan,
style: { cl: { rgb: '#080' } },
},
});
testBed.univer.dispose();
});
it('adds text highlight rules for matching cell contents', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddTextCfCommand);
expect(await testBed.commandService.executeCommand(AddTextCfCommand.id, {
ranges: [range],
operator: CFTextOperator.containsText,
value: 'overdue',
style: { bl: 1 },
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(1);
expect(rules?.[0]).toMatchObject({
ranges: [range],
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.containsText,
value: 'overdue',
style: { bl: 1 },
},
});
testBed.univer.dispose();
});
it('adds time period rules for recent dates', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddTimePeriodCfCommand);
expect(await testBed.commandService.executeCommand(AddTimePeriodCfCommand.id, {
ranges: [range],
operator: CFTimePeriodOperator.last7Days,
style: { it: 1 },
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(1);
expect(rules?.[0]).toMatchObject({
ranges: [range],
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.timePeriod,
operator: CFTimePeriodOperator.last7Days,
style: { it: 1 },
},
});
testBed.univer.dispose();
});
it('adds duplicate and unique value rules independently', async () => {
const testBed = createCfUiTestBed();
testBed.commandService.registerCommand(AddDuplicateValuesCfCommand);
testBed.commandService.registerCommand(AddUniqueValuesCfCommand);
expect(await testBed.commandService.executeCommand(AddDuplicateValuesCfCommand.id, {
ranges: [range],
style: { bg: { rgb: '#fcc' } },
})).toBe(true);
expect(await testBed.commandService.executeCommand(AddUniqueValuesCfCommand.id, {
ranges: [range],
style: { bg: { rgb: '#ccf' } },
stopIfTrue: true,
})).toBe(true);
const rules = testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId);
expect(rules).toHaveLength(2);
let duplicateRule = rules?.[0];
let uniqueRule = rules?.[1];
if (duplicateRule?.rule.type !== CFRuleType.highlightCell || duplicateRule.rule.subType !== CFSubRuleType.duplicateValues) {
duplicateRule = rules?.[1];
uniqueRule = rules?.[0];
}
expect(duplicateRule).toMatchObject({
ranges: [range],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.duplicateValues,
style: { bg: { rgb: '#fcc' } },
},
});
expect(uniqueRule).toMatchObject({
ranges: [range],
stopIfTrue: true,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.uniqueValues,
style: { bg: { rgb: '#ccf' } },
},
});
testBed.univer.dispose();
});
});

View File

@@ -0,0 +1,64 @@
/**
* 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 { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IAverageHighlightCell, IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddAverageCfParams {
ranges: IRange[];
stopIfTrue?: boolean;
style: IAverageHighlightCell['style'];
operator: IAverageHighlightCell['operator'];
}
export const AddAverageCfCommand: ICommand<IAddAverageCfParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-average-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { ranges, style, stopIfTrue, operator } = params;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
const rule: IConditionFormattingRule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.average,
operator,
style,
},
};
const result = commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
return result;
},
};

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 type { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IColorScale, IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddColorScaleConditionalRuleParams {
ranges: IRange[];
stopIfTrue?: boolean;
config: IColorScale['config'];
}
export const AddColorScaleConditionalRuleCommand: ICommand<IAddColorScaleConditionalRuleParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-color-scale-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { ranges, config, stopIfTrue } = params;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
const rule: IConditionFormattingRule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.colorScale,
config,
},
};
return commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
},
};

View File

@@ -0,0 +1,76 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IConditionFormattingRule, IDataBar } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddUniqueValuesConditionalRuleParams {
ranges?: IRange[];
stopIfTrue?: boolean;
min: IDataBar['config']['min'];
max: IDataBar['config']['max'];
nativeColor: IDataBar['config']['nativeColor'];
positiveColor: IDataBar['config']['positiveColor'];
isGradient: IDataBar['config']['isGradient'];
isShowValue: IDataBar['isShowValue'];
}
export const AddDataBarConditionalRuleCommand: ICommand<IAddUniqueValuesConditionalRuleParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-data-bar-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { min, max, nativeColor, positiveColor, isGradient, stopIfTrue, isShowValue } = params;
const ranges = params.ranges ?? accessor.get(SheetsSelectionsService).getCurrentSelections().map((selection) => selection.range);
if (!ranges.length) return false;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const commandService = accessor.get(ICommandService);
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
const rule: IConditionFormattingRule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.dataBar,
isShowValue,
config: {
min,
max,
nativeColor,
positiveColor,
isGradient,
},
},
};
return commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
},
};

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 type { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IConditionFormattingRule, IHighlightCell } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddDuplicateValuesConditionalRuleParams {
ranges: IRange[];
stopIfTrue?: boolean;
style: IHighlightCell['style'];
}
export const AddDuplicateValuesCfCommand: ICommand<IAddDuplicateValuesConditionalRuleParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-duplicate-values-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { ranges, style, stopIfTrue } = params;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
const rule: IConditionFormattingRule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.duplicateValues,
style,
},
};
return commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
},
};

View File

@@ -0,0 +1,60 @@
/**
* 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 { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IConditionFormattingRule, IIconSet } from '@univerjs/sheets-conditional-formatting';
import { CommandType, ICommandService, IUniverInstanceService } from '@univerjs/core';
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddIconSetConditionalRuleParams {
ranges?: IRange[];
stopIfTrue?: boolean;
config: IIconSet['config'];
isShowValue: IIconSet['isShowValue'];
}
export const AddIconSetConditionalRuleCommand: ICommand<IAddIconSetConditionalRuleParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-icon-set-conditional-rule',
handler(accessor, params) {
if (!params) return false;
const ranges = params.ranges ?? accessor.get(SheetsSelectionsService).getCurrentSelections().map((selection) => selection.range);
if (!ranges.length) return false;
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
if (!target) return false;
const { unitId, subUnitId } = target;
const rule: IConditionFormattingRule = {
ranges,
cfId: accessor.get(ConditionalFormattingRuleModel).createCfId(unitId, subUnitId),
stopIfTrue: !!params.stopIfTrue,
rule: {
type: CFRuleType.iconSet,
config: params.config,
isShowValue: params.isShowValue,
},
};
return accessor.get(ICommandService).executeCommand(AddConditionalRuleMutation.id, {
unitId,
subUnitId,
rule,
} as IAddConditionalRuleMutationParams);
},
};

View File

@@ -0,0 +1,88 @@
/**
* 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 { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IConditionFormattingRule, INumberHighlightCell } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFNumberOperator, CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddNumberCfParams {
ranges: IRange[];
stopIfTrue?: boolean;
style: INumberHighlightCell['style'];
operator: INumberHighlightCell['operator'];
value: number | [number, number];
}
export const AddNumberCfCommand: ICommand<IAddNumberCfParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-number-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { ranges, style, stopIfTrue, operator, value } = params;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
let rule: IConditionFormattingRule;
if ([CFNumberOperator.between, CFNumberOperator.notBetween].includes(operator)) {
const _value = value as [number, number];
if (_value.length !== 2 || !Array.isArray(_value)) {
return false;
}
rule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: operator as CFNumberOperator.between | CFNumberOperator.notBetween,
style,
value: _value as [number, number],
},
};
} else {
const _value = value as number;
if (typeof _value !== 'number') {
return false;
}
rule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: operator as any,
style,
value: _value,
},
};
}
return commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
},
};

View File

@@ -0,0 +1,68 @@
/**
* 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 { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IConditionFormattingRule, IRankHighlightCell } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddRankCfParams {
ranges: IRange[];
stopIfTrue?: boolean;
style: IRankHighlightCell['style'];
isPercent: boolean;
isBottom: boolean;
value: number;
}
export const AddRankCfCommand: ICommand<IAddRankCfParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-rank-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { ranges, style, stopIfTrue, isPercent, isBottom, value } = params;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const univerInstanceService = accessor.get(IUniverInstanceService);
const commandService = accessor.get(ICommandService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
const rule: IConditionFormattingRule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.rank,
isPercent,
isBottom,
style,
value,
},
};
return commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
},
};

View File

@@ -0,0 +1,67 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IConditionFormattingRule, ITextHighlightCell } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddAverageCfParams {
ranges: IRange[];
stopIfTrue?: boolean;
style: ITextHighlightCell['style'];
operator: ITextHighlightCell['operator'];
value: ITextHighlightCell['value'];
}
export const AddTextCfCommand: ICommand<IAddAverageCfParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-text-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { ranges, style, stopIfTrue, operator, value } = params;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const commandService = accessor.get(ICommandService);
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
const rule: IConditionFormattingRule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator,
style,
value,
},
};
return commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
},
};

View File

@@ -0,0 +1,64 @@
/**
* 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 { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IConditionFormattingRule, ITimePeriodHighlightCell } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddTimePeriodCf {
ranges: IRange[];
stopIfTrue?: boolean;
style: ITimePeriodHighlightCell['style'];
operator: ITimePeriodHighlightCell['operator'];
}
export const AddTimePeriodCfCommand: ICommand<IAddTimePeriodCf> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-time-period-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { ranges, style, stopIfTrue, operator } = params;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const univerInstanceService = accessor.get(IUniverInstanceService);
const commandService = accessor.get(ICommandService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
const rule: IConditionFormattingRule<ITimePeriodHighlightCell> = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.timePeriod,
operator,
style,
},
};
return commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
},
};

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 { ICommand, IRange } from '@univerjs/core';
import type { IAddConditionalRuleMutationParams, IConditionFormattingRule, IHighlightCell } from '@univerjs/sheets-conditional-formatting';
import {
CommandType,
ICommandService,
IUniverInstanceService,
} from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
interface IAddUniqueValuesConditionalRuleParams {
ranges: IRange[];
stopIfTrue?: boolean;
style: IHighlightCell['style'];
}
export const AddUniqueValuesCfCommand: ICommand<IAddUniqueValuesConditionalRuleParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-uniqueValues-conditional-rule',
handler(accessor, params) {
if (!params) {
return false;
}
const { ranges, style, stopIfTrue } = params;
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const univerInstanceService = accessor.get(IUniverInstanceService);
const commandService = accessor.get(ICommandService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId } = target;
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
const rule: IConditionFormattingRule = {
ranges,
cfId,
stopIfTrue: !!stopIfTrue,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.uniqueValues,
style,
},
};
return commandService.executeCommand(AddConditionalRuleMutation.id, { unitId, subUnitId, rule } as IAddConditionalRuleMutationParams);
},
};

View File

@@ -0,0 +1,197 @@
/**
* 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 { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import { AddConditionalRuleMutation, CFRuleType, CFSubRuleType, CFTextOperator } from '@univerjs/sheets-conditional-formatting';
import { afterEach, describe, expect, it } from 'vitest';
import { createCfUiTestBed } from '../../../__tests__/create-cf-ui-test-bed';
import { ConditionalFormattingPanelController } from '../../../controllers/cf.panel.controller';
import { CF_MENU_OPERATION, OpenConditionalFormattingOperator } from '../open-conditional-formatting-panel';
const range: IRange = {
startRow: 1,
endRow: 2,
startColumn: 3,
endColumn: 4,
};
describe('OpenConditionalFormattingOperator', () => {
afterEach(() => {
// each test disposes its own univer instance
});
function createPanelTestBed() {
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingPanelController]);
testBed.injector.get(ConditionalFormattingPanelController);
testBed.commandService.registerCommand(OpenConditionalFormattingOperator);
testBed.setSelection(range);
return testBed;
}
async function openFromMenu(testBed: ReturnType<typeof createPanelTestBed>, value: CF_MENU_OPERATION) {
expect(await testBed.commandService.executeCommand(OpenConditionalFormattingOperator.id, { value })).toBe(true);
return testBed.sidebarService.options.children?.rule as IConditionFormattingRule | undefined;
}
it('opens the real sidebar panel with a rule seeded from the current selection', async () => {
const testBed = createPanelTestBed();
expect(await testBed.commandService.executeCommand(OpenConditionalFormattingOperator.id, {
value: CF_MENU_OPERATION.formula,
})).toBe(true);
expect(testBed.sidebarService.visible).toBe(true);
expect(testBed.sidebarService.options).toMatchObject({
id: 'sheet.conditional.formatting.panel',
children: expect.objectContaining({
rule: {
ranges: [expect.objectContaining(range)],
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.formula,
value: '=',
},
},
}),
});
testBed.univer.dispose();
});
it('opens the default rule editor for creating a new conditional formatting rule', async () => {
const testBed = createPanelTestBed();
const rule = await openFromMenu(testBed, CF_MENU_OPERATION.createRule);
expect(testBed.sidebarService.visible).toBe(true);
expect(rule).toMatchObject({
ranges: [expect.objectContaining(range)],
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
},
});
testBed.univer.dispose();
});
it('opens specialized rule editors from the conditional formatting menu', async () => {
const testBed = createPanelTestBed();
let rule = await openFromMenu(testBed, CF_MENU_OPERATION.rank);
expect(rule).toMatchObject({
ranges: [expect.objectContaining(range)],
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.rank,
},
});
rule = await openFromMenu(testBed, CF_MENU_OPERATION.colorScale);
expect(rule).toMatchObject({
ranges: [expect.objectContaining(range)],
rule: {
type: CFRuleType.colorScale,
config: [],
},
});
rule = await openFromMenu(testBed, CF_MENU_OPERATION.dataBar);
expect(rule).toMatchObject({
ranges: [expect.objectContaining(range)],
rule: {
type: CFRuleType.dataBar,
isShowValue: true,
},
});
rule = await openFromMenu(testBed, CF_MENU_OPERATION.icon);
expect(rule).toMatchObject({
ranges: [expect.objectContaining(range)],
rule: {
type: CFRuleType.iconSet,
config: [],
isShowValue: true,
},
});
testBed.univer.dispose();
});
it('opens the rule list without seeding a draft rule', async () => {
const testBed = createPanelTestBed();
const rule = await openFromMenu(testBed, CF_MENU_OPERATION.viewRule);
expect(testBed.sidebarService.visible).toBe(true);
expect(rule).toBeUndefined();
testBed.univer.dispose();
});
it('clears range rules and worksheet rules through the real operator flow', async () => {
const testBed = createPanelTestBed();
const addRule = async (cfId: string, ranges = [range]) => {
const rule: IConditionFormattingRule = {
cfId,
ranges,
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'A1',
style: { bg: { rgb: '#ff0' } },
},
};
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule,
});
};
await addRule('cf-range');
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId)).toHaveLength(1);
expect(await testBed.commandService.executeCommand(OpenConditionalFormattingOperator.id, {
value: CF_MENU_OPERATION.clearRangeRules,
})).toBe(true);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId)).toEqual([]);
await addRule('cf-sheet-1');
await addRule('cf-sheet-2', [{
startRow: 5,
endRow: 5,
startColumn: 5,
endColumn: 5,
}]);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId)).toHaveLength(2);
expect(await testBed.commandService.executeCommand(OpenConditionalFormattingOperator.id, {
value: CF_MENU_OPERATION.clearWorkSheetRules,
})).toBe(true);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId)).toEqual([]);
testBed.univer.dispose();
});
});

View File

@@ -0,0 +1,140 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IAccessor, ICommand } from '@univerjs/core';
import type { IClearRangeCfParams, IColorScale, IConditionFormattingRule, IDataBar, IFormulaHighlightCell, IIconSet, IRankHighlightCell } from '@univerjs/sheets-conditional-formatting';
import { CommandType, ICommandService } from '@univerjs/core';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { CFRuleType, CFSubRuleType, ClearRangeCfCommand, ClearWorksheetCfCommand, createDefaultRule } from '@univerjs/sheets-conditional-formatting';
import { ConditionalFormattingPanelController } from '../../controllers/cf.panel.controller';
interface IOpenConditionalFormattingOperatorParams {
value: number;
}
export enum CF_MENU_OPERATION {
createRule = 1,
viewRule,
highlightCell,
rank,
formula,
colorScale,
dataBar,
icon,
clearRangeRules,
clearWorkSheetRules,
}
export const OpenConditionalFormattingOperator: ICommand = {
id: 'sheet.operation.open.conditional.formatting.panel',
type: CommandType.OPERATION,
handler: (accessor: IAccessor, params: IOpenConditionalFormattingOperatorParams) => {
const conditionalFormattingMenuController = accessor.get(ConditionalFormattingPanelController);
const selectionManagerService = accessor.get(SheetsSelectionsService);
const commandService = accessor.get(ICommandService);
const ranges = selectionManagerService.getCurrentSelections()?.map((s) => s.range) || [];
const type = params.value;
switch (type) {
case CF_MENU_OPERATION.highlightCell:{
conditionalFormattingMenuController.openPanel({ ...createDefaultRule(), ranges });
break;
}
case CF_MENU_OPERATION.rank:{
const rule = {
...createDefaultRule,
ranges,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.rank,
},
} as IConditionFormattingRule<IRankHighlightCell>;
conditionalFormattingMenuController.openPanel(rule);
break;
}
case CF_MENU_OPERATION.formula:{
const rule = {
...createDefaultRule,
ranges,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.formula,
value: '=',
},
} as IConditionFormattingRule<IFormulaHighlightCell>;
conditionalFormattingMenuController.openPanel(rule);
break;
}
case CF_MENU_OPERATION.colorScale:{
const rule = {
...createDefaultRule,
ranges,
rule: {
type: CFRuleType.colorScale,
config: [],
},
} as unknown as IConditionFormattingRule<IColorScale>;
conditionalFormattingMenuController.openPanel(rule);
break;
}
case CF_MENU_OPERATION.dataBar:{
const rule = {
...createDefaultRule,
ranges,
rule: {
type: CFRuleType.dataBar,
isShowValue: true,
},
} as unknown as IConditionFormattingRule<IDataBar>;
conditionalFormattingMenuController.openPanel(rule);
break;
}
case CF_MENU_OPERATION.icon:{
const rule = {
...createDefaultRule,
ranges,
rule: {
type: CFRuleType.iconSet,
config: [],
isShowValue: true,
},
} as unknown as IConditionFormattingRule<IIconSet>;
conditionalFormattingMenuController.openPanel(rule);
break;
}
case CF_MENU_OPERATION.viewRule:{
conditionalFormattingMenuController.openPanel();
break;
}
case CF_MENU_OPERATION.createRule:{
conditionalFormattingMenuController.openPanel({ ...createDefaultRule(), ranges });
break;
}
case CF_MENU_OPERATION.clearRangeRules:{
commandService.executeCommand(ClearRangeCfCommand.id, { ranges } as IClearRangeCfParams);
break;
}
case CF_MENU_OPERATION.clearWorkSheetRules:{
commandService.executeCommand(ClearWorksheetCfCommand.id);
break;
}
}
return true;
},
};

View File

@@ -0,0 +1,27 @@
/**
* 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 { MenuConfig } from '@univerjs/ui';
export const SHEETS_CONDITIONAL_FORMATTING_UI_PLUGIN_CONFIG_KEY = 'sheets-conditional-formatting-ui.config';
export const configSymbol = Symbol(SHEETS_CONDITIONAL_FORMATTING_UI_PLUGIN_CONFIG_KEY);
export interface IUniverSheetsConditionalFormattingUIConfig {
menu?: MenuConfig;
}
export const defaultPluginConfig: IUniverSheetsConditionalFormattingUIConfig = {};

View File

@@ -0,0 +1,17 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const CF_PANEL_KEY = 'sheet.conditional.formatting.panel';

View File

@@ -0,0 +1,117 @@
/**
* 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 { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import { ClearSelectionAllCommand, ClearSelectionFormatCommand } from '@univerjs/sheets';
import { AddConditionalRuleMutation, CFRuleType, CFSubRuleType, CFTextOperator } from '@univerjs/sheets-conditional-formatting';
import { afterEach, describe, expect, it } from 'vitest';
import { createCfUiTestBed } from '../../__tests__/create-cf-ui-test-bed';
import { ConditionalFormattingClearController } from '../cf.clear.controller';
describe('ConditionalFormattingClearController', () => {
afterEach(() => {
// each test disposes its own univer instance
});
it('clips intersecting conditional formatting ranges when clearing selection format', async () => {
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingClearController]);
testBed.injector.get(ConditionalFormattingClearController);
testBed.commandService.registerCommand(ClearSelectionFormatCommand);
const rule: IConditionFormattingRule = {
cfId: 'cf-1',
ranges: [{
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 1,
}],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'A',
style: { bg: { rgb: '#ff0' } },
},
};
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule,
});
testBed.setSelection({
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
expect(await testBed.commandService.executeCommand(ClearSelectionFormatCommand.id)).toBe(true);
expect(testBed.ruleModel.getRule(testBed.unitId, testBed.subUnitId, 'cf-1')?.ranges).toEqual([{
startRow: 0,
endRow: 0,
startColumn: 1,
endColumn: 1,
}]);
testBed.univer.dispose();
});
it('removes fully covered conditional formatting rules when clearing selection all', async () => {
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingClearController]);
testBed.injector.get(ConditionalFormattingClearController);
testBed.commandService.registerCommand(ClearSelectionAllCommand);
const rule: IConditionFormattingRule = {
cfId: 'cf-2',
ranges: [{
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
}],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'A',
style: { bg: { rgb: '#ff0' } },
},
};
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule,
});
testBed.setSelection({
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
expect(await testBed.commandService.executeCommand(ClearSelectionAllCommand.id)).toBe(true);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, testBed.subUnitId)).toEqual([]);
testBed.univer.dispose();
});
});

View File

@@ -0,0 +1,383 @@
/**
* 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 { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import { IUniverInstanceService, Range, UniverInstanceType } from '@univerjs/core';
import {
AddConditionalRuleMutation,
CFRuleType,
CFSubRuleType,
CFTextOperator,
DeleteConditionalRuleMutation,
SetConditionalRuleMutation,
} from '@univerjs/sheets-conditional-formatting';
import { COPY_TYPE, PREDEFINED_HOOK_NAME_PASTE } from '@univerjs/sheets-ui';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createCfUiTestBed } from '../../__tests__/create-cf-ui-test-bed';
import { ConditionalFormattingCopyPasteController } from '../cf.copy-paste.controller';
describe('ConditionalFormattingCopyPasteController', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('creates copy-paste mutations from the real rule model and view model', async () => {
vi.spyOn(Range, 'foreach').mockImplementation(() => {
throw new Error('must not enumerate cells');
});
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
const sourceRule: IConditionFormattingRule = {
cfId: 'cf-source',
ranges: [{
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
}],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'A1',
style: { bg: { rgb: '#0f0' } },
},
};
const targetRule: IConditionFormattingRule = {
cfId: 'cf-target',
ranges: [{
startRow: 1,
endRow: 1,
startColumn: 1,
endColumn: 1,
}],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'B2',
style: { bg: { rgb: '#f00' } },
},
};
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: sourceRule,
});
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: targetRule,
});
const hook = testBed.getClipboardHook()!;
expect(hook).toBeDefined();
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
range: { rows: [0], cols: [0] },
},
{
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
range: { rows: [1], cols: [1] },
},
null,
{
copyType: COPY_TYPE.COPY,
pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE,
}
) as {
redos: Array<{ id: string; params: unknown }>;
undos: Array<{ id: string; params: unknown }>;
};
const redoIds = result.redos.map((item: { id: string }) => item.id);
expect(redoIds).toContain(DeleteConditionalRuleMutation.id);
expect(redoIds.some((id: string) => id === AddConditionalRuleMutation.id || id === SetConditionalRuleMutation.id)).toBe(true);
expect(result.undos.length).toBeGreaterThan(0);
const copiedRuleRedo = result.redos.find((item) => {
const params = item.params as {
rule?: {
cfId?: string;
ranges?: Array<{ startRow: number; endRow: number; startColumn: number; endColumn: number }>;
};
};
return (item.id === AddConditionalRuleMutation.id || item.id === SetConditionalRuleMutation.id) &&
params.rule?.cfId === 'cf-source';
});
const copiedRuleParams = copiedRuleRedo?.params as {
rule?: {
ranges?: Array<{ startRow: number; endRow: number; startColumn: number; endColumn: number }>;
};
} | undefined;
expect(copiedRuleParams?.rule?.ranges?.some((range: { startRow: number; endRow: number; startColumn: number; endColumn: number }) =>
range.startRow <= 1 &&
range.endRow >= 1 &&
range.startColumn <= 1 &&
range.endColumn >= 1
)).toBe(true);
testBed.univer.dispose();
});
it('keeps identical overlapping source rules distinct when copying', async () => {
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
const config: IConditionFormattingRule['rule'] = {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'A1',
style: { bg: { rgb: '#0f0' } },
};
for (const cfId of ['cf-source-1', 'cf-source-2']) {
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
cfId,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
stopIfTrue: false,
rule: config,
},
});
}
const hook = testBed.getClipboardHook()!;
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [1], cols: [1] } },
null,
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
);
const copiedRuleIds = result.redos.flatMap((mutation) => {
if (mutation.id !== AddConditionalRuleMutation.id && mutation.id !== SetConditionalRuleMutation.id) {
return [];
}
const rule = (mutation.params as { rule: IConditionFormattingRule }).rule;
return rule.ranges.some((range) => range.startRow <= 1 && range.endRow >= 1 && range.startColumn <= 1 && range.endColumn >= 1)
? [rule.cfId]
: [];
});
expect(new Set(copiedRuleIds)).toEqual(new Set(['cf-source-1', 'cf-source-2']));
testBed.univer.dispose();
});
it('does not reuse a target rule with different stop-if-true semantics', async () => {
const testBed = createCfUiTestBed();
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
const config: IConditionFormattingRule['rule'] = {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'A1',
style: { bg: { rgb: '#0f0' } },
};
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
cfId: 'cf-source',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
stopIfTrue: true,
rule: config,
},
});
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: 'sheet2',
rule: {
cfId: 'cf-target',
ranges: [{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 }],
stopIfTrue: false,
rule: config,
},
});
const hook = testBed.getClipboardHook()!;
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
null,
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
);
const addedRule = result.redos.find((mutation) => mutation.id === AddConditionalRuleMutation.id)?.params as { rule: IConditionFormattingRule } | undefined;
expect(addedRule?.rule.stopIfTrue).toBe(true);
testBed.univer.dispose();
});
it.each([
{ name: 'matching semantics', targetValue: 'source' },
{ name: 'different semantics', targetValue: 'target' },
])('isolates source and target rules with colliding IDs during cross-sheet cut: $name', async ({ targetValue }) => {
const testBed = createCfUiTestBed();
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
const createRule = (value: string, ranges: IConditionFormattingRule['ranges']): IConditionFormattingRule => ({
cfId: 'cf-collision',
ranges,
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value,
style: { bg: { rgb: value === 'source' ? '#0f0' : '#f00' } },
},
});
const sourceRule = createRule('source', [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }]);
const targetRule = createRule(targetValue, [
{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 },
{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 },
]);
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: sourceRule,
});
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: 'sheet2',
rule: targetRule,
});
const hook = testBed.getClipboardHook()!;
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
null,
{ copyType: COPY_TYPE.CUT, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
) as { redos: Array<{ id: string; params: object }>; undos: Array<{ id: string; params: object }> };
for (const mutation of result.redos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.ruleModel.getRule(testBed.unitId, testBed.subUnitId, 'cf-collision')).toBeFalsy();
expect(testBed.ruleModel.getRule(testBed.unitId, 'sheet2', 'cf-collision')?.ranges).toEqual([
{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 },
]);
const copiedRules = testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')?.filter((rule) => (
rule.cfId !== 'cf-collision' && rule.rule.type === CFRuleType.highlightCell && 'value' in rule.rule && rule.rule.value === 'source'
));
expect(copiedRules).toHaveLength(1);
expect(copiedRules?.[0].ranges).toEqual([{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }]);
for (const mutation of result.undos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.ruleModel.getRule(testBed.unitId, testBed.subUnitId, 'cf-collision')).toEqual(sourceRule);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')).toEqual([targetRule]);
testBed.univer.dispose();
});
it('preserves source rule priority when copying across worksheets', async () => {
const testBed = createCfUiTestBed();
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
for (const value of ['low', 'high']) {
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
cfId: `cf-${value}`,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value,
style: { bg: { rgb: value === 'high' ? '#0f0' : '#f00' } },
},
},
});
}
const hook = testBed.getClipboardHook()!;
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
null,
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
) as { redos: Array<{ id: string; params: object }>; undos: Array<{ id: string; params: object }> };
for (const mutation of result.redos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
const copiedValues = testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')?.map((rule) => (
rule.rule.type === CFRuleType.highlightCell && 'value' in rule.rule ? rule.rule.value : undefined
));
expect(copiedValues).toEqual(['high', 'low']);
for (const mutation of result.undos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')).toEqual([]);
testBed.univer.dispose();
});
});

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 { AddCfCommand } from '@univerjs/sheets-conditional-formatting';
import { describe, expect, it, vi } from 'vitest';
import { ConditionalFormattingPermissionController } from '../cf.permission.controller';
describe('ConditionalFormattingPermissionController', () => {
it('blocks adding conditional formatting when target ranges cannot edit cell style', () => {
let beforeCommandHandler: ((command: { id: string; params?: unknown }) => void) | undefined;
const permissionCheck = {
permissionCheckWithRanges: vi.fn(() => false),
blockExecuteWithoutPermission: vi.fn(),
};
const controller = new ConditionalFormattingPermissionController(
{ t: (key: string) => key } as never,
{
beforeCommandExecuted: vi.fn((handler) => {
beforeCommandHandler = handler;
return { dispose: vi.fn() };
}),
} as never,
permissionCheck as never
);
const ranges = [{ startRow: 1, endRow: 2, startColumn: 3, endColumn: 4 }];
beforeCommandHandler?.({
id: AddCfCommand.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
rule: { ranges },
},
});
expect(permissionCheck.permissionCheckWithRanges).toHaveBeenCalledWith(expect.objectContaining({
workbookTypes: expect.any(Array),
worksheetTypes: expect.any(Array),
rangeTypes: expect.any(Array),
}), ranges, 'unit-1', 'sheet-1');
expect(permissionCheck.blockExecuteWithoutPermission).toHaveBeenCalledWith('sheets-conditional-formatting-ui.permission.dialog.setStyleErr');
controller.dispose();
});
});

View File

@@ -0,0 +1,227 @@
/**
* 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 { INTERCEPTOR_POINT } from '@univerjs/sheets';
import { CFRuleType, ConditionalFormattingIcon, DataBar, dataBarUKey, DEFAULT_PADDING, DEFAULT_WIDTH, IconUKey } from '@univerjs/sheets-conditional-formatting';
import { Subject } from 'rxjs';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { SheetsCfRenderController } from '../cf.render.controller';
afterEach(() => {
vi.useRealTimers();
});
interface ITestConditionFormattingRule {
ranges: IRange[];
rule?: { type: CFRuleType };
}
interface ITestInterceptor {
handler: (cell: unknown, context: unknown, next: (cell: unknown) => unknown) => unknown;
}
interface ITestDirtyEvent {
cfId?: string;
rule?: ITestConditionFormattingRule;
subUnitId: string;
unitId: string;
}
function createController() {
let interceptor: ITestInterceptor;
const ruleChange$ = new Subject<ITestDirtyEvent>();
const markDirty$ = new Subject<ITestDirtyEvent>();
const reCalculate = vi.fn();
const makeDirty = vi.fn();
const resetRangeCache = vi.fn();
const dataBar = new DataBar();
const icon = new ConditionalFormattingIcon();
const setDataBarRenderRangeResolver = vi.spyOn(dataBar, 'setRenderRangeResolver');
const setIconRenderRangeResolver = vi.spyOn(icon, 'setRenderRangeResolver');
const getRulesByRanges = vi.fn(() => [] as Array<{ ranges: IRange[]; rule: { type: CFRuleType } }>);
const renderCreated$ = new Subject<unknown>();
const renderDisposed$ = new Subject<string>();
const rowColumnSegment = { startRow: 0, endRow: 10, startColumn: 0, endColumn: 10 };
const getRule = vi.fn<() => ITestConditionFormattingRule>(() => ({
ranges: [{ startRow: 2, endRow: 4, startColumn: 1, endColumn: 3 }],
}));
const getSubunitRules = vi.fn<() => ITestConditionFormattingRule[]>(() => []);
const getExtensionByKey = vi.fn((key: string) => key === dataBarUKey ? dataBar : key === IconUKey ? icon : undefined);
const render = {
unitId: 'unit-1',
type: 'UNIVER_SHEET',
with: vi.fn(() => ({ getCurrentSkeleton: vi.fn(() => ({ resetRangeCache, rowColumnSegment })), reCalculate })),
mainComponent: {
makeDirty,
getExtensionByKey,
},
};
const controller = new SheetsCfRenderController(
{
intercept: vi.fn((point, config: ITestInterceptor) => {
expect(point).toBe(INTERCEPTOR_POINT.CELL_CONTENT);
interceptor = config;
return { dispose: vi.fn() };
}),
} as never,
{
composeStyle: vi.fn(() => ({
style: { bg: { rgb: '#00ff00' } },
isShowValue: false,
dataBar: { color: '#00f' },
iconSet: { icon: 'arrow-up' },
})),
} as never,
{
getCurrentUnitOfType: vi.fn(() => ({
getUnitId: () => 'unit-1',
getActiveSheet: () => ({ getSheetId: () => 'sheet-1' }),
})),
} as never,
{
created$: renderCreated$,
disposed$: renderDisposed$,
getAllRenderersOfType: vi.fn(() => [render]),
getRenderUnitById: vi.fn(() => render),
} as never,
{ markDirty$ } as never,
{ $ruleChange: ruleChange$, getRule, getSubunitRules } as never,
{ getRulesByRanges } as never
);
return { controller, getExtensionByKey, getRule, getRulesByRanges, getSubunitRules, interceptor: () => interceptor, markDirty$, makeDirty, reCalculate, resetRangeCache, rowColumnSegment, ruleChange$, setDataBarRenderRangeResolver, setIconRenderRangeResolver };
}
describe('SheetsCfRenderController', () => {
it('falls back to source rules when the range index has not registered an icon set', () => {
const { controller, getExtensionByKey, getRulesByRanges, getSubunitRules, setDataBarRenderRangeResolver, setIconRenderRangeResolver } = createController();
const ranges = [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }];
const dataBarResolver = setDataBarRenderRangeResolver.mock.calls[0][0];
const iconResolver = setIconRenderRangeResolver.mock.calls[0][0];
expect(getExtensionByKey).toHaveBeenCalledTimes(2);
expect(getExtensionByKey).toHaveBeenCalledWith(dataBarUKey);
expect(getExtensionByKey).toHaveBeenCalledWith(IconUKey);
expect(dataBarResolver).not.toBeNull();
expect(iconResolver).not.toBeNull();
if (!dataBarResolver || !iconResolver) {
throw new Error('Expected conditional-formatting render range resolvers to be bound');
}
getRulesByRanges.mockReturnValue([{ ranges, rule: { type: CFRuleType.dataBar } }]);
expect(dataBarResolver('unit-1', 'sheet-1', ranges)).toEqual([expect.objectContaining(ranges[0])]);
getRulesByRanges.mockReturnValue([]);
getSubunitRules.mockReturnValue([{ ranges, rule: { type: CFRuleType.iconSet } }]);
expect(iconResolver('unit-1', 'sheet-1', ranges)).toEqual([expect.objectContaining(ranges[0])]);
controller.dispose();
expect(setDataBarRenderRangeResolver).toHaveBeenLastCalledWith(null);
expect(setIconRenderRangeResolver).toHaveBeenLastCalledWith(null);
});
it('composes conditional-formatting style, data bar and icon set into rendered cell data', () => {
const { controller, interceptor } = createController();
const rawCell = { v: 10, s: 'style-1' };
const result = interceptor().handler(rawCell, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 1,
col: 2,
rawData: rawCell,
workbook: {
getStyles: vi.fn(() => ({
get: vi.fn(() => ({ fs: 12 })),
})),
},
}, (cell: unknown) => cell) as {
dataBar: unknown;
fontRenderExtension: unknown;
iconSet: unknown;
s: unknown;
};
expect(result).not.toBe(rawCell);
expect(result.s).toEqual({ fs: 12, bg: { rgb: '#00ff00' } });
expect(result.fontRenderExtension).toEqual({
isSkip: true,
leftOffset: DEFAULT_PADDING + DEFAULT_WIDTH,
});
expect(result.dataBar).toEqual({ color: '#00f' });
expect(result.iconSet).toEqual({ icon: 'arrow-up' });
controller.dispose();
});
it('marks the active sheet skeleton dirty when conditional formatting changes affect it', async () => {
vi.useFakeTimers();
const { controller, ruleChange$, markDirty$, reCalculate, makeDirty, resetRangeCache } = createController();
const ranges = [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }];
ruleChange$.next({ unitId: 'unit-1', subUnitId: 'sheet-1', rule: { ranges } });
markDirty$.next({ unitId: 'other-unit', subUnitId: 'sheet-1' });
await vi.advanceTimersByTimeAsync(20);
expect(resetRangeCache).toHaveBeenCalledWith(ranges);
expect(reCalculate).toHaveBeenCalled();
expect(makeDirty).toHaveBeenCalled();
controller.dispose();
});
it('resets changed ranges found from conditional-formatting dirty cfIds', async () => {
vi.useFakeTimers();
const { controller, getRule, markDirty$, resetRangeCache } = createController();
markDirty$.next({ unitId: 'unit-1', subUnitId: 'sheet-1', cfId: 'cf-1' });
await vi.advanceTimersByTimeAsync(20);
expect(getRule).toHaveBeenCalledWith('unit-1', 'sheet-1', 'cf-1');
expect(resetRangeCache).toHaveBeenCalledWith([{ startRow: 2, endRow: 4, startColumn: 1, endColumn: 3 }]);
controller.dispose();
});
it('resets existing active-sheet rule ranges when the controller starts after snapshot rules are loaded', async () => {
const { controller, getSubunitRules, resetRangeCache } = createController();
const ranges = [{ startRow: 5, endRow: 6, startColumn: 2, endColumn: 4 }];
getSubunitRules.mockReturnValue([{ ranges }]);
await Promise.resolve();
expect(getSubunitRules).toHaveBeenCalledWith('unit-1', 'sheet-1');
expect(resetRangeCache).toHaveBeenCalledWith(ranges);
controller.dispose();
});
it('clips large conditional-formatting dirty ranges to the current rendered range', async () => {
vi.useFakeTimers();
const { controller, getRule, markDirty$, resetRangeCache, rowColumnSegment } = createController();
getRule.mockReturnValue({
ranges: [{ startRow: 0, endRow: 20000, startColumn: 0, endColumn: 48 }],
});
markDirty$.next({ unitId: 'unit-1', subUnitId: 'sheet-1', cfId: 'cf-1' });
await vi.advanceTimersByTimeAsync(20);
expect(resetRangeCache).toHaveBeenCalledWith([rowColumnSegment]);
controller.dispose();
});
});

View File

@@ -0,0 +1,262 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDisposable, IMutationInfo, IRange } from '@univerjs/core';
import type { IColorScale, IConditionFormattingRule, IDataBar, IFormulaHighlightCell, IIconSet } from '@univerjs/sheets-conditional-formatting';
import { Disposable, Inject, Injector, toDisposable, Tools } from '@univerjs/core';
import {
AddConditionalRuleMutation,
AddConditionalRuleMutationUndoFactory,
CFRuleType,
CFSubRuleType,
CFValueType,
ConditionalFormattingRuleModel,
createCfId,
DeleteConditionalRuleMutation,
SetConditionalRuleMutation,
setConditionalRuleMutationUndoFactory,
} from '@univerjs/sheets-conditional-formatting';
import { FormulaRefRangeService } from '@univerjs/sheets-formula';
export class ConditionalFormattingFormulaRefRangeController extends Disposable {
private _disposableMap: Map<string, IDisposable> = new Map();
constructor(
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
@Inject(FormulaRefRangeService) private _formulaRefRangeService: FormulaRefRangeService,
@Inject(Injector) private _injector: Injector
) {
super();
this._initRefRange();
}
private _getIdWithUnitId(unitID: string, subUnitId: string, cfId: string) {
return `${unitID}_${subUnitId}_${cfId}`;
}
private _getRuleFormulas(rule: IConditionFormattingRule): string[] {
const formulas: string[] = [];
const ruleConfig = rule.rule;
switch (ruleConfig.type) {
case CFRuleType.highlightCell:
if (ruleConfig.subType === CFSubRuleType.formula) {
formulas.push((ruleConfig as IFormulaHighlightCell).value);
}
break;
case CFRuleType.dataBar: {
const dataBar = ruleConfig as IDataBar;
if (dataBar.config.min.type === CFValueType.formula) {
formulas.push(dataBar.config.min.value as string);
}
if (dataBar.config.max.type === CFValueType.formula) {
formulas.push(dataBar.config.max.value as string);
}
break;
}
case CFRuleType.colorScale: {
const colorScale = ruleConfig as IColorScale;
colorScale.config.forEach((item) => {
if (item.value.type === CFValueType.formula) {
formulas.push(item.value.value as string);
}
});
break;
}
case CFRuleType.iconSet: {
const iconSet = ruleConfig as IIconSet;
iconSet.config.forEach((item) => {
if (item.value.type === CFValueType.formula) {
formulas.push(item.value.value as string);
}
});
break;
}
}
return formulas;
}
private _updateRuleFormulas(rule: IConditionFormattingRule, formulas: string[]): IConditionFormattingRule {
const newRule = Tools.deepClone(rule);
const ruleConfig = newRule.rule;
let formulaIndex = 0;
switch (ruleConfig.type) {
case CFRuleType.highlightCell:
if (ruleConfig.subType === CFSubRuleType.formula) {
(ruleConfig as IFormulaHighlightCell).value = formulas[formulaIndex++];
}
break;
case CFRuleType.dataBar: {
const dataBar = ruleConfig as IDataBar;
if (dataBar.config.min.type === CFValueType.formula) {
dataBar.config.min.value = formulas[formulaIndex++];
}
if (dataBar.config.max.type === CFValueType.formula) {
dataBar.config.max.value = formulas[formulaIndex++];
}
break;
}
case CFRuleType.colorScale: {
const colorScale = ruleConfig as IColorScale;
colorScale.config.forEach((item) => {
if (item.value.type === CFValueType.formula) {
item.value.value = formulas[formulaIndex++];
}
});
break;
}
case CFRuleType.iconSet: {
const iconSet = ruleConfig as IIconSet;
iconSet.config.forEach((item) => {
if (item.value.type === CFValueType.formula) {
item.value.value = formulas[formulaIndex++];
}
});
break;
}
}
return newRule;
}
register(unitId: string, subUnitId: string, rule: IConditionFormattingRule) {
const oldRanges = rule.ranges;
const oldFormulas = this._getRuleFormulas(rule);
const disposable = this._formulaRefRangeService.registerRangeFormula(unitId, subUnitId, oldRanges, oldFormulas, (res: { formulas: string[]; ranges: IRange[] }[]) => {
if (res.length === 0) {
return {
undos: [{
id: AddConditionalRuleMutation.id,
params: {
unitId,
subUnitId,
rule,
},
}],
redos: [{
id: DeleteConditionalRuleMutation.id,
params: {
unitId,
subUnitId,
cfId: rule.cfId,
},
}],
};
}
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
const first = res[0];
const firstRule = this._updateRuleFormulas(rule, first.formulas);
firstRule.ranges = first.ranges;
redos.push({
id: SetConditionalRuleMutation.id,
params: {
unitId,
subUnitId,
cfId: rule.cfId,
rule: firstRule,
},
});
undos.push(...setConditionalRuleMutationUndoFactory(this._injector, {
unitId,
subUnitId,
cfId: rule.cfId,
rule: firstRule,
}));
for (let i = 1; i < res.length; i++) {
const item = res[i];
const newCfId = createCfId();
const newRule = this._updateRuleFormulas(rule, item.formulas);
newRule.cfId = newCfId;
newRule.ranges = item.ranges;
redos.push({
id: AddConditionalRuleMutation.id,
params: {
unitId,
subUnitId,
rule: newRule,
},
});
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, {
unitId,
subUnitId,
rule: newRule,
}));
}
return {
undos,
redos,
};
});
const id = this._getIdWithUnitId(unitId, subUnitId, rule.cfId);
this._disposableMap.set(id, disposable);
}
private _initRefRange() {
const allRules = this._conditionalFormattingRuleModel.getAll();
for (const [unitId, subUnitMap] of allRules) {
for (const [subUnitId, rules] of subUnitMap) {
for (const rule of rules) {
this.register(unitId, subUnitId, rule);
}
}
}
this.disposeWithMe(
this._conditionalFormattingRuleModel.$ruleChange.subscribe((option) => {
const { unitId, subUnitId, rule } = option;
switch (option.type) {
case 'add': {
this.register(unitId, subUnitId, rule);
break;
}
case 'delete': {
const id = this._getIdWithUnitId(unitId, subUnitId, rule.cfId);
const disposable = this._disposableMap.get(id);
if (disposable) {
disposable.dispose();
this._disposableMap.delete(id);
}
break;
}
case 'set': {
const id = this._getIdWithUnitId(unitId, subUnitId, rule.cfId);
const disposable = this._disposableMap.get(id);
if (disposable) {
disposable.dispose();
this._disposableMap.delete(id);
}
this.register(unitId, subUnitId, rule);
break;
}
}
})
);
this.disposeWithMe(toDisposable(() => {
this._disposableMap.forEach((item) => {
item.dispose();
});
this._disposableMap.clear();
}));
}
}

View File

@@ -0,0 +1,168 @@
/**
* 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 { IMutationInfo, IRange, Workbook } from '@univerjs/core';
import type { IDiscreteRange, ISheetAutoFillHook } from '@univerjs/sheets';
import type { IDeleteConditionalRuleMutationParams, ISetConditionalRuleMutationParams } from '@univerjs/sheets-conditional-formatting';
import { Disposable, getIntersectRange, Inject, Injector, IUniverInstanceService, Rectangle, UniverInstanceType } from '@univerjs/core';
import { AUTO_FILL_APPLY_TYPE, AutoFillTools, IAutoFillService } from '@univerjs/sheets';
import {
ConditionalFormattingRangeTransformService,
ConditionalFormattingRuleModel,
DeleteConditionalRuleMutation,
DeleteConditionalRuleMutationUndoFactory,
SetConditionalRuleMutation,
setConditionalRuleMutationUndoFactory,
SHEET_CONDITIONAL_FORMATTING_PLUGIN,
} from '@univerjs/sheets-conditional-formatting';
import { virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
interface IRangeDelta {
add: IRange[];
remove: IRange[];
}
export class ConditionalFormattingAutoFillController extends Disposable {
constructor(
@Inject(Injector) private _injector: Injector,
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(IAutoFillService) private _autoFillService: IAutoFillService,
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
) {
super();
this._initAutoFill();
}
// eslint-disable-next-line max-lines-per-function
private _initAutoFill() {
const noopReturnFunc = () => ({ redos: [], undos: [] });
const generalApplyFunc = (sourceRange: IDiscreteRange, targetRange: IDiscreteRange) => {
const unitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getUnitId();
const subUnitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getActiveSheet()?.getSheetId();
const rangeMap: Map<string, IRange[]> = new Map();
const rangeDeltaMap: Map<string, IRangeDelta> = new Map();
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
if (!unitId || !subUnitId) {
return noopReturnFunc();
}
const virtualization = virtualizeDiscreteRanges([sourceRange, targetRange]);
const [vSourceRange, vTargetRange] = virtualization.ranges;
const repeats = AutoFillTools.getAutoFillRepeatRange(vSourceRange, vTargetRange);
const targetRanges = repeats.flatMap((repeat) => virtualization.mapRange(Rectangle.getPositionRange(repeat.relativeRange, {
startRow: repeat.repeatStartCell.row,
endRow: repeat.repeatStartCell.row,
startColumn: repeat.repeatStartCell.col,
endColumn: repeat.repeatStartCell.col,
})));
const getRangeDelta = (cfId: string) => {
let rangeDelta = rangeDeltaMap.get(cfId);
if (!rangeDelta) {
rangeDelta = { add: [], remove: [] };
rangeDeltaMap.set(cfId, rangeDelta);
}
return rangeDelta;
};
const rules = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) ?? [];
rules.forEach((rule) => {
if (Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
rangeMap.set(rule.cfId, rule.ranges);
getRangeDelta(rule.cfId).remove.push(...targetRanges);
}
const sourceRanges = rule.ranges.flatMap((range) => {
const projected = virtualization.projectRange(range);
const intersected = projected && getIntersectRange(projected, vSourceRange);
return intersected ? [Rectangle.getRelativeRange(intersected, vSourceRange)] : [];
});
const additions = repeats.flatMap((repeat) => sourceRanges.flatMap((range) => {
const copiedRange = getIntersectRange(range, repeat.relativeRange);
return copiedRange
? virtualization.mapRange(Rectangle.getPositionRange(copiedRange, {
startRow: repeat.repeatStartCell.row,
endRow: repeat.repeatStartCell.row,
startColumn: repeat.repeatStartCell.col,
endColumn: repeat.repeatStartCell.col,
}))
: [];
}));
if (additions.length) {
rangeMap.set(rule.cfId, rule.ranges);
getRangeDelta(rule.cfId).add.push(...additions);
}
});
rangeDeltaMap.forEach((rangeDelta, cfId) => {
const ranges = rangeMap.get(cfId);
if (!ranges) {
return;
}
rangeMap.set(cfId, this._conditionalFormattingRangeTransformService.applyRangeDelta(
ranges,
rangeDelta.remove,
rangeDelta.add
));
});
rangeMap.forEach((ranges, cfId) => {
const rule = this._conditionalFormattingRuleModel.getRule(unitId, subUnitId, cfId);
if (!rule) {
return;
}
if (ranges.length) {
const params: ISetConditionalRuleMutationParams = {
unitId,
subUnitId,
rule: { ...rule, ranges },
};
redos.push({ id: SetConditionalRuleMutation.id, params });
undos.push(...setConditionalRuleMutationUndoFactory(this._injector, params));
} else {
const params: IDeleteConditionalRuleMutationParams = {
unitId,
subUnitId,
cfId: rule.cfId,
};
redos.push({ id: DeleteConditionalRuleMutation.id, params });
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, params));
}
});
return {
undos,
redos,
};
};
const hook: ISheetAutoFillHook = {
id: SHEET_CONDITIONAL_FORMATTING_PLUGIN,
onFillData: (location, direction, applyType) => {
if (applyType === AUTO_FILL_APPLY_TYPE.COPY || applyType === AUTO_FILL_APPLY_TYPE.ONLY_FORMAT || applyType === AUTO_FILL_APPLY_TYPE.SERIES) {
const { source, target } = location;
return generalApplyFunc(source, target);
}
return noopReturnFunc();
},
};
this.disposeWithMe(this._autoFillService.addHook(hook));
}
}

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 { IMutationInfo, IRange, Workbook } from '@univerjs/core';
import type {
IConditionalFormattingRuleConfig,
IConditionFormattingRule,
IDeleteConditionalRuleMutationParams,
ISetConditionalRuleMutationParams,
} from '@univerjs/sheets-conditional-formatting';
import { Disposable, Inject, Injector, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { ClearSelectionAllCommand, ClearSelectionFormatCommand, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
import {
ConditionalFormattingRangeIndexModel,
ConditionalFormattingRangeTransformService,
DeleteConditionalRuleMutation,
DeleteConditionalRuleMutationUndoFactory,
SetConditionalRuleMutation,
setConditionalRuleMutationUndoFactory,
} from '@univerjs/sheets-conditional-formatting';
export class ConditionalFormattingClearController extends Disposable {
constructor(
@Inject(Injector) private _injector: Injector,
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
@Inject(SheetsSelectionsService) private _selectionManagerService: SheetsSelectionsService,
@Inject(ConditionalFormattingRangeIndexModel) private _conditionalFormattingRangeIndexModel: ConditionalFormattingRangeIndexModel
) {
super();
this._init();
}
private _init() {
this.disposeWithMe(this._sheetInterceptorService.interceptCommand({
getMutations: (commandInfo) => {
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
const defaultV = { redos, undos };
if ([ClearSelectionFormatCommand.id, ClearSelectionAllCommand.id].includes(commandInfo.id)) {
const ranges = this._selectionManagerService.getCurrentSelections()?.map((s) => s.range);
if (!ranges) {
return defaultV;
}
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet();
if (!worksheet) {
return defaultV;
}
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const allRules = this._conditionalFormattingRangeIndexModel.getRulesByRanges(unitId, subUnitId, ranges);
if (!allRules || !allRules.length) {
return defaultV;
}
const { redos: interceptRedos, undos: interceptUndos } = generateClearCfMutations(this._injector, allRules, ranges, unitId, subUnitId);
redos.push(...interceptRedos);
undos.push(...interceptUndos);
}
return defaultV;
},
}));
this.disposeWithMe(this._sheetInterceptorService.interceptRanges({
getMutations: ({ unitId, subUnitId, ranges }) => {
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
const emptyInterceptorArr = { redos, undos };
if (!ranges || !ranges.length) {
return emptyInterceptorArr;
}
const allRules = this._conditionalFormattingRangeIndexModel.getRulesByRanges(unitId, subUnitId, ranges);
if (!allRules || !allRules.length) {
return emptyInterceptorArr;
}
const { redos: interceptRedos, undos: interceptUndos } = generateClearCfMutations(this._injector, allRules, ranges, unitId, subUnitId);
redos.push(...interceptRedos);
undos.push(...interceptUndos);
return emptyInterceptorArr;
},
}));
}
}
export function generateClearCfMutations(injector: Injector, allRules: IConditionFormattingRule<IConditionalFormattingRuleConfig>[], ranges: IRange[], unitId: string, subUnitId: string) {
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
const rangeTransformService = injector.get(ConditionalFormattingRangeTransformService);
allRules.forEach((rule) => {
const mergeRanges = rangeTransformService.subtractRanges(rule.ranges, ranges);
if (mergeRanges.length) {
const redo: IMutationInfo<ISetConditionalRuleMutationParams> = {
id: SetConditionalRuleMutation.id,
params: {
unitId,
subUnitId,
rule: { ...rule, ranges: mergeRanges },
},
};
const undo = setConditionalRuleMutationUndoFactory(injector, redo.params);
redos.push(redo);
undos.push(...undo);
} else {
const redo: IMutationInfo<IDeleteConditionalRuleMutationParams> = {
id: DeleteConditionalRuleMutation.id,
params: {
unitId,
subUnitId,
cfId: rule.cfId,
},
};
const undo = DeleteConditionalRuleMutationUndoFactory(injector, redo.params);
redos.push(redo);
undos.push(...undo);
}
});
return { redos, undos };
}

View File

@@ -0,0 +1,294 @@
/**
* 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, Nullable } from '@univerjs/core';
import type {
IAddConditionalRuleMutationParams,
IConditionFormattingRule,
IDeleteConditionalRuleMutationParams,
ISetConditionalRuleMutationParams,
} from '@univerjs/sheets-conditional-formatting';
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 {
AddConditionalRuleMutation,
AddConditionalRuleMutationUndoFactory,
ConditionalFormattingRangeTransformService,
ConditionalFormattingRuleModel,
DeleteConditionalRuleMutation,
DeleteConditionalRuleMutationUndoFactory,
SetConditionalRuleMutation,
setConditionalRuleMutationUndoFactory,
SHEET_CONDITIONAL_FORMATTING_PLUGIN,
} from '@univerjs/sheets-conditional-formatting';
import { COPY_TYPE, getRepeatRange, ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE, virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
interface ICopyInfoType {
rules: Map<string, IRange[]>;
info: {
unitId: string;
subUnitId: string;
cfMap: Record<string, Pick<IConditionFormattingRule, 'rule' | 'stopIfTrue'>>;
};
}
const specialPastes: IPasteHookValueType[] = [
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE,
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_BESIDES_BORDER,
];
export class ConditionalFormattingCopyPasteController extends Disposable {
private _copyInfo: Nullable<ICopyInfoType>;
constructor(
@Inject(ISheetClipboardService) private _sheetClipboardService: ISheetClipboardService,
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
@Inject(Injector) private _injector: Injector,
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
) {
super();
this._initClipboardHook();
}
private _initClipboardHook() {
this.disposeWithMe(
this._sheetClipboardService.addClipboardHook({
id: SHEET_CONDITIONAL_FORMATTING_PLUGIN,
onBeforeCopy: (unitId, subUnitId, range) => this._collectConditionalRule(unitId, subUnitId, range),
onPasteCells: (pasteFrom, pasteTo, _data, payload) => {
// If pasteFrom or copyInfo is null, it means the copy from outside of Univer, so not need to handle the conditional formatting, just return empty mutations.
if (!pasteFrom || !this._copyInfo || !specialPastes.includes(payload.pasteType)) {
return { redos: [], undos: [] };
}
return this._generateConditionalFormattingMutations(pasteFrom, pasteTo, payload);
},
})
);
}
private _collectConditionalRule(unitId: string, subUnitId: string, range: IRange) {
const rules = new Map<string, IRange[]>();
const cfMap: Record<string, Pick<IConditionFormattingRule, 'rule' | 'stopIfTrue'>> = {};
this._copyInfo = {
rules,
info: {
unitId,
subUnitId,
cfMap,
},
};
const discreteRange = this._injector.invoke((accessor) => {
return rangeToDiscreteRange(range, accessor, unitId, subUnitId);
});
if (!discreteRange) {
return;
}
const { projectRange } = virtualizeDiscreteRanges([discreteRange]);
this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId)?.forEach((rule) => {
const projectedRanges = rule.ranges.flatMap((ruleRange) => {
const projected = projectRange(ruleRange);
return projected ? [projected] : [];
});
if (projectedRanges.length) {
rules.set(rule.cfId, projectedRanges.length > 1 ? Rectangle.mergeRanges(projectedRanges) : projectedRanges);
cfMap[rule.cfId] = { rule: rule.rule, stopIfTrue: rule.stopIfTrue };
}
});
}
// eslint-disable-next-line max-lines-per-function
private _generateConditionalFormattingMutations(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 conditional formatting, because the move range had handle the ref range of conditional formatting, to see cf-formula-ref-range.controller.ts.
if (copyType === COPY_TYPE.CUT && pastedUnitId === copyUnitId && pastedSubUnitId === copySubUnitId) {
this._copyInfo = null;
return { redos: [], undos: [] };
}
const sourceVirtualization = virtualizeDiscreteRanges([copyRange]);
const sourceVirtualRange = sourceVirtualization.ranges[0];
const targetVirtualization = virtualizeDiscreteRanges([pastedRange]);
const targetVirtualRange = targetVirtualization.ranges[0];
const repeatRange = getRepeatRange(sourceVirtualRange, targetVirtualRange, true);
const targetRanges = targetVirtualization.mapRange(targetVirtualRange);
const isSameSheet = pastedUnitId === copyUnitId && pastedSubUnitId === copySubUnitId;
const effectedConditionalFormattingRuleRanges = new Map<string, {
cfId: string;
unitId: string;
subUnitId: string;
ranges: IRange[];
add: IRange[];
remove: IRange[];
}>();
const getEffectKey = (unitId: string, subUnitId: string, cfId: string) => JSON.stringify([unitId, subUnitId, cfId]);
// 1. delete the conditional formatting rules in the pasted range.
this._conditionalFormattingRuleModel.getSubunitRules(pastedUnitId, pastedSubUnitId)?.forEach((rule) => {
if (!Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
return;
}
effectedConditionalFormattingRuleRanges.set(getEffectKey(pastedUnitId, pastedSubUnitId, rule.cfId), {
cfId: rule.cfId,
unitId: pastedUnitId,
subUnitId: pastedSubUnitId,
ranges: rule.ranges,
add: [],
remove: targetRanges,
});
});
// 2. if it is cut from another worksheet, need to delete the conditional formatting rules in the copy range.
if (copyType === COPY_TYPE.CUT && (pastedUnitId !== copyUnitId || pastedSubUnitId !== copySubUnitId)) {
const sourceRanges = sourceVirtualization.mapRange(sourceVirtualRange);
copyInfo.rules.forEach((_ranges, cfId) => {
const rule = this._conditionalFormattingRuleModel.getRule(copyUnitId, copySubUnitId, cfId);
if (!rule) {
return;
}
effectedConditionalFormattingRuleRanges.set(getEffectKey(copyUnitId, copySubUnitId, cfId), {
cfId,
unitId: copyUnitId,
subUnitId: copySubUnitId,
ranges: rule.ranges,
add: [],
remove: sourceRanges,
});
});
}
const { rules, info } = copyInfo;
const waitAddRule = new Map<string, IConditionFormattingRule>();
const cacheCfIdMap: Record<string, IConditionFormattingRule> = {};
// 3. generate the new conditional formatting rules based on the copy range's conditional formatting rules and the paste position.
const getCurrentSheetCfRule = (copyRangeCfId: string) => {
const oldRule = info?.cfMap[copyRangeCfId];
if (isSameSheet) {
const rule = this._conditionalFormattingRuleModel.getRule(pastedUnitId, pastedSubUnitId, copyRangeCfId);
if (rule) {
cacheCfIdMap[copyRangeCfId] = rule;
return rule;
}
}
const rule: IConditionFormattingRule = {
rule: oldRule.rule,
cfId: this._conditionalFormattingRuleModel.createCfId(pastedUnitId, pastedSubUnitId),
ranges: [],
stopIfTrue: oldRule.stopIfTrue,
};
cacheCfIdMap[copyRangeCfId] = rule;
waitAddRule.set(rule.cfId, rule);
return rule;
};
const sourceRuleEntries = Array.from(rules.entries());
if (!isSameSheet) {
// AddRule prepends, so emit lower-priority cross-sheet clones first.
sourceRuleEntries.reverse();
}
sourceRuleEntries.forEach(([cfId, sourceRanges]) => {
const rule = cacheCfIdMap[cfId] || getCurrentSheetCfRule(cfId);
const effectKey = getEffectKey(pastedUnitId, pastedSubUnitId, rule.cfId);
if (!effectedConditionalFormattingRuleRanges.has(effectKey)) {
effectedConditionalFormattingRuleRanges.set(effectKey, {
cfId: rule.cfId,
unitId: pastedUnitId,
subUnitId: pastedSubUnitId,
ranges: rule.ranges,
add: [],
remove: [],
});
}
const current = effectedConditionalFormattingRuleRanges.get(effectKey)!;
current.add.push(...repeatRange.flatMap((item) => sourceRanges.flatMap((sourceRange) => (
targetVirtualization.mapRange(Rectangle.getPositionRange(sourceRange, item.startRange))
))));
});
const redos = [];
const undos = [];
for (const effect of effectedConditionalFormattingRuleRanges.values()) {
const { cfId, unitId, subUnitId, ranges: sourceRanges, add, remove } = effect;
const ranges = this._conditionalFormattingRangeTransformService.applyRangeDelta(sourceRanges, remove, add);
if (!ranges.length) {
const deleteParams: IDeleteConditionalRuleMutationParams = {
unitId,
subUnitId,
cfId,
};
redos.push({ id: DeleteConditionalRuleMutation.id, params: deleteParams });
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, deleteParams));
continue;
}
const waitAdd = waitAddRule.get(cfId);
if (waitAdd) {
const addParams: IAddConditionalRuleMutationParams = {
unitId: pastedUnitId,
subUnitId: pastedSubUnitId,
rule: { ...waitAdd, ranges },
};
redos.push({ id: AddConditionalRuleMutation.id, params: addParams });
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, addParams));
} else {
const rule = this._conditionalFormattingRuleModel.getRule(unitId, subUnitId, cfId);
if (!rule) {
continue;
}
const setParams: ISetConditionalRuleMutationParams = {
unitId,
subUnitId,
rule: { ...rule, ranges },
};
redos.push({ id: SetConditionalRuleMutation.id, params: setParams });
undos.push(...setConditionalRuleMutationUndoFactory(this._injector, setParams));
}
}
return {
redos,
undos,
};
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Disposable,
Inject,
toDisposable,
} from '@univerjs/core';
import { AFTER_CELL_EDIT, SheetInterceptorService } from '@univerjs/sheets';
import { ConditionalFormattingService } from '@univerjs/sheets-conditional-formatting';
export class ConditionalFormattingEditorController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(ConditionalFormattingService) private readonly _conditionalFormattingService: ConditionalFormattingService
) {
super();
this._initInterceptorEditorEnd();
}
/**
* Process the values after edit
* @private
* @memberof NumfmtService
*/
private _initInterceptorEditorEnd() {
this.disposeWithMe(
toDisposable(
this._sheetInterceptorService.writeCellInterceptor.intercept(
AFTER_CELL_EDIT,
{
handler: (value, context, next) => {
if (!value) {
next(value);
}
const result = this._conditionalFormattingService.composeStyle(context.unitId, context.subUnitId, context.row, context.col);
const cfStyle = result?.style ?? {};
const keys = Object.keys(cfStyle);
if (value?.p) {
value.p.body?.textRuns?.forEach((item) => {
if (item.ts) {
keys.forEach((key) => {
delete item.ts?.[key as keyof typeof item.ts];
});
}
});
return next(value);
} else {
const s = { ...(typeof value?.s === 'string' ? context.workbook.getStyles().get(value.s) : value?.s) || {} };
keys.forEach((key) => {
delete s[key as keyof typeof s];
});
const cellData = { ...value, s: { ...s } };
return next(cellData);
}
},
}
)
)
);
}
}

View File

@@ -0,0 +1,66 @@
/**
* 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 { ReactNode } from 'react';
import { Disposable, Inject, LocaleService } from '@univerjs/core';
export class ConditionalFormattingI18nController extends Disposable {
constructor(@Inject(LocaleService) private _localeService: LocaleService) {
super();
this._initLocal();
}
private _initLocal = () => {
};
public tWithReactNode(key: string, ...args: (ReactNode | string)[]) {
const locale = this._localeService.getLocales();
const keys = key.split('.');
const resolvedValue = locale && this._localeService.resolveKeyPath(locale, keys);
if (typeof resolvedValue === 'string') {
const result: (string | ReactNode)[] = [];
this._findReplaceIndex(resolvedValue).forEach((item, index, list) => {
const preItem = list[index - 1] || { startIndex: 0, endIndex: -1 };
if (preItem.endIndex + 1 < item.startIndex) {
const text = resolvedValue.slice(preItem.endIndex + 1, item.startIndex);
text && result.push(text);
}
args[item.key] && result.push(args[item.key]);
if (index === list.length - 1) {
const text = resolvedValue.slice(item.endIndex + 1);
text && result.push(text);
}
});
return result;
}
return [];
}
private _findReplaceIndex = (text: string) => {
const reg = /\{([^}]+)?\}/g;
const result: { startIndex: number; key: number; endIndex: number }[] = [];
let currentValue = reg.exec(text);
while (currentValue) {
result.push({
startIndex: currentValue.index,
key: Number(currentValue[1]),
endIndex: currentValue.index + currentValue[0].length - 1,
});
currentValue = reg.exec(text);
}
return result;
};
}

View File

@@ -0,0 +1,288 @@
/**
* 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 { IMutationInfo, IRange, Nullable, Workbook } from '@univerjs/core';
import type {
IAddConditionalRuleMutationParams,
IConditionFormattingRule,
IDeleteConditionalRuleMutationParams,
ISetConditionalRuleMutationParams,
} from '@univerjs/sheets-conditional-formatting';
import type { IFormatPainterHook } from '@univerjs/sheets-ui';
import { Disposable, getIntersectRange, Inject, Injector, IUniverInstanceService, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
import { SheetsSelectionsService } from '@univerjs/sheets';
import {
AddConditionalRuleMutation,
AddConditionalRuleMutationUndoFactory,
ConditionalFormattingRangeTransformService,
ConditionalFormattingRuleModel,
DeleteConditionalRuleMutation,
DeleteConditionalRuleMutationUndoFactory,
SetConditionalRuleMutation,
setConditionalRuleMutationUndoFactory,
SHEET_CONDITIONAL_FORMATTING_PLUGIN,
} from '@univerjs/sheets-conditional-formatting';
import { FormatPainterStatus, IFormatPainterService } from '@univerjs/sheets-ui';
interface IRangeDelta {
add: IRange[];
remove: IRange[];
}
const repeatByRange = (sourceRange: IRange, targetRange: IRange) => {
const getRowLength = (range: IRange) => range.endRow - range.startRow + 1;
const getColLength = (range: IRange) => range.endColumn - range.startColumn + 1;
const rowMod = getRowLength(targetRange) % getRowLength(sourceRange);
const colMod = getColLength(targetRange) % getColLength(sourceRange);
const repeatRow = Math.floor(getRowLength(targetRange) / getRowLength(sourceRange));
const repeatCol = Math.floor(getColLength(targetRange) / getColLength(sourceRange));
const repeatList: Array<{ startRange: IRange; repeatRelativeRange: IRange }> = [];
const repeatRelativeRange: IRange = {
startRow: 0,
endRow: getRowLength(sourceRange) - 1,
startColumn: 0,
endColumn: getColLength(sourceRange) - 1,
};
// If the target area is a single cell, copy the entire source area.
if (getRowLength(targetRange) === 1 && getColLength(targetRange) === 1) {
const startRange: IRange = {
startRow: targetRange.startRow,
endRow: targetRange.startRow,
startColumn: targetRange.startColumn,
endColumn: targetRange.startColumn,
};
repeatList.push({ repeatRelativeRange, startRange });
return repeatList;
}
for (let countRow = 0; countRow < (repeatRow + (rowMod ? 0.1 : 0)); countRow++) {
for (let countCol = 0; countCol < (repeatCol + (colMod ? 0.1 : 0)); countCol++) {
const row = getRowLength(sourceRange) * (countRow);
const col = getColLength(sourceRange) * (countCol);
const startRange: IRange = {
startRow: row + targetRange.startRow,
endRow: row + targetRange.startRow,
startColumn: col + targetRange.startColumn,
endColumn: col + targetRange.startColumn,
};
let _repeatRelativeRange = repeatRelativeRange;
if (countRow === repeatRow && rowMod) {
_repeatRelativeRange = { ..._repeatRelativeRange };
_repeatRelativeRange.endRow = _repeatRelativeRange.endRow - (getRowLength(sourceRange) - rowMod);
}
if (countCol === repeatCol && colMod) {
_repeatRelativeRange = { ..._repeatRelativeRange };
_repeatRelativeRange.endColumn = _repeatRelativeRange.endColumn - (getColLength(sourceRange) - colMod);
}
repeatList.push({ repeatRelativeRange: _repeatRelativeRange, startRange });
}
}
return repeatList;
};
export class ConditionalFormattingPainterController extends Disposable {
private _painterConfig: Nullable<{ unitId: string; subUnitId: string; range: IRange }> = null;
constructor(
@Inject(Injector) private _injector: Injector,
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(IFormatPainterService) private _formatPainterService: IFormatPainterService,
@Inject(SheetsSelectionsService) private _sheetsSelectionsService: SheetsSelectionsService,
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
) {
super();
this._initFormattingPainter();
}
// eslint-disable-next-line max-lines-per-function
private _initFormattingPainter() {
const noopReturnFunc = () => ({ redos: [], undos: [] });
// eslint-disable-next-line max-lines-per-function
const generalApplyFunc = (targetUnitId: string, targetSubUnitId: string, targetRange: IRange) => {
const { range: sourceRange, unitId: sourceUnitId, subUnitId: sourceSubUnitId } = this._painterConfig!;
const isSkipSheet = targetUnitId !== sourceUnitId || sourceSubUnitId !== targetSubUnitId;
const rangeMap: Map<string, IRange[]> = new Map();
const rangeDeltaMap: Map<string, IRangeDelta> = new Map();
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
if (!targetUnitId || !targetSubUnitId || !sourceUnitId || !sourceSubUnitId) {
return noopReturnFunc();
}
const repeats = repeatByRange(sourceRange, targetRange);
const targetRanges = repeats.map((repeat) => Rectangle.getPositionRange(repeat.repeatRelativeRange, repeat.startRange));
const getRangeDelta = (cfId: string) => {
let rangeDelta = rangeDeltaMap.get(cfId);
if (!rangeDelta) {
rangeDelta = { add: [], remove: [] };
rangeDeltaMap.set(cfId, rangeDelta);
}
return rangeDelta;
};
const targetRuleList = this._conditionalFormattingRuleModel.getSubunitRules(targetUnitId, targetSubUnitId) ?? [];
const waitAddRule = new Map<string, IConditionFormattingRule>();
targetRuleList.forEach((rule) => {
if (Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
rangeMap.set(rule.cfId, rule.ranges);
getRangeDelta(rule.cfId).remove.push(...targetRanges);
}
});
const sourceRuleList = this._conditionalFormattingRuleModel.getSubunitRules(sourceUnitId, sourceSubUnitId) ?? [];
const sourceRules = isSkipSheet ? [...sourceRuleList].reverse() : sourceRuleList;
sourceRules.forEach((rule) => {
const sourceRanges = rule.ranges.flatMap((range) => {
const intersected = getIntersectRange(range, sourceRange);
return intersected ? [Rectangle.getRelativeRange(intersected, sourceRange)] : [];
});
const additions = repeats.flatMap((repeat) => sourceRanges.flatMap((range) => {
const copiedRange = getIntersectRange(range, repeat.repeatRelativeRange);
return copiedRange ? [Rectangle.getPositionRange(copiedRange, repeat.startRange)] : [];
}));
if (additions.length) {
let targetCfId = rule.cfId;
if (isSkipSheet) {
targetCfId = this._conditionalFormattingRuleModel.createCfId(targetUnitId, targetSubUnitId);
waitAddRule.set(targetCfId, {
...Tools.deepClone(rule),
cfId: targetCfId,
ranges: [],
});
rangeMap.set(targetCfId, []);
}
if (!rangeMap.has(targetCfId)) {
rangeMap.set(targetCfId, rule.ranges);
}
getRangeDelta(targetCfId).add.push(...additions);
}
});
rangeDeltaMap.forEach((rangeDelta, cfId) => {
const ranges = rangeMap.get(cfId);
if (!ranges) {
return;
}
rangeMap.set(cfId, this._conditionalFormattingRangeTransformService.applyRangeDelta(
ranges,
rangeDelta.remove,
rangeDelta.add
));
});
rangeMap.forEach((ranges, cfId) => {
if (!isSkipSheet) {
const rule = this._conditionalFormattingRuleModel.getRule(sourceUnitId, sourceSubUnitId, cfId);
if (!rule) {
return;
}
if (ranges.length) {
const params: ISetConditionalRuleMutationParams = {
unitId: sourceUnitId,
subUnitId: sourceSubUnitId,
rule: { ...rule, ranges },
};
redos.push({ id: SetConditionalRuleMutation.id, params });
undos.push(...setConditionalRuleMutationUndoFactory(this._injector, params));
} else {
const params: IDeleteConditionalRuleMutationParams = {
unitId: sourceUnitId,
subUnitId: sourceSubUnitId,
cfId: rule.cfId,
};
redos.push({ id: DeleteConditionalRuleMutation.id, params });
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, params));
}
} else {
const waitAdd = waitAddRule.get(cfId);
if (waitAdd) {
if (ranges.length) {
const params: IAddConditionalRuleMutationParams = {
unitId: targetUnitId,
subUnitId: targetSubUnitId,
rule: { ...waitAdd, ranges },
};
redos.push({ id: AddConditionalRuleMutation.id, params });
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, params));
}
return;
}
const rule = this._conditionalFormattingRuleModel.getRule(targetUnitId, targetSubUnitId, cfId);
if (rule) {
if (ranges.length) {
const params: ISetConditionalRuleMutationParams = {
unitId: targetUnitId,
subUnitId: targetSubUnitId,
rule: { ...rule, ranges },
};
redos.push({ id: SetConditionalRuleMutation.id, params });
undos.push(...setConditionalRuleMutationUndoFactory(this._injector, params));
} else {
const params: IDeleteConditionalRuleMutationParams = {
unitId: targetUnitId,
subUnitId: targetSubUnitId,
cfId: rule.cfId,
};
redos.push({ id: DeleteConditionalRuleMutation.id, params });
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, params));
}
}
}
});
return {
undos,
redos,
};
};
const hook: IFormatPainterHook = {
id: SHEET_CONDITIONAL_FORMATTING_PLUGIN,
onStatusChange: (status) => {
switch (status) {
case FormatPainterStatus.INFINITE:
case FormatPainterStatus.ONCE: {
const unitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getUnitId();
const subUnitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getActiveSheet()?.getSheetId();
const selection = this._sheetsSelectionsService.getCurrentLastSelection();
const range = selection?.range;
if (unitId && subUnitId && range) {
this._painterConfig = { unitId, subUnitId, range };
}
break;
}
case FormatPainterStatus.OFF: {
this._painterConfig = null;
break;
}
}
},
onApply: (unitId, subUnitId, targetRange) => {
if (this._painterConfig) {
return generalApplyFunc(unitId, subUnitId, targetRange);
}
return {
redos: [],
undos: [],
};
},
};
this._formatPainterService.addHook(hook);
}
}

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 { IDisposable } from '@univerjs/core';
import type { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import type { LocaleKey } from '../locale/types';
import {
Disposable,
generateRandomId,
Inject,
IUniverInstanceService,
LocaleService,
UniverInstanceType,
} from '@univerjs/core';
import { ISidebarService } from '@univerjs/ui';
import { CF_PANEL_KEY } from '../const';
export class ConditionalFormattingPanelController extends Disposable {
private _sidebarDisposable: IDisposable | null = null;
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(ISidebarService) private _sidebarService: ISidebarService,
@Inject(LocaleService) private _localeService: LocaleService
) {
super();
this.disposeWithMe(
this._univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET).subscribe((sheet) => {
if (!sheet) this._sidebarDisposable?.dispose();
})
);
this.disposeWithMe(this._sidebarService.sidebarOptions$.subscribe((info) => {
if (info.id === CF_PANEL_KEY) {
if (!info.visible) {
setTimeout(() => {
this._sidebarService.sidebarOptions$.next({ visible: false });
});
}
}
}));
}
openPanel(rule?: IConditionFormattingRule) {
const props = {
id: CF_PANEL_KEY,
header: { title: this._localeService.t<LocaleKey>('sheets-conditional-formatting-ui.title') },
children: {
label: CF_PANEL_KEY,
rule,
key: generateRandomId(4),
},
onClose: () => this._sidebarDisposable = null,
};
this._sidebarDisposable = this._sidebarService.open(props);
}
}

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 { ICommandInfo } from '@univerjs/core';
import type { IAddCfCommandParams } from '@univerjs/sheets-conditional-formatting';
import type { LocaleKey } from '../locale/types';
import { Disposable, ICommandService, Inject, LocaleService } from '@univerjs/core';
import {
RangeProtectionPermissionEditPoint,
SheetPermissionCheckController,
WorkbookEditablePermission,
WorksheetEditPermission,
WorksheetSetCellStylePermission,
} from '@univerjs/sheets';
import { AddCfCommand } from '@univerjs/sheets-conditional-formatting';
export class ConditionalFormattingPermissionController 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 === AddCfCommand.id) {
const { unitId, subUnitId, rule: { ranges } } = command.params as IAddCfCommandParams;
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-conditional-formatting-ui.permission.dialog.setStyleErr')
);
}
}
})
);
}
}

View File

@@ -0,0 +1,279 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICellDataForSheetInterceptor, IRange, Workbook } from '@univerjs/core';
import type { IConditionalFormattingCellData, IConditionalFormattingRenderRangeResolver, IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import { Disposable, getIntersectRange, Inject, InterceptorEffectEnum, IUniverInstanceService, Rectangle, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
import {
CFRuleType,
ConditionalFormattingIcon,
ConditionalFormattingRangeIndexModel,
ConditionalFormattingRuleModel,
ConditionalFormattingService,
ConditionalFormattingViewModel,
DataBar,
dataBarUKey,
DEFAULT_PADDING,
DEFAULT_WIDTH,
IconUKey,
} from '@univerjs/sheets-conditional-formatting';
import {
SheetSkeletonManagerService,
} from '@univerjs/sheets-ui';
import { merge } from 'rxjs';
import { bufferTime, filter } from 'rxjs/operators';
export class SheetsCfRenderController extends Disposable {
/**
* When a set operation is triggered multiple times over a short period of time, it may result in some callbacks not being disposed,and caused a render cache exception.
* The solution here is to store all the asynchronous tasks and focus on processing after the last callback
*/
private _ruleChangeCacheMap: Map<string, Array<{
oldRule: IConditionFormattingRule;
rule: IConditionFormattingRule;
dispose: () => boolean;
}>> = new Map();
private _boundRenderExtensions = new Map<string, Set<DataBar | ConditionalFormattingIcon>>();
constructor(
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
@Inject(ConditionalFormattingService) private _conditionalFormattingService: ConditionalFormattingService,
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(IRenderManagerService) private _renderManagerService: IRenderManagerService,
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel,
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
@Inject(ConditionalFormattingRangeIndexModel) private _conditionalFormattingRangeIndexModel: ConditionalFormattingRangeIndexModel
) {
super();
this._initViewModelInterceptor();
this._initRenderRangeResolvers();
this._initSkeleton();
queueMicrotask(() => this._markActiveSheetRulesDirty());
this.disposeWithMe(() => {
this._ruleChangeCacheMap.clear();
this._boundRenderExtensions.forEach((extensions) => {
extensions.forEach((extension) => extension.setRenderRangeResolver(null));
});
this._boundRenderExtensions.clear();
});
}
private _initRenderRangeResolvers() {
const dataBarResolver = this._createRenderRangeResolver(CFRuleType.dataBar);
const iconResolver = this._createRenderRangeResolver(CFRuleType.iconSet);
const bind = (unitId: string) => {
this._unbindRenderRangeResolvers(unitId);
const mainComponent = this._renderManagerService.getRenderUnitById(unitId)?.mainComponent;
if (!mainComponent || !('getExtensionByKey' in mainComponent)) {
return;
}
const extensions = new Set<DataBar | ConditionalFormattingIcon>();
const dataBar = mainComponent.getExtensionByKey(dataBarUKey);
if (dataBar instanceof DataBar) {
dataBar.setRenderRangeResolver(dataBarResolver);
extensions.add(dataBar);
}
const icon = mainComponent.getExtensionByKey(IconUKey);
if (icon instanceof ConditionalFormattingIcon) {
icon.setRenderRangeResolver(iconResolver);
extensions.add(icon);
}
if (extensions.size) {
this._boundRenderExtensions.set(unitId, extensions);
}
};
this._renderManagerService.getAllRenderersOfType(UniverInstanceType.UNIVER_SHEET)
.forEach((render) => bind(render.unitId));
this.disposeWithMe(
this._renderManagerService.created$.subscribe((render) => {
if (render.type === UniverInstanceType.UNIVER_SHEET) {
bind(render.unitId);
}
})
);
this.disposeWithMe(
this._renderManagerService.disposed$.subscribe((unitId) => this._unbindRenderRangeResolvers(unitId))
);
}
private _createRenderRangeResolver(ruleType: CFRuleType): IConditionalFormattingRenderRangeResolver {
return (unitId, subUnitId, ranges) => {
const intersections: IRange[] = [];
const indexedRules = this._conditionalFormattingRangeIndexModel
.getRulesByRanges(unitId, subUnitId, ranges)
.filter((rule) => rule.rule.type === ruleType);
const matchingRules = indexedRules.length
? indexedRules
: (this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) ?? [])
.filter((rule) => rule.rule.type === ruleType);
ranges.forEach((range) => {
matchingRules.forEach((rule) => {
rule.ranges.forEach((ruleRange) => {
const intersection = getIntersectRange(range, ruleRange);
if (intersection) {
intersections.push(intersection);
}
});
});
});
return intersections.length > 1 ? Rectangle.mergeRanges(intersections) : intersections;
};
}
private _unbindRenderRangeResolvers(unitId: string) {
const extensions = this._boundRenderExtensions.get(unitId);
extensions?.forEach((extension) => extension.setRenderRangeResolver(null));
this._boundRenderExtensions.delete(unitId);
}
private _collectDirtyRanges(items: Array<{ cfId?: string; rule?: IConditionFormattingRule; subUnitId: string; unitId: string }>, unitId: string, subUnitId: string): IRange[] {
const ranges: IRange[] = [];
items.forEach((item) => {
if (item.unitId !== unitId || item.subUnitId !== subUnitId) {
return;
}
const rule = item.rule ?? (item.cfId ? this._conditionalFormattingRuleModel.getRule(unitId, subUnitId, item.cfId) : null);
if (rule?.ranges?.length) {
ranges.push(...rule.ranges);
}
});
return ranges;
}
private _intersectDirtyRangesWithRenderedRange(ranges: IRange[], renderedRange?: IRange): IRange[] {
if (!renderedRange || renderedRange.startRow < 0 || renderedRange.startColumn < 0) {
return [];
}
return ranges.map((range) => ({
startRow: Math.max(range.startRow, renderedRange.startRow),
endRow: Math.min(range.endRow, renderedRange.endRow),
startColumn: Math.max(range.startColumn, renderedRange.startColumn),
endColumn: Math.min(range.endColumn, renderedRange.endColumn),
})).filter((range) => range.startRow <= range.endRow && range.startColumn <= range.endColumn);
}
private _markDirtySkeleton(items: Array<{ cfId?: string; rule?: IConditionFormattingRule; subUnitId: string; unitId: string }>) {
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const worksheet = workbook?.getActiveSheet();
if (!workbook || !worksheet) {
return;
}
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const render = this._renderManagerService.getRenderUnitById(unitId);
const skeletonManagerService = render?.with(SheetSkeletonManagerService);
const currentSkeleton = skeletonManagerService?.getCurrentSkeleton();
const dirtyRanges = this._intersectDirtyRangesWithRenderedRange(
this._collectDirtyRanges(items, unitId, subUnitId),
currentSkeleton?.rowColumnSegment
);
if (dirtyRanges.length) {
currentSkeleton?.resetRangeCache(dirtyRanges);
}
skeletonManagerService?.reCalculate();
render?.mainComponent?.makeDirty();
}
private _markActiveSheetRulesDirty() {
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const worksheet = workbook?.getActiveSheet();
if (!workbook || !worksheet) {
return;
}
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const rules = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) ?? [];
if (!rules.length) {
return;
}
this._markDirtySkeleton(rules.map((rule) => ({ rule, subUnitId, unitId })));
}
private _initSkeleton() {
this.disposeWithMe(merge(this._conditionalFormattingRuleModel.$ruleChange, this._conditionalFormattingViewModel.markDirty$)
.pipe(
bufferTime(16),
filter((v) => !!v.length),
filter((v) => {
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) return false;
const worksheet = workbook.getActiveSheet();
if (!worksheet) return false;
return v.some((item) => item.unitId === workbook.getUnitId() && item.subUnitId === worksheet.getSheetId());
})
).subscribe((items) => this._markDirtySkeleton(items)));
}
private _initViewModelInterceptor() {
this.disposeWithMe(this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.CELL_CONTENT, {
effect: InterceptorEffectEnum.Style,
handler: (cell, context, next) => {
const result = this._conditionalFormattingService.composeStyle(context.unitId, context.subUnitId, context.row, context.col);
if (!result) {
return next(cell);
}
const styleMap = context.workbook.getStyles();
const defaultStyle = (typeof cell?.s === 'string' ? styleMap.get(cell?.s) : cell?.s) || {};
const cloneCell = (cell === context.rawData ? { ...context.rawData } : cell) as IConditionalFormattingCellData & ICellDataForSheetInterceptor;
if (result.style) {
const activeStyle = {
...defaultStyle,
...result.style,
};
Object.assign(cloneCell, { s: activeStyle });
}
if (!cloneCell.fontRenderExtension) {
cloneCell.fontRenderExtension = {};
if (result.isShowValue !== undefined) {
cloneCell.fontRenderExtension.isSkip = !result.isShowValue;
}
}
if (result.dataBar) {
cloneCell.dataBar = result.dataBar;
}
if (result.iconSet) {
cloneCell.iconSet = result.iconSet;
cloneCell.fontRenderExtension.leftOffset = DEFAULT_PADDING + DEFAULT_WIDTH;
}
return next(cloneCell);
},
priority: 10,
}));
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Workbook } from '@univerjs/core';
import { Disposable, DisposableCollection, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { CONDITIONAL_FORMATTING_VIEWPORT_CACHE_LENGTH, ConditionalFormattingViewModel } from '@univerjs/sheets-conditional-formatting';
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
export class ConditionalFormattingViewportController extends Disposable {
private _unitDisposable: DisposableCollection = new DisposableCollection();
constructor(
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel,
@IUniverInstanceService private _univerInstanceService: IUniverInstanceService,
@IRenderManagerService private _renderManagerService: IRenderManagerService
) {
super();
this._init();
}
private _init() {
const unit = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const bindUnit = (unit: Workbook) => {
this._unitDisposable.dispose();
this._unitDisposable = new DisposableCollection();
const unitId = unit.getUnitId();
const render = this._renderManagerService.getRenderUnitById(unitId);
if (!render) {
return;
}
const sheetSkeletonManagerService = render.with(SheetSkeletonManagerService);
this._unitDisposable.add(sheetSkeletonManagerService.currentSkeleton$.subscribe((s) => {
if (s) {
const range = s.skeleton.rowColumnSegment;
const col = range.endColumn - range.startColumn + 1;
const row = range.endRow - range.startRow + 1;
const length = row * col * 9;
const result = Math.max(CONDITIONAL_FORMATTING_VIEWPORT_CACHE_LENGTH, length);
this._conditionalFormattingViewModel.setCacheLength(result);
}
}));
};
if (unit) {
bindUnit(unit);
}
this.disposeWithMe(
this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET).subscribe((unit) => {
if (!unit) {
this._unitDisposable.dispose();
return;
}
bindUnit(unit);
})
);
}
override dispose(): void {
this._unitDisposable.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, Inject } from '@univerjs/core';
import { ConditionsDoubleIcon, DataBarIcon, RatingIcon } from '@univerjs/icons';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { CF_PANEL_KEY } from '../const';
import { ConditionFormattingPanel } from '../views/ConditionFormattingPanel';
export class ComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
@Inject(IconManager) private readonly _iconManager: IconManager
) {
super();
this._registerComponents();
this._registerIcons();
}
private _registerComponents(): void {
this.disposeWithMe(
this._componentManager.register(CF_PANEL_KEY, ConditionFormattingPanel)
);
}
private _registerIcons(): void {
this.disposeWithMe(this._iconManager.register({
ConditionsDoubleIcon,
DataBarIcon,
RatingIcon,
}));
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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 { ConditionsDoubleIcon, DataBarIcon, RatingIcon } from '@univerjs/icons';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { CF_PANEL_KEY } from '../../const';
import { MobileConditionFormattingPanel } from '../../views/mobile/MobileConditionFormattingPanel';
export class MobileComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) componentManager: ComponentManager,
@Inject(IconManager) iconManager: IconManager
) {
super();
this.disposeWithMe(componentManager.register(CF_PANEL_KEY, MobileConditionFormattingPanel));
this.disposeWithMe(iconManager.register({ ConditionsDoubleIcon, DataBarIcon, RatingIcon }));
}
}

View File

@@ -0,0 +1,2 @@
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,34 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './global.css';
export { AddAverageCfCommand } from './commands/commands/add-average-cf.command';
export { AddColorScaleConditionalRuleCommand } from './commands/commands/add-color-scale-cf.command';
export { AddDataBarConditionalRuleCommand } from './commands/commands/add-data-bar-cf.command';
export { AddDuplicateValuesCfCommand } from './commands/commands/add-duplicate-values-cf.command';
export { AddIconSetConditionalRuleCommand } from './commands/commands/add-icon-set-cf.command';
export { AddNumberCfCommand } from './commands/commands/add-number-cf.command';
export { AddRankCfCommand } from './commands/commands/add-rank-cf.command';
export { AddTextCfCommand } from './commands/commands/add-text-cf.command';
export { AddTimePeriodCfCommand } from './commands/commands/add-time-period-cf.command';
export { AddUniqueValuesCfCommand } from './commands/commands/add-unique-values-cf.command';
export { OpenConditionalFormattingOperator } from './commands/operations/open-conditional-formatting-panel';
export type { IUniverSheetsConditionalFormattingUIConfig } from './config/config';
export { ConditionalFormattingClearController } from './controllers/cf.clear.controller';
export { menuSchema as SheetsConditionalFormattingUIMenuSchema } from './menu/schema';
export { UniverSheetsConditionalFormattingMobileUIPlugin } from './mobile-plugin';
export { UniverSheetsConditionalFormattingUIPlugin } from './plugin';

View File

@@ -0,0 +1,179 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'التنسيق الشرطي',
menu: {
manageConditionalFormatting: 'إدارة التنسيق الشرطي',
createConditionalFormatting: 'إنشاء تنسيق شرطي',
clearRangeRules: 'مسح القواعد للنطاق المحدد',
clearWorkSheetRules: 'مسح القواعد للورقة بأكملها',
},
form: {
lessThan: 'يجب أن تكون القيمة أقل من {0}',
lessThanOrEqual: 'يجب أن تكون القيمة أقل من أو تساوي {0}',
greaterThan: 'يجب أن تكون القيمة أكبر من {0}',
greaterThanOrEqual: 'يجب أن تكون القيمة أكبر من أو تساوي {0}',
},
iconSet: {
direction: 'الاتجاه',
shape: 'الشكل',
rank: 'الترتيب',
rule: 'القاعدة',
icon: 'الأيقونة',
type: 'النوع',
value: 'القيمة',
reverseIconOrder: 'عكس ترتيب الأيقونات',
and: 'و',
when: 'عندما',
onlyShowIcon: 'إظهار الأيقونة فقط',
noCellIcon: 'بدون أيقونة خلية',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'إنشاء قاعدة',
clear: 'مسح كل القواعد',
range: 'نطاق التطبيق',
styleType: 'نوع النمط',
submit: 'إرسال',
cancel: 'إلغاء',
rankAndAverage: 'الأعلى/الأدنى/المتوسط',
styleRule: 'قاعدة النمط',
isNotBottom: 'الأعلى',
isBottom: 'الأدنى',
greaterThanAverage: 'أكبر من المتوسط',
lessThanAverage: 'أقل من المتوسط',
medianValue: 'القيمة الوسيطة',
fillType: 'نوع التعبئة',
pureColor: 'لون ثابت',
gradient: 'تدرج',
colorSet: 'مجموعة الألوان',
positive: 'إيجابي',
native: 'سلبي',
workSheet: 'الورقة بأكملها',
selectedRange: 'النطاق المحدد',
managerRuleSelect: 'إدارة قواعد {0}',
onlyShowDataBar: 'إظهار أشرطة البيانات فقط',
},
preview: {
describe: {
beginsWith: 'يبدأ بـ {0}',
endsWith: 'ينتهي بـ {0}',
containsText: 'النص يحتوي على {0}',
notContainsText: 'النص لا يحتوي على {0}',
equal: 'يساوي {0}',
notEqual: 'لا يساوي {0}',
containsBlanks: 'يحتوي على فراغات',
notContainsBlanks: 'لا يحتوي على فراغات',
containsErrors: 'يحتوي على أخطاء',
notContainsErrors: 'لا يحتوي على أخطاء',
greaterThan: 'أكبر من {0}',
greaterThanOrEqual: 'أكبر من أو يساوي {0}',
lessThan: 'أقل من {0}',
lessThanOrEqual: 'أقل من أو يساوي {0}',
notBetween: 'ليست بين {0} و {1}',
between: 'بين {0} و {1}',
yesterday: 'أمس',
tomorrow: 'غدًا',
last7Days: 'آخر 7 أيام',
thisMonth: 'هذا الشهر',
lastMonth: 'الشهر الماضي',
nextMonth: 'الشهر القادم',
thisWeek: 'هذا الأسبوع',
lastWeek: 'الأسبوع الماضي',
nextWeek: 'الأسبوع القادم',
today: 'اليوم',
topN: 'أعلى {0}',
bottomN: 'أدنى {0}',
topNPercent: 'أعلى {0}%',
bottomNPercent: 'أدنى {0}%',
},
},
operator: {
beginsWith: 'يبدأ بـ',
endsWith: 'ينتهي بـ',
containsText: 'النص يحتوي على',
notContainsText: 'النص لا يحتوي على',
equal: 'يساوي',
notEqual: 'لا يساوي',
containsBlanks: 'يحتوي على فراغات',
notContainsBlanks: 'لا يحتوي على فراغات',
containsErrors: 'يحتوي على أخطاء',
notContainsErrors: 'لا يحتوي على أخطاء',
greaterThan: 'أكبر من',
greaterThanOrEqual: 'أكبر من أو يساوي',
lessThan: 'أقل من',
lessThanOrEqual: 'أقل من أو يساوي',
notBetween: 'ليست بين',
between: 'بين',
yesterday: 'أمس',
tomorrow: 'غدًا',
last7Days: 'آخر 7 أيام',
thisMonth: 'هذا الشهر',
lastMonth: 'الشهر الماضي',
nextMonth: 'الشهر القادم',
thisWeek: 'هذا الأسبوع',
lastWeek: 'الأسبوع الماضي',
nextWeek: 'الأسبوع القادم',
today: 'اليوم',
},
ruleType: {
highlightCell: 'تمييز الخلية',
dataBar: 'شريط البيانات',
colorScale: 'مقياس الألوان',
formula: 'صيغة مخصصة',
iconSet: 'مجموعة أيقونات',
},
subRuleType: {
uniqueValues: 'القيم الفريدة',
duplicateValues: 'القيم المكررة',
text: 'نص',
timePeriod: 'فترة زمنية',
number: 'رقم',
average: 'متوسط',
},
valueType: {
num: 'رقم',
min: 'الحد الأدنى',
max: 'الحد الأقصى',
percent: 'نسبة مئوية',
percentile: 'النسبة المئوية',
formula: 'صيغة',
none: 'لا شيء',
},
errorMessage: {
notBlank: 'لا يمكن أن يكون الشرط فارغًا',
formulaError: 'صيغة خاطئة',
rangeError: 'تحديد غير صالح',
},
permission: {
dialog: {
setStyleErr: 'النطاق محمي، وليس لديك الإذن بتعيين الأنماط. لتعيين الأنماط، يُرجى التواصل مع المنشئ.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Format condicional',
menu: {
manageConditionalFormatting: 'Gestiona el format condicional',
createConditionalFormatting: 'Crea format condicional',
clearRangeRules: 'Esborra les regles del rang seleccionat',
clearWorkSheetRules: 'Esborra les regles de tota la fulla',
},
form: {
lessThan: 'El valor ha de ser menor que {0}',
lessThanOrEqual: 'El valor ha de ser menor o igual que {0}',
greaterThan: 'El valor ha de ser més gran que {0}',
greaterThanOrEqual: 'El valor ha de ser més gran o igual que {0}',
},
iconSet: {
direction: 'Direcció',
shape: 'Forma',
rank: 'Rang',
rule: 'Regla',
icon: 'Icona',
type: 'Tipus',
value: 'Valor',
reverseIconOrder: 'Inverteix l\'ordre de les icones',
and: 'I',
when: 'Quan',
onlyShowIcon: 'Mostra només la icona',
noCellIcon: 'Sense icona de cel·la',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Crea regla',
clear: 'Esborra totes les regles',
range: 'Aplica rang',
styleType: 'Tipus d\'estil',
submit: 'Envia',
cancel: 'Cancel·la',
rankAndAverage: 'Superior/Inferior/Mitjana',
styleRule: 'Regla d\'estil',
isNotBottom: 'Superior',
isBottom: 'Inferior',
greaterThanAverage: 'Més gran que la mitjana',
lessThanAverage: 'Menys que la mitjana',
medianValue: 'Valor mitjà',
fillType: 'Tipus d\'emplenat',
pureColor: 'Color sòlid',
gradient: 'Gradient',
colorSet: 'Conjunt de colors',
positive: 'Positiu',
native: 'Negatiu',
workSheet: 'Tota la fulla',
selectedRange: 'Rang seleccionat',
managerRuleSelect: 'Gestiona les regles de {0}',
onlyShowDataBar: 'Mostra només les barres de dades',
},
preview: {
describe: {
beginsWith: 'Comença amb {0}',
endsWith: 'Acaba amb {0}',
containsText: 'El text conté {0}',
notContainsText: 'El text no conté {0}',
equal: 'Igual a {0}',
notEqual: 'Diferent de {0}',
containsBlanks: 'Conté espais en blanc',
notContainsBlanks: 'No conté espais en blanc',
containsErrors: 'Conté errors',
notContainsErrors: 'No conté errors',
greaterThan: 'Més gran que {0}',
greaterThanOrEqual: 'Més gran o igual que {0}',
lessThan: 'Menys que {0}',
lessThanOrEqual: 'Menys o igual que {0}',
notBetween: 'No està entre {0} i {1}',
between: 'Entre {0} i {1}',
yesterday: 'Ahir',
tomorrow: 'Demà',
last7Days: 'Últims 7 dies',
thisMonth: 'Aquest mes',
lastMonth: 'Mes passat',
nextMonth: 'Mes següent',
thisWeek: 'Aquesta setmana',
lastWeek: 'Setmana passada',
nextWeek: 'Setmana següent',
today: 'Avui',
topN: 'Superior {0}',
bottomN: 'Inferior {0}',
topNPercent: 'Superior {0}%',
bottomNPercent: 'Inferior {0}%',
},
},
operator: {
beginsWith: 'Comença amb',
endsWith: 'Acaba amb',
containsText: 'El text conté',
notContainsText: 'El text no conté',
equal: 'Igual a',
notEqual: 'Diferent de',
containsBlanks: 'Conté espais en blanc',
notContainsBlanks: 'No conté espais en blanc',
containsErrors: 'Conté errors',
notContainsErrors: 'No conté errors',
greaterThan: 'Més gran que',
greaterThanOrEqual: 'Més gran o igual que',
lessThan: 'Menys que',
lessThanOrEqual: 'Menys o igual que',
notBetween: 'No està entre',
between: 'Entre',
yesterday: 'Ahir',
tomorrow: 'Demà',
last7Days: 'Últims 7 dies',
thisMonth: 'Aquest mes',
lastMonth: 'Mes passat',
nextMonth: 'Mes següent',
thisWeek: 'Aquesta setmana',
lastWeek: 'Setmana passada',
nextWeek: 'Setmana següent',
today: 'Avui',
},
ruleType: {
highlightCell: 'Ressalta la cel·la',
dataBar: 'Barra de dades',
colorScale: 'Escala de colors',
formula: 'Fórmula personalitzada',
iconSet: 'Conjunt d\'icones',
},
subRuleType: {
uniqueValues: 'Valors únics',
duplicateValues: 'Valors duplicats',
text: 'Text',
timePeriod: 'Període de temps',
number: 'Número',
average: 'Mitjana',
},
valueType: {
num: 'Número',
min: 'Mínim',
max: 'Màxim',
percent: 'Percentatge',
percentile: 'Percentil',
formula: 'Fórmula',
none: 'Cap',
},
errorMessage: {
notBlank: 'La condició no pot estar buida',
formulaError: 'Fórmula incorrecta',
rangeError: 'Selecció incorrecta',
},
permission: {
dialog: {
setStyleErr: 'L\'interval està protegit i no teniu permís per establir estils. Per establir estils, contacteu amb el creador.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,179 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Bedingte Formatierung',
menu: {
manageConditionalFormatting: 'Bedingte Formatierung verwalten',
createConditionalFormatting: 'Bedingte Formatierung erstellen',
clearRangeRules: 'Regeln für ausgewählten Bereich löschen',
clearWorkSheetRules: 'Regeln für gesamtes Blatt löschen',
},
form: {
lessThan: 'Der Wert muss kleiner als {0} sein',
lessThanOrEqual: 'Der Wert muss kleiner oder gleich {0} sein',
greaterThan: 'Der Wert muss größer als {0} sein',
greaterThanOrEqual: 'Der Wert muss größer oder gleich {0} sein',
},
iconSet: {
direction: 'Richtung',
shape: 'Form',
rank: 'Rang',
rule: 'Regel',
icon: 'Symbol',
type: 'Typ',
value: 'Wert',
reverseIconOrder: 'Symbolreihenfolge umkehren',
and: 'Und',
when: 'Wenn',
onlyShowIcon: 'Nur Symbol anzeigen',
noCellIcon: 'Kein Zellsymbol',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Regel erstellen',
clear: 'Alle Regeln löschen',
range: 'Bereich anwenden',
styleType: 'Stiltyp',
submit: 'Anwenden',
cancel: 'Abbrechen',
rankAndAverage: 'Oben/Unten/Mittelwert',
styleRule: 'Stilregel',
isNotBottom: 'Oben',
isBottom: 'Unten',
greaterThanAverage: 'Größer als Mittelwert',
lessThanAverage: 'Kleiner als Mittelwert',
medianValue: 'Medianwert',
fillType: 'Fülltyp',
pureColor: 'Einfarbig',
gradient: 'Farbverlauf',
colorSet: 'Farbsatz',
positive: 'Positiv',
native: 'Negativ',
workSheet: 'Gesamtes Blatt',
selectedRange: 'Ausgewählter Bereich',
managerRuleSelect: '{0} Regeln verwalten',
onlyShowDataBar: 'Nur Datenbalken anzeigen',
},
preview: {
describe: {
beginsWith: 'Beginnt mit {0}',
endsWith: 'Endet mit {0}',
containsText: 'Text enthält {0}',
notContainsText: 'Text enthält nicht {0}',
equal: 'Gleich {0}',
notEqual: 'Ungleich {0}',
containsBlanks: 'Enthält Leerzellen',
notContainsBlanks: 'Enthält keine Leerzellen',
containsErrors: 'Enthält Fehler',
notContainsErrors: 'Enthält keine Fehler',
greaterThan: 'Größer als {0}',
greaterThanOrEqual: 'Größer oder gleich {0}',
lessThan: 'Kleiner als {0}',
lessThanOrEqual: 'Kleiner oder gleich {0}',
notBetween: 'Nicht zwischen {0} und {1}',
between: 'Zwischen {0} und {1}',
yesterday: 'Gestern',
tomorrow: 'Morgen',
last7Days: 'Letzte 7 Tage',
thisMonth: 'Dieser Monat',
lastMonth: 'Letzter Monat',
nextMonth: 'Nächster Monat',
thisWeek: 'Diese Woche',
lastWeek: 'Letzte Woche',
nextWeek: 'Nächste Woche',
today: 'Heute',
topN: 'Oben {0}',
bottomN: 'Unten {0}',
topNPercent: 'Oben {0}%',
bottomNPercent: 'Unten {0}%',
},
},
operator: {
beginsWith: 'Beginnt mit',
endsWith: 'Endet mit',
containsText: 'Text enthält',
notContainsText: 'Text enthält nicht',
equal: 'Gleich',
notEqual: 'Ungleich',
containsBlanks: 'Enthält Leerzellen',
notContainsBlanks: 'Enthält keine Leerzellen',
containsErrors: 'Enthält Fehler',
notContainsErrors: 'Enthält keine Fehler',
greaterThan: 'Größer als',
greaterThanOrEqual: 'Größer oder gleich',
lessThan: 'Kleiner als',
lessThanOrEqual: 'Kleiner oder gleich',
notBetween: 'Nicht zwischen',
between: 'Zwischen',
yesterday: 'Gestern',
tomorrow: 'Morgen',
last7Days: 'Letzte 7 Tage',
thisMonth: 'Dieser Monat',
lastMonth: 'Letzter Monat',
nextMonth: 'Nächster Monat',
thisWeek: 'Diese Woche',
lastWeek: 'Letzte Woche',
nextWeek: 'Nächste Woche',
today: 'Heute',
},
ruleType: {
highlightCell: 'Zelle hervorheben',
dataBar: 'Datenbalken',
colorScale: 'Farbskala',
formula: 'Benutzerdefinierte Formel',
iconSet: 'Symbolsatz',
},
subRuleType: {
uniqueValues: 'Einzigartige Werte',
duplicateValues: 'Doppelte Werte',
text: 'Text',
timePeriod: 'Zeitraum',
number: 'Zahl',
average: 'Mittelwert',
},
valueType: {
num: 'Zahl',
min: 'Minimum',
max: 'Maximum',
percent: 'Prozent',
percentile: 'Perzentil',
formula: 'Formel',
none: 'Keine',
},
errorMessage: {
notBlank: 'Bedingung darf nicht leer sein',
formulaError: 'Falsche Formel',
rangeError: 'Ungültige Auswahl',
},
permission: {
dialog: {
setStyleErr: 'Der Bereich ist geschützt und Sie haben keine Berechtigung, Stile festzulegen. Um Stile festzulegen, wenden Sie sich bitte an den Ersteller.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,177 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const locale = {
'sheets-conditional-formatting-ui': {
title: 'Conditional Formatting',
menu: {
manageConditionalFormatting: 'Manage Conditional Formatting',
createConditionalFormatting: 'Create Conditional Formatting',
clearRangeRules: 'Clear Rules for Selected Range',
clearWorkSheetRules: 'Clear Rules for Entire Sheet',
},
form: {
lessThan: 'The value must be less than {0}',
lessThanOrEqual: 'The value must be less than or equal to {0}',
greaterThan: 'The value must be greater than {0}',
greaterThanOrEqual: 'The value must be greater than or equal to {0}',
},
iconSet: {
direction: 'Direction',
shape: 'Shape',
rank: 'Rank',
rule: 'Rule',
icon: 'Icon',
type: 'Type',
value: 'Value',
reverseIconOrder: 'Reverse Icon Order',
and: 'And',
when: 'When',
onlyShowIcon: 'Only Show Icon',
noCellIcon: 'No Cell Icon',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Create Rule',
clear: 'Clear All Rules',
range: 'Apply Range',
styleType: 'Style Type',
submit: 'Submit',
cancel: 'Cancel',
rankAndAverage: 'Top/Bottom/Average',
styleRule: 'Style Rule',
isNotBottom: 'Top',
isBottom: 'Bottom',
greaterThanAverage: 'Greater Than Average',
lessThanAverage: 'Less Than Average',
medianValue: 'Median Value',
fillType: 'Fill Type',
pureColor: 'Solid Color',
gradient: 'Gradient',
colorSet: 'Color Set',
positive: 'Positive',
native: 'Negative',
workSheet: 'Entire Sheet',
selectedRange: 'Selected Range',
managerRuleSelect: 'Manage {0} Rules',
onlyShowDataBar: 'Only Show Data Bars',
},
preview: {
describe: {
beginsWith: 'Begins with {0}',
endsWith: 'Ends with {0}',
containsText: 'Text contains {0}',
notContainsText: 'Text does not contain {0}',
equal: 'Equal to {0}',
notEqual: 'Not equal to {0}',
containsBlanks: 'Contains Blanks',
notContainsBlanks: 'Does not contain Blanks',
containsErrors: 'Contains Errors',
notContainsErrors: 'Does not contain Errors',
greaterThan: 'Greater than {0}',
greaterThanOrEqual: 'Greater than or equal to {0}',
lessThan: 'Less than {0}',
lessThanOrEqual: 'Less than or equal to {0}',
notBetween: 'Not between {0} and {1}',
between: 'Between {0} and {1}',
yesterday: 'Yesterday',
tomorrow: 'Tomorrow',
last7Days: 'Last 7 Days',
thisMonth: 'This Month',
lastMonth: 'Last Month',
nextMonth: 'Next Month',
thisWeek: 'This Week',
lastWeek: 'Last Week',
nextWeek: 'Next Week',
today: 'Today',
topN: 'Top {0}',
bottomN: 'Bottom {0}',
topNPercent: 'Top {0}%',
bottomNPercent: 'Bottom {0}%',
},
},
operator: {
beginsWith: 'Begins with',
endsWith: 'Ends with',
containsText: 'Text contains',
notContainsText: 'Text does not contain',
equal: 'Equal to',
notEqual: 'Not equal to',
containsBlanks: 'Contains Blanks',
notContainsBlanks: 'Does not contain Blanks',
containsErrors: 'Contains Errors',
notContainsErrors: 'Does not contain Errors',
greaterThan: 'Greater than',
greaterThanOrEqual: 'Greater than or equal to',
lessThan: 'Less than',
lessThanOrEqual: 'Less than or equal to',
notBetween: 'Not between',
between: 'Between',
yesterday: 'Yesterday',
tomorrow: 'Tomorrow',
last7Days: 'Last 7 Days',
thisMonth: 'This Month',
lastMonth: 'Last Month',
nextMonth: 'Next Month',
thisWeek: 'This Week',
lastWeek: 'Last Week',
nextWeek: 'Next Week',
today: 'Today',
},
ruleType: {
highlightCell: 'Highlight Cell',
dataBar: 'Data Bar',
colorScale: 'Color Scale',
formula: 'Custom Formula',
iconSet: 'Icon Set',
},
subRuleType: {
uniqueValues: 'Unique Values',
duplicateValues: 'Duplicate Values',
text: 'Text',
timePeriod: 'Time Period',
number: 'Number',
average: 'Average',
},
valueType: {
num: 'Number',
min: 'Minimum',
max: 'Maximum',
percent: 'Percentage',
percentile: 'Percentile',
formula: 'Formula',
none: 'None',
},
errorMessage: {
notBlank: 'Condition can not be empty',
formulaError: 'Wrong formula',
rangeError: 'Bad selection',
},
permission: {
dialog: {
setStyleErr: 'The range is protected, and you do not have permission to set styles. To set styles, please contact the creator.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Formato condicional',
menu: {
manageConditionalFormatting: 'Gestionar formato condicional',
createConditionalFormatting: 'Crear formato condicional',
clearRangeRules: 'Borrar reglas del rango seleccionado',
clearWorkSheetRules: 'Borrar reglas de toda la hoja',
},
form: {
lessThan: 'El valor debe ser menor que {0}',
lessThanOrEqual: 'El valor debe ser menor o igual que {0}',
greaterThan: 'El valor debe ser mayor que {0}',
greaterThanOrEqual: 'El valor debe ser mayor o igual que {0}',
},
iconSet: {
direction: 'Dirección',
shape: 'Forma',
rank: 'Rango',
rule: 'Regla',
icon: 'Icono',
type: 'Tipo',
value: 'Valor',
reverseIconOrder: 'Invertir orden de iconos',
and: 'Y',
when: 'Cuando',
onlyShowIcon: 'Mostrar solo icono',
noCellIcon: 'Sin icono de celda',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Crear regla',
clear: 'Borrar todas las reglas',
range: 'Aplicar rango',
styleType: 'Tipo de estilo',
submit: 'Enviar',
cancel: 'Cancelar',
rankAndAverage: 'Superior/Inferior/Promedio',
styleRule: 'Regla de estilo',
isNotBottom: 'Superior',
isBottom: 'Inferior',
greaterThanAverage: 'Mayor que el promedio',
lessThanAverage: 'Menor que el promedio',
medianValue: 'Valor mediano',
fillType: 'Tipo de relleno',
pureColor: 'Color sólido',
gradient: 'Gradiente',
colorSet: 'Conjunto de colores',
positive: 'Positivo',
native: 'Negativo',
workSheet: 'Toda la hoja',
selectedRange: 'Rango seleccionado',
managerRuleSelect: 'Gestionar reglas de {0}',
onlyShowDataBar: 'Mostrar solo barras de datos',
},
preview: {
describe: {
beginsWith: 'Empieza con {0}',
endsWith: 'Termina con {0}',
containsText: 'El texto contiene {0}',
notContainsText: 'El texto no contiene {0}',
equal: 'Igual a {0}',
notEqual: 'Distinto de {0}',
containsBlanks: 'Contiene espacios en blanco',
notContainsBlanks: 'No contiene espacios en blanco',
containsErrors: 'Contiene errores',
notContainsErrors: 'No contiene errores',
greaterThan: 'Mayor que {0}',
greaterThanOrEqual: 'Mayor o igual que {0}',
lessThan: 'Menor que {0}',
lessThanOrEqual: 'Menor o igual que {0}',
notBetween: 'No está entre {0} y {1}',
between: 'Entre {0} y {1}',
yesterday: 'Ayer',
tomorrow: 'Mañana',
last7Days: 'Últimos 7 días',
thisMonth: 'Este mes',
lastMonth: 'Mes pasado',
nextMonth: 'Mes siguiente',
thisWeek: 'Esta semana',
lastWeek: 'Semana pasada',
nextWeek: 'Semana siguiente',
today: 'Hoy',
topN: 'Principales {0}',
bottomN: 'Inferior {0}',
topNPercent: 'Principales {0}%',
bottomNPercent: 'Inferior {0}%',
},
},
operator: {
beginsWith: 'Empieza con',
endsWith: 'Termina con',
containsText: 'El texto contiene',
notContainsText: 'El texto no contiene',
equal: 'Igual a',
notEqual: 'Distinto de',
containsBlanks: 'Contiene espacios en blanco',
notContainsBlanks: 'No contiene espacios en blanco',
containsErrors: 'Contiene errores',
notContainsErrors: 'No contiene errores',
greaterThan: 'Mayor que',
greaterThanOrEqual: 'Mayor o igual que',
lessThan: 'Menor que',
lessThanOrEqual: 'Menor o igual que',
notBetween: 'No está entre',
between: 'Entre',
yesterday: 'Ayer',
tomorrow: 'Mañana',
last7Days: 'Últimos 7 días',
thisMonth: 'Este mes',
lastMonth: 'Mes pasado',
nextMonth: 'Mes siguiente',
thisWeek: 'Esta semana',
lastWeek: 'Semana pasada',
nextWeek: 'Semana siguiente',
today: 'Hoy',
},
ruleType: {
highlightCell: 'Resaltar celda',
dataBar: 'Barra de datos',
colorScale: 'Escala de colores',
formula: 'Fórmula personalizada',
iconSet: 'Conjunto de iconos',
},
subRuleType: {
uniqueValues: 'Valores únicos',
duplicateValues: 'Valores duplicados',
text: 'Texto',
timePeriod: 'Periodo de tiempo',
number: 'Número',
average: 'Promedio',
},
valueType: {
num: 'Número',
min: 'Mínimo',
max: 'Máximo',
percent: 'Porcentaje',
percentile: 'Percentil',
formula: 'Fórmula',
none: 'Ninguno',
},
errorMessage: {
notBlank: 'La condición no puede estar vacía',
formulaError: 'Fórmula incorrecta',
rangeError: 'Selección incorrecta',
},
permission: {
dialog: {
setStyleErr: 'El rango está protegido y no tienes permiso para establecer estilos. Para establecer estilos, contacta al creador.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'قالب‌بندی شرطی',
menu: {
manageConditionalFormatting: 'مدیریت قالب‌بندی شرطی',
createConditionalFormatting: 'ایجاد قالب‌بندی شرطی',
clearRangeRules: 'پاک کردن قوانین برای محدوده انتخاب شده',
clearWorkSheetRules: 'پاک کردن قوانین برای کل برگ',
},
form: {
lessThan: 'مقدار باید کمتر از {0} باشد',
lessThanOrEqual: 'مقدار باید کمتر از یا برابر با {0} باشد',
greaterThan: 'مقدار باید بزرگتر از {0} باشد',
greaterThanOrEqual: 'مقدار باید بزرگتر از یا برابر با {0} باشد',
},
iconSet: {
direction: 'جهت',
shape: 'شکل',
rank: 'رتبه',
rule: 'قانون',
icon: 'آیکون',
type: 'نوع',
value: 'مقدار',
reverseIconOrder: 'معکوس کردن ترتیب آیکون',
and: 'و',
when: 'هنگامی که',
onlyShowIcon: 'فقط نمایش آیکون',
noCellIcon: 'بدون نماد سلول',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'ایجاد قانون',
clear: 'پاک کردن همه قوانین',
range: 'اعمال محدوده',
styleType: 'نوع سبک',
submit: 'ارسال',
cancel: 'انصراف',
rankAndAverage: 'بالا/پایین/میانگین',
styleRule: 'قانون سبک',
isNotBottom: 'بالا',
isBottom: 'پایین',
greaterThanAverage: 'بزرگتر از میانگین',
lessThanAverage: 'کوچکتر از میانگین',
medianValue: 'مقدار میانه',
fillType: 'نوع پر کردن',
pureColor: 'رنگ جامد',
gradient: 'شیب رنگی',
colorSet: 'مجموعه رنگ',
positive: 'مثبت',
native: 'منفی',
workSheet: 'کل برگ',
selectedRange: 'محدوده انتخاب شده',
managerRuleSelect: 'مدیریت {0} قوانین',
onlyShowDataBar: 'فقط نمایش نوار داده',
},
preview: {
describe: {
beginsWith: 'با {0} شروع می‌شود',
endsWith: 'با {0} پایان می‌یابد',
containsText: 'شامل متن {0} است',
notContainsText: 'شامل متن {0} نیست',
equal: 'برابر است با {0}',
notEqual: 'برابر نیست با {0}',
containsBlanks: 'شامل مقادیر خالی است',
notContainsBlanks: 'شامل مقادیر خالی نیست',
containsErrors: 'شامل خطاها است',
notContainsErrors: 'شامل خطاها نیست',
greaterThan: 'بزرگتر از {0}',
greaterThanOrEqual: 'بزرگتر از یا برابر با {0}',
lessThan: 'کوچکتر از {0}',
lessThanOrEqual: 'کوچکتر از یا برابر با {0}',
notBetween: 'بین {0} و {1} نیست',
between: 'بین {0} و {1}',
yesterday: 'دیروز',
tomorrow: 'فردا',
last7Days: '7 روز گذشته',
thisMonth: 'این ماه',
lastMonth: 'ماه گذشته',
nextMonth: 'ماه آینده',
thisWeek: 'این هفته',
lastWeek: 'هفته گذشته',
nextWeek: 'هفته آینده',
today: 'امروز',
topN: '{0} برتر',
bottomN: '{0} پایین‌تر',
topNPercent: '{0}% برتر',
bottomNPercent: '{0}% پایین‌تر',
},
},
operator: {
beginsWith: 'با ... شروع می‌شود',
endsWith: 'با ... پایان می‌یابد',
containsText: 'شامل متن ... است',
notContainsText: 'شامل متن ... نیست',
equal: 'برابر است با',
notEqual: 'برابر نیست با',
containsBlanks: 'شامل مقادیر خالی است',
notContainsBlanks: 'شامل مقادیر خالی نیست',
containsErrors: 'شامل خطاها است',
notContainsErrors: 'شامل خطاها نیست',
greaterThan: 'بزرگتر از',
greaterThanOrEqual: 'بزرگتر از یا برابر با',
lessThan: 'کوچکتر از',
lessThanOrEqual: 'کوچکتر از یا برابر با',
notBetween: 'بین ... و ... نیست',
between: 'بین ... و ...',
yesterday: 'دیروز',
tomorrow: 'فردا',
last7Days: '7 روز گذشته',
thisMonth: 'این ماه',
lastMonth: 'ماه گذشته',
nextMonth: 'ماه آینده',
thisWeek: 'این هفته',
lastWeek: 'هفته گذشته',
nextWeek: 'هفته آینده',
today: 'امروز',
},
ruleType: {
highlightCell: 'برجسته کردن سلول',
dataBar: 'نوار داده',
colorScale: 'مقیاس رنگی',
formula: 'فرمول سفارشی',
iconSet: 'مجموعه آیکون',
},
subRuleType: {
uniqueValues: 'مقادیر منحصربه‌فرد',
duplicateValues: 'مقادیر تکراری',
text: 'متن',
timePeriod: 'دوره زمانی',
number: 'عدد',
average: 'میانگین',
},
valueType: {
num: 'عدد',
min: 'حداقل',
max: 'حداکثر',
percent: 'درصد',
percentile: 'درصد صدک',
formula: 'فرمول',
none: 'هیچ کدام',
},
errorMessage: {
notBlank: 'شرط نمی‌تواند خالی باشد',
formulaError: 'فرمول اشتباه',
rangeError: 'انتخاب نامعتبر',
},
permission: {
dialog: {
setStyleErr: 'محدوده محافظت شده است و شما مجوز تنظیم سبک‌ها را ندارید. برای تنظیم سبک‌ها، لطفا با سازنده تماس بگیرید.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Mise en forme conditionnelle',
menu: {
manageConditionalFormatting: 'Gérer la mise en forme conditionnelle',
createConditionalFormatting: 'Créer une mise en forme conditionnelle',
clearRangeRules: 'Effacer les règles pour la plage sélectionnée',
clearWorkSheetRules: 'Effacer les règles pour toute la feuille',
},
form: {
lessThan: 'La valeur doit être inférieure à {0}',
lessThanOrEqual: 'La valeur doit être inférieure ou égale à {0}',
greaterThan: 'La valeur doit être supérieure à {0}',
greaterThanOrEqual: 'La valeur doit être supérieure ou égale à {0}',
},
iconSet: {
direction: 'Direction',
shape: 'Forme',
rank: 'Rang',
rule: 'Règle',
icon: 'Icône',
type: 'Type',
value: 'Valeur',
reverseIconOrder: 'Inverser l\'ordre des icônes',
and: 'Et',
when: 'Quand',
onlyShowIcon: 'Afficher uniquement l\'icône',
noCellIcon: 'Aucune icône de cellule',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Créer une règle',
clear: 'Effacer toutes les règles',
range: 'Appliquer la plage',
styleType: 'Type de style',
submit: 'Soumettre',
cancel: 'Annuler',
rankAndAverage: 'Haut/Bas/Moyenne',
styleRule: 'Règle de style',
isNotBottom: 'Haut',
isBottom: 'Bas',
greaterThanAverage: 'Supérieur à la moyenne',
lessThanAverage: 'Inférieur à la moyenne',
medianValue: 'Valeur médiane',
fillType: 'Type de remplissage',
pureColor: 'Couleur unie',
gradient: 'Dégradé',
colorSet: 'Ensemble de couleurs',
positive: 'Positif',
native: 'Négatif',
workSheet: 'Toute la feuille',
selectedRange: 'Plage sélectionnée',
managerRuleSelect: 'Gérer les règles de {0}',
onlyShowDataBar: 'Afficher uniquement les barres de données',
},
preview: {
describe: {
beginsWith: 'Commence par {0}',
endsWith: 'Se termine par {0}',
containsText: 'Le texte contient {0}',
notContainsText: 'Le texte ne contient pas {0}',
equal: 'Égal à {0}',
notEqual: 'Différent de {0}',
containsBlanks: 'Contient des blancs',
notContainsBlanks: 'Ne contient pas de blancs',
containsErrors: 'Contient des erreurs',
notContainsErrors: 'Ne contient pas d\'erreurs',
greaterThan: 'Supérieur à {0}',
greaterThanOrEqual: 'Supérieur ou égal à {0}',
lessThan: 'Inférieur à {0}',
lessThanOrEqual: 'Inférieur ou égal à {0}',
notBetween: 'Pas entre {0} et {1}',
between: 'Entre {0} et {1}',
yesterday: 'Hier',
tomorrow: 'Demain',
last7Days: 'Les 7 derniers jours',
thisMonth: 'Ce mois-ci',
lastMonth: 'Le mois dernier',
nextMonth: 'Le mois prochain',
thisWeek: 'Cette semaine',
lastWeek: 'La semaine dernière',
nextWeek: 'La semaine prochaine',
today: 'Aujourd\'hui',
topN: 'Top {0}',
bottomN: 'Bas {0}',
topNPercent: 'Top {0}%',
bottomNPercent: 'Bas {0}%',
},
},
operator: {
beginsWith: 'Commence par',
endsWith: 'Se termine par',
containsText: 'Le texte contient',
notContainsText: 'Le texte ne contient pas',
equal: 'Égal à',
notEqual: 'Différent de',
containsBlanks: 'Contient des blancs',
notContainsBlanks: 'Ne contient pas de blancs',
containsErrors: 'Contient des erreurs',
notContainsErrors: 'Ne contient pas d\'erreurs',
greaterThan: 'Supérieur à',
greaterThanOrEqual: 'Supérieur ou égal à',
lessThan: 'Inférieur à',
lessThanOrEqual: 'Inférieur ou égal à',
notBetween: 'Pas entre',
between: 'Entre',
yesterday: 'Hier',
tomorrow: 'Demain',
last7Days: 'Les 7 derniers jours',
thisMonth: 'Ce mois-ci',
lastMonth: 'Le mois dernier',
nextMonth: 'Le mois prochain',
thisWeek: 'Cette semaine',
lastWeek: 'La semaine dernière',
nextWeek: 'La semaine prochaine',
today: 'Aujourd\'hui',
},
ruleType: {
highlightCell: 'Mettre en surbrillance la cellule',
dataBar: 'Barre de données',
colorScale: 'Échelle de couleurs',
formula: 'Formule personnalisée',
iconSet: 'Jeu d\'icônes',
},
subRuleType: {
uniqueValues: 'Valeurs uniques',
duplicateValues: 'Valeurs en double',
text: 'Texte',
timePeriod: 'Période',
number: 'Nombre',
average: 'Moyenne',
},
valueType: {
num: 'Nombre',
min: 'Minimum',
max: 'Maximum',
percent: 'Pourcentage',
percentile: 'Percentile',
formula: 'Formule',
none: 'Aucun',
},
errorMessage: {
notBlank: 'La condition ne peut pas être vide',
rangeError: 'Plage invalide',
formulaError: 'Formule incorrecte',
},
permission: {
dialog: {
setStyleErr: 'La plage est protégée, et vous n\'avez pas la permission de définir les styles. Pour définir les styles, veuillez contacter le créateur.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,179 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Format Bersyarat',
menu: {
manageConditionalFormatting: 'Kelola Format Bersyarat',
createConditionalFormatting: 'Buat Format Bersyarat',
clearRangeRules: 'Hapus Aturan untuk Rentang yang Dipilih',
clearWorkSheetRules: 'Hapus Aturan untuk Seluruh Lembar',
},
form: {
lessThan: 'Nilai harus kurang dari {0}',
lessThanOrEqual: 'Nilai harus kurang dari atau sama dengan {0}',
greaterThan: 'Nilai harus lebih besar dari {0}',
greaterThanOrEqual: 'Nilai harus lebih besar dari atau sama dengan {0}',
},
iconSet: {
direction: 'Arah',
shape: 'Bentuk',
rank: 'Peringkat',
rule: 'Aturan',
icon: 'Ikon',
type: 'Jenis',
value: 'Nilai',
reverseIconOrder: 'Balikkan Urutan Ikon',
and: 'Dan',
when: 'Ketika',
onlyShowIcon: 'Tampilkan Ikon Saja',
noCellIcon: 'Tanpa Ikon Sel',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Buat Aturan',
clear: 'Hapus Semua Aturan',
range: 'Terapkan Rentang',
styleType: 'Jenis Gaya',
submit: 'Kirim',
cancel: 'Batal',
rankAndAverage: 'Atas/Bawah/Rata-rata',
styleRule: 'Aturan Gaya',
isNotBottom: 'Atas',
isBottom: 'Bawah',
greaterThanAverage: 'Lebih Besar dari Rata-rata',
lessThanAverage: 'Lebih Kecil dari Rata-rata',
medianValue: 'Nilai Median',
fillType: 'Jenis Isian',
pureColor: 'Warna Solid',
gradient: 'Gradien',
colorSet: 'Set Warna',
positive: 'Positif',
native: 'Negatif',
workSheet: 'Seluruh Lembar',
selectedRange: 'Rentang yang Dipilih',
managerRuleSelect: 'Kelola {0} Aturan',
onlyShowDataBar: 'Tampilkan Bilah Data Saja',
},
preview: {
describe: {
beginsWith: 'Dimulai dengan {0}',
endsWith: 'Diakhiri dengan {0}',
containsText: 'Teks mengandung {0}',
notContainsText: 'Teks tidak mengandung {0}',
equal: 'Sama dengan {0}',
notEqual: 'Tidak sama dengan {0}',
containsBlanks: 'Mengandung Kosong',
notContainsBlanks: 'Tidak mengandung Kosong',
containsErrors: 'Mengandung Kesalahan',
notContainsErrors: 'Tidak mengandung Kesalahan',
greaterThan: 'Lebih besar dari {0}',
greaterThanOrEqual: 'Lebih besar dari atau sama dengan {0}',
lessThan: 'Lebih kecil dari {0}',
lessThanOrEqual: 'Lebih kecil dari atau sama dengan {0}',
notBetween: 'Tidak antara {0} dan {1}',
between: 'Antara {0} dan {1}',
yesterday: 'Kemarin',
tomorrow: 'Besok',
last7Days: '7 Hari Terakhir',
thisMonth: 'Bulan Ini',
lastMonth: 'Bulan Lalu',
nextMonth: 'Bulan Depan',
thisWeek: 'Minggu Ini',
lastWeek: 'Minggu Lalu',
nextWeek: 'Minggu Depan',
today: 'Hari Ini',
topN: 'Atas {0}',
bottomN: 'Bawah {0}',
topNPercent: 'Atas {0}%',
bottomNPercent: 'Bawah {0}%',
},
},
operator: {
beginsWith: 'Dimulai dengan',
endsWith: 'Diakhiri dengan',
containsText: 'Teks mengandung',
notContainsText: 'Teks tidak mengandung',
equal: 'Sama dengan',
notEqual: 'Tidak sama dengan',
containsBlanks: 'Mengandung Kosong',
notContainsBlanks: 'Tidak mengandung Kosong',
containsErrors: 'Mengandung Kesalahan',
notContainsErrors: 'Tidak mengandung Kesalahan',
greaterThan: 'Lebih besar dari',
greaterThanOrEqual: 'Lebih besar dari atau sama dengan',
lessThan: 'Lebih kecil dari',
lessThanOrEqual: 'Lebih kecil dari atau sama dengan',
notBetween: 'Tidak antara',
between: 'Antara',
yesterday: 'Kemarin',
tomorrow: 'Besok',
last7Days: '7 Hari Terakhir',
thisMonth: 'Bulan Ini',
lastMonth: 'Bulan Lalu',
nextMonth: 'Bulan Depan',
thisWeek: 'Minggu Ini',
lastWeek: 'Minggu Lalu',
nextWeek: 'Minggu Depan',
today: 'Hari Ini',
},
ruleType: {
highlightCell: 'Sorot Sel',
dataBar: 'Bilah Data',
colorScale: 'Skala Warna',
formula: 'Rumus Kustom',
iconSet: 'Set Ikon',
},
subRuleType: {
uniqueValues: 'Nilai Unik',
duplicateValues: 'Nilai Duplikat',
text: 'Teks',
timePeriod: 'Periode Waktu',
number: 'Angka',
average: 'Rata-rata',
},
valueType: {
num: 'Angka',
min: 'Minimum',
max: 'Maksimum',
percent: 'Persentase',
percentile: 'Persentil',
formula: 'Rumus',
none: 'Tidak ada',
},
errorMessage: {
notBlank: 'Kondisi tidak boleh kosong',
formulaError: 'Rumus salah',
rangeError: 'Pilihan buruk',
},
permission: {
dialog: {
setStyleErr: 'Rentang ini dilindungi, dan Anda tidak memiliki izin untuk mengatur gaya. Untuk mengatur gaya, silakan hubungi pembuatnya.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,179 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Formattazione Condizionale',
menu: {
manageConditionalFormatting: 'Gestisci Formattazione Condizionale',
createConditionalFormatting: 'Crea Formattazione Condizionale',
clearRangeRules: 'Cancella Regole per Intervallo Selezionato',
clearWorkSheetRules: 'Cancella Regole per Intero Foglio',
},
form: {
lessThan: 'Il valore deve essere minore di {0}',
lessThanOrEqual: 'Il valore deve essere minore o uguale a {0}',
greaterThan: 'Il valore deve essere maggiore di {0}',
greaterThanOrEqual: 'Il valore deve essere maggiore o uguale a {0}',
},
iconSet: {
direction: 'Direzione',
shape: 'Forma',
rank: 'Grado',
rule: 'Regola',
icon: 'Icona',
type: 'Tipo',
value: 'Valore',
reverseIconOrder: 'Inverti Ordine Icone',
and: 'E',
when: 'Quando',
onlyShowIcon: 'Mostra Solo Icona',
noCellIcon: 'Nessuna icona cella',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Crea Regola',
clear: 'Cancella Tutte le Regole',
range: 'Applica Intervallo',
styleType: 'Tipo di Stile',
submit: 'Invia',
cancel: 'Annulla',
rankAndAverage: 'Superiore/Inferiore/Media',
styleRule: 'Regola di Stile',
isNotBottom: 'Superiore',
isBottom: 'Inferiore',
greaterThanAverage: 'Maggiore della Media',
lessThanAverage: 'Minore della Media',
medianValue: 'Valore Mediano',
fillType: 'Tipo di Riempimento',
pureColor: 'Colore Solido',
gradient: 'Gradiente',
colorSet: 'Set di Colori',
positive: 'Positivo',
native: 'Negativo',
workSheet: 'Intero Foglio',
selectedRange: 'Intervallo Selezionato',
managerRuleSelect: 'Gestisci {0} Regole',
onlyShowDataBar: 'Mostra Solo Barre Dati',
},
preview: {
describe: {
beginsWith: 'Inizia con {0}',
endsWith: 'Termina con {0}',
containsText: 'Il testo contiene {0}',
notContainsText: 'Il testo non contiene {0}',
equal: 'Uguale a {0}',
notEqual: 'Diverso da {0}',
containsBlanks: 'Contiene Vuoti',
notContainsBlanks: 'Non contiene Vuoti',
containsErrors: 'Contiene Errori',
notContainsErrors: 'Non contiene Errori',
greaterThan: 'Maggiore di {0}',
greaterThanOrEqual: 'Maggiore o uguale a {0}',
lessThan: 'Minore di {0}',
lessThanOrEqual: 'Minore o uguale a {0}',
notBetween: 'Non compreso tra {0} e {1}',
between: 'Compreso tra {0} e {1}',
yesterday: 'Ieri',
tomorrow: 'Domani',
last7Days: 'Ultimi 7 Giorni',
thisMonth: 'Questo Mese',
lastMonth: 'Mese Scorso',
nextMonth: 'Mese Prossimo',
thisWeek: 'Questa Settimana',
lastWeek: 'Settimana Scorso',
nextWeek: 'Settimana Prossima',
today: 'Oggi',
topN: 'Superiore {0}',
bottomN: 'Inferiore {0}',
topNPercent: 'Superiore {0}%',
bottomNPercent: 'Inferiore {0}%',
},
},
operator: {
beginsWith: 'Inizia con',
endsWith: 'Termina con',
containsText: 'Il testo contiene',
notContainsText: 'Il testo non contiene',
equal: 'Uguale a',
notEqual: 'Diverso da',
containsBlanks: 'Contiene Vuoti',
notContainsBlanks: 'Non contiene Vuoti',
containsErrors: 'Contiene Errori',
notContainsErrors: 'Non contiene Errori',
greaterThan: 'Maggiore di',
greaterThanOrEqual: 'Maggiore o uguale a',
lessThan: 'Minore di',
lessThanOrEqual: 'Minore o uguale a',
notBetween: 'Non compreso tra',
between: 'Compreso tra',
yesterday: 'Ieri',
tomorrow: 'Domani',
last7Days: 'Ultimi 7 Giorni',
thisMonth: 'Questo Mese',
lastMonth: 'Mese Scorso',
nextMonth: 'Mese Prossimo',
thisWeek: 'Questa Settimana',
lastWeek: 'Settimana Scorso',
nextWeek: 'Settimana Prossima',
today: 'Oggi',
},
ruleType: {
highlightCell: 'Evidenzia Cella',
dataBar: 'Barra Dati',
colorScale: 'Scala Colori',
formula: 'Formula Personalizzata',
iconSet: 'Set di Icone',
},
subRuleType: {
uniqueValues: 'Valori Unici',
duplicateValues: 'Valori Duplicati',
text: 'Testo',
timePeriod: 'Periodo di Tempo',
number: 'Numero',
average: 'Media',
},
valueType: {
num: 'Numero',
min: 'Minimo',
max: 'Massimo',
percent: 'Percentuale',
percentile: 'Percentile',
formula: 'Formula',
none: 'Nessuno',
},
errorMessage: {
notBlank: 'La condizione non può essere vuota',
formulaError: 'Formula errata',
rangeError: 'Selezione non valida',
},
permission: {
dialog: {
setStyleErr: 'L\'intervallo è protetto e non hai l\'autorizzazione per impostare gli stili. Per impostare gli stili, contatta il creatore.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: '条件付き書式',
menu: {
manageConditionalFormatting: 'ルールの管理',
createConditionalFormatting: '新しいルール',
clearRangeRules: '選択したセルからルールをクリア',
clearWorkSheetRules: 'シート全体からルールをクリア',
},
form: {
lessThan: '{0} より小さい',
lessThanOrEqual: '{0} 以下',
greaterThan: '{0} より大きい',
greaterThanOrEqual: '{0} 以上',
},
iconSet: {
direction: '方向',
shape: '図形',
rank: '評価',
rule: 'ルール',
icon: 'アイコン',
type: '種類',
value: '値',
reverseIconOrder: 'アイコンの順序を逆にする',
and: 'かつ',
when: '次の場合',
onlyShowIcon: 'アイコンのみ表示',
noCellIcon: 'セル アイコンなし',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '≥',
lessThan: '<',
lessThanOrEqual: '≤',
},
panel: {
createRule: '新しいルール',
clear: 'ルールのクリア',
range: '適用先',
styleType: 'スタイルの種類',
submit: 'OK',
cancel: 'キャンセル',
rankAndAverage: '上位/下位/平均',
styleRule: '書式ルール',
isNotBottom: '上位',
isBottom: '下位',
greaterThanAverage: '平均より上',
lessThanAverage: '平均より下',
medianValue: '中央値',
fillType: '塗りつぶしの種類',
pureColor: '単色',
gradient: 'グラデーション',
colorSet: 'カラーセット',
positive: '正の値',
native: '負の値',
workSheet: 'このワークシート',
selectedRange: '現在の選択範囲',
managerRuleSelect: '{0} の書式ルールを表示',
onlyShowDataBar: '棒のみ表示',
},
preview: {
describe: {
beginsWith: '{0} で始まる',
endsWith: '{0} で終わる',
containsText: '{0} を含む',
notContainsText: '{0} を含まない',
equal: '{0} と等しい',
notEqual: '{0} と等しくない',
containsBlanks: '空白を含む',
notContainsBlanks: '空白を含まない',
containsErrors: 'エラーを含む',
notContainsErrors: 'エラーを含まない',
greaterThan: '{0} より大きい',
greaterThanOrEqual: '{0} 以上',
lessThan: '{0} より小さい',
lessThanOrEqual: '{0} 以下',
notBetween: '{0} と {1} の間以外',
between: '{0} と {1} の間',
yesterday: '昨日',
tomorrow: '明日',
last7Days: '過去 7 日間',
thisMonth: '今月',
lastMonth: '先月',
nextMonth: '来月',
thisWeek: '今週',
lastWeek: '先週',
nextWeek: '来週',
today: '今日',
topN: '上位 {0}',
bottomN: '下位 {0}',
topNPercent: '上位 {0}%',
bottomNPercent: '下位 {0}%',
},
},
operator: {
beginsWith: 'で始まる',
endsWith: 'で終わる',
containsText: 'を含む',
notContainsText: 'を含まない',
equal: 'と等しい',
notEqual: 'と等しくない',
containsBlanks: '空白を含む',
notContainsBlanks: '空白を含まない',
containsErrors: 'エラーを含む',
notContainsErrors: 'エラーを含まない',
greaterThan: 'より大きい',
greaterThanOrEqual: '以上',
lessThan: 'より小さい',
lessThanOrEqual: '以下',
notBetween: 'の間以外',
between: 'の間',
yesterday: '昨日',
tomorrow: '明日',
last7Days: '過去 7 日間',
thisMonth: '今月',
lastMonth: '先月',
nextMonth: '来月',
thisWeek: '今週',
lastWeek: '先週',
nextWeek: '来週',
today: '今日',
},
ruleType: {
highlightCell: 'セルの強調表示ルール',
dataBar: 'データバー',
colorScale: 'カラースケール',
formula: '数式を使用して、書式設定するセルを決定',
iconSet: 'アイコンセット',
},
subRuleType: {
uniqueValues: '一意の値',
duplicateValues: '重複する値',
text: '指定の値を含むテキスト',
timePeriod: '日付',
number: '数値',
average: '平均',
},
valueType: {
num: '数値',
min: '最小値',
max: '最大値',
percent: 'パーセント',
percentile: 'パーセンタイル',
formula: '数式',
none: 'なし',
},
errorMessage: {
notBlank: '条件を入力してください。',
formulaError: '数式が正しくありません。',
rangeError: '範囲が正しくありません。',
},
permission: {
dialog: {
setStyleErr: 'この範囲は保護されており、スタイルを変更する権限がありません。変更するには作成者に連絡してください。',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: '조건부 서식',
menu: {
manageConditionalFormatting: '조건부 서식 관리',
createConditionalFormatting: '조건부 서식 만들기',
clearRangeRules: '선택 영역 규칙 지우기',
clearWorkSheetRules: '시트 전체 규칙 지우기',
},
form: {
lessThan: '{0}보다 작음',
lessThanOrEqual: '{0} 이하',
greaterThan: '{0}보다 큼',
greaterThanOrEqual: '{0} 이상',
},
iconSet: {
direction: '방향',
shape: '모양',
rank: '순위',
rule: '규칙',
icon: '아이콘',
type: '유형',
value: '값',
reverseIconOrder: '아이콘 순서 반대로',
and: '그리고',
when: '조건',
onlyShowIcon: '아이콘만 표시',
noCellIcon: '셀 아이콘 없음',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '≥',
lessThan: '<',
lessThanOrEqual: '≤',
},
panel: {
createRule: '규칙 만들기',
clear: '모든 규칙 지우기',
range: '적용 범위',
styleType: '스타일 유형',
submit: '확인',
cancel: '취소',
rankAndAverage: '상위/하위/평균',
styleRule: '스타일 규칙',
isNotBottom: '상위',
isBottom: '하위',
greaterThanAverage: '평균 초과',
lessThanAverage: '평균 미만',
medianValue: '중앙값',
fillType: '채우기 유형',
pureColor: '단색',
gradient: '그라데이션',
colorSet: '색상 세트',
positive: '양수',
native: '음수',
workSheet: '시트 전체',
selectedRange: '선택 영역',
managerRuleSelect: '{0} 규칙 관리',
onlyShowDataBar: '데이터 막대만 표시',
},
preview: {
describe: {
beginsWith: '{0}로 시작',
endsWith: '{0}로 끝남',
containsText: '{0} 포함',
notContainsText: '{0} 미포함',
equal: '{0}와 같음',
notEqual: '{0}와 다름',
containsBlanks: '빈 셀 포함',
notContainsBlanks: '빈 셀 미포함',
containsErrors: '오류 포함',
notContainsErrors: '오류 미포함',
greaterThan: '{0}보다 큼',
greaterThanOrEqual: '{0} 이상',
lessThan: '{0}보다 작음',
lessThanOrEqual: '{0} 이하',
notBetween: '{0}와 {1} 사이 아님',
between: '{0}와 {1} 사이',
yesterday: '어제',
tomorrow: '내일',
last7Days: '최근 7일',
thisMonth: '이번 달',
lastMonth: '지난 달',
nextMonth: '다음 달',
thisWeek: '이번 주',
lastWeek: '지난 주',
nextWeek: '다음 주',
today: '오늘',
topN: '상위 {0}개',
bottomN: '하위 {0}개',
topNPercent: '상위 {0}%',
bottomNPercent: '하위 {0}%',
},
},
operator: {
beginsWith: '로 시작',
endsWith: '로 끝남',
containsText: '포함',
notContainsText: '미포함',
equal: '같음',
notEqual: '다름',
containsBlanks: '빈 셀 포함',
notContainsBlanks: '빈 셀 미포함',
containsErrors: '오류 포함',
notContainsErrors: '오류 미포함',
greaterThan: '보다 큼',
greaterThanOrEqual: '이상',
lessThan: '보다 작음',
lessThanOrEqual: '이하',
notBetween: '사이 아님',
between: '사이',
yesterday: '어제',
tomorrow: '내일',
last7Days: '최근 7일',
thisMonth: '이번 달',
lastMonth: '지난 달',
nextMonth: '다음 달',
thisWeek: '이번 주',
lastWeek: '지난 주',
nextWeek: '다음 주',
today: '오늘',
},
ruleType: {
highlightCell: '셀 강조',
dataBar: '데이터 막대',
colorScale: '색상 눈금',
formula: '사용자 지정 수식',
iconSet: '아이콘 집합',
},
subRuleType: {
uniqueValues: '고유 값',
duplicateValues: '중복 값',
text: '텍스트',
timePeriod: '기간',
number: '숫자',
average: '평균',
},
valueType: {
num: '숫자',
min: '최소값',
max: '최대값',
percent: '백분율',
percentile: '백분위수',
formula: '수식',
none: '없음',
},
errorMessage: {
notBlank: '조건을 비울 수 없습니다.',
formulaError: '잘못된 수식입니다.',
rangeError: '잘못된 선택 영역입니다.',
},
permission: {
dialog: {
setStyleErr: '해당 범위는 보호되어 있어 스타일 지정 권한이 없습니다. 스타일을 변경하려면 작성자에게 문의하세요.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,179 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Formatowanie warunkowe',
menu: {
manageConditionalFormatting: 'Zarządzaj formatowaniem warunkowym',
createConditionalFormatting: 'Utwórz formatowanie warunkowe',
clearRangeRules: 'Wyczyść reguły dla zaznaczonego zakresu',
clearWorkSheetRules: 'Wyczyść reguły dla całego arkusza',
},
form: {
lessThan: 'Wartość musi być mniejsza niż {0}',
lessThanOrEqual: 'Wartość musi być mniejsza niż lub równa {0}',
greaterThan: 'Wartość musi być większa niż {0}',
greaterThanOrEqual: 'Wartość musi być większa niż lub równa {0}',
},
iconSet: {
direction: 'Kierunek',
shape: 'Kształt',
rank: 'Ranga',
rule: 'Reguła',
icon: 'Ikona',
type: 'Typ',
value: 'Wartość',
reverseIconOrder: 'Odwróć kolejność ikon',
and: 'I',
when: 'Gdy',
onlyShowIcon: 'Pokaż tylko ikonę',
noCellIcon: 'Brak ikony komórki',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Utwórz regułę',
clear: 'Wyczyść wszystkie reguły',
range: 'Zakres zastosowania',
styleType: 'Typ stylu',
submit: 'Zastosuj',
cancel: 'Anuluj',
rankAndAverage: 'Góra/Dół/Średnia',
styleRule: 'Reguła stylu',
isNotBottom: 'Góra',
isBottom: 'Dół',
greaterThanAverage: 'Większe niż średnia',
lessThanAverage: 'Mniejsze niż średnia',
medianValue: 'Mediana',
fillType: 'Typ wypełnienia',
pureColor: 'Kolor jednolity',
gradient: 'Gradient',
colorSet: 'Zestaw kolorów',
positive: 'Dodatnie',
native: 'Ujemne',
workSheet: 'Cały arkusz',
selectedRange: 'Zaznaczony zakres',
managerRuleSelect: 'Zarządzaj regułami: {0}',
onlyShowDataBar: 'Pokaż tylko paski danych',
},
preview: {
describe: {
beginsWith: 'Zaczyna się od {0}',
endsWith: 'Kończy się na {0}',
containsText: 'Tekst zawiera {0}',
notContainsText: 'Tekst nie zawiera {0}',
equal: 'Równe {0}',
notEqual: 'Różne od {0}',
containsBlanks: 'Zawiera puste',
notContainsBlanks: 'Nie zawiera pustych',
containsErrors: 'Zawiera błędy',
notContainsErrors: 'Nie zawiera błędów',
greaterThan: 'Większe niż {0}',
greaterThanOrEqual: 'Większe niż lub równe {0}',
lessThan: 'Mniejsze niż {0}',
lessThanOrEqual: 'Mniejsze niż lub równe {0}',
notBetween: 'Nie między {0} a {1}',
between: 'Między {0} a {1}',
yesterday: 'Wczoraj',
tomorrow: 'Jutro',
last7Days: 'Ostatnie 7 dni',
thisMonth: 'Ten miesiąc',
lastMonth: 'Poprzedni miesiąc',
nextMonth: 'Następny miesiąc',
thisWeek: 'Ten tydzień',
lastWeek: 'Poprzedni tydzień',
nextWeek: 'Następny tydzień',
today: 'Dzisiaj',
topN: 'Górne {0}',
bottomN: 'Ostatnie {0}',
topNPercent: 'Górne {0}%',
bottomNPercent: 'Ostatnie {0}%',
},
},
operator: {
beginsWith: 'Zaczyna się od',
endsWith: 'Kończy się na',
containsText: 'Tekst zawiera',
notContainsText: 'Tekst nie zawiera',
equal: 'Równe',
notEqual: 'Różne od',
containsBlanks: 'Zawiera puste',
notContainsBlanks: 'Nie zawiera pustych',
containsErrors: 'Zawiera błędy',
notContainsErrors: 'Nie zawiera błędów',
greaterThan: 'Większe niż',
greaterThanOrEqual: 'Większe niż lub równe',
lessThan: 'Mniejsze niż',
lessThanOrEqual: 'Mniejsze niż lub równe',
notBetween: 'Nie między',
between: 'Między',
yesterday: 'Wczoraj',
tomorrow: 'Jutro',
last7Days: 'Ostatnie 7 dni',
thisMonth: 'Ten miesiąc',
lastMonth: 'Poprzedni miesiąc',
nextMonth: 'Następny miesiąc',
thisWeek: 'Ten tydzień',
lastWeek: 'Poprzedni tydzień',
nextWeek: 'Następny tydzień',
today: 'Dzisiaj',
},
ruleType: {
highlightCell: 'Wyróżnij komórkę',
dataBar: 'Pasek danych',
colorScale: 'Skala kolorów',
formula: 'Niestandardowa formuła',
iconSet: 'Zestaw ikon',
},
subRuleType: {
uniqueValues: 'Unikalne wartości',
duplicateValues: 'Zduplikowane wartości',
text: 'Tekst',
timePeriod: 'Okres',
number: 'Liczba',
average: 'Średnia',
},
valueType: {
num: 'Liczba',
min: 'Minimum',
max: 'Maksimum',
percent: 'Procent',
percentile: 'Percentyl',
formula: 'Formuła',
none: 'Brak',
},
errorMessage: {
notBlank: 'Warunek nie może być pusty',
formulaError: 'Błędna formuła',
rangeError: 'Nieprawidłowy wybór',
},
permission: {
dialog: {
setStyleErr: 'Zakres jest chroniony i nie masz uprawnień do ustawiania stylów. Aby ustawić style, skontaktuj się z twórcą.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,179 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Formatação Condicional',
menu: {
manageConditionalFormatting: 'Gerenciar Formatação Condicional',
createConditionalFormatting: 'Criar Formatação Condicional',
clearRangeRules: 'Limpar Regras do Intervalo Selecionado',
clearWorkSheetRules: 'Limpar Regras da Planilha Inteira',
},
form: {
lessThan: 'O valor deve ser menor que {0}',
lessThanOrEqual: 'O valor deve ser menor ou igual a {0}',
greaterThan: 'O valor deve ser maior que {0}',
greaterThanOrEqual: 'O valor deve ser maior ou igual a {0}',
},
iconSet: {
direction: 'Direção',
shape: 'Forma',
rank: 'Classificação',
rule: 'Regra',
icon: 'Ícone',
type: 'Tipo',
value: 'Valor',
reverseIconOrder: 'Inverter Ordem dos Ícones',
and: 'E',
when: 'Quando',
onlyShowIcon: 'Mostrar Apenas Ícone',
noCellIcon: 'Sem ícone de célula',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Criar Regra',
clear: 'Limpar Todas as Regras',
range: 'Aplicar ao Intervalo',
styleType: 'Tipo de Estilo',
submit: 'Aplicar',
cancel: 'Cancelar',
rankAndAverage: 'Superior/Inferior/Média',
styleRule: 'Regra de Estilo',
isNotBottom: 'Superior',
isBottom: 'Inferior',
greaterThanAverage: 'Maior que a Média',
lessThanAverage: 'Menor que a Média',
medianValue: 'Valor Mediano',
fillType: 'Tipo de Preenchimento',
pureColor: 'Cor Sólida',
gradient: 'Gradiente',
colorSet: 'Conjunto de Cores',
positive: 'Positivo',
native: 'Negativo',
workSheet: 'Planilha Inteira',
selectedRange: 'Intervalo Selecionado',
managerRuleSelect: 'Gerenciar Regras de {0}',
onlyShowDataBar: 'Mostrar Apenas Barras de Dados',
},
preview: {
describe: {
beginsWith: 'Começa com {0}',
endsWith: 'Termina com {0}',
containsText: 'O texto contém {0}',
notContainsText: 'O texto não contém {0}',
equal: 'Igual a {0}',
notEqual: 'Diferente de {0}',
containsBlanks: 'Contém Em Branco',
notContainsBlanks: 'Não Contém Em Branco',
containsErrors: 'Contém Erros',
notContainsErrors: 'Não Contém Erros',
greaterThan: 'Maior que {0}',
greaterThanOrEqual: 'Maior ou igual a {0}',
lessThan: 'Menor que {0}',
lessThanOrEqual: 'Menor ou igual a {0}',
notBetween: 'Não está entre {0} e {1}',
between: 'Entre {0} e {1}',
yesterday: 'Ontem',
tomorrow: 'Amanhã',
last7Days: 'Últimos 7 Dias',
thisMonth: 'Este Mês',
lastMonth: 'Mês Passado',
nextMonth: 'Próximo Mês',
thisWeek: 'Esta Semana',
lastWeek: 'Semana Passada',
nextWeek: 'Próxima Semana',
today: 'Hoje',
topN: 'Superior {0}',
bottomN: 'Inferior {0}',
topNPercent: 'Superior {0}%',
bottomNPercent: 'Inferior {0}%',
},
},
operator: {
beginsWith: 'Começa com',
endsWith: 'Termina com',
containsText: 'O texto contém',
notContainsText: 'O texto não contém',
equal: 'Igual a',
notEqual: 'Diferente de',
containsBlanks: 'Contém Em Branco',
notContainsBlanks: 'Não Contém Em Branco',
containsErrors: 'Contém Erros',
notContainsErrors: 'Não Contém Erros',
greaterThan: 'Maior que',
greaterThanOrEqual: 'Maior ou igual a',
lessThan: 'Menor que',
lessThanOrEqual: 'Menor ou igual a',
notBetween: 'Não está entre',
between: 'Entre',
yesterday: 'Ontem',
tomorrow: 'Amanhã',
last7Days: 'Últimos 7 Dias',
thisMonth: 'Este Mês',
lastMonth: 'Mês Passado',
nextMonth: 'Próximo Mês',
thisWeek: 'Esta Semana',
lastWeek: 'Semana Passada',
nextWeek: 'Próxima Semana',
today: 'Hoje',
},
ruleType: {
highlightCell: 'Realçar Célula',
dataBar: 'Barra de Dados',
colorScale: 'Escala de Cores',
formula: 'Fórmula Personalizada',
iconSet: 'Conjunto de Ícones',
},
subRuleType: {
uniqueValues: 'Valores Únicos',
duplicateValues: 'Valores Duplicados',
text: 'Texto',
timePeriod: 'Período',
number: 'Número',
average: 'Média',
},
valueType: {
num: 'Número',
min: 'Mínimo',
max: 'Máximo',
percent: 'Porcentagem',
percentile: 'Percentil',
formula: 'Fórmula',
none: 'Nenhum',
},
errorMessage: {
notBlank: 'A condição não pode estar vazia',
formulaError: 'Fórmula incorreta',
rangeError: 'Seleção inválida',
},
permission: {
dialog: {
setStyleErr: 'O intervalo está protegido e você não tem permissão para definir estilos. Para definir estilos, entre em contato com o criador.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Условное форматирование',
menu: {
manageConditionalFormatting: 'Управление условным форматированием',
createConditionalFormatting: 'Создание условного форматирования',
clearRangeRules: 'Очистить правила для выбранного диапазона',
clearWorkSheetRules: 'Очистить правила для всего листа',
},
form: {
lessThan: 'Значение должно быть меньше {0}',
lessThanOrEqual: 'Значение должно быть меньше или равно {0}',
greaterThan: 'Значение должно быть больше {0}',
greaterThanOrEqual: 'Значение должно быть больше или равно {0}',
},
iconSet: {
direction: 'Направление',
shape: 'Форма',
rank: 'Ранг',
rule: 'Правило',
icon: 'Иконка',
type: 'Тип',
value: 'Значение',
reverseIconOrder: 'Обратный порядок иконок',
and: 'И',
when: 'Когда',
onlyShowIcon: 'Показывать только иконку',
noCellIcon: 'Без значка ячейки',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Создать правило',
clear: 'Очистить все правила',
range: 'Применить диапазон',
styleType: 'Тип стиля',
submit: 'Подтвердить',
cancel: 'Отменить',
rankAndAverage: 'Верх/Низ/Среднее',
styleRule: 'Правило стиля',
isNotBottom: 'Верх',
isBottom: 'Низ',
greaterThanAverage: 'Больше среднего',
lessThanAverage: 'Меньше среднего',
medianValue: 'Медианное значение',
fillType: 'Тип заполнения',
pureColor: 'Сплошной цвет',
gradient: 'Градиент',
colorSet: 'Набор цветов',
positive: 'Положительный',
native: 'Отрицательный',
workSheet: 'Весь лист',
selectedRange: 'Выбранный диапазон',
managerRuleSelect: 'Управление правилами {0}',
onlyShowDataBar: 'Показывать только гистограммы данных',
},
preview: {
describe: {
beginsWith: 'Начинается с {0}',
endsWith: 'Заканчивается на {0}',
containsText: 'Содержит текст {0}',
notContainsText: 'Не содержит текст {0}',
equal: 'Равно {0}',
notEqual: 'Не равно {0}',
containsBlanks: 'Содержит пустые ячейки',
notContainsBlanks: 'Не содержит пустых ячеек',
containsErrors: 'Содержит ошибки',
notContainsErrors: 'Не содержит ошибок',
greaterThan: 'Больше чем {0}',
greaterThanOrEqual: 'Больше или равно {0}',
lessThan: 'Меньше чем {0}',
lessThanOrEqual: 'Меньше или равно {0}',
notBetween: 'Не между {0} и {1}',
between: 'Между {0} и {1}',
yesterday: 'Вчера',
tomorrow: 'Завтра',
last7Days: 'Последние 7 дней',
thisMonth: 'Этот месяц',
lastMonth: 'Прошлый месяц',
nextMonth: 'Следующий месяц',
thisWeek: 'Эта неделя',
lastWeek: 'Прошлая неделя',
nextWeek: 'Следующая неделя',
today: 'Сегодня',
topN: 'Топ {0}',
bottomN: 'Низ {0}',
topNPercent: 'Топ {0}%',
bottomNPercent: 'Низ {0}%',
},
},
operator: {
beginsWith: 'Начинается с',
endsWith: 'Заканчивается на',
containsText: 'Содержит текст',
notContainsText: 'Не содержит текст',
equal: 'Равно',
notEqual: 'Не равно',
containsBlanks: 'Содержит пустые ячейки',
notContainsBlanks: 'Не содержит пустых ячеек',
containsErrors: 'Содержит ошибки',
notContainsErrors: 'Не содержит ошибок',
greaterThan: 'Больше чем',
greaterThanOrEqual: 'Больше или равно',
lessThan: 'Меньше чем',
lessThanOrEqual: 'Меньше или равно',
notBetween: 'Не между',
between: 'Между',
yesterday: 'Вчера',
tomorrow: 'Завтра',
last7Days: 'Последние 7 дней',
thisMonth: 'Этот месяц',
lastMonth: 'Прошлый месяц',
nextMonth: 'Следующий месяц',
thisWeek: 'Эта неделя',
lastWeek: 'Прошлая неделя',
nextWeek: 'Следующая неделя',
today: 'Сегодня',
},
ruleType: {
highlightCell: 'Выделение ячеек',
dataBar: 'Гистограмма данных',
colorScale: 'Цветовая шкала',
formula: 'Пользовательская формула',
iconSet: 'Набор иконок',
},
subRuleType: {
uniqueValues: 'Уникальные значения',
duplicateValues: 'Дублирующиеся значения',
text: 'Текст',
timePeriod: 'Период времени',
number: 'Число',
average: 'Среднее',
},
valueType: {
num: 'Число',
min: 'Минимум',
max: 'Максимум',
percent: 'Процент',
percentile: 'Процентиль',
formula: 'Формула',
none: 'Нет',
},
errorMessage: {
notBlank: 'Условие не может быть пустым',
formulaError: 'Ошибка формулы',
rangeError: 'Некорректный выбор',
},
permission: {
dialog: {
setStyleErr: 'Диапазон защищен, и у вас нет разрешения на установку стилей. Для установки стилей свяжитесь с создателем.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,179 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Podmienené formátovanie',
menu: {
manageConditionalFormatting: 'Spravovať podmienené formátovanie',
createConditionalFormatting: 'Vytvoriť podmienené formátovanie',
clearRangeRules: 'Vymazať pravidlá pre vybraný rozsah',
clearWorkSheetRules: 'Vymazať pravidlá pre celý hárok',
},
form: {
lessThan: 'Hodnota musí byť menšia ako {0}',
lessThanOrEqual: 'Hodnota musí byť menšia alebo rovná {0}',
greaterThan: 'Hodnota musí byť väčšia ako {0}',
greaterThanOrEqual: 'Hodnota musí byť väčšia alebo rovná {0}',
},
iconSet: {
direction: 'Smer',
shape: 'Tvar',
rank: 'Poradie',
rule: 'Pravidlo',
icon: 'Ikona',
type: 'Typ',
value: 'Hodnota',
reverseIconOrder: 'Obrátiť poradie ikon',
and: 'A',
when: 'Keď',
onlyShowIcon: 'Zobraziť iba ikonu',
noCellIcon: 'Bez ikony bunky',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Vytvoriť pravidlo',
clear: 'Vymazať všetky pravidlá',
range: 'Rozsah použitia',
styleType: 'Typ štýlu',
submit: 'Použiť',
cancel: 'Zrušiť',
rankAndAverage: 'Horné/Dolné/Priemer',
styleRule: 'Pravidlo štýlu',
isNotBottom: 'Horné',
isBottom: 'Dolné',
greaterThanAverage: 'Nad priemerom',
lessThanAverage: 'Pod priemerom',
medianValue: 'Medián',
fillType: 'Typ výplne',
pureColor: 'Jednofarebné',
gradient: 'Prechod',
colorSet: 'Súprava farieb',
positive: 'Pozitívne',
native: 'Negatívne',
workSheet: 'Celý hárok',
selectedRange: 'Vybraný rozsah',
managerRuleSelect: 'Spravovať {0} pravidiel',
onlyShowDataBar: 'Zobraziť iba dátové pruhy',
},
preview: {
describe: {
beginsWith: 'Začína s {0}',
endsWith: 'Končí na {0}',
containsText: 'Text obsahuje {0}',
notContainsText: 'Text neobsahuje {0}',
equal: 'Rovná sa {0}',
notEqual: 'Nerovná sa {0}',
containsBlanks: 'Obsahuje prázdne',
notContainsBlanks: 'Neobsahuje prázdne',
containsErrors: 'Obsahuje chyby',
notContainsErrors: 'Neobsahuje chyby',
greaterThan: 'Väčšie ako {0}',
greaterThanOrEqual: 'Väčšie alebo rovné {0}',
lessThan: 'Menšie ako {0}',
lessThanOrEqual: 'Menšie alebo rovné {0}',
notBetween: 'Nie medzi {0} a {1}',
between: 'Medzi {0} a {1}',
yesterday: 'Včera',
tomorrow: 'Zajtra',
last7Days: 'Posledných 7 dní',
thisMonth: 'Tento mesiac',
lastMonth: 'Minulý mesiac',
nextMonth: 'Budúci mesiac',
thisWeek: 'Tento týždeň',
lastWeek: 'Minulý týždeň',
nextWeek: 'Budúci týždeň',
today: 'Dnes',
topN: 'Horných {0}',
bottomN: 'Dolných {0}',
topNPercent: 'Horných {0}%',
bottomNPercent: 'Dolných {0}%',
},
},
operator: {
beginsWith: 'Začína s',
endsWith: 'Končí na',
containsText: 'Text obsahuje',
notContainsText: 'Text neobsahuje',
equal: 'Rovná sa',
notEqual: 'Nerovná sa',
containsBlanks: 'Obsahuje prázdne',
notContainsBlanks: 'Neobsahuje prázdne',
containsErrors: 'Obsahuje chyby',
notContainsErrors: 'Neobsahuje chyby',
greaterThan: 'Väčšie ako',
greaterThanOrEqual: 'Väčšie alebo rovné',
lessThan: 'Menšie ako',
lessThanOrEqual: 'Menšie alebo rovné',
notBetween: 'Nie medzi',
between: 'Medzi',
yesterday: 'Včera',
tomorrow: 'Zajtra',
last7Days: 'Posledných 7 dní',
thisMonth: 'Tento mesiac',
lastMonth: 'Minulý mesiac',
nextMonth: 'Budúci mesiac',
thisWeek: 'Tento týždeň',
lastWeek: 'Minulý týždeň',
nextWeek: 'Budúci týždeň',
today: 'Dnes',
},
ruleType: {
highlightCell: 'Zvýrazniť bunku',
dataBar: 'Dátový pruh',
colorScale: 'Farebná škála',
formula: 'Vlastný vzorec',
iconSet: 'Súprava ikon',
},
subRuleType: {
uniqueValues: 'Jedinečné hodnoty',
duplicateValues: 'Duplicitné hodnoty',
text: 'Text',
timePeriod: 'Časové obdobie',
number: 'Číslo',
average: 'Priemer',
},
valueType: {
num: 'Číslo',
min: 'Minimum',
max: 'Maximum',
percent: 'Percento',
percentile: 'Percentil',
formula: 'Vzorec',
none: 'Žiadne',
},
errorMessage: {
notBlank: 'Podmienka nemôže byť prázdna',
formulaError: 'Nesprávny vzorec',
rangeError: 'Nesprávny výber',
},
permission: {
dialog: {
setStyleErr: 'Rozsah je chránený a nemáte oprávnenie nastavovať štýly. Ak chcete nastavovať štýly, kontaktujte autora.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,20 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { LocaleLeafKeys } from '@univerjs/core';
import type enUS from './en-US';
export type LocaleKey = LocaleLeafKeys<typeof enUS>;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: 'Định dạng có điều kiện',
menu: {
manageConditionalFormatting: 'Quản lý định dạng có điều kiện',
createConditionalFormatting: 'Tạo định dạng có điều kiện mới',
clearRangeRules: 'Xóa quy tắc cho vùng chọn',
clearWorkSheetRules: 'Xóa quy tắc cho toàn bộ bảng tính',
},
form: {
lessThan: 'Giá trị phải nhỏ hơn {0}',
lessThanOrEqual: 'Giá trị phải nhỏ hơn hoặc bằng {0}',
greaterThan: 'Giá trị phải lớn hơn {0}',
greaterThanOrEqual: 'Giá trị phải lớn hơn hoặc bằng {0}',
},
iconSet: {
direction: 'Hướng',
shape: 'Hình dạng',
rank: 'Hạng',
rule: 'Quy tắc',
icon: 'Biểu tượng',
type: 'Loại',
value: 'Giá trị',
reverseIconOrder: 'Đảo ngược thứ tự biểu tượng',
and: 'Và',
when: 'Khi giá trị',
onlyShowIcon: 'Chỉ hiển thị biểu tượng',
noCellIcon: 'Không có biểu tượng ô',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: 'Tạo quy tắc mới',
clear: 'Xóa tất cả quy tắc',
range: 'Phạm vi áp dụng',
styleType: 'Loại kiểu',
submit: 'Xác nhận',
cancel: 'Hủy bỏ',
rankAndAverage: 'Đầu / Cuối / Giá trị trung bình',
styleRule: 'Quy tắc kiểu',
isNotBottom: 'Đầu',
isBottom: 'Cuối',
greaterThanAverage: 'Lớn hơn giá trị trung bình',
lessThanAverage: 'Nhỏ hơn giá trị trung bình',
medianValue: 'Giá trị trung vị',
fillType: 'Cách thức điền',
pureColor: 'Màu thuần',
gradient: 'Chuyển màu',
colorSet: 'Thiết lập màu sắc',
positive: 'Giá trị dương',
native: 'Giá trị âm',
workSheet: 'Toàn bộ bảng tính',
selectedRange: 'Vùng chọn',
managerRuleSelect: 'Quản lý quy tắc của {0}',
onlyShowDataBar: 'Chỉ hiển thị thanh dữ liệu',
},
preview: {
describe: {
beginsWith: 'Bắt đầu với {0}',
endsWith: 'Kết thúc với {0}',
containsText: 'Chứa văn bản {0}',
notContainsText: 'Không chứa văn bản {0}',
equal: 'Bằng {0}',
notEqual: 'Không bằng {0}',
containsBlanks: 'Rỗng',
notContainsBlanks: 'Không rỗng',
containsErrors: 'Lỗi',
notContainsErrors: 'Không lỗi',
greaterThan: 'Lớn hơn {0}',
greaterThanOrEqual: 'Lớn hơn hoặc bằng {0}',
lessThan: 'Nhỏ hơn {0}',
lessThanOrEqual: 'Nhỏ hơn hoặc bằng {0}',
notBetween: 'Không nằm giữa {0} và {1}',
between: 'Nằm giữa {0} và {1}',
yesterday: 'Hôm qua',
tomorrow: 'Ngày mai',
last7Days: '7 ngày qua',
thisMonth: 'Tháng này',
lastMonth: 'Tháng trước',
nextMonth: 'Tháng sau',
thisWeek: 'Tuần này',
lastWeek: 'Tuần trước',
nextWeek: 'Tuần sau',
today: 'Hôm nay',
topN: 'Trên cùng {0}',
bottomN: 'Cuối {0}',
topNPercent: 'Trên cùng {0}%',
bottomNPercent: 'Cuối {0}%',
},
},
operator: {
beginsWith: 'Bắt đầu với',
endsWith: 'Kết thúc với',
containsText: 'Chứa văn bản',
notContainsText: 'Không chứa văn bản',
equal: 'Bằng',
notEqual: 'Không bằng',
containsBlanks: 'Rỗng',
notContainsBlanks: 'Không rỗng',
containsErrors: 'Lỗi',
notContainsErrors: 'Không lỗi',
greaterThan: 'Lớn hơn',
greaterThanOrEqual: 'Lớn hơn hoặc bằng',
lessThan: 'Nhỏ hơn',
lessThanOrEqual: 'Nhỏ hơn hoặc bằng',
notBetween: 'Không nằm giữa',
between: 'Nằm giữa',
yesterday: 'Hôm qua',
tomorrow: 'Ngày mai',
last7Days: '7 ngày qua',
thisMonth: 'Tháng này',
lastMonth: 'Tháng trước',
nextMonth: 'Tháng sau',
thisWeek: 'Tuần này',
lastWeek: 'Tuần trước',
nextWeek: 'Tuần sau',
today: 'Hôm nay',
},
ruleType: {
highlightCell: 'Làm nổi bật ô',
dataBar: 'Thanh dữ liệu',
colorScale: 'Thang màu',
formula: 'Công thức tùy chỉnh',
iconSet: 'Tập hợp biểu tượng',
},
subRuleType: {
uniqueValues: 'Giá trị duy nhất',
duplicateValues: 'Giá trị trùng lặp',
text: 'Văn bản',
timePeriod: 'Thời gian',
number: 'Giá trị số',
average: 'Giá trị trung bình',
},
valueType: {
num: 'Giá trị số',
min: 'Giá trị nhỏ nhất',
max: 'Giá trị lớn nhất',
percent: 'Phần trăm',
percentile: 'Phần trăm điểm',
formula: 'Công thức',
none: 'Không',
},
errorMessage: {
notBlank: 'Điều kiện không được để trống',
formulaError: 'Lỗi công thức',
rangeError: 'Lựa chọn không hợp lệ',
},
permission: {
dialog: {
setStyleErr: 'Phạm vi này đã được bảo vệ, hiện không có quyền thiết lập kiểu. Nếu cần thiết lập kiểu, vui lòng liên hệ với người tạo.',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,180 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: '条件格式',
menu: {
manageConditionalFormatting: '管理条件格式',
createConditionalFormatting: '新建条件格式',
clearRangeRules: '清除所选区域的规则',
clearWorkSheetRules: '清除整个工作表的规则',
},
form: {
lessThan: '该值必须小于 {0}',
lessThanOrEqual: '该值必须小于等于 {0}',
greaterThan: '该值必大于 {0}',
greaterThanOrEqual: '该值必大于等于 {0}',
},
iconSet: {
direction: '方向',
shape: '形状',
rank: '等级',
rule: '规则',
icon: '图标',
type: '类型',
value: '值',
reverseIconOrder: '反转图标次序',
and: '且',
when: '当值',
onlyShowIcon: '仅显示图标',
noCellIcon: '无单元格图标',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: '新增规则',
clear: '清空所有规则',
range: '应用范围',
styleType: '样式类型',
submit: '确认',
cancel: '取消',
rankAndAverage: '最前/最后/平均值',
styleRule: '样式规则',
isNotBottom: '最前',
isBottom: '最后',
greaterThanAverage: '大于平均值',
lessThanAverage: '小于平均值',
medianValue: '中间值',
fillType: '填充方式',
pureColor: '纯色',
gradient: '渐变',
colorSet: '颜色设置',
positive: '正值',
native: '负值',
workSheet: '整张工作表',
selectedRange: '所选单元格',
managerRuleSelect: '管理 {0} 的规则',
onlyShowDataBar: '仅显示数据条',
},
preview: {
describe: {
beginsWith: '开头为{0}',
endsWith: '结尾为{0}',
containsText: '文本包含{0}',
notContainsText: '文本不包含{0}',
equal: '等于{0}',
notEqual: '不等于{0}',
containsBlanks: '为空',
notContainsBlanks: '不为空',
containsErrors: '错误',
notContainsErrors: '非错误',
greaterThan: '大于{0}',
greaterThanOrEqual: '大于等于{0}',
lessThan: '小于{0}',
lessThanOrEqual: '小于等于{0}',
notBetween: '不介于{0}和{1}之间',
between: '介于{0}和{1}之间',
yesterday: '昨日',
tomorrow: '明日',
last7Days: '最近7天',
thisMonth: '本月',
lastMonth: '上个月',
nextMonth: '下个月',
thisWeek: '本周',
lastWeek: '上周',
nextWeek: '下周',
today: '今日',
topN: '前{0}项',
bottomN: '后{0}项',
topNPercent: '前{0}%',
bottomNPercent: '后{0}%',
},
},
operator: {
beginsWith: '开头为',
endsWith: '结尾为',
containsText: '文本包含',
notContainsText: '文本不包含',
equal: '等于',
notEqual: '不等于',
containsBlanks: '为空',
notContainsBlanks: '不为空',
containsErrors: '错误',
notContainsErrors: '非错误',
greaterThan: '大于',
greaterThanOrEqual: '大于等于',
lessThan: '小于',
lessThanOrEqual: '小于等于',
notBetween: '不介于',
between: '介于',
yesterday: '昨日',
tomorrow: '明日',
last7Days: '最近 7 天',
thisMonth: '本月',
lastMonth: '上个月',
nextMonth: '下个月',
thisWeek: '本周',
lastWeek: '上周',
nextWeek: '下周',
today: '今日',
},
ruleType: {
highlightCell: '突出显示单元格',
dataBar: '数据条',
colorScale: '色阶',
formula: '自定义公式',
iconSet: '图标集',
},
subRuleType: {
uniqueValues: '唯一值',
duplicateValues: '重复值',
text: '文本',
timePeriod: '时间日期',
number: '数值',
average: '平均值',
},
valueType: {
num: '数值',
min: '最小值',
max: '最大值',
percent: '百分比',
percentile: '百分点',
formula: '公式',
none: '无',
},
errorMessage: {
notBlank: '条件不能为空',
rangeError: '选区错误',
formulaError: '公式错误',
},
permission: {
dialog: {
setStyleErr: '该范围已被保护,目前无设置样式权限。如需设置样式,请联系创建者。',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,180 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: '條件格式',
menu: {
manageConditionalFormatting: '管理條件格式',
createConditionalFormatting: '新建條件格式',
clearRangeRules: '清除所選區域的規則',
clearWorkSheetRules: '清除整個工作表的規則',
},
form: {
lessThan: '該值必須小於 {0}',
lessThanOrEqual: '該值必須小於等於 {0}',
greaterThan: '該值必大於 {0}',
greaterThanOrEqual: '該值必大於等於 {0}',
},
iconSet: {
direction: '方向',
shape: '形狀',
rank: '等級',
rule: '規則',
icon: '圖示',
type: '類型',
value: '值',
reverseIconOrder: '反轉圖示次序',
and: '且',
when: '當值',
onlyShowIcon: '僅顯示圖示',
noCellIcon: '無儲存格圖示',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: '新增規則',
clear: '清空所有規則',
range: '應用範圍',
styleType: '樣式類型',
submit: '確認',
cancel: '取消',
rankAndAverage: '最前/最後/平均',
styleRule: '樣式規則',
isNotBottom: '最前面',
isBottom: '最後',
greaterThanAverage: '大於平均值',
lessThanAverage: '小於平均值',
medianValue: '中間值',
fillType: '填滿方式',
pureColor: '純色',
gradient: '漸層',
colorSet: '顏色設定',
positive: '正值',
native: '負值',
workSheet: '整張工作表',
selectedRange: '所選儲存格',
managerRuleSelect: '管理 {0} 的規則',
onlyShowDataBar: '僅顯示資料條',
},
preview: {
describe: {
beginsWith: '開頭為{0}',
endsWith: '結尾為{0}',
containsText: '文字包含{0}',
notContainsText: '文字不包含{0}',
equal: '等於{0}',
notEqual: '不等於{0}',
containsBlanks: '為空',
notContainsBlanks: '不為空',
containsErrors: '錯誤',
notContainsErrors: '非錯誤',
greaterThan: '大於{0}',
greaterThanOrEqual: '大於等於{0}',
lessThan: '小於{0}',
lessThanOrEqual: '小於等於{0}',
notBetween: '不介於{0}和{1}之間',
between: '介於{0}和{1}之間',
yesterday: '昨日',
tomorrow: '明日',
last7Days: '最近7天',
thisMonth: '本月',
lastMonth: '上個月',
nextMonth: '下個月',
thisWeek: '本週',
lastWeek: '上週',
nextWeek: '下週',
today: '今日',
topN: '前{0}項',
bottomN: '後{0}項',
topNPercent: '前{0}%',
bottomNPercent: '後{0}%',
},
},
operator: {
beginsWith: '開頭為',
endsWith: '結尾為',
containsText: '文字包含',
notContainsText: '文字不包含',
equal: '等於',
notEqual: '不等於',
containsBlanks: '為空',
notContainsBlanks: '不為空',
containsErrors: '錯誤',
notContainsErrors: '非錯誤',
greaterThan: '大於',
greaterThanOrEqual: '大於等於',
lessThan: '小於',
lessThanOrEqual: '小於等於',
notBetween: '不介於',
between: '介於',
yesterday: '昨日',
tomorrow: '明日',
last7Days: '最近 7 天',
thisMonth: '本月',
lastMonth: '上個月',
nextMonth: '下個月',
thisWeek: '本週',
lastWeek: '上週',
nextWeek: '下週',
today: '今日',
},
ruleType: {
highlightCell: '突出顯示儲存格',
dataBar: '資料條',
colorScale: '色階',
formula: '自訂公式',
iconSet: '圖示集',
},
subRuleType: {
uniqueValues: '唯一值',
duplicateValues: '重複值',
text: '文字',
timePeriod: '時間日期',
number: '數值',
average: '平均值',
},
valueType: {
num: '數值',
min: '最小值',
max: '最大值',
percent: '百分比',
percentile: '百分點',
formula: '公式',
none: '無',
},
errorMessage: {
notBlank: '條件不能為空',
rangeError: '選區錯誤',
formulaError: '公式錯誤',
},
permission: {
dialog: {
setStyleErr: '該範圍已被保護,目前無設定樣式權限。如需設定樣式,請聯絡建立者。 ',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,180 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-conditional-formatting-ui': {
title: '條件格式',
menu: {
manageConditionalFormatting: '管理條件格式',
createConditionalFormatting: '新建條件格式',
clearRangeRules: '清除所選區域的規則',
clearWorkSheetRules: '清除整個工作表的規則',
},
form: {
lessThan: '該值必須小於 {0}',
lessThanOrEqual: '該值必須小於等於 {0}',
greaterThan: '該值必大於 {0}',
greaterThanOrEqual: '該值必大於等於 {0}',
},
iconSet: {
direction: '方向',
shape: '形狀',
rank: '等級',
rule: '規則',
icon: '圖示',
type: '類型',
value: '值',
reverseIconOrder: '反轉圖示次序',
and: '且',
when: '當值',
onlyShowIcon: '僅顯示圖示',
noCellIcon: '無儲存格圖示',
},
symbol: {
greaterThan: '>',
greaterThanOrEqual: '>=',
lessThan: '<',
lessThanOrEqual: '<=',
},
panel: {
createRule: '新增規則',
clear: '清空所有規則',
range: '應用範圍',
styleType: '樣式類型',
submit: '確認',
cancel: '取消',
rankAndAverage: '最前/最後/平均',
styleRule: '樣式規則',
isNotBottom: '最前面',
isBottom: '最後',
greaterThanAverage: '大於平均值',
lessThanAverage: '小於平均值',
medianValue: '中間值',
fillType: '填滿方式',
pureColor: '純色',
gradient: '漸層',
colorSet: '顏色設定',
positive: '正值',
native: '負值',
workSheet: '整張工作表',
selectedRange: '所選儲存格',
managerRuleSelect: '管理 {0} 的規則',
onlyShowDataBar: '僅顯示資料條',
},
preview: {
describe: {
beginsWith: '開頭為{0}',
endsWith: '結尾為{0}',
containsText: '文字包含{0}',
notContainsText: '文字不包含{0}',
equal: '等於{0}',
notEqual: '不等於{0}',
containsBlanks: '為空',
notContainsBlanks: '不為空',
containsErrors: '錯誤',
notContainsErrors: '非錯誤',
greaterThan: '大於{0}',
greaterThanOrEqual: '大於等於{0}',
lessThan: '小於{0}',
lessThanOrEqual: '小於等於{0}',
notBetween: '不介於{0}和{1}之間',
between: '介於{0}和{1}之間',
yesterday: '昨日',
tomorrow: '明日',
last7Days: '最近7天',
thisMonth: '本月',
lastMonth: '上個月',
nextMonth: '下個月',
thisWeek: '本週',
lastWeek: '上週',
nextWeek: '下週',
today: '今日',
topN: '前{0}項',
bottomN: '後{0}項',
topNPercent: '前{0}%',
bottomNPercent: '後{0}%',
},
},
operator: {
beginsWith: '開頭為',
endsWith: '結尾為',
containsText: '文字包含',
notContainsText: '文字不包含',
equal: '等於',
notEqual: '不等於',
containsBlanks: '為空',
notContainsBlanks: '不為空',
containsErrors: '錯誤',
notContainsErrors: '非錯誤',
greaterThan: '大於',
greaterThanOrEqual: '大於等於',
lessThan: '小於',
lessThanOrEqual: '小於等於',
notBetween: '不介於',
between: '介於',
yesterday: '昨日',
tomorrow: '明日',
last7Days: '最近 7 天',
thisMonth: '本月',
lastMonth: '上個月',
nextMonth: '下個月',
thisWeek: '本週',
lastWeek: '上週',
nextWeek: '下週',
today: '今日',
},
ruleType: {
highlightCell: '突出顯示儲存格',
dataBar: '資料條',
colorScale: '色階',
formula: '自訂公式',
iconSet: '圖示集',
},
subRuleType: {
uniqueValues: '唯一值',
duplicateValues: '重複值',
text: '文字',
timePeriod: '時間日期',
number: '數值',
average: '平均值',
},
valueType: {
num: '數值',
min: '最小值',
max: '最大值',
percent: '百分比',
percentile: '百分點',
formula: '公式',
none: '無',
},
errorMessage: {
notBlank: '條件不能為空',
rangeError: '選區錯誤',
formulaError: '公式錯誤',
},
permission: {
dialog: {
setStyleErr: '該範圍已被保護,目前無設定樣式權限。如需設定樣式,請聯絡建立者。 ',
},
},
},
};
export default locale;

View File

@@ -0,0 +1,57 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDisposable } from '@univerjs/core';
import { ICommandService, toDisposable } from '@univerjs/core';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createCfUiTestBed } from '../../__tests__/create-cf-ui-test-bed';
import { FactoryManageConditionalFormattingRule } from '../manage-rule';
describe('FactoryManageConditionalFormattingRule', () => {
let univer: IDisposable | undefined;
afterEach(() => {
univer?.dispose();
vi.restoreAllMocks();
});
it('disposes command listeners with the selections subscription', () => {
const testBed = createCfUiTestBed();
univer = testBed.univer;
const commandService = testBed.get(ICommandService);
const onCommandExecuted = commandService.onCommandExecuted.bind(commandService);
let activeListeners = 0;
vi.spyOn(commandService, 'onCommandExecuted').mockImplementation((listener) => {
activeListeners++;
const disposable = onCommandExecuted(listener);
return toDisposable(() => {
activeListeners--;
disposable.dispose();
});
});
const menu = FactoryManageConditionalFormattingRule(testBed.injector);
if (!menu.selections || Array.isArray(menu.selections)) {
throw new Error('Expected observable selections');
}
const subscription = menu.selections.subscribe();
expect(activeListeners).toBe(2);
subscription.unsubscribe();
expect(activeListeners).toBe(0);
});
});

View File

@@ -0,0 +1,29 @@
/**
* 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 } from '@univerjs/core';
import { IMenuManagerService } from '@univerjs/ui';
import { menuSchema } from './schema';
export class ConditionalFormattingMenuController extends Disposable {
constructor(
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService
) {
super();
this._menuManagerService.mergeMenu(menuSchema);
}
}

View File

@@ -0,0 +1,228 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IAccessor, Workbook } from '@univerjs/core';
import type { IMenuSelectorItem, IValueOption } from '@univerjs/ui';
import type { LocaleKey } from '../locale/types';
import { ICommandService, IUniverInstanceService, Rectangle, UniverInstanceType } from '@univerjs/core';
import {
checkRangesEditablePermission,
RangeProtectionPermissionEditPoint,
SetWorksheetActiveOperation,
SheetsSelectionsService,
WorkbookEditablePermission,
WorksheetEditPermission,
WorksheetSetCellStylePermission,
} from '@univerjs/sheets';
import {
AddConditionalRuleMutation,
ConditionalFormattingRuleModel,
DeleteConditionalRuleMutation,
MoveConditionalRuleMutation,
SetConditionalRuleMutation,
} from '@univerjs/sheets-conditional-formatting';
import { getCurrentRangeDisable$ } from '@univerjs/sheets-ui';
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
import { merge, Observable } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import {
CF_MENU_OPERATION,
OpenConditionalFormattingOperator,
} from '../commands/operations/open-conditional-formatting-panel';
const commandList = [
SetWorksheetActiveOperation.id,
AddConditionalRuleMutation.id,
SetConditionalRuleMutation.id,
DeleteConditionalRuleMutation.id,
MoveConditionalRuleMutation.id,
];
const commonSelections: IValueOption<LocaleKey>[] = [
{
label: {
name: 'sheets-conditional-formatting-ui.ruleType.highlightCell',
selectable: false,
},
value: CF_MENU_OPERATION.highlightCell,
},
{
label: {
name: 'sheets-conditional-formatting-ui.panel.rankAndAverage',
selectable: false,
},
value: CF_MENU_OPERATION.rank,
},
{
label: {
name: 'sheets-conditional-formatting-ui.ruleType.formula',
selectable: false,
},
value: CF_MENU_OPERATION.formula,
},
{
label: {
name: 'sheets-conditional-formatting-ui.ruleType.colorScale',
selectable: false,
},
value: CF_MENU_OPERATION.colorScale,
},
{
label: {
name: 'sheets-conditional-formatting-ui.ruleType.dataBar',
selectable: false,
},
value: CF_MENU_OPERATION.dataBar,
},
{
label: {
name: 'sheets-conditional-formatting-ui.ruleType.iconSet',
selectable: false,
},
value: CF_MENU_OPERATION.icon,
},
{
label: {
name: 'sheets-conditional-formatting-ui.menu.manageConditionalFormatting',
selectable: false,
},
value: CF_MENU_OPERATION.viewRule,
},
{
label: {
name: 'sheets-conditional-formatting-ui.menu.createConditionalFormatting',
selectable: false,
},
value: CF_MENU_OPERATION.createRule,
},
{
label: {
name: 'sheets-conditional-formatting-ui.menu.clearRangeRules',
selectable: false,
},
value: CF_MENU_OPERATION.clearRangeRules,
disabled: false,
},
{
label: {
name: 'sheets-conditional-formatting-ui.menu.clearWorkSheetRules',
selectable: false,
},
value: CF_MENU_OPERATION.clearWorkSheetRules,
},
];
// eslint-disable-next-line max-lines-per-function
export function FactoryManageConditionalFormattingRule(accessor: IAccessor): IMenuSelectorItem<LocaleKey> {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
const clearRangeEnable$ = new Observable<boolean>((subscriber) => merge(
selectionManagerService.selectionMoveEnd$,
selectionManagerService.selectionSet$,
new Observable<null>((commandSubscribe) => {
const disposable = commandService.onCommandExecuted((commandInfo) => {
const { id, params } = commandInfo;
const unitId = univerInstanceService
.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)
?.getUnitId();
if (commandList.includes(id) && (params as { unitId: string }).unitId === unitId) {
commandSubscribe.next(null);
}
});
return () => disposable.dispose();
})
).pipe(debounceTime(16)).subscribe(() => {
const ranges = selectionManagerService.getCurrentSelections()?.map((selection) => selection.range) || [];
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) return;
const worksheet = workbook.getActiveSheet();
if (!worksheet) return;
const allRule = conditionalFormattingRuleModel.getSubunitRules(workbook.getUnitId(), worksheet.getSheetId()) || [];
const ruleList = allRule.filter((rule) => rule.ranges.some((ruleRange) => ranges.some((range) => Rectangle.intersects(range, ruleRange))));
const hasPermission = ruleList.map((rule) => rule.ranges).every((ranges) => {
return checkRangesEditablePermission(accessor, workbook.getUnitId(), worksheet.getSheetId(), ranges);
});
subscriber.next(hasPermission);
}));
const clearSheetEnable$ = new Observable<boolean>((subscriber) =>
new Observable<null>((commandSubscribe) => {
const disposable = commandService.onCommandExecuted((commandInfo) => {
const { id, params } = commandInfo;
const unitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getUnitId();
if (commandList.includes(id) && (params as { unitId: string }).unitId === unitId) {
commandSubscribe.next(null);
}
});
return () => disposable.dispose();
}).pipe(debounceTime(16)).subscribe(() => {
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) return;
const worksheet = workbook.getActiveSheet();
if (!worksheet) return;
const allRule = conditionalFormattingRuleModel.getSubunitRules(workbook.getUnitId(), worksheet.getSheetId()) || [];
if (!allRule.length) {
subscriber.next(false);
return false;
}
const hasPermission = allRule.map((rule) => rule.ranges).every((ranges) => {
return checkRangesEditablePermission(accessor, workbook.getUnitId(), worksheet.getSheetId(), ranges);
});
subscriber.next(hasPermission);
})
);
const selections$ = new Observable<IValueOption<LocaleKey>[]>((subscriber) => {
const clearRangeSubscription = clearRangeEnable$.subscribe((v) => {
const item = commonSelections.find((item) => item.value === CF_MENU_OPERATION.clearRangeRules);
if (item) {
item.disabled = !v;
subscriber.next(commonSelections);
}
});
const clearSheetSubscription = clearSheetEnable$.subscribe((v) => {
const item = commonSelections.find((item) => item.value === CF_MENU_OPERATION.clearWorkSheetRules);
if (item) {
item.disabled = !v;
subscriber.next(commonSelections);
}
});
subscriber.next(commonSelections);
return () => {
clearRangeSubscription.unsubscribe();
clearSheetSubscription.unsubscribe();
};
});
return {
id: OpenConditionalFormattingOperator.id,
type: MenuItemType.SELECTOR,
icon: 'ConditionsDoubleIcon',
tooltip: 'sheets-conditional-formatting-ui.title',
selections: selections$,
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
disabled$: getCurrentRangeDisable$(accessor, {
workbookTypes: [WorkbookEditablePermission],
worksheetTypes: [WorksheetSetCellStylePermission, WorksheetEditPermission],
rangeTypes: [RangeProtectionPermissionEditPoint],
}),
};
};

View File

@@ -0,0 +1,29 @@
/**
* 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 } from '@univerjs/core';
import { IMenuManagerService } from '@univerjs/ui';
import { mobileMenuSchema } from './mobile-schema';
export class ConditionalFormattingMobileMenuController extends Disposable {
constructor(
@IMenuManagerService menuManagerService: IMenuManagerService
) {
super();
menuManagerService.mergeMenu(mobileMenuSchema);
}
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { MenuSchemaType } from '@univerjs/ui';
import { conditionalFormattingRuleMenuSchema } from './schema';
export const mobileMenuSchema: MenuSchemaType = {
...conditionalFormattingRuleMenuSchema,
};

View File

@@ -0,0 +1,161 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IAccessor } from '@univerjs/core';
import type { IIconSet } from '@univerjs/sheets-conditional-formatting';
import type { IMenuSelectorItem } from '@univerjs/ui';
import type { LocaleKey } from '../locale/types';
import { UniverInstanceType } from '@univerjs/core';
import {
RangeProtectionPermissionEditPoint,
WorkbookEditablePermission,
WorksheetEditPermission,
WorksheetSetCellStylePermission,
} from '@univerjs/sheets';
import {
CFNumberOperator,
CFRuleType,
CFValueType,
ClearRangeCfCommand,
defaultDataBarNativeColor,
defaultDataBarPositiveColor,
IIconSetType,
} from '@univerjs/sheets-conditional-formatting';
import { getCurrentRangeDisable$ } from '@univerjs/sheets-ui';
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
import { AddDataBarConditionalRuleCommand } from '../commands/commands/add-data-bar-cf.command';
import { AddIconSetConditionalRuleCommand } from '../commands/commands/add-icon-set-cf.command';
import { CF_MENU_OPERATION, OpenConditionalFormattingOperator } from '../commands/operations/open-conditional-formatting-panel';
const DATA_BAR_PARAMS = {
min: { type: CFValueType.min },
max: { type: CFValueType.max },
nativeColor: defaultDataBarNativeColor,
positiveColor: defaultDataBarPositiveColor,
isGradient: false,
isShowValue: true,
};
function createIconSetParams(iconType: IIconSetType): { config: IIconSet['config']; isShowValue: boolean } {
return {
config: [
{
iconType,
iconId: '0',
operator: CFNumberOperator.greaterThanOrEqual,
value: { type: CFValueType.percent, value: 67 },
},
{
iconType,
iconId: '1',
operator: CFNumberOperator.greaterThanOrEqual,
value: { type: CFValueType.percent, value: 33 },
},
{
iconType,
iconId: '2',
operator: CFNumberOperator.lessThanOrEqual,
value: { type: CFValueType.percent, value: Number.MAX_SAFE_INTEGER },
},
],
isShowValue: true,
};
}
function getQuickConditionalFormattingMenuState(accessor: IAccessor) {
return {
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
disabled$: getCurrentRangeDisable$(accessor, {
workbookTypes: [WorkbookEditablePermission],
worksheetTypes: [WorksheetSetCellStylePermission, WorksheetEditPermission],
rangeTypes: [RangeProtectionPermissionEditPoint],
}),
};
}
export function quickDataBarMenuFactory(accessor: IAccessor): IMenuSelectorItem<LocaleKey> {
return {
id: AddDataBarConditionalRuleCommand.id,
type: MenuItemType.BUTTON_SELECTOR,
icon: 'DataBarIcon',
tooltip: 'sheets-conditional-formatting-ui.ruleType.dataBar',
params: DATA_BAR_PARAMS,
selectionsCommandId: AddDataBarConditionalRuleCommand.id,
selections: [
{
label: 'sheets-conditional-formatting-ui.ruleType.dataBar',
value: CFRuleType.dataBar,
params: DATA_BAR_PARAMS,
},
{
id: OpenConditionalFormattingOperator.id,
label: 'sheets-conditional-formatting-ui.menu.createConditionalFormatting',
value: CF_MENU_OPERATION.dataBar,
params: { value: CF_MENU_OPERATION.dataBar },
},
{
id: ClearRangeCfCommand.id,
label: 'sheets-conditional-formatting-ui.menu.clearRangeRules',
value: CFRuleType.dataBar,
params: { types: [CFRuleType.dataBar] },
},
],
...getQuickConditionalFormattingMenuState(accessor),
};
}
export function quickIconSetMenuFactory(accessor: IAccessor): IMenuSelectorItem<LocaleKey> {
const ratingParams = createIconSetParams(IIconSetType.threeStars);
return {
id: AddIconSetConditionalRuleCommand.id,
type: MenuItemType.BUTTON_SELECTOR,
icon: 'RatingIcon',
tooltip: 'sheets-conditional-formatting-ui.ruleType.iconSet',
params: ratingParams,
selectionsCommandId: AddIconSetConditionalRuleCommand.id,
selections: [
{
label: 'sheets-conditional-formatting-ui.iconSet.rank',
value: IIconSetType.threeStars,
params: ratingParams,
},
{
label: 'sheets-conditional-formatting-ui.iconSet.shape',
value: IIconSetType.threeTrafficLights1,
params: createIconSetParams(IIconSetType.threeTrafficLights1),
},
{
label: 'sheets-conditional-formatting-ui.iconSet.direction',
value: IIconSetType.threeArrows,
params: createIconSetParams(IIconSetType.threeArrows),
},
{
id: OpenConditionalFormattingOperator.id,
label: 'sheets-conditional-formatting-ui.menu.createConditionalFormatting',
value: CF_MENU_OPERATION.icon,
params: { value: CF_MENU_OPERATION.icon },
},
{
id: ClearRangeCfCommand.id,
label: 'sheets-conditional-formatting-ui.menu.clearRangeRules',
value: CFRuleType.iconSet,
params: { types: [CFRuleType.iconSet] },
},
],
...getQuickConditionalFormattingMenuState(accessor),
};
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { MenuSchemaType } from '@univerjs/ui';
import { RibbonDataGroup, RibbonInsertGroup } from '@univerjs/ui';
import { AddDataBarConditionalRuleCommand } from '../commands/commands/add-data-bar-cf.command';
import { AddIconSetConditionalRuleCommand } from '../commands/commands/add-icon-set-cf.command';
import { OpenConditionalFormattingOperator } from '../commands/operations/open-conditional-formatting-panel';
import { FactoryManageConditionalFormattingRule } from './manage-rule';
import { quickDataBarMenuFactory, quickIconSetMenuFactory } from './quick-insert.menu';
export const conditionalFormattingRuleMenuSchema: MenuSchemaType = {
[RibbonDataGroup.RULES]: {
[OpenConditionalFormattingOperator.id]: {
order: 1,
gridLayout: { row: 2, column: 1, showLabel: true },
menuItemFactory: FactoryManageConditionalFormattingRule,
},
},
};
export const menuSchema: MenuSchemaType = {
[RibbonInsertGroup.CELL]: {
[AddDataBarConditionalRuleCommand.id]: {
order: 3,
menuItemFactory: quickDataBarMenuFactory,
},
[AddIconSetConditionalRuleCommand.id]: {
order: 4,
menuItemFactory: quickIconSetMenuFactory,
},
},
...conditionalFormattingRuleMenuSchema,
};

View File

@@ -0,0 +1,144 @@
/**
* 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 { IUniverSheetsConditionalFormattingUIConfig } from './config/config';
import {
DependentOn,
ICommandService,
IConfigService,
Inject,
Injector,
merge,
Plugin,
registerDependencies,
touchDependencies,
UniverInstanceType,
} from '@univerjs/core';
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import { SHEET_CONDITIONAL_FORMATTING_PLUGIN, UniverSheetsConditionalFormattingPlugin } from '@univerjs/sheets-conditional-formatting';
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
import { UniverSheetsFormulaMobileUIPlugin } from '@univerjs/sheets-formula-ui';
import { UniverSheetsMobileUIPlugin } from '@univerjs/sheets-ui';
import pkg from '../package.json';
import { AddAverageCfCommand } from './commands/commands/add-average-cf.command';
import { AddColorScaleConditionalRuleCommand } from './commands/commands/add-color-scale-cf.command';
import { AddDataBarConditionalRuleCommand } from './commands/commands/add-data-bar-cf.command';
import { AddDuplicateValuesCfCommand } from './commands/commands/add-duplicate-values-cf.command';
import { AddIconSetConditionalRuleCommand } from './commands/commands/add-icon-set-cf.command';
import { AddNumberCfCommand } from './commands/commands/add-number-cf.command';
import { AddRankCfCommand } from './commands/commands/add-rank-cf.command';
import { AddTextCfCommand } from './commands/commands/add-text-cf.command';
import { AddTimePeriodCfCommand } from './commands/commands/add-time-period-cf.command';
import { AddUniqueValuesCfCommand } from './commands/commands/add-unique-values-cf.command';
import { OpenConditionalFormattingOperator } from './commands/operations/open-conditional-formatting-panel';
import { defaultPluginConfig, SHEETS_CONDITIONAL_FORMATTING_UI_PLUGIN_CONFIG_KEY } from './config/config';
import { ConditionalFormattingFormulaRefRangeController } from './controllers/cf-formula-ref-range.controller';
import { ConditionalFormattingCopyPasteController } from './controllers/cf.copy-paste.controller';
import { ConditionalFormattingI18nController } from './controllers/cf.i18n.controller';
import { ConditionalFormattingPanelController } from './controllers/cf.panel.controller';
import { ConditionalFormattingPermissionController } from './controllers/cf.permission.controller';
import { SheetsCfRenderController } from './controllers/cf.render.controller';
import { MobileComponentsController } from './controllers/mobile/components.controller';
import { ConditionalFormattingMobileMenuController } from './menu/mobile-cf.menu.controller';
@DependentOn(
UniverRenderEnginePlugin,
UniverSheetsPlugin,
UniverSheetsFormulaPlugin,
UniverSheetsFormulaMobileUIPlugin,
UniverSheetsConditionalFormattingPlugin,
UniverSheetsMobileUIPlugin
)
export class UniverSheetsConditionalFormattingMobileUIPlugin extends Plugin {
static override pluginName = `${SHEET_CONDITIONAL_FORMATTING_PLUGIN}_UI_PLUGIN`;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: Partial<IUniverSheetsConditionalFormattingUIConfig> = defaultPluginConfig,
@Inject(Injector) override readonly _injector: Injector,
@Inject(ICommandService) private _commandService: ICommandService,
@IConfigService private readonly _configService: IConfigService
) {
super();
// Manage the plugin configuration.
const { menu, ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
if (menu) {
this._configService.setConfig('menu', menu, { merge: true });
}
this._configService.setConfig(SHEETS_CONDITIONAL_FORMATTING_UI_PLUGIN_CONFIG_KEY, rest);
this._initCommand();
}
override onStarting(): void {
registerDependencies(this._injector, [
[MobileComponentsController],
[SheetsCfRenderController],
[ConditionalFormattingCopyPasteController],
[ConditionalFormattingPermissionController],
[ConditionalFormattingI18nController],
[ConditionalFormattingFormulaRefRangeController],
[ConditionalFormattingPanelController],
[ConditionalFormattingMobileMenuController],
]);
touchDependencies(this._injector, [
[MobileComponentsController],
[SheetsCfRenderController],
[ConditionalFormattingFormulaRefRangeController],
]);
}
override onReady(): void {
touchDependencies(this._injector, [
[ConditionalFormattingMobileMenuController],
[ConditionalFormattingPanelController],
]);
}
override onRendered(): void {
touchDependencies(this._injector, [
[ConditionalFormattingCopyPasteController],
[ConditionalFormattingPermissionController],
[ConditionalFormattingI18nController],
]);
}
private _initCommand() {
[
AddAverageCfCommand,
AddColorScaleConditionalRuleCommand,
AddDataBarConditionalRuleCommand,
AddIconSetConditionalRuleCommand,
AddDuplicateValuesCfCommand,
AddNumberCfCommand,
AddRankCfCommand,
AddTextCfCommand,
AddTimePeriodCfCommand,
AddUniqueValuesCfCommand,
OpenConditionalFormattingOperator,
].forEach((m) => {
this._commandService.registerCommand(m);
});
}
}

View File

@@ -0,0 +1,158 @@
/**
* 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 { IUniverSheetsConditionalFormattingUIConfig } from './config/config';
import {
DependentOn,
ICommandService,
IConfigService,
Inject,
Injector,
merge,
Plugin,
registerDependencies,
touchDependencies,
UniverInstanceType,
} from '@univerjs/core';
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import { SHEET_CONDITIONAL_FORMATTING_PLUGIN, UniverSheetsConditionalFormattingPlugin } from '@univerjs/sheets-conditional-formatting';
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import pkg from '../package.json';
import { AddAverageCfCommand } from './commands/commands/add-average-cf.command';
import { AddColorScaleConditionalRuleCommand } from './commands/commands/add-color-scale-cf.command';
import { AddDataBarConditionalRuleCommand } from './commands/commands/add-data-bar-cf.command';
import { AddDuplicateValuesCfCommand } from './commands/commands/add-duplicate-values-cf.command';
import { AddIconSetConditionalRuleCommand } from './commands/commands/add-icon-set-cf.command';
import { AddNumberCfCommand } from './commands/commands/add-number-cf.command';
import { AddRankCfCommand } from './commands/commands/add-rank-cf.command';
import { AddTextCfCommand } from './commands/commands/add-text-cf.command';
import { AddTimePeriodCfCommand } from './commands/commands/add-time-period-cf.command';
import { AddUniqueValuesCfCommand } from './commands/commands/add-unique-values-cf.command';
import { OpenConditionalFormattingOperator } from './commands/operations/open-conditional-formatting-panel';
import { defaultPluginConfig, SHEETS_CONDITIONAL_FORMATTING_UI_PLUGIN_CONFIG_KEY } from './config/config';
import { ConditionalFormattingFormulaRefRangeController } from './controllers/cf-formula-ref-range.controller';
import { ConditionalFormattingAutoFillController } from './controllers/cf.auto-fill.controller';
import { ConditionalFormattingClearController } from './controllers/cf.clear.controller';
import { ConditionalFormattingCopyPasteController } from './controllers/cf.copy-paste.controller';
import { ConditionalFormattingEditorController } from './controllers/cf.editor.controller';
import { ConditionalFormattingI18nController } from './controllers/cf.i18n.controller';
import { ConditionalFormattingPainterController } from './controllers/cf.painter.controller';
import { ConditionalFormattingPanelController } from './controllers/cf.panel.controller';
import { ConditionalFormattingPermissionController } from './controllers/cf.permission.controller';
import { SheetsCfRenderController } from './controllers/cf.render.controller';
import { ConditionalFormattingViewportController } from './controllers/cf.viewport.controller';
import { ComponentsController } from './controllers/components.controller';
import { ConditionalFormattingMenuController } from './menu/cf.menu.controller';
@DependentOn(
UniverRenderEnginePlugin,
UniverSheetsPlugin,
UniverSheetsFormulaPlugin,
UniverSheetsConditionalFormattingPlugin,
UniverSheetsUIPlugin
)
export class UniverSheetsConditionalFormattingUIPlugin extends Plugin {
static override pluginName = `${SHEET_CONDITIONAL_FORMATTING_PLUGIN}_UI_PLUGIN`;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: Partial<IUniverSheetsConditionalFormattingUIConfig> = defaultPluginConfig,
@Inject(Injector) override readonly _injector: Injector,
@Inject(ICommandService) private _commandService: ICommandService,
@IConfigService private readonly _configService: IConfigService
) {
super();
// Manage the plugin configuration.
const { menu, ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
if (menu) {
this._configService.setConfig('menu', menu, { merge: true });
}
this._configService.setConfig(SHEETS_CONDITIONAL_FORMATTING_UI_PLUGIN_CONFIG_KEY, rest);
this._initCommand();
}
override onStarting(): void {
this._injector.add([ComponentsController]);
this._injector.get(ComponentsController);
registerDependencies(this._injector, [
[SheetsCfRenderController],
[ConditionalFormattingCopyPasteController],
[ConditionalFormattingAutoFillController],
[ConditionalFormattingPermissionController],
[ConditionalFormattingPanelController],
[ConditionalFormattingMenuController],
[ConditionalFormattingI18nController],
[ConditionalFormattingEditorController],
[ConditionalFormattingClearController],
[ConditionalFormattingPainterController],
[ConditionalFormattingViewportController],
[ConditionalFormattingFormulaRefRangeController],
]);
touchDependencies(this._injector, [
[SheetsCfRenderController],
[ConditionalFormattingFormulaRefRangeController],
]);
}
override onReady(): void {
touchDependencies(this._injector, [
[ConditionalFormattingMenuController],
[ConditionalFormattingPanelController],
]);
}
override onRendered(): void {
touchDependencies(this._injector, [
[ConditionalFormattingAutoFillController],
[ConditionalFormattingClearController],
[ConditionalFormattingCopyPasteController],
[ConditionalFormattingEditorController],
[ConditionalFormattingI18nController],
[ConditionalFormattingPainterController],
[ConditionalFormattingPermissionController],
[ConditionalFormattingViewportController],
]);
}
private _initCommand() {
[
AddAverageCfCommand,
AddColorScaleConditionalRuleCommand,
AddDataBarConditionalRuleCommand,
AddIconSetConditionalRuleCommand,
AddDuplicateValuesCfCommand,
AddNumberCfCommand,
AddRankCfCommand,
AddTextCfCommand,
AddTimePeriodCfCommand,
AddUniqueValuesCfCommand,
OpenConditionalFormattingOperator,
].forEach((m) => {
this._commandService.registerCommand(m);
});
}
}

View File

@@ -0,0 +1,79 @@
/**
* 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 { IColorPickerProps, IDropdownProps } from '@univerjs/design';
import type { ComponentType, FC } from 'react';
import { ColorKit } from '@univerjs/core';
import { clsx, Dropdown, ColorPicker as OriginColorPicker } from '@univerjs/design';
import { PaintBucketDoubleIcon } from '@univerjs/icons';
import { useMemo } from 'react';
export interface IConditionalColorPickerProps {
color: string;
onChange: (color: string) => void;
disable?: boolean;
Icon?: FC;
className?: string;
DropdownComponent?: ComponentType<IDropdownProps>;
PickerComponent?: ComponentType<IColorPickerProps>;
};
export const ColorPicker = (props: IConditionalColorPickerProps) => {
const {
color,
onChange,
disable = false,
Icon = PaintBucketDoubleIcon,
className,
DropdownComponent = Dropdown,
PickerComponent = OriginColorPicker,
} = props;
const colorKit = useMemo(() => new ColorKit(color), [color]);
const renderIcon = () => {
const iconProps = {
className: clsx('univer-fill-primary-600', disable && className),
extend: { colorChannel1: colorKit.isValid ? color : '' },
};
return (
<Icon {...iconProps} />
);
};
return !disable
? (
<DropdownComponent
overlay={(
<div className="univer-rounded-lg univer-p-4">
<PickerComponent value={color} onChange={onChange} />
</div>
)}
>
<span
className={clsx(`
univer-flex univer-cursor-pointer univer-items-center univer-rounded univer-p-1
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
`, className)}
>
{renderIcon()}
</span>
</DropdownComponent>
)
: renderIcon();
};

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 type { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import { useState } from 'react';
import { RuleEdit } from './panel/RuleEdit';
import { RuleList } from './panel/RuleList';
export interface IConditionFormattingPanelProps {
rule?: IConditionFormattingRule;
RuleEditComponent?: typeof RuleEdit;
RuleListComponent?: typeof RuleList;
}
export const ConditionFormattingPanel = (props: IConditionFormattingPanelProps) => {
const { RuleEditComponent = RuleEdit, RuleListComponent = RuleList } = props;
const [currentEditRule, setCurrentEditRule] = useState<IConditionFormattingRule | undefined>(props.rule);
const [isShowRuleEditor, setIsShowRuleEditor] = useState(!!props.rule);
const createCfRule = () => {
setIsShowRuleEditor(true);
};
const handleCancel = () => {
setIsShowRuleEditor(false);
setCurrentEditRule(undefined);
};
const handleRuleClick = (rule: IConditionFormattingRule) => {
setCurrentEditRule(rule);
setIsShowRuleEditor(true);
};
return (
<div className="univer-flex univer-h-full univer-flex-col univer-justify-between univer-py-4">
{isShowRuleEditor
? (
<RuleEditComponent onCancel={handleCancel} rule={currentEditRule} />
)
: (
<RuleListComponent onClick={handleRuleClick} onCreate={createCfRule} />
)}
</div>
);
};

View File

@@ -0,0 +1,144 @@
/**
* 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 { IHighlightCell } from '@univerjs/sheets-conditional-formatting';
import type { ComponentType } from 'react';
import type { IConditionalColorPickerProps } from './ColorPicker';
import { BooleanNumber } from '@univerjs/core';
import { clsx } from '@univerjs/design';
import { BoldIcon, FontColorDoubleIcon, ItalicIcon, StrikethroughIcon, UnderlineIcon } from '@univerjs/icons';
import { removeUndefinedAttr } from '@univerjs/sheets-conditional-formatting';
import { useEffect, useState } from 'react';
import { ColorPicker } from './ColorPicker';
export interface IConditionalStyleEditorProps {
className?: string;
style?: IHighlightCell['style'];
onChange: (style: IHighlightCell['style']) => void;
ColorPickerComponent?: ComponentType<IConditionalColorPickerProps>;
};
const getAnotherBooleanNumber = (v: BooleanNumber | undefined) => {
return [BooleanNumber.FALSE, undefined].includes(v) ? BooleanNumber.TRUE : BooleanNumber.FALSE;
};
const getBooleanFromNumber = (v: BooleanNumber) => v !== BooleanNumber.FALSE;
export const ConditionalStyleEditor = (props: IConditionalStyleEditorProps) => {
const { style, onChange, className, ColorPickerComponent = ColorPicker } = props;
const [isBold, setIsBold] = useState<BooleanNumber | undefined>(() => {
const defaultV = undefined;
if (!style?.bl) {
return defaultV;
}
return style.bl;
});
const [isItalic, setIsItalic] = useState<BooleanNumber | undefined>(() => {
const defaultV = undefined;
if (!style?.it) {
return defaultV;
}
return style.it;
});
const [isUnderline, setIsUnderline] = useState<BooleanNumber | undefined>(() => {
const defaultV = undefined;
if (!style?.ul) {
return defaultV;
}
return style.ul.s;
});
const [isStrikethrough, setIsStrikethrough] = useState<BooleanNumber | undefined>(() => {
const defaultV = undefined;
if (!style?.st) {
return defaultV;
}
return style.st.s;
});
const [fontColor, setFontColor] = useState(() => {
const defaultV = '#2f56ef';
if (!style?.cl?.rgb) {
return defaultV;
}
return style.cl.rgb;
});
const [bgColor, setBgColor] = useState(() => {
const defaultV = '#e8ecfc';
if (!style?.bg?.rgb) {
return defaultV;
}
return style.bg.rgb;
});
useEffect(() => {
const resultStyle: IHighlightCell['style'] = {
bl: isBold,
it: isItalic,
};
if (fontColor !== undefined) {
resultStyle.cl = { rgb: fontColor };
}
if (bgColor !== undefined) {
resultStyle.bg = { rgb: bgColor };
}
if (isStrikethrough !== undefined) {
resultStyle.st = { s: isStrikethrough };
}
if (isUnderline !== undefined) {
resultStyle.ul = { s: isUnderline };
}
onChange(removeUndefinedAttr(resultStyle));
}, [isBold, isItalic, isUnderline, isStrikethrough, fontColor, bgColor]);
const buttonItemClassName = 'univer-flex univer-cursor-pointer univer-items-center univer-rounded univer-px-1';
return (
<div className={clsx('univer-my-2.5 univer-flex univer-justify-between', className)}>
<div
className={clsx(buttonItemClassName, {
'univer-bg-gray-100 dark:!univer-bg-gray-700': getBooleanFromNumber(isBold || BooleanNumber.FALSE),
})}
onClick={() => setIsBold(getAnotherBooleanNumber(isBold))}
>
<BoldIcon />
</div>
<div
className={clsx(buttonItemClassName, {
'univer-bg-gray-100 dark:!univer-bg-gray-700': getBooleanFromNumber(isItalic || BooleanNumber.FALSE),
})}
onClick={() => setIsItalic(getAnotherBooleanNumber(isItalic))}
>
<ItalicIcon />
</div>
<div
className={clsx(buttonItemClassName, {
'univer-bg-gray-100 dark:!univer-bg-gray-700': getBooleanFromNumber(isUnderline || BooleanNumber.FALSE),
})}
onClick={() => setIsUnderline(getAnotherBooleanNumber(isUnderline))}
>
<UnderlineIcon />
</div>
<div
className={clsx(buttonItemClassName, {
'univer-bg-gray-100 dark:!univer-bg-gray-700': getBooleanFromNumber(isStrikethrough || BooleanNumber.FALSE),
})}
onClick={() => setIsStrikethrough(getAnotherBooleanNumber(isStrikethrough))}
>
<StrikethroughIcon />
</div>
<ColorPickerComponent color={fontColor} onChange={setFontColor} Icon={FontColorDoubleIcon} />
<ColorPickerComponent color={bgColor} onChange={setBgColor} />
</div>
);
};

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 { IConditionalFormattingRuleConfig } from '@univerjs/sheets-conditional-formatting';
import { BooleanNumber, ColorKit } from '@univerjs/core';
import { clsx } from '@univerjs/design';
import { SlashDoubleIcon } from '@univerjs/icons';
import {
CFRuleType,
DEFAULT_BG_COLOR,
DEFAULT_FONT_COLOR,
defaultDataBarNativeColor,
defaultDataBarPositiveColor,
getColorScaleFromValue,
iconMap,
} from '@univerjs/sheets-conditional-formatting';
import { useMemo } from 'react';
export function Preview(props: { rule?: IConditionalFormattingRuleConfig }) {
const { rule } = props;
const colorList = useMemo(() => {
if (rule?.type === CFRuleType.colorScale) {
const config = rule.config.map((c, index) => ({ color: new ColorKit(c.color), value: index }));
const maxValue = config.length - 1;
const valueList = new Array(5).fill('').map((_v, index, arr) => index * maxValue / (arr.length - 1));
return valueList.map((value) => getColorScaleFromValue(config, value));
}
return null;
}, [rule]);
const iconSet = useMemo(() => {
if (rule?.type === CFRuleType.iconSet) {
return rule.config.map((item) => {
const iconList = iconMap[item.iconType];
return iconList && iconList[Number(item.iconId)];
});
}
}, [rule]);
if (!rule) return null;
const previewClassName = 'univer-pointer-events-none univer-flex univer-h-5 univer-min-w-[72px] univer-items-center univer-justify-center univer-text-xs';
switch (rule.type) {
case CFRuleType.dataBar:
{
const { isGradient } = rule.config;
const positiveColor = isGradient ? `linear-gradient(to right, ${rule.config.positiveColor || defaultDataBarPositiveColor}, rgb(255 255 255))` : rule.config.positiveColor;
const nativeColor = isGradient ? `linear-gradient(to right, rgb(255 255 255),${rule.config.nativeColor || defaultDataBarNativeColor})` : rule.config.nativeColor;
return (
<div className={previewClassName}>
<div
className="univer-h-full univer-w-1/2 univer-border univer-border-solid"
style={{
background: nativeColor,
borderColor: rule.config.nativeColor ?? defaultDataBarNativeColor,
}}
/>
<div
className="univer-h-full univer-w-1/2 univer-border univer-border-solid"
style={{
background: positiveColor,
borderColor: rule.config.positiveColor ?? defaultDataBarPositiveColor,
}}
/>
</div>
);
}
case CFRuleType.colorScale: {
return colorList && (
<div className={previewClassName}>
{colorList.map((item, index) => (
<div
key={index}
className="univer-h-full"
style={{ width: `${100 / colorList.length}%`, background: item }}
/>
))}
</div>
);
}
case CFRuleType.iconSet: {
return iconSet && (
<div className={previewClassName}>
{iconSet.map((base64, index) => (
base64 ?
<img key={index} className="univer-h-full" src={base64} draggable={false} />
: <SlashDoubleIcon key={index} />))}
</div>
);
}
case CFRuleType.highlightCell: {
const { ul, st, it, bl, bg, cl } = rule.style;
const isUnderline = ul?.s === BooleanNumber.TRUE;
const isStrikethrough = st?.s === BooleanNumber.TRUE;
const isItalic = it === BooleanNumber.TRUE;
const isBold = bl === BooleanNumber.TRUE;
const bgColor = bg?.rgb ?? DEFAULT_BG_COLOR;
const fontColor = cl?.rgb ?? DEFAULT_FONT_COLOR;
const style = {
textDecoration: `${isUnderline ? 'underline' : ''} ${isStrikethrough ? 'line-through' : ''}`.replace(/^ /, '') ?? undefined,
backgroundColor: bgColor,
color: fontColor,
};
return (
<div
className={clsx(previewClassName, {
'univer-font-bold': isBold,
'univer-italic': isItalic,
})}
style={style}
>
123
</div>
);
}
}
}

View File

@@ -0,0 +1,481 @@
/**
* 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 { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import type { Root } from 'react-dom/client';
import { BooleanNumber, createIdentifier, LocaleService, LocaleType } from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SetWorksheetActiveOperation } from '@univerjs/sheets';
import { AddCfCommand, CFNumberOperator, CFRuleType, CFSubRuleType, SetCfCommand } from '@univerjs/sheets-conditional-formatting';
import { IMarkSelectionService } from '@univerjs/sheets-ui';
import { IDialogService, IShortcutService, RediContext } from '@univerjs/ui';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { of } from 'rxjs';
import { afterEach, describe, expect, it } from 'vitest';
import { createCfUiTestBed } from '../../__tests__/create-cf-ui-test-bed';
import { ConditionalFormattingI18nController } from '../../controllers/cf.i18n.controller';
import { ConditionFormattingPanel } from '../ConditionFormattingPanel';
import { RuleEdit } from '../panel/RuleEdit';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const ACTIVE_RANGE: IRange = {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
};
const IEditorService = createIdentifier<TestEditorService>('univer.editor.service');
const IDescriptionService = createIdentifier<TestDescriptionService>('formula.description-service');
class TestEditorService {
register() {
return { dispose() {} };
}
getEditor() {
return undefined;
}
getFocusId() {
return undefined;
}
focus(): void {}
blur(): void {}
}
class TestRenderManagerService {
getRenderUnitById() {
return undefined;
}
}
class TestShortcutService {
registerShortcut() {
return { dispose() {} };
}
}
class TestMarkSelectionService {
addShape() {
return 'shape-id';
}
removeShape(): void {}
}
class TestDescriptionService {
hasDefinedNameDescription() {
return false;
}
}
class TestDialogService {
open() {
return { dispose: () => {} };
}
close(): void {}
closeAll(): void {}
getDialogs$() {
return of([]);
}
}
function createNumberHighlightRule(cfId: string, value: number): IConditionFormattingRule {
return {
cfId,
ranges: [ACTIVE_RANGE],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: CFNumberOperator.greaterThan,
value,
style: {
bl: BooleanNumber.TRUE,
},
},
};
}
function createPanelTestBed() {
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingI18nController]);
testBed.injector.add([IEditorService, { useClass: TestEditorService as never }]);
testBed.injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
testBed.injector.add([IShortcutService, { useClass: TestShortcutService as never }]);
testBed.injector.add([IMarkSelectionService, { useClass: TestMarkSelectionService as never }]);
testBed.injector.add([IDescriptionService, { useClass: TestDescriptionService as never }]);
testBed.injector.add([IDialogService, { useClass: TestDialogService }]);
testBed.injector.add([LexerTreeBuilder]);
testBed.commandService.registerCommand(AddCfCommand);
testBed.commandService.registerCommand(SetCfCommand);
testBed.commandService.registerCommand(SetWorksheetActiveOperation);
testBed.get(LocaleService).load({
[LocaleType.ZH_CN]: {
'sheets-conditional-formatting-ui': {
panel: {
managerRuleSelect: 'Show {0}',
workSheet: 'Worksheet',
selectedRange: 'Selected range',
createRule: 'Create rule',
clear: 'Clear rules',
range: 'Range',
styleType: 'Style type',
styleRule: 'Style rule',
submit: 'Submit',
cancel: 'Cancel',
},
operator: {
containsText: 'Contains text',
notContainsText: 'Does not contain text',
beginsWith: 'Begins with',
endsWith: 'Ends with',
equal: 'Equals',
notEqual: 'Does not equal',
containsBlanks: 'Contains blanks',
notContainsBlanks: 'Does not contain blanks',
containsErrors: 'Contains errors',
notContainsErrors: 'Does not contain errors',
between: 'Between',
notBetween: 'Not between',
greaterThan: 'Greater than',
greaterThanOrEqual: 'Greater than or equal',
lessThan: 'Less than',
lessThanOrEqual: 'Less than or equal',
},
subRuleType: {
text: 'Text',
number: 'Number',
timePeriod: 'Date',
duplicateValues: 'Duplicate values',
uniqueValues: 'Unique values',
},
ruleType: {
highlightCell: 'Highlight cell',
},
preview: {
describe: {
greaterThan: 'Greater than {0}',
containsText: 'Contains {0}',
},
},
errorMessage: {
notBlank: 'Required',
rangeError: 'Select a range',
},
},
},
});
testBed.setSelection(ACTIVE_RANGE);
return testBed;
}
function writeInput(input: HTMLInputElement, value: string) {
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
valueSetter?.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
}
function clickButton(container: HTMLElement, text: string) {
const button = Array.from(container.querySelectorAll('button'))
.find((item) => item.textContent === text) as HTMLButtonElement | undefined;
if (!button) {
throw new Error(`Button was not rendered: ${text}`);
}
button.click();
}
describe('ConditionFormattingPanel and RuleEdit', () => {
let root: Root | undefined;
let container: HTMLDivElement | undefined;
let currentTestBed: ReturnType<typeof createPanelTestBed> | undefined;
afterEach(() => {
act(() => {
root?.unmount();
});
container?.remove();
currentTestBed?.univer.dispose();
root = undefined;
container = undefined;
currentTestBed = undefined;
});
it('adds a text highlight rule to the active selection from the rule editor', async () => {
currentTestBed = createPanelTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
let cancelRequests = 0;
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleEdit
onCancel={() => {
cancelRequests += 1;
}}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
const textInput = container.querySelector('input[type="text"]') as HTMLInputElement;
await act(async () => {
writeInput(textInput, 'overdue');
await Promise.resolve();
});
await act(async () => {
clickButton(container!, 'Submit');
await Promise.resolve();
await Promise.resolve();
});
expect(currentTestBed.ruleModel.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)).toEqual([
expect.objectContaining({
ranges: [expect.objectContaining(ACTIVE_RANGE)],
stopIfTrue: false,
rule: expect.objectContaining({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
value: 'overdue',
}),
}),
]);
expect(cancelRequests).toBe(1);
});
it('keeps the rule editor open and does not add a rule when the required condition input is empty', async () => {
currentTestBed = createPanelTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
let cancelRequests = 0;
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleEdit
onCancel={() => {
cancelRequests += 1;
}}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
await act(async () => {
clickButton(container!, 'Submit');
await Promise.resolve();
await Promise.resolve();
});
expect(currentTestBed.ruleModel.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId) ?? []).toEqual([]);
expect(cancelRequests).toBe(0);
expect(container.textContent).toContain('Style rule');
});
it('saves changes to an existing number highlight rule without replacing its identity', async () => {
currentTestBed = createPanelTestBed();
const originalRule = {
...createNumberHighlightRule('cf-active', 10),
stopIfTrue: true,
};
await currentTestBed.commandService.executeCommand(AddCfCommand.id, {
unitId: currentTestBed.unitId,
subUnitId: currentTestBed.subUnitId,
rule: originalRule,
});
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
let cancelRequests = 0;
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleEdit
rule={originalRule}
onCancel={() => {
cancelRequests += 1;
}}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
const numberInput = Array.from(container.querySelectorAll('input'))
.find((input) => input.value === '10') as HTMLInputElement | undefined;
if (!numberInput) {
throw new Error('Number condition input was not rendered');
}
await act(async () => {
writeInput(numberInput, '25');
await Promise.resolve();
});
await act(async () => {
clickButton(container!, 'Submit');
await Promise.resolve();
await Promise.resolve();
});
expect(currentTestBed.ruleModel.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)).toEqual([
expect.objectContaining({
cfId: 'cf-active',
ranges: [expect.objectContaining(ACTIVE_RANGE)],
stopIfTrue: true,
rule: expect.objectContaining({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: CFNumberOperator.greaterThan,
value: 25,
}),
}),
]);
expect(cancelRequests).toBe(1);
});
it('opens the clicked worksheet rule for editing and returns to the manager without changing it', async () => {
currentTestBed = createPanelTestBed();
await currentTestBed.commandService.executeCommand(AddCfCommand.id, {
unitId: currentTestBed.unitId,
subUnitId: currentTestBed.subUnitId,
rule: createNumberHighlightRule('cf-active', 10),
});
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<ConditionFormattingPanel />
</RediContext.Provider>
);
await Promise.resolve();
});
const rangeText = Array.from(container.querySelectorAll('div'))
.find((element) => element.textContent === 'A1');
const ruleRow = rangeText?.parentElement?.parentElement as HTMLElement | undefined;
if (!ruleRow) {
throw new Error('Worksheet rule row was not rendered');
}
await act(async () => {
ruleRow.click();
await Promise.resolve();
});
expect(container.textContent).toContain('Range');
expect(container.textContent).toContain('Style type');
await act(async () => {
clickButton(container!, 'Cancel');
await Promise.resolve();
});
expect(container.textContent).toContain('Show');
expect(container.textContent).toContain('A1');
expect(currentTestBed.ruleModel.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)).toEqual([
expect.objectContaining({
cfId: 'cf-active',
rule: expect.objectContaining({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
value: 10,
}),
}),
]);
});
it('cancels rule editing on worksheet activation without saving in-progress changes', async () => {
currentTestBed = createPanelTestBed();
const originalRule = createNumberHighlightRule('cf-active', 10);
await currentTestBed.commandService.executeCommand(AddCfCommand.id, {
unitId: currentTestBed.unitId,
subUnitId: currentTestBed.subUnitId,
rule: originalRule,
});
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
let cancelRequests = 0;
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleEdit
rule={originalRule}
onCancel={() => {
cancelRequests += 1;
}}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
const numberInput = Array.from(container.querySelectorAll('input'))
.find((input) => input.value === '10') as HTMLInputElement | undefined;
if (!numberInput) {
throw new Error('Number condition input was not rendered');
}
await act(async () => {
writeInput(numberInput, '99');
await Promise.resolve();
});
await act(async () => {
await currentTestBed!.commandService.executeCommand(SetWorksheetActiveOperation.id, {
unitId: currentTestBed!.unitId,
subUnitId: currentTestBed!.subUnitId,
});
await Promise.resolve();
});
expect(cancelRequests).toBe(1);
expect(currentTestBed.ruleModel.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)).toEqual([
expect.objectContaining({
cfId: 'cf-active',
rule: expect.objectContaining({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
value: 10,
}),
}),
]);
});
});

View File

@@ -0,0 +1,154 @@
/**
* 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 { IHighlightCell } from '@univerjs/sheets-conditional-formatting';
import type { Root } from 'react-dom/client';
import { BooleanNumber } from '@univerjs/core';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it } from 'vitest';
import { ConditionalStyleEditor } from '../ConditionalStyleEditor';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
describe('ConditionalStyleEditor', () => {
let root: Root | undefined;
let container: HTMLDivElement | undefined;
afterEach(() => {
act(() => {
root?.unmount();
});
container?.remove();
root = undefined;
container = undefined;
});
it('emits highlight style changes when a user toggles text emphasis controls', async () => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
let latestStyle: IHighlightCell['style'] | undefined;
await act(async () => {
root!.render(
<ConditionalStyleEditor
onChange={(style) => {
latestStyle = style;
}}
/>
);
await Promise.resolve();
});
const styleButtons = Array.from(container.firstElementChild!.children) as HTMLElement[];
await act(async () => {
styleButtons[0].dispatchEvent(new MouseEvent('click', { bubbles: true }));
styleButtons[1].dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(latestStyle).toMatchObject({
bl: BooleanNumber.TRUE,
it: BooleanNumber.TRUE,
});
});
it('emits font color changes when a user picks a preset color', async () => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
let latestStyle: IHighlightCell['style'] | undefined;
await act(async () => {
root!.render(
<ConditionalStyleEditor
onChange={(style) => {
latestStyle = style;
}}
/>
);
await Promise.resolve();
});
const styleControls = Array.from(container.firstElementChild!.children) as HTMLElement[];
await act(async () => {
styleControls[4].click();
await Promise.resolve();
});
const firstPresetColor = document.querySelector('[data-u-comp="color-picker-presets"] button') as HTMLButtonElement;
if (!firstPresetColor) {
throw new Error('Preset colors were not rendered');
}
await act(async () => {
firstPresetColor.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(latestStyle).toMatchObject({
cl: {
rgb: '#ffffff',
},
});
});
it('emits background color changes when a user picks a fill preset color', async () => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
let latestStyle: IHighlightCell['style'] | undefined;
await act(async () => {
root!.render(
<ConditionalStyleEditor
onChange={(style) => {
latestStyle = style;
}}
/>
);
await Promise.resolve();
});
const styleControls = Array.from(container.firstElementChild!.children) as HTMLElement[];
await act(async () => {
styleControls[5].click();
await Promise.resolve();
});
const firstPresetColor = document.querySelector('[data-u-comp="color-picker-presets"] button') as HTMLButtonElement;
if (!firstPresetColor) {
throw new Error('Preset colors were not rendered');
}
await act(async () => {
firstPresetColor.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(latestStyle).toMatchObject({
bg: {
rgb: '#ffffff',
},
});
});
});

View File

@@ -0,0 +1,29 @@
/**
* 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 { IConditionalColorPickerProps } from '../ColorPicker';
import { MobileColorPicker, MobileDropdown } from '@univerjs/design';
import { ColorPicker } from '../ColorPicker';
export function MobileConditionalColorPicker(props: IConditionalColorPickerProps) {
return (
<ColorPicker
{...props}
DropdownComponent={MobileDropdown}
PickerComponent={MobileColorPicker}
/>
);
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IConditionFormattingPanelProps } from '../ConditionFormattingPanel';
import { ConditionFormattingPanel } from '../ConditionFormattingPanel';
import { MobileRuleEdit } from './MobileRuleEdit';
import { MobileRuleList } from './MobileRuleList';
export function MobileConditionFormattingPanel(props: IConditionFormattingPanelProps) {
return (
<ConditionFormattingPanel
{...props}
RuleEditComponent={MobileRuleEdit}
RuleListComponent={MobileRuleList}
/>
);
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IRuleEditProps } from '../panel/RuleEdit';
import { MobileDropdown, MobileSelect } from '@univerjs/design';
import { MobileRangeSelector } from '@univerjs/sheets-formula-ui';
import { EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY } from '@univerjs/sheets-ui';
import { ComponentManager, useDependency } from '@univerjs/ui';
import { RuleEdit } from '../panel/RuleEdit';
import { MobileConditionalColorPicker } from './MobileColorPicker';
export function MobileRuleEdit(props: IRuleEditProps) {
const componentManager = useDependency(ComponentManager);
const FormulaEditorComponent = componentManager.get(EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY);
return (
<RuleEdit
{...props}
SelectComponent={MobileSelect}
DropdownComponent={MobileDropdown}
ColorPickerComponent={MobileConditionalColorPicker}
RangeSelectorComponent={MobileRangeSelector}
FormulaEditorComponent={FormulaEditorComponent}
/>
);
}

View File

@@ -0,0 +1,23 @@
/**
* 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 { IRuleListProps } from '../panel/RuleList';
import { MobileSelect } from '@univerjs/design';
import { RuleList } from '../panel/RuleList';
export function MobileRuleList(props: IRuleListProps) {
return <RuleList {...props} SelectComponent={MobileSelect} />;
}

View File

@@ -0,0 +1,331 @@
/**
* 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, Workbook } from '@univerjs/core';
import type { IDropdownProps, ISelectProps } from '@univerjs/design';
import type { IRemoveSheetMutationParams } from '@univerjs/sheets';
import type {
IAddCfCommandParams,
IConditionFormattingRule,
ISetCfCommandParams,
} from '@univerjs/sheets-conditional-formatting';
import type { ComponentType } from 'react';
import type { LocaleKey } from '../../locale/types';
import type { IConditionalColorPickerProps } from '../ColorPicker';
import type { IStyleEditorProps } from './rule-edit/type';
import {
ICommandService,
InterceptorManager,
IUniverInstanceService,
LocaleService,
UniverInstanceType,
} from '@univerjs/core';
import { Button, Select } from '@univerjs/design';
import { deserializeRangeWithSheet, serializeRange } from '@univerjs/engine-formula';
import {
RemoveSheetMutation,
setEndForRange,
SetWorksheetActiveOperation,
SheetsSelectionsService,
} from '@univerjs/sheets';
import {
AddCfCommand,
CFRuleType,
CFSubRuleType,
ConditionalFormattingRuleModel,
SetCfCommand,
} from '@univerjs/sheets-conditional-formatting';
import { FormulaEditor, RangeSelector } from '@univerjs/sheets-formula-ui';
import { useDependency } from '@univerjs/ui';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ColorScaleStyleEditor } from './rule-edit/ColorScale';
import { DataBarStyleEditor } from './rule-edit/DataBar';
import { FormulaStyleEditor } from './rule-edit/Formula';
import { HighlightCellStyleEditor } from './rule-edit/HighlightCell';
import { IconSet } from './rule-edit/IconSet';
import { RankStyleEditor } from './rule-edit/Rank';
import { beforeSubmit, submit } from './rule-edit/type';
export interface IRuleEditProps {
rule?: IConditionFormattingRule;
onCancel: () => void;
SelectComponent?: ComponentType<ISelectProps>;
DropdownComponent?: ComponentType<IDropdownProps>;
ColorPickerComponent?: ComponentType<IConditionalColorPickerProps>;
RangeSelectorComponent?: typeof RangeSelector;
FormulaEditorComponent?: typeof FormulaEditor;
}
const getUnitId = (univerInstanceService: IUniverInstanceService) => univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const getSubUnitId = (univerInstanceService: IUniverInstanceService) => univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
export const RuleEdit = (props: IRuleEditProps) => {
const {
SelectComponent = Select,
DropdownComponent,
ColorPickerComponent,
RangeSelectorComponent = RangeSelector,
FormulaEditorComponent = FormulaEditor,
} = props;
const localeService = useDependency(LocaleService);
const commandService = useDependency(ICommandService);
const univerInstanceService = useDependency(IUniverInstanceService);
const conditionalFormattingRuleModel = useDependency(ConditionalFormattingRuleModel);
const selectionManagerService = useDependency(SheetsSelectionsService);
const unitId = getUnitId(univerInstanceService);
const subUnitId = getSubUnitId(univerInstanceService);
const [errorText, setErrorText] = useState<string | undefined>(undefined);
const rangeResult = useRef<IRange[]>(props.rule?.ranges ?? []);
const rangeSelectorTouched = useRef(false);
const rangeString = useMemo(() => {
let ranges = props.rule?.ranges;
if (!ranges?.length) {
ranges = selectionManagerService.getCurrentSelections()?.map((s) => s.range) ?? [];
}
rangeResult.current = ranges;
if (!ranges?.length) {
return '';
}
return ranges.map((range) => {
const v = serializeRange(range);
return v === 'NaN' ? '' : v;
}).filter((r) => !!r).join(',');
}, [props.rule]);
const options = [
{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.highlightCell'), value: '1' },
{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.rankAndAverage'), value: '2' },
{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.dataBar'), value: '3' },
{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.colorScale'), value: '4' },
{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.formula'), value: '5' },
{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.iconSet'), value: '6' },
];
const [ruleType, setRuleType] = useState(() => {
const type = props.rule?.rule.type;
const defaultType = options[0].value;
if (!type) {
return defaultType;
}
switch (type) {
case CFRuleType.highlightCell: {
const subType = props.rule?.rule.subType;
switch (subType) {
case CFSubRuleType.number:
case CFSubRuleType.text:
case CFSubRuleType.duplicateValues:
case CFSubRuleType.uniqueValues:
case CFSubRuleType.timePeriod: {
return '1';
}
case CFSubRuleType.average:
case CFSubRuleType.rank: {
return '2';
}
case CFSubRuleType.formula: {
return '5';
}
}
break;
}
case CFRuleType.dataBar: {
return '3';
}
case CFRuleType.colorScale: {
return '4';
}
case CFRuleType.iconSet: {
return '6';
}
}
return defaultType;
});
const result = useRef<Parameters<IStyleEditorProps['onChange']>>(undefined);
const interceptorManager = useMemo(() => {
const _interceptorManager = new InterceptorManager({ beforeSubmit, submit });
return _interceptorManager;
}, []);
const StyleEditor = useMemo(() => {
switch (ruleType) {
case '1': {
return HighlightCellStyleEditor;
}
case '2': {
return RankStyleEditor;
}
case '3': {
return DataBarStyleEditor;
}
case '4': {
return ColorScaleStyleEditor;
}
case '5': {
return FormulaStyleEditor;
}
case '6': {
return IconSet;
}
default: {
return HighlightCellStyleEditor;
}
}
}, [ruleType]);
useEffect(() => {
// If the child table which the rule being edited is deleted, exit edit mode
const disposable = commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === RemoveSheetMutation.id) {
const params = commandInfo.params as IRemoveSheetMutationParams;
if (params.subUnitId === subUnitId && params.unitId === unitId) {
props.onCancel();
}
}
if (commandInfo.id === SetWorksheetActiveOperation.id) {
props.onCancel();
}
});
return () => disposable.dispose();
}, []);
const onStyleChange = (config: unknown) => {
result.current = config as Parameters<IStyleEditorProps['onChange']>;
};
const onRangeSelectorChange = (rangeString: string) => {
if (!rangeSelectorTouched.current && rangeString.length < 1 && rangeResult.current.length > 0) {
return;
}
rangeSelectorTouched.current = true;
const result = rangeString.split(',').filter((e) => !!e).map(deserializeRangeWithSheet).map((item) => item.range);
rangeResult.current = result;
};
const handleSubmit = () => {
const getRanges = () => {
const worksheet = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet();
if (!worksheet) {
throw new Error('No active sheet found');
}
const ranges = rangeResult.current.map((range) => setEndForRange(range, worksheet.getRowCount(), worksheet.getColumnCount()));
const result = ranges.filter((range) => !(Number.isNaN(range.startRow) || Number.isNaN(range.startColumn)));
return result;
};
const ranges = getRanges();
if (ranges.length < 1) {
setErrorText(localeService.t<LocaleKey>('sheets-conditional-formatting-ui.errorMessage.rangeError'));
return;
}
const beforeSubmitResult = interceptorManager.fetchThroughInterceptors(interceptorManager.getInterceptPoints().beforeSubmit)(true, null);
if (beforeSubmitResult) {
const result = interceptorManager.fetchThroughInterceptors(interceptorManager.getInterceptPoints().submit)(null, null);
if (result) {
// When you switch the child table, you need to fetch it again here, instead of using the
const unitId = getUnitId(univerInstanceService);
const subUnitId = getSubUnitId(univerInstanceService);
if (!unitId || !subUnitId) {
throw new Error('No active sheet found');
}
let rule = {} as IConditionFormattingRule;
if (props.rule && props.rule.cfId) {
rule = { ...props.rule, ranges, rule: result };
commandService.executeCommand(SetCfCommand.id, { unitId, subUnitId, rule } as ISetCfCommandParams);
props.onCancel();
} else {
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
rule = { cfId, ranges, rule: result, stopIfTrue: false };
commandService.executeCommand(AddCfCommand.id, { unitId, subUnitId, rule } as IAddCfCommandParams);
props.onCancel();
}
}
}
};
const handleCancel = () => {
props.onCancel();
};
const handleVerify = (v: boolean, rangeText: string) => {
if (!rangeSelectorTouched.current && !v && rangeText.length < 1) {
setErrorText(undefined);
return;
}
if (v) {
if (rangeText.length < 1) {
setErrorText(localeService.t<LocaleKey>('sheets-conditional-formatting-ui.errorMessage.rangeError'));
} else {
setErrorText(undefined);
}
} else {
setErrorText(localeService.t<LocaleKey>('sheets-conditional-formatting-ui.errorMessage.rangeError'));
}
};
return (
<div>
<div
className={`
univer-mt-4 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.range')}
</div>
<div className="univer-mt-4">
<RangeSelectorComponent
unitId={unitId}
subUnitId={subUnitId}
initialValue={rangeString}
onChange={(_, text) => onRangeSelectorChange(text)}
onVerify={handleVerify}
/>
{errorText && <div className="univer-mt-1 univer-text-xs univer-text-red-500">{errorText}</div>}
</div>
<div
className={`
univer-mt-4 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.styleType')}
</div>
<SelectComponent
className="univer-mt-4 univer-w-full"
value={ruleType}
options={options}
onChange={(e) => setRuleType(e)}
/>
<StyleEditor
interceptorManager={interceptorManager}
rule={props.rule?.rule as any}
onChange={onStyleChange}
SelectComponent={SelectComponent}
DropdownComponent={DropdownComponent}
ColorPickerComponent={ColorPickerComponent}
FormulaEditorComponent={FormulaEditorComponent}
/>
<div className="univer-mt-4 univer-flex univer-justify-end univer-gap-2">
<Button onClick={handleCancel}>{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.cancel')}</Button>
<Button variant="primary" onClick={handleSubmit}>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.submit')}
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,429 @@
/**
* 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, Workbook } from '@univerjs/core';
import type { ISelectProps } from '@univerjs/design';
import type {
IConditionFormattingRule,
IDeleteCfCommandParams,
IMoveCfCommandParams,
} from '@univerjs/sheets-conditional-formatting';
import type { ComponentType } from 'react';
import type { LocaleKey } from '../../locale/types';
import {
ICommandService,
Injector,
IUniverInstanceService,
LocaleService,
Rectangle,
UniverInstanceType,
} from '@univerjs/core';
import { clsx, DraggableList, Select, Tooltip } from '@univerjs/design';
import { serializeRange } from '@univerjs/engine-formula';
import { DeleteIcon, GripVerticalIcon, IncreaseIcon } from '@univerjs/icons';
import {
checkRangesEditablePermission,
SetSelectionsOperation,
SetWorksheetActiveOperation,
SheetsSelectionsService,
} from '@univerjs/sheets';
import {
AddConditionalRuleMutation,
CFRuleType,
CFSubRuleType,
ClearWorksheetCfCommand,
ConditionalFormattingRuleModel,
DeleteCfCommand,
DeleteConditionalRuleMutation,
MoveCfCommand,
MoveConditionalRuleMutation,
SetConditionalRuleMutation,
} from '@univerjs/sheets-conditional-formatting';
import { useHighlightRange } from '@univerjs/sheets-ui';
import { useDependency, useObservable } from '@univerjs/ui';
import { useMemo, useState } from 'react';
import { debounceTime, filter, map, merge, Observable, share, startWith } from 'rxjs';
import { ConditionalFormattingI18nController } from '../../controllers/cf.i18n.controller';
import { Preview } from '../Preview';
export interface IRuleListProps {
onClick: (rule: IConditionFormattingRule) => void;
onCreate: () => void;
SelectComponent?: ComponentType<ISelectProps>;
};
const getRuleDescribe = (rule: IConditionFormattingRule, localeService: LocaleService) => {
const ruleConfig = rule.rule;
switch (ruleConfig.type) {
case CFRuleType.colorScale: {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.colorScale');
}
case CFRuleType.dataBar: {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.dataBar');
}
case CFRuleType.iconSet: {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.iconSet');
}
case CFRuleType.highlightCell: {
switch (ruleConfig.subType) {
case CFSubRuleType.average: {
const operator = ruleConfig.operator;
return localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.preview.describe.${operator}`, localeService.t<LocaleKey>('sheets-conditional-formatting-ui.subRuleType.average'));
}
case CFSubRuleType.duplicateValues: {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.subRuleType.duplicateValues');
}
case CFSubRuleType.uniqueValues: {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.subRuleType.uniqueValues');
}
case CFSubRuleType.number: {
const operator = ruleConfig.operator;
return localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.preview.describe.${operator}`, ...Array.isArray(ruleConfig.value) ? (ruleConfig.value.map((e) => String(e))) : [String(ruleConfig.value || '')]);
}
case CFSubRuleType.text: {
const operator = ruleConfig.operator;
return localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.preview.describe.${operator}`, ruleConfig.value || '');
}
case CFSubRuleType.timePeriod: {
const operator = ruleConfig.operator;
return localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.preview.describe.${operator}`);
}
case CFSubRuleType.rank: {
if (ruleConfig.isPercent) {
if (ruleConfig.isBottom) {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.preview.describe.bottomNPercent', String(ruleConfig.value));
} else {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.preview.describe.topNPercent', String(ruleConfig.value));
}
} else {
if (ruleConfig.isBottom) {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.preview.describe.bottomN', String(ruleConfig.value));
} else {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.preview.describe.topN', String(ruleConfig.value));
}
}
}
case CFSubRuleType.formula: {
return localeService.t<LocaleKey>('sheets-conditional-formatting-ui.ruleType.formula');
}
}
}
}
};
export function RuleList(props: IRuleListProps) {
const { SelectComponent = Select } = props;
const { onClick } = props;
const conditionalFormattingRuleModel = useDependency(ConditionalFormattingRuleModel);
const univerInstanceService = useDependency(IUniverInstanceService);
const selectionManagerService = useDependency(SheetsSelectionsService);
const commandService = useDependency(ICommandService);
const localeService = useDependency(LocaleService);
const injector = useDependency(Injector);
const conditionalFormattingI18nController = useDependency(ConditionalFormattingI18nController);
const workbook = useObservable(
() => univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET),
undefined,
undefined,
[]
)!;
const unitId = workbook.getUnitId();
const worksheet = workbook.getActiveSheet();
const subUnitId = worksheet.getSheetId();
const [currentRuleRanges, setCurrentRuleRanges] = useState<IRange[]>([]);
const [selectValue, setSelectValue] = useState('2');
const [draggingId, setDraggingId] = useState<string>('');
const selectOption = [
{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.workSheet'), value: '2' },
{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.selectedRange'), value: '1' },
];
const getRuleList = () => {
const ruleList = conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId);
if (!ruleList || !ruleList.length) {
return [];
}
if (selectValue === '1') {
const selection = selectionManagerService.getCurrentLastSelection();
if (!selection) {
return [];
}
const range = selection.range;
const _ruleList = ruleList.filter((rule) => {
return rule.ranges.some((ruleRange) => Rectangle.intersects(ruleRange, range));
});
return _ruleList;
} else if (selectValue === '2') {
return [...ruleList];
}
return [];
};
const ruleList = useObservable(
() => {
const commandEvent$ = new Observable<'immediate' | 'debounced'>((subscriber) => {
const commandList = [
SetSelectionsOperation.id,
AddConditionalRuleMutation.id,
SetConditionalRuleMutation.id,
DeleteConditionalRuleMutation.id,
MoveConditionalRuleMutation.id,
];
const disposable = commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === SetWorksheetActiveOperation.id) {
subscriber.next('immediate');
return;
}
const commandUnitId = (commandInfo.params as { unitId?: string } | undefined)?.unitId;
if (selectValue === '1' && commandList.includes(commandInfo.id) && commandUnitId === unitId) {
subscriber.next('debounced');
}
});
return () => disposable.dispose();
}).pipe(share());
return merge(
conditionalFormattingRuleModel.$ruleChange,
commandEvent$.pipe(filter((event) => event === 'immediate')),
commandEvent$.pipe(
filter((event) => event === 'debounced'),
debounceTime(16)
)
).pipe(
map(getRuleList),
startWith(getRuleList())
);
},
[],
false,
[commandService, conditionalFormattingRuleModel, selectValue, subUnitId, unitId]
);
useHighlightRange(currentRuleRanges);
const handleDelete = (rule: IConditionFormattingRule) => {
const unitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const subUnitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
if (!unitId || !subUnitId) {
throw new Error('No active sheet found');
}
commandService.executeCommand(DeleteCfCommand.id, { unitId, subUnitId, cfId: rule.cfId } as IDeleteCfCommandParams);
};
const handleDragStart = (_layout: unknown, from: { y: number }) => {
const dragRule = ruleListByPermissionCheck[from.y];
setDraggingId(dragRule?.cfId ?? '');
};
const handleDragStop = (_layout: unknown, from: { y: number }, to: { y: number }) => {
setDraggingId('');
const unitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const subUnitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
if (!unitId || !subUnitId) {
throw new Error('No active sheet found');
}
const getSaveIndex = (index: number) => {
const length = ruleListByPermissionCheck.length;
return Math.min(length - 1, Math.max(0, index));
};
const cfId = ruleListByPermissionCheck[getSaveIndex(from.y)]?.cfId;
const targetCfId = ruleListByPermissionCheck[getSaveIndex(to.y)]?.cfId;
if (!cfId || !targetCfId) {
return;
}
if (cfId !== targetCfId) {
commandService.executeCommand(MoveCfCommand.id, { unitId, subUnitId, start: { id: cfId, type: 'self' }, end: { id: targetCfId, type: to.y > from.y ? 'after' : 'before' } } as IMoveCfCommandParams);
}
};
const handleCreate = () => {
props.onCreate();
};
const handleClear = () => {
if (selectValue === '2') {
commandService.executeCommand(ClearWorksheetCfCommand.id);
} else if (selectValue === '1') {
const list = ruleList.map((rule) => ({ unitId, subUnitId, cfId: rule.cfId }));
list.forEach((config) => {
commandService.executeCommand(DeleteCfCommand.id, config);
});
}
};
const ruleListByPermissionCheck = useMemo(() => {
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet();
return ruleList.filter((rule) => {
const ranges = rule.ranges;
const hasPermission = checkRangesEditablePermission(injector, workbook.getUnitId(), worksheet.getSheetId(), ranges);
return hasPermission;
});
}, [ruleList]);
const isHasAllRuleEditPermission = useMemo(() => {
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet();
return ruleList.every((rule) => {
const ranges = rule.ranges;
const hasPermission = checkRangesEditablePermission(injector, workbook.getUnitId(), worksheet.getSheetId(), ranges);
return hasPermission;
});
}, [ruleList]);
return (
<div>
<div
className="
univer-mb-2 univer-flex univer-items-center univer-justify-between univer-gap-2 univer-text-sm
"
>
<div className="univer-flex univer-items-center univer-gap-2">
{conditionalFormattingI18nController.tWithReactNode(
'sheets-conditional-formatting-ui.panel.managerRuleSelect',
<SelectComponent
className="univer-w-36"
options={selectOption}
value={selectValue}
onChange={(v) => { setSelectValue(v); }}
/>
).map((ele, index) => <span key={index}>{ele}</span>)}
</div>
<div className="univer-flex univer-justify-end univer-gap-2">
<Tooltip title={localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.createRule')} placement="bottom">
<a
className="univer-size-5 univer-cursor-pointer"
onClick={handleCreate}
>
<IncreaseIcon />
</a>
</Tooltip>
{(ruleList.length && isHasAllRuleEditPermission)
? (
<Tooltip title={localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.clear')} placement="bottom">
<a
className="univer-size-5 univer-cursor-pointer"
onClick={handleClear}
>
<DeleteIcon className="univer-text-red-500" />
</a>
</Tooltip>
)
: (
<div>
<DeleteIcon className="univer-text-gray-300" />
</div>
)}
</div>
</div>
<div>
<DraggableList
className="univer-w-full"
draggableHandle=".draggableHandle"
list={ruleListByPermissionCheck}
onListChange={() => undefined}
idKey="cfId"
rowHeight={60}
margin={[0, 10]}
onDragStop={handleDragStop}
onDragStart={handleDragStart}
itemRender={(rule) => (
<div
className={clsx(`
univer-group univer-relative univer-flex univer-items-center univer-justify-between
univer-rounded univer-py-2 univer-pl-5 univer-pr-8
hover:univer-bg-gray-100
rtl:univer-pl-8 rtl:univer-pr-5
dark:hover:!univer-bg-gray-700
`, {
'univer-bg-gray-100 dark:!univer-bg-gray-700': draggingId === rule.cfId,
})}
onMouseMove={() => {
rule.ranges !== currentRuleRanges && setCurrentRuleRanges(rule.ranges);
}}
onMouseLeave={() => setCurrentRuleRanges([])}
onClick={() => {
onClick(rule);
}}
>
<div
className={clsx(`
univer-absolute univer-left-0 univer-hidden univer-size-5 univer-cursor-grab
univer-items-center univer-justify-center univer-rounded
group-hover:univer-flex
rtl:univer-left-auto rtl:univer-right-0
`, 'draggableHandle')}
onClick={(e) => e.stopPropagation()}
>
<GripVerticalIcon />
</div>
<div
className="univer-min-w-0 univer-max-w-full univer-flex-shrink univer-overflow-hidden"
>
<div
className={`
univer-text-sm univer-text-gray-900
dark:!univer-text-gray-0
`}
>
{getRuleDescribe(rule, localeService)}
</div>
<div
className="
univer-max-w-[250px] univer-overflow-hidden univer-text-ellipsis univer-text-xs
univer-text-gray-400
"
>
{rule.ranges.map((range) => serializeRange(range)).join(',')}
</div>
</div>
<div>
<Preview rule={rule.rule} />
</div>
<div
className={clsx(`
univer-absolute univer-right-1 univer-hidden univer-size-6 univer-cursor-pointer
univer-items-center univer-justify-center univer-rounded univer-text-red-500
group-hover:univer-flex
hover:univer-bg-gray-200
rtl:univer-left-1 rtl:univer-right-auto
`, {
'univer-flex univer-items-center univer-justify-center': draggingId === rule.cfId,
})}
onClick={(e) => {
e.stopPropagation();
handleDelete(rule);
setCurrentRuleRanges([]);
}}
>
<DeleteIcon />
</div>
</div>
)}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,561 @@
/**
* 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 { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import type { Root } from 'react-dom/client';
import { BooleanNumber, LocaleService, LocaleType, RANGE_TYPE } from '@univerjs/core';
import { SetSelectionsOperation } from '@univerjs/sheets';
import {
AddCfCommand,
CFNumberOperator,
CFRuleType,
CFSubRuleType,
DeleteCfCommand,
MoveCfCommand,
} from '@univerjs/sheets-conditional-formatting';
import { IMarkSelectionService } from '@univerjs/sheets-ui';
import { RediContext } from '@univerjs/ui';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it } from 'vitest';
import { createCfUiTestBed } from '../../../__tests__/create-cf-ui-test-bed';
import { ConditionalFormattingI18nController } from '../../../controllers/cf.i18n.controller';
import { RuleList } from '../RuleList';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const ACTIVE_RANGE: IRange = {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
};
const FAR_RANGE: IRange = {
startRow: 5,
endRow: 5,
startColumn: 5,
endColumn: 5,
};
class TestMarkSelectionService {
readonly addedRanges: IRange[] = [];
removeCount = 0;
addShape(selection: { range: IRange }) {
this.addedRanges.push(selection.range);
return 'shape-id';
}
removeShape() {
this.removeCount += 1;
}
}
function createNumberHighlightRule(cfId: string, range: IRange, value: number): IConditionFormattingRule {
return {
cfId,
ranges: [range],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: CFNumberOperator.greaterThan,
value,
style: {
bl: BooleanNumber.TRUE,
},
},
};
}
async function createRuleListTestBed() {
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingI18nController]);
testBed.injector.add([IMarkSelectionService, { useClass: TestMarkSelectionService as never }]);
testBed.commandService.registerCommand(AddCfCommand);
testBed.commandService.registerCommand(DeleteCfCommand);
testBed.commandService.registerCommand(MoveCfCommand);
testBed.commandService.registerCommand(SetSelectionsOperation);
testBed.get(LocaleService).load({
[LocaleType.ZH_CN]: {
sheets: {
conditional: {
formatting: {
ui: {
panel: {
managerRuleSelect: 'Show {0}',
workSheet: 'Worksheet',
selectedRange: 'Selected range',
createRule: 'Create rule',
clear: 'Clear rules',
},
preview: {
describe: {
greaterThan: 'Greater than {0}',
},
},
},
},
},
},
'sheets-conditional-formatting-ui': {
panel: {
managerRuleSelect: 'Show {0}',
workSheet: 'Worksheet',
selectedRange: 'Selected range',
createRule: 'Create rule',
clear: 'Clear rules',
},
preview: {
describe: {
greaterThan: 'Greater than {0}',
},
},
},
},
});
testBed.setSelection(ACTIVE_RANGE);
await testBed.commandService.executeCommand(AddCfCommand.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: createNumberHighlightRule('cf-active', ACTIVE_RANGE, 10),
});
await testBed.commandService.executeCommand(AddCfCommand.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: createNumberHighlightRule('cf-far', FAR_RANGE, 20),
});
return testBed;
}
describe('RuleList', () => {
let root: Root | undefined;
let container: HTMLDivElement | undefined;
let currentTestBed: Awaited<ReturnType<typeof createRuleListTestBed>> | undefined;
afterEach(() => {
act(() => {
root?.unmount();
});
container?.remove();
currentTestBed?.univer.dispose();
root = undefined;
container = undefined;
currentTestBed = undefined;
});
it('shows only conditional formatting rules intersecting the current sheet selection in selected-range mode', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList onClick={() => undefined} onCreate={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
expect(container.textContent).toContain('A1');
expect(container.textContent).toContain('F6');
await act(async () => {
container!.querySelector('[data-u-comp="select"]')!.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
await Promise.resolve();
});
const selectedRangeOption = Array.from(document.querySelectorAll('[data-slot="dropdown-menu-radio-item"]'))
.find((button) => button.textContent === 'Selected range');
expect(selectedRangeOption).toBeDefined();
await act(async () => {
selectedRangeOption!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(container.textContent).toContain('A1');
expect(container.textContent).not.toContain('F6');
});
it('clears only the conditional formatting rules visible in selected-range mode', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList onClick={() => undefined} onCreate={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
await act(async () => {
container!.querySelector('[data-u-comp="select"]')!.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
await Promise.resolve();
});
const selectedRangeOption = Array.from(document.querySelectorAll('[data-slot="dropdown-menu-radio-item"]'))
.find((button) => button.textContent === 'Selected range');
expect(selectedRangeOption).toBeDefined();
await act(async () => {
selectedRangeOption!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(container.textContent).toContain('A1');
expect(container.textContent).not.toContain('F6');
const clearRulesButton = container.querySelectorAll('a')[1];
expect(clearRulesButton).toBeDefined();
await act(async () => {
clearRulesButton!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
const remainingRules = currentTestBed.ruleModel.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId);
expect(remainingRules?.map((rule) => rule.cfId)).toEqual(['cf-far']);
expect(container.textContent).not.toContain('A1');
});
it('refreshes selected-range rules when the active sheet selection changes', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList onClick={() => undefined} onCreate={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
await act(async () => {
container!.querySelector('[data-u-comp="select"]')!.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
await Promise.resolve();
});
const selectedRangeOption = Array.from(document.querySelectorAll('[data-slot="dropdown-menu-radio-item"]'))
.find((button) => button.textContent === 'Selected range');
expect(selectedRangeOption).toBeDefined();
await act(async () => {
selectedRangeOption!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(container.textContent).toContain('A1');
expect(container.textContent).not.toContain('F6');
await act(async () => {
await currentTestBed!.commandService.executeCommand(SetSelectionsOperation.id, {
unitId: currentTestBed!.unitId,
subUnitId: currentTestBed!.subUnitId,
selections: [{
range: {
...FAR_RANGE,
rangeType: RANGE_TYPE.NORMAL,
},
primary: null,
style: null,
}],
});
await new Promise((resolve) => setTimeout(resolve, 32));
});
expect(container.textContent).not.toContain('A1');
expect(container.textContent).toContain('F6');
});
it('deletes only the clicked conditional formatting rule', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const openedRuleIds: string[] = [];
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList onClick={(rule) => openedRuleIds.push(rule.cfId)} onCreate={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
const activeRangeText = Array.from(container.querySelectorAll('div'))
.find((element) => element.textContent === 'A1');
const activeRuleRow = activeRangeText?.parentElement?.parentElement as HTMLElement | undefined;
const activeRuleDeleteButton = activeRuleRow?.lastElementChild as HTMLElement | undefined;
if (!activeRuleDeleteButton) {
throw new Error('Rule delete control was not rendered');
}
await act(async () => {
activeRuleDeleteButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
const remainingRules = currentTestBed.ruleModel.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId);
expect(openedRuleIds).toEqual([]);
expect(remainingRules?.map((rule) => rule.cfId)).toEqual(['cf-far']);
expect(container.textContent).not.toContain('A1');
expect(container.textContent).toContain('F6');
});
it('opens the clicked conditional formatting rule for editing without changing worksheet rules', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const openedRuleIds: string[] = [];
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList onClick={(rule) => openedRuleIds.push(rule.cfId)} onCreate={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
const activeRangeText = Array.from(container.querySelectorAll('div'))
.find((element) => element.textContent === 'A1');
const activeRuleRow = activeRangeText?.parentElement?.parentElement as HTMLElement | undefined;
const ruleIdsBeforeOpen = currentTestBed.ruleModel
.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)
?.map((rule) => rule.cfId);
if (!activeRuleRow) {
throw new Error('Rule row was not rendered');
}
await act(async () => {
activeRuleRow.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(openedRuleIds).toEqual(['cf-active']);
expect(
currentTestBed.ruleModel
.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)
?.map((rule) => rule.cfId)
).toEqual(ruleIdsBeforeOpen);
});
it('highlights the hovered conditional formatting rule range and clears it on leave', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList onClick={() => undefined} onCreate={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
const activeRangeText = Array.from(container.querySelectorAll('div'))
.find((element) => element.textContent === 'A1');
const activeRuleRow = activeRangeText?.parentElement?.parentElement as HTMLElement | undefined;
const markSelectionService = currentTestBed.injector.get(IMarkSelectionService) as unknown as TestMarkSelectionService;
if (!activeRuleRow) {
throw new Error('Rule row was not rendered');
}
await act(async () => {
activeRuleRow.dispatchEvent(new MouseEvent('mousemove', { bubbles: true }));
await Promise.resolve();
});
expect(markSelectionService.addedRanges).toEqual([ACTIVE_RANGE]);
await act(async () => {
activeRuleRow.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
await Promise.resolve();
await Promise.resolve();
});
expect(markSelectionService.removeCount).toBe(1);
});
it('clears all worksheet conditional formatting rules in worksheet mode', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList onClick={() => undefined} onCreate={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
expect(container.textContent).toContain('A1');
expect(container.textContent).toContain('F6');
const clearRulesButton = container.querySelectorAll('a')[1];
expect(clearRulesButton).toBeDefined();
await act(async () => {
clearRulesButton!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(currentTestBed.ruleModel.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)).toEqual([]);
expect(container.textContent).not.toContain('A1');
expect(container.textContent).not.toContain('F6');
});
it('starts the create-rule flow from the toolbar without changing existing worksheet rules', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
let createRequests = 0;
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList
onClick={() => undefined}
onCreate={() => {
createRequests += 1;
}}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
const createRuleButton = container.querySelectorAll('a')[0];
const ruleIdsBeforeCreate = currentTestBed.ruleModel
.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)
?.map((rule) => rule.cfId);
await act(async () => {
createRuleButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(createRequests).toBe(1);
expect(
currentTestBed.ruleModel
.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)
?.map((rule) => rule.cfId)
).toEqual(ruleIdsBeforeCreate);
});
it('moves a dragged worksheet rule after the drop target rule', async () => {
currentTestBed = await createRuleListTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RuleList onClick={() => undefined} onCreate={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
const ruleRows = Array.from(container.querySelectorAll<HTMLElement>('[data-draggable-list-item-id]'));
const firstRuleRow = ruleRows[0];
const secondRuleRow = ruleRows[1];
const dragHandle = firstRuleRow?.querySelector<HTMLElement>('.draggableHandle');
if (!firstRuleRow || !secondRuleRow || !dragHandle) {
throw new Error('Rule drag controls were not rendered');
}
expect(
currentTestBed.ruleModel
.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)
?.map((rule) => rule.cfId)
).toEqual(['cf-far', 'cf-active']);
Object.defineProperty(firstRuleRow, 'setPointerCapture', {
configurable: true,
value() {},
});
const elementFromPoint = document.elementFromPoint;
Object.defineProperty(document, 'elementFromPoint', {
configurable: true,
value: () => secondRuleRow,
});
try {
await act(async () => {
dragHandle.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: 1, clientY: 1, pointerId: 7 }));
await Promise.resolve();
});
await act(async () => {
window.dispatchEvent(new PointerEvent('pointermove', { bubbles: true, clientX: 1, clientY: 80, pointerId: 7 }));
await Promise.resolve();
});
await act(async () => {
window.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX: 1, clientY: 80, pointerId: 7 }));
await Promise.resolve();
});
} finally {
Object.defineProperty(document, 'elementFromPoint', {
configurable: true,
value: elementFromPoint,
});
}
expect(
currentTestBed.ruleModel
.getSubunitRules(currentTestBed.unitId, currentTestBed.subUnitId)
?.map((rule) => rule.cfId)
).toEqual(['cf-active', 'cf-far']);
});
});

View File

@@ -0,0 +1,478 @@
/**
* 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 { IColorScale } from '@univerjs/sheets-conditional-formatting';
import type { IFormulaEditorRef } from '@univerjs/sheets-formula-ui';
import type { LocaleKey } from '../../../locale/types';
import type { IStyleEditorProps } from './type';
import { IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
import { borderClassName, clsx, InputNumber, Select } from '@univerjs/design';
import { CFRuleType, CFValueType, createDefaultValueByValueType } from '@univerjs/sheets-conditional-formatting';
import { FormulaEditor } from '@univerjs/sheets-formula-ui';
import { useDependency, useSidebarClick } from '@univerjs/ui';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ColorPicker } from '../../ColorPicker';
import { Preview } from '../../Preview';
import { previewClassName } from './styles';
const createOptionItem = (text: CFValueType | 'none'): { label: LocaleKey; value: CFValueType | 'none' } => ({
label: `sheets-conditional-formatting-ui.valueType.${text}`,
value: text,
});
const TextInput = (props: {
id: string;
type: CFValueType | 'none';
value: number | string;
onChange: (v: number | string) => void;
className?: string;
FormulaEditorComponent: NonNullable<IStyleEditorProps['FormulaEditorComponent']>;
}) => {
const { type, className, onChange, value, FormulaEditorComponent } = props;
const univerInstanceService = useDependency(IUniverInstanceService);
const unitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const subUnitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
const formulaInitValue = useMemo(() => {
return String(value).startsWith('=') ? String(value) : '=';
}, [value]);
const config = useMemo(() => {
if ([CFValueType.max, CFValueType.min, 'none'].includes(type as CFValueType)) {
return { disabled: true };
}
if ([CFValueType.percent, CFValueType.percentile].includes(type as CFValueType)) {
return {
min: 0,
max: 100,
};
}
return {
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
};
}, [type]);
const formulaEditorRef = useRef<IFormulaEditorRef>(null);
const [isFocusFormulaEditor, setIsFocusFormulaEditor] = useState(false);
useSidebarClick((e: MouseEvent) => {
const isOutSide = formulaEditorRef.current?.isClickOutSide(e);
isOutSide && setIsFocusFormulaEditor(false);
});
if (type === CFValueType.formula) {
return (
<div className="univer-w-full">
<FormulaEditorComponent
ref={formulaEditorRef}
className={clsx(`
univer-box-border univer-h-8 univer-w-full univer-cursor-pointer univer-items-center
univer-rounded-lg univer-bg-gray-0 univer-pt-2 univer-transition-colors
hover:univer-border-primary-600
dark:!univer-bg-gray-700 dark:!univer-text-gray-0
[&>div:first-child]:univer-px-2.5
[&>div]:univer-h-5 [&>div]:univer-ring-transparent
`, borderClassName)}
initValue={formulaInitValue as any}
unitId={unitId}
subUnitId={subUnitId}
isFocus={isFocusFormulaEditor}
onChange={(v = '') => {
const formula = v || '';
onChange(formula);
}}
onFocus={() => setIsFocusFormulaEditor(true)}
/>
</div>
);
} else {
return <InputNumber className={className} value={Number(props.value) || 0} onChange={(v) => props.onChange(v || 0)} {...config} />;
}
};
export const ColorScaleStyleEditor = (props: IStyleEditorProps) => {
const {
interceptorManager,
SelectComponent = Select,
ColorPickerComponent = ColorPicker,
FormulaEditorComponent = FormulaEditor,
} = props;
const localeService = useDependency(LocaleService);
const rule = props.rule?.type === CFRuleType.colorScale ? props.rule : undefined as IColorScale | undefined;
const commonOptionDefinitions = [createOptionItem(CFValueType.num), createOptionItem(CFValueType.percent), createOptionItem(CFValueType.percentile), createOptionItem(CFValueType.formula)];
const minOptions = [createOptionItem(CFValueType.min), ...commonOptionDefinitions].map((option) => ({ ...option, label: localeService.t(option.label) }));
const medianOptions = [createOptionItem('none'), ...commonOptionDefinitions].map((option) => ({ ...option, label: localeService.t(option.label) }));
const maxOptions = [createOptionItem(CFValueType.max), ...commonOptionDefinitions].map((option) => ({ ...option, label: localeService.t(option.label) }));
const [minType, setMinType] = useState(() => {
const defaultV = CFValueType.min;
if (!rule) {
return defaultV;
}
return rule.config[0]?.value.type || defaultV;
});
const [medianType, setMedianType] = useState<CFValueType | 'none'>(() => {
const defaultV = 'none';
if (!rule) {
return defaultV;
}
if (rule.config.length !== 3) {
return defaultV;
}
return rule.config[1]?.value.type || defaultV;
});
const [maxType, setMaxType] = useState(() => {
const defaultV = CFValueType.max;
if (!rule) {
return defaultV;
}
return rule.config[rule.config.length - 1]?.value.type || defaultV;
});
const [minValue, setMinValue] = useState(() => {
const defaultV = 10;
if (!rule) {
return defaultV;
}
const valueConfig = rule.config[0];
return valueConfig?.value.value === undefined ? defaultV : valueConfig?.value.value;
});
const [medianValue, setMedianValue] = useState(() => {
const defaultV = 50;
if (!rule) {
return defaultV;
}
if (rule.config.length !== 3) {
return defaultV;
}
const v = rule.config[1]?.value.value;
return v === undefined ? defaultV : v;
});
const [maxValue, setMaxValue] = useState(() => {
const defaultV = 90;
if (!rule) {
return defaultV;
}
const v = rule.config[rule.config.length - 1]?.value.value;
return v === undefined ? defaultV : v;
});
const [minColor, setMinColor] = useState(() => {
const defaultV = '#d0d9fb';
if (!rule) {
return defaultV;
}
return rule.config[0]?.color || defaultV;
});
const [medianColor, setMedianColor] = useState(() => {
const defaultV = '#7790f3';
if (!rule) {
return defaultV;
}
if (rule.config.length !== 3) {
return defaultV;
}
return rule.config[1]?.color || defaultV;
});
const [maxColor, setMaxColor] = useState(() => {
const defaultV = '#2e55ef';
if (!rule) {
return defaultV;
}
return rule.config[rule.config.length - 1]?.color || defaultV;
});
const getResult = useMemo(() => (option: {
minType: typeof minType;
medianType: typeof medianType;
maxType: typeof maxType;
minValue: number | string;
medianValue: number | string;
maxValue: number | string;
minColor: string;
medianColor: string;
maxColor: string;
}): IColorScale => {
const { minType, medianType, maxType, minValue, medianValue, maxValue, minColor, medianColor, maxColor } = option;
const list = [];
list.push({ color: minColor, value: { type: minType, value: minValue } });
medianType !== 'none' && list.push({ color: medianColor, value: { type: medianType, value: medianValue } });
list.push({ color: maxColor, value: { type: maxType, value: maxValue } });
const config: IColorScale['config'] = list.map((item, index) => ({ ...item, index }));
return { config, type: CFRuleType.colorScale };
}, []);
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().submit, {
handler() {
return getResult({ minType, medianType, maxType, minValue, medianValue, maxValue, minColor, medianColor, maxColor });
},
});
return dispose as () => void;
}, [getResult, minType, medianType, maxType, minValue, medianValue, maxValue, minColor, medianColor, maxColor, interceptorManager]);
const handleChange = (option: {
minType: typeof minType;
medianType: typeof medianType;
maxType: typeof maxType;
minValue: number | string;
medianValue: number | string;
maxValue: number | string;
minColor: string;
medianColor: string;
maxColor: string;
}) => {
props.onChange(getResult(option));
};
return (
<div>
<div
className={`
univer-mt-4 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.styleRule')}
</div>
<div className={previewClassName}>
<Preview
rule={getResult({
minType,
medianType,
maxType,
minValue,
medianValue,
maxValue,
minColor,
medianColor,
maxColor,
})}
/>
</div>
<div
className={`
univer-mt-3 univer-text-xs univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.valueType.min')}
</div>
<div className="univer-mt-3 univer-flex univer-h-8 univer-items-center univer-gap-2">
<SelectComponent
className="univer-flex-shrink-0"
options={minOptions}
value={minType}
onChange={(v) => {
setMinType(v as CFValueType);
const value = createDefaultValueByValueType(v as CFValueType, 10);
setMinValue(value);
handleChange({
minType: v as CFValueType,
medianType,
maxType,
minValue: value,
medianValue,
maxValue,
minColor,
medianColor,
maxColor,
});
}}
/>
<TextInput
id="min"
value={minValue}
type={minType}
FormulaEditorComponent={FormulaEditorComponent}
onChange={(v) => {
setMinValue(v);
handleChange({
minType,
medianType,
maxType,
minValue: v,
medianValue,
maxValue,
minColor,
medianColor,
maxColor,
});
}}
/>
<ColorPickerComponent
color={minColor}
onChange={(v) => {
setMinColor(v);
handleChange({
minType,
medianType,
maxType,
minValue,
medianValue,
maxValue,
minColor: v,
medianColor,
maxColor,
});
}}
/>
</div>
<div
className={`
univer-mt-3 univer-text-xs univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.medianValue')}
</div>
<div className="univer-mt-3 univer-flex univer-h-8 univer-items-center univer-gap-2">
<SelectComponent
className="univer-flex-shrink-0"
options={medianOptions}
value={medianType}
onChange={(v) => {
setMedianType(v as CFValueType);
const value = createDefaultValueByValueType(v as CFValueType, 50);
setMedianValue(value);
handleChange({
minType,
medianType: v as CFValueType,
maxType,
minValue,
medianValue: value,
maxValue,
minColor,
medianColor,
maxColor,
});
}}
/>
<TextInput
id="median"
value={medianValue}
type={medianType}
FormulaEditorComponent={FormulaEditorComponent}
onChange={(v) => {
setMedianValue(v);
handleChange({
minType,
medianType,
maxType,
minValue,
medianValue: v,
maxValue,
minColor,
medianColor,
maxColor,
});
}}
/>
{medianType !== 'none'
? (
<ColorPickerComponent
color={medianColor}
onChange={(v) => {
setMedianColor(v);
handleChange({
minType,
medianType,
maxType,
minValue,
medianValue,
maxValue,
minColor,
medianColor: v,
maxColor,
});
}}
/>
)
: <div aria-hidden className="univer-size-6 univer-flex-shrink-0" />}
</div>
<div
className={`
univer-mt-3 univer-text-xs univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.valueType.max')}
</div>
<div className="univer-mt-3 univer-flex univer-h-8 univer-items-center univer-gap-2">
<SelectComponent
className="univer-flex-shrink-0"
options={maxOptions}
value={maxType}
onChange={(v) => {
setMaxType(v as CFValueType);
const value = createDefaultValueByValueType(v as CFValueType, 90);
setMaxValue(value);
handleChange({
minType,
medianType,
maxType: v as CFValueType,
minValue,
medianValue,
maxValue: value,
minColor,
medianColor,
maxColor,
});
}}
/>
<TextInput
id="max"
value={maxValue}
type={maxType}
FormulaEditorComponent={FormulaEditorComponent}
onChange={(v) => {
setMaxValue(v);
handleChange({
minType,
medianType,
maxType,
minValue,
medianValue,
maxValue: v,
minColor,
medianColor,
maxColor,
});
}}
/>
<ColorPickerComponent
color={maxColor}
onChange={(v) => {
setMaxColor(v);
handleChange({
minType,
medianType,
maxType,
minValue,
medianValue,
maxValue,
minColor,
medianColor,
maxColor: v,
});
}}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,494 @@
/**
* 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 { IDataBar, IValueConfig } from '@univerjs/sheets-conditional-formatting';
import type { IFormulaEditorRef } from '@univerjs/sheets-formula-ui';
import type { LocaleKey } from '../../../locale/types';
import type { IStyleEditorProps } from './type';
import { IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
import { borderClassName, Checkbox, clsx, InputNumber, Radio, RadioGroup, Select } from '@univerjs/design';
import {
CFRuleType,
CFValueType,
createDefaultValueByValueType,
defaultDataBarNativeColor,
defaultDataBarPositiveColor,
} from '@univerjs/sheets-conditional-formatting';
import { FormulaEditor } from '@univerjs/sheets-formula-ui';
import { useDependency, useSidebarClick } from '@univerjs/ui';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ColorPicker } from '../../ColorPicker';
import { Preview } from '../../Preview';
import { previewClassName } from './styles';
const createOptionItem = (text: CFValueType): { label: LocaleKey; value: CFValueType } => ({
label: `sheets-conditional-formatting-ui.valueType.${text}`,
value: text,
});
const InputText = (props: {
disabled?: boolean;
id: string;
className?: string;
type: CFValueType;
value: string | number;
onChange: (v: string | number) => void;
FormulaEditorComponent: NonNullable<IStyleEditorProps['FormulaEditorComponent']>;
}) => {
const { onChange, className, value, type, disabled = false, FormulaEditorComponent } = props;
const univerInstanceService = useDependency(IUniverInstanceService);
const unitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const subUnitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
const formulaEditorRef = useRef<IFormulaEditorRef>(null);
const [isFocusFormulaEditor, setIsFocusFormulaEditor] = useState(false);
useSidebarClick((e: MouseEvent) => {
const isOutSide = formulaEditorRef.current?.isClickOutSide(e);
isOutSide && setIsFocusFormulaEditor(false);
});
const _value = useRef(value);
const config = useMemo(() => {
if ([CFValueType.percentile, CFValueType.percent].includes(type)) {
return {
max: 100,
min: 0,
};
}
return {
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
};
}, [type]);
if (type === CFValueType.formula) {
const v = String(_value.current).startsWith('=') ? String(_value.current) || '' : '=';
return (
<div className="univer-w-full">
<FormulaEditorComponent
ref={formulaEditorRef}
className={clsx(`
univer-box-border univer-h-8 univer-w-full univer-cursor-pointer univer-items-center
univer-rounded-lg univer-bg-gray-0 univer-pt-2 univer-transition-colors
hover:univer-border-primary-600
dark:!univer-bg-gray-700 dark:!univer-text-gray-0
[&>div:first-child]:univer-px-2.5
[&>div]:univer-h-5 [&>div]:univer-ring-transparent
`, borderClassName)}
initValue={v as any}
unitId={unitId}
subUnitId={subUnitId}
isFocus={isFocusFormulaEditor}
onChange={(v = '') => {
const formula = v || '';
onChange(formula);
}}
onFocus={() => setIsFocusFormulaEditor(true)}
/>
</div>
);
}
return (
<InputNumber
className={className}
value={Number(value) || 0}
disabled={disabled}
onChange={(v) => {
onChange(v || 0);
}}
{...config}
/>
);
};
export const DataBarStyleEditor = (props: IStyleEditorProps) => {
const {
interceptorManager,
SelectComponent = Select,
ColorPickerComponent = ColorPicker,
FormulaEditorComponent = FormulaEditor,
} = props;
const localeService = useDependency(LocaleService);
const rule = props.rule?.type === CFRuleType.dataBar ? props.rule : undefined;
const [isGradient, setIsGradient] = useState(() => {
const defaultV = '0';
if (!rule) {
return defaultV;
}
return rule.config?.isGradient ? '1' : '0';
});
const [positiveColor, setPositiveColor] = useState(() => {
if (!rule) {
return defaultDataBarPositiveColor;
}
return rule.config?.positiveColor || defaultDataBarPositiveColor;
});
const [nativeColor, setNativeColor] = useState(() => {
if (!rule) {
return defaultDataBarNativeColor;
}
return rule.config?.nativeColor || defaultDataBarNativeColor;
});
const commonOptionDefinitions = [
createOptionItem(CFValueType.num),
createOptionItem(CFValueType.percent),
createOptionItem(CFValueType.percentile),
createOptionItem(CFValueType.formula),
];
const minOptions = [createOptionItem(CFValueType.min), ...commonOptionDefinitions].map((option) => ({ ...option, label: localeService.t(option.label) }));
const maxOptions = [createOptionItem(CFValueType.max), ...commonOptionDefinitions].map((option) => ({ ...option, label: localeService.t(option.label) }));
const [minValueType, setMinValueType] = useState<CFValueType>(() => {
const defaultV = minOptions[0].value as CFValueType;
if (!rule) {
return defaultV;
}
return rule.config?.min.type || defaultV;
});
const [maxValueType, setMaxValueType] = useState<CFValueType>(() => {
const defaultV = maxOptions[0].value as CFValueType;
if (!rule) {
return defaultV;
}
return rule.config?.max.type || defaultV;
});
const [minValue, setMinValue] = useState(() => {
const defaultV = 0;
if (!rule) {
return defaultV;
}
const value = rule.config?.min || {};
if (value.type === CFValueType.formula) {
return value.value || '=';
}
return value.value || defaultV;
});
const [maxValue, setMaxValue] = useState(() => {
const defaultV = 100;
if (!rule) {
return defaultV;
}
const value = rule.config?.max || {};
if (value.type === CFValueType.formula) {
return value.value || '=';
}
return value.value === undefined ? defaultV : value.value;
});
const [isShowValue, setIsShowValue] = useState(() => {
const defaultV = true;
if (!rule) {
return defaultV;
}
return rule.isShowValue === undefined ? defaultV : !!rule.isShowValue;
});
const getResult = (option: {
minValueType: CFValueType;
minValue: number | string;
maxValueType: CFValueType;
maxValue: number | string;
isGradient: string;
positiveColor: string;
isShowValue: boolean;
nativeColor: string;
}): IDataBar => {
const config: {
min: IValueConfig;
max: IValueConfig;
isGradient: boolean;
positiveColor: string;
nativeColor: string;
} = {
min: { type: option.minValueType, value: option.minValue },
max: { type: option.maxValueType, value: option.maxValue },
isGradient: option.isGradient === '1',
positiveColor: option.positiveColor || defaultDataBarPositiveColor,
nativeColor: option.nativeColor || defaultDataBarNativeColor,
};
return { config, type: CFRuleType.dataBar, isShowValue: option.isShowValue };
};
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().submit, {
handler() {
return getResult({ isGradient, minValue, minValueType, maxValue, maxValueType, positiveColor, nativeColor, isShowValue });
},
});
return dispose as () => void;
}, [isGradient, minValue, minValueType, maxValue, maxValueType, positiveColor, nativeColor, interceptorManager, isShowValue]);
const handleChange = (option: {
minValueType: CFValueType;
minValue: number | string;
maxValueType: CFValueType;
maxValue: number | string;
isGradient: string;
positiveColor: string;
isShowValue: boolean;
nativeColor: string;
}) => {
props.onChange(getResult(option));
};
const handlePositiveColorChange = (color: string) => {
setPositiveColor(color);
handleChange({
isGradient,
minValue,
minValueType,
maxValue,
maxValueType,
positiveColor: color,
nativeColor,
isShowValue,
});
};
const handleNativeColorChange = (color: string) => {
setNativeColor(color);
handleChange({
isGradient,
minValue,
minValueType,
maxValue,
maxValueType,
positiveColor,
nativeColor: color,
isShowValue,
});
};
const isShowInput = (type: string) => {
return commonOptionDefinitions.map((item) => item.value).includes(type as CFValueType);
};
return (
<div>
<div
className={`
univer-mt-4 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.styleRule')}
</div>
<div className={previewClassName}>
<Preview
rule={getResult({
isGradient,
minValue,
minValueType,
maxValue,
maxValueType,
positiveColor,
nativeColor,
isShowValue,
})}
/>
</div>
<div>
<div
className={`
univer-mt-3 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.fillType')}
</div>
<div className="univer-mt-3 univer-flex univer-items-center univer-gap-3">
<RadioGroup
value={isGradient}
onChange={(v) => {
setIsGradient(v as string);
handleChange({
isGradient: v as string,
minValue,
minValueType,
maxValue,
maxValueType,
positiveColor,
nativeColor,
isShowValue,
});
}}
>
<Radio value="0">
<span className="univer-text-xs">{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.pureColor')}</span>
</Radio>
<Radio value="1">
<span className="univer-text-xs">{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.gradient')}</span>
</Radio>
</RadioGroup>
<div className="univer-flex univer-items-center univer-text-xs">
<Checkbox
checked={!isShowValue}
onChange={(v) => {
setIsShowValue(!v);
handleChange({
isGradient: v as string,
minValue,
minValueType,
maxValue,
maxValueType,
positiveColor,
nativeColor,
isShowValue: !v,
});
}}
/>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.onlyShowDataBar')}
</div>
</div>
</div>
<div>
<div
className={`
univer-mt-3 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.colorSet')}
</div>
<div className="univer-mt-3 univer-flex univer-items-center univer-gap-2">
<div className="univer-flex univer-items-center">
<div className="univer-text-xs">
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.native')}
</div>
<ColorPickerComponent
color={nativeColor}
onChange={handleNativeColorChange}
/>
</div>
<div className="univer-flex univer-items-center univer-gap-3">
<div className="univer-text-xs">
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.positive')}
</div>
<ColorPickerComponent
color={positiveColor}
onChange={handlePositiveColorChange}
/>
</div>
</div>
</div>
<div>
<div
className={`
univer-mt-3 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.valueType.min')}
</div>
<div className="univer-mt-3 univer-flex univer-items-center univer-gap-2">
<SelectComponent
className="univer-w-1/2 univer-flex-shrink-0"
options={minOptions}
value={minValueType}
onChange={(v) => {
setMinValueType(v as CFValueType);
const value = createDefaultValueByValueType(v as CFValueType, 10);
setMinValue(value);
handleChange({
isGradient,
minValue: value,
minValueType: v as CFValueType,
maxValue,
maxValueType,
positiveColor,
nativeColor,
isShowValue,
});
}}
/>
<InputText
id="min"
disabled={!isShowInput(minValueType)}
type={minValueType}
value={minValue}
FormulaEditorComponent={FormulaEditorComponent}
onChange={(v) => {
setMinValue(v || 0);
handleChange({
isGradient,
minValue: v || 0,
minValueType,
maxValue,
maxValueType,
positiveColor,
nativeColor,
isShowValue,
});
}}
/>
</div>
<div
className={`
univer-mt-3 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.valueType.max')}
</div>
<div className="univer-mt-3 univer-flex univer-items-center univer-gap-2">
<SelectComponent
className="univer-w-1/2 univer-flex-shrink-0"
options={maxOptions}
value={maxValueType}
onChange={(v) => {
setMaxValueType(v as CFValueType);
const value = createDefaultValueByValueType(v as CFValueType, 90);
setMaxValue(value);
handleChange({
isGradient,
minValue,
minValueType,
maxValue: value,
maxValueType: v as CFValueType,
positiveColor,
nativeColor,
isShowValue,
});
}}
/>
<InputText
disabled={!isShowInput(maxValueType)}
id="max"
type={maxValueType}
value={maxValue}
FormulaEditorComponent={FormulaEditorComponent}
onChange={(v) => {
setMaxValue(v || 0);
handleChange({
isGradient,
minValue,
minValueType,
maxValue: v || 0,
maxValueType,
positiveColor,
nativeColor,
isShowValue,
});
}}
/>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,174 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Workbook } from '@univerjs/core';
import type {
IFormulaHighlightCell,
IHighlightCell,
} from '@univerjs/sheets-conditional-formatting';
import type { IFormulaEditorRef } from '@univerjs/sheets-formula-ui';
import type { LocaleKey } from '../../../locale/types';
import type { IStyleEditorProps } from './type';
import { IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
import { borderClassName, clsx } from '@univerjs/design';
import { LexerTreeBuilder, operatorToken } from '@univerjs/engine-formula';
import { CFRuleType, CFSubRuleType } from '@univerjs/sheets-conditional-formatting';
import { FormulaEditor } from '@univerjs/sheets-formula-ui';
import { useDependency, useSidebarClick } from '@univerjs/ui';
import { useEffect, useRef, useState } from 'react';
import { ConditionalStyleEditor } from '../../ConditionalStyleEditor';
import { Preview } from '../../Preview';
import { previewClassName } from './styles';
export const FormulaStyleEditor = (props: IStyleEditorProps) => {
const { onChange, interceptorManager, ColorPickerComponent, FormulaEditorComponent = FormulaEditor } = props;
const localeService = useDependency(LocaleService);
const lexerTreeBuilder = useDependency(LexerTreeBuilder);
const univerInstanceService = useDependency(IUniverInstanceService);
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet();
const rule = props.rule?.type === CFRuleType.highlightCell ? props.rule : undefined;
const divEleRef = useRef<HTMLDivElement>(null);
const [isFocusFormulaEditor, setIsFocusFormulaEditor] = useState(false);
const formulaEditorRef = useRef<IFormulaEditorRef>(null);
const [style, setStyle] = useState<IHighlightCell['style']>({});
const [formula, setFormula] = useState(() => {
if (rule?.subType === CFSubRuleType.formula) {
return rule.value;
}
return '=';
});
const [formulaError, setFormulaError] = useState<string | undefined>(undefined);
const getResult = (config: {
style: IHighlightCell['style'];
formula: string;
}): IFormulaHighlightCell => {
return {
style: config.style,
value: formula,
type: CFRuleType.highlightCell,
subType: CFSubRuleType.formula,
};
};
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().submit, {
handler() {
return getResult({ style, formula });
},
});
return dispose as () => void;
}, [style, formula, interceptorManager]);
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().beforeSubmit, {
handler: (v, _c, next) => {
const isFormulaValid = formula.length > 1
&& formula.startsWith(operatorToken.EQUALS)
&& lexerTreeBuilder.checkIfAddBracket(formula) === 0;
if (formulaError || !isFormulaValid) {
setFormulaError(localeService.t<LocaleKey>('sheets-conditional-formatting-ui.errorMessage.formulaError'));
return false;
}
return next(v);
},
});
return dispose as () => void;
}, [
formulaError,
formula,
interceptorManager,
lexerTreeBuilder,
localeService,
]);
const _onChange = (config: {
formula: string;
style: IHighlightCell['style'];
}) => {
onChange(getResult(config));
};
useSidebarClick((e: MouseEvent) => {
const isOutSide = formulaEditorRef.current?.isClickOutSide(e);
isOutSide && setIsFocusFormulaEditor(false);
});
return (
<div ref={divEleRef}>
<div
className={`
univer-mt-4 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.styleRule')}
</div>
<div className="univer-mt-3">
<FormulaEditorComponent
ref={formulaEditorRef}
className={clsx(`
univer-box-border univer-h-8 univer-w-full univer-cursor-pointer univer-items-center
univer-rounded-lg univer-bg-gray-0 univer-pt-2 univer-transition-colors
hover:univer-border-primary-600
dark:!univer-bg-gray-700 dark:!univer-text-gray-0
[&>div:first-child]:univer-px-2.5
[&>div]:univer-h-5 [&>div]:univer-ring-transparent
`, borderClassName)}
errorText={formulaError}
isFocus={isFocusFormulaEditor}
initValue={formula as any}
unitId={workbook.getUnitId()}
subUnitId={worksheet?.getSheetId()}
onFocus={() => { setIsFocusFormulaEditor(true); }}
onChange={(formula) => {
setFormula(formula);
_onChange({ style, formula });
}}
onVerify={(result, formula) => {
if (!result || formula.length === 1) {
setFormulaError(localeService.t<LocaleKey>('sheets-conditional-formatting-ui.errorMessage.formulaError'));
} else {
setFormulaError(undefined);
}
}}
/>
</div>
<div className={previewClassName}>
<Preview
rule={getResult({
style,
formula,
})}
/>
</div>
<ConditionalStyleEditor
ColorPickerComponent={ColorPickerComponent}
style={rule?.style}
className="univer-mt-3"
onChange={(v) => {
setStyle(v);
_onChange({ style: v, formula });
}}
/>
</div>
);
};

View File

@@ -0,0 +1,455 @@
/**
* 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 {
IDuplicateValuesHighlightCell,
IHighlightCell,
INumberHighlightCell,
ITextHighlightCell,
ITimePeriodHighlightCell,
IUniqueValuesHighlightCell,
} from '@univerjs/sheets-conditional-formatting';
import type { LocaleKey } from '../../../locale/types';
import type { IStyleEditorProps } from './type';
import { LocaleService } from '@univerjs/core';
import { Input, InputNumber, Select } from '@univerjs/design';
import {
CFNumberOperator,
CFRuleType,
CFSubRuleType,
CFTextOperator,
CFTimePeriodOperator,
createDefaultValue,
} from '@univerjs/sheets-conditional-formatting';
import { useDependency } from '@univerjs/ui';
import { useEffect, useMemo, useState } from 'react';
import { ConditionalStyleEditor } from '../../ConditionalStyleEditor';
import { Preview } from '../../Preview';
import { WrapperError } from '../../wrapper-error/WrapperError';
import { previewClassName } from './styles';
type IEditableHighlightCell = ITextHighlightCell | INumberHighlightCell | ITimePeriodHighlightCell | IDuplicateValuesHighlightCell | IUniqueValuesHighlightCell;
type IEditableSubType = IEditableHighlightCell['subType'];
type IEditableOperator = ITextHighlightCell['operator'] | INumberHighlightCell['operator'] | ITimePeriodHighlightCell['operator'];
const textOperators = new Set<string>(Object.values(CFTextOperator));
const numberOperators = new Set<string>(Object.values(CFNumberOperator));
const timePeriodOperators = new Set<string>(Object.values(CFTimePeriodOperator));
const isTextOperator = (value: unknown): value is CFTextOperator => typeof value === 'string' && textOperators.has(value);
const isNumberOperator = (value: unknown): value is CFNumberOperator => typeof value === 'string' && numberOperators.has(value);
const isTimePeriodOperator = (value: unknown): value is CFTimePeriodOperator => typeof value === 'string' && timePeriodOperators.has(value);
const createOptionItem = <T extends IEditableOperator>(text: T): { label: LocaleKey; value: T } => ({
label: `sheets-conditional-formatting-ui.operator.${text}`,
value: text,
});
type IValue = number | string | [number, number];
function HighlightCellInput(props: {
type: IEditableSubType;
operator?: IEditableOperator;
interceptorManager: IStyleEditorProps['interceptorManager'];
rule?: IHighlightCell;
value: IValue;
onChange: (value: IValue) => void;
}) {
const { type, operator, onChange, value, interceptorManager } = props;
const localeService = useDependency(LocaleService);
const [inputNumberValue, setInputNumberValue] = useState(() => typeof value === 'number' ? value : 0);
const [numberError, setNumberError] = useState('');
const [inputTextValue, setInputTextValue] = useState(() => typeof value === 'string' ? value : '');
const [textError, setTextError] = useState('');
const [inputNumberMin, setInputNumberMin] = useState(() => Array.isArray(value) ? value[0] === undefined ? 0 : value[0] : 0);
const [numberMinError, setNumberMinError] = useState('');
const [inputNumberMax, setInputNumberMax] = useState(() => Array.isArray(value) ? value[1] === undefined ? 100 : value[1] : 100);
const [numberMaxError, setNumberMaxError] = useState('');
useEffect(() => {
switch (type) {
case CFSubRuleType.text: {
if ([CFTextOperator.beginsWith, CFTextOperator.endsWith, CFTextOperator.containsText, CFTextOperator.notContainsText, CFTextOperator.equal, CFTextOperator.notEqual].includes(operator as CFTextOperator)) {
onChange(inputTextValue);
}
break;
}
case CFSubRuleType.number: {
if ([CFNumberOperator.equal, CFNumberOperator.notEqual, CFNumberOperator.greaterThan, CFNumberOperator.greaterThanOrEqual, CFNumberOperator.lessThan, CFNumberOperator.lessThanOrEqual].includes(operator as CFNumberOperator)) {
onChange(inputNumberValue);
}
if ([CFNumberOperator.between, CFNumberOperator.notBetween].includes(operator as CFNumberOperator)) {
onChange([inputNumberMin, inputNumberMax]);
}
break;
}
}
}, [type]);
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().beforeSubmit, {
handler: (v, _c, next) => {
switch (type) {
case CFSubRuleType.text: {
if ([CFTextOperator.beginsWith, CFTextOperator.containsText, CFTextOperator.endsWith, CFTextOperator.notEqual, CFTextOperator.notContainsText, CFTextOperator.equal].includes(operator as any)) {
if (!inputTextValue) {
setTextError(localeService.t<LocaleKey>('sheets-conditional-formatting-ui.errorMessage.notBlank'));
return false;
}
return next(v);
}
}
}
return next(v);
},
});
return () => {
dispose();
};
}, [type, inputNumberValue, inputTextValue, operator]);
switch (type) {
case CFSubRuleType.text: {
if ([CFTextOperator.beginsWith, CFTextOperator.endsWith, CFTextOperator.containsText, CFTextOperator.notContainsText, CFTextOperator.equal, CFTextOperator.notEqual].includes(operator as CFTextOperator)) {
const _onChange = (value: string) => {
setInputTextValue(value);
onChange(value);
};
return (
<div className="univer-mt-3">
<WrapperError errorText={textError}>
<Input
value={inputTextValue}
onChange={(v) => {
setTextError('');
_onChange(v);
}}
/>
</WrapperError>
</div>
);
}
break;
}
case CFSubRuleType.number: {
if ([CFNumberOperator.equal, CFNumberOperator.notEqual, CFNumberOperator.greaterThan, CFNumberOperator.greaterThanOrEqual, CFNumberOperator.lessThan, CFNumberOperator.lessThanOrEqual].includes(operator as CFNumberOperator)) {
const _onChange = (value: number | null) => {
setInputNumberValue(value || 0);
onChange(value || 0);
setNumberError('');
};
return (
<div className="univer-mt-3">
<WrapperError errorText={numberError}>
<InputNumber
className="univer-w-full"
min={Number.MIN_SAFE_INTEGER}
max={Number.MAX_SAFE_INTEGER}
value={inputNumberValue}
onChange={_onChange}
/>
</WrapperError>
</div>
);
}
if ([CFNumberOperator.between, CFNumberOperator.notBetween].includes(operator as CFNumberOperator)) {
const onChangeMin = (_value: number | null) => {
setInputNumberMin(_value || 0);
const value: [number, number] = [_value || 0, inputNumberMax];
onChange(value);
setNumberMinError('');
};
const onChangeMax = (_value: number | null) => {
setInputNumberMax(_value || 0);
const value: [number, number] = [inputNumberMin, _value || 0];
onChange(value);
setNumberMaxError('');
};
return (
<div className="univer-mt-3 univer-flex univer-items-center">
<WrapperError errorText={numberMinError}>
<InputNumber min={Number.MIN_SAFE_INTEGER} max={Number.MAX_SAFE_INTEGER} value={inputNumberMin} onChange={onChangeMin} />
</WrapperError>
<WrapperError errorText={numberMaxError}>
<InputNumber
className="univer-ml-3"
min={Number.MIN_SAFE_INTEGER}
max={Number.MAX_SAFE_INTEGER}
value={inputNumberMax}
onChange={onChangeMax}
/>
</WrapperError>
</div>
);
}
}
}
return null;
};
const getOperatorOptions = (type: IEditableSubType) => {
switch (type) {
case CFSubRuleType.text: {
return [
createOptionItem(CFTextOperator.containsText),
createOptionItem(CFTextOperator.notContainsText),
createOptionItem(CFTextOperator.beginsWith),
createOptionItem(CFTextOperator.endsWith),
createOptionItem(CFTextOperator.equal),
createOptionItem(CFTextOperator.notEqual),
createOptionItem(CFTextOperator.containsBlanks),
createOptionItem(CFTextOperator.notContainsBlanks),
createOptionItem(CFTextOperator.containsErrors),
createOptionItem(CFTextOperator.notContainsErrors),
];
}
case CFSubRuleType.number: {
return [
createOptionItem(CFNumberOperator.between),
createOptionItem(CFNumberOperator.notBetween),
createOptionItem(CFNumberOperator.equal),
createOptionItem(CFNumberOperator.notEqual),
createOptionItem(CFNumberOperator.greaterThan),
createOptionItem(CFNumberOperator.greaterThanOrEqual),
createOptionItem(CFNumberOperator.lessThan),
createOptionItem(CFNumberOperator.lessThanOrEqual),
];
}
case CFSubRuleType.timePeriod: {
return [
createOptionItem(CFTimePeriodOperator.yesterday),
createOptionItem(CFTimePeriodOperator.today),
createOptionItem(CFTimePeriodOperator.tomorrow),
createOptionItem(CFTimePeriodOperator.last7Days),
createOptionItem(CFTimePeriodOperator.lastWeek),
createOptionItem(CFTimePeriodOperator.thisWeek),
createOptionItem(CFTimePeriodOperator.nextWeek),
createOptionItem(CFTimePeriodOperator.lastMonth),
createOptionItem(CFTimePeriodOperator.thisMonth),
createOptionItem(CFTimePeriodOperator.nextMonth),
];
}
default:
return [];
}
};
export const HighlightCellStyleEditor = (props: IStyleEditorProps<any, IEditableHighlightCell>) => {
const { interceptorManager, onChange, SelectComponent = Select, ColorPickerComponent } = props;
const localeService = useDependency(LocaleService);
const rule = props.rule?.type === CFRuleType.highlightCell ? props.rule : undefined;
const [subType, setSubType] = useState<IEditableSubType>(() => {
const defaultV = CFSubRuleType.text;
if (!rule) {
return defaultV;
}
return rule.subType || defaultV;
});
const typeOptions: Array<{ value: IEditableSubType; label: string }> = [{
value: CFSubRuleType.text,
label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.subRuleType.text'),
}, {
value: CFSubRuleType.number,
label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.subRuleType.number'),
}, {
value: CFSubRuleType.timePeriod,
label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.subRuleType.timePeriod'),
}, {
value: CFSubRuleType.duplicateValues,
label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.subRuleType.duplicateValues'),
}, {
value: CFSubRuleType.uniqueValues,
label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.subRuleType.uniqueValues'),
}];
const operatorOptions = useMemo(() => getOperatorOptions(subType).map((option) => ({ ...option, label: localeService.t(option.label) })), [localeService, subType]);
const [operator, setOperator] = useState<IEditableOperator | undefined>(() => {
const defaultV = operatorOptions[0]?.value;
if (!rule) {
return defaultV;
}
return 'operator' in rule ? rule.operator : defaultV;
});
const [value, setValue] = useState<IValue>(() => {
const defaultV = '';
if (!rule) {
return defaultV;
}
if ('value' in rule && rule.value !== undefined) {
return rule.value;
}
return 'operator' in rule ? createDefaultValue(rule.subType, rule.operator) : defaultV;
});
const [style, setStyle] = useState<IHighlightCell['style']>({});
const getResult = useMemo(() => (option: { subType?: IEditableSubType; operator?: IEditableOperator; value?: IValue; style?: IHighlightCell['style'] }): IEditableHighlightCell => {
const nextSubType = option.subType ?? subType;
const nextOperator = option.operator ?? operator;
const nextValue = option.value ?? value;
const nextStyle = option.style ?? style;
switch (nextSubType) {
case CFSubRuleType.duplicateValues: {
return {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.duplicateValues,
style: nextStyle,
};
}
case CFSubRuleType.uniqueValues:
return {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.uniqueValues,
style: nextStyle,
};
case CFSubRuleType.text: {
return {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: isTextOperator(nextOperator) ? nextOperator : CFTextOperator.containsText,
style: nextStyle,
value: typeof nextValue === 'string' ? nextValue : undefined,
};
}
case CFSubRuleType.timePeriod: {
return {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.timePeriod,
operator: isTimePeriodOperator(nextOperator) ? nextOperator : CFTimePeriodOperator.today,
style: nextStyle,
};
}
case CFSubRuleType.number: {
const numberOperator = isNumberOperator(nextOperator) ? nextOperator : CFNumberOperator.equal;
if (numberOperator === CFNumberOperator.between || numberOperator === CFNumberOperator.notBetween) {
const range: [number, number] = Array.isArray(nextValue) ? nextValue : [0, 100];
return {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: numberOperator,
style: nextStyle,
value: range,
};
}
return {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: numberOperator,
style: nextStyle,
value: typeof nextValue === 'number' ? nextValue : undefined,
};
}
}
}, [subType, operator, value, style]);
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().submit, {
handler() {
return getResult({});
},
});
return dispose as () => void;
}, [getResult, interceptorManager]);
useEffect(() => {
if (!typeOptions.some((item) => item.value === subType)) {
setSubType(typeOptions[0].value as CFSubRuleType.text);
}
}, [typeOptions]);
const onTypeChange = (v: string) => {
const _subType = typeOptions.find((item) => item.value === v)?.value;
if (!_subType) {
return;
}
const operatorList = getOperatorOptions(_subType).map((option) => ({ ...option, label: localeService.t(option.label) }));
const _operator = operatorList[0]?.value;
setSubType(_subType);
setOperator(_operator);
_operator && setValue(createDefaultValue(_subType, _operator));
onChange(getResult({ subType: _subType, operator: _operator }));
};
const onOperatorChange = (v: string) => {
const _operator = operatorOptions.find((item) => item.value === v)?.value;
if (!_operator) {
return;
}
setOperator(_operator);
onChange(getResult({ operator: _operator }));
};
const onInputChange = (value: number | string | [number, number]) => {
setValue(value);
onChange(getResult({ value }));
};
return (
<div>
<div
className={`
univer-mt-4 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.styleRule')}
</div>
<div className="univer-flex univer-justify-between univer-gap-4">
<SelectComponent
className="univer-mt-3 univer-w-full"
onChange={onTypeChange}
value={subType}
options={typeOptions}
/>
{operatorOptions?.length && (
<SelectComponent
className="univer-mt-3 univer-w-full"
onChange={onOperatorChange}
value={operator || ''}
options={operatorOptions}
/>
)}
</div>
<HighlightCellInput
key={`${subType}_${operator}`}
value={value}
interceptorManager={interceptorManager}
type={subType}
operator={operator}
rule={rule}
onChange={onInputChange}
/>
<div className={previewClassName}>
<Preview rule={getResult({})} />
</div>
<ConditionalStyleEditor
ColorPickerComponent={ColorPickerComponent}
style={rule?.style}
className="univer-ml-1"
onChange={(v) => {
setStyle(v);
onChange(getResult({ style: v }));
}}
/>
</div>
);
};

View File

@@ -0,0 +1,704 @@
/**
* 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 { IIconSet } from '@univerjs/sheets-conditional-formatting';
import type { IFormulaEditorRef } from '@univerjs/sheets-formula-ui';
import type { LocaleKey } from '../../../locale/types';
import type { IStyleEditorProps } from './type';
import { get, IUniverInstanceService, LocaleService, set, Tools, UniverInstanceType } from '@univerjs/core';
import { borderClassName, Checkbox, clsx, Dropdown, InputNumber, Select } from '@univerjs/design';
import { MoreDownIcon, SlashDoubleIcon } from '@univerjs/icons';
import {
CFNumberOperator,
CFRuleType,
CFSubRuleType,
CFValueType,
compareWithNumber,
createDefaultValue,
getOppositeOperator,
iconGroup,
iconMap,
IIconSetType,
} from '@univerjs/sheets-conditional-formatting';
import { FormulaEditor } from '@univerjs/sheets-formula-ui';
import { useDependency, useObservable, useSidebarClick } from '@univerjs/ui';
import { useEffect, useMemo, useRef, useState } from 'react';
const getIcon = (iconType: IIconSetType, iconId: string | number) => {
const arr = iconMap[iconType] || [];
return arr[Number(iconId)] || '';
};
const TextInput = (props: {
id: number;
type: CFValueType;
value: number | string;
onChange: (v: number | string) => void;
error?: string;
FormulaEditorComponent: NonNullable<IStyleEditorProps['FormulaEditorComponent']>;
}) => {
const { error, type, onChange, FormulaEditorComponent } = props;
const univerInstanceService = useDependency(IUniverInstanceService);
const unitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const subUnitId = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
const formulaEditorRef = useRef<IFormulaEditorRef>(null);
const [isFocusFormulaEditor, setIsFocusFormulaEditor] = useState(false);
useSidebarClick((e: MouseEvent) => {
const isOutSide = formulaEditorRef.current?.isClickOutSide(e);
isOutSide && setIsFocusFormulaEditor(false);
});
return (
<div className="univer-relative">
{type !== CFValueType.formula
? (
<>
<InputNumber
className={clsx({
'univer-border-red-500': error,
})}
value={Number(props.value) || 0}
onChange={(v) => onChange(v ?? 0)}
/>
{error && (
<div className="univer-absolute univer-text-xs univer-text-red-500">
{error}
</div>
)}
</>
)
: (
<div className="univer-w-full">
<FormulaEditorComponent
ref={formulaEditorRef}
className={clsx(`
univer-box-border univer-h-8 univer-w-full univer-cursor-pointer univer-items-center
univer-rounded-lg univer-bg-gray-0 univer-pt-2 univer-transition-colors
hover:univer-border-primary-600
dark:!univer-bg-gray-700 dark:!univer-text-gray-0
[&>div:first-child]:univer-px-2.5
[&>div]:univer-h-5 [&>div]:univer-ring-transparent
`, borderClassName)}
initValue={String(props.value) as any}
unitId={unitId}
subUnitId={subUnitId}
isFocus={isFocusFormulaEditor}
onChange={(v = '') => {
const formula = v || '';
onChange(formula);
}}
onFocus={() => setIsFocusFormulaEditor(true)}
/>
</div>
)}
</div>
);
};
const createDefaultConfigItem = (iconType: IIconSetType, index: number, list: unknown[]): IIconSet['config'][number] => ({
operator: CFNumberOperator.greaterThan,
value: { type: CFValueType.num, value: (list.length - 1 - index) * 10 },
iconType,
iconId: String(index),
});
interface IIconGroupListProps {
onClick: (iconType: IIconSetType) => void;
iconType?: IIconSetType;
};
const IconGroupList = (props: IIconGroupListProps) => {
const { iconType, onClick } = props;
const localeService = useDependency(LocaleService);
const direction = useObservable(localeService.direction$, localeService.getDirection());
return (
<div dir={direction} className="univer-w-80">
{iconGroup.map((group) => {
return (
<div
key={group.title}
className="
univer-mb-4
last:univer-mb-0
"
>
<div
className="
univer-mb-2 univer-px-1 univer-text-xs univer-font-medium univer-text-gray-500
dark:!univer-text-gray-400
"
>
{localeService.t(group.title)}
</div>
<div
dir={direction}
data-u-comp="cf-icon-set-group-grid"
className="univer-grid univer-grid-cols-2 univer-gap-1"
>
{group.group.map((groupItem, groupItemIndex) => {
const selected = groupItem.name === iconType;
return (
<button
key={groupItem.name}
type="button"
aria-label={`${localeService.t(group.title)} ${groupItemIndex + 1}`}
aria-pressed={selected}
className={clsx(
`
univer-flex univer-h-8 univer-w-full univer-cursor-pointer
univer-items-center univer-justify-start univer-rounded-md univer-border-0
univer-bg-transparent univer-px-2 univer-outline-none
univer-transition-colors
focus-visible:univer-ring-2 focus-visible:univer-ring-primary-600
`,
selected
? `
univer-bg-primary-50 univer-ring-1 univer-ring-primary-600
dark:!univer-bg-primary-900
`
: `
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
`
)}
onClick={() => { onClick(groupItem.name); }}
>
<span dir={direction} className="univer-flex univer-items-center univer-gap-0.5">
{groupItem.list.map((base64, index) => (
<img
key={index}
alt=""
className="univer-size-5"
src={base64}
draggable={false}
/>
))}
</span>
</button>
);
})}
</div>
</div>
);
})}
</div>
);
};
const IconItemList = (props: { onClick: (iconType: IIconSetType, iconId: string) => void; iconType?: IIconSetType; iconId: string }) => {
const { onClick } = props;
const localeService = useDependency(LocaleService);
const direction = useObservable(localeService.direction$, localeService.getDirection());
const list = useMemo(() => {
const result: { iconType: IIconSetType; iconId: string; base64: string }[] = [];
for (const key in iconMap) {
const iconType = key as IIconSetType;
const list = iconMap[iconType];
list.forEach((base64, index) => {
result.push({
iconType,
base64,
iconId: String(index),
});
});
}
return result;
}, []);
const handleClick = (item: typeof list[0]) => {
onClick(item.iconType, item.iconId);
};
return (
<div dir={direction}>
<div
data-u-comp="cf-icon-set-no-icon-option"
className="
univer-mb-2.5 univer-flex univer-cursor-pointer univer-items-center univer-gap-2 univer-text-sm
"
onClick={() => handleClick({ iconType: IIconSetType.empty, iconId: '', base64: '' })}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.noCellIcon')}
</div>
<div
className="univer-flex univer-w-64 univer-flex-wrap univer-gap-2"
>
{list.map((item) => (
<div
key={`${item.iconType}_${item.iconId}`}
className={`
univer-mb-2 univer-flex univer-cursor-pointer univer-items-center univer-justify-center
univer-rounded
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
`}
>
<img
className="univer-size-5"
src={item.base64}
draggable={false}
onClick={() => handleClick(item)}
/>
</div>
))}
</div>
</div>
);
};
const IconSetRuleEdit = (props: {
configList: IIconSet['config'];
onChange: (keys: string[], value: unknown) => void;
errorMap?: Record<string, string>;
DropdownComponent: NonNullable<IStyleEditorProps['DropdownComponent']>;
SelectComponent: NonNullable<IStyleEditorProps['SelectComponent']>;
FormulaEditorComponent: NonNullable<IStyleEditorProps['FormulaEditorComponent']>;
}) => {
const {
onChange,
configList,
errorMap = {},
DropdownComponent,
SelectComponent,
FormulaEditorComponent,
} = props;
const localeService = useDependency(LocaleService);
const options = [{
label: localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.symbol.${CFNumberOperator.greaterThan}`),
value: CFNumberOperator.greaterThan,
}, {
label: localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.symbol.${CFNumberOperator.greaterThanOrEqual}`),
value: CFNumberOperator.greaterThanOrEqual,
}];
const valueTypeOptions = [
{
label: localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.valueType.${CFValueType.num}`),
value: CFValueType.num,
},
{
label: localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.valueType.${CFValueType.percent}`),
value: CFValueType.percent,
},
{
label: localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.valueType.${CFValueType.percentile}`),
value: CFValueType.percentile,
},
{
label: localeService.t<LocaleKey>(`sheets-conditional-formatting-ui.valueType.${CFValueType.formula}`),
value: CFValueType.formula,
},
];
const handleValueValueChange = (v: number | string, index: number) => {
onChange([String(index), 'value', 'value'], v);
};
const handleOperatorChange = (operator: CFNumberOperator, index: number) => {
onChange([String(index), 'operator'], operator);
const defaultValue = createDefaultValue(CFSubRuleType.number, operator) as number;
handleValueValueChange(defaultValue, index);
};
const handleValueTypeChange = (v: string, index: number) => {
onChange([String(index), 'value', 'type'], v);
const item = configList[index];
const defaultValue = createDefaultValue(CFSubRuleType.number, item.operator) as number;
handleValueValueChange(defaultValue, index);
};
const render = useMemo(() => {
return configList.map((item, index) => {
const error = errorMap[index];
const icon = getIcon(item.iconType, item.iconId);
const isEnd = index === configList.length - 1;
const isFirst = index === 0;
const preItem = configList[index - 1];
const lessThanText = preItem?.value.type === CFValueType.formula ? localeService.t<LocaleKey>('sheets-conditional-formatting-ui.valueType.formula') : preItem?.value.value;
const handleIconClick = (iconType: IIconSetType, iconId: string) => {
const value = { ...item, iconId, iconType } as typeof item;
onChange([String(index)], value);
};
return (
<div
key={index}
className={index ? 'univer-mt-6' : 'univer-mt-3'}
>
<div
className={`
univer-mt-3 univer-flex univer-items-center univer-justify-between univer-text-sm
univer-text-gray-600
dark:!univer-text-gray-200
`}
>
<div
className="univer-w-[45%]"
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.icon')}
{index + 1}
</div>
<div className="univer-w-[45%]">
<>
{!isFirst && !isEnd && localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.rule')}
{!isFirst && !isEnd && (
<span
className={`
univer-font-medium univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.when')}
{' '}
{localeService.t(`sheets-conditional-formatting-ui.symbol.${getOppositeOperator(preItem.operator)}`)}
{lessThanText}
{isEnd ? '' : ` ${localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.and')} `}
</span>
)}
</>
</div>
</div>
<div
data-u-comp="cf-icon-set-rule-grid"
className="
univer-mt-3 univer-grid univer-grid-cols-2 univer-gap-4
rtl:univer-grid-flow-dense
"
>
<div className="univer-flex univer-items-center">
<DropdownComponent
overlay={(
<div className="univer-rounded-lg univer-p-4">
<IconItemList onClick={handleIconClick} iconId={item.iconId} iconType={item.iconType} />
</div>
)}
>
<div
className={clsx(`
univer-box-border univer-flex univer-h-8 univer-w-full univer-items-center
univer-justify-between univer-rounded-md univer-bg-gray-0 univer-px-4 univer-py-2
univer-text-xs univer-text-gray-600 univer-transition-all
hover:univer-border-primary-600
dark:!univer-text-gray-200
`, borderClassName)}
>
{icon
? <img src={icon} className="univer-size-4" draggable={false} />
: <SlashDoubleIcon />}
<MoreDownIcon />
</div>
</DropdownComponent>
</div>
{!isEnd
? (
<SelectComponent
options={options}
value={item.operator}
onChange={(v) => { handleOperatorChange(v as CFNumberOperator, index); }}
/>
)
: (
<div
className={`
univer-mt-0 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.rule')}
<span className="univer-font-medium">
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.when')}
{' '}
{localeService.t(`sheets-conditional-formatting-ui.symbol.${getOppositeOperator(preItem.operator)}`)}
{lessThanText}
{isEnd ? '' : ` ${localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.and')} `}
</span>
</div>
)}
</div>
{!isEnd
? (
<>
<div
className={`
univer-mt-3 univer-grid univer-grid-cols-2 univer-gap-4 univer-text-sm
univer-text-gray-600
dark:!univer-text-gray-200
`}
>
<div>{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.type')}</div>
<div>{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.value')}</div>
</div>
<div
className="univer-mt-3 univer-grid univer-grid-cols-2 univer-gap-4"
>
<SelectComponent
options={valueTypeOptions}
value={item.value.type}
onChange={(v) => {
handleValueTypeChange(v as CFNumberOperator, index);
}}
/>
<TextInput
id={index}
type={item.value.type}
error={error}
value={item.value.value || ''}
FormulaEditorComponent={FormulaEditorComponent}
onChange={(v) => {
handleValueValueChange(v, index);
}}
/>
</div>
</>
)
: <div />}
</div>
);
});
}, [configList, DropdownComponent, errorMap, FormulaEditorComponent, SelectComponent]);
return render;
};
export const IconSet = (props: IStyleEditorProps<unknown, IIconSet>) => {
const {
interceptorManager,
DropdownComponent = Dropdown,
SelectComponent = Select,
FormulaEditorComponent = FormulaEditor,
} = props;
const rule = props.rule?.type === CFRuleType.iconSet ? props.rule : undefined;
const localeService = useDependency(LocaleService);
const direction = useObservable(localeService.direction$, localeService.getDirection());
const [errorMap, setErrorMap] = useState<Record<string, string>>({});
const [currentIconType, setCurrentIconType] = useState<IIconSetType>(() => {
const defaultV = Object.keys(iconMap)[0] as IIconSetType;
if (rule && rule.config.length) {
const type = rule.config[0].iconType;
const isNotSame = rule.config.some((item) => item.iconType !== type);
if (!isNotSame) {
return type;
}
}
return defaultV;
});
const [configList, setConfigList] = useState(() => {
if (rule && rule.config.length) {
return Tools.deepClone(rule?.config);
}
const list = iconMap[currentIconType] || [];
return new Array(list.length).fill('').map((_e, index, list) => {
if (index === list.length - 1) {
// The last condition is actually the complement of the above conditions,
// packages/sheets-conditional-formatting/src/services/calculate-unit/icon-set.ts
return {
operator: CFNumberOperator.lessThanOrEqual,
value: { type: CFValueType.num, value: Number.MAX_SAFE_INTEGER },
iconType: currentIconType,
iconId: String(index),
};
}
return createDefaultConfigItem(currentIconType, index, list);
});
});
const [isShowValue, setIsShowValue] = useState(() => {
if (!rule) {
return true;
}
return !!rule.isShowValue;
});
const previewIcon = useMemo(() => {
const list = configList.map((item) => {
return getIcon(item.iconType, item.iconId);
});
return (
<div
data-u-comp="cf-icon-set-preview"
className="
univer-flex univer-items-center
rtl:univer-flex-row-reverse
"
>
{list.map((icon, index) => (icon
? (
<img
className="univer-size-5"
key={index}
src={icon}
/>
)
: <SlashDoubleIcon className="univer-size-5" key={index} />))}
</div>
);
}, [configList]);
const checkResult = (_configList: typeof configList) => {
const isTypeSame = _configList.reduce((pre, cur, index) => {
if ((pre.preType && !pre.result) || _configList.length - 1 === index) {
return pre;
}
if (cur.value.type === CFValueType.formula) {
return {
preType: CFValueType.formula,
result: false,
};
}
if (pre.preType) {
return {
result: pre.preType === cur.value.type,
preType: cur.value.type,
};
} else {
return {
result: true,
preType: cur.value.type,
};
}
}, { result: true, preType: '' }).result;
if (isTypeSame && [CFValueType.num, CFValueType.percent, CFValueType.percentile].includes(_configList[0].value.type)) {
const result: Record<string, string> = {};
_configList.forEach((item, index, arr) => {
const preIndex = index - 1;
if (preIndex < 0 || index === arr.length - 1) {
return;
}
const preItem = _configList[index - 1];
const preOperator = getOppositeOperator(preItem.operator);
if (!compareWithNumber({ operator: preOperator, value: preItem.value.value as number }, item.value.value as number)) {
result[index] = `${localeService.t(`sheets-conditional-formatting-ui.form.${preOperator}`, String(preItem.value.value))} `;
}
});
return result;
}
return {};
};
const handleChange = (keys: string[], v: unknown) => {
const oldV = get(configList, keys);
if (oldV !== v) {
set(configList, keys, v);
setConfigList([...configList]);
setErrorMap(checkResult(configList));
}
};
const handleClickIconList = (iconType: IIconSetType) => {
setCurrentIconType(iconType);
const list = iconMap[iconType] || [];
const config = new Array(list.length).fill('').map((_e, index, list) => createDefaultConfigItem(iconType, index, list));
setConfigList(config);
setErrorMap(checkResult(config));
};
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().submit, {
handler() {
const result: IIconSet = { type: CFRuleType.iconSet, isShowValue, config: configList } as IIconSet;
return result;
},
});
return () => {
dispose();
};
}, [isShowValue, configList, interceptorManager]);
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().beforeSubmit, {
handler() {
const currentErrorMap = checkResult(configList);
setErrorMap(currentErrorMap);
return Object.keys(currentErrorMap).length === 0;
},
});
return () => {
dispose();
};
}, [isShowValue, configList, interceptorManager, errorMap]);
const reverseIcon = () => {
const iconList = configList.map((item) => ({ ...item }));
configList.forEach((item, index) => {
const mirrorIndex = configList.length - 1 - index;
const newIcon = iconList[mirrorIndex];
item.iconId = newIcon.iconId;
item.iconType = newIcon.iconType;
});
setConfigList([...configList]);
};
return (
<div data-u-comp="cf-icon-set-editor" dir={direction}>
<div className="univer-mt-4 univer-text-sm univer-text-gray-600">
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.styleRule')}
</div>
<div className="univer-mt-3">
<DropdownComponent
overlay={(
<div dir={direction} className="univer-rounded-lg univer-p-3">
<IconGroupList
iconType={currentIconType}
onClick={handleClickIconList}
/>
</div>
)}
>
<div
className={clsx(`
univer-box-border univer-flex univer-h-8 univer-w-full univer-items-center
univer-justify-between univer-rounded-md univer-bg-gray-0 univer-px-4 univer-py-2
univer-text-xs univer-text-gray-600 univer-transition-all
hover:univer-border-primary-600
`, borderClassName)}
>
{previewIcon}
<MoreDownIcon />
</div>
</DropdownComponent>
</div>
<div
className="
univer-mt-3 univer-flex univer-items-center univer-gap-2 univer-text-xs
rtl:univer-flex-row-reverse rtl:univer-justify-end
"
>
<div
data-u-comp="cf-icon-set-option-row"
className="univer-flex univer-items-center univer-text-xs"
>
<Checkbox onChange={reverseIcon} />
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.reverseIconOrder')}
</div>
<div
data-u-comp="cf-icon-set-option-row"
className="univer-flex univer-items-center univer-text-xs"
>
<Checkbox checked={!isShowValue} onChange={(v) => { setIsShowValue(!v); }} />
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.iconSet.onlyShowIcon')}
</div>
</div>
<IconSetRuleEdit
errorMap={errorMap}
onChange={handleChange}
configList={configList}
DropdownComponent={DropdownComponent}
SelectComponent={SelectComponent}
FormulaEditorComponent={FormulaEditorComponent}
/>
</div>
);
};

View File

@@ -0,0 +1,219 @@
/**
* 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 {
IAverageHighlightCell,
IHighlightCell,
IRankHighlightCell,
} from '@univerjs/sheets-conditional-formatting';
import type { LocaleKey } from '../../../locale/types';
import type { IStyleEditorProps } from './type';
import { LocaleService } from '@univerjs/core';
import { Checkbox, InputNumber, Select } from '@univerjs/design';
import { CFNumberOperator, CFRuleType, CFSubRuleType } from '@univerjs/sheets-conditional-formatting';
import { useDependency } from '@univerjs/ui';
import { useEffect, useState } from 'react';
import { ConditionalStyleEditor } from '../../ConditionalStyleEditor';
import { Preview } from '../../Preview';
import { previewClassName } from './styles';
type IRankRuleType = 'isNotBottom' | 'isBottom' | 'greaterThanAverage' | 'lessThanAverage';
export const RankStyleEditor = (props: IStyleEditorProps) => {
const { onChange, interceptorManager, SelectComponent = Select, ColorPickerComponent } = props;
const localeService = useDependency(LocaleService);
const rule = props.rule?.type === CFRuleType.highlightCell ? props.rule : undefined;
const options: Array<{ label: string; value: IRankRuleType }> = [{ label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.isNotBottom'), value: 'isNotBottom' }, { label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.isBottom'), value: 'isBottom' }, { label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.greaterThanAverage'), value: 'greaterThanAverage' }, { label: localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.lessThanAverage'), value: 'lessThanAverage' }];
const [type, setType] = useState<IRankRuleType>(() => {
const defaultV = options[0].value;
const type = rule?.type;
if (!rule) {
return defaultV;
}
switch (type) {
case CFRuleType.highlightCell:{
const subType = rule.subType;
switch (subType) {
case CFSubRuleType.average:{
if ([CFNumberOperator.greaterThan, CFNumberOperator.greaterThanOrEqual].includes(rule.operator)) {
return 'greaterThanAverage';
}
if ([CFNumberOperator.lessThan, CFNumberOperator.lessThanOrEqual].includes(rule.operator)) {
return 'lessThanAverage';
}
return defaultV;
}
case CFSubRuleType.rank:{
if (rule.isBottom) {
return 'isBottom';
} else {
return 'isNotBottom';
}
}
}
}
}
return defaultV;
});
const [value, setValue] = useState(() => {
const defaultV = 10;
const type = rule?.type;
if (!rule) {
return defaultV;
}
switch (type) {
case CFRuleType.highlightCell:{
const subType = rule.subType;
switch (subType) {
case CFSubRuleType.rank:{
return rule.value || defaultV;
}
}
}
}
return defaultV;
});
const [isPercent, setIsPercent] = useState(() => {
const defaultV = false;
const type = rule?.type;
if (!rule) {
return defaultV;
}
switch (type) {
case CFRuleType.highlightCell:{
const subType = rule.subType;
switch (subType) {
case CFSubRuleType.rank:{
return rule.isPercent || defaultV;
}
}
}
}
return defaultV;
});
const [style, setStyle] = useState<IHighlightCell['style']>({});
const getResult = (config: {
type: IRankRuleType;
isPercent: boolean;
value: number;
style: IHighlightCell['style'];
}): IRankHighlightCell | IAverageHighlightCell => {
const { type, isPercent, value, style } = config;
switch (type) {
case 'isNotBottom':
return { type: CFRuleType.highlightCell, subType: CFSubRuleType.rank, isPercent, isBottom: false, value, style };
case 'isBottom':
return { type: CFRuleType.highlightCell, subType: CFSubRuleType.rank, isPercent, isBottom: true, value, style };
case 'greaterThanAverage':
return { type: CFRuleType.highlightCell, subType: CFSubRuleType.average, operator: CFNumberOperator.greaterThan, style };
case 'lessThanAverage':
return { type: CFRuleType.highlightCell, subType: CFSubRuleType.average, operator: CFNumberOperator.lessThan, style };
}
};
useEffect(() => {
const dispose = interceptorManager.intercept(interceptorManager.getInterceptPoints().submit, {
handler() {
return getResult({ type, isPercent, value, style });
},
});
return dispose as () => void;
}, [type, isPercent, value, style, interceptorManager]);
const _onChange = (config: {
type: IRankRuleType;
isPercent: boolean;
value: number;
style: IHighlightCell['style'];
}) => {
onChange(getResult(config));
};
return (
<div>
<div
className={`
univer-mt-4 univer-text-sm univer-text-gray-600
dark:!univer-text-gray-200
`}
>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.panel.styleRule')}
</div>
<SelectComponent
className="univer-mt-3 univer-w-full"
value={type}
options={options}
onChange={(v) => {
const nextType = options.find((option) => option.value === v)?.value;
if (nextType) {
setType(nextType);
_onChange({ type: nextType, isPercent, value, style });
}
}}
/>
{['isNotBottom', 'isBottom'].includes(type) && (
<div className="univer-mt-3 univer-flex univer-items-center univer-gap-2">
<InputNumber
min={1}
max={1000}
value={value}
onChange={(v) => {
const value = v || 0;
setValue(value);
_onChange({ type, isPercent, value, style });
}}
/>
<div
className="univer-flex univer-items-center univer-text-xs"
>
<Checkbox
checked={isPercent}
onChange={(v) => {
setIsPercent(!!v);
_onChange({ type, isPercent: !!v, value, style });
}}
/>
{localeService.t<LocaleKey>('sheets-conditional-formatting-ui.valueType.percent')}
</div>
</div>
)}
<div className={previewClassName}>
<Preview
rule={getResult({
type,
isPercent,
value,
style,
})}
/>
</div>
<ConditionalStyleEditor
ColorPickerComponent={ColorPickerComponent}
style={rule?.style}
className="univer-mt-3"
onChange={(v) => {
setStyle(v);
_onChange({ type, isPercent, value, style: v });
}}
/>
</div>
);
};

View File

@@ -0,0 +1,925 @@
/**
* 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 { IConditionalFormattingRuleConfig } from '@univerjs/sheets-conditional-formatting';
import type { Root } from 'react-dom/client';
import { createIdentifier, InterceptorManager, LocaleService, LocaleType } from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { IRenderManagerService } from '@univerjs/engine-render';
import { IRefSelectionsService } from '@univerjs/sheets';
import {
CFNumberOperator,
CFRuleType,
CFSubRuleType,
CFTextOperator,
CFTimePeriodOperator,
CFValueType,
IIconSetType,
} from '@univerjs/sheets-conditional-formatting';
import { IContextMenuService, IDialogService, ILayoutService, IShortcutService, RediContext } from '@univerjs/ui';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { of, Subject } from 'rxjs';
import { afterEach, describe, expect, it } from 'vitest';
import { createCfUiTestBed } from '../../../../__tests__/create-cf-ui-test-bed';
import enUS from '../../../../locale/en-US';
import { ColorScaleStyleEditor } from '../ColorScale';
import { DataBarStyleEditor } from '../DataBar';
import { FormulaStyleEditor } from '../Formula';
import { HighlightCellStyleEditor } from '../HighlightCell';
import { IconSet } from '../IconSet';
import { RankStyleEditor } from '../Rank';
import { beforeSubmit, submit } from '../type';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const IEditorService = createIdentifier<TestEditorService>('univer.editor.service');
const IDescriptionService = createIdentifier<TestDescriptionService>('formula.description-service');
class TestLayoutService {
readonly rootContainerElement = document.body;
}
class TestEditorService {
register() {
return { dispose() {} };
}
getEditor() {
return undefined;
}
getFocusId() {
return undefined;
}
focus(): void {}
blur(): void {}
}
class TestRenderManagerService {
getRenderUnitById() {
return undefined;
}
}
class TestDocSelectionManagerService {
readonly textSelection$ = new Subject();
}
class TestDescriptionService {
hasDefinedNameDescription() {
return false;
}
}
class TestDialogService {
open() {
return { dispose: () => {} };
}
close(): void {}
closeAll(): void {}
getDialogs$() {
return of([]);
}
}
class TestRefSelectionsService {
setSelections(): void {}
clear(): void {}
}
class TestShortcutService {
registerShortcut() {
return { dispose() {} };
}
}
class TestContextMenuService {
disable(): void {}
enable(): void {}
}
function createEditorTestBed() {
const testBed = createCfUiTestBed();
testBed.injector.add([ILayoutService, { useClass: TestLayoutService as never }]);
testBed.injector.add([IEditorService, { useClass: TestEditorService as never }]);
testBed.injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
testBed.injector.add([IDescriptionService, { useClass: TestDescriptionService as never }]);
testBed.injector.add([IDialogService, { useClass: TestDialogService }]);
testBed.injector.add([IRefSelectionsService, { useClass: TestRefSelectionsService as never }]);
testBed.injector.add([IShortcutService, { useClass: TestShortcutService as never }]);
testBed.injector.add([IContextMenuService, { useClass: TestContextMenuService as never }]);
testBed.injector.add([LexerTreeBuilder]);
testBed.get(LocaleService).load({ [LocaleType.EN_US]: enUS, [LocaleType.ZH_CN]: {} });
return testBed;
}
async function createFormulaEditorTestBed() {
const testBed = createEditorTestBed();
const docsSelectionServiceUrl = new URL('../docs/src/services/doc-selection-manager.service.ts', `file://${process.cwd()}/`).href;
const { DocSelectionManagerService } = await import(/* @vite-ignore */ docsSelectionServiceUrl);
testBed.injector.add([DocSelectionManagerService, { useClass: TestDocSelectionManagerService as never }]);
return testBed;
}
function createRuleInterceptorManager() {
return new InterceptorManager({ beforeSubmit, submit });
}
function submitRule(interceptorManager: ReturnType<typeof createRuleInterceptorManager>) {
return interceptorManager.fetchThroughInterceptors(submit)(null, null) as IConditionalFormattingRuleConfig | null;
}
function inputText(input: HTMLInputElement, value: string) {
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
valueSetter?.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
}
async function selectOption(selectIndex: number, optionText: string) {
const select = Array.from(document.querySelectorAll<HTMLElement>('[data-u-comp="select"]'))[selectIndex];
select.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
select.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
const option = Array.from(document.querySelectorAll<HTMLElement>('[data-slot="dropdown-menu-radio-item"], [role="menuitemradio"]'))
.find((item) => item.textContent?.includes(optionText));
if (!option) {
throw new Error(`Rule editor option ${optionText} was not rendered`);
}
option.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
async function openDropdown(dropdownIndex: number) {
const dropdown = Array.from(document.querySelectorAll<HTMLElement>('[data-slot="popover-trigger"]'))[dropdownIndex];
if (!dropdown) {
throw new Error(`Dropdown ${dropdownIndex} was not rendered`);
}
dropdown.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
dropdown.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
}
describe('conditional formatting rule editors', () => {
let root: Root | undefined;
let container: HTMLDivElement | undefined;
let currentTestBed: ReturnType<typeof createEditorTestBed> | undefined;
afterEach(() => {
act(() => {
root?.unmount();
});
container?.remove();
currentTestBed?.univer.dispose();
root = undefined;
container = undefined;
currentTestBed = undefined;
});
it('keeps the icon-set gallery grid RTL when the locale direction is RTL', async () => {
currentTestBed = createEditorTestBed();
currentTestBed.get(LocaleService).setDirection('rtl');
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<IconSet interceptorManager={interceptorManager} onChange={() => undefined} />
</RediContext.Provider>
);
await Promise.resolve();
});
await openDropdown(0);
expect(document.querySelector('[data-u-comp="cf-icon-set-group-grid"]')?.getAttribute('dir')).toBe('rtl');
expect(document.querySelector('[data-u-comp="cf-icon-set-group-grid"] button')?.className).toContain('univer-justify-start');
expect(document.querySelector('[data-u-comp="cf-icon-set-group-grid"] button > span')?.getAttribute('dir')).toBe('rtl');
expect(document.querySelector('[data-u-comp="cf-icon-set-group-grid"] button > span')?.className).not.toContain('univer-flex-row-reverse');
});
it('blocks submitting an empty text condition and submits the entered text condition rule', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
let lastRule: unknown;
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<HighlightCellStyleEditor
interceptorManager={interceptorManager}
onChange={(rule) => {
lastRule = rule;
}}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
let canSubmit: unknown;
await act(async () => {
canSubmit = interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null);
await Promise.resolve();
});
expect(canSubmit).toBe(false);
const textInput = container.querySelector('input[type="text"]') as HTMLInputElement;
await act(async () => {
inputText(textInput, 'overdue');
await Promise.resolve();
});
await act(async () => {
canSubmit = interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null);
await Promise.resolve();
});
expect(canSubmit).toBe(true);
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
value: 'overdue',
});
expect(lastRule).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
value: 'overdue',
});
});
it('submits the configured rank rule instead of a generic highlight rule', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RankStyleEditor
interceptorManager={interceptorManager}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.rank,
isBottom: false,
isPercent: false,
value: 10,
});
});
it('preserves an existing bottom percent rank rule when reopening the editor', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<RankStyleEditor
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.highlightCell,
subType: CFSubRuleType.rank,
isBottom: true,
isPercent: true,
value: 15,
style: {
cl: {
rgb: '#c00000',
},
},
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.rank,
isBottom: true,
isPercent: true,
value: 15,
style: {
cl: {
rgb: '#c00000',
},
},
});
});
it('submits data bar rule configuration with min/max value types and visibility', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<DataBarStyleEditor
interceptorManager={interceptorManager}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.dataBar,
isShowValue: true,
config: {
min: { type: CFValueType.min },
max: { type: CFValueType.max },
isGradient: false,
},
});
});
it('preserves custom colors, gradient fill, thresholds, and hidden cell values when reopening a data bar rule', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<DataBarStyleEditor
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.dataBar,
isShowValue: false,
config: {
min: { type: CFValueType.percent, value: 15 },
max: { type: CFValueType.percentile, value: 85 },
isGradient: true,
positiveColor: '#63be7b',
nativeColor: '#f8696b',
},
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.dataBar,
isShowValue: false,
config: {
min: { type: CFValueType.percent, value: 15 },
max: { type: CFValueType.percentile, value: 85 },
isGradient: true,
positiveColor: '#63be7b',
nativeColor: '#f8696b',
},
});
});
it('submits a two-point color scale when the median value type is disabled', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<ColorScaleStyleEditor
interceptorManager={interceptorManager}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.colorScale,
config: [
{ index: 0, value: { type: CFValueType.min } },
{ index: 1, value: { type: CFValueType.max } },
],
});
});
it('preserves an existing three-point color scale rule when reopening the editor', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<ColorScaleStyleEditor
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.colorScale,
config: [
{
index: 0,
color: '#63be7b',
value: { type: CFValueType.min },
},
{
index: 1,
color: '#ffeb84',
value: { type: CFValueType.percentile, value: 50 },
},
{
index: 2,
color: '#f8696b',
value: { type: CFValueType.max },
},
],
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.colorScale,
config: [
{
index: 0,
color: '#63be7b',
value: { type: CFValueType.min },
},
{
index: 1,
color: '#ffeb84',
value: { type: CFValueType.percentile, value: 50 },
},
{
index: 2,
color: '#f8696b',
value: { type: CFValueType.max },
},
],
});
});
it('submits a valid default icon set rule with visible cell values', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<IconSet
interceptorManager={interceptorManager}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null)).toBe(true);
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.iconSet,
isShowValue: true,
config: [
{
iconType: '3Arrows',
iconId: '0',
value: { type: CFValueType.num, value: 20 },
},
{
iconType: '3Arrows',
iconId: '1',
value: { type: CFValueType.num, value: 10 },
},
{
iconType: '3Arrows',
iconId: '2',
value: { type: CFValueType.num, value: Number.MAX_SAFE_INTEGER },
},
],
});
});
it('preserves an existing number highlight rule when reopening the editor', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<HighlightCellStyleEditor
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: CFNumberOperator.between,
value: [5, 12],
style: {
bg: {
rgb: '#ffeecc',
},
},
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.number,
operator: CFNumberOperator.between,
value: [5, 12],
style: {
bg: {
rgb: '#ffeecc',
},
},
});
});
it('preserves an existing text highlight rule and validates it through beforeSubmit', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<HighlightCellStyleEditor
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.endsWith,
value: '.com',
style: {
cl: {
rgb: '#0070c0',
},
},
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null)).toBe(true);
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.endsWith,
value: '.com',
style: {
cl: {
rgb: '#0070c0',
},
},
});
});
it('preserves an existing time period highlight rule without requiring an input value', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<HighlightCellStyleEditor
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.highlightCell,
subType: CFSubRuleType.timePeriod,
operator: CFTimePeriodOperator.last7Days,
style: {
bg: {
rgb: '#fff2cc',
},
},
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null)).toBe(true);
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.timePeriod,
operator: CFTimePeriodOperator.last7Days,
style: {
bg: {
rgb: '#fff2cc',
},
},
});
});
it('submits a duplicate-values highlight rule after switching the condition type', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
let lastRule: unknown;
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<HighlightCellStyleEditor
interceptorManager={interceptorManager}
onChange={(rule) => {
lastRule = rule;
}}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
await act(async () => {
await selectOption(0, 'duplicateValues');
await Promise.resolve();
});
expect(interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null)).toBe(true);
const submittedRule = submitRule(interceptorManager);
expect(submittedRule).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.duplicateValues,
});
expect(submittedRule).not.toHaveProperty('operator');
expect(lastRule).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.duplicateValues,
});
expect(lastRule).not.toHaveProperty('operator');
});
it('preserves an existing formula highlight rule and validates it through beforeSubmit', async () => {
currentTestBed = await createFormulaEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<FormulaStyleEditor
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.highlightCell,
subType: CFSubRuleType.formula,
value: '=MOD(ROW(),2)=0',
style: {
bg: {
rgb: '#ddebf7',
},
cl: {
rgb: '#1f4e79',
},
},
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null)).toBe(true);
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.highlightCell,
subType: CFSubRuleType.formula,
value: '=MOD(ROW(),2)=0',
style: {
bg: {
rgb: '#ddebf7',
},
cl: {
rgb: '#1f4e79',
},
},
});
});
it('blocks submitting a reopened formula highlight rule with an invalid formula', async () => {
currentTestBed = await createFormulaEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<FormulaStyleEditor
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.highlightCell,
subType: CFSubRuleType.formula,
value: '=SUM(',
style: {
bg: {
rgb: '#fce4d6',
},
},
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null)).toBe(false);
});
it('preserves an existing icon set rule with hidden cell values', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<IconSet
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.iconSet,
isShowValue: false,
config: [
{
iconType: IIconSetType.threeTrafficLights1,
iconId: '0',
operator: CFNumberOperator.greaterThanOrEqual,
value: { type: CFValueType.percent, value: 75 },
},
{
iconType: IIconSetType.threeTrafficLights1,
iconId: '1',
operator: CFNumberOperator.greaterThan,
value: { type: CFValueType.percent, value: 25 },
},
{
iconType: IIconSetType.threeTrafficLights1,
iconId: '2',
operator: CFNumberOperator.lessThanOrEqual,
value: { type: CFValueType.percent, value: Number.MAX_SAFE_INTEGER },
},
],
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null)).toBe(true);
expect(submitRule(interceptorManager)).toMatchObject({
type: CFRuleType.iconSet,
isShowValue: false,
config: [
{
iconType: IIconSetType.threeTrafficLights1,
iconId: '0',
value: { type: CFValueType.percent, value: 75 },
},
{
iconType: IIconSetType.threeTrafficLights1,
iconId: '1',
value: { type: CFValueType.percent, value: 25 },
},
{
iconType: IIconSetType.threeTrafficLights1,
iconId: '2',
value: { type: CFValueType.percent, value: Number.MAX_SAFE_INTEGER },
},
],
});
});
it('blocks submitting an icon set whose thresholds are not ordered by their operators', async () => {
currentTestBed = createEditorTestBed();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const interceptorManager = createRuleInterceptorManager();
await act(async () => {
root!.render(
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
<IconSet
interceptorManager={interceptorManager}
rule={{
type: CFRuleType.iconSet,
isShowValue: true,
config: [
{
iconType: IIconSetType.threeArrows,
iconId: '0',
operator: CFNumberOperator.greaterThan,
value: { type: CFValueType.num, value: 20 },
},
{
iconType: IIconSetType.threeArrows,
iconId: '1',
operator: CFNumberOperator.greaterThan,
value: { type: CFValueType.num, value: 30 },
},
{
iconType: IIconSetType.threeArrows,
iconId: '2',
operator: CFNumberOperator.lessThanOrEqual,
value: { type: CFValueType.num, value: Number.MAX_SAFE_INTEGER },
},
],
}}
onChange={() => undefined}
/>
</RediContext.Provider>
);
await Promise.resolve();
});
expect(interceptorManager.fetchThroughInterceptors(beforeSubmit)(true, null)).toBe(false);
});
});

View File

@@ -0,0 +1,19 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { borderClassName, clsx } from '@univerjs/design';
export const previewClassName = clsx('univer-mt-5 univer-px-1 univer-py-2 univer-rounded', borderClassName);

View File

@@ -0,0 +1,36 @@
/**
* 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 { InterceptorManager } from '@univerjs/core';
import type { IDropdownProps, ISelectProps } from '@univerjs/design';
import type { IConditionalFormattingRuleConfig } from '@univerjs/sheets-conditional-formatting';
import type { FormulaEditor } from '@univerjs/sheets-formula-ui';
import type { ComponentType } from 'react';
import type { IConditionalColorPickerProps } from '../../ColorPicker';
import { createInterceptorKey } from '@univerjs/core';
export const beforeSubmit = createInterceptorKey<boolean, null>('beforeSubmit');
export const submit = createInterceptorKey<any, null>('submit');
export interface IStyleEditorProps<S = any, R = IConditionalFormattingRuleConfig> {
onChange: (style: S) => void;
rule?: R;
interceptorManager: InterceptorManager<{ beforeSubmit: typeof beforeSubmit; submit: typeof submit }>;
SelectComponent?: ComponentType<ISelectProps>;
DropdownComponent?: ComponentType<IDropdownProps>;
ColorPickerComponent?: ComponentType<IConditionalColorPickerProps>;
FormulaEditorComponent?: typeof FormulaEditor;
};

View File

@@ -0,0 +1,34 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ReactElement } from 'react';
// TODO@gggpound: Error checking needs to be implemented by the form control
export const WrapperError = (props: {
errorText: string;
children: ReactElement;
}) => {
return (
<div>
{props.children}
{props.errorText && (
<div className="univer-mt-1 univer-text-xs univer-text-red-500">
{props.errorText}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,27 @@
/**
* 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 { Config } from 'tailwindcss';
import preset from '@univerjs-infra/shared/tailwind';
const config: Config = {
presets: [preset],
content: [
'./src/**/*.{js,ts,jsx,tsx}',
],
};
export default config;

View File

@@ -0,0 +1,8 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/node",
"compilerOptions": {
"rootDir": "src",
"declaration": true,
"emitDeclarationOnly": true,
"noEmit": false,
"outDir": "lib/types"
},
"include": [
"src"
],
"exclude": [
"src/**/__tests__/**",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -0,0 +1,3 @@
import createConfig from '@univerjs-infra/shared/vitest';
export default createConfig();