chore(deps): upgrade Handsontable to v18

Bump handsontable and @handsontable/angular-wrapper 17.1 -> 18.0 and
adapt to its stricter TypeScript-core types: moved handsontable/common
and deep walkontable imports to their new public export paths, added
explicit generics/casts where getData()/getCellMeta()/getSelected() are
now typed unknown/nullable instead of the loose v17 shapes, and updated
the AutocompleteEditor override to the new open() signature.

Also allowlists @handsontable/angular-wrapper@18.0.0 and
handsontable@18.0.0 in licenseChecker.js — both versions report the
same non-SPDX "SEE LICENSE IN LICENSE.txt" field that license-checker
mis-resolves as a disallowed custom license, same as prior versions.
This commit is contained in:
YuryShkoda
2026-07-06 15:44:27 +03:00
parent 6864c044dc
commit 005b616adb
9 changed files with 59 additions and 751 deletions
+1
View File
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"Handsontable",
"Licence",
"SYSERRORTEXT",
"SYSWARNINGTEXT",
+1 -1
View File
@@ -10,7 +10,7 @@ const check = (cwd) => {
onlyAllow:
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
excludePackages:
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;@handsontable/angular-wrapper@17.1.0;handsontable@^16.0.1;handsontable@16.2.0;handsontable@17.1.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;hyperformula@3.2.0;hyperformula@3.3.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1;buffers@0.1.1'
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;@handsontable/angular-wrapper@17.1.0;@handsontable/angular-wrapper@18.0.0;handsontable@^16.0.1;handsontable@16.2.0;handsontable@17.1.0;handsontable@18.0.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;hyperformula@3.2.0;hyperformula@3.3.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1;buffers@0.1.1'
},
(error, json) => {
if (error) {
+11 -722
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -54,7 +54,7 @@
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
"@clr/icons": "^13.0.2",
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
"@handsontable/angular-wrapper": "^17.1.0",
"@handsontable/angular-wrapper": "^18.0.0",
"@sasjs/adapter": "^4.17.0",
"@sasjs/utils": "^3.5.3",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
@@ -67,7 +67,7 @@
"d3-graphviz": "^5.0.2",
"exceljs": "^4.4.0",
"fs-extra": "^7.0.1",
"handsontable": "^17.1.0",
"handsontable": "^18.0.0",
"https-browserify": "1.0.0",
"hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0",
+15 -7
View File
@@ -11,7 +11,7 @@ import {
ViewEncapsulation
} from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import Handsontable from 'handsontable'
import Handsontable, { CellRange } from 'handsontable'
import { Subject, Subscription } from 'rxjs'
import { sanitiseForSas } from '../shared/utils/sanitise'
import { SasStoreService } from '../services/sas-store.service'
@@ -21,7 +21,6 @@ type AOA = any[][]
import { HotTableComponent } from '@handsontable/angular-wrapper'
import { UploadFile } from '@sasjs/adapter'
import { isSpecialMissing } from '@sasjs/utils/input/validators'
import CellRange from 'handsontable/3rdparty/walkontable/src/cell/range'
import { CellValidationSource } from '../models/CellValidationSource'
import { FileUploader } from '../models/FileUploader.class'
import { FilterGroup, FilterQuery } from '../models/FilterQuery'
@@ -161,8 +160,11 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
items: {
edit_row: {
name: 'Edit row',
hidden() {
const hot: Handsontable.Core = this
// HOT 18's MenuItemConfig types `hidden` as a plain `() => boolean` with
// no `this` type, so an object-literal method here has `this` inferred
// as the surrounding item config unless declared explicitly.
hidden(this: Handsontable.Core) {
const hot = this
// Hide editing actions in read-only (view) mode.
if (hot.getSettings().readOnly) return true
@@ -1399,7 +1401,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
hot.removeCellMeta(rowIndex, col, 'valid')
hot.removeCellMeta(rowIndex, col, 'dupKey')
// Remove our custom class from cell metadata
const cellMeta = hot.getCellMeta(rowIndex, col)
// getCellMeta<M>() defaults to Record<string, unknown> in HOT 18; pin it to
// CellMeta so `.className` keeps its real string | string[] type below.
const cellMeta = hot.getCellMeta<Handsontable.CellMeta>(rowIndex, col)
if (cellMeta.className) {
let cleanedClassName: string
if (Array.isArray(cellMeta.className)) {
@@ -2246,8 +2250,10 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
validationSourceIndex
].values.map((el) => el.RAW_VALUE)
// `.source` (dropdown/autocomplete list) is typed unknown[] | Function here since
// getCellMeta() isn't given a type param; cast to the array branch we actually use.
const cellHadSource =
(hot.getCellMeta(row, column).source || []).length < 1
((hot.getCellMeta(row, column).source as unknown[]) || []).length < 1
const cellHasValue = cellData !== ' '
hot.batch(() => {
@@ -3123,7 +3129,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
let textInfo = 'No info found'
if (this.hotInstance) {
const hotSelected: [number, number, number, number][] =
// getSelected() is typed number[][] in HOT 18 (was a 4-tuple
// array before); loosen the annotation to match.
const hotSelected: number[][] =
this.hotInstance.getSelected() || []
const selectedCol: number = hotSelected
? hotSelected[0][1]
@@ -22,10 +22,9 @@ import { ExcelRule } from '../models/TableData'
import { HotTableInterface } from '../models/HotTable.interface'
import { Col } from '../shared/dc-validator/models/col.model'
import { SpreadsheetService } from '../services/spreadsheet.service'
import Handsontable from 'handsontable'
import Handsontable, { CellChange, ChangeSource } from 'handsontable'
import { HotTableComponent } from '@handsontable/angular-wrapper'
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
import { CellChange, ChangeSource } from 'handsontable/common'
import { baseAfterGetColHeader } from '../shared/utils/hot.utils'
import { ColumnSettings } from 'handsontable/settings'
import { UploadFile } from '@sasjs/adapter'
@@ -502,7 +501,9 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
if (changes) {
for (let change of changes) {
if (change && change[3]) {
change[3] = change[3].toUpperCase()
// CellValue is `unknown` in HOT 18 (was a concrete union before);
// this column is always a dataset name string.
change[3] = (change[3] as string).toUpperCase()
}
}
}
@@ -552,7 +553,9 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
dynamicCellValidations() {
if (!this.hotInstanceUserDataset) return
const hotData = this.hotInstanceUserDataset.getData()
// getData() returns unknown[][] in HOT 18 (was CellValue[][]); this grid's
// data is always library/table name strings.
const hotData = this.hotInstanceUserDataset.getData() as string[][]
hotData.forEach((row, rowIndex) => {
const library = row[0]
@@ -573,10 +576,12 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
if (dataAtRow && dataAtRow[0] && dataAtRow[1]) {
if (!this.matchedDatasets.includes(dataset)) {
// getCellMetaAtRow() returns Record<string, unknown>[] in HOT 18, so
// `.col` needs casting back to number for setCellMeta() (same below).
cellMetaAtRow.forEach((cellMeta) => {
this.hotInstanceUserDataset.setCellMeta(
row,
cellMeta.col,
cellMeta.col as number,
'className',
'not-matched'
)
@@ -585,7 +590,7 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
cellMetaAtRow.forEach((cellMeta) => {
this.hotInstanceUserDataset.setCellMeta(
row,
cellMeta.col,
cellMeta.col as number,
'className',
''
)
@@ -595,7 +600,7 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
cellMetaAtRow.forEach((cellMeta) => {
this.hotInstanceUserDataset.setCellMeta(
row,
cellMeta.col,
cellMeta.col as number,
'className',
''
)
@@ -991,7 +996,8 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
private getDatasetsFromHot(): string[] {
if (!this.hotInstanceUserDataset) return []
const hotData = this.hotInstanceUserDataset.getData()
// getData() returns unknown[][] in HOT 18; see dynamicCellValidations() above.
const hotData = this.hotInstanceUserDataset.getData() as string[][]
return hotData
.filter((row) => row[0]?.length && row[1]?.length)
@@ -1,10 +1,9 @@
import Handsontable from 'handsontable'
import Core from 'handsontable/core'
import Handsontable, { HotInstance } from 'handsontable'
export class CustomAutocompleteEditor
extends Handsontable.editors.AutocompleteEditor
{
constructor(instance: Core) {
constructor(instance: HotInstance) {
super(instance)
}
@@ -13,8 +12,9 @@ export class CustomAutocompleteEditor
}
// Listbox open
open(event?: Event | undefined): void {
super.open(event)
// HOT 18's AutocompleteEditor.open() takes no arguments (was `event?: Event`).
open(): void {
super.open()
if (this.isCellNumeric()) {
this.htContainer.classList.add('numericListbox')
@@ -52,12 +52,14 @@ export async function exportGrid(
// Mirror HOT's own export item: only honor a selection that spans more than
// one cell. A right-click places a single-cell cursor, and the corner click
// is select-all (negative coords) — both mean "export the whole table".
const isCornerSelectAll = !!sel && sel.from.row < 0 && sel.from.col < 0
// sel.from/to.row/col are typed nullable (CellCoords allows an unset state),
// but a range returned by getSelectedRangeLast() always has real coordinates.
const isCornerSelectAll = !!sel && sel.from.row! < 0 && sel.from.col! < 0
if (sel && !sel.isSingleCell() && !isCornerSelectAll) {
const top = Math.max(0, Math.min(sel.from.row, sel.to.row))
const left = Math.max(0, Math.min(sel.from.col, sel.to.col))
const bottom = Math.max(sel.from.row, sel.to.row)
const right = Math.max(sel.from.col, sel.to.col)
const top = Math.max(0, Math.min(sel.from.row!, sel.to.row!))
const left = Math.max(0, Math.min(sel.from.col!, sel.to.col!))
const bottom = Math.max(sel.from.row!, sel.to.row!)
const right = Math.max(sel.from.col!, sel.to.col!)
opts['range'] = [top, Math.max(left, skipLeadingCols), bottom, right]
}
+3 -1
View File
@@ -228,7 +228,9 @@ export class ViewerComponent
!this.isTableSwitching
) {
try {
const hotSelected: [number, number, number, number][] =
// getSelected() is typed number[][] in HOT 18 (was a 4-tuple
// array before); loosen the annotation to match.
const hotSelected: number[][] =
this.hotInstance.getSelected() || []
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
const colName = this.hotInstance.colToProp(selectedCol)