From 79e2c9ff4b4345ef3c7a5a465b8a608b6de3e4bf Mon Sep 17 00:00:00 2001 From: wpxp123456 <2677556700@qq.com> Date: Tue, 26 May 2026 16:10:02 +0800 Subject: [PATCH] fix: ignore custom metadata when checking empty cells (#6957) --- .../src/sheets/__tests__/cell-data.spec.ts | 1 + packages/core/src/sheets/typedef.ts | 6 +- .../engine-formula/src/basics/is-null-cell.ts | 44 -------- .../reference-object/base-reference-object.ts | 5 +- .../src/services/runtime.service.ts | 9 +- .../__tests__/spreadsheet.integration.spec.ts | 4 + .../set-range-custom-metadata.command.spec.ts | 103 ++++++++++++++++++ 7 files changed, 115 insertions(+), 57 deletions(-) delete mode 100644 packages/engine-formula/src/basics/is-null-cell.ts create mode 100644 packages/sheets/src/commands/commands/__tests__/set-range-custom-metadata.command.spec.ts diff --git a/packages/core/src/sheets/__tests__/cell-data.spec.ts b/packages/core/src/sheets/__tests__/cell-data.spec.ts index ab5df88c6f..7393ebfa74 100644 --- a/packages/core/src/sheets/__tests__/cell-data.spec.ts +++ b/packages/core/src/sheets/__tests__/cell-data.spec.ts @@ -102,6 +102,7 @@ describe('Test cell data', () => { expect(isNullCell({ si: null })).toBeTruthy(); expect(isNullCell({ si: undefined })).toBeTruthy(); expect(isNullCell({ si: '' })).toBeTruthy(); + expect(isNullCell({ custom: { key: 'value' } })).toBeTruthy(); expect(isNullCell({ v: 1 })).toBeFalsy(); expect(isNullCell({ p: DOCUMENT_DATA })).toBeFalsy(); diff --git a/packages/core/src/sheets/typedef.ts b/packages/core/src/sheets/typedef.ts index 542c0d3b13..d8ab3ab64f 100644 --- a/packages/core/src/sheets/typedef.ts +++ b/packages/core/src/sheets/typedef.ts @@ -356,7 +356,7 @@ export function isNullCell(cell: Nullable) { return true; } - const { v, f, si, p, custom } = cell; + const { v, f, si, p } = cell; if (!(v == null || (typeof v === 'string' && v.length === 0))) { return false; @@ -370,10 +370,6 @@ export function isNullCell(cell: Nullable) { return false; } - if (custom != null) { - return false; - } - return true; } diff --git a/packages/engine-formula/src/basics/is-null-cell.ts b/packages/engine-formula/src/basics/is-null-cell.ts deleted file mode 100644 index a3152be67c..0000000000 --- a/packages/engine-formula/src/basics/is-null-cell.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ICellData, Nullable } from '@univerjs/core'; - -/** - * Examine if a cell is empty (null) in the formula-related modules. This is not - * interchangable with {@link isNullCell} from the core package, because for - * formulas, "custom" field is meaningless. - */ -export function isNullCellForFormula(cell: Nullable) { - if (cell == null) { - return true; - } - - const { v, f, si, p } = cell; - - if (!(v == null || (typeof v === 'string' && v.length === 0))) { - return false; - } - - if ((f != null && f.length > 0) || (si != null && si.length > 0)) { - return false; - } - - if (p != null) { - return false; - } - - return true; -} diff --git a/packages/engine-formula/src/engine/reference-object/base-reference-object.ts b/packages/engine-formula/src/engine/reference-object/base-reference-object.ts index 9c7f8b8254..8c657d1952 100644 --- a/packages/engine-formula/src/engine/reference-object/base-reference-object.ts +++ b/packages/engine-formula/src/engine/reference-object/base-reference-object.ts @@ -17,10 +17,9 @@ import type { ICellData, IRange, Nullable } from '@univerjs/core'; import type { IArrayFormulaRangeType, IRuntimeUnitDataType, IUnitData, IUnitSheetNameMap, IUnitStylesData } from '../../basics/common'; import type { BaseValueObject, IArrayValueObject } from '../value-object/base-value-object'; -import { CellValueType, isTextFormat, moveRangeByOffset, ObjectMatrix } from '@univerjs/core'; +import { CellValueType, isNullCell, isTextFormat, moveRangeByOffset, ObjectMatrix } from '@univerjs/core'; import { FormulaAstLRU } from '../../basics/cache-lru'; import { ERROR_TYPE_SET, ErrorType } from '../../basics/error-type'; -import { isNullCellForFormula } from '../../basics/is-null-cell'; import { ObjectClassType } from '../../basics/object-class-type'; import { getCellValue } from '../utils/cell'; import { getRuntimeFeatureCell } from '../utils/get-runtime-feature-cell'; @@ -173,7 +172,7 @@ export class BaseReferenceObject extends ObjectClassType { const cell = this.getCellData(r, c)!; let result: Nullable = false; - if (isNullCellForFormula(cell)) { + if (isNullCell(cell)) { result = callback(null, r, c); continue; } diff --git a/packages/engine-formula/src/services/runtime.service.ts b/packages/engine-formula/src/services/runtime.service.ts index 2afe480bb1..b2fff5b7b7 100644 --- a/packages/engine-formula/src/services/runtime.service.ts +++ b/packages/engine-formula/src/services/runtime.service.ts @@ -29,11 +29,10 @@ import type { BaseReferenceObject, FunctionVariantType } from '../engine/referen import type { ArrayValueObject } from '../engine/value-object/array-value-object'; import type { BaseValueObject } from '../engine/value-object/base-value-object'; import type { StringValueObject } from '../engine/value-object/primitive-object'; -import { createIdentifier, Disposable, ObjectMatrix } from '@univerjs/core'; +import { createIdentifier, Disposable, isNullCell, ObjectMatrix } from '@univerjs/core'; import { isInDirtyRange } from '../basics/dirty'; import { ErrorType } from '../basics/error-type'; import { CELL_INVERTED_INDEX_CACHE } from '../basics/inverted-index-cache'; -import { isNullCellForFormula } from '../basics/is-null-cell'; import { FORMULA_REF_TO_ARRAY_CACHE } from '../engine/reference-object/base-reference-object'; import { getRuntimeFeatureCell } from '../engine/utils/get-runtime-feature-cell'; import { clearNumberFormatTypeCache, clearStringToNumberPatternCache } from '../engine/utils/numfmt-kit'; @@ -853,10 +852,10 @@ export class FormulaRuntimeService extends Disposable implements IFormulaRuntime const isPreviousCellOfCurrentArrayFormula = this._arrayCellHasData(arrayDataCell) && this._isInArrayFormulaRange(previousArrayFormulaRange, r, c) && (currentCell == null || this._isSameCellValue(currentCell, arrayDataCell)); - const hasRuntimeCell = !isNullCellForFormula(cell); + const hasRuntimeCell = !isNullCell(cell); const isInOtherArrayFormulaRange = this._isInOtherArrayFormulaRange(formulaUnitId, formulaSheetId, formulaRow, formulaColumn, r, c); - const currentCellBlocks = !isNullCellForFormula(currentCell) && !isPreviousCellOfCurrentArrayFormula; - const featureCellBlocks = !isNullCellForFormula(featureCell); + const currentCellBlocks = !isNullCell(currentCell) && !isPreviousCellOfCurrentArrayFormula; + const featureCellBlocks = !isNullCell(featureCell); // arrayDataCell may display 0 as {v: null}. Although it is an empty cell, it is considered to have a value. if ( diff --git a/packages/engine-render/src/components/sheets/__tests__/spreadsheet.integration.spec.ts b/packages/engine-render/src/components/sheets/__tests__/spreadsheet.integration.spec.ts index aa2e69e772..575be9b1d2 100644 --- a/packages/engine-render/src/components/sheets/__tests__/spreadsheet.integration.spec.ts +++ b/packages/engine-render/src/components/sheets/__tests__/spreadsheet.integration.spec.ts @@ -91,6 +91,7 @@ const workbookDataFactory = (): IWorkbookData => ({ 0: { v: 'A1' }, 1: { v: 'very-long-text-for-overflow-path', s: 'style-bg-border' }, 2: { v: 'wrapped line text', s: 'style-bg-border' }, + 4: { s: 'style-bg-border', custom: { key: 'value' } }, }, 1: { 1: { v: 'rotate-text', s: 'style-rotate' }, @@ -255,6 +256,9 @@ describe('spreadsheet integration', () => { })); expect(skeleton.rowColumnSegment.endRow).toBeGreaterThanOrEqual(0); expect(skeleton.stylesCache.fontMatrix.getSizeOf()).toBeGreaterThan(0); + expect(skeleton.stylesCache.border?.getValue(0, 4)).toBeTruthy(); + expect(skeleton.stylesCache.fontMatrix.getValue(0, 4)).toBeUndefined(); + expect(skeleton.overflowCache.getValue(0, 4)).toBeUndefined(); const autoHeights = skeleton.calculateAutoHeightInRange([{ startRow: 0, endRow: 6, startColumn: 0, endColumn: 3, rangeType: RANGE_TYPE.NORMAL }]); expect(autoHeights.length).toBeGreaterThan(0); diff --git a/packages/sheets/src/commands/commands/__tests__/set-range-custom-metadata.command.spec.ts b/packages/sheets/src/commands/commands/__tests__/set-range-custom-metadata.command.spec.ts new file mode 100644 index 0000000000..c66b072b85 --- /dev/null +++ b/packages/sheets/src/commands/commands/__tests__/set-range-custom-metadata.command.spec.ts @@ -0,0 +1,103 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IWorkbookData, Univer } from '@univerjs/core'; +import { BorderStyleTypes, CellValueType, ICommandService, IUniverInstanceService, LocaleType, RANGE_TYPE } from '@univerjs/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation'; +import { SetRangeCustomMetadataCommand } from '../set-range-custom-metadata.command'; +import { createCommandTestBed } from './create-command-test-bed'; + +const WORKBOOK_DATA: IWorkbookData = { + id: 'test', + appVersion: '3.0.0-alpha', + locale: LocaleType.ZH_CN, + name: '', + sheetOrder: ['sheet1'], + styles: {}, + sheets: { + sheet1: { + id: 'sheet1', + name: 'sheet1', + cellData: { + 0: { + 2: { + s: { + bd: { + t: { s: BorderStyleTypes.THIN, cl: { rgb: '#000000' } }, + r: { s: BorderStyleTypes.THIN, cl: { rgb: '#000000' } }, + b: { s: BorderStyleTypes.THIN, cl: { rgb: '#000000' } }, + l: { s: BorderStyleTypes.THIN, cl: { rgb: '#000000' } }, + }, + }, + t: CellValueType.STRING, + }, + }, + }, + }, + }, +}; + +describe('SetRangeCustomMetadataCommand', () => { + let univer: Univer; + let commandService: ICommandService; + let univerInstanceService: IUniverInstanceService; + + beforeEach(() => { + const testBed = createCommandTestBed(WORKBOOK_DATA); + univer = testBed.univer; + commandService = testBed.get(ICommandService); + univerInstanceService = testBed.get(IUniverInstanceService); + + commandService.registerCommand(SetRangeCustomMetadataCommand); + commandService.registerCommand(SetRangeValuesMutation); + }); + + afterEach(() => univer.dispose()); + + it('preserves existing cell type and border style when setting metadata on a styled empty cell', async () => { + const result = await commandService.executeCommand(SetRangeCustomMetadataCommand.id, { + unitId: 'test', + subUnitId: 'sheet1', + range: { + startRow: 0, + endRow: 0, + startColumn: 2, + endColumn: 2, + rangeType: RANGE_TYPE.NORMAL, + }, + customMetadata: { + custom: { key: 'value' }, + }, + }); + + const workbook = univerInstanceService.getUniverSheetInstance('test')!; + const cell = workbook.getSheetBySheetId('sheet1')!.getCellRaw(0, 2); + const style = workbook.getStyles().getStyleByCell(cell); + + expect(result).toBe(true); + expect(cell?.custom).toEqual({ key: 'value' }); + expect(cell?.t).toBe(CellValueType.STRING); + expect(style).toMatchObject({ + bd: { + t: { s: BorderStyleTypes.THIN }, + r: { s: BorderStyleTypes.THIN }, + b: { s: BorderStyleTypes.THIN }, + l: { s: BorderStyleTypes.THIN }, + }, + }); + }); +});