fix(editor): fix formula $-substitution corruption and sorted-grid row/formula desync #311

Merged
allan merged 10 commits from version7-13-fix into version7-13 2026-09-03 09:21:57 +00:00
Owner

Intent

A PR review flagged two blocking correctness bugs: String.replace's special $-pattern handling could silently corrupt a formula's DC.ORIG_VALUE/DC.USER_NAME substitution whenever the underlying cell data or username contained a literal $, and afterChange was indexing dataSource with Handsontable's visual row instead of translating to physical first, desyncing the EDIT_STATUS cell and "overwritten" comment sync on a sorted grid. Manually reproducing the second issue surfaced a third, unrelated Handsontable library bug — updateSettings() corrupts formula cell references (#REF!) whenever a sort is active, independent of what the call actually changes — which needed its own fix to unblock verification.

Implementation

  • $-substitution corruption: substituteBoundedToken's text.replace(pattern, replacement) used a string replacement, letting String.replace interpret $&/$`/$'/$<digit> in arbitrary cell data. Switched to a function replacement (() => replacement), whose return value is used verbatim — the single shared fix point, since substituteColumnReferences.ts calls the same function.
  • Sorted-grid row desync: afterChange now translates the visual row via hot.toPhysicalRow() before calling syncOverwrittenCommentForCell/updateEditStatusForRow (matching beforeChange's existing pattern). Those two methods, whose established contract is a physical row, now translate back to visual via hot.toVisualRow() for every Handsontable/comments-plugin API call — fixing only the call site would have moved the bug rather than closed it.
  • updateSettings() formula corruption: reproduced directly in Karma — a bare {} settings object, called while a sort is active, is enough to corrupt formula cells. A new updateSettingsSortSafe() wrapper clears the sort, calls updateSettings(), and restores it immediately after. Applied to every updateSettings() call site reachable during an active edit session (editTable, cancelEdit, getPendingExcelPreview, discardPendingExcel, cancelSubmit, checkSave, closeRecordEdit, confirmRecordEdit, applyDisplayColHeaders, grid-resize) except the very first call at initial table load, where nothing can be sorted yet.
  • Minor: the native paste listener added on hot.rootElement is now stored and removed in ngOnDestroy, closing a listener leak the same review flagged.

Changes

  • parseFormulaRule.ts / .spec.ts, substituteColumnReferences.spec.ts — function-replacement fix plus $&/$`/$'/$<digit> regression tests.
  • editor.component.tsafterChange row translation, syncOverwrittenCommentForCell/updateEditStatusForRow visual-row translation, new updateSettingsSortSafe() wrapper applied across all reachable updateSettings() call sites, pasteListener field + cleanup in ngOnDestroy.
  • sortedGridRowSync.integration.spec.ts (new) — reproduces the visual/physical row desync and the updateSettings-while-sorted formula corruption against real Handsontable + HyperFormula instances, and proves both fixes.
  • editor.cy.ts — test 53: sorts a table, edits the row that moved, asserts DC.ROW_STATUS updates on the correct row and no other row's status changes.
## Intent A PR review flagged two blocking correctness bugs: `String.replace`'s special `$`-pattern handling could silently corrupt a formula's `DC.ORIG_VALUE`/`DC.USER_NAME` substitution whenever the underlying cell data or username contained a literal `$`, and `afterChange` was indexing `dataSource` with Handsontable's visual row instead of translating to physical first, desyncing the `EDIT_STATUS` cell and "overwritten" comment sync on a sorted grid. Manually reproducing the second issue surfaced a third, unrelated Handsontable library bug — `updateSettings()` corrupts formula cell references (`#REF!`) whenever a sort is active, independent of what the call actually changes — which needed its own fix to unblock verification. ## Implementation - **`$`-substitution corruption**: `substituteBoundedToken`'s `text.replace(pattern, replacement)` used a string replacement, letting `String.replace` interpret `$&`/`` $` ``/`$'`/`$<digit>` in arbitrary cell data. Switched to a function replacement (`() => replacement`), whose return value is used verbatim — the single shared fix point, since `substituteColumnReferences.ts` calls the same function. - **Sorted-grid row desync**: `afterChange` now translates the visual row via `hot.toPhysicalRow()` before calling `syncOverwrittenCommentForCell`/`updateEditStatusForRow` (matching `beforeChange`'s existing pattern). Those two methods, whose established contract is a physical row, now translate back to visual via `hot.toVisualRow()` for every Handsontable/comments-plugin API call — fixing only the call site would have moved the bug rather than closed it. - **`updateSettings()` formula corruption**: reproduced directly in Karma — a bare `{}` settings object, called while a sort is active, is enough to corrupt formula cells. A new `updateSettingsSortSafe()` wrapper clears the sort, calls `updateSettings()`, and restores it immediately after. Applied to every `updateSettings()` call site reachable during an active edit session (`editTable`, `cancelEdit`, `getPendingExcelPreview`, `discardPendingExcel`, `cancelSubmit`, `checkSave`, `closeRecordEdit`, `confirmRecordEdit`, `applyDisplayColHeaders`, grid-resize) except the very first call at initial table load, where nothing can be sorted yet. - **Minor**: the native `paste` listener added on `hot.rootElement` is now stored and removed in `ngOnDestroy`, closing a listener leak the same review flagged. ## Changes - `parseFormulaRule.ts` / `.spec.ts`, `substituteColumnReferences.spec.ts` — function-replacement fix plus `$&`/`` $` ``/`$'`/`$<digit>` regression tests. - `editor.component.ts` — `afterChange` row translation, `syncOverwrittenCommentForCell`/`updateEditStatusForRow` visual-row translation, new `updateSettingsSortSafe()` wrapper applied across all reachable `updateSettings()` call sites, `pasteListener` field + cleanup in `ngOnDestroy`. - `sortedGridRowSync.integration.spec.ts` (new) — reproduces the visual/physical row desync and the `updateSettings`-while-sorted formula corruption against real Handsontable + HyperFormula instances, and proves both fixes. - `editor.cy.ts` — test 53: sorts a table, edits the row that moved, asserts `DC.ROW_STATUS` updates on the correct row and no other row's status changes.
Yury self-assigned this 2026-09-02 14:22:17 +00:00
Yury added 1 commit 2026-09-02 14:22:17 +00:00
fix(editor): fix formula $-substitution corruption and sorted-grid row/formula desync
Build / Build-and-ng-test (pull_request) Successful in 5m23s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m6s
Build / Build-and-test-development (pull_request) Successful in 25m7s
e139c37fbf
String.replace interpreted $&/$`/$'/$<digit> specially in DC.ORIG_VALUE/DC.USER_NAME substitutions, corrupting formulas whose cell data happened to contain a literal $. Separately, afterChange indexed dataSource with Handsontable's visual row instead of translating to physical (as beforeChange already does), desyncing EDIT_STATUS/overwritten-comment writes on a sorted grid. Investigating that surfaced a third, unrelated Handsontable bug: updateSettings() corrupts formula cell references whenever a sort is active, regardless of what it changes - fixed by clearing the sort around every updateSettings() call.
Yury requested review from allan 2026-09-02 14:22:17 +00:00
allan added 1 commit 2026-09-02 14:39:28 +00:00
Merge branch 'version7-13' into version7-13-fix
Build / Build-and-ng-test (pull_request) Successful in 5m55s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m23s
Build / Build-and-test-development (pull_request) Successful in 25m15s
d3b881fa85
hermes left a comment
Collaborator

4gl-reviewer test — this is a connectivity check. I will post a full review shortly.

**4gl-reviewer test** — this is a connectivity check. I will post a full review shortly.
hermes left a comment
Collaborator

Code Review — 4gl-reviewer

Summary: Three well-reasoned bug fixes (formula $-substitution corruption, visual/physical row desync on sorted grids, and updateSettings+sort formula #REF! corruption) backed by thorough integration and e2e tests. High-quality PR.

Issues

  • editor.component.ts:1790if (visualRow === null) return is dead code. Handsontable.toVisualRow() returns number, never null. The TypeScript type wouldn't allow this check to ever fire. Not harmful, but misleading — a future reader may think there's a null-return path to handle. Same in updateEditStatusForRow at line 1982.

Suggestions

  • editor.component.tsupdateSettingsSortSafe() is used for 7 call sites, but editTable() (line 1268) and cancelEdit() (line 1379) use an inline if (sortConfigs.length > 0) columnSorting.clearSort() pattern instead of the wrapper. The inline approach is correct for those cases (the sort is restored later in the method, not immediately after), but a one-line comment in updateSettingsSortSafe noting "editTable/cancelEdit manage sort clear/restore inline because their restore point is later in the method, not immediately after updateSettings" would prevent a future maintainer from "fixing" the inconsistency.

  • sortedGridRowSync.integration.spec.ts:731const sortConfigs = Array.isArray(sortConfig) ? sortConfig : sortConfig ? [sortConfig] : [] duplicates getCurrentSortConfigs() logic from editor.component.ts. If that method is exported/testable, the test should use it directly to avoid drift.

Looks Good

  • The String.replace → function-replacement fix (() => replacement) is the correct fix for the $&/$`/$' corruption. Verified: node -e confirms string replacement interprets $& as the matched substring and $` as the preceding text, while function replacement preserves them verbatim.
  • Integration spec uses a real Handsontable + multiColumnSorting + HyperFormula instance to demonstrate both failure modes and fixes — not mocks. This is the right approach for visual/physical divergence bugs.
  • The Cypress e2e test (test 53) snapshots every other row's status and asserts none changed, which correctly distinguishes "edit landed on the right row" from "edit landed on whatever moved into the visual slot."
  • The pasteListener cleanup in ngOnDestroy fixes a real listener leak (previously attached a new closure per edit session with no removal).
  • The updateSettingsSortSafe workaround for the Handsontable sort+formulas #REF! bug is pragmatic and well-documented.

Reviewed by 4gl-reviewer

## Code Review — 4gl-reviewer **Summary:** Three well-reasoned bug fixes (formula `$`-substitution corruption, visual/physical row desync on sorted grids, and `updateSettings`+sort formula `#REF!` corruption) backed by thorough integration and e2e tests. High-quality PR. ### Issues - **editor.component.ts:1790** — `if (visualRow === null) return` is dead code. `Handsontable.toVisualRow()` returns `number`, never `null`. The TypeScript type wouldn't allow this check to ever fire. Not harmful, but misleading — a future reader may think there's a null-return path to handle. Same in `updateEditStatusForRow` at line 1982. ### Suggestions - **editor.component.ts** — `updateSettingsSortSafe()` is used for 7 call sites, but `editTable()` (line 1268) and `cancelEdit()` (line 1379) use an inline `if (sortConfigs.length > 0) columnSorting.clearSort()` pattern instead of the wrapper. The inline approach is correct for those cases (the sort is restored later in the method, not immediately after), but a one-line comment in `updateSettingsSortSafe` noting "editTable/cancelEdit manage sort clear/restore inline because their restore point is later in the method, not immediately after updateSettings" would prevent a future maintainer from "fixing" the inconsistency. - **sortedGridRowSync.integration.spec.ts:731** — `const sortConfigs = Array.isArray(sortConfig) ? sortConfig : sortConfig ? [sortConfig] : []` duplicates `getCurrentSortConfigs()` logic from editor.component.ts. If that method is exported/testable, the test should use it directly to avoid drift. ### Looks Good - The `String.replace` → function-replacement fix (`() => replacement`) is the correct fix for the `$&`/`` $` ``/`$'` corruption. Verified: `node -e` confirms string replacement interprets `$&` as the matched substring and `` $` `` as the preceding text, while function replacement preserves them verbatim. - Integration spec uses a real Handsontable + multiColumnSorting + HyperFormula instance to demonstrate both failure modes and fixes — not mocks. This is the right approach for visual/physical divergence bugs. - The Cypress e2e test (test 53) snapshots every other row's status and asserts none changed, which correctly distinguishes "edit landed on the right row" from "edit landed on whatever moved into the visual slot." - The `pasteListener` cleanup in `ngOnDestroy` fixes a real listener leak (previously attached a new closure per edit session with no removal). - The `updateSettingsSortSafe` workaround for the Handsontable sort+formulas `#REF!` bug is pragmatic and well-documented. *Reviewed by 4gl-reviewer*
Yury added 1 commit 2026-09-03 05:57:51 +00:00
refactor(editor): document updateSettingsSortSafe's exceptions and dedupe sort-config normalization
Build / Build-and-ng-test (pull_request) Failing after 2m7s
Build / Build-and-test-development (pull_request) Skipped
Lighthouse Checks / lighthouse (pull_request) Successful in 21m46s
cc68c218d8
Author
Owner

Code Review — 4gl-reviewer

Summary: Three well-reasoned bug fixes (formula $-substitution corruption, visual/physical row desync on sorted grids, and updateSettings+sort formula #REF! corruption) backed by thorough integration and e2e tests. High-quality PR.

Issues

  • editor.component.ts:1790if (visualRow === null) return is dead code. Handsontable.toVisualRow() returns number, never null. The TypeScript type wouldn't allow this check to ever fire. Not harmful, but misleading — a future reader may think there's a null-return path to handle. Same in updateEditStatusForRow at line 1982.

Suggestions

  • editor.component.tsupdateSettingsSortSafe() is used for 7 call sites, but editTable() (line 1268) and cancelEdit() (line 1379) use an inline if (sortConfigs.length > 0) columnSorting.clearSort() pattern instead of the wrapper. The inline approach is correct for those cases (the sort is restored later in the method, not immediately after), but a one-line comment in updateSettingsSortSafe noting "editTable/cancelEdit manage sort clear/restore inline because their restore point is later in the method, not immediately after updateSettings" would prevent a future maintainer from "fixing" the inconsistency.

  • sortedGridRowSync.integration.spec.ts:731const sortConfigs = Array.isArray(sortConfig) ? sortConfig : sortConfig ? [sortConfig] : [] duplicates getCurrentSortConfigs() logic from editor.component.ts. If that method is exported/testable, the test should use it directly to avoid drift.

Looks Good

  • The String.replace → function-replacement fix (() => replacement) is the correct fix for the $&/$`/$' corruption. Verified: node -e confirms string replacement interprets $& as the matched substring and $` as the preceding text, while function replacement preserves them verbatim.
  • Integration spec uses a real Handsontable + multiColumnSorting + HyperFormula instance to demonstrate both failure modes and fixes — not mocks. This is the right approach for visual/physical divergence bugs.
  • The Cypress e2e test (test 53) snapshots every other row's status and asserts none changed, which correctly distinguishes "edit landed on the right row" from "edit landed on whatever moved into the visual slot."
  • The pasteListener cleanup in ngOnDestroy fixes a real listener leak (previously attached a new closure per edit session with no removal).
  • The updateSettingsSortSafe workaround for the Handsontable sort+formulas #REF! bug is pragmatic and well-documented.

Reviewed by 4gl-reviewer

Applied both suggestions: updateSettingsSortSafe() now documents why editTable()/cancelEdit() manage sort clear/restore inline instead of using it, and the sort-config normalization is extracted into a shared normalizeSortConfig() used by both the component and the test.

On the visualRow === null check: I don't think it's dead code — Handsontable's public .d.ts declares toVisualRow() as returning number, but the underlying IndexMapper.getVisualFromPhysicalIndex() it calls is declared number | null, and this file already has a pre-existing example (rowHeaders/afterGetRowHeader) treating toPhysicalRow()'s return the same way. tsc also compiles clean on the comparison, which it wouldn't if TS considered it unreachable. Keeping the guard.

> ## Code Review — 4gl-reviewer > > **Summary:** Three well-reasoned bug fixes (formula `$`-substitution corruption, visual/physical row desync on sorted grids, and `updateSettings`+sort formula `#REF!` corruption) backed by thorough integration and e2e tests. High-quality PR. > > ### Issues > > - **editor.component.ts:1790** — `if (visualRow === null) return` is dead code. `Handsontable.toVisualRow()` returns `number`, never `null`. The TypeScript type wouldn't allow this check to ever fire. Not harmful, but misleading — a future reader may think there's a null-return path to handle. Same in `updateEditStatusForRow` at line 1982. > > ### Suggestions > > - **editor.component.ts** — `updateSettingsSortSafe()` is used for 7 call sites, but `editTable()` (line 1268) and `cancelEdit()` (line 1379) use an inline `if (sortConfigs.length > 0) columnSorting.clearSort()` pattern instead of the wrapper. The inline approach is correct for those cases (the sort is restored later in the method, not immediately after), but a one-line comment in `updateSettingsSortSafe` noting "editTable/cancelEdit manage sort clear/restore inline because their restore point is later in the method, not immediately after updateSettings" would prevent a future maintainer from "fixing" the inconsistency. > > - **sortedGridRowSync.integration.spec.ts:731** — `const sortConfigs = Array.isArray(sortConfig) ? sortConfig : sortConfig ? [sortConfig] : []` duplicates `getCurrentSortConfigs()` logic from editor.component.ts. If that method is exported/testable, the test should use it directly to avoid drift. > > ### Looks Good > > - The `String.replace` → function-replacement fix (`() => replacement`) is the correct fix for the `$&`/`` $` ``/`$'` corruption. Verified: `node -e` confirms string replacement interprets `$&` as the matched substring and `` $` `` as the preceding text, while function replacement preserves them verbatim. > - Integration spec uses a real Handsontable + multiColumnSorting + HyperFormula instance to demonstrate both failure modes and fixes — not mocks. This is the right approach for visual/physical divergence bugs. > - The Cypress e2e test (test 53) snapshots every other row's status and asserts none changed, which correctly distinguishes "edit landed on the right row" from "edit landed on whatever moved into the visual slot." > - The `pasteListener` cleanup in `ngOnDestroy` fixes a real listener leak (previously attached a new closure per edit session with no removal). > - The `updateSettingsSortSafe` workaround for the Handsontable sort+formulas `#REF!` bug is pragmatic and well-documented. > > *Reviewed by 4gl-reviewer* Applied both suggestions: updateSettingsSortSafe() now documents why editTable()/cancelEdit() manage sort clear/restore inline instead of using it, and the sort-config normalization is extracted into a shared normalizeSortConfig() used by both the component and the test. On the visualRow === null check: I don't think it's dead code — Handsontable's public .d.ts declares toVisualRow() as returning number, but the underlying IndexMapper.getVisualFromPhysicalIndex() it calls is declared number | null, and this file already has a pre-existing example (rowHeaders/afterGetRowHeader) treating toPhysicalRow()'s return the same way. tsc also compiles clean on the comparison, which it wouldn't if TS considered it unreachable. Keeping the guard.
hermes left a comment
Collaborator

Code Review — 4gl-reviewer

Summary: Solid PR fixing three real bugs: $-substitution corruption in parseFormulaRule (string-replace interpreting $&/$1 etc.), visual/physical row desync in the afterChange hook and its callee methods on sorted grids, and a Handsontable updateSettings()-while-sorted formula #REF! corruption. Fixes are correct, well-documented, and well-tested. One inconsistency to address.

Issues:

  • editor.component.ts:1351 (cancelEdit) — Still uses the inline Array.isArray(columnSortConfig) ? columnSortConfig : [columnSortConfig] pattern instead of the new getCurrentSortConfigs()/normalizeSortConfig() helper. This misses the third branch (falsy/undefined): if getSortConfig() returns falsy, sortConfigs becomes [undefined] (length 1), so clearSort() runs (harmless), but the restore loop calls columnSorting.sort(undefined) which may throw or misbehave. editTable() at line 1264 correctly uses getCurrentSortConfigs()cancelEdit() should too.

Suggestions:

  • editor.component.ts:4479,1798,1989 — The === null guard for toPhysicalRow/toVisualRow is good, but some Handsontable type definitions also permit undefined. Consider if (row == null) (loose equality) to catch both, for robustness against version differences.
  • editor.component.ts:1349-1352 — Since cancelEdit() deliberately can't use updateSettingsSortSafe (later restore point), consider extracting just the normalizeSortConfig call here too, to keep the sort-config normalization in one place even when the full wrapper can't be used.

Looks good:

  • The () => replacement function-replacement fix in parseFormulaRule.ts is the correct, idiomatic fix for String.replace's $-pattern interpretation. Excellent edge-case test coverage ($&, $', $`, $1).
  • updateSettingsSortSafe is a clean wrapper with a thorough comment explaining why editTable()/cancelEdit() can't use it (their restore point is later). Good separation of concerns.
  • The integration spec (sortedGridRowSync.integration.spec.ts) using a real Handsontable + multiColumnSorting instance to demonstrate both the buggy and fixed behavior is exactly the right testing strategy — unit mocks couldn't prove visual/physical divergence.
  • Cypress test 53 is well-designed: sorting first, then verifying other rows' statuses didn't change (not just that the edited row got 'M') proves the edit landed on the correct row.
  • Storing the paste listener as this.pasteListener for proper ngOnDestroy cleanup is correct.
## Code Review — 4gl-reviewer **Summary:** Solid PR fixing three real bugs: `$`-substitution corruption in `parseFormulaRule` (string-replace interpreting `$&`/`$1` etc.), visual/physical row desync in the `afterChange` hook and its callee methods on sorted grids, and a Handsontable `updateSettings()`-while-sorted formula `#REF!` corruption. Fixes are correct, well-documented, and well-tested. One inconsistency to address. **Issues:** - `editor.component.ts:1351` (cancelEdit) — Still uses the inline `Array.isArray(columnSortConfig) ? columnSortConfig : [columnSortConfig]` pattern instead of the new `getCurrentSortConfigs()`/`normalizeSortConfig()` helper. This misses the third branch (falsy/`undefined`): if `getSortConfig()` returns falsy, `sortConfigs` becomes `[undefined]` (length 1), so `clearSort()` runs (harmless), but the restore loop calls `columnSorting.sort(undefined)` which may throw or misbehave. `editTable()` at line 1264 correctly uses `getCurrentSortConfigs()` — `cancelEdit()` should too. **Suggestions:** - `editor.component.ts:4479,1798,1989` — The `=== null` guard for `toPhysicalRow`/`toVisualRow` is good, but some Handsontable type definitions also permit `undefined`. Consider `if (row == null)` (loose equality) to catch both, for robustness against version differences. - `editor.component.ts:1349-1352` — Since `cancelEdit()` deliberately can't use `updateSettingsSortSafe` (later restore point), consider extracting just the `normalizeSortConfig` call here too, to keep the sort-config normalization in one place even when the full wrapper can't be used. **Looks good:** - The `() => replacement` function-replacement fix in `parseFormulaRule.ts` is the correct, idiomatic fix for `String.replace`'s `$`-pattern interpretation. Excellent edge-case test coverage (`$&`, `$'`, `` $` ``, `$1`). - `updateSettingsSortSafe` is a clean wrapper with a thorough comment explaining why `editTable()`/`cancelEdit()` can't use it (their restore point is later). Good separation of concerns. - The integration spec (`sortedGridRowSync.integration.spec.ts`) using a real Handsontable + multiColumnSorting instance to demonstrate both the buggy and fixed behavior is exactly the right testing strategy — unit mocks couldn't prove visual/physical divergence. - Cypress test 53 is well-designed: sorting first, then verifying *other rows'* statuses didn't change (not just that the edited row got 'M') proves the edit landed on the correct row. - Storing the paste listener as `this.pasteListener` for proper `ngOnDestroy` cleanup is correct.
Yury added 1 commit 2026-09-03 06:10:15 +00:00
fix: pin fast-uri to 3.1.7 to close high-severity SSRF/host-confusion advisories
Build / Build-and-ng-test (pull_request) Failing after 53s
Build / Build-and-test-development (pull_request) Skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 1m5s
f4325b2056
Yury added 1 commit 2026-09-03 06:14:14 +00:00
chore: regenerated client/package-lock.json
Build / Build-and-ng-test (pull_request) Failing after 54s
Build / Build-and-test-development (pull_request) Skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 1m12s
8f12fe74a6
hermes left a comment
Collaborator

Code Review — 4gl-reviewer

Summary: Solid, well-tested fix for two genuine bugs: visual/physical row-index confusion in the afterChange → EDIT_STATUS/overwritten-comment sync path on sorted grids, and a HOT multiColumnSorting+formulas #REF! corruption triggered by updateSettings() while sorted. The substituteBoundedToken $-escaping fix is correct and well-covered.

Issues:

  • editor.component.ts (cancelEdit, ~L1350) — cancelEdit() inlines Array.isArray(columnSortConfig) ? columnSortConfig : [columnSortConfig] instead of reusing this.getCurrentSortConfigs() / normalizeSortConfig(). If getSortConfig() returns undefined (no active sort), this yields [undefined], so the guard if (sortConfigs.length > 0) columnSorting.clearSort() passes (length 1) and the restore loop calls columnSorting.sort(undefined). editTable() was already migrated to the defensive helper; cancelEdit() should be too, for both consistency and correctness.
  • editor.component.ts (cancelEdit, ~L1350) — same call calls columnSorting.getSortConfig() directly with no try/catch, but getCurrentSortConfigs() was specifically written to wrap that call because it throws when the plugin's internal state is null (VA embed / early load). cancelEdit() is now the one un-guarded caller of the same throwing path.
  • editor.component.ts (cancelEdit, ~L1376-1382) — getFormulaCellsToPreserveOnCancel's callbacks pass the iterating physical rowIndex straight into commentsPlugin.getCommentAtCell(rowIndex, ...) and hot.getDataAtRowProp(rowIndex, prop) — both HOT APIs expect a visual row. On a sorted grid this reads the wrong cell, the same class of bug this PR fixes in syncOverwrittenCommentForCell/updateEditStatusForRow. Pre-existing, but since this PR is specifically about that translation it's worth closing here.

Suggestions:

  • editor.component.ts (cancelEdit) — replace the inline normalization with const sortConfigs = this.getCurrentSortConfigs() so it gets the undefined[] handling and the try/catch guard for free.
  • editor.component.ts (updateSettingsSortSafe) — consider try/finally around hot.updateSettings(settings, render) so a thrown settings error still restores the sort (otherwise the grid is left unsorted on failure).
  • editor.component.ts (afterChange hook) — the new const row = hot.toPhysicalRow(visualRow); if (row === null) continue is good; consider logging/skipping silently is fine, but note changedRows.add(null) is avoided by the guard — verify changedRows is typed Set<number> so a stray null can't slip in elsewhere.

Looks good:

  • substituteBoundedToken switch from string to function replacement (() => replacement) is the correct fix for the $&/$`/$'/$1 corruption — and the new spec cases (a$&b, a$'b, a$`b, a$1b) lock it in.
  • normalizeSortConfig extraction is clean, and the integration spec (sortedGridRowSync.integration.spec.ts) reproduces all three failure modes (afterChange row, EDIT_STATUS write, comment placement, and the #REF! corruption) against a real HOT instance — exactly the right level of proof.
  • The Cypress e2e (test 53) snapshots every other row's status rather than asserting a specific other row, correctly avoiding seed-data coincidence.
  • pasteListener stored on the instance and removed in ngOnDestroy is a clean listener-leak fix.
## Code Review — 4gl-reviewer **Summary:** Solid, well-tested fix for two genuine bugs: visual/physical row-index confusion in the afterChange → EDIT_STATUS/overwritten-comment sync path on sorted grids, and a HOT multiColumnSorting+formulas `#REF!` corruption triggered by `updateSettings()` while sorted. The `substituteBoundedToken` `$`-escaping fix is correct and well-covered. **Issues:** - `editor.component.ts` (cancelEdit, ~L1350) — `cancelEdit()` inlines `Array.isArray(columnSortConfig) ? columnSortConfig : [columnSortConfig]` instead of reusing `this.getCurrentSortConfigs()` / `normalizeSortConfig()`. If `getSortConfig()` returns `undefined` (no active sort), this yields `[undefined]`, so the guard `if (sortConfigs.length > 0) columnSorting.clearSort()` passes (length 1) and the restore loop calls `columnSorting.sort(undefined)`. `editTable()` was already migrated to the defensive helper; `cancelEdit()` should be too, for both consistency and correctness. - `editor.component.ts` (cancelEdit, ~L1350) — same call calls `columnSorting.getSortConfig()` directly with no try/catch, but `getCurrentSortConfigs()` was specifically written to wrap that call because it throws when the plugin's internal state is null (VA embed / early load). `cancelEdit()` is now the one un-guarded caller of the same throwing path. - `editor.component.ts` (cancelEdit, ~L1376-1382) — `getFormulaCellsToPreserveOnCancel`'s callbacks pass the iterating physical `rowIndex` straight into `commentsPlugin.getCommentAtCell(rowIndex, ...)` and `hot.getDataAtRowProp(rowIndex, prop)` — both HOT APIs expect a *visual* row. On a sorted grid this reads the wrong cell, the same class of bug this PR fixes in `syncOverwrittenCommentForCell`/`updateEditStatusForRow`. Pre-existing, but since this PR is specifically about that translation it's worth closing here. **Suggestions:** - `editor.component.ts` (cancelEdit) — replace the inline normalization with `const sortConfigs = this.getCurrentSortConfigs()` so it gets the `undefined`→`[]` handling and the try/catch guard for free. - `editor.component.ts` (updateSettingsSortSafe) — consider `try/finally` around `hot.updateSettings(settings, render)` so a thrown settings error still restores the sort (otherwise the grid is left unsorted on failure). - `editor.component.ts` (afterChange hook) — the new `const row = hot.toPhysicalRow(visualRow); if (row === null) continue` is good; consider logging/skipping silently is fine, but note `changedRows.add(null)` is avoided by the guard — verify `changedRows` is typed `Set<number>` so a stray null can't slip in elsewhere. **Looks good:** - `substituteBoundedToken` switch from string to function replacement (`() => replacement`) is the correct fix for the `$&`/`` $` ``/`$'`/`$1` corruption — and the new spec cases (`a$&b`, `a$'b`, `` a$`b ``, `a$1b`) lock it in. - `normalizeSortConfig` extraction is clean, and the integration spec (`sortedGridRowSync.integration.spec.ts`) reproduces all three failure modes (afterChange row, EDIT_STATUS write, comment placement, and the `#REF!` corruption) against a real HOT instance — exactly the right level of proof. - The Cypress e2e (test 53) snapshots *every other row's* status rather than asserting a specific other row, correctly avoiding seed-data coincidence. - `pasteListener` stored on the instance and removed in `ngOnDestroy` is a clean listener-leak fix.
Yury added 1 commit 2026-09-03 06:38:06 +00:00
fix(deps): regenerate lockfile with Node 24 and pin handsontable to 18.0.0
Lighthouse Checks / lighthouse (pull_request) Failing after 1m3s
Build / Build-and-ng-test (pull_request) Failing after 45s
Build / Build-and-test-development (pull_request) Skipped
7da29bdb11
hermes left a comment
Collaborator

Code Review — 4gl-reviewer

Summary: Well-executed PR fixing three distinct but related bugs in the editor: $-substitution corruption in formula rules, visual/physical row desync on sorted grids, and formula #REF! corruption from updateSettings() during active sort. Excellent test coverage and documentation throughout.

Issues:

  • [editor.component.ts:1228] — updateSettingsSortSafe clears sort before updateSettings and restores after, but if updateSettings throws, the sort is lost (not restored). A try/finally wrapping the updateSettings call would guarantee sort restoration even on exception.

Suggestions:

  • [editor.component.ts:3642] — The resize observer's updateSettingsSortSafe({ height }, false) will clear/restore sort on every window resize while sorted. Since the render: false flag signals an intent to avoid unnecessary rendering, the forced sort clear/restore partially defeats that. Consider whether the height-only path truly needs the wrapper or could guard on sortConfigs.length > 0 && hasFormulaColumns to skip the overhead for non-formula grids. Minor — correctness is fine, just a performance consideration for large sorted grids during rapid resize.
  • [normalizeSortConfig.ts:9] — Using any for both param and return is pragmatic given SortConfig isn't publicly exported, but a generic signature export const normalizeSortConfig = <T>(cfg: T | T[] | undefined): T[] => ... would preserve type info for callers without needing the deep import.

Looks good:

  • The () => replacement function-replacement fix in substituteBoundedToken is the correct approach — string replacement's $&/$`/$'/$<n> interpretation is a classic footgun, and the test cases for each special pattern are thorough.
  • Integration spec (sortedGridRowSync.integration.spec.ts) is exemplary: each describe block demonstrates the failure mode first, then the fix, against a real Handsontable + multiColumnSorting instance. This proves the bugs are real and the fixes close them, not just that unsorted grids still work.
  • The decision to keep editTable()/cancelEdit() with inline clear/restore (rather than using the wrapper) is correctly justified — their restore point is later in the method, and the comment explicitly warns against "simplifying" them.
  • Paste listener cleanup in ngOnDestroy properly prevents the accumulated-listener leak.
  • quoteLiteral already doubles embedded " characters, so the function-replacement fix handles the full round-trip for arbitrary user data correctly.
## Code Review — 4gl-reviewer **Summary:** Well-executed PR fixing three distinct but related bugs in the editor: `$`-substitution corruption in formula rules, visual/physical row desync on sorted grids, and formula `#REF!` corruption from `updateSettings()` during active sort. Excellent test coverage and documentation throughout. **Issues:** - [`editor.component.ts:1228`] — `updateSettingsSortSafe` clears sort before `updateSettings` and restores after, but if `updateSettings` throws, the sort is lost (not restored). A `try/finally` wrapping the `updateSettings` call would guarantee sort restoration even on exception. **Suggestions:** - [`editor.component.ts:3642`] — The resize observer's `updateSettingsSortSafe({ height }, false)` will clear/restore sort on every window resize while sorted. Since the `render: false` flag signals an intent to avoid unnecessary rendering, the forced sort clear/restore partially defeats that. Consider whether the height-only path truly needs the wrapper or could guard on `sortConfigs.length > 0 && hasFormulaColumns` to skip the overhead for non-formula grids. Minor — correctness is fine, just a performance consideration for large sorted grids during rapid resize. - [`normalizeSortConfig.ts:9`] — Using `any` for both param and return is pragmatic given SortConfig isn't publicly exported, but a generic signature `export const normalizeSortConfig = <T>(cfg: T | T[] | undefined): T[] => ...` would preserve type info for callers without needing the deep import. **Looks good:** - The `() => replacement` function-replacement fix in `substituteBoundedToken` is the correct approach — string replacement's `$&`/`` $` ``/`$'`/`$<n>` interpretation is a classic footgun, and the test cases for each special pattern are thorough. - Integration spec (`sortedGridRowSync.integration.spec.ts`) is exemplary: each `describe` block demonstrates the failure mode first, then the fix, against a real Handsontable + multiColumnSorting instance. This proves the bugs are real and the fixes close them, not just that unsorted grids still work. - The decision to keep `editTable()`/`cancelEdit()` with inline clear/restore (rather than using the wrapper) is correctly justified — their restore point is later in the method, and the comment explicitly warns against "simplifying" them. - Paste listener cleanup in `ngOnDestroy` properly prevents the accumulated-listener leak. - `quoteLiteral` already doubles embedded `"` characters, so the function-replacement fix handles the full round-trip for arbitrary user data correctly.
Yury added 1 commit 2026-09-03 07:19:50 +00:00
fix(deps): regenerate lockfile with strict peer-dep resolution to fix npm ci in CI
Build / Build-and-ng-test (pull_request) Failing after 1m51s
Build / Build-and-test-development (pull_request) Skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 2m0s
efd2e18c7b
hermes left a comment
Collaborator

Code Review — 4gl-reviewer

Summary: Well-crafted fix for three interrelated Handsontable issues: formula $-substitution corruption via String.replace, sorted-grid visual/physical row desync in afterChange/updateEditStatusForRow/syncOverwrittenCommentForCell, and updateSettings corrupting formula refs while a sort is active. Thorough test coverage including integration tests against real Handsontable instances.

Issues:

  • [editor.component.ts ~L4590] — this.pasteListener is reassigned on each call without removing the previous listener. If the registration code runs more than once (e.g. switching tables mid-session), the old listener leaks since only the latest reference is stored. Add a guard: if (this.pasteListener) { hot.rootElement.removeEventListener('paste', this.pasteListener) } before re-assigning, or guard registration with if (!this.pasteListener).

Suggestions:

  • [normalizeSortConfig.ts:10] — any[] return type loses type safety. Consider Record<string, unknown>[] at minimum, or the Handsontable sort config type if reachable.
  • [package.json] — The fast-uri devDependency addition and readdir-glob nested override seem unrelated to the formula/sort bug fixes. Consider splitting into a separate PR for cleaner history.
  • [editor.component.ts] — Pinning handsontable to exact 18.0.0 (removing caret) is reasonable but a brief comment explaining why would help future maintainers understand the lock.

Looks good:

  • Excellent test design: each integration test demonstrates the failure mode first, then the fix — making the bug and resolution self-documenting.
  • The updateSettingsSortSafe wrapper is a clean abstraction, and the comment explaining why editTable()/cancelEdit() deliberately don't use it (different restore point) prevents future "simplification" regressions.
  • The $-substitution fix (string → function replacement in substituteBoundedToken) is the correct, minimal change with targeted test cases for each special $ pattern ($&, $`, $', $1).
  • Cypress test 53 is thorough: snapshotting every other row's status before/after the edit proves the write landed on the correct row, not just "some row changed".
## Code Review — 4gl-reviewer **Summary:** Well-crafted fix for three interrelated Handsontable issues: formula `$`-substitution corruption via `String.replace`, sorted-grid visual/physical row desync in `afterChange`/`updateEditStatusForRow`/`syncOverwrittenCommentForCell`, and `updateSettings` corrupting formula refs while a sort is active. Thorough test coverage including integration tests against real Handsontable instances. **Issues:** - [editor.component.ts ~L4590] — `this.pasteListener` is reassigned on each call without removing the previous listener. If the registration code runs more than once (e.g. switching tables mid-session), the old listener leaks since only the latest reference is stored. Add a guard: `if (this.pasteListener) { hot.rootElement.removeEventListener('paste', this.pasteListener) }` before re-assigning, or guard registration with `if (!this.pasteListener)`. **Suggestions:** - [normalizeSortConfig.ts:10] — `any[]` return type loses type safety. Consider `Record<string, unknown>[]` at minimum, or the Handsontable sort config type if reachable. - [package.json] — The `fast-uri` devDependency addition and `readdir-glob` nested override seem unrelated to the formula/sort bug fixes. Consider splitting into a separate PR for cleaner history. - [editor.component.ts] — Pinning `handsontable` to exact `18.0.0` (removing caret) is reasonable but a brief comment explaining why would help future maintainers understand the lock. **Looks good:** - Excellent test design: each integration test demonstrates the failure mode first, then the fix — making the bug and resolution self-documenting. - The `updateSettingsSortSafe` wrapper is a clean abstraction, and the comment explaining why `editTable()`/`cancelEdit()` deliberately don't use it (different restore point) prevents future "simplification" regressions. - The `$`-substitution fix (string → function replacement in `substituteBoundedToken`) is the correct, minimal change with targeted test cases for each special `$` pattern (`$&`, `` $` ``, `$'`, `$1`). - Cypress test 53 is thorough: snapshotting every other row's status before/after the edit proves the write landed on the correct row, not just "some row changed".
Yury added 1 commit 2026-09-03 07:41:08 +00:00
fix(deps): pin @handsontable/angular-wrapper to 18.0.0
Build / Build-and-ng-test (pull_request) Successful in 5m24s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m35s
Build / Build-and-test-development (pull_request) Successful in 25m4s
2d7df66fc2
18.1.0 has a malformed license field (points at a logo SVG instead of a real identifier), failing the license checker's --onlyAllow gate.
hermes left a comment
Collaborator

Code Review — 4gl-reviewer

Summary: Solid fix for two real bugs — formula $-substitution corruption via String.replace pattern interpretation, and sorted-grid visual/physical row desync causing wrong-cell writes. Excellent test coverage demonstrating both failure modes and fixes. Two consistency issues in cancelEdit() worth addressing.

Issues:

  • [editor.component.ts:1349-1352] — cancelEdit() inlines Array.isArray(columnSortConfig) ? columnSortConfig : [columnSortConfig] instead of calling this.getCurrentSortConfigs(). When getSortConfig() returns undefined (no sort active), this produces [undefined] (length 1), which triggers clearSort() (harmless) but then calls columnSorting.sort(undefined) in the restore loop at line 1414 — potentially throwing or behaving unpredictably. editTable() correctly uses getCurrentSortConfigs() which normalizes to [] via normalizeSortConfig. These should be consistent.
  • [editor.component.ts:1366-1374] — The getFormulaCellsToPreserveOnCancel callbacks (getCommentAtCell(rowIndex, ...) and getDataAtRowProp(rowIndex, prop)) run before clearSort() is called (line 1393), so the sort is still active. The function iterates 0..dataSource.length (physical indices), but both Handsontable APIs expect visual rows. On a sorted grid, the wrong cells would be checked for comments/values — the same class of desync bug this PR fixes in afterChange/updateEditStatusForRow/syncOverwrittenCommentForCell. Consider translating via hot.toVisualRow(rowIndex) in the callbacks, or moving clearSort() before this block.

Suggestions:

  • [normalizeSortConfig.ts:9] — The any return type is understandable given SortConfig isn't publicly exported in this Handsontable version, but consider a structural type (e.g. { column: number; sortOrder: string }[]) for minimal type safety on the fields actually used by callers.
  • [editor.component.ts:1240] — updateSettingsSortSafe calls columnSorting.clearSort() without checking if the plugin is initialized. getCurrentSortConfigs() already handles the null-plugin case defensively, but columnSorting.clearSort() itself would throw if the plugin is null. Consider a null-check or try/catch matching the defensive style of getCurrentSortConfigs().

Looks good:

  • The substituteBoundedToken fix — switching from string to function replacement (() => replacement) — is the correct way to prevent String.replace from interpreting $&/$`/$'/$<digit> in arbitrary user data. The spec tests covering $&, $', $`, and $1 are thorough.
  • updateSettingsSortSafe is a clean abstraction for the Handsontable sort+formulas #REF! corruption workaround, with an excellent comment explaining why editTable()/cancelEdit() can't use it directly.
  • The integration spec (sortedGridRowSync.integration.spec.ts) is outstanding — it reproduces each failure mode against a real Handsontable instance, then proves the fix, rather than mocking.
  • The paste listener refactor to a stored reference with proper removeEventListener cleanup in ngOnDestroy prevents listener leaks across repeated edit sessions.
  • Cypress e2e test #53 is well-designed — snapshotting all other rows' statuses and asserting none changed is a more robust proof than asserting a single expected value.
## Code Review — 4gl-reviewer **Summary:** Solid fix for two real bugs — formula `$`-substitution corruption via `String.replace` pattern interpretation, and sorted-grid visual/physical row desync causing wrong-cell writes. Excellent test coverage demonstrating both failure modes and fixes. Two consistency issues in `cancelEdit()` worth addressing. **Issues:** - [editor.component.ts:1349-1352] — `cancelEdit()` inlines `Array.isArray(columnSortConfig) ? columnSortConfig : [columnSortConfig]` instead of calling `this.getCurrentSortConfigs()`. When `getSortConfig()` returns `undefined` (no sort active), this produces `[undefined]` (length 1), which triggers `clearSort()` (harmless) but then calls `columnSorting.sort(undefined)` in the restore loop at line 1414 — potentially throwing or behaving unpredictably. `editTable()` correctly uses `getCurrentSortConfigs()` which normalizes to `[]` via `normalizeSortConfig`. These should be consistent. - [editor.component.ts:1366-1374] — The `getFormulaCellsToPreserveOnCancel` callbacks (`getCommentAtCell(rowIndex, ...)` and `getDataAtRowProp(rowIndex, prop)`) run *before* `clearSort()` is called (line 1393), so the sort is still active. The function iterates `0..dataSource.length` (physical indices), but both Handsontable APIs expect visual rows. On a sorted grid, the wrong cells would be checked for comments/values — the same class of desync bug this PR fixes in `afterChange`/`updateEditStatusForRow`/`syncOverwrittenCommentForCell`. Consider translating via `hot.toVisualRow(rowIndex)` in the callbacks, or moving `clearSort()` before this block. **Suggestions:** - [normalizeSortConfig.ts:9] — The `any` return type is understandable given `SortConfig` isn't publicly exported in this Handsontable version, but consider a structural type (e.g. `{ column: number; sortOrder: string }[]`) for minimal type safety on the fields actually used by callers. - [editor.component.ts:1240] — `updateSettingsSortSafe` calls `columnSorting.clearSort()` without checking if the plugin is initialized. `getCurrentSortConfigs()` already handles the null-plugin case defensively, but `columnSorting.clearSort()` itself would throw if the plugin is null. Consider a null-check or try/catch matching the defensive style of `getCurrentSortConfigs()`. **Looks good:** - The `substituteBoundedToken` fix — switching from string to function replacement (`() => replacement`) — is the correct way to prevent `String.replace` from interpreting `$&`/`` $` ``/`$'`/`$<digit>` in arbitrary user data. The spec tests covering `$&`, `$'`, `` $` ``, and `$1` are thorough. - `updateSettingsSortSafe` is a clean abstraction for the Handsontable sort+formulas `#REF!` corruption workaround, with an excellent comment explaining why `editTable()`/`cancelEdit()` can't use it directly. - The integration spec (`sortedGridRowSync.integration.spec.ts`) is outstanding — it reproduces each failure mode against a real Handsontable instance, then proves the fix, rather than mocking. - The paste listener refactor to a stored reference with proper `removeEventListener` cleanup in `ngOnDestroy` prevents listener leaks across repeated edit sessions. - Cypress e2e test #53 is well-designed — snapshotting all other rows' statuses and asserting none changed is a more robust proof than asserting a single expected value.
Yury added 1 commit 2026-09-03 07:59:32 +00:00
refactor(editor): harden pasteListener cleanup and document handsontable version pin
Build / Build-and-ng-test (pull_request) Successful in 5m58s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m39s
Build / Build-and-test-development (pull_request) Successful in 25m27s
3380150d1c
hermes left a comment
Collaborator

Code Review — 4gl-reviewer

Summary: Well-structured fix for two real Handsontable integration bugs: formula $-substitution corruption via String.replace's special pattern interpretation, and sorted-grid row/formula desync caused by visual/physical row confusion. The integration spec and cypress test provide solid coverage of the failure modes.

Issues:

  • [editor.component.ts:1376-1383] — In cancelEdit(), the getFormulaCellsToPreserveOnCancel callbacks call commentsPlugin.getCommentAtCell(rowIndex, ...) and hot.getDataAtRowProp(rowIndex, prop) before clearSort() runs (line 1399), while the sort is still active. rowIndex here iterates 0..dataSource.length as a physical index, but getCommentAtCell/getDataAtRowProp expect visual rows. On a sorted grid these calls will read the wrong cell/comment. The sort clear happens 14 lines later (after toPreserve is already computed). Consider moving the clearSort() call above the getFormulaCellsToPreserveOnCancel block, or translating rowIndex to visual within the callbacks.

Suggestions:

  • [editor.component.ts:1357-1359] — cancelEdit() still uses the inline normalization (Array.isArray(columnSortConfig) ? columnSortConfig : [columnSortConfig]) instead of the new normalizeSortConfig helper, and doesn't handle the undefined case (if no sort is active, getSortConfig() returns undefined, [undefined] won't be empty, so clearSort() runs unnecessarily). Consider using this.getCurrentSortConfigs() like editTable() does, which delegates to normalizeSortConfig and handles undefined.

Looks good:

  • Excellent test coverage: the integration spec reproduces each failure mode against a real Handsontable instance, and the cypress test proves the fix end-to-end with the sort-then-edit sequence.
  • The substituteBoundedToken fix (using a function replacement to avoid $&/$\``/$'/$` corruption) is correct and well-documented, with targeted spec coverage for each special pattern.
  • The updateSettingsSortSafe wrapper is a clean, well-commented abstraction, and the explicit note about why editTable/cancelEdit don't use it (their restore point is later) shows good awareness of the control flow.
  • The paste listener cleanup in ngOnDestroy properly prevents the listener leak, and removing any previous listener before re-registering is a nice defensive touch.
  • Pinning handsontable to exact 18.0.0 with the explanatory comment about version-dependent workarounds is a pragmatic choice.
## Code Review — 4gl-reviewer **Summary:** Well-structured fix for two real Handsontable integration bugs: formula `$`-substitution corruption via `String.replace`'s special pattern interpretation, and sorted-grid row/formula desync caused by visual/physical row confusion. The integration spec and cypress test provide solid coverage of the failure modes. **Issues:** - [editor.component.ts:1376-1383] — In `cancelEdit()`, the `getFormulaCellsToPreserveOnCancel` callbacks call `commentsPlugin.getCommentAtCell(rowIndex, ...)` and `hot.getDataAtRowProp(rowIndex, prop)` **before** `clearSort()` runs (line 1399), while the sort is still active. `rowIndex` here iterates 0..dataSource.length as a physical index, but `getCommentAtCell`/`getDataAtRowProp` expect visual rows. On a sorted grid these calls will read the wrong cell/comment. The sort clear happens 14 lines later (after `toPreserve` is already computed). Consider moving the `clearSort()` call above the `getFormulaCellsToPreserveOnCancel` block, or translating `rowIndex` to visual within the callbacks. **Suggestions:** - [editor.component.ts:1357-1359] — `cancelEdit()` still uses the inline normalization (`Array.isArray(columnSortConfig) ? columnSortConfig : [columnSortConfig]`) instead of the new `normalizeSortConfig` helper, and doesn't handle the `undefined` case (if no sort is active, `getSortConfig()` returns `undefined`, `[undefined]` won't be empty, so `clearSort()` runs unnecessarily). Consider using `this.getCurrentSortConfigs()` like `editTable()` does, which delegates to `normalizeSortConfig` and handles `undefined`. **Looks good:** - Excellent test coverage: the integration spec reproduces each failure mode against a real Handsontable instance, and the cypress test proves the fix end-to-end with the sort-then-edit sequence. - The `substituteBoundedToken` fix (using a function replacement to avoid `$&`/`$\``/`$'`/`$<n>` corruption) is correct and well-documented, with targeted spec coverage for each special pattern. - The `updateSettingsSortSafe` wrapper is a clean, well-commented abstraction, and the explicit note about why `editTable`/`cancelEdit` don't use it (their restore point is later) shows good awareness of the control flow. - The paste listener cleanup in `ngOnDestroy` properly prevents the listener leak, and removing any previous listener before re-registering is a nice defensive touch. - Pinning `handsontable` to exact `18.0.0` with the explanatory comment about version-dependent workarounds is a pragmatic choice.
Yury added 1 commit 2026-09-03 08:48:02 +00:00
fix(editor): clear sort before reading cells to preserve on cancel
Build / Build-and-ng-test (pull_request) Successful in 5m16s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m2s
Build / Build-and-test-development (pull_request) Successful in 24m42s
4137ebdf6b
cancelEdit()'s getFormulaCellsToPreserveOnCancel callbacks passed a
physical rowIndex straight into visual-row-expecting Handsontable APIs
(getCommentAtCell, getDataAtRowProp) while the grid could still be
sorted, since clearSort() only ran later, right before updateSettings().
On a sorted grid this read the wrong row's comment/value, corrupting
which formula cells got preserved across cancel. Move clearSort() (via
getCurrentSortConfigs()) ahead of those callbacks instead.
hermes left a comment
Collaborator

Code Review — 4gl-reviewer

Summary: Well-executed PR fixing three interrelated bugs: $-substitution corruption in formula parsing, visual/physical row index desync on sorted grids, and formula #REF! corruption when updateSettings runs during an active sort. The fixes are correct, thoroughly tested with integration specs demonstrating both failure modes and fixes, and well-documented.

Issues:

  • None blocking.

Suggestions:

  • editor.component.ts (updateSettingsSortSafe, ~line 1218) — Multi-column sort restoration uses a loop of individual columnSorting.sort(sortConfig) calls. Single-column sort is covered by existing tests but multi-column sort restoration is not. Consider adding a test case to confirm Handsontable's sort() accumulates rather than replaces across successive calls after clearSort().
  • package.json:57,72 — Pinning handsontable and @handsontable/angular-wrapper to exact 18.0.0 (removing ^) prevents automatic patch updates including potential security fixes. Worth documenting the rationale in release notes or a comment.
  • normalizeSortConfig.ts:13 — The any return type is well-justified in the comment. If feasible in a future cleanup, a type SortConfig = Record<string, unknown> alias would add marginal type safety without fighting the type system.

Looks good:

  • Excellent test coverage — sortedGridRowSync.integration.spec.ts demonstrates both the failure mode and the fix for each bug against a real Handsontable instance, making the regression value clear.
  • The pasteListener memory leak fix (storing the reference, removing in ngOnDestroy, defensive remove-before-attach) is clean and correct.
  • The $-substitution fix (string replacement → function replacement () => replacement) is the textbook correct approach for String.replace with untrusted replacement values.
  • The inline sort clear/restore in editTable()/cancelEdit() is correctly NOT migrated to updateSettingsSortSafe — the JSDoc clearly explains why the restore point must be later in the method.
  • Null checks on toVisualRow/toPhysicalRow return values are good defensive programming.
  • The cypress e2e test (test 53) verifies the end-to-end sorted-grid edit scenario, including snapshotting all other rows' statuses to prove the edit landed on the correct row.
## Code Review — 4gl-reviewer **Summary:** Well-executed PR fixing three interrelated bugs: `$`-substitution corruption in formula parsing, visual/physical row index desync on sorted grids, and formula `#REF!` corruption when `updateSettings` runs during an active sort. The fixes are correct, thoroughly tested with integration specs demonstrating both failure modes and fixes, and well-documented. **Issues:** - None blocking. **Suggestions:** - `editor.component.ts` (`updateSettingsSortSafe`, ~line 1218) — Multi-column sort restoration uses a loop of individual `columnSorting.sort(sortConfig)` calls. Single-column sort is covered by existing tests but multi-column sort restoration is not. Consider adding a test case to confirm Handsontable's `sort()` accumulates rather than replaces across successive calls after `clearSort()`. - `package.json:57,72` — Pinning `handsontable` and `@handsontable/angular-wrapper` to exact `18.0.0` (removing `^`) prevents automatic patch updates including potential security fixes. Worth documenting the rationale in release notes or a comment. - `normalizeSortConfig.ts:13` — The `any` return type is well-justified in the comment. If feasible in a future cleanup, a `type SortConfig = Record<string, unknown>` alias would add marginal type safety without fighting the type system. **Looks good:** - Excellent test coverage — `sortedGridRowSync.integration.spec.ts` demonstrates both the failure mode and the fix for each bug against a real Handsontable instance, making the regression value clear. - The `pasteListener` memory leak fix (storing the reference, removing in `ngOnDestroy`, defensive remove-before-attach) is clean and correct. - The `$`-substitution fix (string replacement → function replacement `() => replacement`) is the textbook correct approach for `String.replace` with untrusted replacement values. - The inline sort clear/restore in `editTable()`/`cancelEdit()` is correctly NOT migrated to `updateSettingsSortSafe` — the JSDoc clearly explains why the restore point must be later in the method. - Null checks on `toVisualRow`/`toPhysicalRow` return values are good defensive programming. - The cypress e2e test (test 53) verifies the end-to-end sorted-grid edit scenario, including snapshotting all other rows' statuses to prove the edit landed on the correct row.
allan merged commit f797b1130a into version7-13 2026-09-03 09:21:57 +00:00
allan deleted branch version7-13-fix 2026-09-03 09:21:57 +00:00
Sign in to join this conversation.
No Reviewers
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dc/dc#311