|
|
|
@@ -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
|
|
|
|
|