feat(data-validation): transform support for data validation (#1915)

* feat: add-range & delete-range support spillable ref-range strategy

* feat: ref-range

* feat: move resource and sheet controller to data-validation pkg

* fix: unique editor id

* fix: eslint

* feat: lock

* fix: move-range

* fix: css

* feat: add ref-range tldr pic

* feat: test

* feat: ref-range unit testt

* fix: redi warning

* feat: lint

* feat: build

* feat: move mark-dirty mutation to dv-pkg

* feat: add dayjs default plugin

* feat: text-length i18n

* feat: rename ref-range util

* feat: rename

* feat: support data-validation transform

* feat: data-validation list support formula

* feat: remove useless code

* feat: dv-demo

* fix: build

* feat: delay load data validation resource on web app

* feat: build fix

* fix: build

* feat: lint

* feat: add unitId bind

* feat: subUnitId

* feat: update

* feat: update

* feat: update

* feat: update

* feat: update

* feat: update

* fix: empty range

* feat: update

* feat: add polyfill of old version

* feat: update

* feat: update

* feat: update

* feat: update

* feat: fix deps issue

* feat: style update

* feat: update

* feat: update

* feat: update

* feat: update

* feat: update

* feat: lock

* feat: build

* feat: temp

* feat: update

* feat: update

* feat: update

* feat: update

* feat: update

* feat: test

* feat: update

* fix: fix lockfile

* fix: popup

* feat: locale

* feat: lint

---------

Co-authored-by: 白熱 <sonne@asaki.me>
This commit is contained in:
zhangw
2024-04-22 15:57:57 +08:00
committed by GitHub
co-authored by 白熱
parent 8c3bb904ee
commit 22ccbb17e4
24 changed files with 484 additions and 391 deletions
@@ -0,0 +1,174 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 DEFAULT_WORKBOOK_DATA_VALIDATION = {
id: 'workbook-01',
sheetOrder: [
'80FIpJ1SdQLVIUEbrnC3J',
],
name: 'UniverSheet Demo',
appVersion: '3.0.0-alpha',
locale: 'zhCN',
styles: {},
sheets: {
'80FIpJ1SdQLVIUEbrnC3J': {
name: '工作表1',
id: '80FIpJ1SdQLVIUEbrnC3J',
tabColor: '',
hidden: 0,
rowCount: 1000,
columnCount: 20,
zoomRatio: 1,
freeze: {
xSplit: 0,
ySplit: 0,
startRow: -1,
startColumn: -1,
},
scrollTop: 0,
scrollLeft: 0,
defaultColumnWidth: 73,
defaultRowHeight: 19,
mergeData: [],
cellData: {
0: {
0: {
v: 'YES',
t: 1,
},
2: {
v: 'IF',
t: 1,
},
5: {
v: '中文',
t: 1,
},
6: {
v: '一',
t: 1,
},
7: {
v: '二',
t: 1,
},
8: {
v: '三',
t: 1,
},
},
1: {
5: {
v: '数字',
t: 1,
},
6: {
v: 1,
t: 2,
},
7: {
v: 2,
t: 2,
},
8: {
v: 3,
t: 2,
},
},
2: {
2: {
v: '级联',
t: 1,
},
3: {
v: '中文',
t: 1,
},
4: {
v: '一',
t: 1,
},
5: null,
6: null,
7: null,
8: null,
},
3: {
3: null,
4: null,
5: null,
6: null,
7: null,
8: null,
},
},
rowData: {
0: {
hd: 0,
h: 19,
ah: 20,
},
2: {
hd: 0,
h: 19,
ah: 20,
},
3: {
hd: 0,
h: 19,
ah: 19,
},
},
columnData: {},
showGridlines: 1,
rowHeader: {
width: 46,
hidden: 0,
},
columnHeader: {
height: 20,
hidden: 0,
},
selections: [
'A1',
],
rightToLeft: 0,
},
},
resources: [
{
name: 'SHEET_NUMFMT_PLUGIN',
data: '',
},
{
name: 'SHEET_DEFINED_NAME_PLUGIN',
data: '',
},
{
name: 'SHEET_DATA_VALIDATION',
data: '{"sheet-0011":[{"uid":"xxx-1","type":"decimal","ranges":[{"startRow":0,"endRow":5,"startColumn":0,"endColumn":2}],"operator":"greaterThan","formula1":"111","errorStyle":1},{"uid":"xxx-0","type":"date","ranges":[{"startRow":0,"endRow":5,"startColumn":3,"endColumn":5}],"operator":"greaterThan","formula1":"100","errorStyle":1},{"uid":"xxx-2","type":"checkbox","ranges":[{"startRow":6,"endRow":10,"startColumn":0,"endColumn":5}]},{"uid":"xxx-3","type":"list","ranges":[{"startRow":11,"endRow":15,"startColumn":0,"endColumn":5}],"formula1":"1,2,3,hahaha"},{"uid":"xxx-4","type":"custom","ranges":[{"startRow":16,"endRow":20,"startColumn":0,"endColumn":5}],"formula1":"=A1"},{"uid":"xxx-5","type":"listMultiple","ranges":[{"startRow":21,"endRow":21,"startColumn":0,"endColumn":0}],"formula1":"1,2,3,4,5,哈哈哈哈"}],"80FIpJ1SdQLVIUEbrnC3J":[{"uid":"pQCe2q","type":"list","formula1":"=IF(A1=\\"YES\\",G1:I1,G2:I2)","ranges":[{"startRow":0,"endRow":0,"startColumn":3,"endColumn":3}],"formula2":""},{"uid":"JL6foF","type":"list","formula1":"=F1:F2","ranges":[{"startRow":2,"startColumn":3,"endRow":2,"endColumn":3,"rangeType":0}],"formula2":""},{"uid":"XlFuI6","type":"list","formula1":"=IF(D3=\\"中文\\",G1:I1,G2:I2)","ranges":[{"startRow":2,"startColumn":4,"endRow":2,"endColumn":4,"rangeType":0}],"formula2":""}]}',
},
{
name: 'SHEET_CONDITIONAL_FORMATTING_PLUGIN',
data: '',
},
],
__env__: {
gitHash: '840580212',
gitBranch: 'feat/dv-list-formula',
buildTime: '2024-04-10T11:19:00.298Z',
},
};
+1
View File
@@ -73,6 +73,7 @@
},
"devDependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/protocol": "^0.1.14",
"@univerjs/shared": "workspace:*",
"@univerjs/sheets": "workspace:*",
"@wendellhu/redi": "^0.13.0",
@@ -34,13 +34,7 @@ export const AddDataValidationMutation: ICommand<IAddDataValidationMutationParam
}
const { unitId, subUnitId, rule, index } = params;
const dataValidationModel = accessor.get(DataValidationModel);
if (Array.isArray(rule)) {
rule.forEach((ruleItem) => {
dataValidationModel.addRule(unitId, subUnitId, ruleItem, index);
});
} else {
dataValidationModel.addRule(unitId, subUnitId, rule!, index);
}
dataValidationModel.addRule(unitId, subUnitId, rule, index);
return true;
},
+3 -3
View File
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { DataValidationOperator, DataValidationType, type IDataValidationRule, type IDataValidationRuleBase, type IDataValidationRuleOptions, Tools } from '@univerjs/core';
import { DataValidationOperator, DataValidationType, type IDataValidationRuleBase, type IDataValidationRuleOptions, Tools } from '@univerjs/core';
import { SelectionManagerService } from '@univerjs/sheets';
import type { IAccessor } from '@wendellhu/redi';
export function getRuleSetting(rule: IDataValidationRule): IDataValidationRuleBase {
export function getRuleSetting(rule: IDataValidationRuleBase): IDataValidationRuleBase {
return {
type: rule.type,
operator: rule.operator,
@@ -28,7 +28,7 @@ export function getRuleSetting(rule: IDataValidationRule): IDataValidationRuleBa
};
}
export function getRuleOptions(rule: IDataValidationRule): IDataValidationRuleOptions {
export function getRuleOptions(rule: IDataValidationRuleOptions): IDataValidationRuleOptions {
return {
error: rule.error,
errorStyle: rule.errorStyle,
@@ -19,6 +19,7 @@ import { DataValidationStatus, Disposable } from '@univerjs/core';
import { Subject } from 'rxjs';
import type { IUpdateRulePayload } from '../types/interfaces/i-update-rule-payload';
import { UpdateRuleType } from '../types/enum/update-rule-type';
import { getRuleOptions, getRuleSetting } from '../common/util';
export class DataValidationManager<T extends IDataValidationRule> extends Disposable {
private _dataValidations: T[];
@@ -66,22 +67,30 @@ export class DataValidationManager<T extends IDataValidationRule> extends Dispos
return this._dataValidations.findIndex((rule) => rule.uid === id);
}
addRule(rule: T, index?: number) {
addRule(rule: T | T[], index?: number) {
const _rules = Array.isArray(rule) ? rule : [rule];
const rules = _rules.filter((item) => !this._dataValidationMap.has(item.uid));
if (typeof index === 'number' && index < this._dataValidations.length) {
this._dataValidations.splice(index, 0, rule);
this._dataValidations.splice(index, 0, ...rules);
} else {
this._dataValidations.push(rule);
this._dataValidations.push(...rules);
}
this._dataValidationMap.set(rule.uid, rule);
rules.forEach((item) => {
this._dataValidationMap.set(item.uid, item);
});
this._notice();
}
removeRule(ruleId: string) {
const index = this._dataValidations.findIndex((item) => item.uid === ruleId);
this._dataValidations.splice(index, 1);
this._dataValidationMap.delete(ruleId);
this._notice();
if (index > -1) {
this._dataValidations.splice(index, 1);
this._dataValidationMap.delete(ruleId);
this._notice();
}
}
updateRule(ruleId: string, payload: IUpdateRulePayload) {
@@ -94,18 +103,19 @@ export class DataValidationManager<T extends IDataValidationRule> extends Dispos
const rule = { ...oldRule };
switch (payload.type) {
case UpdateRuleType.RANGE: {
rule.ranges = payload.payload;
break;
}
case UpdateRuleType.SETTING: {
Object.assign(rule, payload.payload);
Object.assign(rule, getRuleSetting(payload.payload));
break;
}
case UpdateRuleType.OPTIONS: {
Object.assign(rule, payload.payload);
Object.assign(rule, getRuleOptions(payload.payload));
break;
}
default:
@@ -78,16 +78,29 @@ export class DataValidationModel<T extends IDataValidationRule = IDataValidation
return manager;
}
addRule(unitId: string, subUnitId: string, rule: T, index?: number) {
private _addRuleSideEffect(unitId: string, subUnitId: string, rule: T) {
const manager = this.ensureManager(unitId, subUnitId);
const oldRule = manager.getRuleById(rule.uid);
if (oldRule) {
return;
}
this._ruleChange$.next({
rule,
type: 'add',
unitId,
subUnitId,
});
}
addRule(unitId: string, subUnitId: string, rule: T | T[], index?: number) {
try {
const manager = this.ensureManager(unitId, subUnitId);
manager.addRule(rule, index);
this._ruleChange$.next({
rule: rule as any,
type: 'add',
unitId,
subUnitId,
const rules = Array.isArray(rule) ? rule : [rule];
rules.forEach((item) => {
this._addRuleSideEffect(unitId, subUnitId, item);
});
manager.addRule(rule, index);
} catch (error) {
this._logService.error(error);
}
@@ -113,13 +126,15 @@ export class DataValidationModel<T extends IDataValidationRule = IDataValidation
try {
const manager = this.ensureManager(unitId, subUnitId);
const oldRule = manager.getRuleById(ruleId);
manager.removeRule(ruleId);
this._ruleChange$.next({
rule: oldRule,
type: 'remove',
unitId,
subUnitId,
});
if (oldRule) {
manager.removeRule(ruleId);
this._ruleChange$.next({
rule: oldRule,
type: 'remove',
unitId,
subUnitId,
});
}
} catch (error) {
this._logService.error(error);
}
+1 -1
View File
@@ -23,7 +23,7 @@ import { AddDataValidationCommand, RemoveAllDataValidationCommand, RemoveDataVal
import { AddDataValidationMutation, RemoveDataValidationMutation, UpdateDataValidationMutation } from './commands/mutations/data-validation.mutation';
import { DataValidationResourceController } from './controllers/dv-resource.controller';
import { DataValidationSheetController } from './controllers/dv-sheet.controller';
import { DataValidationFormulaMarkDirty } from './commands/mutations/formula.mutation.ts';
import { DataValidationFormulaMarkDirty } from './commands/mutations/formula.mutation';
const PLUGIN_NAME = 'data-validation';
@@ -29,6 +29,7 @@ export interface IFormulaInputProps {
subUnitId: string;
showError?: boolean;
validResult?: IFormulaValidResult;
ruleId: string;
}
export type FormulaInputType = React.ComponentType<IFormulaInputProps>;
@@ -51,7 +51,6 @@ export function DraggableList<T = any>(props: IDraggableListProps<T>) {
return (
<ReactGridLayout
useCSSTransforms={false}
{...gridProps}
cols={12}
preventCollision={false}
@@ -79,7 +79,7 @@ const calcPopupPosition = (layout: IPopupLayoutInfo) => {
function RectPopup(props: IRectPopupProps) {
const { children, anchorRect, direction = 'vertical', onClickOutside, excludeOutside } = props;
const nodeRef = useRef<HTMLElement>(null);
const clickOtherFn = useEvent(onClickOutside ?? (() => {}));
const clickOtherFn = useEvent(onClickOutside ?? (() => { }));
const [position, setPosition] = useState<Partial<IAbsolutePosition>>({
top: -9999,
@@ -102,7 +102,7 @@ function RectPopup(props: IRectPopupProps) {
)
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[
anchorRect.left,
anchorRect.top,
@@ -112,36 +112,30 @@ function RectPopup(props: IRectPopupProps) {
]);
useEffect(() => {
const current = nodeRef.current;
const parent = current?.parentElement;
const handleClickOther = (e: MouseEvent) => {
if (excludeOutside && (excludeOutside.indexOf(e.target as any) > -1)) {
return;
}
if (parent) {
const handleClickOther = (e: MouseEvent) => {
if (excludeOutside && (excludeOutside.indexOf(e.target as any) > -1)) {
return;
}
const bounding = parent.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
if (x <= anchorRect.right && x >= anchorRect.left && y <= anchorRect.bottom && y >= anchorRect.top) {
return;
}
clickOtherFn(e);
};
const x = e.clientX - bounding.left;
const y = e.clientY - bounding.top;
if (x <= anchorRect.right && x >= anchorRect.left && y <= anchorRect.bottom && y >= anchorRect.top) {
return;
}
clickOtherFn(e);
};
window.addEventListener('click', handleClickOther);
window.addEventListener('click', handleClickOther);
return () => {
window.removeEventListener('click', handleClickOther);
};
}
return () => {
window.removeEventListener('click', handleClickOther);
};
}, [anchorRect, anchorRect.bottom, anchorRect.left, anchorRect.right, anchorRect.top, clickOtherFn, excludeOutside]);
return (
<section
ref={nodeRef}
className={styles.popupAbsolute}
className={styles.popup}
style={position}
onClick={(e) => {
e.stopPropagation();
@@ -14,23 +14,22 @@
* limitations under the License.
*/
import type { IRange, ISheetDataValidationRule, Workbook } from '@univerjs/core';
import { DataValidationType, Disposable, DisposableCollection, ICommandService, isRangesEqual, IUniverInstanceService, LifecycleStages, OnLifecycle, Range, Rectangle, toDisposable, UniverInstanceType } from '@univerjs/core';
import type { EffectRefRangeParams, ISetRangeValuesMutationParams } from '@univerjs/sheets';
import type { IRange, ISheetDataValidationRule } from '@univerjs/core';
import { DataValidationType, Disposable, DisposableCollection, ICommandService, isRangesEqual, IUniverInstanceService, LifecycleStages, OnLifecycle, toDisposable } from '@univerjs/core';
import type { EffectRefRangeParams } from '@univerjs/sheets';
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { handleCommonDefaultRangeChangeWithEffectRefCommands, handleDefaultRangeChangeWithEffectRefCommands, RefRangeService, SetRangeValuesMutation } from '@univerjs/sheets';
import { handleCommonDefaultRangeChangeWithEffectRefCommands, RefRangeService } from '@univerjs/sheets';
import { Inject, Injector } from '@wendellhu/redi';
import { merge, Observable } from 'rxjs';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import type { IRemoveDataValidationMutationParams, IUpdateDataValidationMutationParams } from '@univerjs/data-validation';
import { DataValidationModel, RemoveDataValidationMutation, removeDataValidationUndoFactory, UpdateDataValidationMutation, UpdateRuleType } from '@univerjs/data-validation';
import { FormulaRefRangeService } from '@univerjs/sheets-formula';
import { deserializeRangeWithSheet, isReferenceStringWithEffectiveColumn, serializeRangeWithSpreadsheet } from '@univerjs/engine-formula';
import { DataValidationCustomFormulaService } from '../services/dv-custom-formula.service';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { DataValidationCacheService } from '../services/dv-cache.service';
@OnLifecycle(LifecycleStages.Rendered, DataValidationRefRangeController)
@OnLifecycle(LifecycleStages.Ready, DataValidationRefRangeController)
export class DataValidationRefRangeController extends Disposable {
private _disposableMap: Map<string, Set<() => void>> = new Map();
@@ -57,9 +56,9 @@ export class DataValidationRefRangeController extends Disposable {
registerRule = (unitId: string, subUnitId: string, rule: ISheetDataValidationRule) => {
this.register(unitId, subUnitId, rule);
this.registerFormula(unitId, subUnitId, rule);
this.registerRange(unitId, subUnitId, rule);
};
// eslint-disable-next-line max-lines-per-function
registerFormula(unitId: string, subUnitId: string, rule: ISheetDataValidationRule) {
const ruleId = rule.uid;
const id = this._getIdWithUnitId(unitId, subUnitId, ruleId);
@@ -97,7 +96,6 @@ export class DataValidationRefRangeController extends Disposable {
type: oldRule.type,
formula1: oldRule.formula1,
formula2: oldRule.formula2,
[type]: formulaString,
},
},
};
@@ -208,136 +206,6 @@ export class DataValidationRefRangeController extends Disposable {
this._disposableMap.set(id, current);
};
registerRange(propUnitId: string, propSubUnitId: string, rule: ISheetDataValidationRule) {
const { uid: ruleId, formula1, type } = rule;
if ((type !== DataValidationType.LIST && type !== DataValidationType.LIST_MULTIPLE)) {
return;
}
if (!isReferenceStringWithEffectiveColumn(formula1 ?? '')) {
return;
}
const gridRange = deserializeRangeWithSheet(formula1 ?? '');
const id = this._getIdWithUnitId(propUnitId, propSubUnitId, ruleId);
const rangeUnitId = gridRange.unitId || propUnitId;
const workbook = this._univerInstanceService.getUniverSheetInstance(rangeUnitId) ?? this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.SHEET)!;
const sheetId = workbook.getSheetBySheetName(gridRange.sheetName)?.getSheetId() ?? propSubUnitId;
const worksheet = workbook.getSheetBySheetId(sheetId) ?? workbook.getActiveSheet();
const disposableCollection = new DisposableCollection();
const range = gridRange.range;
const finalUnitId = workbook.getUnitId();
const finalSubUnitId = worksheet.getSheetId();
disposableCollection.add(
this._refRangeService.registerRefRange(
range,
(info) => {
let newRange = handleDefaultRangeChangeWithEffectRefCommands(range, info);
if (!newRange) {
newRange = {
startColumn: -1,
endColumn: -1,
startRow: -1,
endRow: -1,
};
}
if (Rectangle.equals(newRange, range)) {
return {
redos: [],
undos: [],
};
}
const newRangeStr = serializeRangeWithSpreadsheet(finalUnitId, finalSubUnitId, newRange);
const oldRule = this._dataValidationModel.getRuleById(propUnitId, propSubUnitId, ruleId);
if (!oldRule) {
return {
redos: [],
undos: [],
};
}
const redoParams: IUpdateDataValidationMutationParams = {
unitId: propUnitId,
subUnitId: propSubUnitId,
ruleId,
payload: {
type: UpdateRuleType.SETTING,
payload: {
formula1: newRangeStr,
formula2: oldRule.formula2,
type: oldRule.type,
},
},
};
const undoParams: IUpdateDataValidationMutationParams = {
unitId: propUnitId,
subUnitId: propSubUnitId,
ruleId,
payload: {
type: UpdateRuleType.SETTING,
payload: {
formula1: oldRule.formula1,
formula2: oldRule.formula2,
type: oldRule.type,
},
},
};
return {
redos: [{
id: UpdateDataValidationMutation.id,
params: redoParams,
}],
undos: [{
id: UpdateDataValidationMutation.id,
params: undoParams,
}],
};
},
finalUnitId,
finalUnitId
)
);
disposableCollection.add(this.disposeWithMe(
this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === SetRangeValuesMutation.id && commandInfo.params) {
const params = commandInfo.params as ISetRangeValuesMutationParams;
const {
cellValue,
unitId,
subUnitId,
} = params;
if (unitId === finalUnitId && subUnitId === finalSubUnitId) {
if (cellValue) {
let marked = false;
Range.foreach(range, (row, col) => {
const rowValue = cellValue[row];
if (rowValue && (col in rowValue)) {
if (marked) {
return;
}
marked = true;
this._dataValidationCacheService.markRangeDirty(unitId, subUnitId, rule.ranges);
}
});
} else {
this._dataValidationCacheService.markRangeDirty(unitId, subUnitId, rule.ranges);
}
}
}
})
));
const current = this._disposableMap.get(id) ?? new Set();
current.add(() => disposableCollection.dispose());
this._disposableMap.set(id, current);
}
private _initRefRange() {
this.disposeWithMe(
merge(
@@ -26,6 +26,7 @@ const locale: typeof zhCN = {
integer: 'Please input integer or formula',
date: 'Please input date or formula',
list: 'Please input options',
listInvalid: 'The list source must be a delimited list or a reference to a single row or column',
},
panel: {
title: 'Data validation management',
@@ -119,6 +120,7 @@ const locale: typeof zhCN = {
options: 'Options',
customOptions: 'Custom',
refOptions: 'From a range',
formulaError: 'The list source must be a delimited list of data, or a reference to a single row or column.',
},
listMultiple: {
title: 'Dropdown-Multiple',
@@ -24,6 +24,7 @@ const locale = {
integer: '请输入合法的整数或公式',
date: '请输入合法的日期或公式',
list: '请输入至少一个合法选项',
listInvalid: '列表源必须是分隔列表或对单行或列的引用。',
},
panel: {
title: '管理数据验证',
@@ -78,10 +79,10 @@ const locale = {
title: '日期',
operators: {
between: '介于',
greaterThan: '大于',
greaterThanOrEqual: '大于或等于',
lessThan: '小于',
lessThanOrEqual: '小于或等于',
greaterThan: '晚于',
greaterThanOrEqual: '晚于或等于',
lessThan: '早于',
lessThanOrEqual: '早于或等于',
equal: '等于',
notEqual: '不等于',
notBetween: '未介于',
@@ -119,7 +120,7 @@ const locale = {
options: '选项来源',
customOptions: '自定义',
refOptions: '引用数据',
formulaError: '列表源必须是划定分界后的数据列表,或是对单一行或一列的引用。',
},
listMultiple: {
title: '下拉菜单-多选',
@@ -16,10 +16,11 @@
import type { CellValue, ISheetDataValidationRule, Nullable } from '@univerjs/core';
import { DataValidationManager, DataValidatorRegistryService, UpdateRuleType } from '@univerjs/data-validation';
import { DataValidationStatus, ObjectMatrix, Range } from '@univerjs/core';
import { DataValidationStatus, DataValidationType, ObjectMatrix, Range } from '@univerjs/core';
import type { IUpdateRulePayload } from '@univerjs/data-validation';
import type { ISheetLocation } from '@univerjs/sheets';
import type { Injector } from '@wendellhu/redi';
import { isReferenceString } from '@univerjs/engine-formula';
import type { IDataValidationResCache } from '../services/dv-cache.service';
import { DataValidationCacheService } from '../services/dv-cache.service';
import { DataValidationFormulaService } from '../services/dv-formula.service';
@@ -69,11 +70,30 @@ export class SheetDataValidationManager extends DataValidationManager<ISheetData
this._ruleMatrix = new RuleMatrix(matrix);
}
override addRule(rule: ISheetDataValidationRule, index?: number): void {
private _addRuleSideEffect(rule: ISheetDataValidationRule) {
const { unitId, subUnitId } = this;
if (rule.type === DataValidationType.LIST || rule.type === DataValidationType.LIST_MULTIPLE) {
if (isReferenceString(rule.formula1 ?? '')) {
// polyfill old-version ref-string, transform to formula
rule.formula1 = `=${rule.formula1}`;
}
}
this._ruleMatrix.addRule(rule);
this._dataValidationCacheService.addRule(this.unitId, this.subUnitId, rule);
this._dataValidationFormulaService.addRule(this.unitId, this.subUnitId, rule.uid, rule.formula1, rule.formula2);
this._dataValidationCustomFormulaService.addRule(this.unitId, this.subUnitId, rule);
this._dataValidationCacheService.addRule(unitId, subUnitId, rule);
this._dataValidationFormulaService.addRule(unitId, subUnitId, rule.uid, rule.formula1, rule.formula2);
this._dataValidationCustomFormulaService.addRule(unitId, subUnitId, rule);
}
override addRule(rule: ISheetDataValidationRule | ISheetDataValidationRule[], index?: number): void {
const rules = Array.isArray(rule) ? rule : [rule];
rules.forEach((item) => {
this._addRuleSideEffect(item);
});
super.addRule(rule, index);
}
@@ -26,7 +26,7 @@ import { FormulaResultStatus, type IDataValidationFormulaResult } from './formul
export class RegisterOtherFormulaService extends Disposable {
private _formulaCacheMap: Map<string, Map<string, Map<string, IDataValidationFormulaResult>>> = new Map();
private _formulaChange$ = new Subject<{ unitId: string; subUnitId: string; ruleId: string; formulaText: string; formulaId: string }>();
private _formulaChange$ = new Subject<{ unitId: string; subUnitId: string; formulaText: string; formulaId: string }>();
public formulaChange$ = this._formulaChange$.asObservable();
private _formulaResult$ = new Subject<Record<string, Record<string, IDataValidationFormulaResult[]>>>();
@@ -149,7 +149,6 @@ export class RegisterOtherFormulaService extends Disposable {
});
this._formulaChange$.next({
unitId,
ruleId,
subUnitId,
formulaText,
formulaId,
@@ -175,7 +174,7 @@ export class RegisterOtherFormulaService extends Disposable {
return Promise.resolve(null);
}
if (item.status === FormulaResultStatus.SUCCESS || FormulaResultStatus.ERROR) {
if (item.status === FormulaResultStatus.SUCCESS || item.status === FormulaResultStatus.ERROR) {
return Promise.resolve(item);
}
@@ -186,6 +185,13 @@ export class RegisterOtherFormulaService extends Disposable {
});
}
async getTempFormulaResult(unitId: string, subUnitId: string, formulaString: string) {
const formulaId = this.registerFormula(unitId, subUnitId, 'temp', formulaString);
const formulaValue = await this.getFormulaValue(unitId, subUnitId, formulaId);
this.deleteFormula(unitId, subUnitId, [formulaId]);
return formulaValue;
}
getFormulaValueSync(unitId: string, subUnitId: string, formulaId: string): Nullable<IDataValidationFormulaResult> {
const cacheMap = this._ensureCacheMap(unitId, subUnitId);
return cacheMap.get(formulaId);
@@ -14,102 +14,16 @@
* limitations under the License.
*/
import { DataValidationType, IUniverInstanceService, Tools, UniverInstanceType } from '@univerjs/core';
import type { CellValue, DataValidationOperator, IDataValidationRule, IDataValidationRuleBase, Nullable, Workbook } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import { BaseDataValidator } from '@univerjs/data-validation';
import { deserializeRangeWithSheet, isReferenceStringWithEffectiveColumn } from '@univerjs/engine-formula';
import { LIST_FORMULA_INPUT_NAME } from '../views/formula-input';
import { LIST_DROPDOWN_KEY } from '../views';
import { DataValidationType } from '@univerjs/core';
import { DropdownMultipleWidget } from '../widgets/dropdown-multiple-widget';
import { deserializeListOptions, getSheetRangeValueSet } from './util';
export class ListMultipleValidator extends BaseDataValidator {
id: string = DataValidationType.LIST_MULTIPLE;
title: string = 'dataValidation.listMultiple.title';
operators: DataValidationOperator[] = [];
scopes: string | string[] = ['sheet'];
formulaInput: string = LIST_FORMULA_INPUT_NAME;
private _univerInstanceService = this.injector.get(IUniverInstanceService);
import { ListValidator } from './list-validator';
export class ListMultipleValidator extends ListValidator {
override id: string = DataValidationType.LIST_MULTIPLE;
override title: string = 'dataValidation.listMultiple.title';
override canvasRender = this.injector.createInstance(DropdownMultipleWidget);
override dropdown: string | undefined = LIST_DROPDOWN_KEY;
override skipDefaultFontRender() {
return true;
}
override validatorFormula(rule: IDataValidationRuleBase): IFormulaValidResult {
const success = !Tools.isBlank(rule.formula1);
return {
success,
formula1: success ? undefined : this.localeService.t('dataValidation.validFail.list'),
};
}
parseCellValue(cellValue: CellValue, rule: IDataValidationRule) {
const cellString = cellValue.toString();
if (!cellString) {
return [];
}
return deserializeListOptions(cellString);
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult<string[] | undefined>> {
const { formula1 = '' } = rule;
return {
formula1: isReferenceStringWithEffectiveColumn(formula1) ? getSheetRangeValueSet(deserializeRangeWithSheet(formula1), this._univerInstanceService, unitId, subUnitId) : deserializeListOptions(formula1),
formula2: undefined,
};
}
override async isValidType(cellInfo: IValidatorCellInfo<Nullable<CellValue>>, formula: IFormulaResult<any>, rule: IDataValidationRule): Promise<boolean> {
const { value } = cellInfo;
const { formula1 } = formula;
const selected = this.parseCellValue(value!, rule);
return selected.every((i) => formula1.includes(i));
}
override generateRuleName() {
return this.localeService.t('dataValidation.list.name');
}
override generateRuleErrorMessage(): string {
return this.localeService.t('dataValidation.list.error');
}
getList(rule: IDataValidationRule, propUnitId?: string, propSubUnitId?: string) {
const { formula1 = '' } = rule;
const univerInstanceService = this.injector.get(IUniverInstanceService);
const workbook = (propUnitId ? univerInstanceService.getUniverSheetInstance(propUnitId) : undefined) ?? univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.SHEET);
if (!workbook) return [];
const worksheet = (propSubUnitId ? workbook.getSheetBySheetId(propSubUnitId) : undefined) ?? workbook.getActiveSheet();
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
return isReferenceStringWithEffectiveColumn(formula1) ? getSheetRangeValueSet(deserializeRangeWithSheet(formula1), this._univerInstanceService, unitId, subUnitId) : deserializeListOptions(formula1);
}
getListWithColor(rule: IDataValidationRule, currentUnitId?: string, currentSubUnitId?: string) {
const list = this.getList(rule, currentUnitId, currentSubUnitId);
const colorList = rule.formula2 ? rule.formula2.split(',') : [];
return list.map((label, i) => ({ label, color: colorList[i] }));
}
getListWithColorMap(rule: IDataValidationRule, currentUnitId?: string, currentSubUnitId?: string) {
const list = this.getListWithColor(rule, currentUnitId, currentSubUnitId);
const map: Record<string, string> = {};
list.forEach((item) => {
if (item.color) {
map[item.label] = item.color;
}
});
return map;
}
}
@@ -14,27 +14,73 @@
* limitations under the License.
*/
import { DataValidationRenderMode, DataValidationType, IUniverInstanceService, Tools, UniverInstanceType } from '@univerjs/core';
import type { CellValue, DataValidationOperator, IDataValidationRule, IDataValidationRuleBase, ISheetDataValidationRule, Nullable, Workbook } from '@univerjs/core';
import type { IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import { DataValidationRenderMode, DataValidationType, isFormulaString, IUniverInstanceService, Tools, UniverInstanceType } from '@univerjs/core';
import type { CellValue, DataValidationOperator, ICellData, IDataValidationRule, IDataValidationRuleBase, ISheetDataValidationRule, Nullable, Workbook } from '@univerjs/core';
import type { IBaseDataValidationWidget, IFormulaResult, IFormulaValidResult, IValidatorCellInfo } from '@univerjs/data-validation';
import { BaseDataValidator } from '@univerjs/data-validation';
import { deserializeRangeWithSheet, isReferenceStringWithEffectiveColumn } from '@univerjs/engine-formula';
import { isReferenceString, LexerTreeBuilder, sequenceNodeType } from '@univerjs/engine-formula';
import { LIST_FORMULA_INPUT_NAME } from '../views/formula-input';
import { LIST_DROPDOWN_KEY } from '../views';
import { DropdownWidget } from '../widgets/dropdown-widget';
import { ListRenderModeInput } from '../views/render-mode';
import { deserializeListOptions, getSheetRangeValueSet } from './util';
import { DataValidationFormulaService } from '../services/dv-formula.service';
import { getCellValueOrigin } from '../utils/get-cell-data-origin';
import { deserializeListOptions } from './util';
export function getRuleFormulaResultSet(result: Nullable<Nullable<ICellData>[][]>) {
if (!result) {
return [];
}
const resultSet = new Set<string>();
result.forEach(
(row) => {
row.forEach((cell) => {
const value = getCellValueOrigin(cell);
if (value !== null && value !== undefined) {
resultSet.add(value.toString());
}
});
}
);
return [...resultSet];
}
const supportedFormula = [
'if',
'indirect',
'choose',
'offset',
];
// 1. must have REFERENCE or DEFINED_NAME node.
// 2. only support some formula
export function isValidListFormula(formula: string, lexer: LexerTreeBuilder) {
if (!isFormulaString(formula)) {
return true;
}
const isRefString = isReferenceString(formula.slice(1));
if (isRefString) {
return true;
}
const nodes = lexer.sequenceNodesBuilder(formula);
return (nodes) && nodes.some((node) => typeof node === 'object' && node.nodeType === sequenceNodeType.FUNCTION && supportedFormula.indexOf(node.token.toLowerCase()) > -1);
}
export class ListValidator extends BaseDataValidator {
protected formulaService = this.injector.get(DataValidationFormulaService);
private _lexer = this.injector.get(LexerTreeBuilder);
id: string = DataValidationType.LIST;
title: string = 'dataValidation.list.title';
operators: DataValidationOperator[] = [];
scopes: string | string[] = ['sheet'];
formulaInput: string = LIST_FORMULA_INPUT_NAME;
private _univerInstanceService = this.injector.get(IUniverInstanceService);
override canvasRender = this.injector.createInstance(DropdownWidget);
override canvasRender: Nullable<IBaseDataValidationWidget> = this.injector.createInstance(DropdownWidget);
override dropdown: string | undefined = LIST_DROPDOWN_KEY;
@@ -46,31 +92,37 @@ export class ListValidator extends BaseDataValidator {
override validatorFormula(rule: IDataValidationRuleBase): IFormulaValidResult {
const success = !Tools.isBlank(rule.formula1);
const valid = isValidListFormula(rule.formula1 ?? '', this._lexer);
return {
success,
formula1: success ? undefined : this.localeService.t('dataValidation.validFail.list'),
success: Boolean(success && valid),
formula1: success
? valid ?
undefined :
this.localeService.t('dataValidation.validFail.listInvalid')
: this.localeService.t('dataValidation.validFail.list'),
};
}
private _parseCellValue(cellValue: CellValue, rule: IDataValidationRule) {
parseCellValue(cellValue: CellValue) {
const cellString = cellValue.toString();
return deserializeListOptions(cellString);
}
override async parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise<IFormulaResult<string[] | undefined>> {
const { formula1 = '' } = rule;
const results = await this.formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
return {
formula1: isReferenceStringWithEffectiveColumn(formula1) ? getSheetRangeValueSet(deserializeRangeWithSheet(formula1), this._univerInstanceService, unitId, subUnitId) : deserializeListOptions(formula1),
formula1: isFormulaString(formula1) ? getRuleFormulaResultSet(results?.[0]?.result) : deserializeListOptions(formula1),
formula2: undefined,
};
}
override async isValidType(cellInfo: IValidatorCellInfo<Nullable<CellValue>>, formula: IFormulaResult<any>, rule: IDataValidationRule): Promise<boolean> {
override async isValidType(cellInfo: IValidatorCellInfo<Nullable<CellValue>>, formula: IFormulaResult<string[] | undefined>, rule: IDataValidationRule): Promise<boolean> {
const { value } = cellInfo;
const { formula1 } = formula;
const selected = this._parseCellValue(value!, rule);
const { formula1 = [] } = formula;
const selected = this.parseCellValue(value!);
return selected.every((i) => formula1.includes(i));
}
@@ -91,9 +143,22 @@ export class ListValidator extends BaseDataValidator {
const worksheet = (currentSubUnitId ? workbook.getSheetBySheetId(currentSubUnitId) : undefined) ?? workbook.getActiveSheet();
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
return isReferenceStringWithEffectiveColumn(formula1)
? getSheetRangeValueSet(deserializeRangeWithSheet(formula1), this._univerInstanceService, unitId, subUnitId)
: deserializeListOptions(formula1);
const results = this.formulaService.getRuleFormulaResultSync(unitId, subUnitId, rule.uid);
return isFormulaString(formula1) ? getRuleFormulaResultSet(results?.[0]?.result) : deserializeListOptions(formula1);
}
async getListAsync(rule: IDataValidationRule, currentUnitId?: string, currentSubUnitId?: string) {
const { formula1 = '' } = rule;
const univerInstanceService = this.injector.get(IUniverInstanceService);
const workbook = (currentUnitId ? univerInstanceService.getUniverSheetInstance(currentUnitId) : undefined) ?? univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.SHEET);
if (!workbook) {
return [];
}
const worksheet = (currentSubUnitId ? workbook.getSheetBySheetId(currentSubUnitId) : undefined) ?? workbook.getActiveSheet();
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const results = await this.formulaService.getRuleFormulaResult(unitId, subUnitId, rule.uid);
return isFormulaString(formula1) ? getRuleFormulaResultSet(results?.[0]?.result) : deserializeListOptions(formula1);
}
getListWithColor(rule: IDataValidationRule, currentUnitId?: string, currentSubUnitId?: string) {
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import type { DataValidationOperator, IDataValidationRuleBase, IDataValidationRuleOptions, IExecutionOptions, ISheetDataValidationRule, IUnitRange } from '@univerjs/core';
import { createInternalEditorID, DataValidationType, debounce, ICommandService, isUnitRangesEqual, isValidRange, LocaleService, RedoCommand, shallowEqual, UndoCommand } from '@univerjs/core';
import type { DataValidationOperator, DataValidationType, IDataValidationRuleBase, IDataValidationRuleOptions, IExecutionOptions, ISheetDataValidationRule, IUnitRange } from '@univerjs/core';
import { createInternalEditorID, debounce, ICommandService, isUnitRangesEqual, isValidRange, LocaleService, RedoCommand, shallowEqual, UndoCommand } from '@univerjs/core';
import type { IUpdateDataValidationSettingCommandParams } from '@univerjs/data-validation';
import { DataValidationModel, DataValidatorRegistryScope, DataValidatorRegistryService, getRuleOptions, getRuleSetting, RemoveDataValidationCommand, TWO_FORMULA_OPERATOR_COUNT, UpdateDataValidationOptionsCommand, UpdateDataValidationSettingCommand } from '@univerjs/data-validation';
import { Button, FormLayout, Select } from '@univerjs/design';
@@ -41,6 +41,7 @@ const debounceExecuteFactory = (commandService: ICommandService) => debounce(
275
);
// eslint-disable-next-line max-lines-per-function
export function DataValidationDetail() {
const [key, setKey] = useState(0);
const dataValidationPanelService = useDependency(DataValidationPanelService);
@@ -101,6 +102,11 @@ export function DataValidationDetail() {
...localRule,
ranges,
});
if (ranges.length === 0) {
return;
}
const params: IUpdateSheetDataValidationRangeCommandParams = {
unitId,
subUnitId,
@@ -158,18 +164,18 @@ export function DataValidationDetail() {
}
const operators = validator.operators;
const newRule = {
...localRule,
type: newType as DataValidationType,
operator: operators[0],
...(newType === DataValidationType.CHECKBOX
? {
formula1: undefined,
formula2: undefined,
}
: null),
};
const rule = dataValidationModel.getRuleById(unitId, subUnitId, ruleId);
const newRule = newType === rule?.type
? {
...rule,
}
: {
...localRule,
type: newType as DataValidationType,
operator: operators[0],
formula1: undefined,
formula2: undefined,
};
setLocalRule(newRule);
commandService.executeCommand(UpdateDataValidationSettingCommand.id, {
@@ -274,6 +280,7 @@ export function DataValidationDetail() {
validResult={validator.validatorFormula(localRule)}
unitId={unitId}
subUnitId={subUnitId}
ruleId={ruleId}
/>
)
: null}
@@ -37,6 +37,8 @@ export function CustomFormulaInput(props: IFormulaInputProps) {
});
}}
onlyInputFormula
openForSheetUnitId={unitId}
openForSheetSubUnitId={subUnitId}
/>
</FormLayout>
);
@@ -53,7 +53,6 @@
width: 92px;
margin-left: 4px;
margin-right: 8px;
height: 31px;
}
&-color-select-panel {
@@ -80,6 +79,5 @@
height: 16px;
box-sizing: border-box;
font-size: 16px;
margin-top: 2px;
}
}
@@ -14,24 +14,19 @@
* limitations under the License.
*/
import { RangeSelector, useEvent } from '@univerjs/ui';
import { TextEditor, useEvent, useObservable } from '@univerjs/ui';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { DraggableList, FormLayout, Input, Radio, RadioGroup, Select } from '@univerjs/design';
import { deserializeRangeWithSheet, isReferenceStringWithEffectiveColumn, serializeRangeWithSheet } from '@univerjs/engine-formula';
import { useDependency } from '@wendellhu/redi/react-bindings';
import type { IRange, Workbook } from '@univerjs/core';
import { createInternalEditorID, IUniverInstanceService, LocaleService, Tools, UniverInstanceType } from '@univerjs/core';
import type { IFormulaInputProps } from '@univerjs/data-validation';
import { createInternalEditorID, DataValidationType, isFormulaString, LocaleService, Tools } from '@univerjs/core';
import { DataValidationModel, DataValidatorRegistryService, type IFormulaInputProps } from '@univerjs/data-validation';
import { DeleteSingle, IncreaseSingle, SequenceSingle } from '@univerjs/icons';
import cs from 'clsx';
import { deserializeListOptions, getSheetRangeValueSet, serializeListOptions } from '../../validators/util';
import { deserializeListOptions, serializeListOptions } from '../../validators/util';
import { DROP_DOWN_DEFAULT_COLOR } from '../../common/const';
import type { ListValidator } from '../../validators';
import styles from './index.module.less';
function isRangeInValid(range: IRange) {
return Number.isNaN(range.startColumn) || Number.isNaN(range.endColumn) || Number.isNaN(range.startRow) || Number.isNaN(range.endRow);
}
const DEFAULT_COLOR_PRESET = [
'#FFFFFF',
'#FEE7E7',
@@ -146,24 +141,50 @@ const Template = (props: { item: IDropdownItem; commonProps: any; style?: React.
);
};
export function ListFormulaInput(props: IFormulaInputProps) {
const { value, onChange: _onChange = () => { }, unitId, subUnitId, validResult, showError } = props;
const { value, onChange: _onChange = () => { }, unitId, subUnitId, validResult, showError, ruleId } = props;
const { formula1 = '', formula2 = '' } = value || {};
const containerRef = useRef<HTMLDivElement>(null);
const [isRefRange, setIsRefRange] = useState(() => isReferenceStringWithEffectiveColumn(formula1) ? '1' : '0');
const [refRange, setRefRange] = useState(isRefRange === '1' ? formula1 : '');
const [isFormulaStr, setIsFormulaStr] = useState(() => isFormulaString(formula1) ? '1' : '0');
const [formulaStr, setFormulaStr] = useState(isFormulaStr === '1' ? formula1 : '=');
const [formulaStrCopy, setFormulaStrCopy] = useState(isFormulaStr === '1' ? formula1 : '=');
const localeService = useDependency(LocaleService);
const univerInstanceService = useDependency(IUniverInstanceService);
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.SHEET)!;
const worksheet = workbook.getActiveSheet();
const dataValidatorRegistryService = useDependency(DataValidatorRegistryService);
const dataValidationModel = useDependency(DataValidationModel);
const [refColors, setRefColors] = useState(() => formula2.split(','));
const listValidator = dataValidatorRegistryService.getValidatorItem(DataValidationType.LIST) as ListValidator;
const [refOptions, setRefOptions] = useState<string[]>([]);
const [localError, setLocalError] = useState('');
const formula1Res = showError ? validResult?.formula1 : '';
const ruleChange = useObservable(dataValidationModel.ruleChange$);
const onChange = useEvent(_onChange);
useEffect(() => {
(async () => {
await new Promise<any>((resolve) => {
setTimeout(() => resolve(true), 100);
});
const rule = dataValidationModel.getRuleById(unitId, subUnitId, ruleId);
const formula1 = rule?.formula1;
if (isFormulaString(formula1) && listValidator && rule) {
const res = await listValidator.getListAsync(rule, unitId, subUnitId);
setRefOptions(res);
}
})();
}, [dataValidationModel, ruleChange, listValidator, ruleId, subUnitId, unitId]);
useEffect(() => {
if (isFormulaString(formula1) && formula1 !== formulaStrCopy) {
setFormulaStr(formula1);
setFormulaStrCopy(formulaStrCopy);
}
}, [formulaStrCopy, formula1]);
const [strList, setStrList] = useState<IDropdownItem[]>(() => {
const strOptions = isRefRange !== '1' ? deserializeListOptions(formula1) : [];
const strOptions = isFormulaStr !== '1' ? deserializeListOptions(formula1) : [];
const strColors = formula2.split(',');
return strOptions.map((label, i) => ({
label,
@@ -193,13 +214,6 @@ export function ListFormulaInput(props: IFormulaInputProps) {
}
};
const refOptions = useMemo(() => getSheetRangeValueSet(
deserializeRangeWithSheet(refRange),
univerInstanceService,
workbook.getUnitId(),
worksheet.getSheetId()
), [refRange, univerInstanceService, workbook, worksheet]);
const colorList = formula2.split(',');
const refFinalList: IDropdownItem[] = useMemo(() => refOptions.map((label, i) => ({
@@ -259,43 +273,49 @@ export function ListFormulaInput(props: IFormulaInputProps) {
});
}, [strList, onChange]);
const updateFormula = useMemo(
() =>
async (str: string) => {
if (!isFormulaString(str)) {
onChange?.({
formula1: '',
formula2,
});
return;
}
onChange?.({
formula1: isFormulaString(str) ? str : '',
formula2,
});
},
[formula2, onChange]
);
return (
<>
<FormLayout label={localeService.t('dataValidation.list.options')}>
<RadioGroup value={isRefRange} onChange={(v) => setIsRefRange(v as string)}>
<RadioGroup value={isFormulaStr} onChange={(v) => setIsFormulaStr(v as string)}>
<Radio value="0">{localeService.t('dataValidation.list.customOptions')}</Radio>
<Radio value="1">{localeService.t('dataValidation.list.refOptions')}</Radio>
</RadioGroup>
</FormLayout>
{isRefRange === '1'
{isFormulaStr === '1'
? (
<>
<FormLayout error={formula1Res}>
<RangeSelector
<FormLayout error={formula1Res || localError}>
<TextEditor
id={createInternalEditorID(`list-ref-range-${unitId}-${subUnitId}`)}
value={refRange}
value={formulaStr}
openForSheetUnitId={unitId}
openForSheetSubUnitId={subUnitId}
onChange={(ranges) => {
const range = ranges[0];
if (!range || isRangeInValid(range.range)) {
onChange?.({
formula1: '',
formula2,
});
setRefRange('');
} else {
const workbook = univerInstanceService.getUniverSheetInstance(range.unitId) ?? univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.SHEET)!;
const worksheet = workbook?.getSheetBySheetId(range.sheetId) ?? workbook.getActiveSheet();
const rangeStr = serializeRangeWithSheet(worksheet.getName(), range.range);
onChange?.({
formula1: rangeStr,
formula2,
});
setRefRange(rangeStr);
}
onlyInputFormula
onChange={async (newString) => {
const str = newString ?? '';
setFormulaStrCopy(str);
updateFormula(str);
}}
isSingleChoice
/>
</FormLayout>
<FormLayout>
@@ -309,12 +329,13 @@ export function ListFormulaInput(props: IFormulaInputProps) {
)
: (
<FormLayout error={formula1Res}>
<div ref={containerRef} style={{ margin: '-12px 0' }}>
<div ref={containerRef} style={{ marginTop: '-12px' }}>
<DraggableList
list={strList}
onListChange={setStrList}
rowHeight={32}
margin={[0, 12]}
draggableHandle=".draggableHandle"
itemRender={(item) => (
<Template
key={item.id}
@@ -71,6 +71,7 @@ export class DropdownMultipleWidget implements IBaseDataValidationWidget {
ctx.restore();
}
// eslint-disable-next-line max-lines-per-function
drawWith(ctx: UniverRenderingContext2D, info: ICellRenderContext, skeleton: SpreadsheetSkeleton, spreadsheets: Spreadsheet): void {
const { primaryWithCoord, row, col, style, data, subUnitId } = info;
const cellBounding = primaryWithCoord.isMergedMainCell ? primaryWithCoord.mergeInfo : primaryWithCoord;
@@ -92,7 +93,7 @@ export class DropdownMultipleWidget implements IBaseDataValidationWidget {
const { vt: _vt, ht } = style || {};
const vt = _vt ?? VerticalAlign.MIDDLE;
const cellValue = getCellValueOrigin(data) ?? '';
const items = validator.parseCellValue(cellValue, rule);
const items = validator.parseCellValue(cellValue);
const labelColorMap = validator.getListWithColorMap(rule);
const layout = layoutDropdowns(items, fontStyle, cellWidth, cellHeight);
this._drawDownIcon(ctx, cellBounding, cellWidth, cellHeight, vt);
@@ -164,9 +165,9 @@ export class DropdownMultipleWidget implements IBaseDataValidationWidget {
const cellWidth = cellBounding.endX - cellBounding.startX;
const cellHeight = cellBounding.endY - cellBounding.startY;
const cellValue = getCellValueOrigin(data) ?? '';
const { rule, validator: _validator } = validation;
const { validator: _validator } = validation;
const validator = _validator as ListMultipleValidator;
const items = validator.parseCellValue(cellValue, rule);
const items = validator.parseCellValue(cellValue);
const fontStyle = getFontStyleString(style ?? undefined);
const layout = layoutDropdowns(items, fontStyle, cellWidth, cellHeight);
return layout.cellAutoHeight;
@@ -73,8 +73,8 @@ export class SheetCanvasPopManagerService extends Disposable {
const bounding = engine.getCanvasElement().getBoundingClientRect();
const position: IBoundRectNoAngle = {
left: ((cellInfo.startX - scrollXY.x) * scaleX),
right: (cellInfo.endX - scrollXY.x) * scaleX,
left: ((cellInfo.startX - scrollXY.x) * scaleX) + bounding.left,
right: (cellInfo.endX - scrollXY.x) * scaleX + bounding.left,
top: ((cellInfo.startY - scrollXY.y) * scaleY) + bounding.top,
bottom: ((cellInfo.endY - scrollXY.y) * scaleY) + bounding.top,
};
@@ -39,7 +39,7 @@ const EDITOR_DEFAULT_POSITION = {
* Floating editor's container.
* @returns
*/
// eslint-disable-next-line max-lines-per-function
export const EditorContainer: React.FC<ICellIEditorProps> = () => {
const [state, setState] = useState({
...EDITOR_DEFAULT_POSITION,