Skip to content

Commit

Permalink
feat(sheets): support data validation (#1676)
Browse files Browse the repository at this point in the history
  • Loading branch information
weird94 committed Apr 2, 2024
1 parent a573166 commit 9961b32
Show file tree
Hide file tree
Showing 189 changed files with 13,445 additions and 444 deletions.
2 changes: 2 additions & 0 deletions examples/package.json
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/data-validation": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/docs": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
Expand All @@ -23,6 +24,7 @@
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-conditional-formatting": "workspace:*",
"@univerjs/sheets-conditional-formatting-ui": "workspace:*",
"@univerjs/sheets-data-validation": "workspace:*",
"@univerjs/sheets-find-replace": "workspace:*",
"@univerjs/sheets-formula": "workspace:*",
"@univerjs/sheets-numfmt": "workspace:*",
Expand Down
87 changes: 85 additions & 2 deletions examples/src/data/sheets/demo/default-workbook-data-demo.ts
Expand Up @@ -15,8 +15,9 @@
*/

import type { IDocumentData, IWorkbookData } from '@univerjs/core';
import { BooleanNumber, LocaleType } from '@univerjs/core';
import { BooleanNumber, DataValidationErrorStyle, DataValidationOperator, DataValidationType, LocaleType } from '@univerjs/core';

import { DATA_VALIDATION_PLUGIN_NAME } from '@univerjs/sheets-data-validation';
import { PAGE5_RICHTEXT_1 } from '../../slides/rich-text/page5-richtext1';

const richTextDemo: IDocumentData = {
Expand Down Expand Up @@ -100,6 +101,78 @@ const richTextDemo1: IDocumentData = {
},
};

const dataValidation = [
{
uid: 'xxx-1',
type: DataValidationType.DECIMAL,
ranges: [{
startRow: 0,
endRow: 5,
startColumn: 0,
endColumn: 2,
}],
operator: DataValidationOperator.GREATER_THAN,
formula1: '111',
errorStyle: DataValidationErrorStyle.STOP,
},
{
uid: 'xxx-0',
type: DataValidationType.DATE,
ranges: [{
startRow: 0,
endRow: 5,
startColumn: 3,
endColumn: 5,
}],
operator: DataValidationOperator.GREATER_THAN,
formula1: '100',
errorStyle: DataValidationErrorStyle.STOP,
},
{
uid: 'xxx-2',
type: DataValidationType.CHECKBOX,
ranges: [{
startRow: 6,
endRow: 10,
startColumn: 0,
endColumn: 5,
}],
},
{
uid: 'xxx-3',
type: DataValidationType.LIST,
ranges: [{
startRow: 11,
endRow: 15,
startColumn: 0,
endColumn: 5,
}],
formula1: '1,2,3,hahaha',
},
{
uid: 'xxx-4',
type: DataValidationType.CUSTOM,
ranges: [{
startRow: 16,
endRow: 20,
startColumn: 0,
endColumn: 5,
}],
formula1: '=A1',
},
{
uid: 'xxx-5',
type: DataValidationType.LIST_MULTIPLE,
ranges: [{
startRow: 21,
endRow: 21,
startColumn: 0,
endColumn: 0,
}],
formula1: '1,2,3,4,5,哈哈哈哈',
},
];

export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'workbook-01',
locale: LocaleType.ZH_CN,
Expand Down Expand Up @@ -13875,7 +13948,7 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'sheet-0011',
tabColor: '',
hidden: 0,
rowCount: 1000000,
rowCount: 1000,
columnCount: 20,
zoomRatio: 1,
cellData: {
Expand Down Expand Up @@ -15147,9 +15220,11 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = {
21: {
0: {
s: 'u5otPe',
v: '1,2',
},
1: {
s: 'u5otPe',
v: '1,2,3',
},
2: {
s: 'u5otPe',
Expand Down Expand Up @@ -23303,6 +23378,14 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = {
// },
// },
},
resources: [
{
name: DATA_VALIDATION_PLUGIN_NAME,
data: JSON.stringify({
'sheet-0011': dataValidation,
}),
},
],
// namedRanges: [
// {
// namedRangeId: 'named-rang',
Expand Down
7 changes: 7 additions & 0 deletions examples/src/sheets/main.ts
Expand Up @@ -30,6 +30,8 @@ import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor';
import { UniverUIPlugin } from '@univerjs/ui';
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation';
import { SheetsConditionalFormattingUiPlugin } from '@univerjs/sheets-conditional-formatting-ui';

import { DebuggerPlugin } from '../plugins/debugger';
Expand Down Expand Up @@ -78,6 +80,11 @@ univer.registerPlugin(UniverRPCMainThreadPlugin, {

// find replace
univer.registerPlugin(UniverFindReplacePlugin);
// univer.registerPlugin(UniverSheetsFindPlugin);

// data validation
univer.registerPlugin(UniverDataValidationPlugin);
univer.registerPlugin(UniverSheetsDataValidationPlugin);
univer.registerPlugin(UniverSheetsFindReplacePlugin);

// create univer sheet instance
Expand Down
81 changes: 81 additions & 0 deletions packages/core/src/common/equal.ts
@@ -0,0 +1,81 @@
/**
* 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.
*/

import { Rectangle } from '../shared/rectangle';
import type { IRange, IUnitRange } from '../types/interfaces';

/**
* 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 isRangesEqual = (oldRanges: IRange[], ranges: IRange[]) => {
return ranges.length === oldRanges.length && !oldRanges.some((oldRange) => ranges.some((range) => !Rectangle.equals(range, oldRange)));
};

export const isUnitRangesEqual = (oldRanges: IUnitRange[], ranges: IUnitRange[]) => {
return ranges.length === oldRanges.length && oldRanges.every((oldRange, i) => {
const current = ranges[i];
return current.unitId === oldRange.unitId && current.sheetId === oldRange.sheetId && Rectangle.equals(oldRange.range, current.range);
});
};

export function shallowEqual(objA: any, objB: any) {
if (Object.is(objA, objB)) {
return true;
}

if (typeof objA !== 'object' || !objA || typeof objB !== 'object' || !objB) {
return false;
}

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

if (keysA.length !== keysB.length) {
return false;
}

const bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);

// Test for A's keys different from B.
for (let idx = 0; idx < keysA.length; idx++) {
const key = keysA[idx];

if (!bHasOwnProperty(key)) {
return false;
}

const valueA = objA[key];
const valueB = objB[key];
if (valueA !== valueB) {
return false;
}
}

return true;
}
10 changes: 10 additions & 0 deletions packages/core/src/index.ts
Expand Up @@ -16,6 +16,7 @@

import { installShims } from './common/shims';

export { shallowEqual, isRangesEqual, isUnitRangesEqual } from './common/equal';
export * from './basics';
export { dedupe, remove, rotate, groupBy } from './common/array';
export {
Expand Down Expand Up @@ -151,4 +152,13 @@ export { getSheetBlocksFromSnapshot } from './services/snapshot/snapshot-transfo
export { isBlackColor, isWhiteColor } from './shared/color/color-kit';
export { cellToRange } from './shared/common';

export type { IDataValidationRule, IDataValidationRuleBase, IDataValidationRuleInfo, IDataValidationRuleOptions, ISheetDataValidationRule } from './types/interfaces/i-data-validation';
export type { ICellCustomRender, ICellRenderContext } from './types/interfaces/i-cell-custom-render';

export { DataValidationErrorStyle } from './types/enum/data-validation-error-style';
export { DataValidationImeMode } from './types/enum/data-validation-ime-mode';
export { DataValidationOperator } from './types/enum/data-validation-operator';
export { DataValidationType } from './types/enum/data-validation-type';
export { DataValidationStatus } from './types/enum/data-validation-status';

installShims();
11 changes: 10 additions & 1 deletion packages/core/src/shared/common.ts
Expand Up @@ -522,7 +522,16 @@ export function getDocsUpdateBody(model: IDocumentData, segmentId?: string) {

export function isValidRange(range: IRange): boolean {
const { startRow, endRow, startColumn, endColumn } = range;
if (startRow < 0 || startColumn < 0 || endRow < 0 || endColumn < 0) {
if (
startRow < 0
|| startColumn < 0
|| endRow < 0
|| endColumn < 0
|| Number.isNaN(startRow)
|| Number.isNaN(endRow)
|| Number.isNaN(startColumn)
|| Number.isNaN(endColumn)
) {
return false;
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/shared/index.ts
Expand Up @@ -37,3 +37,4 @@ export * from './sort-rules';
export * from './tools';
export * from './types';
export * from './debounce';
export { queryObjectMatrix } from './object-matrix-query';
101 changes: 101 additions & 0 deletions packages/core/src/shared/object-matrix-query.ts
@@ -0,0 +1,101 @@
/**
* 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.
*/

import type { Nullable } from 'vitest';
import { Range } from '../sheets/range';
import type { IRange } from '../types/interfaces';
import type { ObjectMatrix } from './object-matrix';

function maximalRectangle<T>(matrix: T[][], match: (val: T) => boolean) {
if (matrix.length === 0 || matrix[0].length === 0) return null;

const heights = new Array(matrix[0].length).fill(0);
let maxArea = 0;
let maxRect = null;

for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[0].length; col++) {
heights[col] = match(matrix[row][col]) ? heights[col] + 1 : 0;
}

const areaWithRect = largestRectangleArea(heights);
if (areaWithRect.area > maxArea) {
maxArea = areaWithRect.area;
// Adjust the rectangle's top row to the current row minus the height plus one
maxRect = {
startColumn: areaWithRect.start,
startRow: row - areaWithRect.height + 1,
endColumn: areaWithRect.end,
endRow: row,
};
}
}

return maxRect;
}

function largestRectangleArea(heights: number[]) {
const stack: number[] = [];
let maxArea = 0;
let maxRect = { area: 0, height: 0, start: 0, end: 0 };
let index = 0;

while (index < heights.length) {
if (stack.length === 0 || heights[index] >= heights[stack[stack.length - 1]]) {
stack.push(index++);
} else {
const height = heights[stack.pop()!];
const width = stack.length === 0 ? index : index - stack[stack.length - 1] - 1;
if (height * width > maxArea) {
maxArea = height * width;
maxRect = { area: maxArea, height, start: stack.length === 0 ? 0 : stack[stack.length - 1] + 1, end: index - 1 };
}
}
}

while (stack.length > 0) {
const height = heights[stack.pop()!];
const width = stack.length === 0 ? index : index - stack[stack.length - 1] - 1;
if (height * width > maxArea) {
maxArea = height * width;
maxRect = { area: maxArea, height, start: stack.length === 0 ? 0 : stack[stack.length - 1] + 1, end: index - 1 };
}
}

return maxRect;
}

function resetMatrix<T>(matrix: Nullable<T>[][], range: IRange) {
Range.foreach(range, (row, col) => {
matrix[row][col] = undefined;
});
}

export function queryObjectMatrix<T>(matrix: ObjectMatrix<T>, match: (value: T) => boolean) {
const arrayMatrix = matrix.toFullArray();
const results: IRange[] = [];
while (true) {
const rectangle = maximalRectangle(arrayMatrix, match);
if (!rectangle) {
break;
}

results.push(rectangle);
resetMatrix(arrayMatrix, rectangle);
}

return results;
}

0 comments on commit 9961b32

Please sign in to comment.