Skip to content

Commit 308b2d5

Browse files
authored
Fix issue where cells where not recalculated after adding, removing and renaming sheets (handsontable#1570)
* Add unit test that reproduce the issue * Add changelog entry * Don't omit end address in RemoveSheetTransformer * Add npm script for npm audit * Add more unit tests * Fix linter in tests * Make SheetMapping store reserved sheet names * Adjust graphComparator * Adjust evaluator to handle not-added sheets correctly * Adjust tests in compute-hash-from-tokens.spec.ts * Adjust tests in parser.spec.ts * Fix numeric aggregation plugin * Make addSheet operation update the relevant dependency * Adjust tests in named-expressions.spec.ts * Adjust tests in mitting-events.spec.ts * Add complex range test scenarios * Remove vertices only if they are not referenced by existing sheets * Adjust tests in removing-sheet.spec.ts * Refactor: create SheetReferenceRegistrar * Remove sheet from AddressMapping if nothing else depends on it * Remove unused functions * Remove unused constructor parameters * Adjust range vertices after rename sheet * Update sheet strategy in AddressMapping if placeholder exists * Improve docs in SheetMapping * Refactor NumericAggregationPlugin * Rename function to more descriptive name * Refactor addressRepresentationConverters * Refactor Graph.ts * Refactor SheetReferenceRegistrar.ts * Refactor RangeMapping * Remove RemoveSheetTransformem which is not needed anymore * Rename FormulaCellVertex -> ScalarFormulaVertex and ArrayVertex -> ArrayFormulaVertex * Refactor DependencyGraph * Refactor Operations * Refactor DependencyGraph.removeSheet * Make sure renameSheet handles dependency graph correctly * Refactor DependencyGraph.mergeSheets() * Refactor DependencyGraph * Hande undoAddSheet and redoAddSheet * Hande undoRemoveSheet and redoRemoveSheet * Add tests for undo/redo renameSheet() * Handle undo for renameSheet() * Apply suggestions from agent review * Configure eslint to allow null assertions in test files * Remove placeholder sheets when not needed enymore * Apply suggestions after agentic code review of the test files * Test the cleanup of placeholder sheets
1 parent 8de58f9 commit 308b2d5

83 files changed

Lines changed: 4965 additions & 1435 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ module.exports = {
128128
'jest/no-standalone-expect': 'warn',
129129
'jest/no-test-prefixes': 'off',
130130
'jest/prefer-to-be': 'warn',
131-
'jest/prefer-to-have-length': 'warn',
131+
'jest/prefer-to-have-length': 'off',
132132
},
133133
overrides: [
134134
{
@@ -143,5 +143,11 @@ module.exports = {
143143
'sort-keys': ['error', 'asc'],
144144
}
145145
},
146+
{
147+
files: ['**/*.spec.ts'],
148+
rules: {
149+
'@typescript-eslint/no-non-null-assertion': 'off',
150+
}
151+
}
146152
],
147153
}

.github/workflows/audit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ jobs:
3232

3333
- name: Run audit
3434
run: |
35-
npm audit --omit='dev'
35+
npm run audit

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99

1010
### Fixed
1111

12+
- Fixed an issue where cells were not recalculated after adding, removing and renaming sheets. [#1116](https://github.com/handsontable/hyperformula/issues/1116)
1213
- Fixed an issue where overwriting a non-computed cell caused the `Value of the formula cell is not computed` error. [#1194](https://github.com/handsontable/hyperformula/issues/1194)
1314

1415
## [3.1.0] - 2025-10-14

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"test": "npm-run-all lint test:unit test:browser test:compatibility",
7979
"test:unit": "cross-env NODE_ICU_DATA=node_modules/full-icu jest",
8080
"test:watch": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch",
81-
"test:tdd": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch named-expressions",
81+
"test:tdd": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch adding-sheet",
8282
"test:coverage": "npm run test:unit -- --coverage",
8383
"test:logMemory": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --runInBand --logHeapUsage",
8484
"test:unit.ci": "cross-env NODE_ICU_DATA=node_modules/full-icu node --expose-gc ./node_modules/jest/bin/jest --forceExit",
@@ -93,6 +93,7 @@
9393
"benchmark:compare-benchmarks": "npm run tsnode test/performance/compare-benchmarks.ts",
9494
"lint": "eslint . --ext .js,.ts",
9595
"lint:fix": "eslint . --ext .js,.ts --fix",
96+
"audit": "npm audit --omit=dev",
9697
"clean": "rimraf coverage/ commonjs/ dist/ es/ languages/ lib/ typings/ test-jasmine/",
9798
"compile": "tsc",
9899
"check:licenses": "license-checker --production --excludePackages=\"[email protected]\" --onlyAllow=\"MIT; Apache-2.0; BSD-3-Clause; BSD-2-Clause; ISC; BSD; Unlicense\"",

src/BuildEngineFactory.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,17 @@ export class BuildEngineFactory {
8787
throw new SheetSizeLimitExceededError()
8888
}
8989
const sheetId = sheetMapping.addSheet(sheetName)
90-
addressMapping.autoAddSheet(sheetId, boundaries)
90+
addressMapping.addSheetAndSetStrategyBasedOnBoundaries(sheetId, boundaries, { throwIfSheetAlreadyExists: true })
9191
}
9292
}
9393

