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:
1
packages/sheets-conditional-formatting/.gitignore
vendored
Normal file
1
packages/sheets-conditional-formatting/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
src/assets/icon-map.ts
|
||||
50
packages/sheets-conditional-formatting/README.md
Normal file
50
packages/sheets-conditional-formatting/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# @univerjs/sheets-conditional-formatting
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-conditional-formatting)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-conditional-formatting)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-conditional-formatting)
|
||||
|
||||
`@univerjs/sheets-conditional-formatting` adds the core conditional formatting model, commands, and calculation support for Univer Sheets.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/sheets-conditional-formatting` | `UniverSheetsConditionalFormatting` | No | Yes | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/sheets-conditional-formatting
|
||||
# or
|
||||
npm install @univerjs/sheets-conditional-formatting
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { LocaleType, mergeLocales, Univer } from '@univerjs/core';
|
||||
import { UniverSheetsConditionalFormattingPlugin } from '@univerjs/sheets-conditional-formatting';
|
||||
import EnUS from '@univerjs/sheets-conditional-formatting/locale/en-US';
|
||||
|
||||
const univer = new Univer({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: mergeLocales(EnUS),
|
||||
},
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverSheetsConditionalFormattingPlugin);
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
Use this package with `@univerjs/sheets-conditional-formatting-ui` when users need menus and rule-management panels.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/sheets-conditional-formatting)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
101
packages/sheets-conditional-formatting/package.json
Normal file
101
packages/sheets-conditional-formatting/package.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "@univerjs/sheets-conditional-formatting",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Conditional formatting model, commands, and calculation support 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",
|
||||
"spreadsheet",
|
||||
"plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./facade": "./src/facade/index.ts",
|
||||
"./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"
|
||||
},
|
||||
"./facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "node --experimental-strip-types ./scripts/build-icons.mts",
|
||||
"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": {
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/engine-formula": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/sheets": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"@univerjs/icons-svg": "^1.38.0",
|
||||
"@univerjs/sheets-formula": "workspace:*",
|
||||
"rxjs": "^7.8.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
146
packages/sheets-conditional-formatting/scripts/build-icons.mts
Normal file
146
packages/sheets-conditional-formatting/scripts/build-icons.mts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 fs from 'node:fs';
|
||||
|
||||
const progress = [
|
||||
['progress0', '@univerjs/icons-svg/double/progress-0-double-icon.svg', { black: '#7A7A7A', '#E5E5E5': '#7A7A7A' }],
|
||||
['progress25', '@univerjs/icons-svg/double/progress-25-double-icon.svg', { black: '#7A7A7A', '#E5E5E5': '#7A7A7A' }],
|
||||
['progress50', '@univerjs/icons-svg/double/progress-50-double-icon.svg', { black: '#7A7A7A', '#E5E5E5': '#7A7A7A' }],
|
||||
['progress75', '@univerjs/icons-svg/double/progress-75-double-icon.svg', { black: '#7A7A7A', '#E5E5E5': '#7A7A7A' }],
|
||||
['progress100', '@univerjs/icons-svg/double/progress-100-double-icon.svg', { black: '#7A7A7A', '#E5E5E5': '#7A7A7A' }],
|
||||
];
|
||||
|
||||
const star = [
|
||||
['starEmpty', '@univerjs/icons-svg/double/star-empty-double-icon.svg', { black: '#FFBD37' }],
|
||||
['starIncomplete', '@univerjs/icons-svg/double/star-incomplete-double-icon.svg', { black: '#FFBD37' }],
|
||||
['starFull', '@univerjs/icons-svg/double/star-full-double-icon.svg', { black: '#FFBD37' }],
|
||||
];
|
||||
|
||||
const feeling = [
|
||||
['guffaw', '@univerjs/icons-svg/double/guffaw-double-icon.svg', { black: '#8F5F00', '#E5E5E5': '#FFBD37' }],
|
||||
['smile', '@univerjs/icons-svg/double/smile-double-icon.svg', { black: '#8F5F00', '#E5E5E5': '#FFBD37' }],
|
||||
['noninductive', '@univerjs/icons-svg/double/noninductive-double-icon.svg', { black: '#8F5F00', '#E5E5E5': '#FFBD37' }],
|
||||
['dissatisfied', '@univerjs/icons-svg/double/dissatisfied-double-icon.svg', { black: '#8F5F00', '#E5E5E5': '#FFBD37' }],
|
||||
['impatient', '@univerjs/icons-svg/double/impatient-double-icon.svg', { black: '#8F5F00', '#E5E5E5': '#FFBD37' }],
|
||||
];
|
||||
|
||||
const signal = [
|
||||
['signal0', '@univerjs/icons-svg/double/signal-0-double-icon.svg', { black: '#0493EE' }],
|
||||
['signal25', '@univerjs/icons-svg/double/signal-25-double-icon.svg', { black: '#0493EE' }],
|
||||
['signal50', '@univerjs/icons-svg/double/signal-50-double-icon.svg', { black: '#0493EE' }],
|
||||
['signal75', '@univerjs/icons-svg/double/signal-75-double-icon.svg', { black: '#0493EE' }],
|
||||
['signal100', '@univerjs/icons-svg/double/signal-100-double-icon.svg', { black: '#0493EE' }],
|
||||
];
|
||||
|
||||
const feedback = [
|
||||
['mistake', '@univerjs/icons-svg/double/mistake-double-icon.svg', { black: '#FE4B4B', '#E5E5E5': '#FFFFFF' }],
|
||||
['warn', '@univerjs/icons-svg/double/warn-double-icon.svg', { black: '#FFBD37', '#E5E5E5': '#FFFFFF' }],
|
||||
['correct', '@univerjs/icons-svg/double/correct-double-icon.svg', { black: '#59D01E', '#E5E5E5': '#FFFFFF' }],
|
||||
];
|
||||
|
||||
const feedback2 = [
|
||||
['mistake2', '@univerjs/icons-svg/single/close-icon.svg', { black: '#FE4B4B' }],
|
||||
['warn2', '@univerjs/icons-svg/single/exclamation-icon.svg', { black: '#FFBD37' }],
|
||||
['correct2', '@univerjs/icons-svg/single/check-mark-icon.svg', { black: '#59D01E' }],
|
||||
];
|
||||
|
||||
const arrow = [
|
||||
['down-red', '@univerjs/icons-svg/single/arrow-down-icon.svg', { black: '#FE4B4B' }],
|
||||
['right-gold', '@univerjs/icons-svg/single/arrow-right-icon.svg', { black: '#FFBD37' }],
|
||||
['up-green', '@univerjs/icons-svg/single/arrow-up-icon.svg', { black: '#59D01E' }],
|
||||
['rightAndDown-gold', '@univerjs/icons-svg/single/arrow-tilt-down-icon.svg', { black: '#FFBD37' }],
|
||||
['rightAndUp-gold', '@univerjs/icons-svg/single/arrow-tilt-up-icon.svg', { black: '#FFBD37' }],
|
||||
['down-gray', '@univerjs/icons-svg/single/arrow-down-icon.svg', { black: '#999999' }],
|
||||
['right-gray', '@univerjs/icons-svg/single/arrow-right-icon.svg', { black: '#999999' }],
|
||||
['up-gray', '@univerjs/icons-svg/single/arrow-up-icon.svg', { black: '#999999' }],
|
||||
['rightAndDown-gray', '@univerjs/icons-svg/single/arrow-tilt-down-icon.svg', { black: '#999999' }],
|
||||
['rightAndUp-gray', '@univerjs/icons-svg/single/arrow-tilt-up-icon.svg', { black: '#999999' }],
|
||||
];
|
||||
|
||||
const flag = [
|
||||
['flag-green', '@univerjs/icons-svg/single/flag-icon.svg', { black: '#59D01E' }],
|
||||
['flag-gold', '@univerjs/icons-svg/single/flag-icon.svg', { black: '#FFBD37' }],
|
||||
['flag-red', '@univerjs/icons-svg/single/flag-icon.svg', { black: '#FE4B4B' }],
|
||||
];
|
||||
|
||||
const shape = [
|
||||
['cross', '@univerjs/icons-svg/single/cross-icon.svg', { black: '#FFBD37' }],
|
||||
['up', '@univerjs/icons-svg/single/up-icon.svg', { black: '#59D01E' }],
|
||||
['down', '@univerjs/icons-svg/single/down-icon.svg', { black: '#FE4B4B' }],
|
||||
|
||||
['rhomboid-red', '@univerjs/icons-svg/single/rhomboid-icon.svg', { black: '#FE4B4B' }],
|
||||
['rhomboid-gold', '@univerjs/icons-svg/single/rhomboid-icon.svg', { black: '#FFBD37' }],
|
||||
['roundness-greed', '@univerjs/icons-svg/single/roundness-icon.svg', { black: '#59D01E' }],
|
||||
['roundness-gold', '@univerjs/icons-svg/single/roundness-icon.svg', { black: '#FFBD37' }],
|
||||
['roundness-red', '@univerjs/icons-svg/single/roundness-icon.svg', { black: '#FE4B4B' }],
|
||||
['roundness-pink', '@univerjs/icons-svg/single/roundness-icon.svg', { black: '#FB9D9D' }],
|
||||
['roundness-gray', '@univerjs/icons-svg/single/roundness-icon.svg', { black: '#999999' }],
|
||||
['roundness-black', '@univerjs/icons-svg/single/roundness-icon.svg'],
|
||||
['triangle-gold', '@univerjs/icons-svg/single/triangle-icon.svg', { black: '#FFBD37' }],
|
||||
|
||||
['indicate-greed', '@univerjs/icons-svg/double/indicate-double-icon.svg', { '#E5E5E5': '#59D01E' }],
|
||||
['indicate-gold', '@univerjs/icons-svg/double/indicate-double-icon.svg', { '#E5E5E5': '#FFBD37' }],
|
||||
['indicate-red', '@univerjs/icons-svg/double/indicate-double-icon.svg', { '#E5E5E5': '#FE4B4B' }],
|
||||
];
|
||||
|
||||
const cell = [
|
||||
['cell-0', '@univerjs/icons-svg/double/cell-0-double-icon.svg', { white: '#0493EE', black: '#0493EE' }],
|
||||
['cell-25', '@univerjs/icons-svg/double/cell-25-double-icon.svg', { white: '#0493EE', black: '#0493EE' }],
|
||||
['cell-50', '@univerjs/icons-svg/double/cell-50-double-icon.svg', { white: '#0493EE', black: '#0493EE' }],
|
||||
['cell-75', '@univerjs/icons-svg/double/cell-75-double-icon.svg', { white: '#0493EE', black: '#0493EE' }],
|
||||
['cell-100', '@univerjs/icons-svg/double/cell-100-double-icon.svg', { white: '#0493EE', black: '#0493EE' }],
|
||||
|
||||
];
|
||||
|
||||
function replaceFillAndConvertToBase64(filePath: string, replaceMap: Record<string, string>) {
|
||||
try {
|
||||
let svgContent = fs.readFileSync(filePath, 'utf8');
|
||||
if (replaceMap) {
|
||||
for (const key in replaceMap) {
|
||||
svgContent = svgContent.replace(new RegExp(`"${key}"`, 'g'), `"${replaceMap[key]}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const base64Encoded = encodeURIComponent(svgContent);
|
||||
return base64Encoded;
|
||||
} catch (error) {
|
||||
console.error('An error occurred:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function runTask() {
|
||||
const base64EncodedSVGs: Record<string, Record<string, string>> = {};
|
||||
const obj = { feedback, star, progress, signal, feeling, arrow, shape, feedback2, flag, cell };
|
||||
for (const iconType in obj) {
|
||||
const list = obj[iconType as keyof typeof obj];
|
||||
const map: Record<string, string> = {};
|
||||
base64EncodedSVGs[iconType] = map;
|
||||
for (const element of list) {
|
||||
const [key, path, replaceMap] = element;
|
||||
const base64 = replaceFillAndConvertToBase64(`./node_modules/${path}`, typeof replaceMap === 'object' ? replaceMap : {});
|
||||
map[key as keyof typeof map] = `data:image/svg+xml;charset=utf-8,${base64}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync('./src/assets')) {
|
||||
fs.mkdirSync('./src/assets');
|
||||
}
|
||||
|
||||
fs.writeFileSync('./src/assets/icon-map.ts', `/* eslint-disable */\nexport const ICON_MAP = ${JSON.stringify(base64EncodedSVGs, null, 4)};\n`);
|
||||
};
|
||||
|
||||
runTask();
|
||||
128
packages/sheets-conditional-formatting/src/base/const.ts
Normal file
128
packages/sheets-conditional-formatting/src/base/const.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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, ITextHighlightCell } from '../models/type';
|
||||
|
||||
export const SHEET_CONDITIONAL_FORMATTING_PLUGIN = 'SHEET_CONDITIONAL_FORMATTING_PLUGIN';
|
||||
export enum CFTextOperator {
|
||||
beginsWith = 'beginsWith',
|
||||
endsWith = 'endsWith',
|
||||
containsText = 'containsText',
|
||||
notContainsText = 'notContainsText',
|
||||
equal = 'equal',
|
||||
notEqual = 'notEqual',
|
||||
containsBlanks = 'containsBlanks',
|
||||
notContainsBlanks = 'notContainsBlanks',
|
||||
containsErrors = 'containsErrors',
|
||||
notContainsErrors = 'notContainsErrors',
|
||||
}
|
||||
export enum CFTimePeriodOperator {
|
||||
today = 'today',
|
||||
yesterday = 'yesterday',
|
||||
tomorrow = 'tomorrow',
|
||||
last7Days = 'last7Days',
|
||||
thisMonth = 'thisMonth',
|
||||
lastMonth = 'lastMonth',
|
||||
nextMonth = 'nextMonth',
|
||||
thisWeek = 'thisWeek',
|
||||
lastWeek = 'lastWeek',
|
||||
nextWeek = 'nextWeek',
|
||||
}
|
||||
export enum CFNumberOperator {
|
||||
greaterThan = 'greaterThan',
|
||||
greaterThanOrEqual = 'greaterThanOrEqual',
|
||||
lessThan = 'lessThan',
|
||||
lessThanOrEqual = 'lessThanOrEqual',
|
||||
notBetween = 'notBetween',
|
||||
between = 'between',
|
||||
equal = 'equal',
|
||||
notEqual = 'notEqual',
|
||||
}
|
||||
|
||||
export enum CFRuleType {
|
||||
highlightCell = 'highlightCell',
|
||||
dataBar = 'dataBar',
|
||||
colorScale = 'colorScale',
|
||||
iconSet = 'iconSet',
|
||||
}
|
||||
export enum CFSubRuleType {
|
||||
uniqueValues = 'uniqueValues',
|
||||
duplicateValues = 'duplicateValues',
|
||||
rank = 'rank',
|
||||
text = 'text',
|
||||
timePeriod = 'timePeriod',
|
||||
number = 'number',
|
||||
average = 'average',
|
||||
formula = 'formula',
|
||||
|
||||
}
|
||||
|
||||
export enum CFValueType {
|
||||
num = 'num',
|
||||
min = 'min',
|
||||
max = 'max',
|
||||
percent = 'percent',
|
||||
|
||||
percentile = 'percentile',
|
||||
|
||||
formula = 'formula',
|
||||
}
|
||||
|
||||
export const DEFAULT_BG_COLOR = '#fff';
|
||||
export const DEFAULT_FONT_COLOR = '#000000';
|
||||
|
||||
export const createDefaultRule = () => ({
|
||||
cfId: undefined as unknown as string,
|
||||
ranges: [],
|
||||
stopIfTrue: false,
|
||||
rule: { type: CFRuleType.highlightCell, subType: CFSubRuleType.text, operator: CFTextOperator.containsText } as ITextHighlightCell,
|
||||
} as IConditionFormattingRule);
|
||||
|
||||
export const createDefaultValue = (subType: CFSubRuleType, operator: CFTextOperator | CFNumberOperator | CFTimePeriodOperator) => {
|
||||
switch (subType) {
|
||||
case CFSubRuleType.text:{
|
||||
if ([CFTextOperator.beginsWith, CFTextOperator.containsText, CFTextOperator.endsWith, CFTextOperator.equal, CFTextOperator.notContainsText, CFTextOperator.notEqual].includes(operator as CFTextOperator)) {
|
||||
return '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CFSubRuleType.number:{
|
||||
if ([CFNumberOperator.between, CFNumberOperator.notBetween].includes(operator as CFNumberOperator)) {
|
||||
return [10, 100] as [number, number];
|
||||
}
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const createDefaultValueByValueType = (type: CFValueType, defaultValue?: number) => {
|
||||
switch (type) {
|
||||
case CFValueType.formula:{
|
||||
return '=';
|
||||
}
|
||||
case CFValueType.max:
|
||||
case CFValueType.min:{
|
||||
return '';
|
||||
}
|
||||
case CFValueType.percent:
|
||||
case CFValueType.percentile:
|
||||
case CFValueType.num:{
|
||||
return defaultValue !== undefined ? defaultValue : 10;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 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, Worksheet } from '@univerjs/core';
|
||||
import { ICommandService, IUndoRedoService, IUniverInstanceService } from '@univerjs/core';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ConditionalFormattingRangeIndexModel } from '../../../models/conditional-formatting-range-index-model';
|
||||
import { ConditionalFormattingRuleModel } from '../../../models/conditional-formatting-rule-model';
|
||||
import { ConditionalFormattingRangeTransformService } from '../../../services/conditional-formatting-range-transform.service';
|
||||
import { DeleteConditionalRuleMutation } from '../../mutations/delete-conditional-rule.mutation';
|
||||
import { SetConditionalRuleMutation } from '../../mutations/set-conditional-rule.mutation';
|
||||
import { ClearRangeCfCommand } from '../clear-range-cf.command';
|
||||
import { ClearWorksheetCfCommand } from '../clear-worksheet-cf.command';
|
||||
|
||||
class TestWorksheet {
|
||||
constructor(private readonly _sheetId = 'sheet-1') {}
|
||||
|
||||
getSheetId() {
|
||||
return this._sheetId;
|
||||
}
|
||||
}
|
||||
|
||||
class TestWorkbook {
|
||||
constructor(
|
||||
private readonly _unitId = 'unit-1',
|
||||
private readonly _worksheet: TestWorksheet | null = new TestWorksheet()
|
||||
) {}
|
||||
|
||||
getUnitId() {
|
||||
return this._unitId;
|
||||
}
|
||||
|
||||
getSheetBySheetId(sheetId: string) {
|
||||
if (this._worksheet?.getSheetId() === sheetId) {
|
||||
return this._worksheet as unknown as Worksheet;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getActiveSheet() {
|
||||
return this._worksheet as unknown as Worksheet;
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
constructor(private readonly _workbook: TestWorkbook | null = new TestWorkbook()) {}
|
||||
|
||||
getUnit(unitId: string) {
|
||||
if (this._workbook?.getUnitId() === unitId) {
|
||||
return this._workbook as unknown as Workbook;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getCurrentUnitOfType() {
|
||||
return this._workbook as unknown as Workbook;
|
||||
}
|
||||
}
|
||||
|
||||
function createRuleModel() {
|
||||
const ruleModel = new ConditionalFormattingRuleModel();
|
||||
|
||||
ruleModel.addRule('unit-1', 'sheet-1', {
|
||||
cfId: 'rule-keep',
|
||||
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 2 }],
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: 'highlightCell',
|
||||
subType: 'text',
|
||||
operator: 'containsText',
|
||||
style: {},
|
||||
value: 'todo',
|
||||
},
|
||||
} as never);
|
||||
|
||||
ruleModel.addRule('unit-1', 'sheet-1', {
|
||||
cfId: 'rule-delete',
|
||||
ranges: [{ startRow: 0, endRow: 0, startColumn: 2, endColumn: 2 }],
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: 'highlightCell',
|
||||
subType: 'text',
|
||||
operator: 'containsText',
|
||||
style: {},
|
||||
value: 'todo',
|
||||
},
|
||||
} as never);
|
||||
|
||||
return ruleModel;
|
||||
}
|
||||
|
||||
function createAccessor(ruleModel: ConditionalFormattingRuleModel, univerInstanceService = new TestUniverInstanceService()) {
|
||||
const rangeIndexModel = new ConditionalFormattingRangeIndexModel(ruleModel);
|
||||
const rangeTransformService = new ConditionalFormattingRangeTransformService();
|
||||
const commandService = {
|
||||
syncExecuteCommand: vi.fn(() => true),
|
||||
};
|
||||
const undoRedoService = {
|
||||
pushUndoRedo: vi.fn(),
|
||||
};
|
||||
const selectionManagerService = {
|
||||
getCurrentSelections: vi.fn(() => [{
|
||||
range: { startRow: 0, endRow: 0, startColumn: 2, endColumn: 2 },
|
||||
}]),
|
||||
};
|
||||
const accessor = {
|
||||
get(token: unknown) {
|
||||
if (token === ConditionalFormattingRuleModel) {
|
||||
return ruleModel;
|
||||
}
|
||||
|
||||
if (token === ConditionalFormattingRangeIndexModel) {
|
||||
return rangeIndexModel;
|
||||
}
|
||||
|
||||
if (token === ConditionalFormattingRangeTransformService) {
|
||||
return rangeTransformService;
|
||||
}
|
||||
|
||||
if (token === ICommandService) {
|
||||
return commandService;
|
||||
}
|
||||
|
||||
if (token === IUndoRedoService) {
|
||||
return undoRedoService;
|
||||
}
|
||||
|
||||
if (token === SheetsSelectionsService) {
|
||||
return selectionManagerService;
|
||||
}
|
||||
|
||||
if (token === IUniverInstanceService) {
|
||||
return univerInstanceService;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown dependency: ${String(token)}`);
|
||||
},
|
||||
} as IAccessor;
|
||||
|
||||
return {
|
||||
accessor,
|
||||
commandService,
|
||||
undoRedoService,
|
||||
selectionManagerService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('clear conditional formatting commands', () => {
|
||||
it('clears the selected range, updates overlapping rules, and records undo/redo', () => {
|
||||
const ruleModel = createRuleModel();
|
||||
const { accessor, commandService, undoRedoService } = createAccessor(ruleModel);
|
||||
|
||||
expect(ClearRangeCfCommand.handler(accessor, { ranges: [{ startRow: 0, endRow: 0, startColumn: 2, endColumn: 2 }] })).toBe(true);
|
||||
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenCalledTimes(2);
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenNthCalledWith(1, DeleteConditionalRuleMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
cfId: 'rule-delete',
|
||||
}, undefined);
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenNthCalledWith(2, SetConditionalRuleMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
rule: {
|
||||
cfId: 'rule-keep',
|
||||
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 1 }],
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: 'highlightCell',
|
||||
subType: 'text',
|
||||
operator: 'containsText',
|
||||
style: {},
|
||||
value: 'todo',
|
||||
},
|
||||
},
|
||||
}, undefined);
|
||||
expect(undoRedoService.pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'unit-1',
|
||||
redoMutations: expect.arrayContaining([
|
||||
expect.objectContaining({ id: DeleteConditionalRuleMutation.id }),
|
||||
expect.objectContaining({ id: SetConditionalRuleMutation.id }),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('clears conditional formatting from the current selection when ranges are omitted', () => {
|
||||
const ruleModel = createRuleModel();
|
||||
const { accessor, commandService, undoRedoService } = createAccessor(ruleModel);
|
||||
|
||||
expect(ClearRangeCfCommand.handler(accessor, {})).toBe(true);
|
||||
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenCalledWith(DeleteConditionalRuleMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
cfId: 'rule-delete',
|
||||
}, undefined);
|
||||
expect(undoRedoService.pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'unit-1',
|
||||
redoMutations: expect.arrayContaining([
|
||||
expect.objectContaining({ id: DeleteConditionalRuleMutation.id }),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('clears every rule on the worksheet and records one delete per rule', () => {
|
||||
const ruleModel = createRuleModel();
|
||||
const { accessor, commandService, undoRedoService } = createAccessor(ruleModel);
|
||||
|
||||
expect(ClearWorksheetCfCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1' })).toBe(true);
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenNthCalledWith(1, DeleteConditionalRuleMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
cfId: 'rule-delete',
|
||||
}, undefined);
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenNthCalledWith(2, DeleteConditionalRuleMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
cfId: 'rule-keep',
|
||||
}, undefined);
|
||||
expect(undoRedoService.pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'unit-1',
|
||||
redoMutations: [
|
||||
{ id: DeleteConditionalRuleMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', cfId: 'rule-delete' } },
|
||||
{ id: DeleteConditionalRuleMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', cfId: 'rule-keep' } },
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns false when the target, selection, or rules are missing', () => {
|
||||
const ruleModel = new ConditionalFormattingRuleModel();
|
||||
const { accessor, commandService, selectionManagerService } = createAccessor(ruleModel);
|
||||
const missingTarget = createAccessor(ruleModel, new TestUniverInstanceService(null));
|
||||
|
||||
expect(ClearRangeCfCommand.handler(accessor, undefined as never)).toBe(false);
|
||||
|
||||
expect(ClearRangeCfCommand.handler(missingTarget.accessor, { ranges: [] })).toBe(false);
|
||||
expect(ClearWorksheetCfCommand.handler(missingTarget.accessor, { unitId: 'unit-1', subUnitId: 'sheet-1' })).toBe(false);
|
||||
|
||||
selectionManagerService.getCurrentSelections.mockReturnValue([]);
|
||||
expect(ClearRangeCfCommand.handler(accessor, {})).toBe(false);
|
||||
expect(commandService.syncExecuteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not record undo redo when clearing rules fails during mutation sequence', () => {
|
||||
const ruleModel = createRuleModel();
|
||||
const { accessor, commandService, undoRedoService } = createAccessor(ruleModel);
|
||||
commandService.syncExecuteCommand.mockReturnValue(false);
|
||||
|
||||
expect(ClearRangeCfCommand.handler(accessor, { ranges: [{ startRow: 0, endRow: 0, startColumn: 2, endColumn: 2 }] })).toBe(false);
|
||||
expect(undoRedoService.pushUndoRedo).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -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 } from '@univerjs/core';
|
||||
import type { IConditionFormattingRule } from '../../models/type';
|
||||
import type { MakePropertyOptional } from '../../utils/type';
|
||||
import type { IAddConditionalRuleMutationParams } from '../mutations/add-conditional-rule.mutation';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
} from '@univerjs/core';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { AddConditionalRuleMutation, AddConditionalRuleMutationUndoFactory } from '../mutations/add-conditional-rule.mutation';
|
||||
|
||||
export interface IAddCfCommandParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
rule: MakePropertyOptional<IConditionFormattingRule, 'cfId'>;
|
||||
};
|
||||
|
||||
export const AddCfCommand: ICommand<IAddCfCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.add-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const { rule } = params;
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const cfId = conditionalFormattingRuleModel.createCfId(unitId, subUnitId);
|
||||
const config: IAddConditionalRuleMutationParams = { unitId, subUnitId, rule: { ...rule, cfId: rule.cfId || cfId } };
|
||||
const undo = AddConditionalRuleMutationUndoFactory(accessor, config);
|
||||
const result = commandService.syncExecuteCommand(AddConditionalRuleMutation.id, config);
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
redoMutations: [{ id: AddConditionalRuleMutation.id, params: config }],
|
||||
undoMutations: [undo],
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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, IMutationInfo, IRange } from '@univerjs/core';
|
||||
import type { CFRuleType } from '../../base/const';
|
||||
import type { IConditionFormattingRule } from '../../models/type';
|
||||
import type { IDeleteConditionalRuleMutationParams } from '../mutations/delete-conditional-rule.mutation';
|
||||
import type { ISetConditionalRuleMutationParams } from '../mutations/set-conditional-rule.mutation';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { ConditionalFormattingRangeIndexModel } from '../../models/conditional-formatting-range-index-model';
|
||||
import { ConditionalFormattingRangeTransformService } from '../../services/conditional-formatting-range-transform.service';
|
||||
import { DeleteConditionalRuleMutation, DeleteConditionalRuleMutationUndoFactory } from '../mutations/delete-conditional-rule.mutation';
|
||||
import { SetConditionalRuleMutation, setConditionalRuleMutationUndoFactory } from '../mutations/set-conditional-rule.mutation';
|
||||
|
||||
export interface IClearRangeCfParams {
|
||||
ranges?: IRange[];
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
types?: CFRuleType[];
|
||||
}
|
||||
export const ClearRangeCfCommand: ICommand<IClearRangeCfParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.clear-range-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const conditionalFormattingRangeIndexModel = accessor.get(ConditionalFormattingRangeIndexModel);
|
||||
const conditionalFormattingRangeTransformService = accessor.get(ConditionalFormattingRangeTransformService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const ranges = params.ranges ?? selectionManagerService.getCurrentSelections()?.map((selection) => selection.range) ?? [];
|
||||
const allRuleList = conditionalFormattingRangeIndexModel
|
||||
.getRulesByRanges(unitId, subUnitId, ranges)
|
||||
.filter((rule) => !params.types?.length || params.types.includes(rule.rule.type as CFRuleType));
|
||||
if (!allRuleList?.length || !ranges.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
allRuleList.forEach((oldRule) => {
|
||||
const newRanges = conditionalFormattingRangeTransformService.subtractRanges(oldRule.ranges, ranges);
|
||||
if (newRanges.length) {
|
||||
const rule: IConditionFormattingRule = { ...oldRule, ranges: newRanges };
|
||||
const params = { unitId, subUnitId, rule } as ISetConditionalRuleMutationParams;
|
||||
const undo = setConditionalRuleMutationUndoFactory(accessor, params);
|
||||
redos.push({ id: SetConditionalRuleMutation.id, params });
|
||||
undos.push(...undo);
|
||||
} else {
|
||||
const params = { unitId, subUnitId, cfId: oldRule.cfId } as IDeleteConditionalRuleMutationParams;
|
||||
const undo = DeleteConditionalRuleMutationUndoFactory(accessor, params);
|
||||
redos.push({ id: DeleteConditionalRuleMutation.id, params });
|
||||
undos.push(...undo);
|
||||
}
|
||||
});
|
||||
|
||||
const result = sequenceExecute(redos, commandService).result;
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
redoMutations: redos,
|
||||
undoMutations: undos,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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, IMutationInfo } from '@univerjs/core';
|
||||
import type { IDeleteConditionalRuleMutationParams } from '../mutations/delete-conditional-rule.mutation';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { DeleteConditionalRuleMutation, DeleteConditionalRuleMutationUndoFactory } from '../mutations/delete-conditional-rule.mutation';
|
||||
|
||||
export interface IClearWorksheetCfParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
}
|
||||
export const ClearWorksheetCfCommand: ICommand<IClearWorksheetCfParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.clear-worksheet-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const ruleList = conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId);
|
||||
if (!ruleList?.length) {
|
||||
return false;
|
||||
}
|
||||
const configList: IDeleteConditionalRuleMutationParams[] = ruleList.map((rule) => ({ cfId: rule.cfId, unitId, subUnitId }));
|
||||
const redos: IMutationInfo[] = configList.map((config) => ({ id: DeleteConditionalRuleMutation.id, params: config }));
|
||||
const undos: IMutationInfo[] = configList.map((config) => DeleteConditionalRuleMutationUndoFactory(accessor, config)[0]);
|
||||
|
||||
const result = sequenceExecute(redos, commandService).result;
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
redoMutations: redos,
|
||||
undoMutations: undos,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IDeleteConditionalRuleMutationParams } from '../mutations/delete-conditional-rule.mutation';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
} from '@univerjs/core';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { DeleteConditionalRuleMutation, DeleteConditionalRuleMutationUndoFactory } from '../mutations/delete-conditional-rule.mutation';
|
||||
|
||||
export interface IDeleteCfCommandParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
cfId: string;
|
||||
}
|
||||
export const DeleteCfCommand: ICommand<IDeleteCfCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.delete-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const config: IDeleteConditionalRuleMutationParams = { unitId, subUnitId, cfId: params.cfId };
|
||||
const undos = DeleteConditionalRuleMutationUndoFactory(accessor, config);
|
||||
const result = commandService.syncExecuteCommand(DeleteConditionalRuleMutation.id, config);
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({ unitID: unitId, undoMutations: undos, redoMutations: [{ id: DeleteConditionalRuleMutation.id, params: config }] });
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IAnchor } from '../../utils/anchor';
|
||||
import type { IMoveConditionalRuleMutationParams } from '../mutations/move-conditional-rule.mutation';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
} from '@univerjs/core';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { transformSupportSymmetryAnchor } from '../../utils/anchor';
|
||||
import { MoveConditionalRuleMutation, MoveConditionalRuleMutationUndoFactory } from '../mutations/move-conditional-rule.mutation';
|
||||
|
||||
export interface IMoveCfCommandParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
start: IAnchor;
|
||||
end: IAnchor;
|
||||
};
|
||||
export const MoveCfCommand: ICommand<IMoveCfCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.move-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const anchorList = transformSupportSymmetryAnchor(params.start, params.end, conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) || [], (rule) => rule.cfId);
|
||||
if (!anchorList) {
|
||||
return false;
|
||||
}
|
||||
const [start, end] = anchorList;
|
||||
const config: IMoveConditionalRuleMutationParams = { unitId, subUnitId, start, end };
|
||||
const undos = MoveConditionalRuleMutationUndoFactory(config);
|
||||
const result = commandService.syncExecuteCommand(MoveConditionalRuleMutation.id, config);
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
redoMutations: [{ id: MoveConditionalRuleMutation.id, params: config }],
|
||||
undoMutations: undos,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IConditionFormattingRule } from '../../models/type';
|
||||
import type { ISetConditionalRuleMutationParams } from '../mutations/set-conditional-rule.mutation';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
} from '@univerjs/core';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { SetConditionalRuleMutation, setConditionalRuleMutationUndoFactory } from '../mutations/set-conditional-rule.mutation';
|
||||
|
||||
export interface ISetCfCommandParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
cfId?: string;
|
||||
rule: IConditionFormattingRule;
|
||||
};
|
||||
export const SetCfCommand: ICommand<ISetCfCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const config: ISetConditionalRuleMutationParams = { unitId, subUnitId, rule: params.rule, cfId: params.cfId };
|
||||
const undos = setConditionalRuleMutationUndoFactory(accessor, config);
|
||||
const result = commandService.syncExecuteCommand(SetConditionalRuleMutation.id, config);
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({ unitID: unitId, undoMutations: undos, redoMutations: [{ id: SetConditionalRuleMutation.id, params: config }] });
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -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 { describe, expect, it } from 'vitest';
|
||||
import { ConditionalFormattingFormulaMarkDirty } from '../formula-mark-dirty.mutation';
|
||||
|
||||
describe('ConditionalFormattingFormulaMarkDirty', () => {
|
||||
it('accepts dirty formula maps as a command message', () => {
|
||||
expect(ConditionalFormattingFormulaMarkDirty.handler({} as never, {
|
||||
'book-1': {
|
||||
'sheet-1': {
|
||||
'cf-1_=A1>0': true,
|
||||
},
|
||||
},
|
||||
})).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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, IMutation } from '@univerjs/core';
|
||||
import type { IConditionFormattingRule } from '../../models/type';
|
||||
import type { IDeleteConditionalRuleMutationParams } from './delete-conditional-rule.mutation';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { DeleteConditionalRuleMutation } from './delete-conditional-rule.mutation';
|
||||
|
||||
export interface IAddConditionalRuleMutationParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
rule: IConditionFormattingRule;
|
||||
}
|
||||
export const AddConditionalRuleMutationUndoFactory = (accessor: IAccessor, param: IAddConditionalRuleMutationParams) => {
|
||||
return { id: DeleteConditionalRuleMutation.id, params: { unitId: param.unitId, subUnitId: param.subUnitId, cfId: param.rule.cfId } as IDeleteConditionalRuleMutationParams };
|
||||
};
|
||||
export const AddConditionalRuleMutation: IMutation<IAddConditionalRuleMutationParams> = {
|
||||
type: CommandType.MUTATION,
|
||||
id: 'sheet.mutation.add-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const { unitId, subUnitId, rule } = params;
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
conditionalFormattingRuleModel.addRule(unitId, subUnitId, rule);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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, IMutation, IMutationInfo } from '@univerjs/core';
|
||||
import type { IAddConditionalRuleMutationParams } from './add-conditional-rule.mutation';
|
||||
import type { IMoveConditionalRuleMutationParams } from './move-conditional-rule.mutation';
|
||||
import { CommandType, Tools } from '@univerjs/core';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { transformSupportSymmetryAnchor } from '../../utils/anchor';
|
||||
import { AddConditionalRuleMutation } from './add-conditional-rule.mutation';
|
||||
import { MoveConditionalRuleMutation } from './move-conditional-rule.mutation';
|
||||
|
||||
export interface IDeleteConditionalRuleMutationParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
cfId: string;
|
||||
}
|
||||
export const DeleteConditionalRuleMutationUndoFactory = (accessor: IAccessor, param: IDeleteConditionalRuleMutationParams) => {
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
const { unitId, subUnitId, cfId } = param;
|
||||
const ruleList = ([...(conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) || [])]);
|
||||
const index = ruleList.findIndex((item) => item.cfId === cfId);
|
||||
const beforeRule = ruleList[index - 1];
|
||||
if (index > -1) {
|
||||
const rule = ruleList[index];
|
||||
const result: IMutationInfo[] = [{
|
||||
id: AddConditionalRuleMutation.id,
|
||||
params: { unitId, subUnitId, rule: Tools.deepClone(rule) } as IAddConditionalRuleMutationParams,
|
||||
}];
|
||||
ruleList.splice(index, 1);
|
||||
if (index !== 0) {
|
||||
const firstRule = ruleList[0];
|
||||
if (firstRule) {
|
||||
const transformResult = transformSupportSymmetryAnchor({ id: firstRule.cfId, type: 'before' }, { id: beforeRule.cfId, type: 'after' }, ruleList, (rule) => rule.cfId);
|
||||
if (!transformResult) {
|
||||
return result;
|
||||
}
|
||||
const [start, end] = transformResult;
|
||||
const params: IMoveConditionalRuleMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
start,
|
||||
end,
|
||||
};
|
||||
result.push({ id: MoveConditionalRuleMutation.id, params });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
export const DeleteConditionalRuleMutation: IMutation<IDeleteConditionalRuleMutationParams> = {
|
||||
type: CommandType.MUTATION,
|
||||
id: 'sheet.mutation.delete-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const { unitId, subUnitId, cfId } = params;
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
conditionalFormattingRuleModel.deleteRule(unitId, subUnitId, cfId);
|
||||
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 { IMutation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export interface IConditionalFormattingFormulaMarkDirtyParams { [unitId: string]: { [sunUnitId: string]: { [formulaId: string]: boolean } } }
|
||||
export const ConditionalFormattingFormulaMarkDirty: IMutation<IConditionalFormattingFormulaMarkDirtyParams> = {
|
||||
type: CommandType.MUTATION,
|
||||
id: 'sheet.mutation.conditional-formatting-formula-mark-dirty',
|
||||
handler() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import type { IAnchor } from '../../utils/anchor';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { anchorUndoFactory } from '../../utils/anchor';
|
||||
|
||||
export interface IMoveConditionalRuleMutationParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
start: IAnchor;
|
||||
end: IAnchor;
|
||||
}
|
||||
|
||||
export const MoveConditionalRuleMutation: IMutation<IMoveConditionalRuleMutationParams> = {
|
||||
type: CommandType.MUTATION,
|
||||
id: 'sheet.mutation.move-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const { unitId, subUnitId, start, end } = params;
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
conditionalFormattingRuleModel.moveRulePriority(unitId, subUnitId, start, end);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
export const MoveConditionalRuleMutationUndoFactory = (param: IMoveConditionalRuleMutationParams) => {
|
||||
const { unitId, subUnitId } = param;
|
||||
const undo = anchorUndoFactory(param.start, param.end);
|
||||
|
||||
if (!undo) {
|
||||
return [];
|
||||
}
|
||||
const [start, end] = undo;
|
||||
|
||||
return [{ id: MoveConditionalRuleMutation.id, params: { unitId, subUnitId, start, end } as IMoveConditionalRuleMutationParams },
|
||||
];
|
||||
};
|
||||
@@ -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 { IAccessor, IMutation } from '@univerjs/core';
|
||||
import type { IConditionFormattingRule } from '../../models/type';
|
||||
import { CommandType, Tools } from '@univerjs/core';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
|
||||
export interface ISetConditionalRuleMutationParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
cfId?: string;
|
||||
rule: IConditionFormattingRule;
|
||||
}
|
||||
|
||||
export const SetConditionalRuleMutation: IMutation<ISetConditionalRuleMutationParams> = {
|
||||
type: CommandType.MUTATION,
|
||||
id: 'sheet.mutation.set-conditional-rule',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const { unitId, subUnitId, rule } = params;
|
||||
const cfId = params.cfId || params.rule.cfId;
|
||||
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
conditionalFormattingRuleModel.setRule(unitId, subUnitId, rule, cfId);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const setConditionalRuleMutationUndoFactory = (accessor: IAccessor, param: ISetConditionalRuleMutationParams) => {
|
||||
const conditionalFormattingRuleModel = accessor.get(ConditionalFormattingRuleModel);
|
||||
const { unitId, subUnitId } = param;
|
||||
const cfId = param.cfId || param.rule.cfId;
|
||||
const rule = conditionalFormattingRuleModel.getRule(unitId, subUnitId, cfId);
|
||||
if (rule) {
|
||||
return [{
|
||||
id: SetConditionalRuleMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cfId,
|
||||
rule: Tools.deepClone(rule),
|
||||
} as ISetConditionalRuleMutationParams,
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
24
packages/sheets-conditional-formatting/src/config/config.ts
Normal file
24
packages/sheets-conditional-formatting/src/config/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 SHEETS_CONDITIONAL_FORMATTING_PLUGIN_CONFIG_KEY = 'ssheets-conditional-formatting.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_CONDITIONAL_FORMATTING_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsConditionalFormattingConfig {
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsConditionalFormattingConfig = {};
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 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, IWorkbookData, UnitModel } from '@univerjs/core';
|
||||
import type { IRender } from '@univerjs/engine-render';
|
||||
import {
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
set,
|
||||
ThemeService,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import {
|
||||
ActiveDirtyManagerService,
|
||||
DefinedNamesService,
|
||||
DescriptionService,
|
||||
FormulaCalculationSessionController,
|
||||
FormulaCalculationSessionService,
|
||||
FormulaDataModel,
|
||||
FunctionService,
|
||||
IActiveDirtyManagerService,
|
||||
IDefinedNamesService,
|
||||
IDescriptionService,
|
||||
IFunctionService,
|
||||
IRegisterFunctionService,
|
||||
ISheetRowFilteredService,
|
||||
ISuperTableService,
|
||||
LexerTreeBuilder,
|
||||
RegisterFunctionService,
|
||||
RegisterOtherFormulaService,
|
||||
SheetRowFilteredService,
|
||||
SuperTableService,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { Engine, IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
RefRangeService,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
ConditionalFormattingFormulaService,
|
||||
ConditionalFormattingRangeIndexModel,
|
||||
ConditionalFormattingRangeTransformService,
|
||||
ConditionalFormattingRuleModel,
|
||||
ConditionalFormattingService,
|
||||
ConditionalFormattingStyleComposer,
|
||||
ConditionalFormattingViewModel,
|
||||
} from '@univerjs/sheets-conditional-formatting';
|
||||
import enUS from '@univerjs/sheets/locale/en-US';
|
||||
import zhCN from '@univerjs/sheets/locale/zh-CN';
|
||||
import '@univerjs/sheets/facade';
|
||||
import '@univerjs/sheets-conditional-formatting/facade';
|
||||
|
||||
function getTestWorkbookDataDemo(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
3: {
|
||||
f: '=SUM(A1)',
|
||||
si: '3e4r5t',
|
||||
},
|
||||
4: {
|
||||
v: 123,
|
||||
t: 2,
|
||||
},
|
||||
},
|
||||
1: {
|
||||
3: {
|
||||
f: '=SUM(A2)',
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
3: {
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
3: {
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
},
|
||||
rowCount: 100,
|
||||
columnCount: 100,
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
resources: [
|
||||
{
|
||||
name: 'SHEET_CONDITIONAL_FORMATTING_PLUGIN',
|
||||
data: '{"sheet1":[{"cfId":"AEGZdW8C","ranges":[{"startRow":2,"startColumn":1,"endRow":5,"endColumn":5,"startAbsoluteRefType":0,"endAbsoluteRefType":0,"rangeType":0}],"rule":{"type":"highlightCell","subType":"text","operator":"containsText","style":{"cl":{"rgb":"#2f56ef"},"bg":{"rgb":"#e8ecfc"}},"value":""},"stopIfTrue":false},{"cfId":"4ICEXdJj","ranges":[{"startRow":4,"startColumn":1,"endRow":7,"endColumn":7,"startAbsoluteRefType":0,"endAbsoluteRefType":0,"rangeType":0}],"rule":{"type":"highlightCell","subType":"text","operator":"containsText","style":{"cl":{"rgb":"#2f56ef"},"bg":{"rgb":"#e8ecfc"}},"value":""},"stopIfTrue":false},{"cfId":"geCv018z","ranges":[{"startRow":11,"startColumn":1,"endRow":12,"endColumn":5,"startAbsoluteRefType":0,"endAbsoluteRefType":0,"rangeType":0}],"rule":{"type":"highlightCell","subType":"text","operator":"containsText","style":{"cl":{"rgb":"#2f56ef"},"bg":{"rgb":"#e8ecfc"}},"value":""},"stopIfTrue":false}]}',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
sheet: UnitModel<IWorkbookData>;
|
||||
univerAPI: FUniver;
|
||||
injector: Injector;
|
||||
}
|
||||
|
||||
class RenderManagerServiceTestBed extends RenderManagerService {
|
||||
override createRender(unitId: string): IRender {
|
||||
const renderer = this._createRender(unitId, new Engine('', { elementHeight: 100, elementWidth: 100 }));
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
|
||||
export function createFacadeTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
|
||||
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([SheetsSelectionsService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerServiceTestBed }]);
|
||||
injector.add([SheetSkeletonService]);
|
||||
injector.add([FormulaDataModel]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
injector.add([RefRangeService]);
|
||||
injector.add([IFunctionService, { useClass: FunctionService }]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([ISuperTableService, { useClass: SuperTableService }]);
|
||||
injector.add([IDescriptionService, { useClass: DescriptionService }]);
|
||||
injector.add([IRegisterFunctionService, { useClass: RegisterFunctionService }]);
|
||||
injector.add([FormulaCalculationSessionService]);
|
||||
injector.add([FormulaCalculationSessionController]);
|
||||
|
||||
// register feature modules
|
||||
([
|
||||
// conditional formatting
|
||||
[ConditionalFormattingService],
|
||||
[ConditionalFormattingFormulaService],
|
||||
[ConditionalFormattingRangeTransformService],
|
||||
[ConditionalFormattingStyleComposer],
|
||||
[ConditionalFormattingRuleModel],
|
||||
[ConditionalFormattingRangeIndexModel],
|
||||
[ConditionalFormattingViewModel],
|
||||
[RegisterOtherFormulaService],
|
||||
[IActiveDirtyManagerService, { useClass: ActiveDirtyManagerService }],
|
||||
[ISheetRowFilteredService, { useClass: SheetRowFilteredService }],
|
||||
] as Dependency[]).forEach((d) => {
|
||||
injector.add(d);
|
||||
});
|
||||
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
|
||||
this._injector.get(SheetInterceptorService);
|
||||
this._injector.get(FormulaCalculationSessionController);
|
||||
this._injector.get(ConditionalFormattingService);
|
||||
this._injector.get(ConditionalFormattingViewModel);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// load i18n
|
||||
injector.get(LocaleService).load({ zhCN, enUS });
|
||||
|
||||
// load theme service
|
||||
const themeService = injector.get(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
// register builtin plugins
|
||||
// note that UI plugins are not registered here, because the unit test environment does not have a UI
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData || getTestWorkbookDataDemo());
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
// set log level
|
||||
const logService = injector.get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT); // NOTE: change this to `LogLevel.VERBOSE` to debug tests via logs
|
||||
|
||||
// init data validation
|
||||
const univerAPI = FUniver.newAPI(injector);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
sheet,
|
||||
univerAPI,
|
||||
injector,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* 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 { Injector, Univer } from '@univerjs/core';
|
||||
import type { FUniver } from '@univerjs/core/facade';
|
||||
import { ICommandService } from '@univerjs/core/services/command/command.service.js';
|
||||
import {
|
||||
FormulaExecuteStageType,
|
||||
OtherFormulaMarkDirty,
|
||||
RemoveOtherFormulaMutation,
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
SetOtherFormulaMutation,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { SetSelectionsOperation } from '@univerjs/sheets';
|
||||
import {
|
||||
AddCfCommand,
|
||||
AddConditionalRuleMutation,
|
||||
CFNumberOperator,
|
||||
CFTimePeriodOperator,
|
||||
CFValueType,
|
||||
ClearRangeCfCommand,
|
||||
ClearWorksheetCfCommand,
|
||||
ConditionalFormattingFormulaService,
|
||||
ConditionalFormattingViewModel,
|
||||
DeleteCfCommand,
|
||||
DeleteConditionalRuleMutation,
|
||||
IIconSetType,
|
||||
MoveCfCommand,
|
||||
MoveConditionalRuleMutation,
|
||||
SetCfCommand,
|
||||
SetConditionalRuleMutation,
|
||||
} from '@univerjs/sheets-conditional-formatting';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createFacadeTestBed } from './create-test-bed';
|
||||
import '@univerjs/sheets-formula/facade';
|
||||
|
||||
describe('Test conditional formatting facade', () => {
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let univerAPI: FUniver;
|
||||
let univer: Univer = null as any;
|
||||
|
||||
beforeEach(() => {
|
||||
if (univer) {
|
||||
univer.dispose();
|
||||
}
|
||||
const testBed = createFacadeTestBed();
|
||||
get = testBed.get;
|
||||
univerAPI = testBed.univerAPI;
|
||||
univer = testBed.univer;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
[
|
||||
AddCfCommand,
|
||||
AddConditionalRuleMutation,
|
||||
ClearRangeCfCommand,
|
||||
ClearWorksheetCfCommand,
|
||||
DeleteCfCommand,
|
||||
DeleteConditionalRuleMutation,
|
||||
MoveCfCommand,
|
||||
MoveConditionalRuleMutation,
|
||||
SetConditionalRuleMutation,
|
||||
SetCfCommand,
|
||||
SetSelectionsOperation,
|
||||
OtherFormulaMarkDirty,
|
||||
RemoveOtherFormulaMutation,
|
||||
SetOtherFormulaMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
].forEach((m) => {
|
||||
commandService.registerCommand(m);
|
||||
});
|
||||
|
||||
vi.stubGlobal('requestIdleCallback', ((callback: IdleRequestCallback) => {
|
||||
callback({ didTimeout: false, timeRemaining: () => 16 } as IdleDeadline);
|
||||
return 1;
|
||||
}) as typeof requestIdleCallback);
|
||||
});
|
||||
it('Gets all the conditional formatting for the current sheet', () => {
|
||||
const rules = univerAPI.getActiveWorkbook()?.getActiveSheet().getConditionalFormattingRules();
|
||||
expect(rules?.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('Gets all the conditional formatting for the current range', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
workbook?.setActiveRange(worksheet?.getRange(5, 5, 3, 3)!);
|
||||
const rules = univerAPI.getActiveWorkbook()?.getActiveRange()?.getConditionalFormattingRules();
|
||||
expect(rules?.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('Creates a constructor for conditional formatting', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
const range = worksheet?.getRange(0, 0, 2, 2);
|
||||
const rule = worksheet?.newConditionalFormattingRule()
|
||||
.whenCellNotEmpty()
|
||||
.setRanges([range!.getRange()])
|
||||
.setBold(true)
|
||||
.setItalic(false)
|
||||
.setStrikethrough(true)
|
||||
.setUnderline(true)
|
||||
.setBackground('red')
|
||||
.setFontColor('green')
|
||||
.setBackground()
|
||||
.setFontColor()
|
||||
.build();
|
||||
expect({ ...rule, cfId: 123 }).toEqual({
|
||||
rule: {
|
||||
type: 'highlightCell',
|
||||
subType: 'text',
|
||||
operator: 'notEqual',
|
||||
value: '',
|
||||
style: {
|
||||
bl: 1,
|
||||
it: 0,
|
||||
st: {
|
||||
s: 1,
|
||||
},
|
||||
ul: {
|
||||
s: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
ranges: [
|
||||
{
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
unitId: 'test',
|
||||
sheetId: 'sheet1',
|
||||
},
|
||||
],
|
||||
cfId: 123,
|
||||
stopIfTrue: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('Builds conditional formatting rules for text, numbers, dates, rank, duplicate values, data bars, color scales, and icon sets', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook()!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const range = worksheet.getRange('A1:B2').getRange();
|
||||
const builder = worksheet.newConditionalFormattingRule();
|
||||
|
||||
expect(univerAPI.Enum.ConditionFormatNumberOperatorEnum.greaterThan).toBe(CFNumberOperator.greaterThan);
|
||||
expect(univerAPI.Enum.ConditionFormatTimePeriodOperatorEnum.last7Days).toBe(CFTimePeriodOperator.last7Days);
|
||||
expect(univerAPI.Enum.ConditionFormatValueTypeEnum.num).toBe(CFValueType.num);
|
||||
expect(univerAPI.Enum.ConditionFormatIconSetTypeEnum.threeArrows).toBe(IIconSetType.threeArrows);
|
||||
|
||||
const rules = [
|
||||
builder.whenCellEmpty().setRanges([range]).build(),
|
||||
builder.whenDate(CFTimePeriodOperator.last7Days).setRanges([range]).build(),
|
||||
builder.whenFormulaSatisfied('=$A1>10').setRanges([range]).build(),
|
||||
builder.whenNumberBetween(20, 10).setRanges([range]).build(),
|
||||
builder.whenNumberEqualTo(10).setRanges([range]).build(),
|
||||
builder.whenNumberGreaterThan(10).setRanges([range]).build(),
|
||||
builder.whenNumberGreaterThanOrEqualTo(10).setRanges([range]).build(),
|
||||
builder.whenNumberLessThan(10).setRanges([range]).build(),
|
||||
builder.whenNumberLessThanOrEqualTo(10).setRanges([range]).build(),
|
||||
builder.whenNumberNotBetween(20, 10).setRanges([range]).build(),
|
||||
builder.whenNumberNotEqualTo(10).setRanges([range]).build(),
|
||||
builder.whenTextContains('apple').setRanges([range]).build(),
|
||||
builder.whenTextDoesNotContain('apple').setRanges([range]).build(),
|
||||
builder.whenTextEndsWith('.ai').setRanges([range]).build(),
|
||||
builder.whenTextEqualTo('done').setRanges([range]).build(),
|
||||
builder.whenTextStartsWith('https://').setRanges([range]).build(),
|
||||
builder.setAverage(CFNumberOperator.greaterThan).setRanges([range]).build(),
|
||||
builder.setUniqueValues().setRanges([range]).build(),
|
||||
builder.setDuplicateValues().setRanges([range]).build(),
|
||||
builder.setRank({ isBottom: true, isPercent: true, value: 10 }).setRanges([range]).build(),
|
||||
builder.setDataBar({
|
||||
min: { type: CFValueType.num, value: -100 },
|
||||
max: { type: CFValueType.num, value: 100 },
|
||||
positiveColor: '#00ff00',
|
||||
nativeColor: '#ff0000',
|
||||
isGradient: true,
|
||||
isShowValue: true,
|
||||
}).setRanges([range]).build(),
|
||||
builder.setColorScale([
|
||||
{ index: 0, color: '#00ff00', value: { type: CFValueType.min } },
|
||||
{ index: 1, color: '#ffff00', value: { type: CFValueType.percent, value: 50 } },
|
||||
{ index: 2, color: '#ff0000', value: { type: CFValueType.max } },
|
||||
]).setRanges([range]).build(),
|
||||
builder.setIconSet({
|
||||
isShowValue: true,
|
||||
iconConfigs: [
|
||||
{
|
||||
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: 10 },
|
||||
},
|
||||
{
|
||||
iconType: IIconSetType.threeArrows,
|
||||
iconId: '2',
|
||||
operator: CFNumberOperator.lessThanOrEqual,
|
||||
value: { type: CFValueType.num, value: 10 },
|
||||
},
|
||||
],
|
||||
}).setRanges([range]).build(),
|
||||
];
|
||||
|
||||
expect(rules.map((rule) => rule.rule.type)).toEqual([
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'highlightCell',
|
||||
'dataBar',
|
||||
'colorScale',
|
||||
'iconSet',
|
||||
]);
|
||||
expect(rules.map((rule) => ('operator' in rule.rule ? rule.rule.operator : undefined))).toContain(CFNumberOperator.between);
|
||||
expect(rules.map((rule) => ('operator' in rule.rule ? rule.rule.operator : undefined))).toContain(CFNumberOperator.notBetween);
|
||||
expect(rules[3].rule).toMatchObject({ value: [10, 20] });
|
||||
expect(rules[20].rule).toMatchObject({
|
||||
type: 'dataBar',
|
||||
isShowValue: true,
|
||||
config: {
|
||||
min: { type: 'num', value: -100 },
|
||||
max: { type: 'num', value: 100 },
|
||||
positiveColor: '#00ff00',
|
||||
nativeColor: '#ff0000',
|
||||
isGradient: true,
|
||||
},
|
||||
});
|
||||
expect(rules[21].rule).toMatchObject({ type: 'colorScale', config: [{ index: 0 }, { index: 1 }, { index: 2 }] });
|
||||
expect(rules[22].rule).toMatchObject({ type: 'iconSet', isShowValue: true, config: [{ iconId: '0' }, { iconId: '1' }, { iconId: '2' }] });
|
||||
expect(builder.getIconMap()[IIconSetType.threeArrows].length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('Creates rule and add', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
const rule = worksheet?.newConditionalFormattingRule()
|
||||
.whenCellNotEmpty()
|
||||
.setRanges([{ startRow: 0, endRow: 100, startColumn: 0, endColumn: 100 }])
|
||||
.setItalic(true)
|
||||
.setItalic(true)
|
||||
.setBackground('red')
|
||||
.setFontColor('green')
|
||||
.build();
|
||||
worksheet?.addConditionalFormattingRule(rule!);
|
||||
|
||||
const conditionalFormattingViewModel = get(ConditionalFormattingViewModel);
|
||||
const cell4 = conditionalFormattingViewModel.getCellCfs(workbook!.getId(), worksheet!.getSheetId(), 0, 4);
|
||||
const cell5 = conditionalFormattingViewModel.getCellCfs(workbook!.getId(), worksheet!.getSheetId(), 0, 5);
|
||||
expect(cell4?.map((e) => e.result)).toEqual([
|
||||
{
|
||||
it: 1,
|
||||
bg: {
|
||||
rgb: 'rgb(255,0,0)',
|
||||
},
|
||||
cl: {
|
||||
rgb: 'rgb(0,128,0)',
|
||||
},
|
||||
},
|
||||
|
||||
]);
|
||||
expect(cell5?.map((e) => e.result)).toEqual([{}]);
|
||||
});
|
||||
|
||||
it('resolves onCalculationResultApplied after conditional-formatting custom formula results are emitted', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook()!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const unitId = workbook.getId();
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
const formula = univerAPI.getFormula();
|
||||
|
||||
await univerAPI.executeCommand('sheet.mutation.add-conditional-rule', {
|
||||
unitId,
|
||||
subUnitId,
|
||||
rule: {
|
||||
cfId: 'cf-1',
|
||||
stopIfTrue: false,
|
||||
ranges: [
|
||||
{
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 9,
|
||||
endColumn: 0,
|
||||
startAbsoluteRefType: 0,
|
||||
endAbsoluteRefType: 0,
|
||||
rangeType: 0,
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
},
|
||||
],
|
||||
rule: {
|
||||
type: 'highlightCell',
|
||||
subType: 'formula',
|
||||
operator: 'containsText',
|
||||
value: '=VALUE($A1)<=3',
|
||||
style: {
|
||||
bg: { rgb: 'rgb(254,243,199)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const formulaService = get(ConditionalFormattingFormulaService);
|
||||
const registeredFormula = formulaService.getSubUnitFormulaMap(unitId, subUnitId)?.getValues()[0];
|
||||
expect(registeredFormula?.cfId).toBe('cf-1');
|
||||
expect(registeredFormula?.formulaText).toBe('=VALUE($A1)<=3');
|
||||
|
||||
const waitForResult = formula.onCalculationResultApplied();
|
||||
|
||||
await commandService.executeCommand(SetFormulaCalculationStartMutation.id, {}, { onlyLocal: true });
|
||||
await commandService.executeCommand(SetFormulaCalculationNotificationMutation.id, {
|
||||
stageInfo: {
|
||||
stage: FormulaExecuteStageType.START_CALCULATION,
|
||||
completedFormulasCount: 0,
|
||||
completedArrayFormulasCount: 0,
|
||||
formulaCycleIndex: 0,
|
||||
totalArrayFormulasToCalculate: 0,
|
||||
totalFormulasToCalculate: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await commandService.executeCommand(SetFormulaCalculationResultMutation.id, {
|
||||
unitData: {},
|
||||
unitOtherData: {
|
||||
[unitId]: {
|
||||
[subUnitId]: {
|
||||
[registeredFormula!.formulaId]: {
|
||||
0: {
|
||||
0: [[{ v: true }]],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(waitForResult).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('Delete conditional format according to cfId', async () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
const rules = worksheet?.getConditionalFormattingRules();
|
||||
expect(rules?.length).toEqual(3);
|
||||
await worksheet?.deleteConditionalFormattingRule(rules![0].cfId);
|
||||
expect(worksheet?.getConditionalFormattingRules()?.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('Modify the priority of the conditional format', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
const rules = worksheet?.getConditionalFormattingRules()!;
|
||||
const rule = rules[2];
|
||||
const targetRule = rules[0];
|
||||
worksheet?.moveConditionalFormattingRule(rule.cfId, targetRule.cfId, 'before');
|
||||
const index = worksheet?.getConditionalFormattingRules()!.findIndex((r) => r.cfId === rule.cfId);
|
||||
expect(index).toEqual(0);
|
||||
});
|
||||
|
||||
it('Set the conditional format according to cfId', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
const rules = worksheet?.getConditionalFormattingRules()!;
|
||||
const rule = rules[0];
|
||||
worksheet?.setConditionalFormattingRule(rule.cfId, { ...rule, ranges: [] });
|
||||
const afterEditRule = worksheet?.getConditionalFormattingRules()[0];
|
||||
expect(afterEditRule?.cfId).toEqual(rule.cfId);
|
||||
expect(afterEditRule?.ranges).toEqual([]);
|
||||
});
|
||||
|
||||
it('Clears conditional formatting from a range without removing unrelated sheet rules', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook()!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
const rule = range.createConditionalFormattingRule()
|
||||
.whenNumberGreaterThan(100)
|
||||
.setBackground('#ff0000')
|
||||
.build();
|
||||
|
||||
worksheet.addConditionalFormattingRule(rule);
|
||||
expect(range.getConditionalFormattingRules().map((item) => item.cfId)).toContain(rule.cfId);
|
||||
|
||||
range.clearConditionalFormatRules();
|
||||
|
||||
expect(range.getConditionalFormattingRules().map((item) => item.cfId)).not.toContain(rule.cfId);
|
||||
expect(worksheet.getConditionalFormattingRules().length).toBe(3);
|
||||
});
|
||||
|
||||
it('Clears all conditional formatting rules from a worksheet', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook()!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
|
||||
expect(worksheet.getConditionalFormattingRules().length).toBeGreaterThan(0);
|
||||
worksheet.clearConditionalFormatRules();
|
||||
|
||||
expect(worksheet.getConditionalFormattingRules()).toEqual([]);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
68
packages/sheets-conditional-formatting/src/facade/f-enum.ts
Normal file
68
packages/sheets-conditional-formatting/src/facade/f-enum.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FEnum } from '@univerjs/core/facade';
|
||||
import { CFNumberOperator, CFTimePeriodOperator, CFValueType, IIconSetType } from '@univerjs/sheets-conditional-formatting';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFSheetsConditionalFormattingEnumMixin {
|
||||
/**
|
||||
* Conditional formatting number operator
|
||||
*/
|
||||
ConditionFormatNumberOperatorEnum: typeof CFNumberOperator;
|
||||
|
||||
/**
|
||||
* Conditional formatting time period operator
|
||||
*/
|
||||
ConditionFormatTimePeriodOperatorEnum: typeof CFTimePeriodOperator;
|
||||
|
||||
/**
|
||||
* Conditional formatting icon set type
|
||||
*/
|
||||
ConditionFormatIconSetTypeEnum: typeof IIconSetType;
|
||||
|
||||
/**
|
||||
* Conditional formatting value type
|
||||
*/
|
||||
ConditionFormatValueTypeEnum: typeof CFValueType;
|
||||
}
|
||||
|
||||
export class FSheetsConditionalFormattingEnumMixin extends FEnum implements IFSheetsConditionalFormattingEnumMixin {
|
||||
override get ConditionFormatNumberOperatorEnum(): typeof CFNumberOperator {
|
||||
return CFNumberOperator;
|
||||
}
|
||||
|
||||
override get ConditionFormatTimePeriodOperatorEnum(): typeof CFTimePeriodOperator {
|
||||
return CFTimePeriodOperator;
|
||||
}
|
||||
|
||||
override get ConditionFormatIconSetTypeEnum(): typeof IIconSetType {
|
||||
return IIconSetType;
|
||||
}
|
||||
|
||||
override get ConditionFormatValueTypeEnum(): typeof CFValueType {
|
||||
return CFValueType;
|
||||
}
|
||||
}
|
||||
|
||||
FEnum.extend(FSheetsConditionalFormattingEnumMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
export interface FEnum extends IFSheetsConditionalFormattingEnumMixin {
|
||||
}
|
||||
}
|
||||
133
packages/sheets-conditional-formatting/src/facade/f-range.ts
Normal file
133
packages/sheets-conditional-formatting/src/facade/f-range.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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 {
|
||||
IClearRangeCfParams,
|
||||
IConditionFormattingRule,
|
||||
} from '@univerjs/sheets-conditional-formatting';
|
||||
import { Rectangle } from '@univerjs/core';
|
||||
import {
|
||||
ClearRangeCfCommand,
|
||||
ConditionalFormattingRuleModel,
|
||||
} from '@univerjs/sheets-conditional-formatting';
|
||||
|
||||
import { FRange } from '@univerjs/sheets/facade';
|
||||
import { FConditionalFormattingBuilder } from './f-conditional-formatting-builder';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFRangeSheetsConditionalFormattingMixin {
|
||||
/**
|
||||
* Gets all the conditional formatting for the current range.
|
||||
* @returns {IConditionFormattingRule[]} conditional formatting rules for the current range.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a conditional formatting rule that sets the cell format to italic, red background, and green font color when the cell is not empty.
|
||||
* const fRange = fWorksheet.getRange('A1:T100');
|
||||
* const rule = fWorksheet.newConditionalFormattingRule()
|
||||
* .whenCellNotEmpty()
|
||||
* .setRanges([fRange.getRange()])
|
||||
* .setItalic(true)
|
||||
* .setBackground('red')
|
||||
* .setFontColor('green')
|
||||
* .build();
|
||||
* fWorksheet.addConditionalFormattingRule(rule);
|
||||
*
|
||||
* // Get all the conditional formatting rules for the range F6:H8.
|
||||
* const targetRange = fWorksheet.getRange('F6:H8');
|
||||
* const rules = targetRange.getConditionalFormattingRules();
|
||||
* console.log(rules);
|
||||
* ```
|
||||
*/
|
||||
getConditionalFormattingRules(): IConditionFormattingRule[];
|
||||
|
||||
/**
|
||||
* Creates a constructor for conditional formatting
|
||||
* @returns {FConditionalFormattingBuilder} The conditional formatting builder
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a conditional formatting rule that sets the cell format to italic, red background, and green font color when the cell is not empty.
|
||||
* const fRange = fWorksheet.getRange('A1:T100');
|
||||
* const rule = fRange.createConditionalFormattingRule()
|
||||
* .whenCellNotEmpty()
|
||||
* .setItalic(true)
|
||||
* .setBackground('red')
|
||||
* .setFontColor('green')
|
||||
* .build();
|
||||
* fWorksheet.addConditionalFormattingRule(rule);
|
||||
* console.log(fRange.getConditionalFormattingRules());
|
||||
* ```
|
||||
*/
|
||||
createConditionalFormattingRule(): FConditionalFormattingBuilder;
|
||||
|
||||
/**
|
||||
* Clear the conditional rules for the range.
|
||||
* @returns {FRange} Returns the current range instance for method chaining
|
||||
* @memberof IFRangeConditionalFormattingMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const fRange = fWorksheet.getRange('A1:T100');
|
||||
*
|
||||
* // Clear all conditional format rules for the range
|
||||
* fRange.clearConditionalFormatRules();
|
||||
* console.log(fRange.getConditionalFormattingRules()); // []
|
||||
* ```
|
||||
*/
|
||||
clearConditionalFormatRules(): FRange;
|
||||
}
|
||||
|
||||
export class FRangeSheetsConditionalFormattingMixin extends FRange implements IFRangeSheetsConditionalFormattingMixin {
|
||||
private _getConditionalFormattingRuleModel(): ConditionalFormattingRuleModel {
|
||||
return this._injector.get(ConditionalFormattingRuleModel);
|
||||
}
|
||||
|
||||
override getConditionalFormattingRules(): IConditionFormattingRule[] {
|
||||
const rules = this._getConditionalFormattingRuleModel().getSubunitRules(this._workbook.getUnitId(), this._worksheet.getSheetId()) || [];
|
||||
return [...rules].filter((rule) => rule.ranges.some((range) => Rectangle.intersects(range, this._range)));
|
||||
}
|
||||
|
||||
override createConditionalFormattingRule(): FConditionalFormattingBuilder {
|
||||
return new FConditionalFormattingBuilder({ ranges: [this._range] });
|
||||
}
|
||||
|
||||
override clearConditionalFormatRules(): FRange {
|
||||
const params: IClearRangeCfParams = {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
ranges: [this._range],
|
||||
};
|
||||
this._commandService.syncExecuteCommand(ClearRangeCfCommand.id, params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
FRange.extend(FRangeSheetsConditionalFormattingMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FRange extends IFRangeSheetsConditionalFormattingMixin { }
|
||||
}
|
||||
262
packages/sheets-conditional-formatting/src/facade/f-worksheet.ts
Normal file
262
packages/sheets-conditional-formatting/src/facade/f-worksheet.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IAddCfCommandParams,
|
||||
IAnchor,
|
||||
IClearWorksheetCfParams,
|
||||
IConditionFormattingRule,
|
||||
IDeleteCfCommandParams,
|
||||
IMoveCfCommandParams,
|
||||
ISetCfCommandParams,
|
||||
} from '@univerjs/sheets-conditional-formatting';
|
||||
import {
|
||||
AddCfCommand,
|
||||
ClearWorksheetCfCommand,
|
||||
ConditionalFormattingRuleModel,
|
||||
DeleteCfCommand,
|
||||
MoveCfCommand,
|
||||
SetCfCommand,
|
||||
} from '@univerjs/sheets-conditional-formatting';
|
||||
import { FWorksheet } from '@univerjs/sheets/facade';
|
||||
import { FConditionalFormattingBuilder } from './f-conditional-formatting-builder';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFWorksheetConditionalFormattingMixin {
|
||||
/**
|
||||
* Gets all the conditional formatting for the current sheet
|
||||
* @returns {IConditionFormattingRule[]} conditional formatting rules for the current sheet
|
||||
* @memberof IFWorksheetConditionalFormattingMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const rules = fWorksheet.getConditionalFormattingRules();
|
||||
* console.log(rules);
|
||||
* ```
|
||||
*/
|
||||
getConditionalFormattingRules(): IConditionFormattingRule[];
|
||||
|
||||
/**
|
||||
* Creates a constructor for conditional formatting
|
||||
* @returns {FConditionalFormattingBuilder} The conditional formatting builder
|
||||
* @memberof IFWorksheetConditionalFormattingMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a conditional formatting rule that sets the cell format to italic, red background, and green font color when the cell is not empty.
|
||||
* const fRange = fWorksheet.getRange('A1:T100');
|
||||
* const rule = fWorksheet.newConditionalFormattingRule()
|
||||
* .whenCellNotEmpty()
|
||||
* .setRanges([fRange.getRange()])
|
||||
* .setItalic(true)
|
||||
* .setBackground('red')
|
||||
* .setFontColor('green')
|
||||
* .build();
|
||||
* fWorksheet.addConditionalFormattingRule(rule);
|
||||
* ```
|
||||
*/
|
||||
newConditionalFormattingRule(): FConditionalFormattingBuilder;
|
||||
|
||||
/**
|
||||
* Add a new conditional format
|
||||
* @param {IConditionFormattingRule} rule - The conditional formatting rule to add
|
||||
* @returns {FWorksheet} Returns the current worksheet instance for method chaining
|
||||
* @memberof IFWorksheetConditionalFormattingMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a conditional formatting rule that sets the cell format to italic, red background, and green font color when the cell is not empty.
|
||||
* const fRange = fWorksheet.getRange('A1:T100');
|
||||
* const rule = fWorksheet.newConditionalFormattingRule()
|
||||
* .whenCellNotEmpty()
|
||||
* .setRanges([fRange.getRange()])
|
||||
* .setItalic(true)
|
||||
* .setBackground('red')
|
||||
* .setFontColor('green')
|
||||
* .build();
|
||||
* fWorksheet.addConditionalFormattingRule(rule);
|
||||
* ```
|
||||
*/
|
||||
addConditionalFormattingRule(rule: IConditionFormattingRule): FWorksheet;
|
||||
|
||||
/**
|
||||
* Delete conditional format according to `cfId`
|
||||
* @param {string} cfId - The conditional formatting rule id to delete
|
||||
* @returns {FWorksheet} Returns the current worksheet instance for method chaining
|
||||
* @memberof IFWorksheetConditionalFormattingMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const rules = fWorksheet.getConditionalFormattingRules();
|
||||
*
|
||||
* // Delete the first rule
|
||||
* fWorksheet.deleteConditionalFormattingRule(rules[0]?.cfId);
|
||||
* ```
|
||||
*/
|
||||
deleteConditionalFormattingRule(cfId: string): FWorksheet;
|
||||
|
||||
/**
|
||||
* Modify the priority of the conditional format
|
||||
* @param {string} cfId - The conditional formatting rule id to move
|
||||
* @param {string} toCfId Target rule
|
||||
* @param {IAnchor['type']} [type] After the default move to the destination rule, if type = before moves to the front, the default value is after
|
||||
* @returns {FWorksheet} Returns the current worksheet instance for method chaining
|
||||
* @memberof FWorksheetConditionalFormattingMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const rules = fWorksheet.getConditionalFormattingRules();
|
||||
*
|
||||
* // Move the third rule before the first rule
|
||||
* const rule = rules[2];
|
||||
* const targetRule = rules[0];
|
||||
* fWorksheet.moveConditionalFormattingRule(rule?.cfId, targetRule?.cfId, 'before');
|
||||
* ```
|
||||
*/
|
||||
moveConditionalFormattingRule(cfId: string, toCfId: string, type?: IAnchor['type']): FWorksheet;
|
||||
|
||||
/**
|
||||
* Set the conditional format according to `cfId`
|
||||
* @param {string} cfId - The conditional formatting rule id to set
|
||||
* @param {IConditionFormattingRule} rule - The conditional formatting rule to set
|
||||
* @returns {FWorksheet} Returns the current worksheet instance for method chaining
|
||||
* @memberof IFWorksheetConditionalFormattingMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a conditional formatting rule that sets the cell format to italic, red background, and green font color when the cell is not empty.
|
||||
* const fRange = fWorksheet.getRange('A1:T100');
|
||||
* const rule = fWorksheet.newConditionalFormattingRule()
|
||||
* .whenCellNotEmpty()
|
||||
* .setRanges([fRange.getRange()])
|
||||
* .setItalic(true)
|
||||
* .setBackground('red')
|
||||
* .setFontColor('green')
|
||||
* .build();
|
||||
* fWorksheet.addConditionalFormattingRule(rule);
|
||||
*
|
||||
* // Modify the first rule to apply to a new range
|
||||
* const rules = fWorksheet.getConditionalFormattingRules();
|
||||
* const newRuleRange = fWorksheet.getRange('A1:D10');
|
||||
* fWorksheet.setConditionalFormattingRule(rules[0]?.cfId, { ...rules[0], ranges: [newRuleRange.getRange()] });
|
||||
* ```
|
||||
*/
|
||||
setConditionalFormattingRule(cfId: string, rule: IConditionFormattingRule): FWorksheet;
|
||||
|
||||
/**
|
||||
* Removes all conditional format rules from the sheet.
|
||||
* @returns {FWorksheet} Returns the current worksheet instance for method chaining
|
||||
* @memberof IFWorksheetConditionalFormattingMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* fWorksheet.clearConditionalFormatRules();
|
||||
* console.log(fWorksheet.getConditionalFormattingRules()); // []
|
||||
* ```
|
||||
*/
|
||||
clearConditionalFormatRules(): FWorksheet;
|
||||
}
|
||||
|
||||
export class FWorksheetConditionalFormattingMixin extends FWorksheet implements IFWorksheetConditionalFormattingMixin {
|
||||
private _getConditionalFormattingRuleModel(): ConditionalFormattingRuleModel {
|
||||
return this._injector.get(ConditionalFormattingRuleModel);
|
||||
}
|
||||
|
||||
override getConditionalFormattingRules(): IConditionFormattingRule[] {
|
||||
const rules = this._getConditionalFormattingRuleModel().getSubunitRules(this._workbook.getUnitId(), this._worksheet.getSheetId()) || [];
|
||||
return [...rules];
|
||||
}
|
||||
|
||||
override newConditionalFormattingRule(): FConditionalFormattingBuilder {
|
||||
return new FConditionalFormattingBuilder();
|
||||
}
|
||||
|
||||
override addConditionalFormattingRule(rule: IConditionFormattingRule): FWorksheet {
|
||||
const params: IAddCfCommandParams = {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
rule,
|
||||
};
|
||||
this._commandService.syncExecuteCommand(AddCfCommand.id, params);
|
||||
return this;
|
||||
}
|
||||
|
||||
override deleteConditionalFormattingRule(cfId: string): FWorksheet {
|
||||
const params: IDeleteCfCommandParams = {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
cfId,
|
||||
};
|
||||
this._commandService.syncExecuteCommand(DeleteCfCommand.id, params);
|
||||
return this;
|
||||
}
|
||||
|
||||
override moveConditionalFormattingRule(cfId: string, toCfId: string, type: IAnchor['type'] = 'after'): FWorksheet {
|
||||
const params: IMoveCfCommandParams = {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
start: { id: cfId, type: 'self' },
|
||||
end: { id: toCfId, type },
|
||||
};
|
||||
this._commandService.syncExecuteCommand(MoveCfCommand.id, params);
|
||||
return this;
|
||||
}
|
||||
|
||||
override setConditionalFormattingRule(cfId: string, rule: IConditionFormattingRule): FWorksheet {
|
||||
const params: ISetCfCommandParams = {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
cfId,
|
||||
rule,
|
||||
};
|
||||
this._commandService.syncExecuteCommand(SetCfCommand.id, params);
|
||||
return this;
|
||||
}
|
||||
|
||||
override clearConditionalFormatRules(): FWorksheet {
|
||||
const params: IClearWorksheetCfParams = {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
};
|
||||
this._commandService.syncExecuteCommand(ClearWorksheetCfCommand.id, params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
FWorksheet.extend(FWorksheetConditionalFormattingMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FWorksheet extends IFWorksheetConditionalFormattingMixin { }
|
||||
}
|
||||
23
packages/sheets-conditional-formatting/src/facade/index.ts
Normal file
23
packages/sheets-conditional-formatting/src/facade/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './f-range';
|
||||
import './f-worksheet';
|
||||
import './f-enum';
|
||||
|
||||
export { FConditionalFormattingBuilder } from './f-conditional-formatting-builder';
|
||||
export type { IFRangeSheetsConditionalFormattingMixin } from './f-range';
|
||||
export type { IFWorksheetConditionalFormattingMixin } from './f-worksheet';
|
||||
68
packages/sheets-conditional-formatting/src/index.ts
Normal file
68
packages/sheets-conditional-formatting/src/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './base/const';
|
||||
export { AddCfCommand } from './commands/commands/add-cf.command';
|
||||
export type { IAddCfCommandParams } from './commands/commands/add-cf.command';
|
||||
export { ClearRangeCfCommand } from './commands/commands/clear-range-cf.command';
|
||||
export type { IClearRangeCfParams } from './commands/commands/clear-range-cf.command';
|
||||
export { ClearWorksheetCfCommand } from './commands/commands/clear-worksheet-cf.command';
|
||||
export type { IClearWorksheetCfParams } from './commands/commands/clear-worksheet-cf.command';
|
||||
export { DeleteCfCommand } from './commands/commands/delete-cf.command';
|
||||
export type { IDeleteCfCommandParams } from './commands/commands/delete-cf.command';
|
||||
export { MoveCfCommand } from './commands/commands/move-cf.command';
|
||||
export type { IMoveCfCommandParams } from './commands/commands/move-cf.command';
|
||||
export { SetCfCommand } from './commands/commands/set-cf.command';
|
||||
export type { ISetCfCommandParams } from './commands/commands/set-cf.command';
|
||||
export {
|
||||
AddConditionalRuleMutation,
|
||||
AddConditionalRuleMutationUndoFactory,
|
||||
} from './commands/mutations/add-conditional-rule.mutation';
|
||||
export type { IAddConditionalRuleMutationParams } from './commands/mutations/add-conditional-rule.mutation';
|
||||
export {
|
||||
DeleteConditionalRuleMutation,
|
||||
DeleteConditionalRuleMutationUndoFactory,
|
||||
} from './commands/mutations/delete-conditional-rule.mutation';
|
||||
export type { IDeleteConditionalRuleMutationParams } from './commands/mutations/delete-conditional-rule.mutation';
|
||||
export {
|
||||
MoveConditionalRuleMutation,
|
||||
MoveConditionalRuleMutationUndoFactory,
|
||||
} from './commands/mutations/move-conditional-rule.mutation';
|
||||
export type { IMoveConditionalRuleMutationParams } from './commands/mutations/move-conditional-rule.mutation';
|
||||
export type { ISetConditionalRuleMutationParams } from './commands/mutations/set-conditional-rule.mutation';
|
||||
export {
|
||||
SetConditionalRuleMutation,
|
||||
setConditionalRuleMutationUndoFactory,
|
||||
} from './commands/mutations/set-conditional-rule.mutation';
|
||||
export type { IUniverSheetsConditionalFormattingConfig } from './config/config';
|
||||
export * from './models/calculate-unit-v2/utils';
|
||||
export * from './models/conditional-formatting-range-index-model';
|
||||
export * from './models/conditional-formatting-rule-model';
|
||||
export * from './models/conditional-formatting-view-model';
|
||||
export * from './models/icon-map';
|
||||
export * from './models/type';
|
||||
export { UniverSheetsConditionalFormattingPlugin } from './plugin';
|
||||
export * from './render/data-bar.render';
|
||||
export * from './render/icon.render';
|
||||
export * from './render/type';
|
||||
export * from './services/conditional-formatting-formula.service';
|
||||
export * from './services/conditional-formatting-range-transform.service';
|
||||
export * from './services/conditional-formatting-style-composer.service';
|
||||
export * from './services/conditional-formatting.service';
|
||||
export * from './utils/anchor';
|
||||
export * from './utils/create-cf-id';
|
||||
export * from './utils/remove-undefined-attr';
|
||||
export * from './utils/type';
|
||||
30
packages/sheets-conditional-formatting/src/locale/ar-SA.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/ar-SA.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'الاتجاه',
|
||||
shape: 'الشكل',
|
||||
mark: 'العلامة',
|
||||
rank: 'الترتيب',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/ca-ES.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/ca-ES.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Direcció',
|
||||
shape: 'Forma',
|
||||
mark: 'Marca',
|
||||
rank: 'Rang',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/de-DE.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/de-DE.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Richtung',
|
||||
shape: 'Form',
|
||||
mark: 'Zeichen',
|
||||
rank: 'Rang',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
28
packages/sheets-conditional-formatting/src/locale/en-US.ts
Normal file
28
packages/sheets-conditional-formatting/src/locale/en-US.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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': {
|
||||
iconSet: {
|
||||
direction: 'Direction',
|
||||
shape: 'Shape',
|
||||
mark: 'Mark',
|
||||
rank: 'Rank',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/es-ES.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/es-ES.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Dirección',
|
||||
shape: 'Forma',
|
||||
mark: 'Marca',
|
||||
rank: 'Rango',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/fa-IR.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/fa-IR.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'جهت',
|
||||
shape: 'شکل',
|
||||
mark: 'علامت',
|
||||
rank: 'رتبه',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/fr-FR.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/fr-FR.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Direction',
|
||||
shape: 'Forme',
|
||||
mark: 'Marque',
|
||||
rank: 'Rang',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/id-ID.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/id-ID.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Arah',
|
||||
shape: 'Bentuk',
|
||||
mark: 'Tanda',
|
||||
rank: 'Peringkat',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/it-IT.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/it-IT.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Direzione',
|
||||
shape: 'Forma',
|
||||
mark: 'Marca',
|
||||
rank: 'Grado',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/ja-JP.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/ja-JP.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: '方向',
|
||||
shape: '図形',
|
||||
mark: 'インジケーター',
|
||||
rank: '評価',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/ko-KR.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/ko-KR.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: '방향',
|
||||
shape: '모양',
|
||||
mark: '표식',
|
||||
rank: '순위',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/pl-PL.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/pl-PL.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Kierunek',
|
||||
shape: 'Kształt',
|
||||
mark: 'Znak',
|
||||
rank: 'Ranga',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/pt-BR.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/pt-BR.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Direção',
|
||||
shape: 'Forma',
|
||||
mark: 'Marca',
|
||||
rank: 'Classificação',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/ru-RU.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/ru-RU.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Направление',
|
||||
shape: 'Форма',
|
||||
mark: 'Знак',
|
||||
rank: 'Ранг',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/sk-SK.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/sk-SK.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Smer',
|
||||
shape: 'Tvar',
|
||||
mark: 'Značka',
|
||||
rank: 'Poradie',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/sheets-conditional-formatting/src/locale/types.ts
Normal file
20
packages/sheets-conditional-formatting/src/locale/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { LocaleLeafKeys } from '@univerjs/core';
|
||||
import type enUS from './en-US';
|
||||
|
||||
export type LocaleKey = LocaleLeafKeys<typeof enUS>;
|
||||
30
packages/sheets-conditional-formatting/src/locale/vi-VN.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/vi-VN.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: 'Hướng',
|
||||
shape: 'Hình dạng',
|
||||
mark: 'Dấu',
|
||||
rank: 'Hạng',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/zh-CN.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: '方向',
|
||||
shape: '形状',
|
||||
mark: '标记',
|
||||
rank: '等级',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/zh-HK.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: '方向',
|
||||
shape: '形狀',
|
||||
mark: '標記',
|
||||
rank: '等級',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-conditional-formatting/src/locale/zh-TW.ts
Normal file
30
packages/sheets-conditional-formatting/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-conditional-formatting': {
|
||||
iconSet: {
|
||||
direction: '方向',
|
||||
shape: '形狀',
|
||||
mark: '標記',
|
||||
rank: '等級',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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 '../type';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CFRuleType, CFSubRuleType, CFTextOperator } from '../../base/const';
|
||||
import { ConditionalFormattingRangeIndexModel } from '../conditional-formatting-range-index-model';
|
||||
import { ConditionalFormattingRuleModel } from '../conditional-formatting-rule-model';
|
||||
|
||||
function createRule(cfId: string, startRow: number, endRow: number, startColumn: number, endColumn: number): IConditionFormattingRule {
|
||||
return {
|
||||
cfId,
|
||||
ranges: [{ startRow, endRow, startColumn, endColumn }],
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.containsText,
|
||||
value: 'x',
|
||||
style: { bg: { rgb: '#fff' } },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('ConditionalFormattingRangeIndexModel', () => {
|
||||
it('queries rule ids by cell and range through rule model changes', () => {
|
||||
const ruleModel = new ConditionalFormattingRuleModel();
|
||||
const rangeIndex = new ConditionalFormattingRangeIndexModel(ruleModel);
|
||||
const unitId = 'unit';
|
||||
const subUnitId = 'sheet';
|
||||
|
||||
ruleModel.addRule(unitId, subUnitId, createRule('a', 0, 2, 0, 2));
|
||||
ruleModel.addRule(unitId, subUnitId, createRule('b', 10, 12, 10, 12));
|
||||
|
||||
expect(rangeIndex.getRuleIdsByCell(unitId, subUnitId, 1, 1)).toEqual(new Set(['a']));
|
||||
expect(rangeIndex.getRuleIdsByRanges(unitId, subUnitId, [{
|
||||
startRow: 1,
|
||||
endRow: 11,
|
||||
startColumn: 1,
|
||||
endColumn: 11,
|
||||
}])).toEqual(new Set(['a', 'b']));
|
||||
|
||||
ruleModel.setRule(unitId, subUnitId, createRule('a', 20, 21, 20, 21), 'a');
|
||||
|
||||
expect(rangeIndex.getRuleIdsByCell(unitId, subUnitId, 1, 1)).toEqual(new Set());
|
||||
expect(rangeIndex.getRuleIdsByCell(unitId, subUnitId, 20, 20)).toEqual(new Set(['a']));
|
||||
|
||||
ruleModel.deleteRule(unitId, subUnitId, 'a');
|
||||
|
||||
expect(rangeIndex.getRuleIdsByCell(unitId, subUnitId, 20, 20)).toEqual(new Set());
|
||||
|
||||
rangeIndex.dispose();
|
||||
});
|
||||
|
||||
it('indexes rules that already exist before construction', () => {
|
||||
const ruleModel = new ConditionalFormattingRuleModel();
|
||||
const unitId = 'unit';
|
||||
const subUnitId = 'sheet';
|
||||
|
||||
ruleModel.addRule(unitId, subUnitId, createRule('a', 0, 2, 0, 2));
|
||||
|
||||
const rangeIndex = new ConditionalFormattingRangeIndexModel(ruleModel);
|
||||
|
||||
expect(rangeIndex.getRuleIdsByCell(unitId, subUnitId, 1, 1)).toEqual(new Set(['a']));
|
||||
|
||||
rangeIndex.dispose();
|
||||
});
|
||||
|
||||
it('can rebuild after unit rules are removed without rule change events', () => {
|
||||
const ruleModel = new ConditionalFormattingRuleModel();
|
||||
const rangeIndex = new ConditionalFormattingRangeIndexModel(ruleModel);
|
||||
const unitId = 'unit';
|
||||
const subUnitId = 'sheet';
|
||||
|
||||
ruleModel.addRule(unitId, subUnitId, createRule('a', 0, 2, 0, 2));
|
||||
expect(rangeIndex.getRuleIdsByCell(unitId, subUnitId, 1, 1)).toEqual(new Set(['a']));
|
||||
|
||||
ruleModel.deleteUnitId(unitId);
|
||||
rangeIndex.rebuild();
|
||||
|
||||
expect(rangeIndex.getRuleIdsByCell(unitId, subUnitId, 1, 1)).toEqual(new Set());
|
||||
|
||||
rangeIndex.dispose();
|
||||
});
|
||||
});
|
||||
@@ -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 { IColorScale } from '../../type';
|
||||
import { CellValueType } from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CFRuleType, CFValueType } from '../../../base/const';
|
||||
import { ColorScaleCalculateUnit } from '../color-scale-calculate-unit';
|
||||
|
||||
describe('test-color-scale', () => {
|
||||
it('test-sync-computing', () => {
|
||||
const colorScaleUnit = new ColorScaleCalculateUnit({
|
||||
unitId: '',
|
||||
subUnitId: '',
|
||||
workbook: null as any,
|
||||
worksheet: null as any,
|
||||
rule: {
|
||||
cfId: '123',
|
||||
ranges: [],
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.colorScale,
|
||||
config: [{ index: 0, value: { type: CFValueType.num, value: 10 }, color: 'red' }, { index: 1, value: { type: CFValueType.num, value: 20 }, color: 'white' }, { index: 2, value: { type: CFValueType.num, value: 30 }, color: 'green' }],
|
||||
} as IColorScale,
|
||||
},
|
||||
limit: 1000,
|
||||
accessor: null as any,
|
||||
getCellValue: (row, col) => ({ v: row + col, t: CellValueType.NUMBER }),
|
||||
});
|
||||
const count = vi.spyOn(colorScaleUnit, 'getCellResult' as any);
|
||||
|
||||
const color1 = colorScaleUnit.getCell(1, 1);
|
||||
const color2 = colorScaleUnit.getCell(2, 12);
|
||||
const color3 = colorScaleUnit.getCell(5, 20);
|
||||
const color4 = colorScaleUnit.getCell(11, 20);
|
||||
expect(color1).toBe('rgb(255,0,0)');
|
||||
expect(color2).toBe('rgb(255,102,102)');
|
||||
expect(color3).toBe('rgb(128,192,128)');
|
||||
expect(color4).toBe('rgb(0,128,0)');
|
||||
// 测试缓存
|
||||
colorScaleUnit.getCell(1, 1);
|
||||
// 之前已经 colorScaleUnit.getCell(1, 1) 后,会缓存,而跳过执行一次计算.
|
||||
expect(count.mock.calls.length).toBe(4);
|
||||
colorScaleUnit.clearCache();
|
||||
colorScaleUnit.getCell(1, 1);
|
||||
// 清除缓存后,会重新计算一次。
|
||||
expect(count.mock.calls.length).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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 { IColorScale, IConditionFormattingRule } from '../../type';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { CFRuleType, CFValueType } from '../../../base/const';
|
||||
import { createTestBed } from './test.util';
|
||||
|
||||
describe('Test colorScale', () => {
|
||||
let testBed: ReturnType<typeof createTestBed>;
|
||||
|
||||
beforeEach(() => {
|
||||
testBed && testBed.univer.dispose();
|
||||
testBed = createTestBed();
|
||||
testBed.getConditionalFormattingViewModel();
|
||||
});
|
||||
|
||||
it('colorScale normal test', () => {
|
||||
const params: IConditionFormattingRule<IColorScale> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 5, endColumn: 6 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.colorScale,
|
||||
config: [{
|
||||
index: 0,
|
||||
color: '#d0d9fb',
|
||||
value: {
|
||||
type: CFValueType.min,
|
||||
},
|
||||
}, {
|
||||
index: 1,
|
||||
color: '#2e55ef',
|
||||
value: {
|
||||
type: CFValueType.max,
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 0);
|
||||
const one = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 0);
|
||||
const two = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const three = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 2);
|
||||
const four = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 3);
|
||||
const five = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 4);
|
||||
expect(one).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(208,217,251)',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(two).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(181,195,249)',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(three).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(154,173,247)',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(four).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(127,151,245)',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(five).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(100,129,243)',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('The colors are consistent, but the values are interchangeable', () => {
|
||||
const params: IConditionFormattingRule<IColorScale> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 5, endColumn: 6 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.colorScale,
|
||||
config: [{
|
||||
index: 0,
|
||||
color: '#d0d9fb',
|
||||
value: {
|
||||
type: CFValueType.num,
|
||||
value: 1,
|
||||
},
|
||||
}, {
|
||||
index: 1,
|
||||
color: '#2e55ef',
|
||||
value: {
|
||||
type: CFValueType.num,
|
||||
value: -5,
|
||||
},
|
||||
}, {
|
||||
index: 2,
|
||||
color: 'rgb(231, 37, 143)',
|
||||
value: {
|
||||
type: CFValueType.num,
|
||||
value: 3,
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 0);
|
||||
const one = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 0); // 1
|
||||
const two = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1); // 2
|
||||
const three = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 2);// 3
|
||||
const four = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 3); // 4
|
||||
const five = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 6); // is same as 3
|
||||
expect(one).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(46,85,239)',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(two).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(139,61,191)',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(three).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(231,37,143)',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(four).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(231,37,143)',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(five).toEqual({
|
||||
style: {
|
||||
bg: {
|
||||
rgb: 'rgb(231,37,143)',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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, IDataBar } from '../../type';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { CFRuleType, CFValueType } from '../../../base/const';
|
||||
import { createTestBed } from './test.util';
|
||||
|
||||
describe('Test conditional formatting data bar', () => {
|
||||
let testBed: ReturnType<typeof createTestBed>;
|
||||
|
||||
beforeEach(() => {
|
||||
testBed && testBed.univer.dispose();
|
||||
testBed = createTestBed();
|
||||
testBed.getConditionalFormattingViewModel();
|
||||
});
|
||||
|
||||
it('The maximum and minimum values of the data bar are corrected automatically when minimum is greater than maximum.', () => {
|
||||
const params: IConditionFormattingRule<IDataBar> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 6 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.dataBar,
|
||||
isShowValue: false,
|
||||
config: {
|
||||
// The minimum is greater than the maximum
|
||||
min: { type: CFValueType.num, value: 7 },
|
||||
max: { type: CFValueType.num, value: 1 },
|
||||
isGradient: false,
|
||||
positiveColor: '#abd91a',
|
||||
nativeColor: '#ffbe38',
|
||||
},
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
|
||||
const start = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 0, 0);
|
||||
const end = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 0, 6);
|
||||
|
||||
expect(start).toEqual({});
|
||||
|
||||
expect(end).toEqual({
|
||||
dataBar: {
|
||||
color: '#abd91a',
|
||||
startPoint: 0,
|
||||
value: 100,
|
||||
isGradient: false,
|
||||
isShowValue: false,
|
||||
},
|
||||
isShowValue: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('test data bar normal', () => {
|
||||
const params: IConditionFormattingRule<IDataBar> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 6, endColumn: 6 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.dataBar,
|
||||
isShowValue: true,
|
||||
config: {
|
||||
min: { value: 2, type: CFValueType.num },
|
||||
max: { value: 5, type: CFValueType.num },
|
||||
isGradient: true,
|
||||
positiveColor: '#abd91a',
|
||||
nativeColor: '#ffbe38',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const one = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 0);
|
||||
const two = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const three = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 2);
|
||||
const four = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 3);
|
||||
const five = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 4);
|
||||
const six = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 6, 0);
|
||||
|
||||
// If less than the minimum value,do not render the data bar.
|
||||
expect(one).toEqual({});
|
||||
expect(two).toEqual({});
|
||||
expect(three).toEqual({
|
||||
dataBar: {
|
||||
color: '#abd91a',
|
||||
startPoint: 0,
|
||||
value: 33.333333333333336,
|
||||
isGradient: true,
|
||||
isShowValue: true,
|
||||
},
|
||||
isShowValue: true,
|
||||
|
||||
});
|
||||
|
||||
expect(four).toEqual({
|
||||
dataBar: {
|
||||
color: '#abd91a',
|
||||
startPoint: 0,
|
||||
value: 66.66666666666667,
|
||||
isGradient: true,
|
||||
isShowValue: true,
|
||||
},
|
||||
isShowValue: true,
|
||||
|
||||
});
|
||||
expect(five).toEqual({
|
||||
dataBar: {
|
||||
color: '#abd91a',
|
||||
startPoint: 0,
|
||||
value: 100,
|
||||
isGradient: true,
|
||||
isShowValue: true,
|
||||
},
|
||||
isShowValue: true,
|
||||
});
|
||||
expect(six).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,749 @@
|
||||
/**
|
||||
* 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, IConditionFormattingRule, INumberHighlightCell, IRankHighlightCell, ITextHighlightCell } from '../../type';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { CFNumberOperator, CFRuleType, CFSubRuleType, CFTextOperator } from '../../../base/const';
|
||||
import { createTestBed } from './test.util';
|
||||
|
||||
describe('Test conditional formatting highlight', () => {
|
||||
let testBed: ReturnType<typeof createTestBed>;
|
||||
|
||||
beforeEach(() => {
|
||||
testBed && testBed.univer.dispose();
|
||||
testBed = createTestBed();
|
||||
testBed.getConditionalFormattingViewModel();
|
||||
});
|
||||
|
||||
describe('Test Number', async () => {
|
||||
it('Should apply higher-priority rule style when multiple rules are matched', () => {
|
||||
const lowerPriorityRule: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#FFC000' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.between,
|
||||
value: [0, 10],
|
||||
},
|
||||
};
|
||||
|
||||
const higherPriorityRule: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#FFFF00' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.between,
|
||||
value: [0, 10],
|
||||
},
|
||||
};
|
||||
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, lowerPriorityRule);
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, higherPriorityRule);
|
||||
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#FFFF00' } } });
|
||||
});
|
||||
|
||||
it('Should not stop at stopIfTrue rule when the rule is not matched', () => {
|
||||
const shouldApplyLaterRule: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.between,
|
||||
value: [0, 10],
|
||||
},
|
||||
};
|
||||
|
||||
const stopIfTrueButNotMatched: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: true,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#FF0000' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.greaterThan,
|
||||
value: 100,
|
||||
},
|
||||
};
|
||||
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, shouldApplyLaterRule);
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, stopIfTrueButNotMatched);
|
||||
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
|
||||
it('Is CFNumberOperator.between', async () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.between,
|
||||
value: [0, 10],
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
|
||||
it('Is not CFNumberOperator.between', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.between,
|
||||
value: [5, 10],
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('Is CFNumberOperator.equal', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.equal,
|
||||
value: 2,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('Is not CFNumberOperator.equal', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.equal,
|
||||
value: 2,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 2);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('Is CFNumberOperator.greaterThan', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.greaterThan,
|
||||
value: 0,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('Is not CFNumberOperator.greaterThan', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.greaterThan,
|
||||
value: 2,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('Is CFNumberOperator.greaterThanOrEqual', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.greaterThanOrEqual,
|
||||
value: 0,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('Is not CFNumberOperator.greaterThanOrEqual', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.greaterThanOrEqual,
|
||||
value: 3,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('Is CFNumberOperator.lessThan', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.lessThan,
|
||||
value: 3,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it(`Is not ${CFNumberOperator.lessThan}`, () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.lessThan,
|
||||
value: 0,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it(`Is ${CFNumberOperator.lessThanOrEqual}`, () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.lessThanOrEqual,
|
||||
value: 3,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('Is not CFNumberOperator.lessThanOrEqual', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.lessThanOrEqual,
|
||||
value: 0,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('Is CFNumberOperator.notBetween', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.notBetween,
|
||||
value: [5, 7],
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('Is not CFNumberOperator.notBetween', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.notBetween,
|
||||
value: [0, 3],
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('Is CFNumberOperator.notEqual', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.notEqual,
|
||||
value: 11,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('Is not CFNumberOperator.notEqual', () => {
|
||||
const params: IConditionFormattingRule<INumberHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.notEqual,
|
||||
value: 2,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
});
|
||||
describe('Test Text', () => {
|
||||
it('is CFTextOperator.beginsWith', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.beginsWith,
|
||||
value: '2',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('is not CFTextOperator.beginsWith', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.beginsWith,
|
||||
value: '11',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
|
||||
it('is CFTextOperator.endsWith', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.endsWith,
|
||||
value: '2',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('is not CFTextOperator.endsWith', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.endsWith,
|
||||
value: '11',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
|
||||
it('is CFTextOperator.containsText', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.containsText,
|
||||
value: '2',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('is not CFTextOperator.containsText', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.containsText,
|
||||
value: '11',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('is CFTextOperator.equal', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.containsText,
|
||||
value: '2',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('is not CFTextOperator.equal', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.containsText,
|
||||
value: '11',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('is CFTextOperator.notEqual', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.notEqual,
|
||||
value: '22',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('is not CFTextOperator.notEqual', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.notEqual,
|
||||
value: '2',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
it('is CFTextOperator.notContainsText', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.notEqual,
|
||||
value: '11',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it('is not CFTextOperator.notContainsText', () => {
|
||||
const params: IConditionFormattingRule<ITextHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.notEqual,
|
||||
value: '2',
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: {} });
|
||||
});
|
||||
});
|
||||
describe('Test average', () => {
|
||||
it(`is ${CFNumberOperator.greaterThanOrEqual} average`, () => {
|
||||
const params: IConditionFormattingRule<IAverageHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.average,
|
||||
operator: CFNumberOperator.greaterThanOrEqual,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
it(`is ${CFNumberOperator.lessThanOrEqual} average`, () => {
|
||||
const params: IConditionFormattingRule<IAverageHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.average,
|
||||
operator: CFNumberOperator.lessThanOrEqual,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 0);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
});
|
||||
describe('Test rank', () => {
|
||||
it('is bottom 2', () => {
|
||||
const params: IConditionFormattingRule<IRankHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 4, endColumn: 4 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.rank,
|
||||
isBottom: true,
|
||||
isPercent: false,
|
||||
value: 2,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 0);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 0);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
|
||||
it('is bottom 6', () => {
|
||||
const params: IConditionFormattingRule<IRankHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 4, endColumn: 4 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.rank,
|
||||
isBottom: true,
|
||||
isPercent: false,
|
||||
value: 6,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
const result = testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 1, 1);
|
||||
expect(result).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
});
|
||||
|
||||
it('is bottom 22%', () => {
|
||||
const params: IConditionFormattingRule<IRankHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 8 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.rank,
|
||||
isBottom: true,
|
||||
isPercent: true,
|
||||
value: 22,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const results = [
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 0, 0),
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 0, 1),
|
||||
];
|
||||
expect(results[0]).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
expect(results[1]).toEqual({ style: {} });
|
||||
});
|
||||
|
||||
it('is bottom 23%', () => {
|
||||
const params: IConditionFormattingRule<IRankHighlightCell> = {
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 8 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.rank,
|
||||
isBottom: true,
|
||||
isPercent: true,
|
||||
value: 23,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const results = [
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 0, 0),
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 0, 1),
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 0, 2),
|
||||
];
|
||||
expect(results[0]).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
expect(results[1]).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
expect(results[2]).toEqual({ style: {} });
|
||||
});
|
||||
|
||||
// In this case, 2 items should be highlighted. Where the second item is exactly 20%.
|
||||
it('is bottom 20%', () => {
|
||||
const params: IConditionFormattingRule<IRankHighlightCell> = {
|
||||
ranges: [{ startRow: 7, startColumn: 0, endRow: 7, endColumn: 9 }],
|
||||
cfId: testBed.getConditionalFormattingRuleModel().createCfId(testBed.unitId, testBed.subUnitId),
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
style: { bg: { rgb: '#2AEAEA' } },
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.rank,
|
||||
isBottom: true,
|
||||
isPercent: true,
|
||||
value: 20,
|
||||
},
|
||||
};
|
||||
testBed.getConditionalFormattingRuleModel().addRule(testBed.unitId, testBed.subUnitId, params);
|
||||
const results = [
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 7, 0),
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 7, 1),
|
||||
testBed.getConditionalFormattingService().composeStyle(testBed.unitId, testBed.subUnitId, 7, 2),
|
||||
];
|
||||
expect(results[0]).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
expect(results[1]).toEqual({ style: { bg: { rgb: '#2AEAEA' } } });
|
||||
expect(results[2]).toEqual({ style: {} });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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, IWorkbookData } from '@univerjs/core';
|
||||
import { ICommandService, Inject, Injector, IUniverInstanceService, LocaleType, Plugin, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { IActiveDirtyManagerService, RegisterOtherFormulaService } from '@univerjs/engine-formula';
|
||||
import { SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { AddConditionalRuleMutation } from '../../../commands/mutations/add-conditional-rule.mutation';
|
||||
import { DeleteConditionalRuleMutation } from '../../../commands/mutations/delete-conditional-rule.mutation';
|
||||
import { MoveConditionalRuleMutation } from '../../../commands/mutations/move-conditional-rule.mutation';
|
||||
import { SetConditionalRuleMutation } from '../../../commands/mutations/set-conditional-rule.mutation';
|
||||
import { ConditionalFormattingFormulaService } from '../../../services/conditional-formatting-formula.service';
|
||||
import { ConditionalFormattingRangeTransformService } from '../../../services/conditional-formatting-range-transform.service';
|
||||
import { ConditionalFormattingStyleComposer } from '../../../services/conditional-formatting-style-composer.service';
|
||||
import { ConditionalFormattingService } from '../../../services/conditional-formatting.service';
|
||||
import { ConditionalFormattingRangeIndexModel } from '../../conditional-formatting-range-index-model';
|
||||
import { ConditionalFormattingRuleModel } from '../../conditional-formatting-rule-model';
|
||||
import { ConditionalFormattingViewModel } from '../../conditional-formatting-view-model';
|
||||
|
||||
const TEST_WORKBOOK_DATA_DEMO: () => IWorkbookData = () => ({
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: { 0: { v: 1, t: 2 }, 1: { v: 2, t: 2 }, 2: { v: 3, t: 2 }, 3: { v: 4, t: 2 }, 4: { v: 5, t: 2 }, 5: { v: 6, t: 2 }, 6: { v: 7, t: 2 }, 7: { v: 8, t: 2 }, 8: { v: 9, t: 2 } },
|
||||
1: { 0: { v: 1, t: 2 }, 1: { v: 2, t: 2 }, 2: { v: 3, t: 2 }, 3: { v: 4, t: 2 }, 4: { v: 5, t: 2 }, 5: { v: 6, t: 2 }, 6: { v: 7, t: 2 } },
|
||||
2: { 0: { v: 1, t: 2 }, 1: { v: 2, t: 2 }, 2: { v: 3, t: 2 }, 3: { v: 4, t: 2 }, 4: { v: 5, t: 2 }, 5: { v: 6, t: 2 }, 6: { v: 7, t: 2 } },
|
||||
3: { 0: { v: 1, t: 2 }, 1: { v: 2, t: 2 }, 2: { v: 3, t: 2 }, 3: { v: 4, t: 2 }, 4: { v: 5, t: 2 }, 5: { v: 6, t: 2 }, 6: { v: 7, t: 2 } },
|
||||
4: { 0: { v: 1, t: 2 }, 1: { v: 2, t: 2 }, 2: { v: 3, t: 2 }, 3: { v: 4, t: 2 }, 4: { v: 5, t: 2 }, 5: { v: 6, t: 2 }, 6: { v: 7, t: 2 } },
|
||||
5: { 0: { v: 1, t: 2 }, 1: { v: 2, t: 2 }, 2: { v: 3, t: 2 }, 3: { v: 4, t: 2 }, 4: { v: 5, t: 2 }, 5: { v: 6, t: 2 }, 6: { v: 7, t: 2 } },
|
||||
6: { 0: { v: 0, t: 2 } },
|
||||
7: { 0: { v: 1, t: 2 }, 1: { v: 2, t: 2 }, 2: { v: 3, t: 2 }, 3: { v: 4, t: 2 }, 4: { v: 5, t: 2 }, 5: { v: 6, t: 2 }, 6: { v: 7, t: 2 }, 7: { v: 8, t: 2 }, 8: { v: 9, t: 2 }, 9: { v: 10, t: 2 } },
|
||||
},
|
||||
rowCount: 8,
|
||||
columnCount: 10,
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
resources: [],
|
||||
});
|
||||
|
||||
export const createTestBed = (dependencies?: Dependency[]) => {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
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();
|
||||
this._injector.add([SheetInterceptorService]);
|
||||
this._injector.add([ConditionalFormattingService]);
|
||||
this._injector.add([RegisterOtherFormulaService]);
|
||||
this._injector.add([ConditionalFormattingFormulaService]);
|
||||
this._injector.add([ConditionalFormattingRangeTransformService]);
|
||||
this._injector.add([ConditionalFormattingStyleComposer]);
|
||||
this._injector.add([ConditionalFormattingRuleModel]);
|
||||
this._injector.add([ConditionalFormattingRangeIndexModel]);
|
||||
this._injector.add([ConditionalFormattingViewModel]);
|
||||
this._injector.add([IActiveDirtyManagerService, { useFactory: () => ({ register: () => { /* empty */ } } as any) }]);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
dependencies?.forEach((d) => this._injector.add(d));
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
const workbookJson = TEST_WORKBOOK_DATA_DEMO();
|
||||
const workbook = univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookJson);
|
||||
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
const commandService = injector.get(ICommandService);
|
||||
|
||||
[
|
||||
AddConditionalRuleMutation,
|
||||
DeleteConditionalRuleMutation,
|
||||
SetConditionalRuleMutation,
|
||||
MoveConditionalRuleMutation,
|
||||
].forEach((commandInfo) => {
|
||||
commandService.registerCommand(commandInfo);
|
||||
});
|
||||
|
||||
const unitId = workbookJson.id;
|
||||
const subUnitId = workbookJson.sheets.sheet1.id!;
|
||||
|
||||
univerInstanceService.focusUnit('test');
|
||||
univerInstanceService.setCurrentUnitForType(unitId);
|
||||
|
||||
const getConditionalFormattingRuleModel = () => injector.get(ConditionalFormattingRuleModel);
|
||||
const getConditionalFormattingViewModel = () => injector.get(ConditionalFormattingViewModel);
|
||||
const getConditionalFormattingService = () => injector.get(ConditionalFormattingService);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
workbook,
|
||||
unitId,
|
||||
subUnitId,
|
||||
commandService,
|
||||
getConditionalFormattingRuleModel,
|
||||
getConditionalFormattingViewModel,
|
||||
getConditionalFormattingService,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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 '../../type';
|
||||
import { BooleanNumber, CellValueType, ColorKit, ObjectMatrix } from '@univerjs/core';
|
||||
import { FormulaResultStatus } from '@univerjs/engine-formula';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CFNumberOperator, CFRuleType, CFSubRuleType, CFValueType } from '../../../base/const';
|
||||
import {
|
||||
compareWithNumber,
|
||||
filterRange,
|
||||
getCacheStyleMatrix,
|
||||
getCellValue,
|
||||
getColorScaleFromValue,
|
||||
getMaxInFormulaResult,
|
||||
getOppositeOperator,
|
||||
getValueByType,
|
||||
isFloatsEqual,
|
||||
isNullable,
|
||||
serialTimeToTimestamp,
|
||||
} from '../utils';
|
||||
|
||||
describe('conditional formatting calculate utilities', () => {
|
||||
it('normalizes cell values used by conditional formatting rules', () => {
|
||||
expect(isFloatsEqual(1, 1 + Number.EPSILON / 2)).toBe(true);
|
||||
expect(isNullable(null)).toBe(true);
|
||||
expect(isNullable(undefined)).toBe(true);
|
||||
expect(isNullable(' ')).toBe(true);
|
||||
expect(getCellValue()).toBeNull();
|
||||
expect(getCellValue({ t: CellValueType.BOOLEAN, v: BooleanNumber.TRUE })).toBe('TRUE');
|
||||
expect(getCellValue({ t: CellValueType.BOOLEAN, v: BooleanNumber.FALSE })).toBe('FALSE');
|
||||
expect(getCellValue({ v: '', p: { body: { dataStream: 'rich\r\n' } } as any })).toBe('rich');
|
||||
expect(getCellValue({ v: 'plain' })).toBe('plain');
|
||||
});
|
||||
|
||||
it('converts spreadsheet serial dates and filters ranges to sheet bounds', () => {
|
||||
expect(serialTimeToTimestamp(44927.5)).toBe(new Date(2023, 0, 1, 12).getTime());
|
||||
|
||||
expect(filterRange([
|
||||
{ startRow: 0, startColumn: 0, endRow: 10, endColumn: 10 },
|
||||
{ startRow: 100, startColumn: 0, endRow: 101, endColumn: 1 },
|
||||
], 5, 6)).toEqual([
|
||||
{ startRow: 0, startColumn: 0, endRow: 5, endColumn: 6 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('calculates threshold values from matrices for data bars, color scales and icon sets', () => {
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
matrix.setValue(0, 0, 2);
|
||||
matrix.setValue(0, 1, 6);
|
||||
matrix.setValue(0, 2, 10);
|
||||
const context = {
|
||||
cfId: 'cf-1',
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
accessor: {
|
||||
get() {
|
||||
throw new Error('formula service is not used by this test');
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
expect(getValueByType({ type: CFValueType.max }, matrix, context)).toEqual({
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: 10,
|
||||
});
|
||||
expect(getValueByType({ type: CFValueType.min }, matrix, context)).toEqual({
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: 2,
|
||||
});
|
||||
expect(getValueByType({ type: CFValueType.percent, value: 50 }, matrix, context)).toEqual({
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: 6,
|
||||
});
|
||||
expect(getValueByType({ type: CFValueType.percentile, value: 50 }, matrix, context)).toEqual({
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: 6,
|
||||
});
|
||||
expect(getValueByType({ type: CFValueType.num, value: 'bad-number' }, matrix, context)).toEqual({
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('compares numbers and derives opposite operators for icon range bands', () => {
|
||||
expect(compareWithNumber({ operator: CFNumberOperator.between, value: [10, 1] }, 5)).toBe(true);
|
||||
expect(compareWithNumber({ operator: CFNumberOperator.notBetween, value: [1, 10] }, 12)).toBe(true);
|
||||
expect(compareWithNumber({ operator: CFNumberOperator.equal, value: 3 }, 3)).toBe(true);
|
||||
expect(compareWithNumber({ operator: CFNumberOperator.notEqual, value: 3 }, 4)).toBe(true);
|
||||
expect(compareWithNumber({ operator: CFNumberOperator.greaterThan, value: 3 }, 4)).toBe(true);
|
||||
expect(compareWithNumber({ operator: CFNumberOperator.greaterThanOrEqual, value: 3 }, 3)).toBe(true);
|
||||
expect(compareWithNumber({ operator: CFNumberOperator.lessThan, value: 3 }, 2)).toBe(true);
|
||||
expect(compareWithNumber({ operator: CFNumberOperator.lessThanOrEqual, value: 3 }, 3)).toBe(true);
|
||||
expect(compareWithNumber({ operator: 'unknown' as CFNumberOperator, value: 3 }, 3)).toBe(false);
|
||||
expect(getOppositeOperator(CFNumberOperator.greaterThan)).toBe(CFNumberOperator.lessThanOrEqual);
|
||||
expect(getOppositeOperator(CFNumberOperator.greaterThanOrEqual)).toBe(CFNumberOperator.lessThan);
|
||||
expect(getOppositeOperator(CFNumberOperator.lessThan)).toBe(CFNumberOperator.greaterThanOrEqual);
|
||||
expect(getOppositeOperator(CFNumberOperator.lessThanOrEqual)).toBe(CFNumberOperator.greaterThan);
|
||||
expect(getOppositeOperator(CFNumberOperator.equal)).toBe(CFNumberOperator.equal);
|
||||
});
|
||||
|
||||
it('interpolates colors, reads cached style results, and computes formula result maxima', () => {
|
||||
expect(getColorScaleFromValue([
|
||||
{ value: 0, color: new ColorKit('#000000') },
|
||||
{ value: 10, color: new ColorKit('#ffffff') },
|
||||
], -1)).toBe('rgb(0,0,0)');
|
||||
expect(getColorScaleFromValue([
|
||||
{ value: 0, color: new ColorKit('#000000') },
|
||||
{ value: 10, color: new ColorKit('#ffffff') },
|
||||
], 5)).toBe('rgb(128,128,128)');
|
||||
expect(getColorScaleFromValue([
|
||||
{ value: 0, color: new ColorKit('#000000') },
|
||||
{ value: 10, color: new ColorKit('#ffffff') },
|
||||
], 20)).toBe('rgb(255,255,255)');
|
||||
|
||||
const rule: IConditionFormattingRule = {
|
||||
cfId: 'cf-1',
|
||||
ranges: [{ startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }],
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.number,
|
||||
operator: CFNumberOperator.equal,
|
||||
style: {},
|
||||
},
|
||||
} as any;
|
||||
const matrix = getCacheStyleMatrix<string>('unit', 'sheet', rule, {
|
||||
accessor: {
|
||||
get() {
|
||||
return {
|
||||
getCellCfs(_unitId: string, _subUnitId: string, row: number, col: number) {
|
||||
return row === 0 && col === 0
|
||||
? [{ cfId: 'cf-1', result: 'hit' }, { cfId: 'cf-2', result: 'miss' }]
|
||||
: undefined;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
expect(matrix.getValue(0, 0)).toBe('hit');
|
||||
expect(matrix.getValue(1, 1)).toBeUndefined();
|
||||
expect(getMaxInFormulaResult({ 0: { 0: 1, 1: '9' } })).toBe(9);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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, ICellData, Nullable, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { IConditionFormattingRule } from '../type';
|
||||
import { LRUMap } from '@univerjs/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
export enum CalculateEmitStatus {
|
||||
preComputingStart = 'preComputingStart',
|
||||
preComputing = 'preComputing',
|
||||
preComputingEnd = 'preComputingEnd',
|
||||
preComputingError = 'preComputingError',
|
||||
|
||||
}
|
||||
export interface IContext {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
workbook: Workbook;
|
||||
worksheet: Worksheet;
|
||||
accessor: IAccessor;
|
||||
rule: IConditionFormattingRule;
|
||||
getCellValue: (row: number, col: number) => ICellData;
|
||||
limit: number;
|
||||
}
|
||||
/**
|
||||
* Processing Main Path Calculation Logic
|
||||
*/
|
||||
export abstract class BaseCalculateUnit<C = any, S = any> {
|
||||
/**
|
||||
* 3nd-level cache
|
||||
*/
|
||||
private _cache: LRUMap<string, Nullable<S>>;
|
||||
|
||||
protected _preComputingStatus$ = new BehaviorSubject<CalculateEmitStatus>(CalculateEmitStatus.preComputingStart);
|
||||
public preComputingStatus$ = this._preComputingStatus$.asObservable().pipe(distinctUntilChanged());
|
||||
/**
|
||||
* 2nd-level cache
|
||||
*/
|
||||
private _preComputingCache: Nullable<C>;
|
||||
private _rule: IConditionFormattingRule;
|
||||
|
||||
constructor(private _context: IContext) {
|
||||
this._cache = new LRUMap(_context.limit);
|
||||
this._rule = _context.rule;
|
||||
this._preComputingCache = null;
|
||||
this._initClearCacheListener();
|
||||
}
|
||||
|
||||
public setCacheLength(length: number) {
|
||||
this._cache.limit = length;
|
||||
}
|
||||
|
||||
public clearCache() {
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
public resetPreComputingCache() {
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputingStart);
|
||||
this._preComputingCache = null;
|
||||
}
|
||||
|
||||
public updateRule(rule: IConditionFormattingRule) {
|
||||
this._rule = rule;
|
||||
this.resetPreComputingCache();
|
||||
}
|
||||
|
||||
public getCell(row: number, col: number) {
|
||||
const key = this._createKey(row, col);
|
||||
|
||||
if (this._preComputingStatus$.getValue() === CalculateEmitStatus.preComputing) {
|
||||
// If the pre-calculation is not yet complete, return the previous result directly.
|
||||
return this._cache.get(key);
|
||||
}
|
||||
let preComputingResult = this.getPreComputingResult(row, col);
|
||||
if (preComputingResult === null) {
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputingStart);
|
||||
this.preComputing(row, col, this._getContext());
|
||||
preComputingResult = this.getPreComputingResult(row, col);
|
||||
|
||||
if (preComputingResult === null) {
|
||||
// If the calculation is not yet complete, return the previous cache.
|
||||
return this._cache.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputingEnd);
|
||||
|
||||
if (this._cache.has(key)) {
|
||||
return this._cache.get(key);
|
||||
}
|
||||
const result = this.getCellResult(row, col, preComputingResult, this._getContext());
|
||||
if (result !== null) {
|
||||
this._setCache(row, col, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// If a null value is set, it indicates that caching is not required.
|
||||
abstract preComputing(row: number, col: number, context: IContext): void;
|
||||
|
||||
/**
|
||||
* If a null value is returned, it indicates that caching is not required.
|
||||
*/
|
||||
protected abstract getCellResult(row: number, col: number, preComputingResult: Nullable<C>, context: IContext): Nullable<S>;
|
||||
|
||||
protected setPreComputingCache(v: C) {
|
||||
this._preComputingCache = v;
|
||||
}
|
||||
|
||||
protected getPreComputingResult(_row: number, _col: number) {
|
||||
return this._preComputingCache;
|
||||
}
|
||||
|
||||
private _createKey(row: number, col: number) {
|
||||
return `${row}_${col}`;
|
||||
}
|
||||
|
||||
private _setCache(row: number, col: number, v: Nullable<S>) {
|
||||
const key = this._createKey(row, col);
|
||||
this._cache.set(key, v);
|
||||
}
|
||||
|
||||
private _getContext() {
|
||||
return { ...this._context, rule: this._rule };
|
||||
}
|
||||
|
||||
private _initClearCacheListener() {
|
||||
this.preComputingStatus$.subscribe((e) => {
|
||||
if (e === CalculateEmitStatus.preComputingEnd) {
|
||||
this.clearCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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 { IColorScale, IConditionFormattingRule } from '../type';
|
||||
import type { IContext } from './base-calculate-unit';
|
||||
import { CellValueType, ColorKit, ObjectMatrix, Range } from '@univerjs/core';
|
||||
import { FormulaResultStatus } from '@univerjs/engine-formula';
|
||||
import { isObject } from '@univerjs/engine-render';
|
||||
import { CFValueType } from '../../base/const';
|
||||
import { BaseCalculateUnit, CalculateEmitStatus } from './base-calculate-unit';
|
||||
import { filterRange, getColorScaleFromValue, getValueByType, isNullable } from './utils';
|
||||
|
||||
;
|
||||
|
||||
interface IConfigItem {
|
||||
value: number;
|
||||
color: ColorKit;
|
||||
}
|
||||
|
||||
export class ColorScaleCalculateUnit extends BaseCalculateUnit<IConfigItem[], string> {
|
||||
override preComputing(_row: number, _col: number, context: IContext): void {
|
||||
const rule = context.rule as IConditionFormattingRule<IColorScale>;
|
||||
const worksheet = context.worksheet;
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
const isNeedMatrix = !rule.rule.config.every((item) => item.value.type === CFValueType.num);
|
||||
if (isNeedMatrix) {
|
||||
const ranges = filterRange(rule.ranges, worksheet.getMaxRows() - 1, worksheet.getMaxColumns() - 1);
|
||||
ranges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
const cell = context.getCellValue(row, col);
|
||||
const v = cell && cell.v;
|
||||
if (!isNullable(v) && cell?.t === CellValueType.NUMBER) {
|
||||
const _value = Number(v);
|
||||
!Number.isNaN(_value) && matrix.setValue(row, col, _value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
const _configList = [...rule.rule.config].sort((a, b) => a.index - b.index).map((config) => {
|
||||
return {
|
||||
value: getValueByType(config.value, matrix, {
|
||||
...context,
|
||||
cfId: rule.cfId,
|
||||
}),
|
||||
color: new ColorKit(config.color),
|
||||
};
|
||||
});
|
||||
const isAllFinished = !_configList.some((item) => isObject(item.value) ? item.value.status !== FormulaResultStatus.SUCCESS : false);
|
||||
|
||||
if (isAllFinished) {
|
||||
const colorList = _configList
|
||||
.map((item) => item.color)
|
||||
.reduce((result, color, index) => {
|
||||
result.result.push({ color, value: result.sortValue[index] });
|
||||
return result;
|
||||
}, {
|
||||
result: [] as { value: number; color: ColorKit }[],
|
||||
sortValue: _configList.map((item) => item.value.result as number).sort((a, b) => a - b),
|
||||
})
|
||||
.result;
|
||||
this.setPreComputingCache(colorList);
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputingEnd);
|
||||
return;
|
||||
}
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputing);
|
||||
}
|
||||
|
||||
protected override getCellResult(row: number, col: number, preComputingResult: IConfigItem[], context: IContext) {
|
||||
if (!preComputingResult) {
|
||||
return null;
|
||||
}
|
||||
const cell = context.getCellValue(row, col);
|
||||
if (cell.t === CellValueType.NUMBER) {
|
||||
const v = Number(cell.v);
|
||||
if (!Number.isNaN(v)) {
|
||||
const color = getColorScaleFromValue(preComputingResult, v);
|
||||
return color;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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 { ICellData } from '@univerjs/core';
|
||||
import type { IConditionFormattingRule, IDataBar } from '../type';
|
||||
import type { IContext } from './base-calculate-unit';
|
||||
import { CellValueType, ObjectMatrix, Range } from '@univerjs/core';
|
||||
import { FormulaResultStatus } from '@univerjs/engine-formula';
|
||||
import { CFValueType } from '../../base/const';
|
||||
import { defaultDataBarNativeColor, defaultDataBarPositiveColor, defaultPlaceholderColor } from '../../render/data-bar.render';
|
||||
import { BaseCalculateUnit, CalculateEmitStatus } from './base-calculate-unit';
|
||||
import { filterRange, getValueByType, isNullable } from './utils';
|
||||
|
||||
;
|
||||
|
||||
const getSafeValue = (v: number) => Math.max(Math.min(100, v), 0);
|
||||
|
||||
const getValue = (row: number, col: number, getCell: (row: number, col: number) => ICellData) => {
|
||||
const cell = getCell(row, col);
|
||||
if (cell && cell.t === CellValueType.NUMBER) {
|
||||
const value = Number(cell.v);
|
||||
return Number.isNaN(value) ? null : value;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
interface IConfig {
|
||||
min: number;
|
||||
max: number;
|
||||
startPoint: number;
|
||||
}
|
||||
export class DataBarCalculateUnit extends BaseCalculateUnit<IConfig> {
|
||||
override preComputing(row: number, col: number, context: IContext): void {
|
||||
const rule = context.rule as IConditionFormattingRule<IDataBar>;
|
||||
const ruleConfig = rule.rule;
|
||||
const worksheet = context.worksheet;
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
const isNeedMatrix = ![rule.rule.config.max, rule.rule.config.min].every((e) => e.type === CFValueType.num);
|
||||
if (isNeedMatrix) {
|
||||
const ranges = filterRange(rule.ranges, worksheet.getMaxRows() - 1, worksheet.getMaxColumns() - 1);
|
||||
ranges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
const cell = context.getCellValue(row, col);
|
||||
const v = cell && cell.v;
|
||||
if (!isNullable(v) && cell?.t === CellValueType.NUMBER) {
|
||||
const _value = Number(v);
|
||||
!Number.isNaN(_value) && matrix.setValue(row, col, _value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
const _min = getValueByType(ruleConfig.config.min, matrix, { ...context, cfId: rule.cfId });
|
||||
const _max = getValueByType(ruleConfig.config.max, matrix, { ...context, cfId: rule.cfId });
|
||||
let min = 0;
|
||||
let max = 0;
|
||||
|
||||
// If the formula triggers the calculation, wait for the result,
|
||||
// and use the previous style cache until the result comes out。
|
||||
if (_min.status === FormulaResultStatus.SUCCESS) {
|
||||
const v = Number(_min.result); // Get the minimum value
|
||||
min = Number.isNaN(v) ? 0 : v;
|
||||
} else {
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputing);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_max.status === FormulaResultStatus.SUCCESS) {
|
||||
const maxResult = Number(_max.result); // Get the maximum value
|
||||
const v = Number.isNaN(maxResult) ? 0 : maxResult;
|
||||
max = Math.max(v, min);
|
||||
min = Math.min(v, min);
|
||||
let startPoint = 50;
|
||||
if (min < 0 && max <= 0) {
|
||||
startPoint = 100;
|
||||
} else if (min < 0 && max > 0) {
|
||||
const length = Math.abs(max) + Math.abs(min);
|
||||
startPoint = Math.abs(min) / length * 100;
|
||||
} else if (min >= 0 && max > 0) {
|
||||
startPoint = 0;
|
||||
}
|
||||
this.setPreComputingCache({ min, max, startPoint });
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputingEnd);
|
||||
return;
|
||||
}
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputing);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
protected override getCellResult(row: number, col: number, preComputingResult: IConfig, context: IContext) {
|
||||
const { min, max, startPoint } = preComputingResult;
|
||||
const rule = context.rule as IConditionFormattingRule<IDataBar>;
|
||||
const ruleConfig = rule.rule;
|
||||
const isShowValue = ruleConfig.isShowValue;
|
||||
const isGradient = ruleConfig.config.isGradient;
|
||||
const value = getValue(row, col, context.getCellValue);
|
||||
if (value === null || value < min || (min === max || max < min)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (value === 0) {
|
||||
// Renders a placeholder if the current value is 0
|
||||
return { color: defaultPlaceholderColor, startPoint, value: 0, isGradient, isShowValue };
|
||||
}
|
||||
if (min < 0 && max <= 0) {
|
||||
const length = max - min;
|
||||
|
||||
const v = getSafeValue((max - value) / length * 100);
|
||||
if (v === 0) {
|
||||
// Renders noting if the result value is 0
|
||||
return undefined;
|
||||
}
|
||||
return { color: ruleConfig.config.nativeColor || defaultDataBarNativeColor, startPoint, value: -v, isGradient, isShowValue };
|
||||
} else if (min < 0 && max > 0) {
|
||||
if (value > 0) {
|
||||
const v = getSafeValue(Math.min(value / max, 1) * 100);
|
||||
if (v === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return { color: ruleConfig.config.positiveColor || defaultDataBarPositiveColor, startPoint, value: v, isGradient, isShowValue };
|
||||
} else {
|
||||
const v = getSafeValue(Math.min(Math.abs(value) / Math.abs(min), 1) * 100);
|
||||
if (v === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return { color: ruleConfig.config.nativeColor || defaultDataBarNativeColor, startPoint, value: -v, isGradient, isShowValue };
|
||||
}
|
||||
} else if (min >= 0 && max > 0) {
|
||||
const length = max - min;
|
||||
const startPoint = 0;
|
||||
const v = getSafeValue((1 - (max - value) / length) * 100);
|
||||
if (v === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return { color: ruleConfig.config.positiveColor || defaultDataBarPositiveColor, startPoint, value: v, isGradient, isShowValue };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* 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 { IStyleData, Nullable } from '@univerjs/core';
|
||||
import type { IAverageHighlightCell, IFormulaHighlightCell, IHighlightCell, INumberHighlightCell, IRankHighlightCell, ITextHighlightCell, ITimePeriodHighlightCell } from '../type';
|
||||
import type { IContext } from './base-calculate-unit';
|
||||
import { CellValueType, dateKit, Range, Tools } from '@univerjs/core';
|
||||
import { ERROR_TYPE_SET, FormulaResultStatus } from '@univerjs/engine-formula';
|
||||
import { CFNumberOperator, CFSubRuleType, CFTextOperator, CFTimePeriodOperator } from '../../base/const';
|
||||
import { ConditionalFormattingFormulaService } from '../../services/conditional-formatting-formula.service';
|
||||
import { BaseCalculateUnit, CalculateEmitStatus } from './base-calculate-unit';
|
||||
import { compareWithNumber, filterRange, getCellValue, isFloatsEqual, isNullable, serialTimeToTimestamp } from './utils';
|
||||
|
||||
interface IConfig {
|
||||
value: any;
|
||||
type: CFSubRuleType;
|
||||
}
|
||||
|
||||
const isFormulaResultMatched = (value: unknown) => value === true || value === 1 || value === 'TRUE';
|
||||
|
||||
const sortRangesByTopLeft = <T extends { startRow: number; startColumn: number }>(ranges: T[]) => [...ranges].sort((a, b) => {
|
||||
if (a.startRow !== b.startRow) {
|
||||
return a.startRow - b.startRow;
|
||||
}
|
||||
|
||||
return a.startColumn - b.startColumn;
|
||||
});
|
||||
|
||||
export class HighlightCellCalculateUnit extends BaseCalculateUnit<Nullable<IConfig>, Nullable<IStyleData>> {
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
override preComputing(row: number, col: number, context: IContext): void {
|
||||
const ruleConfig = context.rule.rule as IHighlightCell;
|
||||
const ranges = filterRange(context.rule.ranges, context.worksheet.getMaxRows() - 1, context.worksheet.getMaxColumns() - 1);
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
const getCache = () => {
|
||||
switch (ruleConfig.subType) {
|
||||
case CFSubRuleType.average: {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
ranges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
const cell = context.getCellValue(row, col);
|
||||
const v = getCellValue(cell);
|
||||
if (cell && cell.t === CellValueType.NUMBER && !isNullable(v)) {
|
||||
sum += Number(v) || 0;
|
||||
count++;
|
||||
}
|
||||
});
|
||||
});
|
||||
return { value: sum / count, type: ruleConfig.subType };
|
||||
}
|
||||
case CFSubRuleType.uniqueValues:
|
||||
case CFSubRuleType.duplicateValues: {
|
||||
const cacheMap: Map<any, number> = new Map();
|
||||
ranges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
const cell = context.getCellValue(row, col);
|
||||
const v = getCellValue(cell);
|
||||
if (!isNullable(v)) {
|
||||
const cache = cacheMap.get(v);
|
||||
if (cache) {
|
||||
cacheMap.set(v, cache + 1);
|
||||
} else {
|
||||
cacheMap.set(v, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return { value: cacheMap, type: ruleConfig.subType };
|
||||
}
|
||||
case CFSubRuleType.rank: {
|
||||
let allValue: number[] = [];
|
||||
ranges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
const cell = context.getCellValue(row, col);
|
||||
const v = getCellValue(cell);
|
||||
if (cell && cell.t === CellValueType.NUMBER && !isNullable(v)) {
|
||||
allValue.push(Number(v) || 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
allValue.sort((a, b) => b - a);
|
||||
const configRule = context.rule.rule as IRankHighlightCell;
|
||||
if (configRule.isPercent) {
|
||||
if (configRule.isBottom) {
|
||||
allValue = allValue.slice().reverse();
|
||||
}
|
||||
|
||||
// Calculate the index directly based on the threshold percentage.
|
||||
const threshold = Tools.clamp(configRule.value, 0, 100) / 100;
|
||||
const targetIndex = Math.floor(threshold * allValue.length);
|
||||
// Ensure the index is within bounds
|
||||
const safeIndex = Tools.clamp(targetIndex - 1, 0, allValue.length - 1);
|
||||
return { value: allValue[safeIndex], type: ruleConfig.subType };
|
||||
}
|
||||
|
||||
const targetIndex = Math.floor(Tools.clamp(configRule.isBottom ? (configRule.value - 1) : configRule.value, 0, allValue.length));
|
||||
if (configRule.isBottom) {
|
||||
return { value: allValue[allValue.length - targetIndex - 1], type: ruleConfig.subType };
|
||||
} else {
|
||||
return { value: allValue[Math.max(targetIndex - 1, 0)], type: ruleConfig.subType };
|
||||
}
|
||||
}
|
||||
case CFSubRuleType.formula: {
|
||||
const _ruleConfig = ruleConfig as IFormulaHighlightCell;
|
||||
const conditionalFormattingFormulaService = context.accessor.get(ConditionalFormattingFormulaService);
|
||||
const normalizedRanges = sortRangesByTopLeft(context.rule.ranges);
|
||||
conditionalFormattingFormulaService.registerFormulaWithRange(context.unitId, context.subUnitId, context.rule.cfId, _ruleConfig.value, normalizedRanges);
|
||||
const result = conditionalFormattingFormulaService.getFormulaMatrix(context.unitId, context.subUnitId, context.rule.cfId, _ruleConfig.value);
|
||||
if (result && result.status === FormulaResultStatus.SUCCESS) {
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputingEnd);
|
||||
return {
|
||||
value: result.result,
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
} else {
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case CFSubRuleType.timePeriod: {
|
||||
const subRuleConfig = ruleConfig as ITimePeriodHighlightCell;
|
||||
switch (subRuleConfig.operator) {
|
||||
case CFTimePeriodOperator.last7Days: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().subtract(7, 'day').valueOf(),
|
||||
end: dateKit().valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.lastMonth: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('month').subtract(1, 'month').valueOf(),
|
||||
end: dateKit().endOf('month').subtract(1, 'month').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.lastWeek: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('week').subtract(1, 'week').valueOf(),
|
||||
end: dateKit().endOf('week').subtract(1, 'week').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.nextMonth: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('month').add(1, 'month').valueOf(),
|
||||
end: dateKit().endOf('month').add(1, 'month').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.nextWeek: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('week').add(1, 'week').valueOf(),
|
||||
end: dateKit().endOf('week').add(1, 'week').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.thisMonth: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('month').valueOf(),
|
||||
end: dateKit().endOf('month').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.thisWeek: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('week').valueOf(),
|
||||
end: dateKit().endOf('week').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.today: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('day').valueOf(),
|
||||
end: dateKit().endOf('day').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.tomorrow: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('day').add(1, 'day').valueOf(),
|
||||
end: dateKit().endOf('day').add(1, 'day').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
case CFTimePeriodOperator.yesterday: {
|
||||
return {
|
||||
value: {
|
||||
start: dateKit().startOf('day').subtract(1, 'day').valueOf(),
|
||||
end: dateKit().endOf('day').subtract(1, 'day').valueOf(),
|
||||
},
|
||||
type: ruleConfig.subType,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = getCache();
|
||||
this.setPreComputingCache(result);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
protected override getCellResult(row: number, col: number, preComputingResult: Nullable<IConfig>, context: IContext) {
|
||||
const cellValue = context.getCellValue(row, col);
|
||||
const ruleConfig = context.rule.rule as IHighlightCell;
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
const run = () => {
|
||||
switch (ruleConfig.subType) {
|
||||
case CFSubRuleType.number: {
|
||||
const v = cellValue && Number(cellValue.v);
|
||||
const isNumber = cellValue?.t === CellValueType.NUMBER;
|
||||
const subRuleConfig = ruleConfig as INumberHighlightCell;
|
||||
if (!isNumber) {
|
||||
if ([CFNumberOperator.notEqual, CFNumberOperator.notBetween].includes(subRuleConfig.operator)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNullable(v) || Number.isNaN(v)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return compareWithNumber({ operator: subRuleConfig.operator, value: subRuleConfig.value || 0 }, v || 0);
|
||||
}
|
||||
case CFSubRuleType.text: {
|
||||
const subRuleConfig = ruleConfig as ITextHighlightCell;
|
||||
const value = getCellValue(cellValue);
|
||||
const v = isNullable(value) ? '' : String(value);
|
||||
const condition = subRuleConfig.value || '';
|
||||
switch (subRuleConfig.operator) {
|
||||
case CFTextOperator.beginsWith: {
|
||||
return v.startsWith(condition);
|
||||
}
|
||||
case CFTextOperator.containsBlanks: {
|
||||
return /^\s*$/.test(v);
|
||||
}
|
||||
case CFTextOperator.notContainsBlanks: {
|
||||
return !/^\s*$/.test(v);
|
||||
}
|
||||
case CFTextOperator.containsErrors: {
|
||||
return (ERROR_TYPE_SET as Set<unknown>).has(v);
|
||||
}
|
||||
case CFTextOperator.notContainsErrors: {
|
||||
return !(ERROR_TYPE_SET as Set<unknown>).has(v);
|
||||
}
|
||||
case CFTextOperator.containsText: {
|
||||
return v.indexOf(condition) > -1;
|
||||
}
|
||||
case CFTextOperator.notContainsText: {
|
||||
return v.indexOf(condition) === -1;
|
||||
}
|
||||
case CFTextOperator.endsWith: {
|
||||
return v.endsWith(condition);
|
||||
}
|
||||
case CFTextOperator.equal: {
|
||||
return v === condition;
|
||||
}
|
||||
case CFTextOperator.notEqual: {
|
||||
return v !== condition;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
case CFSubRuleType.timePeriod: {
|
||||
const value = getCellValue(cellValue);
|
||||
if (isNullable(value) || Number.isNaN(Number(value)) || cellValue?.t !== CellValueType.NUMBER || !preComputingResult) {
|
||||
return;
|
||||
}
|
||||
const v = serialTimeToTimestamp(Number(value), context.workbook.getDateSystem());
|
||||
const { start, end } = preComputingResult.value!;
|
||||
return v >= start && v <= end;
|
||||
}
|
||||
case CFSubRuleType.average: {
|
||||
const value = cellValue && cellValue.v;
|
||||
const v = Number(value);
|
||||
const isNumber = cellValue?.t === CellValueType.NUMBER;
|
||||
const subRuleConfig = ruleConfig as IAverageHighlightCell;
|
||||
|
||||
if (!isNumber) {
|
||||
if (CFNumberOperator.notEqual === subRuleConfig.operator) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNullable(value) || Number.isNaN(v) || !preComputingResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const average = preComputingResult.value;
|
||||
|
||||
switch (subRuleConfig.operator) {
|
||||
case CFNumberOperator.greaterThan: {
|
||||
return v > average;
|
||||
}
|
||||
case CFNumberOperator.greaterThanOrEqual: {
|
||||
return v >= average;
|
||||
}
|
||||
case CFNumberOperator.lessThan: {
|
||||
return v < average;
|
||||
}
|
||||
case CFNumberOperator.lessThanOrEqual: {
|
||||
return v <= average;
|
||||
}
|
||||
case CFNumberOperator.equal: {
|
||||
return isFloatsEqual(v, average);
|
||||
}
|
||||
case CFNumberOperator.notEqual: {
|
||||
return !isFloatsEqual(v, average);
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
case CFSubRuleType.rank: {
|
||||
const value = getCellValue(cellValue);
|
||||
|
||||
const v = Number(value);
|
||||
|
||||
if (isNullable(value) || Number.isNaN(v) || cellValue?.t !== CellValueType.NUMBER || !preComputingResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetValue = preComputingResult.value;
|
||||
const subRuleConfig = ruleConfig as IRankHighlightCell;
|
||||
if (subRuleConfig.isBottom) {
|
||||
return v <= targetValue;
|
||||
} else {
|
||||
return v >= targetValue;
|
||||
}
|
||||
}
|
||||
case CFSubRuleType.uniqueValues: {
|
||||
const value = getCellValue(cellValue);
|
||||
|
||||
if (isNullable(value) || !preComputingResult) {
|
||||
return false;
|
||||
}
|
||||
const uniqueCache = preComputingResult.value;
|
||||
return uniqueCache.get(value) === 1;
|
||||
}
|
||||
case CFSubRuleType.duplicateValues: {
|
||||
const value = getCellValue(cellValue);
|
||||
|
||||
if (isNullable(value) || !preComputingResult) {
|
||||
return false;
|
||||
}
|
||||
const uniqueCacheValue = preComputingResult.value.get(value);
|
||||
|
||||
return uniqueCacheValue && uniqueCacheValue !== 1;
|
||||
}
|
||||
case CFSubRuleType.formula: {
|
||||
const _ruleConfig = ruleConfig as IFormulaHighlightCell;
|
||||
const conditionalFormattingFormulaService = context.accessor.get(ConditionalFormattingFormulaService);
|
||||
|
||||
// The formula engine stores results at relative offsets from the first range's top-left.
|
||||
const firstRange = sortRangesByTopLeft(context.rule.ranges)[0];
|
||||
const relativeRow = row - firstRange.startRow;
|
||||
const relativeCol = col - firstRange.startColumn;
|
||||
|
||||
const formulaResult = conditionalFormattingFormulaService.getFormulaResultWithCoords(
|
||||
context.unitId,
|
||||
context.subUnitId,
|
||||
context.rule.cfId,
|
||||
_ruleConfig.value,
|
||||
relativeRow,
|
||||
relativeCol
|
||||
);
|
||||
|
||||
if (formulaResult.status === FormulaResultStatus.SUCCESS && formulaResult.result !== undefined) {
|
||||
return isFormulaResultMatched(formulaResult.result);
|
||||
}
|
||||
|
||||
const cache = preComputingResult?.value;
|
||||
if (cache) {
|
||||
const value = cache.getValue(relativeRow, relativeCol);
|
||||
return isFormulaResultMatched(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = run();
|
||||
return result ? ruleConfig.style : {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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 { ICellData, Nullable } from '@univerjs/core';
|
||||
import type { CFNumberOperator } from '../../base/const';
|
||||
import type { IIconSet } from '../type';
|
||||
import type { IContext } from './base-calculate-unit';
|
||||
import { CellValueType, ObjectMatrix, Range } from '@univerjs/core';
|
||||
import { FormulaResultStatus } from '@univerjs/engine-formula';
|
||||
import { CFValueType } from '../../base/const';
|
||||
import { BaseCalculateUnit, CalculateEmitStatus } from './base-calculate-unit';
|
||||
import { compareWithNumber, filterRange, getOppositeOperator, getValueByType, isNullable } from './utils';
|
||||
|
||||
;
|
||||
|
||||
const getValue = (row: number, col: number, getCell: (row: number, col: number) => ICellData) => {
|
||||
const cell = getCell(row, col);
|
||||
if (cell && cell.t === CellValueType.NUMBER) {
|
||||
const value = Number(cell.v);
|
||||
return Number.isNaN(value) ? null : value;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
interface IConfigItem {
|
||||
operator: CFNumberOperator;
|
||||
value: number;
|
||||
iconType: string;
|
||||
iconId: string;
|
||||
}
|
||||
export class IconSetCalculateUnit extends BaseCalculateUnit<IConfigItem[]> {
|
||||
override preComputing(_row: number, _col: number, context: IContext): void {
|
||||
const ruleConfig = context.rule.rule as IIconSet;
|
||||
const worksheet = context.worksheet;
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
const isNeedMatrix = !ruleConfig.config.every((e) => e.value.type === CFValueType.num);
|
||||
if (isNeedMatrix) {
|
||||
const ranges = filterRange(context.rule.ranges, worksheet.getMaxRows() - 1, worksheet.getMaxColumns() - 1);
|
||||
ranges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
const cell = context.getCellValue(row, col);
|
||||
const v = cell && cell.v;
|
||||
if (!isNullable(v) && cell?.t === CellValueType.NUMBER) {
|
||||
const _value = Number(v);
|
||||
!Number.isNaN(_value) && matrix.setValue(row, col, _value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
const splitValueResult = ruleConfig.config.map((v) => getValueByType(v.value, matrix, { ...context, cfId: context.rule.cfId }));
|
||||
const isAllFinished = !splitValueResult.some((item) => item.status !== FormulaResultStatus.SUCCESS);
|
||||
if (isAllFinished) {
|
||||
const splitValue = splitValueResult.map((item, index) => ({
|
||||
operator: ruleConfig.config[index].operator,
|
||||
value: Number(item.result) || 0,
|
||||
})).reduce((result, cur, index, list) => {
|
||||
const item = ruleConfig.config[index];
|
||||
if (!index || index === list.length - 1) {
|
||||
result.push({ ...cur, iconId: item.iconId, iconType: item.iconType });
|
||||
} else {
|
||||
const pre = list[index - 1];
|
||||
if (!compareWithNumber(pre, cur.value)) {
|
||||
result.push({ ...cur, iconId: item.iconId, iconType: item.iconType });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [] as { operator: CFNumberOperator; value: number; iconType: string; iconId: string }[]);
|
||||
this.setPreComputingCache(splitValue);
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputingEnd);
|
||||
return;
|
||||
}
|
||||
this._preComputingStatus$.next(CalculateEmitStatus.preComputing);
|
||||
}
|
||||
|
||||
protected override getCellResult(row: number, col: number, preComputingResult: Nullable<IConfigItem[]>, context: IContext) {
|
||||
if (!preComputingResult) {
|
||||
return null;
|
||||
}
|
||||
const value = getValue(row, col, context.getCellValue);
|
||||
|
||||
if (value === null) {
|
||||
return;
|
||||
}
|
||||
const ruleConfig = context.rule.rule as IIconSet;
|
||||
|
||||
const isShowValue = ruleConfig.isShowValue === undefined ? true : !!ruleConfig.isShowValue;
|
||||
|
||||
for (let index = 0; index < preComputingResult.length; index++) {
|
||||
const item = preComputingResult[index];
|
||||
const start = { ...item };
|
||||
const end = { ...item };
|
||||
const { iconId, iconType } = item;
|
||||
if (index === 0) {
|
||||
if (compareWithNumber(item, value)) {
|
||||
return { iconId, iconType, isShowValue };
|
||||
}
|
||||
} else if (index === preComputingResult.length - 1) {
|
||||
return { iconId, iconType, isShowValue };
|
||||
} else {
|
||||
const pre = preComputingResult[index - 1];
|
||||
end.operator = getOppositeOperator(pre.operator);
|
||||
end.value = pre.value;
|
||||
if (compareWithNumber(start, value) && compareWithNumber(end, value)) {
|
||||
return { iconId, iconType, isShowValue };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { CellValue, ICellData, IObjectMatrixPrimitiveType, IRange, Nullable } from '@univerjs/core';
|
||||
import type { IConditionFormattingRule, IValueConfig } from '../type';
|
||||
import type { IContext } from './base-calculate-unit';
|
||||
import { BooleanNumber, CellValueType, ColorKit, DateSystem, excelSerialToDateTime, ObjectMatrix, Range } from '@univerjs/core';
|
||||
import { BooleanValue, FormulaResultStatus } from '@univerjs/engine-formula';
|
||||
import { CFNumberOperator, CFValueType } from '../../base/const';
|
||||
import { ConditionalFormattingFormulaService } from '../../services/conditional-formatting-formula.service';
|
||||
import { ConditionalFormattingViewModel } from '../conditional-formatting-view-model';
|
||||
|
||||
export function isFloatsEqual(a: number, b: number) {
|
||||
return Math.abs(a - b) < Number.EPSILON;
|
||||
}
|
||||
export const isNullable = (v: Nullable<CellValue>) => v === null || v === undefined || (typeof v === 'string' && v.trim() === '');
|
||||
export const getCellValue = (cell?: ICellData) => {
|
||||
if (!cell) {
|
||||
return null;
|
||||
}
|
||||
if (cell.t === CellValueType.BOOLEAN) {
|
||||
return cell.v === BooleanNumber.TRUE ? BooleanValue.TRUE : BooleanValue.FALSE;
|
||||
}
|
||||
const v = cell.v;
|
||||
const dataStream = cell.p?.body?.dataStream.replace(/\r\n$/, '');
|
||||
return !isNullable(v) ? v : !isNullable(dataStream) ? dataStream : null;
|
||||
};
|
||||
export const serialTimeToTimestamp = (value: number, dateSystem = DateSystem.Date1900) => {
|
||||
const utcDate = excelSerialToDateTime(value, dateSystem);
|
||||
if (Number.isNaN(utcDate.getTime())) {
|
||||
return Number.NaN;
|
||||
}
|
||||
// Conditional-formatting date comparisons use local Date timestamps for compatibility with existing predicates.
|
||||
return new Date(
|
||||
utcDate.getUTCFullYear(),
|
||||
utcDate.getUTCMonth(),
|
||||
utcDate.getUTCDate(),
|
||||
utcDate.getUTCHours(),
|
||||
utcDate.getUTCMinutes(),
|
||||
utcDate.getUTCSeconds(),
|
||||
utcDate.getUTCMilliseconds()
|
||||
).getTime();
|
||||
};
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const getValueByType = (value: IValueConfig, matrix: ObjectMatrix<number>, context: IContext & { cfId: string }) => {
|
||||
switch (value.type) {
|
||||
case CFValueType.max: {
|
||||
let max = 0;
|
||||
matrix.forValue((row, col, value) => {
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
});
|
||||
return {
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: max,
|
||||
};
|
||||
}
|
||||
case CFValueType.min: {
|
||||
let min: number | undefined;
|
||||
matrix.forValue((row, col, value) => {
|
||||
if (min === undefined) {
|
||||
min = value;
|
||||
}
|
||||
if (value < min!) {
|
||||
min = value;
|
||||
}
|
||||
});
|
||||
return {
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: min,
|
||||
};
|
||||
}
|
||||
case CFValueType.percent: {
|
||||
let max: number | undefined;
|
||||
let min: number | undefined;
|
||||
matrix.forValue((row, col, value) => {
|
||||
if (max === undefined || min === undefined) {
|
||||
max = value;
|
||||
min = value;
|
||||
}
|
||||
if (value > max!) {
|
||||
max = value;
|
||||
}
|
||||
if (value < min!) {
|
||||
min = value;
|
||||
}
|
||||
});
|
||||
|
||||
const length = (max || 0) - (min || 0);
|
||||
const v = Math.max(Math.min(Number(value.value) || 0, 100), 0);
|
||||
return {
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: length * (v / 100) + (min || 0),
|
||||
};
|
||||
}
|
||||
case CFValueType.percentile: {
|
||||
const list = matrix.toNativeArray().sort((a, b) => a - b);
|
||||
const v = Math.max(Math.min(Number(value.value) || 0, 100), 0);
|
||||
const index = (list.length - 1) * v / 100;
|
||||
const intIndex = Math.floor(index);
|
||||
const decimalIndex = index - intIndex;
|
||||
const result = list[intIndex] + (list[Math.min(intIndex + 1, list.length - 1)] - list[intIndex]) * decimalIndex;
|
||||
return {
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
case CFValueType.formula: {
|
||||
const { accessor, unitId, subUnitId, cfId } = context;
|
||||
const formulaText = String(value.value);
|
||||
const conditionalFormattingFormulaService = accessor.get(ConditionalFormattingFormulaService);
|
||||
conditionalFormattingFormulaService.registerFormulaWithRange(unitId, subUnitId, cfId, formulaText);
|
||||
const result = conditionalFormattingFormulaService.getFormulaResultWithCoords(unitId, subUnitId, cfId, formulaText);
|
||||
return result;
|
||||
}
|
||||
case CFValueType.num:
|
||||
default: {
|
||||
const v = Number(value.value);
|
||||
return {
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: Number.isNaN(v) ? 0 : v,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getCacheStyleMatrix = <S = any>(unitId: string, subUnitId: string, rule: IConditionFormattingRule, context: IContext) => {
|
||||
const { accessor } = context;
|
||||
const conditionalFormattingViewModel = accessor.get(ConditionalFormattingViewModel);
|
||||
const matrix = new ObjectMatrix<S>();
|
||||
rule.ranges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
const cellCfItem = conditionalFormattingViewModel.getCellCfs(unitId, subUnitId, row, col);
|
||||
if (cellCfItem) {
|
||||
const item = cellCfItem.find((item) => item.cfId === rule.cfId);
|
||||
item?.result && matrix.setValue(row, col, item.result);
|
||||
}
|
||||
});
|
||||
});
|
||||
return matrix;
|
||||
};
|
||||
// eslint-disable-next-line complexity
|
||||
export const compareWithNumber = (config: { operator: CFNumberOperator; value: number | [number, number] }, v: number) => {
|
||||
switch (config.operator) {
|
||||
case CFNumberOperator.between: {
|
||||
if (typeof config.value !== 'object' || !(config.value as unknown as Array<number>).length) {
|
||||
return;
|
||||
}
|
||||
const start = Math.min(...config.value as [number, number]);
|
||||
const end = Math.max(...config.value as [number, number]);
|
||||
return v >= start && v <= end;
|
||||
}
|
||||
case CFNumberOperator.notBetween: {
|
||||
if (typeof config.value !== 'object' || !(config.value as unknown as Array<number>).length) {
|
||||
return;
|
||||
}
|
||||
const start = Math.min(...config.value as [number, number]);
|
||||
const end = Math.max(...config.value as [number, number]);
|
||||
return !(v >= start && v <= end);
|
||||
}
|
||||
case CFNumberOperator.equal: {
|
||||
const condition = (config.value || 0) as number;
|
||||
return isFloatsEqual(condition, v);
|
||||
}
|
||||
case CFNumberOperator.notEqual: {
|
||||
const condition = (config.value || 0) as number;
|
||||
return !isFloatsEqual(condition, v);
|
||||
}
|
||||
case CFNumberOperator.greaterThan: {
|
||||
const condition = (config.value || 0) as number;
|
||||
return v > condition;
|
||||
}
|
||||
case CFNumberOperator.greaterThanOrEqual: {
|
||||
const condition = (config.value || 0) as number;
|
||||
return v >= condition;
|
||||
}
|
||||
case CFNumberOperator.lessThan: {
|
||||
const condition = (config.value || 0) as number;
|
||||
return v < condition;
|
||||
}
|
||||
case CFNumberOperator.lessThanOrEqual: {
|
||||
const condition = (config.value || 0) as number;
|
||||
return v <= condition;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
export const getOppositeOperator = (operator: CFNumberOperator) => {
|
||||
switch (operator) {
|
||||
case CFNumberOperator.greaterThan: {
|
||||
return CFNumberOperator.lessThanOrEqual;
|
||||
}
|
||||
case CFNumberOperator.greaterThanOrEqual: {
|
||||
return CFNumberOperator.lessThan;
|
||||
}
|
||||
case CFNumberOperator.lessThan: {
|
||||
return CFNumberOperator.greaterThanOrEqual;
|
||||
}
|
||||
case CFNumberOperator.lessThanOrEqual: {
|
||||
return CFNumberOperator.greaterThan;
|
||||
}
|
||||
}
|
||||
return operator;
|
||||
};
|
||||
|
||||
export const getColorScaleFromValue = (colorList: { color: ColorKit; value: number }[], value: number) => {
|
||||
interface IRgbColor {
|
||||
b: number;
|
||||
g: number;
|
||||
r: number;
|
||||
a?: number;
|
||||
}
|
||||
const prefixRgba = (rgb: IRgbColor): Required<IRgbColor> => {
|
||||
if (rgb.a !== undefined) {
|
||||
return rgb as Required<IRgbColor>;
|
||||
} else {
|
||||
return { ...rgb, a: 1 };
|
||||
}
|
||||
};
|
||||
const index = colorList.findIndex((item) => item.value >= value);
|
||||
const preIndex = index - 1;
|
||||
if (index === 0) {
|
||||
return colorList[0].color.toRgbString();
|
||||
} else if (preIndex >= 0) {
|
||||
const minItem = colorList[preIndex];
|
||||
const maxItem = colorList[index];
|
||||
|
||||
if (minItem.color.isValid && maxItem.color.isValid) {
|
||||
const minRgb = prefixRgba(minItem.color.toRgb());
|
||||
const maxRgb = prefixRgba(maxItem.color.toRgb());
|
||||
const length = maxItem.value - minItem.value;
|
||||
const v = (value - minItem.value) / length;
|
||||
const rgbResult = ['r', 'g', 'b', 'a'].reduce((obj, key) => {
|
||||
const minV = minRgb[key as unknown as keyof IRgbColor];
|
||||
obj[key as unknown as keyof IRgbColor] = (maxRgb[key as unknown as keyof IRgbColor] - minV) * v + minV;
|
||||
return obj;
|
||||
}, {} as IRgbColor);
|
||||
const result = new ColorKit(rgbResult).toRgbString();
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
return colorList[colorList.length - 1].color.toRgbString();
|
||||
}
|
||||
};
|
||||
|
||||
export const filterRange = (ranges: IRange[], maxRow: number, maxCol: number): IRange[] => {
|
||||
return ranges.map((range) => {
|
||||
if (range.startColumn > maxCol || range.startRow > maxRow) {
|
||||
return null as unknown as IRange;
|
||||
}
|
||||
const _range = { ...range };
|
||||
_range.endRow = Math.min(_range.endRow, maxRow);
|
||||
_range.endColumn = Math.min(_range.endColumn, maxCol);
|
||||
return _range;
|
||||
}).filter((range) => !!range);
|
||||
};
|
||||
|
||||
export function getMaxInFormulaResult(result: IObjectMatrixPrimitiveType<Nullable<CellValue>>) {
|
||||
let max = 0;
|
||||
new ObjectMatrix(result).forValue((row, col, value) => {
|
||||
max = Math.max(Number.isNaN(max) ? 0 : max, Number(value));
|
||||
});
|
||||
return max;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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 { Disposable, Inject, RTree } from '@univerjs/core';
|
||||
import { ConditionalFormattingRuleModel } from './conditional-formatting-rule-model';
|
||||
|
||||
export class ConditionalFormattingRangeIndexModel extends Disposable {
|
||||
private _rTreeManager: RTree = new RTree();
|
||||
|
||||
constructor(
|
||||
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel
|
||||
) {
|
||||
super();
|
||||
|
||||
this.rebuild();
|
||||
this._initRuleListener();
|
||||
}
|
||||
|
||||
getRuleIdsByCell(unitId: string, subUnitId: string, row: number, col: number) {
|
||||
return this.getRuleIdsByRanges(unitId, subUnitId, [{
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
}]);
|
||||
}
|
||||
|
||||
getRuleIdsByRanges(unitId: string, subUnitId: string, ranges: IRange[]) {
|
||||
if (!ranges.length) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
const ids = this._rTreeManager.bulkSearch(ranges.map((range) => ({
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
range,
|
||||
})));
|
||||
|
||||
return new Set(Array.from(ids).map((id) => String(id)));
|
||||
}
|
||||
|
||||
getRulesByRanges(unitId: string, subUnitId: string, ranges: IRange[]) {
|
||||
const ids = this.getRuleIdsByRanges(unitId, subUnitId, ranges);
|
||||
const ruleList = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) ?? [];
|
||||
return ruleList.filter((rule) => ids.has(rule.cfId));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._rTreeManager.clear();
|
||||
}
|
||||
|
||||
rebuild() {
|
||||
this.clear();
|
||||
this._conditionalFormattingRuleModel.getAll().forEach((subUnitMap, unitId) => {
|
||||
subUnitMap.forEach((rules, subUnitId) => {
|
||||
rules.forEach((rule) => {
|
||||
this._insert(unitId, subUnitId, rule.cfId, rule.ranges);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _initRuleListener() {
|
||||
this.disposeWithMe(
|
||||
this._conditionalFormattingRuleModel.$ruleChange.subscribe((e) => {
|
||||
const { unitId, subUnitId, rule } = e;
|
||||
const { cfId, ranges } = rule;
|
||||
|
||||
switch (e.type) {
|
||||
case 'add': {
|
||||
this._insert(unitId, subUnitId, cfId, ranges);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
this._remove(unitId, subUnitId, cfId, ranges);
|
||||
break;
|
||||
}
|
||||
case 'set': {
|
||||
const oldRule = e.oldRule!;
|
||||
this._remove(unitId, subUnitId, oldRule.cfId, oldRule.ranges);
|
||||
this._insert(unitId, subUnitId, cfId, ranges);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _insert(unitId: string, subUnitId: string, cfId: string, ranges: IRange[]) {
|
||||
this._rTreeManager.bulkInsert(ranges.map((range) => ({ unitId, sheetId: subUnitId, id: cfId, range })));
|
||||
}
|
||||
|
||||
private _remove(unitId: string, subUnitId: string, cfId: string, ranges: IRange[]) {
|
||||
this._rTreeManager.bulkRemove(ranges.map((range) => ({ unitId, sheetId: subUnitId, id: cfId, range })));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 { IAnchor } from '../utils/anchor';
|
||||
import type { IConditionFormattingRule, IRuleModel } from './type';
|
||||
import { Tools } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { findIndexByAnchor, moveByAnchor } from '../utils/anchor';
|
||||
|
||||
import { createCfId } from '../utils/create-cf-id';
|
||||
|
||||
type RuleOperatorType = 'delete' | 'set' | 'add' | 'sort';
|
||||
|
||||
export class ConditionalFormattingRuleModel {
|
||||
// Map<unitID ,<sheetId ,IConditionFormattingRule[]>>
|
||||
private _model: IRuleModel = new Map();
|
||||
private _ruleChange$ = new Subject<{ rule: IConditionFormattingRule; oldRule?: IConditionFormattingRule; unitId: string; subUnitId: string; type: RuleOperatorType }>();
|
||||
public $ruleChange = this._ruleChange$.asObservable();
|
||||
|
||||
private _ensureList(unitId: string, subUnitId: string) {
|
||||
let list = this.getSubunitRules(unitId, subUnitId);
|
||||
if (!list) {
|
||||
list = [];
|
||||
let unitMap = this._model.get(unitId);
|
||||
if (!unitMap) {
|
||||
unitMap = new Map<string, IConditionFormattingRule[]>();
|
||||
this._model.set(unitId, unitMap);
|
||||
}
|
||||
unitMap.set(subUnitId, list);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
getRule(unitId: string, subUnitId: string, cfId?: string) {
|
||||
const list = this.getSubunitRules(unitId, subUnitId);
|
||||
if (list) {
|
||||
return list.find((item) => item.cfId === cfId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getUnitRules(unitId: string) {
|
||||
const map = this._model.get(unitId);
|
||||
return map || null;
|
||||
}
|
||||
|
||||
getSubunitRules(unitId: string, subUnitId: string) {
|
||||
const list = this._model.get(unitId)?.get(subUnitId);
|
||||
return list || null;
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
deleteRule(unitId: string, subUnitId: string, cfId: string) {
|
||||
const list = this.getSubunitRules(unitId, subUnitId);
|
||||
if (list) {
|
||||
const index = list.findIndex((e) => e.cfId === cfId);
|
||||
const rule = list[index];
|
||||
if (rule) {
|
||||
list.splice(index, 1);
|
||||
this._ruleChange$.next({ rule, subUnitId, unitId, type: 'delete' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRule(unitId: string, subUnitId: string, rule: IConditionFormattingRule, oldCfId: string) {
|
||||
const list = this._ensureList(unitId, subUnitId);
|
||||
const oldRule = list.find((item) => item.cfId === oldCfId);
|
||||
if (oldRule) {
|
||||
const cloneRule = Tools.deepClone(oldRule);
|
||||
Object.assign(oldRule, rule);
|
||||
this._ruleChange$.next({ rule: oldRule, subUnitId, unitId, type: 'set', oldRule: cloneRule });
|
||||
}
|
||||
}
|
||||
|
||||
addRule(unitId: string, subUnitId: string, rule: IConditionFormattingRule) {
|
||||
const list = this._ensureList(unitId, subUnitId);
|
||||
const item = list.find((item) => item.cfId === rule.cfId);
|
||||
if (!item) {
|
||||
// The new conditional formatting has a higher priority
|
||||
list.unshift(rule);
|
||||
}
|
||||
this._ruleChange$.next({ rule, subUnitId, unitId, type: 'add' });
|
||||
}
|
||||
|
||||
/**
|
||||
* example [1,2,3,4,5,6],if you move behind 5 to 2, then cfId=5,targetId=2.
|
||||
* if targetId does not exist, it defaults to top
|
||||
*/
|
||||
moveRulePriority(unitId: string, subUnitId: string, start: IAnchor, end: IAnchor) {
|
||||
const list = this._ensureList(unitId, subUnitId);
|
||||
const curIndex = findIndexByAnchor(start, list, (rule) => rule.cfId);
|
||||
const targetCfIndex = findIndexByAnchor(end, list, (rule) => rule.cfId);
|
||||
if (targetCfIndex === null || curIndex === null || targetCfIndex === curIndex) {
|
||||
return;
|
||||
}
|
||||
const rule = list[curIndex];
|
||||
if (rule) {
|
||||
moveByAnchor(start, end, list, (rule) => rule.cfId);
|
||||
this._ruleChange$.next({ rule, subUnitId, unitId, type: 'sort' });
|
||||
}
|
||||
}
|
||||
|
||||
createCfId(_unitId: string, _subUnitId: string) {
|
||||
return createCfId();
|
||||
}
|
||||
|
||||
deleteUnitId(unitId: string) {
|
||||
this._model.delete(unitId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* 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 { BaseCalculateUnit, IContext } from './calculate-unit-v2/base-calculate-unit';
|
||||
import type { IConditionFormattingRule } from './type';
|
||||
import { Disposable, Inject, Injector, IUniverInstanceService, LRUMap } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { bufferTime, filter, map } from 'rxjs/operators';
|
||||
import { CFRuleType, CFSubRuleType } from '../base/const';
|
||||
import { ConditionalFormattingFormulaService } from '../services/conditional-formatting-formula.service';
|
||||
import { ColorScaleCalculateUnit } from './calculate-unit-v2/color-scale-calculate-unit';
|
||||
import { DataBarCalculateUnit } from './calculate-unit-v2/data-bar-calculate-unit';
|
||||
import { HighlightCellCalculateUnit } from './calculate-unit-v2/highlight-cell-calculate-unit';
|
||||
import { IconSetCalculateUnit } from './calculate-unit-v2/icon-set-calculate-unit';
|
||||
import { ConditionalFormattingRangeIndexModel } from './conditional-formatting-range-index-model';
|
||||
import { ConditionalFormattingRuleModel } from './conditional-formatting-rule-model';
|
||||
|
||||
// The default is a 50-row,20-column viewable area.
|
||||
export const CONDITIONAL_FORMATTING_VIEWPORT_CACHE_LENGTH = 50 * 20 * 3 * 3;
|
||||
export class ConditionalFormattingViewModel extends Disposable {
|
||||
// Map<unitID ,<sheetId ,ObjectMatrix>>
|
||||
private _calculateUnitManagers: Map<string, Map<string, Map<string, BaseCalculateUnit>>> = new Map();
|
||||
|
||||
/**
|
||||
* 1nd-level cache
|
||||
*/
|
||||
private _cellCache = new LRUMap<string, { cfId: string; result: any; priority: number }[]>(CONDITIONAL_FORMATTING_VIEWPORT_CACHE_LENGTH);
|
||||
|
||||
private _markDirty$ = new Subject<{ cfId: string; unitId: string; subUnitId: string; isImmediately?: boolean }>();
|
||||
private _cacheClear$ = new Subject<void>();
|
||||
/**
|
||||
* The rendering layer listens to this variable to determine whether a reRender is necessary.
|
||||
* @memberof ConditionalFormattingViewModel
|
||||
*/
|
||||
public markDirty$ = this._markDirty$.asObservable();
|
||||
public cacheClear$ = this._cacheClear$.asObservable();
|
||||
constructor(
|
||||
@Inject(Injector) private _injector: Injector,
|
||||
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
|
||||
@Inject(ConditionalFormattingRangeIndexModel) private _conditionalFormattingRangeIndexModel: ConditionalFormattingRangeIndexModel,
|
||||
@Inject(ConditionalFormattingFormulaService) private _conditionalFormattingFormulaService: ConditionalFormattingFormulaService,
|
||||
@IUniverInstanceService private _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
this._initRuleListener();
|
||||
this._handleCustomFormulasSeparately();
|
||||
this._initCFFormulaListener();
|
||||
}
|
||||
|
||||
private _initCFFormulaListener() {
|
||||
// If a conditional format depends on multiple formula results,removing the 2nd level cache when a single result is returned can cause screen flickering.
|
||||
// It is necessary to wait until all results are ready before removing the cache.
|
||||
this.disposeWithMe(
|
||||
this._conditionalFormattingFormulaService.result$.subscribe(({ unitId, subUnitId, cfId, isAllFinished }) => {
|
||||
if (isAllFinished) {
|
||||
this._markRuleDirtyAtOnce(unitId, subUnitId, cfId, isAllFinished);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public getCellCfs(unitId: string, subUnitId: string, row: number, col: number) {
|
||||
const key = this._createCacheKey(unitId, subUnitId, row, col);
|
||||
if (this._cellCache.has(key)) {
|
||||
return this._cellCache.get(key);
|
||||
}
|
||||
const result = this._getCellCfs(unitId, subUnitId, row, col);
|
||||
if (result.length) {
|
||||
this._cellCache.set(key, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _getCellCfs(unitId: string, subUnitId: string, row: number, col: number) {
|
||||
const subunitRules = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) ?? [];
|
||||
const _calculateUnitManagers = this._ensureCalculateUnitManager(unitId, subUnitId);
|
||||
const list = this._conditionalFormattingRangeIndexModel.getRuleIdsByCell(unitId, subUnitId, row, col);
|
||||
const rules = subunitRules.filter((rule) => list.has(rule.cfId));
|
||||
if (!rules.length) {
|
||||
return [];
|
||||
}
|
||||
const result = rules.map((rule) => {
|
||||
const calculateUnit = _calculateUnitManagers.get(rule.cfId);
|
||||
if (!calculateUnit) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
cfId: rule.cfId,
|
||||
result: calculateUnit.getCell(row, col),
|
||||
};
|
||||
}).filter((e) => !!e).map((e, index) => ({ ...e, priority: index }));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
`isNeedResetPreComputingCache` indicates whether it is necessary to remove the 2nd-level cache for each rule individually.
|
||||
Generally, when the logic of a rule calculation is modified, the cache for that rule needs to be removed.
|
||||
Changes in style/priority do not require the clearing of the 2nd-level cache.
|
||||
Rule changes/region changes require the removal of the 2nd-level cache.
|
||||
There is also a situation where preComputing is asynchronously calculated.
|
||||
After the calculation is finished, it is only for marking as dirty, and the 2nd-level cache need to be cleared.
|
||||
* @param {boolean} [isNeedResetPreComputingCache]
|
||||
*/
|
||||
private _markRuleDirtyAtOnce = (unitId: string, subUnitId: string, cfId: string, isNeedResetPreComputingCache: boolean = true) => {
|
||||
this._cellCache.clear();
|
||||
if (isNeedResetPreComputingCache) {
|
||||
const _calculateUnitManagers = this._ensureCalculateUnitManager(unitId, subUnitId);
|
||||
const calculateUnit = _calculateUnitManagers.get(cfId);
|
||||
if (calculateUnit) {
|
||||
calculateUnit.resetPreComputingCache();
|
||||
}
|
||||
}
|
||||
|
||||
this._markDirty$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
cfId,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* For the same condition format being marked dirty multiple times at the same time,
|
||||
* it will cause the style cache to be cleared, thereby causing the screen to flicker.
|
||||
* Here,multiple dirties are merged into one..
|
||||
*/
|
||||
public markRuleDirty = (() => {
|
||||
const rxItem = new Subject<{ unitId: string; subUnitId: string; cfId: string; isNeedResetPreComputingCache: boolean }>();
|
||||
this.disposeWithMe(rxItem.pipe(bufferTime(100), filter((e) => !!e.length), map((list) => {
|
||||
const set = new Set();
|
||||
const result: typeof list = [];
|
||||
list.forEach((item) => {
|
||||
const kye = `${item.unitId}_${item.subUnitId}_${item.cfId}`;
|
||||
if (set.has(kye)) {
|
||||
if (item.isNeedResetPreComputingCache) {
|
||||
const i = result.find((e) => e.cfId === item.cfId)!;
|
||||
i.isNeedResetPreComputingCache = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
set.add(kye);
|
||||
result.push({ ...item });
|
||||
});
|
||||
return result;
|
||||
})).subscribe((list) => {
|
||||
list.forEach((item) => {
|
||||
this._markRuleDirtyAtOnce(item.unitId, item.subUnitId, item.cfId, item.isNeedResetPreComputingCache);
|
||||
});
|
||||
}));
|
||||
return (unitId: string, subUnitId: string, cfId: string, isNeedResetPreComputingCache: boolean = true) => {
|
||||
rxItem.next({ unitId, subUnitId, cfId, isNeedResetPreComputingCache });
|
||||
};
|
||||
})();
|
||||
|
||||
override dispose(): void {
|
||||
this.clearCache();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this._calculateUnitManagers.clear();
|
||||
this._cellCache.clear();
|
||||
this._cacheClear$.next();
|
||||
}
|
||||
|
||||
private _handleCustomFormulasSeparately() {
|
||||
this.disposeWithMe(
|
||||
this._conditionalFormattingRuleModel.$ruleChange.subscribe((e) => {
|
||||
if (e.type === 'set') {
|
||||
const { unitId, subUnitId } = e;
|
||||
const oldRule = e.oldRule!;
|
||||
// If the range of a custom formula is modified,then the cached formulas must be cleared and recalculated.
|
||||
if (oldRule.rule.type === CFRuleType.highlightCell && oldRule.rule.subType === CFSubRuleType.formula) {
|
||||
this._conditionalFormattingFormulaService.deleteCache(unitId, subUnitId, oldRule.cfId);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initRuleListener() {
|
||||
this.disposeWithMe(
|
||||
this._conditionalFormattingRuleModel.$ruleChange.subscribe((e) => {
|
||||
const { unitId, subUnitId, rule } = e;
|
||||
const { cfId, ranges } = rule;
|
||||
const calculateUnitManager = this._ensureCalculateUnitManager(unitId, subUnitId);
|
||||
this.markRuleDirty(unitId, subUnitId, cfId);
|
||||
switch (e.type) {
|
||||
case 'add': {
|
||||
const instance = this._createRuleCalculateUnitInstance(unitId, subUnitId, rule);
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
calculateUnitManager.set(rule.cfId, instance);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
calculateUnitManager.delete(rule.cfId);
|
||||
break;
|
||||
}
|
||||
case 'set': {
|
||||
const oldRule = e.oldRule!;
|
||||
|
||||
if (oldRule.rule.type !== rule.rule.type) {
|
||||
const instance = this._createRuleCalculateUnitInstance(unitId, subUnitId, rule);
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
calculateUnitManager.delete(oldRule.cfId);
|
||||
calculateUnitManager.set(rule.cfId, instance);
|
||||
} else {
|
||||
const instance = calculateUnitManager.get(oldRule.cfId);
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
instance.updateRule(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _ensureCalculateUnitManager(unitId: string, subUnitId: string) {
|
||||
let unitMap = this._calculateUnitManagers.get(unitId);
|
||||
if (!unitMap) {
|
||||
unitMap = new Map();
|
||||
this._calculateUnitManagers.set(unitId, unitMap);
|
||||
}
|
||||
let subUnitMap = unitMap.get(subUnitId);
|
||||
if (!subUnitMap) {
|
||||
subUnitMap = new Map();
|
||||
unitMap.set(subUnitId, subUnitMap);
|
||||
}
|
||||
return subUnitMap;
|
||||
}
|
||||
|
||||
private _createRuleCalculateUnitInstance(unitId: string, subUnitId: string, rule: IConditionFormattingRule) {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!workbook || !worksheet) {
|
||||
return;
|
||||
}
|
||||
const context: IContext = {
|
||||
workbook,
|
||||
worksheet,
|
||||
unitId,
|
||||
subUnitId,
|
||||
accessor: this._injector,
|
||||
rule,
|
||||
limit: CONDITIONAL_FORMATTING_VIEWPORT_CACHE_LENGTH,
|
||||
getCellValue: (row: number, col: number) => worksheet.getCellRaw(row, col) || {},
|
||||
};
|
||||
switch (rule.rule.type) {
|
||||
case CFRuleType.colorScale: {
|
||||
return new ColorScaleCalculateUnit(context);
|
||||
}
|
||||
case CFRuleType.dataBar: {
|
||||
return new DataBarCalculateUnit(context);
|
||||
}
|
||||
case CFRuleType.highlightCell: {
|
||||
return new HighlightCellCalculateUnit(context);
|
||||
}
|
||||
case CFRuleType.iconSet: {
|
||||
return new IconSetCalculateUnit(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _createCacheKey(unitId: string, subUnitId: string, row: number, col: number) {
|
||||
return `${unitId}_${subUnitId}_${row}_${col}`;
|
||||
}
|
||||
|
||||
public setCacheLength(length: number = CONDITIONAL_FORMATTING_VIEWPORT_CACHE_LENGTH) {
|
||||
if (this._cellCache.limit === length) {
|
||||
return;
|
||||
}
|
||||
this._cellCache.limit = length;
|
||||
this._calculateUnitManagers.forEach((subunitMap) => {
|
||||
subunitMap.forEach((unitMap) => {
|
||||
unitMap.forEach((unit) => {
|
||||
unit.setCacheLength(length);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
164
packages/sheets-conditional-formatting/src/models/icon-map.ts
Normal file
164
packages/sheets-conditional-formatting/src/models/icon-map.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { ICON_MAP } from '../assets/icon-map';
|
||||
|
||||
export enum IIconSetType {
|
||||
threeArrows = '3Arrows',
|
||||
threeArrowsGray = '3ArrowsGray',
|
||||
fourArrows = '4Arrows',
|
||||
fourArrowsGray = '4ArrowsGray',
|
||||
fiveArrows = '5Arrows',
|
||||
fiveArrowsGray = '5ArrowsGray',
|
||||
threeTriangles = '3Triangles',
|
||||
threeTrafficLights1 = '3TrafficLights1',
|
||||
threeSigns = '3Signs',
|
||||
threeTrafficLights2 = '3TrafficLights2',
|
||||
fourRedToBlack = '4RedToBlack',
|
||||
fourTrafficLights = '4TrafficLights',
|
||||
threeSymbols = '3Symbols',
|
||||
threeSymbols2 = '3Symbols2',
|
||||
threeFlags = '3Flags',
|
||||
fourRating = '4Rating',
|
||||
fiveRating = '5Rating',
|
||||
fiveQuarters = '5Quarters',
|
||||
fiveFelling = '_5Felling',
|
||||
fiveBoxes = '5Boxes',
|
||||
threeStars = '3Stars',
|
||||
empty = 'EMPTY_ICON_TYPE',
|
||||
}
|
||||
|
||||
export const iconGroup: { title: LocaleKey; group: { name: IIconSetType; list: string[] }[] }[] = [
|
||||
{
|
||||
title: 'sheets-conditional-formatting.iconSet.direction',
|
||||
group: [
|
||||
{
|
||||
name: IIconSetType.threeArrows,
|
||||
list: [ICON_MAP.arrow['up-green'], ICON_MAP.arrow['right-gold'], ICON_MAP.arrow['down-red']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.threeArrowsGray,
|
||||
list: [ICON_MAP.arrow['up-gray'], ICON_MAP.arrow['right-gray'], ICON_MAP.arrow['down-gray']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fourArrows,
|
||||
list: [ICON_MAP.arrow['up-green'], ICON_MAP.arrow['rightAndUp-gold'], ICON_MAP.arrow['rightAndDown-gold'], ICON_MAP.arrow['down-red']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fourArrowsGray,
|
||||
list: [ICON_MAP.arrow['up-gray'], ICON_MAP.arrow['rightAndUp-gray'], ICON_MAP.arrow['rightAndDown-gray'], ICON_MAP.arrow['down-gray']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fiveArrows,
|
||||
list: [ICON_MAP.arrow['up-green'], ICON_MAP.arrow['rightAndUp-gold'], ICON_MAP.arrow['right-gold'], ICON_MAP.arrow['rightAndDown-gold'], ICON_MAP.arrow['down-red']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fiveArrowsGray,
|
||||
list: [ICON_MAP.arrow['up-gray'], ICON_MAP.arrow['rightAndUp-gray'], ICON_MAP.arrow['right-gray'], ICON_MAP.arrow['rightAndDown-gray'], ICON_MAP.arrow['down-gray']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.threeTriangles,
|
||||
list: [ICON_MAP.shape.up, ICON_MAP.shape.cross, ICON_MAP.shape.down],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'sheets-conditional-formatting.iconSet.shape',
|
||||
group: [
|
||||
{
|
||||
name: IIconSetType.threeTrafficLights1,
|
||||
list: [ICON_MAP.shape['roundness-greed'], ICON_MAP.shape['roundness-gold'], ICON_MAP.shape['roundness-red']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.threeSigns,
|
||||
list: [ICON_MAP.shape['roundness-greed'], ICON_MAP.shape['triangle-gold'], ICON_MAP.shape['rhomboid-red']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.threeTrafficLights2,
|
||||
list: [ICON_MAP.shape['indicate-greed'], ICON_MAP.shape['indicate-gold'], ICON_MAP.shape['indicate-red']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fourRedToBlack,
|
||||
list: [ICON_MAP.shape['roundness-red'], ICON_MAP.shape['roundness-pink'], ICON_MAP.shape['roundness-gray'], ICON_MAP.shape['roundness-black']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fourTrafficLights,
|
||||
list: [ICON_MAP.shape['roundness-greed'], ICON_MAP.shape['roundness-gold'], ICON_MAP.shape['roundness-red'], ICON_MAP.shape['roundness-black']],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'sheets-conditional-formatting.iconSet.mark',
|
||||
group: [
|
||||
{
|
||||
name: IIconSetType.threeSymbols,
|
||||
list: [ICON_MAP.feedback.correct, ICON_MAP.feedback.warn, ICON_MAP.feedback.mistake],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.threeSymbols2,
|
||||
list: [ICON_MAP.feedback2.correct2, ICON_MAP.feedback2.warn2, ICON_MAP.feedback2.mistake2],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.threeFlags,
|
||||
list: [ICON_MAP.flag['flag-green'], ICON_MAP.flag['flag-gold'], ICON_MAP.flag['flag-red']],
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'sheets-conditional-formatting.iconSet.rank',
|
||||
group: [
|
||||
{
|
||||
name: IIconSetType.fourRating,
|
||||
list: [ICON_MAP.signal.signal25, ICON_MAP.signal.signal50, ICON_MAP.signal.signal75, ICON_MAP.signal.signal100],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fiveRating,
|
||||
list: [ICON_MAP.signal.signal0, ICON_MAP.signal.signal25, ICON_MAP.signal.signal50, ICON_MAP.signal.signal75, ICON_MAP.signal.signal100],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fiveQuarters,
|
||||
list: [ICON_MAP.progress.progress100, ICON_MAP.progress.progress75, ICON_MAP.progress.progress50, ICON_MAP.progress.progress25, ICON_MAP.progress.progress0],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fiveFelling,
|
||||
list: [ICON_MAP.feeling.guffaw, ICON_MAP.feeling.smile, ICON_MAP.feeling.noninductive, ICON_MAP.feeling.dissatisfied, ICON_MAP.feeling.impatient],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.fiveBoxes,
|
||||
list: [ICON_MAP.cell['cell-100'], ICON_MAP.cell['cell-75'], ICON_MAP.cell['cell-50'], ICON_MAP.cell['cell-25'], ICON_MAP.cell['cell-0']],
|
||||
},
|
||||
{
|
||||
name: IIconSetType.threeStars,
|
||||
list: [ICON_MAP.star.starFull, ICON_MAP.star.starIncomplete, ICON_MAP.star.starEmpty],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const iconMap = iconGroup.reduce((result, cur) => {
|
||||
const { group } = cur;
|
||||
for (const v of group) {
|
||||
result[v.name] = v.list;
|
||||
}
|
||||
return result;
|
||||
}, {} as Record<IIconSetType, string[]>);
|
||||
|
||||
for (const key in iconMap) {
|
||||
const v = iconMap[key as IIconSetType];
|
||||
Object.freeze(v);
|
||||
}
|
||||
112
packages/sheets-conditional-formatting/src/models/type.ts
Normal file
112
packages/sheets-conditional-formatting/src/models/type.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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, IStyleBase } from '@univerjs/core';
|
||||
import type { CFNumberOperator, CFRuleType, CFSubRuleType, CFTextOperator, CFTimePeriodOperator, CFValueType } from '../base/const';
|
||||
import type { IIconSetType } from './icon-map';
|
||||
|
||||
export interface IBaseCfRule {
|
||||
type: string;
|
||||
}
|
||||
export interface IHighlightCell extends IBaseCfRule {
|
||||
style: IStyleBase;
|
||||
type: CFRuleType.highlightCell; // cellIs
|
||||
subType: CFSubRuleType;
|
||||
}
|
||||
|
||||
export interface IValueConfig {
|
||||
type: CFValueType;
|
||||
value?: number | string;
|
||||
}
|
||||
|
||||
export interface IUniqueValuesHighlightCell extends IHighlightCell {
|
||||
subType: CFSubRuleType.uniqueValues;
|
||||
}
|
||||
export interface IDuplicateValuesHighlightCell extends IHighlightCell {
|
||||
subType: CFSubRuleType.duplicateValues;
|
||||
}
|
||||
|
||||
export interface IRankHighlightCell extends IHighlightCell {
|
||||
subType: CFSubRuleType.rank; // top10
|
||||
isBottom: boolean;
|
||||
isPercent: boolean;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface ITextHighlightCell extends IHighlightCell {
|
||||
subType: CFSubRuleType.text;
|
||||
operator: CFTextOperator;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface ITimePeriodHighlightCell extends IHighlightCell {
|
||||
subType: CFSubRuleType.timePeriod;
|
||||
operator: CFTimePeriodOperator;
|
||||
}
|
||||
|
||||
export interface IFormulaHighlightCell extends IHighlightCell {
|
||||
subType: CFSubRuleType.formula;
|
||||
value: string;
|
||||
}
|
||||
export type INumberHighlightCell = ({
|
||||
subType: CFSubRuleType.number;
|
||||
operator: CFNumberOperator.between | CFNumberOperator.notBetween;
|
||||
value: [number, number];
|
||||
} & IHighlightCell) | ({
|
||||
subType: CFSubRuleType.number;
|
||||
operator: CFNumberOperator.equal | CFNumberOperator.notEqual | CFNumberOperator.greaterThan | CFNumberOperator.greaterThanOrEqual | CFNumberOperator.lessThanOrEqual | CFNumberOperator.lessThan;
|
||||
value?: number;
|
||||
} & IHighlightCell);
|
||||
|
||||
export interface IAverageHighlightCell extends IHighlightCell {
|
||||
subType: CFSubRuleType.average;
|
||||
operator: CFNumberOperator.greaterThan | CFNumberOperator.greaterThanOrEqual | CFNumberOperator.lessThan | CFNumberOperator.lessThanOrEqual | CFNumberOperator.equal | CFNumberOperator.notEqual;
|
||||
}
|
||||
|
||||
export interface IDataBar extends IBaseCfRule {
|
||||
type: CFRuleType.dataBar;
|
||||
isShowValue: boolean;
|
||||
config: {
|
||||
min: IValueConfig;
|
||||
max: IValueConfig;
|
||||
isGradient: boolean;
|
||||
positiveColor: string;
|
||||
nativeColor: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IColorScale extends IBaseCfRule {
|
||||
type: CFRuleType.colorScale;
|
||||
config: { index: number; color: string; value: IValueConfig }[];
|
||||
}
|
||||
|
||||
export interface IIconSet extends IBaseCfRule {
|
||||
type: CFRuleType.iconSet;
|
||||
isShowValue: boolean;
|
||||
config: { operator: CFNumberOperator; value: IValueConfig; iconType: IIconSetType; iconId: string }[];
|
||||
}
|
||||
|
||||
export type IConditionalFormattingRuleConfig = IColorScale | IDataBar | IUniqueValuesHighlightCell |
|
||||
IDuplicateValuesHighlightCell | IRankHighlightCell | ITextHighlightCell |
|
||||
ITimePeriodHighlightCell | INumberHighlightCell | IAverageHighlightCell | IFormulaHighlightCell | IIconSet;
|
||||
export interface IConditionFormattingRule<C = IConditionalFormattingRuleConfig> {
|
||||
ranges: IRange [];
|
||||
cfId: string;
|
||||
stopIfTrue: boolean;
|
||||
rule: C;
|
||||
}
|
||||
export type IRuleModel = Map<string, Map<string, IConditionFormattingRule[]>>;
|
||||
export type IRuleModelJson = Record<string, Record<string, IConditionFormattingRule[]>>;
|
||||
105
packages/sheets-conditional-formatting/src/plugin.ts
Normal file
105
packages/sheets-conditional-formatting/src/plugin.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IUniverSheetsConditionalFormattingConfig } from './config/config';
|
||||
import { DependentOn, ICommandService, IConfigService, Inject, Injector, merge, Plugin, touchDependencies, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import pkg from '../package.json';
|
||||
import { SHEET_CONDITIONAL_FORMATTING_PLUGIN } from './base/const';
|
||||
import { AddCfCommand } from './commands/commands/add-cf.command';
|
||||
import { ClearRangeCfCommand } from './commands/commands/clear-range-cf.command';
|
||||
import { ClearWorksheetCfCommand } from './commands/commands/clear-worksheet-cf.command';
|
||||
import { DeleteCfCommand } from './commands/commands/delete-cf.command';
|
||||
import { MoveCfCommand } from './commands/commands/move-cf.command';
|
||||
import { SetCfCommand } from './commands/commands/set-cf.command';
|
||||
import { AddConditionalRuleMutation } from './commands/mutations/add-conditional-rule.mutation';
|
||||
import { DeleteConditionalRuleMutation } from './commands/mutations/delete-conditional-rule.mutation';
|
||||
import { MoveConditionalRuleMutation } from './commands/mutations/move-conditional-rule.mutation';
|
||||
import { SetConditionalRuleMutation } from './commands/mutations/set-conditional-rule.mutation';
|
||||
import {
|
||||
defaultPluginConfig,
|
||||
SHEETS_CONDITIONAL_FORMATTING_PLUGIN_CONFIG_KEY,
|
||||
} from './config/config';
|
||||
import { ConditionalFormattingRangeIndexModel } from './models/conditional-formatting-range-index-model';
|
||||
import { ConditionalFormattingRuleModel } from './models/conditional-formatting-rule-model';
|
||||
import { ConditionalFormattingViewModel } from './models/conditional-formatting-view-model';
|
||||
import { ConditionalFormattingFormulaService } from './services/conditional-formatting-formula.service';
|
||||
import { ConditionalFormattingRangeTransformService } from './services/conditional-formatting-range-transform.service';
|
||||
import { ConditionalFormattingStyleComposer } from './services/conditional-formatting-style-composer.service';
|
||||
import { ConditionalFormattingService } from './services/conditional-formatting.service';
|
||||
|
||||
@DependentOn(UniverFormulaEnginePlugin, UniverSheetsPlugin)
|
||||
export class UniverSheetsConditionalFormattingPlugin extends Plugin {
|
||||
static override pluginName = SHEET_CONDITIONAL_FORMATTING_PLUGIN;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsConditionalFormattingConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(SHEETS_CONDITIONAL_FORMATTING_PLUGIN_CONFIG_KEY, rest);
|
||||
|
||||
([
|
||||
[ConditionalFormattingService],
|
||||
[ConditionalFormattingFormulaService],
|
||||
[ConditionalFormattingRangeTransformService],
|
||||
[ConditionalFormattingStyleComposer],
|
||||
[ConditionalFormattingRuleModel],
|
||||
[ConditionalFormattingRangeIndexModel],
|
||||
[ConditionalFormattingViewModel],
|
||||
] as Dependency[]).forEach((dependency) => {
|
||||
this._injector.add(dependency);
|
||||
});
|
||||
|
||||
[
|
||||
AddCfCommand,
|
||||
ClearRangeCfCommand,
|
||||
ClearWorksheetCfCommand,
|
||||
DeleteCfCommand,
|
||||
MoveCfCommand,
|
||||
SetCfCommand,
|
||||
AddConditionalRuleMutation,
|
||||
DeleteConditionalRuleMutation,
|
||||
SetConditionalRuleMutation,
|
||||
MoveConditionalRuleMutation,
|
||||
].forEach((m) => {
|
||||
this._commandService.registerCommand(m);
|
||||
});
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.get(ConditionalFormattingService);
|
||||
touchDependencies(this._injector, [
|
||||
[ConditionalFormattingService],
|
||||
[ConditionalFormattingRangeIndexModel],
|
||||
[ConditionalFormattingViewModel],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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 { describe, expect, it, vi } from 'vitest';
|
||||
import { IIconSetType } from '../../models/icon-map';
|
||||
import { DataBar } from '../data-bar.render';
|
||||
import { ConditionalFormattingIcon } from '../icon.render';
|
||||
|
||||
function createCtx() {
|
||||
const gradient = { addColorStop: vi.fn() };
|
||||
return {
|
||||
save: vi.fn(),
|
||||
restore: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
arcTo: vi.fn(),
|
||||
closePath: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
drawImage: vi.fn(),
|
||||
createLinearGradient: vi.fn(() => gradient),
|
||||
fillStyle: '',
|
||||
strokeStyle: '',
|
||||
lineWidth: 0,
|
||||
gradient,
|
||||
} as any;
|
||||
}
|
||||
|
||||
function createCellBounds(row: number, col: number, overrides?: Record<string, unknown>) {
|
||||
const startX = col * 80;
|
||||
const startY = row * 24;
|
||||
return {
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
startX,
|
||||
endX: startX + 80,
|
||||
startY,
|
||||
endY: startY + 24,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
mergeInfo: {
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
startX,
|
||||
endX: startX + 80,
|
||||
startY,
|
||||
endY: startY + 24,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createSkeleton(cells: Record<string, unknown>, hiddenRows = new Set<number>(), hiddenCols = new Set<number>()) {
|
||||
return {
|
||||
rowColumnSegment: { startRow: 0, endRow: 2, startColumn: 0, endColumn: 2 },
|
||||
worksheet: {
|
||||
getUnitId: () => 'unit-1',
|
||||
getSheetId: () => 'sheet-1',
|
||||
getMergeData: () => [],
|
||||
getRowVisible: (row: number) => !hiddenRows.has(row),
|
||||
getColVisible: (col: number) => !hiddenCols.has(col),
|
||||
getCell: vi.fn((row: number, col: number) => cells[`${row},${col}`]),
|
||||
},
|
||||
getCellWithCoordByIndex: (row: number, col: number) => createCellBounds(row, col),
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('conditional formatting render extensions', () => {
|
||||
it('draws data bars for positive and negative cell values in visible cells', () => {
|
||||
const ctx = createCtx();
|
||||
const skeleton = createSkeleton({
|
||||
'0,0': {
|
||||
dataBar: {
|
||||
color: '#2f56ef',
|
||||
value: 75,
|
||||
startPoint: 25,
|
||||
isGradient: false,
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
'0,1': {
|
||||
dataBar: {
|
||||
color: '#f45531',
|
||||
value: -50,
|
||||
startPoint: 60,
|
||||
isGradient: true,
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
'1,0': {
|
||||
dataBar: {
|
||||
color: '#111111',
|
||||
value: 100,
|
||||
startPoint: 0,
|
||||
isGradient: false,
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
}, new Set([1]));
|
||||
|
||||
new DataBar().draw(ctx, { scaleX: 1, scaleY: 1 }, skeleton, [
|
||||
{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
|
||||
]);
|
||||
|
||||
expect(ctx.save).toHaveBeenCalled();
|
||||
expect(ctx.fill).toHaveBeenCalledTimes(2);
|
||||
expect(ctx.createLinearGradient).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.gradient.addColorStop).toHaveBeenCalledWith(0, 'rgb(255 255 255)');
|
||||
expect(ctx.gradient.addColorStop).toHaveBeenCalledWith(1, '#f45531');
|
||||
expect(ctx.stroke).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.restore).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips data bar drawing when worksheet is missing or bar dimensions collapse', () => {
|
||||
const ctx = createCtx();
|
||||
const dataBar = new DataBar();
|
||||
expect(dataBar.draw(ctx, { scaleX: 1, scaleY: 1 }, { worksheet: undefined } as any, [])).toBe(false);
|
||||
|
||||
(dataBar as any)._drawRectWithRoundedCorner(ctx, 0, 0, 0, 10, true, true, true, true);
|
||||
(dataBar as any)._drawRectWithRoundedCorner(ctx, 0, 0, 10, 0, true, true, true, true);
|
||||
expect(ctx.beginPath).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips data bar cell visits only when a configured rule-range resolver confirms no matching rule', () => {
|
||||
const ctx = createCtx();
|
||||
const skeleton = createSkeleton({
|
||||
'0,0': {
|
||||
dataBar: {
|
||||
color: '#2f56ef',
|
||||
value: 75,
|
||||
startPoint: 25,
|
||||
isGradient: false,
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const dataBar = new DataBar();
|
||||
const resolver = vi.fn(() => []);
|
||||
dataBar.setRenderRangeResolver(resolver);
|
||||
|
||||
const ranges = [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }];
|
||||
dataBar.draw(ctx, { scaleX: 1, scaleY: 1 }, skeleton, ranges);
|
||||
|
||||
expect(resolver).toHaveBeenCalledWith('unit-1', 'sheet-1', ranges);
|
||||
expect(skeleton.worksheet.getCell).not.toHaveBeenCalled();
|
||||
expect(ctx.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to the original data bar path when indexed ranges are unavailable or merges exist', () => {
|
||||
const ctx = createCtx();
|
||||
const skeleton = createSkeleton({
|
||||
'0,0': {
|
||||
dataBar: {
|
||||
color: '#2f56ef',
|
||||
value: 75,
|
||||
startPoint: 25,
|
||||
isGradient: false,
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const dataBar = new DataBar();
|
||||
const resolver = vi.fn<() => IRange[] | null>(() => null);
|
||||
dataBar.setRenderRangeResolver(resolver);
|
||||
|
||||
const ranges = [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }];
|
||||
dataBar.draw(ctx, { scaleX: 1, scaleY: 1 }, skeleton, ranges);
|
||||
expect(ctx.fill).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolver.mockReturnValue([]);
|
||||
skeleton.worksheet.getMergeData = () => [ranges[0]] as never;
|
||||
dataBar.draw(ctx, { scaleX: 1, scaleY: 1 }, skeleton, ranges);
|
||||
|
||||
expect(resolver).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.fill).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('draws configured conditional-formatting icons and skips unavailable icons', () => {
|
||||
const ctx = createCtx();
|
||||
const icon = new ConditionalFormattingIcon();
|
||||
const nativeImage = document.createElement('img');
|
||||
(icon as any)._imageMap.set(`${IIconSetType.threeArrows}_0`, nativeImage);
|
||||
|
||||
const skeleton = createSkeleton({
|
||||
'0,0': {
|
||||
iconSet: {
|
||||
iconType: IIconSetType.threeArrows,
|
||||
iconId: '0',
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
'0,1': {
|
||||
iconSet: {
|
||||
iconType: IIconSetType.empty,
|
||||
iconId: '0',
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
'0,2': {
|
||||
iconSet: {
|
||||
iconType: IIconSetType.threeArrows,
|
||||
iconId: '99',
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
icon.draw(ctx, { scaleX: 1, scaleY: 1 }, skeleton, [
|
||||
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 2 },
|
||||
]);
|
||||
|
||||
expect(ctx.drawImage).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.drawImage).toHaveBeenCalledWith(nativeImage, 2, 4.5, 15, 15);
|
||||
});
|
||||
|
||||
it('skips icon drawing for missing worksheet and cells too small for the icon', () => {
|
||||
const ctx = createCtx();
|
||||
const icon = new ConditionalFormattingIcon();
|
||||
const nativeImage = document.createElement('img');
|
||||
(icon as any)._imageMap.set(`${IIconSetType.threeArrows}_0`, nativeImage);
|
||||
|
||||
expect(icon.draw(ctx, { scaleX: 1, scaleY: 1 }, { worksheet: undefined } as any, [])).toBe(false);
|
||||
|
||||
const skeleton = createSkeleton({
|
||||
'0,0': {
|
||||
iconSet: {
|
||||
iconType: IIconSetType.threeArrows,
|
||||
iconId: '0',
|
||||
isShowValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
skeleton.getCellWithCoordByIndex = () => createCellBounds(0, 0, {
|
||||
endX: 10,
|
||||
endY: 10,
|
||||
mergeInfo: {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startX: 0,
|
||||
endX: 10,
|
||||
startY: 0,
|
||||
endY: 10,
|
||||
},
|
||||
}) as any;
|
||||
|
||||
icon.draw(ctx, { scaleX: 1, scaleY: 1 }, skeleton, [
|
||||
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
]);
|
||||
expect(ctx.drawImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips icon cell visits when a configured rule-range resolver confirms no matching rule', () => {
|
||||
const ctx = createCtx();
|
||||
const skeleton = createSkeleton({});
|
||||
const icon = new ConditionalFormattingIcon();
|
||||
const resolver = vi.fn(() => []);
|
||||
icon.setRenderRangeResolver(resolver);
|
||||
|
||||
const ranges = [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 2 }];
|
||||
icon.draw(ctx, { scaleX: 1, scaleY: 1 }, skeleton, ranges);
|
||||
|
||||
expect(resolver).toHaveBeenCalledWith('unit-1', 'sheet-1', ranges);
|
||||
expect(skeleton.worksheet.getCell).not.toHaveBeenCalled();
|
||||
expect(ctx.save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 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, IScale } from '@univerjs/core';
|
||||
import type { SpreadsheetSkeleton, UniverRenderingContext } from '@univerjs/engine-render';
|
||||
import type { IConditionalFormattingRenderRangeResolver, IDataBarCellData } from './type';
|
||||
import { Range } from '@univerjs/core';
|
||||
import { FIX_ONE_PIXEL_BLUR_OFFSET, SheetExtension, SpreadsheetExtensionRegistry } from '@univerjs/engine-render';
|
||||
|
||||
export const dataBarUKey = 'sheet-conditional-rule-data-bar';
|
||||
export const defaultDataBarPositiveColor = '#ffbe38';
|
||||
export const defaultDataBarNativeColor = '#abd91a';
|
||||
|
||||
export const defaultPlaceholderColor = '#000';
|
||||
|
||||
const EXTENSION_Z_INDEX = 34;
|
||||
|
||||
const stringifyRange = (range: IRange) => {
|
||||
const { startRow, endRow, startColumn, endColumn } = range;
|
||||
return `${startRow}-${endRow}-${startColumn}-${endColumn}`;
|
||||
};
|
||||
|
||||
export class DataBar extends SheetExtension {
|
||||
private _paddingRightAndLeft = 2;
|
||||
private _paddingTopAndBottom = 2;
|
||||
private _renderRangeResolver: IConditionalFormattingRenderRangeResolver | null = null;
|
||||
override uKey = dataBarUKey;
|
||||
|
||||
override Z_INDEX = EXTENSION_Z_INDEX;
|
||||
_radius = 1;
|
||||
|
||||
setRenderRangeResolver(resolver: IConditionalFormattingRenderRangeResolver | null) {
|
||||
this._renderRangeResolver = resolver;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
override draw(
|
||||
ctx: UniverRenderingContext,
|
||||
_parentScale: IScale,
|
||||
spreadsheetSkeleton: SpreadsheetSkeleton,
|
||||
diffRanges: IRange[]
|
||||
) {
|
||||
const { worksheet } = spreadsheetSkeleton;
|
||||
if (!worksheet) {
|
||||
return false;
|
||||
}
|
||||
const mergeCellRendered = new Set<string>();
|
||||
const sourceRanges = diffRanges?.length ? diffRanges : [spreadsheetSkeleton.rowColumnSegment];
|
||||
const renderRanges = this._renderRangeResolver && worksheet.getMergeData().length === 0
|
||||
? this._renderRangeResolver(worksheet.getUnitId(), worksheet.getSheetId(), sourceRanges) ?? sourceRanges
|
||||
: sourceRanges;
|
||||
if (!renderRanges.length) {
|
||||
return;
|
||||
}
|
||||
ctx.save();
|
||||
// ctx.globalCompositeOperation = 'destination-over';
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
renderRanges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
if (!worksheet.getRowVisible(row) || !worksheet.getColVisible(col)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryWithCoord = spreadsheetSkeleton.getCellWithCoordByIndex(row, col, false);
|
||||
const { isMerged, isMergedMainCell, mergeInfo } = primaryWithCoord;
|
||||
|
||||
let cellData = worksheet.getCell(row, col) as IDataBarCellData;
|
||||
if (isMerged) {
|
||||
cellData = worksheet.getCell(mergeInfo.startRow, mergeInfo.startColumn) as IDataBarCellData;
|
||||
}
|
||||
|
||||
if (!cellData?.dataBar) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isRenderDiffRangesByCell(mergeInfo, diffRanges)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMerged || isMergedMainCell) {
|
||||
const rangeStr = stringifyRange(mergeInfo);
|
||||
if (mergeCellRendered.has(rangeStr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergeCellRendered.add(rangeStr);
|
||||
}
|
||||
|
||||
const { color, value, startPoint, isGradient } = cellData.dataBar;
|
||||
const { startX, endX, startY, endY } = (isMerged || isMergedMainCell) ? mergeInfo : primaryWithCoord;
|
||||
const borderWidth = endX - startX;
|
||||
const borderHeight = (endY + FIX_ONE_PIXEL_BLUR_OFFSET) - startY;
|
||||
const width = borderWidth - this._paddingRightAndLeft * 2;
|
||||
const height = borderHeight - this._paddingTopAndBottom * 2;
|
||||
|
||||
if (value > 0) {
|
||||
// Width less than 1, almost invisible
|
||||
const dataBarWidth = Math.max(width * (1 - startPoint / 100) * value / 100, 1);
|
||||
const x0 = startX + this._paddingRightAndLeft + (startPoint / 100) * width;
|
||||
const y0 = startY + this._paddingTopAndBottom;
|
||||
if (isGradient) {
|
||||
const gradient = ctx.createLinearGradient(x0, y0, x0 + dataBarWidth, y0);
|
||||
gradient.addColorStop(0, color);
|
||||
gradient.addColorStop(1, 'rgb(255 255 255)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 1;
|
||||
} else {
|
||||
ctx.fillStyle = color;
|
||||
}
|
||||
|
||||
this._drawRectWithRoundedCorner(ctx, x0, y0, dataBarWidth, height, false, true, true, false);
|
||||
if (isGradient) {
|
||||
ctx.stroke();
|
||||
}
|
||||
} else {
|
||||
// Width less than 1, almost invisible
|
||||
const dataBarWidth = Math.max(width * startPoint / 100 * Math.abs(value) / 100, 1);
|
||||
const x0 = startX + this._paddingRightAndLeft + (startPoint / 100) * width - dataBarWidth;
|
||||
const y0 = startY + this._paddingTopAndBottom;
|
||||
if (isGradient) {
|
||||
const gradient = ctx.createLinearGradient(x0, y0, x0 + dataBarWidth, y0);
|
||||
gradient.addColorStop(0, 'rgb(255 255 255)');
|
||||
gradient.addColorStop(1, color);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 1;
|
||||
} else {
|
||||
ctx.fillStyle = color;
|
||||
}
|
||||
|
||||
this._drawRectWithRoundedCorner(ctx, x0, y0, dataBarWidth, height, true, false, false, true);
|
||||
if (isGradient) {
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._renderRangeResolver = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _drawRectWithRoundedCorner(ctx: UniverRenderingContext, x: number, y: number, width: number, height: number, topLeftRadius: boolean, topRightRadius: boolean, bottomRightRadius: boolean, bottomLeftRadius: boolean) {
|
||||
const radius = this._radius;
|
||||
if (!height || !width) {
|
||||
return;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + width - radius, y);
|
||||
if (topRightRadius) {
|
||||
ctx.arcTo(x + width, y, x + width, y + radius, radius);
|
||||
} else {
|
||||
ctx.lineTo(x + width, y);
|
||||
}
|
||||
ctx.lineTo(x + width, y + height - radius);
|
||||
if (bottomRightRadius) {
|
||||
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
|
||||
} else {
|
||||
ctx.lineTo(x + width, y + height);
|
||||
}
|
||||
ctx.lineTo(x + radius, y + height);
|
||||
if (bottomLeftRadius) {
|
||||
ctx.arcTo(x, y + height, x, y + height - radius, radius);
|
||||
} else {
|
||||
ctx.lineTo(x, y + height);
|
||||
}
|
||||
ctx.lineTo(x, y + radius);
|
||||
if (topLeftRadius) {
|
||||
ctx.arcTo(x, y, x + radius, y, radius);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
SpreadsheetExtensionRegistry.add(DataBar);
|
||||
154
packages/sheets-conditional-formatting/src/render/icon.render.ts
Normal file
154
packages/sheets-conditional-formatting/src/render/icon.render.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange, IScale } from '@univerjs/core';
|
||||
import type { SpreadsheetSkeleton, UniverRenderingContext } from '@univerjs/engine-render';
|
||||
import type { IConditionalFormattingRenderRangeResolver, IIconSetCellData } from './type';
|
||||
import { Range } from '@univerjs/core';
|
||||
import { SheetExtension, SpreadsheetExtensionRegistry } from '@univerjs/engine-render';
|
||||
import { iconMap, IIconSetType } from '../models/icon-map';
|
||||
|
||||
export const IconUKey = 'sheet-conditional-rule-icon';
|
||||
const EXTENSION_Z_INDEX = 35;
|
||||
export const DEFAULT_WIDTH = 15;
|
||||
export const DEFAULT_PADDING = 2;
|
||||
|
||||
const stringifyRange = (range: IRange) => {
|
||||
const { startRow, endRow, startColumn, endColumn } = range;
|
||||
return `${startRow}-${endRow}-${startColumn}-${endColumn}`;
|
||||
};
|
||||
|
||||
export class ConditionalFormattingIcon extends SheetExtension {
|
||||
private _paddingRightAndLeft = DEFAULT_PADDING;
|
||||
|
||||
private _width = DEFAULT_WIDTH;
|
||||
|
||||
private _imageMap: Map<string, HTMLImageElement> = new Map();
|
||||
private _renderRangeResolver: IConditionalFormattingRenderRangeResolver | null = null;
|
||||
override uKey = IconUKey;
|
||||
|
||||
override Z_INDEX = EXTENSION_Z_INDEX;
|
||||
_radius = 1;
|
||||
constructor() {
|
||||
super();
|
||||
this._init();
|
||||
}
|
||||
|
||||
setRenderRangeResolver(resolver: IConditionalFormattingRenderRangeResolver | null) {
|
||||
this._renderRangeResolver = resolver;
|
||||
}
|
||||
|
||||
override draw(
|
||||
ctx: UniverRenderingContext,
|
||||
_parentScale: IScale,
|
||||
spreadsheetSkeleton: SpreadsheetSkeleton,
|
||||
diffRanges: IRange[]
|
||||
) {
|
||||
const { worksheet } = spreadsheetSkeleton;
|
||||
if (!worksheet) {
|
||||
return false;
|
||||
}
|
||||
const mergeCellRendered = new Set<string>();
|
||||
const sourceRanges = diffRanges?.length ? diffRanges : [spreadsheetSkeleton.rowColumnSegment];
|
||||
const renderRanges = this._renderRangeResolver && worksheet.getMergeData().length === 0
|
||||
? this._renderRangeResolver(worksheet.getUnitId(), worksheet.getSheetId(), sourceRanges) ?? sourceRanges
|
||||
: sourceRanges;
|
||||
if (!renderRanges.length) {
|
||||
return;
|
||||
}
|
||||
ctx.save();
|
||||
// ctx.globalCompositeOperation = 'destination-over';
|
||||
renderRanges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
if (!worksheet.getRowVisible(row) || !worksheet.getColVisible(col)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryWithCoord = spreadsheetSkeleton.getCellWithCoordByIndex(row, col, false);
|
||||
const { isMerged, isMergedMainCell, mergeInfo } = primaryWithCoord;
|
||||
|
||||
let cellData = worksheet.getCell(row, col) as IIconSetCellData;
|
||||
if (isMerged) {
|
||||
cellData = worksheet.getCell(mergeInfo.startRow, mergeInfo.startColumn) as IIconSetCellData;
|
||||
}
|
||||
|
||||
if (!cellData?.iconSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { iconType, iconId } = cellData.iconSet;
|
||||
if (iconType === IIconSetType.empty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const icon = this._imageMap.get(this._createKey(iconType, iconId));
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isRenderDiffRangesByCell(mergeInfo, diffRanges)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMerged || isMergedMainCell) {
|
||||
const rangeStr = stringifyRange(mergeInfo);
|
||||
if (mergeCellRendered.has(rangeStr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergeCellRendered.add(rangeStr);
|
||||
}
|
||||
|
||||
const { startX, endX, startY, endY } = (isMerged || isMergedMainCell) ? mergeInfo : primaryWithCoord;
|
||||
const borderWidth = endX - startX;
|
||||
const borderHeight = endY - startY;
|
||||
if (this._width > borderHeight || this._width > borderWidth + this._paddingRightAndLeft * 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Highly centered processing
|
||||
const y = (borderHeight - this._width) / 2 + startY;
|
||||
ctx.drawImage(icon, startX + this._paddingRightAndLeft, y, this._width, this._width);
|
||||
});
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._renderRangeResolver = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
for (const type in iconMap) {
|
||||
const list = iconMap[type as IIconSetType];
|
||||
list.forEach((base64, index) => {
|
||||
const key = this._createKey(type as IIconSetType, String(index));
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
this._imageMap.set(key, image);
|
||||
};
|
||||
image.src = base64;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _createKey(iconType: IIconSetType, iconIndex: string) {
|
||||
return `${iconType}_${iconIndex}`;
|
||||
}
|
||||
}
|
||||
|
||||
SpreadsheetExtensionRegistry.add(ConditionalFormattingIcon);
|
||||
48
packages/sheets-conditional-formatting/src/render/type.ts
Normal file
48
packages/sheets-conditional-formatting/src/render/type.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 { ICellData, IRange } from '@univerjs/core';
|
||||
import type { IIconSetType } from '../models/icon-map';
|
||||
|
||||
export type IConditionalFormattingRenderRangeResolver = (
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
ranges: IRange[]
|
||||
) => IRange[] | null;
|
||||
|
||||
export interface IDataBarRenderParams {
|
||||
color: string;
|
||||
value: number; // -100 - 100.
|
||||
startPoint: number; //0-100
|
||||
isGradient: boolean;
|
||||
isShowValue: boolean;
|
||||
|
||||
}
|
||||
export interface IDataBarCellData extends ICellData {
|
||||
dataBar?: IDataBarRenderParams;
|
||||
}
|
||||
|
||||
export interface IIconSetRenderParams {
|
||||
iconId: string;
|
||||
iconType: IIconSetType;
|
||||
isShowValue: boolean;
|
||||
}
|
||||
|
||||
export interface IIconSetCellData extends ICellData {
|
||||
iconSet?: IIconSetRenderParams;
|
||||
_originV?: ICellData['v'];
|
||||
}
|
||||
export type IConditionalFormattingCellData = IDataBarCellData & IIconSetCellData;
|
||||
@@ -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 { CellValueType, Injector } from '@univerjs/core';
|
||||
import { FormulaResultStatus, RegisterOtherFormulaService } from '@univerjs/engine-formula';
|
||||
import { Subject } from 'rxjs';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CFRuleType, CFSubRuleType, CFValueType } from '../../base/const';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { ConditionalFormattingFormulaService } from '../conditional-formatting-formula.service';
|
||||
|
||||
describe('ConditionalFormattingFormulaService', () => {
|
||||
let service: ConditionalFormattingFormulaService;
|
||||
let registerFormulaWithRange: ReturnType<typeof vi.fn>;
|
||||
let deleteFormula: ReturnType<typeof vi.fn>;
|
||||
let formulaResults: Map<string, unknown>;
|
||||
let formulaResult$: Subject<Record<string, Record<string, unknown[]>>>;
|
||||
let ruleChange$: Subject<any>;
|
||||
|
||||
class TestRegisterOtherFormulaService {
|
||||
formulaResult$ = formulaResult$;
|
||||
registerFormulaWithRange = registerFormulaWithRange;
|
||||
getFormulaValueSync = (_unitId: string, _subUnitId: string, formulaId: string) => formulaResults.get(formulaId);
|
||||
deleteFormula = deleteFormula;
|
||||
}
|
||||
|
||||
class TestConditionalFormattingRuleModel {
|
||||
$ruleChange = ruleChange$;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
formulaResults = new Map();
|
||||
registerFormulaWithRange = vi.fn(() => 'formula-1');
|
||||
deleteFormula = vi.fn();
|
||||
formulaResult$ = new Subject();
|
||||
ruleChange$ = new Subject();
|
||||
const injector = new Injector();
|
||||
injector.add([RegisterOtherFormulaService, { useClass: TestRegisterOtherFormulaService as never }]);
|
||||
injector.add([ConditionalFormattingRuleModel, { useClass: TestConditionalFormattingRuleModel as never }]);
|
||||
injector.add([ConditionalFormattingFormulaService]);
|
||||
service = injector.get(ConditionalFormattingFormulaService);
|
||||
});
|
||||
|
||||
it('registers conditional-formatting formulas with a stable top-left anchor range', () => {
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=A1>0', [
|
||||
{ startRow: 5, endRow: 5, startColumn: 5, endColumn: 5 },
|
||||
{ startRow: 1, endRow: 1, startColumn: 2, endColumn: 2 },
|
||||
]);
|
||||
|
||||
expect(registerFormulaWithRange).toHaveBeenCalledWith(
|
||||
'book-1',
|
||||
'sheet-1',
|
||||
'=A1>0',
|
||||
[
|
||||
{ startRow: 1, endRow: 1, startColumn: 2, endColumn: 2 },
|
||||
{ startRow: 5, endRow: 5, startColumn: 5, endColumn: 5 },
|
||||
],
|
||||
undefined,
|
||||
expect.any(String),
|
||||
'cf-1'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns boolean formula results as business booleans for conditional checks', () => {
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=A1>0');
|
||||
formulaResults.set('formula-1', {
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: { 0: { 0: [[{ t: CellValueType.BOOLEAN, v: 1 }]] } },
|
||||
});
|
||||
|
||||
expect(service.getFormulaResultWithCoords('book-1', 'sheet-1', 'cf-1', '=A1>0')).toEqual({
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns not-register and pending statuses when formula results are unavailable', () => {
|
||||
expect(service.getFormulaResultWithCoords('book-1', 'sheet-1', 'cf-1', '=A1>0')).toEqual({
|
||||
status: FormulaResultStatus.NOT_REGISTER,
|
||||
});
|
||||
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=A1>0');
|
||||
expect(service.getFormulaResultWithCoords('book-1', 'sheet-1', 'cf-1', '=A1>0')).toEqual({
|
||||
status: FormulaResultStatus.NOT_REGISTER,
|
||||
});
|
||||
|
||||
formulaResults.set('formula-1', { status: FormulaResultStatus.WAIT });
|
||||
expect(service.getFormulaResultWithCoords('book-1', 'sheet-1', 'cf-1', '=A1>0')).toEqual({
|
||||
status: FormulaResultStatus.WAIT,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formula matrices with extracted scalar cell values', () => {
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=A1:B2');
|
||||
formulaResults.set('formula-1', {
|
||||
status: FormulaResultStatus.SUCCESS,
|
||||
result: {
|
||||
0: {
|
||||
0: [[{ t: CellValueType.BOOLEAN, v: 0 }]],
|
||||
1: [[{ v: 12 }]],
|
||||
},
|
||||
1: {
|
||||
0: [[null]],
|
||||
1: [[{ v: 'done' }]],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const matrix = service.getFormulaMatrix('book-1', 'sheet-1', 'cf-1', '=A1:B2');
|
||||
|
||||
expect(matrix.status).toBe(FormulaResultStatus.SUCCESS);
|
||||
expect(matrix.result?.getValue(0, 0)).toBe(false);
|
||||
expect(matrix.result?.getValue(0, 1)).toBe(12);
|
||||
expect(matrix.result?.getValue(1, 0)).toBe(false);
|
||||
expect(matrix.result?.getValue(1, 1)).toBe('done');
|
||||
});
|
||||
|
||||
it('registers and removes formulas in response to rule changes', () => {
|
||||
ruleChange$.next({
|
||||
type: 'add',
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
rule: {
|
||||
cfId: 'cf-1',
|
||||
ranges: [{ startRow: 3, endRow: 3, startColumn: 1, endColumn: 1 }],
|
||||
rule: {
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.formula,
|
||||
value: '=A1>0',
|
||||
},
|
||||
},
|
||||
});
|
||||
ruleChange$.next({
|
||||
type: 'delete',
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
rule: { cfId: 'cf-1' },
|
||||
});
|
||||
|
||||
expect(registerFormulaWithRange).toHaveBeenCalledWith(
|
||||
'book-1',
|
||||
'sheet-1',
|
||||
'=A1>0',
|
||||
[{ startRow: 3, endRow: 3, startColumn: 1, endColumn: 1 }],
|
||||
undefined,
|
||||
expect.any(String),
|
||||
'cf-1'
|
||||
);
|
||||
expect(deleteFormula).toHaveBeenCalledWith('book-1', 'sheet-1', ['formula-1']);
|
||||
});
|
||||
|
||||
it('removes formula caches when formula-backed scale rules are replaced', () => {
|
||||
registerFormulaWithRange.mockReturnValueOnce('formula-1').mockReturnValueOnce('formula-2');
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=MIN(A:A)');
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=MAX(A:A)');
|
||||
|
||||
ruleChange$.next({
|
||||
type: 'set',
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
rule: {
|
||||
cfId: 'cf-1',
|
||||
rule: {
|
||||
type: CFRuleType.colorScale,
|
||||
config: [{ value: { type: CFValueType.formula } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(deleteFormula).toHaveBeenCalledWith('book-1', 'sheet-1', ['formula-1', 'formula-2']);
|
||||
expect(service.getSubUnitFormulaMap('book-1', 'sheet-1')?.getValues()).toEqual([]);
|
||||
});
|
||||
|
||||
it('publishes result changes and reports whether every formula for the rule finished', () => {
|
||||
registerFormulaWithRange.mockReturnValueOnce('formula-1').mockReturnValueOnce('formula-2');
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=A1>0');
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=B1>0');
|
||||
const results: unknown[] = [];
|
||||
service.result$.subscribe((result) => results.push(result));
|
||||
|
||||
formulaResults.set('formula-1', { status: FormulaResultStatus.SUCCESS, result: {} });
|
||||
formulaResults.set('formula-2', { status: FormulaResultStatus.WAIT });
|
||||
formulaResult$.next({ 'book-1': { 'sheet-1': [{ formulaId: 'formula-1' }] } });
|
||||
|
||||
formulaResults.set('formula-2', { status: FormulaResultStatus.SUCCESS, result: {} });
|
||||
formulaResult$.next({ 'book-1': { 'sheet-1': [{ formulaId: 'formula-2' }, { formulaId: 'unknown' }] } });
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({ formulaId: 'formula-1', isAllFinished: false }),
|
||||
expect.objectContaining({ formulaId: 'formula-2', isAllFinished: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('deletes one formula cache by text without unregistering other cached formulas', () => {
|
||||
registerFormulaWithRange.mockReturnValueOnce('formula-1').mockReturnValueOnce('formula-2');
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=A1>0');
|
||||
service.registerFormulaWithRange('book-1', 'sheet-1', 'cf-1', '=B1>0');
|
||||
|
||||
expect(service.deleteCache('book-1', 'sheet-1', 'cf-1', '=A1>0')).toEqual([]);
|
||||
expect(service.getSubUnitFormulaMap('book-1', 'sheet-1')?.getValue('cf-1_=A1>0', ['id'])).toBeNull();
|
||||
expect(service.getSubUnitFormulaMap('book-1', 'sheet-1')?.getValue('cf-1_=B1>0', ['id'])).toMatchObject({
|
||||
formulaId: 'formula-2',
|
||||
});
|
||||
expect(service.createCFormulaId('cf-1', '=B1>0')).toBe('cf-1_=B1>0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injector } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ConditionalFormattingRangeTransformService } from '../conditional-formatting-range-transform.service';
|
||||
|
||||
const sortRanges = <T extends { startRow: number; startColumn: number; endRow: number; endColumn: number }>(ranges: T[]) => {
|
||||
return [...ranges].sort((a, b) =>
|
||||
a.startRow - b.startRow ||
|
||||
a.startColumn - b.startColumn ||
|
||||
a.endRow - b.endRow ||
|
||||
a.endColumn - b.endColumn
|
||||
);
|
||||
};
|
||||
|
||||
function createService(): ConditionalFormattingRangeTransformService {
|
||||
const injector = new Injector();
|
||||
injector.add([ConditionalFormattingRangeTransformService]);
|
||||
return injector.get(ConditionalFormattingRangeTransformService);
|
||||
}
|
||||
|
||||
describe('ConditionalFormattingRangeTransformService', () => {
|
||||
it('subtracts ranges without expanding to a cell matrix', () => {
|
||||
const service = createService();
|
||||
|
||||
const ranges = service.subtractRanges(
|
||||
[{ startRow: 0, endRow: 4, startColumn: 0, endColumn: 4 }],
|
||||
[{ startRow: 1, endRow: 3, startColumn: 1, endColumn: 3 }]
|
||||
);
|
||||
|
||||
expect(sortRanges(ranges)).toEqual(sortRanges([
|
||||
{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 4 },
|
||||
{ startRow: 4, startColumn: 0, endRow: 4, endColumn: 4 },
|
||||
{ startRow: 1, startColumn: 0, endRow: 3, endColumn: 0 },
|
||||
{ startRow: 1, startColumn: 4, endRow: 3, endColumn: 4 },
|
||||
]));
|
||||
});
|
||||
|
||||
it('intersects source ranges and translates them to a target anchor', () => {
|
||||
const service = createService();
|
||||
|
||||
const ranges = service.copyIntersectingRanges(
|
||||
[{ startRow: 0, endRow: 2, startColumn: 0, endColumn: 2 }],
|
||||
{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2 },
|
||||
{ row: 10, col: 20 }
|
||||
);
|
||||
|
||||
expect(ranges).toEqual([
|
||||
{ startRow: 10, endRow: 10, startColumn: 20, endColumn: 21 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds and merges translated ranges', () => {
|
||||
const service = createService();
|
||||
|
||||
const ranges = service.addRanges(
|
||||
[{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
|
||||
[{ startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 }]
|
||||
);
|
||||
|
||||
expect(ranges).toEqual([{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 1 }]);
|
||||
});
|
||||
|
||||
it('applies batched subtract and add operations once per rule', () => {
|
||||
const service = createService();
|
||||
|
||||
const ranges = service.applyRangeDelta(
|
||||
[{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 2 }],
|
||||
[{ startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 }],
|
||||
[{ startRow: 1, endRow: 1, startColumn: 0, endColumn: 1 }]
|
||||
);
|
||||
|
||||
expect(sortRanges(ranges)).toEqual(sortRanges([
|
||||
{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 },
|
||||
{ startRow: 0, startColumn: 2, endRow: 0, endColumn: 2 },
|
||||
{ startRow: 1, startColumn: 0, endRow: 1, endColumn: 1 },
|
||||
]));
|
||||
});
|
||||
});
|
||||
@@ -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 { Injector } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CFRuleType } from '../../base/const';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { ConditionalFormattingViewModel } from '../../models/conditional-formatting-view-model';
|
||||
import { ConditionalFormattingStyleComposer } from '../conditional-formatting-style-composer.service';
|
||||
|
||||
describe('ConditionalFormattingStyleComposer', () => {
|
||||
let service: ConditionalFormattingStyleComposer;
|
||||
const rules = new Map<string, unknown>();
|
||||
let cellCfs: Array<{ cfId: string; result: unknown; priority: number }>;
|
||||
let ruleChange$: Subject<void>;
|
||||
let markDirty$: Subject<void>;
|
||||
let cacheClear$: Subject<void>;
|
||||
const getCellCfs = vi.fn(() => cellCfs);
|
||||
|
||||
class TestConditionalFormattingRuleModel {
|
||||
$ruleChange = ruleChange$;
|
||||
|
||||
getRule(_unitId: string, _subUnitId: string, cfId: string) {
|
||||
return rules.get(cfId);
|
||||
}
|
||||
}
|
||||
|
||||
class TestConditionalFormattingViewModel {
|
||||
markDirty$ = markDirty$;
|
||||
cacheClear$ = cacheClear$;
|
||||
|
||||
getCellCfs = getCellCfs;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
rules.clear();
|
||||
cellCfs = [];
|
||||
ruleChange$ = new Subject();
|
||||
markDirty$ = new Subject();
|
||||
cacheClear$ = new Subject();
|
||||
getCellCfs.mockClear();
|
||||
const injector = new Injector();
|
||||
injector.add([ConditionalFormattingRuleModel, { useClass: TestConditionalFormattingRuleModel as never }]);
|
||||
injector.add([ConditionalFormattingViewModel, { useClass: TestConditionalFormattingViewModel as never }]);
|
||||
injector.add([ConditionalFormattingStyleComposer]);
|
||||
service = injector.get(ConditionalFormattingStyleComposer);
|
||||
});
|
||||
|
||||
it('merges matched conditional styles in rule priority order', () => {
|
||||
rules.set('color-scale', { stopIfTrue: false, rule: { type: CFRuleType.colorScale } });
|
||||
rules.set('highlight', { stopIfTrue: false, rule: { type: CFRuleType.highlightCell } });
|
||||
cellCfs = [
|
||||
{ cfId: 'color-scale', result: '#ffeeaa', priority: 1 },
|
||||
{ cfId: 'highlight', result: { cl: { rgb: '#333333' } }, priority: 2 },
|
||||
];
|
||||
|
||||
expect(service.composeStyle('book-1', 'sheet-1', 1, 1)).toEqual({
|
||||
style: { cl: { rgb: '#333333' }, bg: { rgb: '#ffeeaa' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('composes highlight, data bar, and icon set rules on the same cell', () => {
|
||||
const dataBar = {
|
||||
color: '#38bdf8',
|
||||
isGradient: false,
|
||||
isShowValue: true,
|
||||
startPoint: 0,
|
||||
value: 50,
|
||||
};
|
||||
const iconSet = {
|
||||
iconId: '0',
|
||||
iconType: '3Arrows',
|
||||
isShowValue: true,
|
||||
};
|
||||
rules.set('highlight', { stopIfTrue: false, rule: { type: CFRuleType.highlightCell } });
|
||||
rules.set('data-bar', { stopIfTrue: false, rule: { type: CFRuleType.dataBar } });
|
||||
rules.set('icon-set', { stopIfTrue: false, rule: { type: CFRuleType.iconSet } });
|
||||
cellCfs = [
|
||||
{ cfId: 'icon-set', result: iconSet, priority: 1 },
|
||||
{ cfId: 'data-bar', result: dataBar, priority: 2 },
|
||||
{ cfId: 'highlight', result: { bg: { rgb: '#fff7cc' } }, priority: 3 },
|
||||
];
|
||||
|
||||
expect(service.composeStyle('book-1', 'sheet-1', 1, 1)).toEqual({
|
||||
style: { bg: { rgb: '#fff7cc' } },
|
||||
dataBar,
|
||||
iconSet,
|
||||
isShowValue: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('stops evaluating lower-priority rules after a matched stop-if-true rule', () => {
|
||||
rules.set('stop', { stopIfTrue: true, rule: { type: CFRuleType.highlightCell } });
|
||||
rules.set('ignored', { stopIfTrue: false, rule: { type: CFRuleType.colorScale } });
|
||||
cellCfs = [
|
||||
{ cfId: 'stop', result: { bg: { rgb: '#ff0000' } }, priority: 1 },
|
||||
{ cfId: 'ignored', result: '#00ff00', priority: 2 },
|
||||
];
|
||||
|
||||
expect(service.composeStyle('book-1', 'sheet-1', 1, 1)).toEqual({
|
||||
style: { bg: { rgb: '#ff0000' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('reuses the composed result until conditional formatting changes', () => {
|
||||
rules.set('highlight', { stopIfTrue: false, rule: { type: CFRuleType.highlightCell } });
|
||||
cellCfs = [{ cfId: 'highlight', result: { cl: { rgb: '#333333' } }, priority: 1 }];
|
||||
const first = service.composeStyle('book-1', 'sheet-1', 1, 1);
|
||||
const second = service.composeStyle('book-1', 'sheet-1', 1, 1);
|
||||
|
||||
expect(second).toBe(first);
|
||||
expect(getCellCfs).toHaveBeenCalledOnce();
|
||||
|
||||
markDirty$.next();
|
||||
service.composeStyle('book-1', 'sheet-1', 1, 1);
|
||||
expect(getCellCfs).toHaveBeenCalledTimes(2);
|
||||
|
||||
ruleChange$.next();
|
||||
service.composeStyle('book-1', 'sheet-1', 1, 1);
|
||||
expect(getCellCfs).toHaveBeenCalledTimes(3);
|
||||
|
||||
cacheClear$.next();
|
||||
service.composeStyle('book-1', 'sheet-1', 1, 1);
|
||||
expect(getCellCfs).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 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 { ICommandService, Injector, IResourceManagerService, IUniverInstanceService } from '@univerjs/core';
|
||||
import {
|
||||
CopySheetCommand,
|
||||
InsertColMutation,
|
||||
InsertRowMutation,
|
||||
MoveColsMutation,
|
||||
MoveRangeMutation,
|
||||
MoveRowsMutation,
|
||||
RemoveSheetCommand,
|
||||
ReorderRangeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ConditionalFormattingRangeIndexModel } from '../../models/conditional-formatting-range-index-model';
|
||||
import { ConditionalFormattingRuleModel } from '../../models/conditional-formatting-rule-model';
|
||||
import { ConditionalFormattingViewModel } from '../../models/conditional-formatting-view-model';
|
||||
import { ConditionalFormattingStyleComposer } from '../conditional-formatting-style-composer.service';
|
||||
import { ConditionalFormattingService } from '../conditional-formatting.service';
|
||||
|
||||
describe('ConditionalFormattingService', () => {
|
||||
let service: ConditionalFormattingService;
|
||||
let composeStyle: ReturnType<typeof vi.fn>;
|
||||
let resourceConfig: any;
|
||||
let sheetInterceptorConfig: any;
|
||||
let commandListener: (commandInfo: any) => void;
|
||||
let unitRules: Map<string, unknown[]> | null;
|
||||
let deleteUnitId: ReturnType<typeof vi.fn>;
|
||||
let addRule: ReturnType<typeof vi.fn>;
|
||||
let rebuild: ReturnType<typeof vi.fn>;
|
||||
let clearCache: ReturnType<typeof vi.fn>;
|
||||
let markRuleDirty: ReturnType<typeof vi.fn>;
|
||||
|
||||
class TestConditionalFormattingRuleModel {
|
||||
getUnitRules = () => unitRules;
|
||||
getSubunitRules = (_unitId: string, subUnitId: string) => unitRules?.get(subUnitId);
|
||||
deleteUnitId = deleteUnitId;
|
||||
addRule = addRule;
|
||||
createCfId = (_unitId: string, subUnitId: string) => `${subUnitId}-new-cf`;
|
||||
}
|
||||
|
||||
class TestConditionalFormattingRangeIndexModel {
|
||||
rebuild = rebuild;
|
||||
getRulesByRanges = (_unitId: string, _subUnitId: string, ranges: unknown[]) => ranges.length ? [{ cfId: 'cf-1' }] : [];
|
||||
remove = vi.fn();
|
||||
add = vi.fn();
|
||||
}
|
||||
|
||||
class TestConditionalFormattingStyleComposer {
|
||||
composeStyle = composeStyle;
|
||||
}
|
||||
|
||||
class TestConditionalFormattingViewModel {
|
||||
clearCache = clearCache;
|
||||
markRuleDirty = markRuleDirty;
|
||||
markCellDirty = vi.fn();
|
||||
}
|
||||
|
||||
const worksheet = {
|
||||
getSheetId: () => 'sheet-1',
|
||||
getRowCount: () => 100,
|
||||
getColumnCount: () => 50,
|
||||
};
|
||||
const workbook = {
|
||||
getUnitId: () => 'book-1',
|
||||
getSheetBySheetId: (sheetId: string) => (sheetId === 'sheet-1' || sheetId === 'sheet-2' ? { ...worksheet, getSheetId: () => sheetId } : null),
|
||||
getActiveSheet: () => worksheet,
|
||||
};
|
||||
|
||||
class TestUniverInstanceService {
|
||||
getUnit = (unitId: string) => (unitId === 'book-1' ? workbook : null);
|
||||
getCurrentUnitOfType = () => workbook;
|
||||
}
|
||||
|
||||
class TestResourceManagerService {
|
||||
registerPluginResource(config: unknown) {
|
||||
resourceConfig = config;
|
||||
return { dispose: vi.fn() };
|
||||
}
|
||||
}
|
||||
|
||||
class TestSheetInterceptorService {
|
||||
interceptCommand = vi.fn((config: unknown) => {
|
||||
sheetInterceptorConfig = config;
|
||||
return { dispose: vi.fn() };
|
||||
});
|
||||
}
|
||||
|
||||
class TestCommandService {
|
||||
onCommandExecuted(listener: (commandInfo: any) => void) {
|
||||
commandListener = listener;
|
||||
return { dispose: vi.fn() };
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
composeStyle = vi.fn(() => ({ style: { bg: { rgb: '#fff000' } } }));
|
||||
resourceConfig = null;
|
||||
sheetInterceptorConfig = null;
|
||||
commandListener = () => undefined;
|
||||
unitRules = null;
|
||||
deleteUnitId = vi.fn();
|
||||
addRule = vi.fn();
|
||||
rebuild = vi.fn();
|
||||
clearCache = vi.fn();
|
||||
markRuleDirty = vi.fn();
|
||||
const injector = new Injector();
|
||||
injector.add([ConditionalFormattingRuleModel, { useClass: TestConditionalFormattingRuleModel as never }]);
|
||||
injector.add([ConditionalFormattingRangeIndexModel, { useClass: TestConditionalFormattingRangeIndexModel as never }]);
|
||||
injector.add([ConditionalFormattingStyleComposer, { useClass: TestConditionalFormattingStyleComposer as never }]);
|
||||
injector.add([ConditionalFormattingViewModel, { useClass: TestConditionalFormattingViewModel as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([IResourceManagerService, { useClass: TestResourceManagerService as never }]);
|
||||
injector.add([SheetInterceptorService, { useClass: TestSheetInterceptorService as never }]);
|
||||
injector.add([ICommandService, { useClass: TestCommandService as never }]);
|
||||
injector.add([ConditionalFormattingService]);
|
||||
service = injector.get(ConditionalFormattingService);
|
||||
});
|
||||
|
||||
it('composes the conditional style for the requested cell through the style composer', () => {
|
||||
expect(service.composeStyle('book-1', 'sheet-1', 2, 3)).toEqual({ style: { bg: { rgb: '#fff000' } } });
|
||||
expect(composeStyle).toHaveBeenCalledWith('book-1', 'sheet-1', 2, 3);
|
||||
});
|
||||
|
||||
it('serializes, loads, and unloads conditional-formatting snapshot resources', () => {
|
||||
const firstRule = { cfId: 'cf-1', ranges: [] };
|
||||
const secondRule = { cfId: 'cf-2', ranges: [] };
|
||||
unitRules = new Map([['sheet-1', [firstRule, secondRule]]]);
|
||||
|
||||
expect(resourceConfig.toJson('book-1')).toBe(JSON.stringify({ 'sheet-1': [firstRule, secondRule] }));
|
||||
expect(resourceConfig.parseJson('')).toEqual({});
|
||||
expect(resourceConfig.parseJson('{bad')).toEqual({});
|
||||
expect(resourceConfig.parseJson('{"sheet-1":[]}')).toEqual({ 'sheet-1': [] });
|
||||
|
||||
resourceConfig.onLoad('book-2', { 'sheet-2': [firstRule, secondRule] });
|
||||
expect(addRule).toHaveBeenNthCalledWith(1, 'book-2', 'sheet-2', secondRule);
|
||||
expect(addRule).toHaveBeenNthCalledWith(2, 'book-2', 'sheet-2', firstRule);
|
||||
|
||||
resourceConfig.onUnLoad('book-1');
|
||||
expect(deleteUnitId).toHaveBeenCalledWith('book-1');
|
||||
expect(rebuild).toHaveBeenCalled();
|
||||
expect(clearCache).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('marks affected conditional-formatting rules dirty after cell value and reorder mutations', () => {
|
||||
commandListener({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
cellValue: {
|
||||
1: { 2: { v: 10 } },
|
||||
3: { 4: { s: 'style-only' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
commandListener({
|
||||
id: ReorderRangeMutation.id,
|
||||
params: {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 0, endColumn: 2 },
|
||||
},
|
||||
});
|
||||
commandListener({ id: 'unrelated.command', params: {} });
|
||||
|
||||
expect(markRuleDirty).toHaveBeenCalledTimes(2);
|
||||
expect(markRuleDirty).toHaveBeenCalledWith('book-1', 'sheet-1', 'cf-1');
|
||||
});
|
||||
|
||||
it('creates conditional-formatting undo redo mutations for sheet remove and copy commands', () => {
|
||||
const firstRule = { cfId: 'cf-1', ranges: [] };
|
||||
const secondRule = { cfId: 'cf-2', ranges: [] };
|
||||
unitRules = new Map([['sheet-1', [firstRule, secondRule]]]);
|
||||
|
||||
const removeResult = sheetInterceptorConfig.getMutations({
|
||||
id: RemoveSheetCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1' },
|
||||
});
|
||||
expect(removeResult.redos).toEqual([
|
||||
{ id: 'sheet.mutation.delete-conditional-rule', params: { unitId: 'book-1', subUnitId: 'sheet-1', cfId: 'cf-1' } },
|
||||
{ id: 'sheet.mutation.delete-conditional-rule', params: { unitId: 'book-1', subUnitId: 'sheet-1', cfId: 'cf-2' } },
|
||||
]);
|
||||
expect(removeResult.undos.length).toBeGreaterThan(0);
|
||||
|
||||
const copyResult = sheetInterceptorConfig.getMutations({
|
||||
id: CopySheetCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', targetSubUnitId: 'sheet-2' },
|
||||
});
|
||||
expect(copyResult.redos).toEqual([
|
||||
{
|
||||
id: 'sheet.mutation.add-conditional-rule',
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-2', rule: { cfId: 'sheet-2-new-cf', ranges: [] } },
|
||||
},
|
||||
{
|
||||
id: 'sheet.mutation.add-conditional-rule',
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-2', rule: { cfId: 'sheet-2-new-cf', ranges: [] } },
|
||||
},
|
||||
]);
|
||||
expect(sheetInterceptorConfig.getMutations({ id: 'unrelated.command', params: {} })).toEqual({ redos: [], undos: [] });
|
||||
expect(sheetInterceptorConfig.getMutations({ id: RemoveSheetCommand.id, params: { unitId: 'missing', subUnitId: 'sheet-1' } })).toEqual({ redos: [], undos: [] });
|
||||
});
|
||||
|
||||
it('marks conditional-formatting rules dirty after row and column structure changes', () => {
|
||||
const range = { startRow: 2, endRow: 3, startColumn: 4, endColumn: 5 };
|
||||
commandListener({ id: InsertColMutation.id, params: { unitId: 'book-1', subUnitId: 'sheet-1', range } });
|
||||
commandListener({ id: InsertRowMutation.id, params: { unitId: 'book-1', subUnitId: 'sheet-1', range } });
|
||||
commandListener({
|
||||
id: MoveRowsMutation.id,
|
||||
params: {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
sourceRange: { startRow: 5, endRow: 6, startColumn: 0, endColumn: 10 },
|
||||
targetRange: { startRow: 1, endRow: 2, startColumn: 0, endColumn: 10 },
|
||||
},
|
||||
});
|
||||
commandListener({
|
||||
id: MoveColsMutation.id,
|
||||
params: {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
sourceRange: { startRow: 0, endRow: 10, startColumn: 5, endColumn: 6 },
|
||||
targetRange: { startRow: 0, endRow: 10, startColumn: 1, endColumn: 2 },
|
||||
},
|
||||
});
|
||||
commandListener({
|
||||
id: MoveRangeMutation.id,
|
||||
params: {
|
||||
unitId: 'book-1',
|
||||
from: { subUnitId: 'sheet-1', value: { 0: { 0: { v: 1 } } } },
|
||||
to: { subUnitId: 'sheet-1', value: { 2: { 2: { v: 1 } } } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(markRuleDirty).toHaveBeenCalledWith('book-1', 'sheet-1', 'cf-1');
|
||||
expect(markRuleDirty.mock.calls.length).toBeGreaterThanOrEqual(6);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 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 { ICellData, IRange, Nullable } from '@univerjs/core';
|
||||
import type { IOtherFormulaResult } from '@univerjs/engine-formula';
|
||||
import type { IConditionalFormattingRuleConfig, IConditionFormattingRule } from '../models/type';
|
||||
import { BooleanNumber, CellValueType, Disposable, Inject, ObjectMatrix, RefAlias } from '@univerjs/core';
|
||||
import { FormulaResultStatus, OtherFormulaBizType, RegisterOtherFormulaService } from '@univerjs/engine-formula';
|
||||
import { Subject } from 'rxjs';
|
||||
import { CFRuleType, CFSubRuleType, CFValueType } from '../base/const';
|
||||
import { ConditionalFormattingRuleModel } from '../models/conditional-formatting-rule-model';
|
||||
|
||||
// eslint-disable-next-line ts/consistent-type-definitions
|
||||
type IFormulaItem = {
|
||||
formulaText: string;
|
||||
cfId: string;
|
||||
id: string;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
formulaId: string;
|
||||
};
|
||||
// TODO: @Gggpound
|
||||
// It may be possible later to abstract a service that manages the results of an asynchronous calculation to handle the use of the last calculation before waiting for the result to return.
|
||||
export class ConditionalFormattingFormulaService extends Disposable {
|
||||
// Cache Formula ID and formula mapping.
|
||||
private _formulaMap: Map<string, Map<string, RefAlias<IFormulaItem, 'id' | 'formulaId'>>> = new Map();
|
||||
|
||||
private _result$ = new Subject<IFormulaItem & { isAllFinished: boolean }>();
|
||||
public result$ = this._result$.asObservable();
|
||||
|
||||
constructor(
|
||||
@Inject(RegisterOtherFormulaService) private _registerOtherFormulaService: RegisterOtherFormulaService,
|
||||
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel
|
||||
) {
|
||||
super();
|
||||
this._initFormulaResultChange();
|
||||
this._initRuleChange();
|
||||
}
|
||||
|
||||
private _initRuleChange() {
|
||||
const isNeedMarkFormulaDirty = (rule: IConditionalFormattingRuleConfig) => {
|
||||
switch (rule.type) {
|
||||
case CFRuleType.colorScale: {
|
||||
return rule.config.some((item) => item.value.type === CFValueType.formula);
|
||||
}
|
||||
case CFRuleType.dataBar: {
|
||||
return [rule.config.max, rule.config.min].some((item) => item.type === CFValueType.formula);
|
||||
}
|
||||
case CFRuleType.iconSet: {
|
||||
return rule.config.some((item) => item.value.type === CFValueType.formula);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.disposeWithMe(this._conditionalFormattingRuleModel.$ruleChange.subscribe((option) => {
|
||||
const { unitId, subUnitId, rule, oldRule } = option;
|
||||
if (option.type === 'delete') {
|
||||
this._removeFormulaByCfId(unitId, subUnitId, rule.cfId);
|
||||
}
|
||||
if (option.type === 'set') {
|
||||
if (isNeedMarkFormulaDirty(rule.rule) || (oldRule && isNeedMarkFormulaDirty(oldRule.rule))) {
|
||||
this._removeFormulaByCfId(unitId, subUnitId, rule.cfId);
|
||||
}
|
||||
}
|
||||
if (option.type === 'add') {
|
||||
this._registerRuleFormulas(unitId, subUnitId, rule);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register formulas for a specific rule based on its type
|
||||
*/
|
||||
private _registerRuleFormulas(unitId: string, subUnitId: string, rule: IConditionFormattingRule): void {
|
||||
switch (rule.rule.type) {
|
||||
case CFRuleType.highlightCell: {
|
||||
// For highlight cell with formula subtype
|
||||
if (rule.rule.subType === CFSubRuleType.formula) {
|
||||
const formulaText = rule.rule.value;
|
||||
if (formulaText) {
|
||||
this.registerFormulaWithRange(
|
||||
unitId,
|
||||
subUnitId,
|
||||
rule.cfId,
|
||||
formulaText,
|
||||
rule.ranges
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _initFormulaResultChange() {
|
||||
// Subscribe to formula calculation results
|
||||
this.disposeWithMe(this._registerOtherFormulaService.formulaResult$.subscribe((results: Record<string, Record<string, IOtherFormulaResult[]>>) => {
|
||||
for (const unitId in results) {
|
||||
for (const subUnitId in results[unitId]) {
|
||||
const subUnitResults = results[unitId][subUnitId];
|
||||
for (const formulaResult of subUnitResults) {
|
||||
const formulaMapAlias = this._ensureSubunitFormulaMap(unitId, subUnitId).getValue(formulaResult.formulaId, ['formulaId']);
|
||||
if (!formulaMapAlias) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const allFormulaMapAlias = this._getAllFormulaResultByCfId(unitId, subUnitId, formulaMapAlias.cfId);
|
||||
const isAllFinished = allFormulaMapAlias.every((item) => {
|
||||
const result = this._registerOtherFormulaService.getFormulaValueSync(unitId, subUnitId, item.formulaId);
|
||||
return result?.status === FormulaResultStatus.SUCCESS;
|
||||
});
|
||||
this._result$.next({ ...formulaMapAlias, isAllFinished });
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _ensureSubunitFormulaMap(unitId: string, subUnitId: string) {
|
||||
let unitMap = this._formulaMap.get(unitId);
|
||||
if (!unitMap) {
|
||||
unitMap = new Map<string, RefAlias<IFormulaItem, 'formulaId' | 'id'>>();
|
||||
this._formulaMap.set(unitId, unitMap);
|
||||
}
|
||||
let subUnitMap = unitMap.get(subUnitId);
|
||||
if (!subUnitMap) {
|
||||
subUnitMap = new RefAlias<IFormulaItem, 'formulaId' | 'id'>([], ['formulaId', 'id']);
|
||||
unitMap.set(subUnitId, subUnitMap);
|
||||
}
|
||||
return subUnitMap;
|
||||
}
|
||||
|
||||
public getSubUnitFormulaMap(unitId: string, subUnitId: string) {
|
||||
return this._formulaMap.get(unitId)?.get(subUnitId);
|
||||
}
|
||||
|
||||
public registerFormulaWithRange(unitId: string, subUnitId: string, cfId: string, formulaText: string, ranges: IRange[] = [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }]) {
|
||||
const formulaMap = this._ensureSubunitFormulaMap(unitId, subUnitId);
|
||||
const cfFormulaId = this.createCFormulaId(cfId, formulaText);
|
||||
if (formulaMap.getValue(cfFormulaId, ['id'])) {
|
||||
return;
|
||||
}
|
||||
// Always sort ranges by top-left so the formula engine uses a consistent anchor (ranges[0])
|
||||
// regardless of the call path (e.g. rule-add event vs preComputing).
|
||||
const sortedRanges = [...ranges].sort((a, b) =>
|
||||
a.startRow !== b.startRow ? a.startRow - b.startRow : a.startColumn - b.startColumn
|
||||
);
|
||||
const formulaId = this._registerOtherFormulaService.registerFormulaWithRange(unitId, subUnitId, formulaText, sortedRanges, undefined, OtherFormulaBizType.CONDITIONAL_FORMATTING, cfId);
|
||||
formulaMap.addValue({
|
||||
formulaText,
|
||||
unitId,
|
||||
subUnitId,
|
||||
cfId,
|
||||
id: cfFormulaId,
|
||||
formulaId,
|
||||
});
|
||||
}
|
||||
|
||||
private _removeFormulaByCfId(unitId: string, subUnitId: string, cfId: string) {
|
||||
const values = this.deleteCache(unitId, subUnitId, cfId);
|
||||
const formulaIdList = values.map((item) => item.formulaId);
|
||||
this._registerOtherFormulaService.deleteFormula(unitId, subUnitId, formulaIdList);
|
||||
}
|
||||
|
||||
public getFormulaResultWithCoords(unitId: string, subUnitId: string, cfId: string, formulaText: string, row: number = 0, col: number = 0) {
|
||||
const map = this.getSubUnitFormulaMap(unitId, subUnitId);
|
||||
if (!map) {
|
||||
return { status: FormulaResultStatus.NOT_REGISTER };
|
||||
}
|
||||
const item = map.getValue(this.createCFormulaId(cfId, formulaText), ['id']);
|
||||
if (!item) {
|
||||
return { status: FormulaResultStatus.NOT_REGISTER };
|
||||
}
|
||||
const formulaResult = this._registerOtherFormulaService.getFormulaValueSync(unitId, subUnitId, item.formulaId);
|
||||
if (!formulaResult) {
|
||||
return { status: FormulaResultStatus.NOT_REGISTER };
|
||||
}
|
||||
|
||||
if (formulaResult.status === FormulaResultStatus.SUCCESS && formulaResult.result) {
|
||||
const cellData = formulaResult.result[row]?.[col];
|
||||
const result = this._getCellValue(cellData);
|
||||
return { result, status: FormulaResultStatus.SUCCESS };
|
||||
}
|
||||
|
||||
return { status: formulaResult.status };
|
||||
}
|
||||
|
||||
public getFormulaMatrix(unitId: string, subUnitId: string, cfId: string, formulaText: string) {
|
||||
const map = this.getSubUnitFormulaMap(unitId, subUnitId);
|
||||
if (!map) {
|
||||
return { status: FormulaResultStatus.NOT_REGISTER };
|
||||
}
|
||||
const item = map.getValue(this.createCFormulaId(cfId, formulaText), ['id']);
|
||||
if (!item) {
|
||||
return { status: FormulaResultStatus.NOT_REGISTER };
|
||||
}
|
||||
const formulaResult = this._registerOtherFormulaService.getFormulaValueSync(unitId, subUnitId, item.formulaId);
|
||||
if (!formulaResult) {
|
||||
return { status: FormulaResultStatus.NOT_REGISTER };
|
||||
}
|
||||
|
||||
if (formulaResult.status === FormulaResultStatus.SUCCESS && formulaResult.result) {
|
||||
const result = new ObjectMatrix<string | number | boolean | undefined | null | void>();
|
||||
const resultMatrix = new ObjectMatrix(formulaResult.result);
|
||||
resultMatrix.forValue((row, col, cellData) => {
|
||||
result.setValue(row, col, this._getCellValue(cellData));
|
||||
});
|
||||
return { result, status: FormulaResultStatus.SUCCESS };
|
||||
}
|
||||
|
||||
return { status: formulaResult.status };
|
||||
}
|
||||
|
||||
private _getCellValue(cellData: Nullable<ICellData>[][]): string | number | boolean | undefined | null | void {
|
||||
if (!cellData || !cellData[0] || !cellData[0][0]) {
|
||||
return false;
|
||||
}
|
||||
const cell = cellData[0][0];
|
||||
if (cell?.t === CellValueType.BOOLEAN) {
|
||||
return cell.v === BooleanNumber.TRUE || cell.v === true;
|
||||
}
|
||||
return cell?.v;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `formulaText` is not provided, then all caches related to `cfId` will be deleted.
|
||||
*/
|
||||
public deleteCache(unitId: string, subUnitId: string, cfId: string, formulaText?: string) {
|
||||
const map = this.getSubUnitFormulaMap(unitId, subUnitId);
|
||||
if (!map) {
|
||||
return [];
|
||||
}
|
||||
if (formulaText) {
|
||||
const key = this.createCFormulaId(cfId, formulaText);
|
||||
map.deleteValue(key, ['id']);
|
||||
return [];
|
||||
} else {
|
||||
const values = map.getValues().filter((v) => v.cfId === cfId);
|
||||
values.forEach((e) => {
|
||||
map.deleteValue(e.formulaId, ['formulaId']);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
private _getAllFormulaResultByCfId(unitId: string, subUnitId: string, cfId: string) {
|
||||
const map = this.getSubUnitFormulaMap(unitId, subUnitId);
|
||||
if (!map) {
|
||||
return [];
|
||||
}
|
||||
const values = map.getValues().filter((v) => v.cfId === cfId);
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* A conditional formatting may have multiple formulas;if the formulas are identical,then the results will be consistent.
|
||||
*/
|
||||
public createCFormulaId(cfId: string, formulaText: string) {
|
||||
return `${cfId}_${formulaText}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 { getIntersectRange, Rectangle } from '@univerjs/core';
|
||||
|
||||
export interface IRangeAnchor {
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
||||
|
||||
export class ConditionalFormattingRangeTransformService {
|
||||
subtractRanges(sourceRanges: IRange[], rangesToSubtract: IRange[]) {
|
||||
return Rectangle.mergeRanges(Rectangle.subtractMulti(sourceRanges, rangesToSubtract));
|
||||
}
|
||||
|
||||
addRanges(sourceRanges: IRange[], rangesToAdd: IRange[]) {
|
||||
return Rectangle.mergeRanges([...sourceRanges, ...rangesToAdd]);
|
||||
}
|
||||
|
||||
applyRangeDelta(sourceRanges: IRange[], rangesToSubtract: IRange[], rangesToAdd: IRange[]) {
|
||||
const ranges = rangesToSubtract.length
|
||||
? this.subtractRanges(sourceRanges, rangesToSubtract)
|
||||
: sourceRanges;
|
||||
|
||||
return rangesToAdd.length
|
||||
? this.addRanges(ranges, rangesToAdd)
|
||||
: ranges;
|
||||
}
|
||||
|
||||
copyIntersectingRanges(sourceRanges: IRange[], sourceRange: IRange, targetAnchor: IRangeAnchor) {
|
||||
const fragments: IRange[] = [];
|
||||
|
||||
sourceRanges.forEach((range) => {
|
||||
const intersect = getIntersectRange(range, sourceRange);
|
||||
if (!intersect) {
|
||||
return;
|
||||
}
|
||||
|
||||
fragments.push(this.translateRange(intersect, {
|
||||
row: targetAnchor.row - sourceRange.startRow,
|
||||
col: targetAnchor.col - sourceRange.startColumn,
|
||||
}));
|
||||
});
|
||||
|
||||
return Rectangle.mergeRanges(fragments);
|
||||
}
|
||||
|
||||
translateRange(range: IRange, offset: IRangeAnchor): IRange {
|
||||
return {
|
||||
startRow: range.startRow + offset.row,
|
||||
endRow: range.endRow + offset.row,
|
||||
startColumn: range.startColumn + offset.col,
|
||||
endColumn: range.endColumn + offset.col,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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, IHighlightCell } from '../models/type';
|
||||
import type { IDataBarCellData, IDataBarRenderParams, IIconSetCellData, IIconSetRenderParams } from '../render/type';
|
||||
import { Disposable, Inject, LRUMap, merge } from '@univerjs/core';
|
||||
import { CFRuleType } from '../base/const';
|
||||
import { ConditionalFormattingRuleModel } from '../models/conditional-formatting-rule-model';
|
||||
import { CONDITIONAL_FORMATTING_VIEWPORT_CACHE_LENGTH, ConditionalFormattingViewModel } from '../models/conditional-formatting-view-model';
|
||||
|
||||
type IComposedStyle = { style?: IHighlightCell['style'] } & IDataBarCellData & IIconSetCellData & { isShowValue: boolean };
|
||||
|
||||
export class ConditionalFormattingStyleComposer extends Disposable {
|
||||
private _cache = new LRUMap<string, IComposedStyle | null>(CONDITIONAL_FORMATTING_VIEWPORT_CACHE_LENGTH);
|
||||
|
||||
constructor(
|
||||
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
|
||||
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel
|
||||
) {
|
||||
super();
|
||||
this.disposeWithMe(this._conditionalFormattingRuleModel.$ruleChange.subscribe(() => this._cache.clear()));
|
||||
this.disposeWithMe(this._conditionalFormattingViewModel.markDirty$.subscribe(() => this._cache.clear()));
|
||||
this.disposeWithMe(this._conditionalFormattingViewModel.cacheClear$.subscribe(() => this._cache.clear()));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._cache.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Conditional formats need to be evaluated in priority order.
|
||||
// Evaluation of subsequent rules stops only if the current rule is matched and stopIfTrue=true.
|
||||
composeStyle(unitId: string, subUnitId: string, row: number, col: number) {
|
||||
const cacheKey = `${unitId}_${subUnitId}_${row}_${col}`;
|
||||
if (this._cache.has(cacheKey)) {
|
||||
return this._cache.get(cacheKey) ?? null;
|
||||
}
|
||||
|
||||
const cellCfs = this._conditionalFormattingViewModel.getCellCfs(unitId, subUnitId, row, col);
|
||||
|
||||
if (!cellCfs?.length) {
|
||||
this._cache.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const matchedRules: Array<{
|
||||
rule: IConditionFormattingRule;
|
||||
cacheItem: (typeof cellCfs)[number];
|
||||
}> = [];
|
||||
|
||||
let stopIfTrueIndex = -1;
|
||||
for (const cacheItem of cellCfs) {
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(unitId, subUnitId, cacheItem.cfId);
|
||||
if (!rule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matchedRules.push({ rule, cacheItem });
|
||||
|
||||
if (stopIfTrueIndex === -1 && rule.stopIfTrue && this._isRuleMatched(rule, cacheItem.result)) {
|
||||
stopIfTrueIndex = matchedRules.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedRules.length) {
|
||||
this._cache.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveRules = stopIfTrueIndex > -1
|
||||
? matchedRules.slice(0, stopIfTrueIndex + 1)
|
||||
: matchedRules;
|
||||
|
||||
const result = {} as IComposedStyle;
|
||||
|
||||
for (let i = effectiveRules.length - 1; i >= 0; i--) {
|
||||
const { rule, cacheItem } = effectiveRules[i];
|
||||
this._mergeComposeResult(result, rule, cacheItem.result);
|
||||
}
|
||||
|
||||
this._cache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private _mergeComposeResult(
|
||||
result: { style?: IHighlightCell['style'] } & IDataBarCellData & IIconSetCellData & { isShowValue: boolean },
|
||||
rule: IConditionFormattingRule,
|
||||
ruleResult: unknown
|
||||
) {
|
||||
const type = rule.rule.type;
|
||||
|
||||
if (type === CFRuleType.highlightCell) {
|
||||
ruleResult && merge(result, { style: ruleResult });
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === CFRuleType.colorScale) {
|
||||
if (ruleResult && typeof ruleResult === 'string') {
|
||||
const preStyle = result.style || {};
|
||||
result.style = { ...preStyle, bg: { rgb: ruleResult } };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === CFRuleType.dataBar) {
|
||||
const ruleCache = ruleResult as IDataBarRenderParams;
|
||||
if (ruleCache) {
|
||||
result.dataBar = ruleCache;
|
||||
result.isShowValue = ruleCache.isShowValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === CFRuleType.iconSet) {
|
||||
const ruleCache = ruleResult as IIconSetRenderParams;
|
||||
if (ruleCache) {
|
||||
result.iconSet = ruleCache;
|
||||
result.isShowValue = ruleCache.isShowValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _isRuleMatched(rule: IConditionFormattingRule, ruleResult: unknown) {
|
||||
if (rule.rule.type === CFRuleType.highlightCell) {
|
||||
return !!ruleResult && typeof ruleResult === 'object' && Object.keys(ruleResult as object).length > 0;
|
||||
}
|
||||
|
||||
return !!ruleResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type {
|
||||
ICopySheetCommandParams,
|
||||
IInsertColMutationParams,
|
||||
IInsertRowMutationParams,
|
||||
IMoveColumnsMutationParams,
|
||||
IMoveRangeMutationParams,
|
||||
IMoveRowsMutationParams,
|
||||
IRemoveColMutationParams,
|
||||
IRemoveRowsMutationParams,
|
||||
IRemoveSheetCommandParams,
|
||||
IReorderRangeMutationParams,
|
||||
ISetRangeValuesMutationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import type { IAddConditionalRuleMutationParams } from '../commands/mutations/add-conditional-rule.mutation';
|
||||
import type { IDeleteConditionalRuleMutationParams } from '../commands/mutations/delete-conditional-rule.mutation';
|
||||
import type { IRuleModelJson } from '../models/type';
|
||||
import {
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
Injector,
|
||||
IResourceManagerService,
|
||||
isInternalEditorID,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
CopySheetCommand,
|
||||
getSheetCommandTarget,
|
||||
InsertColMutation,
|
||||
InsertRowMutation,
|
||||
MoveColsMutation,
|
||||
MoveRangeMutation,
|
||||
MoveRowsMutation,
|
||||
RemoveColMutation,
|
||||
RemoveRowMutation,
|
||||
RemoveSheetCommand,
|
||||
ReorderRangeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { SHEET_CONDITIONAL_FORMATTING_PLUGIN } from '../base/const';
|
||||
import { AddConditionalRuleMutation, AddConditionalRuleMutationUndoFactory } from '../commands/mutations/add-conditional-rule.mutation';
|
||||
import { DeleteConditionalRuleMutation, DeleteConditionalRuleMutationUndoFactory } from '../commands/mutations/delete-conditional-rule.mutation';
|
||||
import { ConditionalFormattingRangeIndexModel } from '../models/conditional-formatting-range-index-model';
|
||||
import { ConditionalFormattingRuleModel } from '../models/conditional-formatting-rule-model';
|
||||
import { ConditionalFormattingViewModel } from '../models/conditional-formatting-view-model';
|
||||
import { ConditionalFormattingStyleComposer } from './conditional-formatting-style-composer.service';
|
||||
|
||||
export class ConditionalFormattingService extends Disposable {
|
||||
get _conditionalFormattingViewModelV2() {
|
||||
return this._injector.get(ConditionalFormattingViewModel);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
|
||||
@Inject(ConditionalFormattingRangeIndexModel) private _conditionalFormattingRangeIndexModel: ConditionalFormattingRangeIndexModel,
|
||||
@Inject(ConditionalFormattingStyleComposer) private _conditionalFormattingStyleComposer: ConditionalFormattingStyleComposer,
|
||||
@Inject(Injector) private _injector: Injector,
|
||||
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(IResourceManagerService) private _resourceManagerService: IResourceManagerService,
|
||||
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
this._initCellChange();
|
||||
this._initSnapshot();
|
||||
this._initSheetChange();
|
||||
}
|
||||
|
||||
public composeStyle(unitId: string, subUnitId: string, row: number, col: number) {
|
||||
return this._conditionalFormattingStyleComposer.composeStyle(unitId, subUnitId, row, col);
|
||||
}
|
||||
|
||||
private _initSnapshot() {
|
||||
const toJson = (unitID: string) => {
|
||||
const map = this._conditionalFormattingRuleModel.getUnitRules(unitID);
|
||||
const resultMap: IRuleModelJson[keyof IRuleModelJson] = {};
|
||||
if (map) {
|
||||
map.forEach((v, key) => {
|
||||
resultMap[key] = v;
|
||||
});
|
||||
return JSON.stringify(resultMap);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const parseJson = (json: string): IRuleModelJson[keyof IRuleModelJson] => {
|
||||
if (!json) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
this.disposeWithMe(
|
||||
this._resourceManagerService.registerPluginResource<IRuleModelJson[keyof IRuleModelJson]>({
|
||||
pluginName: SHEET_CONDITIONAL_FORMATTING_PLUGIN,
|
||||
businesses: [UniverInstanceType.UNIVER_SHEET],
|
||||
toJson: (unitID) => toJson(unitID),
|
||||
parseJson: (json) => parseJson(json),
|
||||
onUnLoad: (unitID) => {
|
||||
this._conditionalFormattingRuleModel.deleteUnitId(unitID);
|
||||
this._conditionalFormattingRangeIndexModel.rebuild();
|
||||
if (isInternalEditorID(unitID)) return;
|
||||
this._conditionalFormattingViewModelV2.clearCache();
|
||||
},
|
||||
onLoad: (unitID, value) => {
|
||||
Object.keys(value).forEach((subunitId) => {
|
||||
const ruleList = [...value[subunitId]].reverse();
|
||||
ruleList.forEach((rule) => {
|
||||
this._conditionalFormattingRuleModel.addRule(unitID, subunitId, rule);
|
||||
});
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initSheetChange() {
|
||||
this.disposeWithMe(
|
||||
this._sheetInterceptorService.interceptCommand({
|
||||
getMutations: (commandInfo) => {
|
||||
if (commandInfo.id === RemoveSheetCommand.id) {
|
||||
const params = commandInfo.params as IRemoveSheetCommandParams;
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, params);
|
||||
if (!target) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
|
||||
const ruleList = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId);
|
||||
if (!ruleList) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
ruleList.forEach((item) => {
|
||||
const params: IDeleteConditionalRuleMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cfId: item.cfId,
|
||||
};
|
||||
redos.push({
|
||||
id: DeleteConditionalRuleMutation.id,
|
||||
params,
|
||||
});
|
||||
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, params));
|
||||
});
|
||||
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
} else if (commandInfo.id === CopySheetCommand.id) {
|
||||
const params = commandInfo.params as ICopySheetCommandParams & { targetSubUnitId: string };
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, params);
|
||||
if (!target) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const { targetSubUnitId } = params;
|
||||
|
||||
const ruleList = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId);
|
||||
if (!ruleList) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
ruleList.forEach((item) => {
|
||||
const params: IAddConditionalRuleMutationParams = {
|
||||
unitId,
|
||||
subUnitId: targetSubUnitId,
|
||||
rule: {
|
||||
...item,
|
||||
cfId: this._conditionalFormattingRuleModel.createCfId(unitId, targetSubUnitId),
|
||||
},
|
||||
};
|
||||
redos.push({ id: AddConditionalRuleMutation.id, params });
|
||||
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, params));
|
||||
});
|
||||
|
||||
return { redos, undos };
|
||||
}
|
||||
return { redos: [], undos: [] };
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initCellChange() {
|
||||
this.disposeWithMe(
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
this._commandService.onCommandExecuted((commandInfo) => {
|
||||
const markRulesDirtyByRanges = (unitId: string, subUnitId: string, ranges: IRange[]) => {
|
||||
const rules = this._conditionalFormattingRangeIndexModel.getRulesByRanges(unitId, subUnitId, ranges);
|
||||
rules.forEach((rule) => {
|
||||
this._conditionalFormattingViewModelV2.markRuleDirty(unitId, subUnitId, rule.cfId);
|
||||
});
|
||||
};
|
||||
|
||||
switch (commandInfo.id) {
|
||||
case SetRangeValuesMutation.id: {
|
||||
const params = commandInfo.params as ISetRangeValuesMutationParams;
|
||||
const { subUnitId, unitId, cellValue } = params;
|
||||
const ranges: IRange[] = [];
|
||||
new ObjectMatrix(cellValue).forValue((row, col, value) => {
|
||||
// When P and V are involved
|
||||
const result = value && Object.keys(value).some((key) => ['p', 'v'].includes(key));
|
||||
if (result) {
|
||||
ranges.push({ startRow: row, endRow: row, startColumn: col, endColumn: col });
|
||||
}
|
||||
});
|
||||
markRulesDirtyByRanges(unitId, subUnitId, ranges);
|
||||
break;
|
||||
}
|
||||
case InsertColMutation.id:
|
||||
case RemoveColMutation.id: {
|
||||
const params = commandInfo.params as IInsertColMutationParams | IRemoveColMutationParams;
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, params);
|
||||
if (!target) return;
|
||||
|
||||
const { worksheet, unitId, subUnitId } = target;
|
||||
const { range } = params;
|
||||
const effectRange: IRange = { ...range, endColumn: worksheet.getColumnCount() - 1 };
|
||||
|
||||
markRulesDirtyByRanges(unitId, subUnitId, [effectRange]);
|
||||
break;
|
||||
}
|
||||
case RemoveRowMutation.id:
|
||||
case InsertRowMutation.id: {
|
||||
const params = commandInfo.params as IRemoveRowsMutationParams | IInsertRowMutationParams;
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, params);
|
||||
if (!target) return;
|
||||
|
||||
const { worksheet, unitId, subUnitId } = target;
|
||||
const { range } = params;
|
||||
const effectRange: IRange = { ...range, endRow: worksheet.getRowCount() - 1 };
|
||||
|
||||
markRulesDirtyByRanges(unitId, subUnitId, [effectRange]);
|
||||
break;
|
||||
}
|
||||
case MoveRowsMutation.id: {
|
||||
const params = commandInfo.params as IMoveRowsMutationParams;
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, params);
|
||||
if (!target) return;
|
||||
|
||||
const { worksheet, unitId, subUnitId } = target;
|
||||
const { sourceRange, targetRange } = params;
|
||||
const effectRange: IRange = {
|
||||
startRow: Math.min(sourceRange.startRow, targetRange.startRow),
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
startColumn: 0,
|
||||
endColumn: worksheet.getColumnCount() - 1,
|
||||
};
|
||||
|
||||
markRulesDirtyByRanges(unitId, subUnitId, [effectRange]);
|
||||
break;
|
||||
}
|
||||
case MoveColsMutation.id: {
|
||||
const params = commandInfo.params as IMoveColumnsMutationParams;
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, params);
|
||||
if (!target) return;
|
||||
|
||||
const { worksheet, unitId, subUnitId } = target;
|
||||
const { sourceRange, targetRange } = params;
|
||||
const effectRange: IRange = {
|
||||
startRow: 0,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
startColumn: Math.min(sourceRange.startColumn, targetRange.startColumn),
|
||||
endColumn: worksheet.getColumnCount() - 1,
|
||||
};
|
||||
|
||||
markRulesDirtyByRanges(unitId, subUnitId, [effectRange]);
|
||||
break;
|
||||
}
|
||||
case MoveRangeMutation.id: {
|
||||
const { unitId, to, from } = commandInfo.params as IMoveRangeMutationParams;
|
||||
const handleSubUnit = (value: IMoveRangeMutationParams['from']) => {
|
||||
const ranges: IRange[] = [];
|
||||
new ObjectMatrix(value.value).forValue((row, col) => {
|
||||
ranges.push({ startRow: row, endRow: row, startColumn: col, endColumn: col });
|
||||
});
|
||||
markRulesDirtyByRanges(unitId, value.subUnitId, ranges);
|
||||
};
|
||||
handleSubUnit(to);
|
||||
handleSubUnit(from);
|
||||
break;
|
||||
}
|
||||
case ReorderRangeMutation.id: {
|
||||
const { range, unitId, subUnitId } = commandInfo.params as IReorderRangeMutationParams;
|
||||
markRulesDirtyByRanges(unitId, subUnitId, [range]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* 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 '../../models/type';
|
||||
import type { IAnchor } from '../anchor';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { anchorUndoFactory, moveByAnchor, transformSupportSymmetryAnchor } from '../anchor';
|
||||
|
||||
describe('Test anchor', () => {
|
||||
let list: IConditionFormattingRule[] = [];
|
||||
beforeEach(() => {
|
||||
list = [];
|
||||
new Array(10).fill('').forEach((_v, index) => {
|
||||
list.push({ cfId: String(index) } as IConditionFormattingRule);
|
||||
});
|
||||
});
|
||||
describe('Move forward from the back', () => {
|
||||
it('Move 8 in front of 2', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'self' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0182345679');
|
||||
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 8 in behind of 2', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'self' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'after' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0128345679');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 8 in front of 2, self', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'self' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'self' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0182345679');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
|
||||
it('Move 7 in front of 2', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'before' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0172345689');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 7 in behind of 2', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'before' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'after' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0127345689');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 7 in front of 2,self', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'before' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'self' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0172345689');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
|
||||
it('Move 9 in front of 2', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0192345678');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 9 in behind of 2', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'after' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0129345678');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
it('Move 9 in front of 2,self', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '2', type: 'self' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0192345678');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 9 in front of 0,self', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '0', type: 'self' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('9012345678');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 9 in front of 0', () => {
|
||||
const startAnchor: IAnchor = { id: '8', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '0', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('9012345678');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
});
|
||||
describe('Move back and forth', () => {
|
||||
it('Move 2 in front of 8', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'self' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0134567289');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 2 in behind of 8', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'self' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'after' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0134567829');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 2 in front of 8, self', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'self' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'self' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0134567289');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
|
||||
it('Move 1 in front of 8', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'before' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0234567189');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 1 in behind of 8', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'before' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'after' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0234567819');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 1 in front of 8,self', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'before' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'self' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0234567189');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
|
||||
it('Move 3 in front of 8', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0124567389');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
it('Move 3 in behind of 8', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'after' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0124567839');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
it('Move 3 in front of 8,self', () => {
|
||||
const startAnchor: IAnchor = { id: '2', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'self' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0124567389');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
|
||||
it('Move 0 in front of 8', () => {
|
||||
const startAnchor: IAnchor = { id: '0', type: 'self' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('1234567089');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
it('Move 0 in behind of 8', () => {
|
||||
const startAnchor: IAnchor = { id: '1', type: 'before' };
|
||||
const endAnchor: IAnchor = { id: '8', type: 'before' };
|
||||
const redoList = transformSupportSymmetryAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId)!;
|
||||
expect(!!redoList).toBeTruthy();
|
||||
moveByAnchor(...redoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('1234567089');
|
||||
const undoList = anchorUndoFactory(...redoList)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
it('use before and after', () => {
|
||||
const startAnchor: IAnchor = { id: '1', type: 'before' };
|
||||
const endAnchor: IAnchor = { id: '1', type: 'after' };
|
||||
moveByAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('1023456789');
|
||||
const undoList = anchorUndoFactory(startAnchor, endAnchor)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
|
||||
it('use after and before', () => {
|
||||
const startAnchor: IAnchor = { id: '1', type: 'after' };
|
||||
const endAnchor: IAnchor = { id: '1', type: 'before' };
|
||||
moveByAnchor(startAnchor, endAnchor, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0213456789');
|
||||
const undoList = anchorUndoFactory(startAnchor, endAnchor)!;
|
||||
expect(!!undoList).toBeTruthy();
|
||||
moveByAnchor(...undoList, list, (rule) => rule.cfId);
|
||||
expect(list.map((item) => item.cfId).join('')).toBe('0123456789');
|
||||
});
|
||||
});
|
||||
});
|
||||
143
packages/sheets-conditional-formatting/src/utils/anchor.ts
Normal file
143
packages/sheets-conditional-formatting/src/utils/anchor.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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 interface IAnchor {
|
||||
id: string;
|
||||
type: 'before' | 'after' | 'self';
|
||||
}
|
||||
|
||||
export const findIndexByAnchor = <T = unknown[]>(anchor: IAnchor, ruleList: T[], get: (v: T) => string): number | null => {
|
||||
if (!ruleList) {
|
||||
return null;
|
||||
}
|
||||
const anchorIndex = ruleList.findIndex((rule) => get(rule) === anchor.id);
|
||||
if (anchorIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
switch (anchor.type) {
|
||||
case 'after': {
|
||||
return anchorIndex + 1;
|
||||
}
|
||||
case 'before': {
|
||||
return anchorIndex - 1;
|
||||
}
|
||||
case 'self': {
|
||||
return anchorIndex;
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* This function has side effects that modify the ruleList
|
||||
* @param {IAnchor} start
|
||||
* @param {IAnchor} end
|
||||
* @param {ReturnType<ConditionalFormattingRuleModel['getSubunitRules']>} ruleList
|
||||
* @return {*}
|
||||
*/
|
||||
export const moveByAnchor = <T = unknown[]>(start: IAnchor, end: IAnchor, ruleList: T[], get: (v: T) => string) => {
|
||||
if (!ruleList) {
|
||||
return null;
|
||||
}
|
||||
const startIndex = findIndexByAnchor(start, ruleList, get);
|
||||
let endIndex = findIndexByAnchor(end, ruleList, get);
|
||||
|
||||
if (startIndex === null || endIndex === null || startIndex === endIndex) {
|
||||
return;
|
||||
}
|
||||
const rule = ruleList.splice(startIndex, 1)[0];
|
||||
|
||||
if (startIndex < endIndex) {
|
||||
endIndex = findIndexByAnchor(end, ruleList, get)!;
|
||||
}
|
||||
|
||||
switch (end.type) {
|
||||
case 'before': {
|
||||
ruleList.splice(endIndex + 1, 0, rule);
|
||||
break;
|
||||
}
|
||||
case 'self':
|
||||
case 'after': {
|
||||
ruleList.splice(endIndex, 0, rule);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Only [after,after] and [after,before] can support symmetric operations
|
||||
*/
|
||||
export const transformSupportSymmetryAnchor = <T = unknown[]>(start: IAnchor, end: IAnchor, ruleList: T[], get: (v: T) => string): [IAnchor, IAnchor] | null => {
|
||||
if (start.type === 'after' && ['after', 'before'].includes(end.type)) {
|
||||
return [start, end];
|
||||
}
|
||||
const _start = { ...start };
|
||||
const _end = { ...end };
|
||||
if (_start.type !== 'after') {
|
||||
const index = findIndexByAnchor(_start, ruleList, get);
|
||||
if (index === null) {
|
||||
return null;
|
||||
}
|
||||
if (index - 1 < 0) {
|
||||
const nextItem = ruleList[index + 1];
|
||||
if (!nextItem) {
|
||||
return null;
|
||||
} else {
|
||||
_start.id = get(nextItem);
|
||||
_start.type = 'before';
|
||||
}
|
||||
} else {
|
||||
const id = get(ruleList[index - 1]);
|
||||
_start.id = id;
|
||||
_start.type = 'after';
|
||||
}
|
||||
}
|
||||
if (!['after', 'before'].includes(_end.type)) {
|
||||
const index = findIndexByAnchor(_end, ruleList, get);
|
||||
if (index === null) {
|
||||
return null;
|
||||
}
|
||||
if (index === 0) {
|
||||
_end.type = 'before';
|
||||
} else if (index - 1 >= 0) {
|
||||
const id = get(ruleList[index - 1]);
|
||||
_end.id = id;
|
||||
_end.type = 'after';
|
||||
} else if (index + 1 <= ruleList.length - 1) {
|
||||
const id = get(ruleList[index + 1]);
|
||||
_end.id = id;
|
||||
_end.type = 'before';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (_start.id === _end.id && _start.type === _end.type) {
|
||||
return null;
|
||||
}
|
||||
return [_start, _end];
|
||||
};
|
||||
export const anchorUndoFactory = (start: IAnchor, end: IAnchor): [IAnchor, IAnchor] | null => {
|
||||
if (['after', 'before'].includes(end.type)) {
|
||||
if (start.type === 'after') {
|
||||
return [end, start];
|
||||
} else if (start.type === 'before') {
|
||||
return [end, { ...start, type: 'self' }];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isAnchorEqual = (anchor1: IAnchor, anchor2: IAnchor) => {
|
||||
return anchor1.id === anchor2.id && anchor1.type === anchor2.type;
|
||||
};
|
||||
@@ -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 { generateRandomId } from '@univerjs/core';
|
||||
|
||||
// Given that unit and sunUnit will change in the case of replica creation, the ID will not be spelled in here
|
||||
export const createCfId = () => `${generateRandomId(8)}`;
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export function removeUndefinedAttr<T = unknown>(obj: T): T {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const value = removeUndefinedAttr(obj[key]);
|
||||
if (value !== undefined) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
}
|
||||
19
packages/sheets-conditional-formatting/src/utils/type.ts
Normal file
19
packages/sheets-conditional-formatting/src/utils/type.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type MakePropertyOptional<T, K extends keyof T> = {
|
||||
[P in keyof T]: P extends K ? T[P] | undefined : T[P];
|
||||
};
|
||||
8
packages/sheets-conditional-formatting/tsconfig.json
Normal file
8
packages/sheets-conditional-formatting/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/tsconfig.node.json
Normal file
18
packages/sheets-conditional-formatting/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"
|
||||
]
|
||||
}
|
||||
3
packages/sheets-conditional-formatting/vitest.config.ts
Normal file
3
packages/sheets-conditional-formatting/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user