Compare commits

...

3 Commits

Author SHA1 Message Date
semantic-release-bot
db5887de21 chore(release): 7.2.3 [skip ci]
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)

### Bug Fixes

* opening second table in viewer throws an error ([6c6b1cb](6c6b1cbf46))
2025-10-02 15:10:30 +00:00
fe24d9bcbd Merge pull request 'opening second table in viewer throws an error' (#191) from issue-189 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m47s
Release / Build-and-test-development (push) Successful in 8m52s
Release / release (push) Successful in 8m25s
Reviewed-on: #191
Reviewed-by: allan <allan@4gl.io>
2025-10-02 14:54:13 +00:00
M
6c6b1cbf46 fix: opening second table in viewer throws an error
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m59s
Build / Build-and-test-development (pull_request) Successful in 9m5s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m55s
2025-10-02 16:23:00 +02:00
3 changed files with 204 additions and 39 deletions

View File

@@ -1,3 +1,10 @@
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)
### Bug Fixes
* opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5))
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)

View File

@@ -157,15 +157,30 @@ export class ViewerComponent
return ' '
},
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
const column = this.hotInstance?.colToProp(col) as string
// CRITICAL: Prevent "colToProp method cannot be called because this Handsontable instance has been destroyed" error
// This callback can be triggered even after the instance is destroyed during rapid table switching
if (
!this.hotInstance ||
this.hotInstance.isDestroyed ||
this.isTableSwitching
) {
// Graceful fallback: apply only dark mode styling when instance is unavailable
th.classList.add(globals.handsontable.darkTableHeaderClass)
return
}
// header columns styling - primary keys
const isPKCol = column && this.headerPks.indexOf(column) > -1
try {
const column = this.hotInstance.colToProp(col) as string
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
const isPKCol = column && this.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass)
// Apply dark mode styling to all headers
th.classList.add(globals.handsontable.darkTableHeaderClass)
} catch (error) {
// Safety net: if colToProp() fails, still apply basic styling
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
},
rowHeaderWidth: 15,
rowHeights: 20,
@@ -199,12 +214,21 @@ export class ViewerComponent
let colInfo: DataFormat | undefined
let textInfo = 'No info found'
if (this.hotInstance) {
const hotSelected: [number, number, number, number][] =
this.hotInstance.getSelected() || []
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
const colName = this.hotInstance?.colToProp(selectedCol)
colInfo = this.$dataFormats?.vars[colName]
if (
this.hotInstance &&
!this.hotInstance.isDestroyed &&
!this.isTableSwitching
) {
try {
const hotSelected: [number, number, number, number][] =
this.hotInstance.getSelected() || []
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
const colName = this.hotInstance.colToProp(selectedCol)
colInfo = this.$dataFormats?.vars[colName]
} catch (error) {
// Ignore errors during table switching
colInfo = undefined
}
if (colInfo)
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
@@ -224,6 +248,13 @@ export class ViewerComponent
private hotInstance: Handsontable | null = null
public hotInstanceClickListener: boolean = false
// Race condition prevention for rapid table switching
private isTableSwitching: boolean = false
private switchingTimeout: any = null
// Prevents duplicate setupHot() calls within short time windows
private lastSetupTime: number = 0
public viewboxOpen: boolean = false
constructor(
@@ -599,10 +630,24 @@ export class ViewerComponent
}
public onTableClick(libTable: any, library: any) {
this.lib = library.LIBRARYREF
this.table = libTable
this.selectLibTable(libTable)
this.viewData(0)
// OPTIMIZATION: Prevent race conditions and destroyed instance errors during rapid table switching
if (this.isTableSwitching) {
return
}
// Clear any existing timeout to prevent stale operations
if (this.switchingTimeout) {
clearTimeout(this.switchingTimeout)
}
// PERFORMANCE: Debounce table switches to prevent rapid successive calls
// This ensures only the final table selection is processed
this.switchingTimeout = setTimeout(() => {
this.lib = library.LIBRARYREF
this.table = libTable
this.selectLibTable(libTable)
this.viewData(0)
}, 50) // 50ms debounce - fast enough for good UX, slow enough to prevent issues
}
public async selectTable(lib: string, initial?: boolean, library?: any) {
@@ -721,6 +766,14 @@ export class ViewerComponent
}
public async viewData(filter_pk: number) {
// CRITICAL: Set switching flag to prevent concurrent operations and race conditions
// This prevents callbacks from accessing destroyed instances during table switching
this.isTableSwitching = true
// CLEANUP: Ensure any existing Handsontable instance is properly destroyed
// This prevents "instance destroyed" errors
this.cleanupHotInstance()
this.loadingTableView = true
let libDataset: any
@@ -961,14 +1014,25 @@ export class ViewerComponent
this.loadingTableView = false
//If we try to setup hot when no data is returned it errors `isDestoryed`.
//That is intorduced by HOT update
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot()
// Setup Handsontable after async operations complete
// Original issue: setupHot() called before API responses populated headerPks array
// Solution: Delay ensures both API paths (lines 328 & 886) have chance to set headerPks
setTimeout(() => {
if (!this.noData && !this.noDataReqErr && libDataset) {
this.setupHot()
}
}, 50) // Optimized from 100ms - fast enough for API completion, slow enough to prevent race conditions
// RACE CONDITION PREVENTION: Reset switching flag after setup completion
// This allows new table switches after current operation finishes
setTimeout(() => {
this.isTableSwitching = false
}, 300) // Optimized from 700ms to match reduced setup times
// Fix ARIA accessibility issues after data loading
setTimeout(() => {
this.fixAriaAccessibility()
}, 1500)
}, 500)
/**
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
@@ -988,7 +1052,7 @@ export class ViewerComponent
})
this.hotInstanceClickListener = true
}
}, 2000)
}, 1000)
}
}
@@ -1116,12 +1180,74 @@ export class ViewerComponent
}
}
/**
* CRITICAL CLEANUP (workaround needed for HOT version 16 and above): Safely destroys Handsontable instances
*
* Purpose: Prevents "instance destroyed" errors and memory leaks during table switching
*
* Called from:
* - viewData() - before loading new table data
* - setupHot() - before creating new instance
* - ngOnDestroy() - component cleanup
*
* Safety features:
* - Checks if instance exists and is not already destroyed
* - Try-catch prevents errors during destruction
* - Sets instance to null to prevent stale references
*/
private cleanupHotInstance() {
if (this.hotInstance && !this.hotInstance.isDestroyed) {
try {
this.hotInstance.destroy()
} catch (error) {
console.warn('Error destroying Handsontable instance:', error)
}
}
this.hotInstance = null
}
/**
* PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)
*
* 1. Duplicate call prevention (500ms window)
* 2. Reduced timeout delays (200ms + 50ms vs original 1000ms + 200ms)
* 3. Multiple validation checks to prevent race conditions
* 4. Forced render for immediate primary key styling
*
* Timeline: 50ms (viewData) + 200ms (main) + 50ms (component ready) = ~300ms total
* Previous: 100ms + 600ms + 100ms = 800ms (plus render delays = ~2 seconds)
*/
private setupHot() {
// DUPLICATE PREVENTION: Avoid multiple setup calls during rapid table switching
const now = Date.now()
if (now - this.lastSetupTime < 500) {
return
}
this.lastSetupTime = now
setTimeout(() => {
if (!this.loadingTableView && this.libDataset) {
// VALIDATION: Don't setup if we're currently switching tables or still loading
if (this.loadingTableView || !this.libDataset) {
return
}
// CLEANUP: Ensure clean slate before new setup
this.cleanupHotInstance()
// TIMING: Wait for Angular component to be ready (optimized from 100ms to 50ms)
setTimeout(() => {
// DOUBLE-CHECK: Ensure we're still in valid state after delays
if (
this.isTableSwitching ||
this.loadingTableView ||
!this.libDataset
) {
return
}
this.hotInstance = this.hotTableComponent?.hotInstance
if (this.hotInstance) {
if (this.hotInstance && !this.hotInstance.isDestroyed) {
this.hotInstance.updateSettings({
height: this.hotTable.height,
modifyColWidth: (width: any, col: any) => {
@@ -1129,15 +1255,29 @@ export class ViewerComponent
else return width
},
afterGetColHeader: (col: number, th: any) => {
const column = this.hotInstance?.colToProp(col) as string
// CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
if (
!this.hotInstance ||
this.hotInstance.isDestroyed ||
this.isTableSwitching
) {
th.classList.add(globals.handsontable.darkTableHeaderClass)
return
}
// header columns styling - primary keys
const isPKCol = column && this.headerPks.indexOf(column) > -1
try {
const column = this.hotInstance.colToProp(col) as string
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
const isPKCol = column && this.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass)
// DARK MODE: Apply to all headers
th.classList.add(globals.handsontable.darkTableHeaderClass)
} catch (error) {
// SAFETY NET: Ensure basic styling is always applied
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
}
})
@@ -1153,14 +1293,18 @@ export class ViewerComponent
this.fixAriaAccessibility()
}, 50)
})
}
}
// Fix ARIA accessibility issues after table setup
setTimeout(() => {
this.fixAriaAccessibility()
}, 500)
}, 1000)
// Force immediate render to apply primary key styling
// Without this, styling would wait for ~2 seconds to be applied
// With this, styling appears in ~300ms total (workaround needed for HOT version 16 and above)
setTimeout(() => {
if (this.hotInstance && !this.hotInstance.isDestroyed) {
this.hotInstance.render()
}
}, 10)
}
}, 50) // Optimized Angular component readiness delay
}, 200) // Optimized main setup delay (was 600ms)
}
async loadWithParameters() {
@@ -1233,13 +1377,27 @@ export class ViewerComponent
}
ngOnDestroy() {
// Clean up the MutationObserver
// Proper component destruction to prevent memory leaks and errors
// Prevent any new operations during cleanup
this.isTableSwitching = true
// Clear any pending debounced table switches
if (this.switchingTimeout) {
clearTimeout(this.switchingTimeout)
this.switchingTimeout = null
}
// Safely destroy Handsontable instance
this.cleanupHotInstance()
// Clean up ARIA accessibility observers
if (this.ariaObserver) {
this.ariaObserver.disconnect()
this.ariaObserver = undefined
}
// Clean up the interval
// Clear ARIA check intervals
if (this.ariaCheckInterval) {
clearInterval(this.ariaCheckInterval)
this.ariaCheckInterval = undefined

View File

@@ -1,6 +1,6 @@
{
"name": "dcfrontend",
"version": "7.2.2",
"version": "7.2.3",
"description": "Data Controller",
"devDependencies": {
"@saithodev/semantic-release-gitea": "^2.1.0",