94-
const parser = new ParserWithCaching(config, functionRegistry, sheetMapping.get)
94+
const parser = new ParserWithCaching(
95+
config,
96+
functionRegistry,
97+
dependencyGraph.sheetReferenceRegistrar.ensureSheetRegistered.bind(dependencyGraph.sheetReferenceRegistrar)
98+
)
9599
lazilyTransformingAstService.parser = parser
96-
const unparser = new Unparser(config, buildLexerConfig(config), sheetMapping.fetchDisplayName, namedExpressions)
100+
const unparser = new Unparser(config, sheetMapping, namedExpressions)
97101
const dateTimeHelper = new DateTimeHelper(config)
98102
const numberLiteralHelper = new NumberLiteralHelper(config)
99103
const arithmeticHelper = new ArithmeticHelper(config, dateTimeHelper, numberLiteralHelper)
@@ -106,7 +110,7 @@ export class BuildEngineFactory {
106110
const clipboardOperations = new ClipboardOperations(config, dependencyGraph, operations)
107111
const crudOperations = new CrudOperations(config, operations, undoRedo, clipboardOperations, dependencyGraph, columnSearch, parser, cellContentParser, lazilyTransformingAstService, namedExpressions)
108112

109-
const exporter = new Exporter(config, namedExpressions, sheetMapping.fetchDisplayName, lazilyTransformingAstService)
113+
const exporter = new Exporter(config, namedExpressions, sheetMapping, lazilyTransformingAstService)
110114
const serialization = new Serialization(dependencyGraph, unparser, exporter)
111115

112116
const interpreter = new Interpreter(config, dependencyGraph, columnSearch, stats, arithmeticHelper, functionRegistry, namedExpressions, serialization, arraySizePredictor, dateTimeHelper)

src/Cell.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* Copyright (c) 2025 Handsoncode. All rights reserved.
44
*/
55

6-
import {ArrayVertex, CellVertex, FormulaCellVertex, ParsingErrorVertex, ValueCellVertex} from './DependencyGraph'
7-
import {FormulaVertex} from './DependencyGraph/FormulaCellVertex'
6+
import {ArrayFormulaVertex, CellVertex, ScalarFormulaVertex, ParsingErrorVertex, ValueCellVertex} from './DependencyGraph'
7+
import {FormulaVertex} from './DependencyGraph/FormulaVertex'
88
import {ErrorMessage} from './error-message'
99
import {
1010
EmptyValue,
@@ -59,14 +59,14 @@ export enum CellType {
5959
}
6060

6161
export const getCellType = (vertex: Maybe<CellVertex>, address: SimpleCellAddress): CellType => {
62-
if (vertex instanceof ArrayVertex) {
62+
if (vertex instanceof ArrayFormulaVertex) {
6363
if (vertex.isLeftCorner(address)) {
6464
return CellType.ARRAYFORMULA
6565
} else {
6666
return CellType.ARRAY
6767
}
6868
}
69-
if (vertex instanceof FormulaCellVertex || vertex instanceof ParsingErrorVertex) {
69+
if (vertex instanceof ScalarFormulaVertex || vertex instanceof ParsingErrorVertex) {
7070
return CellType.FORMULA
7171
}
7272
if (vertex instanceof ValueCellVertex) {
@@ -196,7 +196,12 @@ export interface SimpleCellAddress {
196196
}
197197

198198
export const simpleCellAddress = (sheet: number, col: number, row: number): SimpleCellAddress => ({sheet, col, row})
199-
export const invalidSimpleCellAddress = (address: SimpleCellAddress): boolean => (address.col < 0 || address.row < 0)
199+
200+
/**
201+
* Checks if the column or row id is negative.
202+
*/
203+
export const isColOrRowInvalid = (address: SimpleCellAddress): boolean => (address.col < 0 || address.row < 0)
204+
200205
export const movedSimpleCellAddress = (address: SimpleCellAddress, toSheet: number, toRight: number, toBottom: number): SimpleCellAddress => {
201206
return simpleCellAddress(toSheet, address.col + toRight, address.row + toBottom)
202207
}

src/ClipboardOperations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import {AbsoluteCellRange} from './AbsoluteCellRange'
7-
import {invalidSimpleCellAddress, simpleCellAddress, SimpleCellAddress} from './Cell'
7+
import {isColOrRowInvalid, simpleCellAddress, SimpleCellAddress} from './Cell'
88
import {RawCellContent} from './CellContentParser'
99
import {Config} from './Config'
1010
import {DependencyGraph} from './DependencyGraph'
@@ -119,7 +119,7 @@ export class ClipboardOperations {
119119
return
120120
}
121121

122-
if (invalidSimpleCellAddress(destinationLeftCorner) ||
122+
if (isColOrRowInvalid(destinationLeftCorner) ||
123123
!this.dependencyGraph.sheetMapping.hasSheetWithId(destinationLeftCorner.sheet)) {
124124
throw new InvalidArgumentsError('a valid target address.')
125125
}

src/CrudOperations.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import {AbsoluteCellRange} from './AbsoluteCellRange'
7-
import {invalidSimpleCellAddress, simpleCellAddress, SimpleCellAddress} from './Cell'
7+
import {isColOrRowInvalid, simpleCellAddress, SimpleCellAddress} from './Cell'
88
import {CellContent, CellContentParser, RawCellContent} from './CellContentParser'
99
import {ClipboardCell, ClipboardOperations} from './ClipboardOperations'
1010
import {Config} from './Config'
@@ -206,29 +206,35 @@ export class CrudOperations {
206206
this.ensureItIsPossibleToAddSheet(name)
207207
}
208208
this.undoRedo.clearRedoStack()
209-
const addedSheetName = this.operations.addSheet(name)
210-
this.undoRedo.saveOperation(new AddSheetUndoEntry(addedSheetName))
211-
return addedSheetName
209+
const { sheetName, sheetId } = this.operations.addSheet(name)
210+
this.undoRedo.saveOperation(new AddSheetUndoEntry(sheetName, sheetId))
211+
return sheetName
212212
}
213213

214214
public removeSheet(sheetId: number): void {
215215
this.ensureScopeIdIsValid(sheetId)
216216
this.undoRedo.clearRedoStack()
217217
this.clipboardOperations.abortCut()
218-
const originalName = this.sheetMapping.fetchDisplayName(sheetId)
218+
const originalName = this.sheetMapping.getSheetNameOrThrowError(sheetId)
219219
const oldSheetContent = this.operations.getSheetClipboardCells(sheetId)
220-
const {version, scopedNamedExpressions} = this.operations.removeSheet(sheetId)
221-
this.undoRedo.saveOperation(new RemoveSheetUndoEntry(originalName, sheetId, oldSheetContent, scopedNamedExpressions, version))
220+
const scopedNamedExpressions = this.operations.removeSheet(sheetId)
221+
this.undoRedo.saveOperation(new RemoveSheetUndoEntry(originalName, sheetId, oldSheetContent, scopedNamedExpressions))
222222
}
223223

224224
public renameSheet(sheetId: number, newName: string): Maybe<string> {
225225
this.ensureItIsPossibleToRenameSheet(sheetId, newName)
226-
const oldName = this.operations.renameSheet(sheetId, newName)
227-
if (oldName !== undefined) {
226+
const { previousDisplayName, version, mergedPlaceholderSheetId } = this.operations.renameSheet(sheetId, newName)
227+
if (previousDisplayName !== undefined) {
228228
this.undoRedo.clearRedoStack()
229-
this.undoRedo.saveOperation(new RenameSheetUndoEntry(sheetId, oldName, newName))
229+
this.undoRedo.saveOperation(new RenameSheetUndoEntry(
230+
sheetId,
231+
previousDisplayName,
232+
newName,
233+
version,
234+
mergedPlaceholderSheetId,
235+
))
230236
}
231-
return oldName
237+
return previousDisplayName
232238
}
233239

234240
public clearSheet(sheetId: number): void {
@@ -495,8 +501,8 @@ export class CrudOperations {
495501

496502
if (
497503
!this.sheetMapping.hasSheetWithId(sheet)
498-
|| invalidSimpleCellAddress(sourceStart)
499-
|| invalidSimpleCellAddress(targetStart)
504+
|| isColOrRowInvalid(sourceStart)
505+
|| isColOrRowInvalid(targetStart)
500506
|| !isPositiveInteger(numberOfRows)
501507
|| (targetRow <= startRow + numberOfRows && targetRow >= startRow)
502508
) {
@@ -522,8 +528,8 @@ export class CrudOperations {
522528

523529
if (
524530
!this.sheetMapping.hasSheetWithId(sheet)
525-
|| invalidSimpleCellAddress(sourceStart)
526-
|| invalidSimpleCellAddress(targetStart)
531+
|| isColOrRowInvalid(sourceStart)
532+
|| isColOrRowInvalid(targetStart)
527533
|| !isPositiveInteger(numberOfColumns)
528534
|| (targetColumn <= startColumn + numberOfColumns && targetColumn >= startColumn)
529535
) {
@@ -552,14 +558,14 @@ export class CrudOperations {
552558
throw new NoSheetWithIdError(sheetId)
553559
}
554560

555-
const existingSheetId = this.sheetMapping.get(name)
561+
const existingSheetId = this.sheetMapping.getSheetId(name)
556562
if (existingSheetId !== undefined && existingSheetId !== sheetId) {
557563
throw new SheetNameAlreadyTakenError(name)
558564
}
559565
}
560566

561567
public ensureItIsPossibleToChangeContent(address: SimpleCellAddress): void {
562-
if (invalidSimpleCellAddress(address)) {
568+
if (isColOrRowInvalid(address)) {
563569
throw new InvalidAddressError(address)
564570
}
565571
if (!this.sheetMapping.hasSheetWithId(address.sheet)) {

0 commit comments

Comments
 (0)