mirror of
https://github.com/dream-num/univer.git
synced 2026-09-24 15:32:18 +08:00
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:
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user