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
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:
46
packages/sheets-conditional-formatting-ui/README.md
Normal file
46
packages/sheets-conditional-formatting-ui/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# @univerjs/sheets-conditional-formatting-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-conditional-formatting-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-conditional-formatting-ui)
|
||||
[](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)
|
||||
98
packages/sheets-conditional-formatting-ui/package.json
Normal file
98
packages/sheets-conditional-formatting-ui/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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 = {};
|
||||
17
packages/sheets-conditional-formatting-ui/src/const.ts
Normal file
17
packages/sheets-conditional-formatting-ui/src/const.ts
Normal 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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
2
packages/sheets-conditional-formatting-ui/src/global.css
Normal file
2
packages/sheets-conditional-formatting-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
34
packages/sheets-conditional-formatting-ui/src/index.ts
Normal file
34
packages/sheets-conditional-formatting-ui/src/index.ts
Normal 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';
|
||||
179
packages/sheets-conditional-formatting-ui/src/locale/ar-SA.ts
Normal file
179
packages/sheets-conditional-formatting-ui/src/locale/ar-SA.ts
Normal 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;
|
||||
178
packages/sheets-conditional-formatting-ui/src/locale/ca-ES.ts
Normal file
178
packages/sheets-conditional-formatting-ui/src/locale/ca-ES.ts
Normal 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;
|
||||
179
packages/sheets-conditional-formatting-ui/src/locale/de-DE.ts
Normal file
179
packages/sheets-conditional-formatting-ui/src/locale/de-DE.ts
Normal 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;
|
||||
177
packages/sheets-conditional-formatting-ui/src/locale/en-US.ts
Normal file
177
packages/sheets-conditional-formatting-ui/src/locale/en-US.ts
Normal 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;
|
||||
178
packages/sheets-conditional-formatting-ui/src/locale/es-ES.ts
Normal file
178
packages/sheets-conditional-formatting-ui/src/locale/es-ES.ts
Normal 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;
|
||||
178
packages/sheets-conditional-formatting-ui/src/locale/fa-IR.ts
Normal file
178
packages/sheets-conditional-formatting-ui/src/locale/fa-IR.ts
Normal 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;
|
||||
178
packages/sheets-conditional-formatting-ui/src/locale/fr-FR.ts
Normal file
178
packages/sheets-conditional-formatting-ui/src/locale/fr-FR.ts
Normal 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;
|
||||
179
packages/sheets-conditional-formatting-ui/src/locale/id-ID.ts
Normal file
179
packages/sheets-conditional-formatting-ui/src/locale/id-ID.ts
Normal 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;
|
||||
179
packages/sheets-conditional-formatting-ui/src/locale/it-IT.ts
Normal file
179
packages/sheets-conditional-formatting-ui/src/locale/it-IT.ts
Normal 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;
|
||||
178
packages/sheets-conditional-formatting-ui/src/locale/ja-JP.ts
Normal file
178
packages/sheets-conditional-formatting-ui/src/locale/ja-JP.ts
Normal 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;
|
||||
178
packages/sheets-conditional-formatting-ui/src/locale/ko-KR.ts
Normal file
178
packages/sheets-conditional-formatting-ui/src/locale/ko-KR.ts
Normal 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;
|
||||
179
packages/sheets-conditional-formatting-ui/src/locale/pl-PL.ts
Normal file
179
packages/sheets-conditional-formatting-ui/src/locale/pl-PL.ts
Normal 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;
|
||||
179
packages/sheets-conditional-formatting-ui/src/locale/pt-BR.ts
Normal file
179
packages/sheets-conditional-formatting-ui/src/locale/pt-BR.ts
Normal 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;
|
||||
178
packages/sheets-conditional-formatting-ui/src/locale/ru-RU.ts
Normal file
178
packages/sheets-conditional-formatting-ui/src/locale/ru-RU.ts
Normal 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;
|
||||
179
packages/sheets-conditional-formatting-ui/src/locale/sk-SK.ts
Normal file
179
packages/sheets-conditional-formatting-ui/src/locale/sk-SK.ts
Normal 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;
|
||||
@@ -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>;
|
||||
178
packages/sheets-conditional-formatting-ui/src/locale/vi-VN.ts
Normal file
178
packages/sheets-conditional-formatting-ui/src/locale/vi-VN.ts
Normal 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;
|
||||
180
packages/sheets-conditional-formatting-ui/src/locale/zh-CN.ts
Normal file
180
packages/sheets-conditional-formatting-ui/src/locale/zh-CN.ts
Normal 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;
|
||||
180
packages/sheets-conditional-formatting-ui/src/locale/zh-HK.ts
Normal file
180
packages/sheets-conditional-formatting-ui/src/locale/zh-HK.ts
Normal 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;
|
||||
180
packages/sheets-conditional-formatting-ui/src/locale/zh-TW.ts
Normal file
180
packages/sheets-conditional-formatting-ui/src/locale/zh-TW.ts
Normal 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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
47
packages/sheets-conditional-formatting-ui/src/menu/schema.ts
Normal file
47
packages/sheets-conditional-formatting-ui/src/menu/schema.ts
Normal 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,
|
||||
};
|
||||
144
packages/sheets-conditional-formatting-ui/src/mobile-plugin.ts
Normal file
144
packages/sheets-conditional-formatting-ui/src/mobile-plugin.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
158
packages/sheets-conditional-formatting-ui/src/plugin.ts
Normal file
158
packages/sheets-conditional-formatting-ui/src/plugin.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
135
packages/sheets-conditional-formatting-ui/src/views/Preview.tsx
Normal file
135
packages/sheets-conditional-formatting-ui/src/views/Preview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
27
packages/sheets-conditional-formatting-ui/tailwind.config.ts
Normal file
27
packages/sheets-conditional-formatting-ui/tailwind.config.ts
Normal 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;
|
||||
8
packages/sheets-conditional-formatting-ui/tsconfig.json
Normal file
8
packages/sheets-conditional-formatting-ui/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/sheets-conditional-formatting-ui/tsconfig.node.json
Normal file
18
packages/sheets-conditional-formatting-ui/tsconfig.node.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user