Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4924df2ef3 | ||
| 2e141a5d52 | |||
|
|
cb1978bcaf | ||
|
|
387f5122f1 | ||
|
|
db5887de21 | ||
| fe24d9bcbd | |||
|
|
6c6b1cbf46 | ||
|
|
4d65c9c999 | ||
| 4417279275 | |||
|
|
365f12996d | ||
|
|
ef1015f33b | ||
| b43dfb5cf4 | |||
|
|
225e693d1f |
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,3 +1,32 @@
|
|||||||
|
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4))
|
||||||
|
* snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3))
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* jsrsasign, @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42))
|
||||||
|
|
||||||
|
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing localhost from index.html ([225e693](https://git.datacontroller.io/dc/dc/commit/225e693d1fd4381f2b8ce42fecb508f0a9e9dad8))
|
||||||
|
|
||||||
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
30
client/package-lock.json
generated
30
client/package-lock.json
generated
@@ -37,7 +37,7 @@
|
|||||||
"hyperformula": "^2.5.0",
|
"hyperformula": "^2.5.0",
|
||||||
"iconv-lite": "^0.5.0",
|
"iconv-lite": "^0.5.0",
|
||||||
"jquery-datetimepicker": "^2.5.21",
|
"jquery-datetimepicker": "^2.5.21",
|
||||||
"jsrsasign": "^10.2.0",
|
"jsrsasign": "^11.1.0",
|
||||||
"marked": "^5.0.0",
|
"marked": "^5.0.0",
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.26.0",
|
||||||
"ngx-clipboard": "^16.0.0",
|
"ngx-clipboard": "^16.0.0",
|
||||||
@@ -939,9 +939,9 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@angular-devkit/build-angular/node_modules/vite": {
|
"node_modules/@angular-devkit/build-angular/node_modules/vite": {
|
||||||
"version": "5.4.19",
|
"version": "5.4.20",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
|
||||||
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6716,14 +6716,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@sasjs/adapter": {
|
"node_modules/@sasjs/adapter": {
|
||||||
"version": "4.12.2",
|
"version": "4.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.14.0.tgz",
|
||||||
"integrity": "sha512-OV5mx3N2Hywhp0M+SBLTuG42x/LDnMxrb2/pwG4RQbhfzvAwdHVEoXRouDJ49RMSY9s6TJcwUPh+Xzafl5sG/g==",
|
"integrity": "sha512-kLjThuLayOr35A+MloXx4Z2tqjui48vHYmVXXmqiFe26Wal6i2jflzhVN5+srmYlGzJX+G0SN78kYTKfvr3lcA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/utils": "3.5.2",
|
"@sasjs/utils": "3.5.2",
|
||||||
"axios": "1.8.2",
|
"axios": "1.12.2",
|
||||||
"axios-cookiejar-support": "5.0.5",
|
"axios-cookiejar-support": "5.0.5",
|
||||||
"form-data": "4.0.4",
|
"form-data": "4.0.4",
|
||||||
"https": "1.0.0",
|
"https": "1.0.0",
|
||||||
@@ -8874,13 +8874,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.8.2",
|
"version": "1.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||||
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.4",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -15137,9 +15137,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsrsasign": {
|
"node_modules/jsrsasign": {
|
||||||
"version": "10.9.0",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.0.tgz",
|
||||||
"integrity": "sha512-QWLUikj1SBJGuyGK8tjKSx3K7Y69KYJnrs/pQ1KZ6wvZIkHkWjZ1PJDpuvc1/28c1uP0KW9qn1eI1LzHQqDOwQ==",
|
"integrity": "sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/kjur/jsrsasign#donations"
|
"url": "https://github.com/kjur/jsrsasign#donations"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
"hyperformula": "^2.5.0",
|
"hyperformula": "^2.5.0",
|
||||||
"iconv-lite": "^0.5.0",
|
"iconv-lite": "^0.5.0",
|
||||||
"jquery-datetimepicker": "^2.5.21",
|
"jquery-datetimepicker": "^2.5.21",
|
||||||
"jsrsasign": "^10.2.0",
|
"jsrsasign": "^11.1.0",
|
||||||
"marked": "^5.0.0",
|
"marked": "^5.0.0",
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.26.0",
|
||||||
"ngx-clipboard": "^16.0.0",
|
"ngx-clipboard": "^16.0.0",
|
||||||
|
|||||||
@@ -171,24 +171,9 @@ export class EditRecordComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyToClip(text: string) {
|
public copyToClip(text: string) {
|
||||||
const modalElement = document.querySelector('#recordModalRef .modal-title')
|
navigator.clipboard.writeText(text)
|
||||||
|
|
||||||
if (modalElement) {
|
|
||||||
const selBox = document.createElement('textarea')
|
|
||||||
selBox.style.position = 'fixed'
|
|
||||||
selBox.style.left = '0'
|
|
||||||
selBox.style.top = '0'
|
|
||||||
selBox.style.opacity = '0'
|
|
||||||
selBox.style.zIndex = '5000'
|
|
||||||
selBox.value = text
|
|
||||||
modalElement.appendChild(selBox)
|
|
||||||
selBox.focus()
|
|
||||||
selBox.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
modalElement.removeChild(selBox)
|
|
||||||
this.generatedRecordUrl = text
|
this.generatedRecordUrl = text
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async generateEditRecordUrl() {
|
async generateEditRecordUrl() {
|
||||||
if (this.generatedRecordUrl) {
|
if (this.generatedRecordUrl) {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export class LicensingComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
private licenceService: LicenceService,
|
private licenceService: LicenceService,
|
||||||
private sasService: SasService,
|
private sasService: SasService,
|
||||||
private appService: AppService
|
private appService: AppService
|
||||||
@@ -124,7 +125,9 @@ export class LicensingComponent implements OnInit {
|
|||||||
res.adapterResponse.return[0] &&
|
res.adapterResponse.return[0] &&
|
||||||
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
||||||
) {
|
) {
|
||||||
location.replace(location.href.split('#')[0])
|
this.router.navigateByUrl('/').then(() => {
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -746,28 +746,13 @@ export class LineageComponent {
|
|||||||
return URL.createObjectURL(svg_blob)
|
return URL.createObjectURL(svg_blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSVGBlob() {
|
|
||||||
let svg: any = document.getElementById('graph')
|
|
||||||
let serializer = new XMLSerializer()
|
|
||||||
let svg_blob = new Blob([serializer.serializeToString(svg)], {
|
|
||||||
type: 'image/svg+xml'
|
|
||||||
})
|
|
||||||
return svg_blob
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadSVG() {
|
downloadSVG() {
|
||||||
d3Viz.graphviz('#graph').resetZoom()
|
d3Viz.graphviz('#graph').resetZoom()
|
||||||
|
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
|
||||||
window.navigator.msSaveBlob(this.getSVGBlob(), this.constructName('svg'))
|
|
||||||
} else {
|
|
||||||
let downloadLink = document.createElement('a')
|
let downloadLink = document.createElement('a')
|
||||||
downloadLink.href = this.getSVGURL()
|
downloadLink.href = this.getSVGURL()
|
||||||
downloadLink.download = this.constructName('svg')
|
downloadLink.download = this.constructName('svg')
|
||||||
document.body.appendChild(downloadLink)
|
|
||||||
downloadLink.click()
|
downloadLink.click()
|
||||||
document.body.removeChild(downloadLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadPNG() {
|
async downloadPNG() {
|
||||||
@@ -795,16 +780,11 @@ export class LineageComponent {
|
|||||||
var a = document.createElement('a')
|
var a = document.createElement('a')
|
||||||
var blob = new Blob([csvArray], { type: 'text/csv' })
|
var blob = new Blob([csvArray], { type: 'text/csv' })
|
||||||
|
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
|
||||||
window.navigator.msSaveBlob(blob, this.constructName('csv'))
|
|
||||||
} else {
|
|
||||||
var url = window.URL.createObjectURL(blob)
|
var url = window.URL.createObjectURL(blob)
|
||||||
a.href = url
|
a.href = url
|
||||||
a.download = this.constructName('csv')
|
a.download = this.constructName('csv')
|
||||||
a.click()
|
a.click()
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
a.remove()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDotUrl() {
|
private getDotUrl() {
|
||||||
@@ -813,23 +793,11 @@ export class LineageComponent {
|
|||||||
return window.URL.createObjectURL(dot_blob)
|
return window.URL.createObjectURL(dot_blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDotBlob() {
|
|
||||||
let data = this.vizInput
|
|
||||||
let dot_blob = new Blob([data], { type: 'text/plain' })
|
|
||||||
return dot_blob
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadDot() {
|
downloadDot() {
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
|
||||||
window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt'))
|
|
||||||
} else {
|
|
||||||
let downloadLink = document.createElement('a')
|
let downloadLink = document.createElement('a')
|
||||||
downloadLink.href = this.getDotUrl()
|
downloadLink.href = this.getDotUrl()
|
||||||
downloadLink.download = this.constructName('txt')
|
downloadLink.download = this.constructName('txt')
|
||||||
document.body.appendChild(downloadLink)
|
|
||||||
downloadLink.click()
|
downloadLink.click()
|
||||||
document.body.removeChild(downloadLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public showSvg() {
|
public showSvg() {
|
||||||
|
|||||||
@@ -157,15 +157,30 @@ export class ViewerComponent
|
|||||||
return ' '
|
return ' '
|
||||||
},
|
},
|
||||||
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const column = this.hotInstance.colToProp(col) as string
|
||||||
|
|
||||||
// header columns styling - primary keys
|
|
||||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||||
|
|
||||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||||
|
|
||||||
// Dark mode
|
// Apply dark mode styling to all headers
|
||||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
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,
|
rowHeaderWidth: 15,
|
||||||
rowHeights: 20,
|
rowHeights: 20,
|
||||||
@@ -199,12 +214,21 @@ export class ViewerComponent
|
|||||||
let colInfo: DataFormat | undefined
|
let colInfo: DataFormat | undefined
|
||||||
let textInfo = 'No info found'
|
let textInfo = 'No info found'
|
||||||
|
|
||||||
if (this.hotInstance) {
|
if (
|
||||||
|
this.hotInstance &&
|
||||||
|
!this.hotInstance.isDestroyed &&
|
||||||
|
!this.isTableSwitching
|
||||||
|
) {
|
||||||
|
try {
|
||||||
const hotSelected: [number, number, number, number][] =
|
const hotSelected: [number, number, number, number][] =
|
||||||
this.hotInstance.getSelected() || []
|
this.hotInstance.getSelected() || []
|
||||||
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
||||||
const colName = this.hotInstance?.colToProp(selectedCol)
|
const colName = this.hotInstance.colToProp(selectedCol)
|
||||||
colInfo = this.$dataFormats?.vars[colName]
|
colInfo = this.$dataFormats?.vars[colName]
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors during table switching
|
||||||
|
colInfo = undefined
|
||||||
|
}
|
||||||
|
|
||||||
if (colInfo)
|
if (colInfo)
|
||||||
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
|
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
|
private hotInstance: Handsontable | null = null
|
||||||
public hotInstanceClickListener: boolean = false
|
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
|
public viewboxOpen: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -505,17 +536,7 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyToClip() {
|
public copyToClip() {
|
||||||
let selBox = document.createElement('textarea')
|
navigator.clipboard.writeText(this.webQueryText)
|
||||||
selBox.style.position = 'fixed'
|
|
||||||
selBox.style.left = '0'
|
|
||||||
selBox.style.top = '0'
|
|
||||||
selBox.style.opacity = '0'
|
|
||||||
selBox.value = this.webQueryText
|
|
||||||
document.body.appendChild(selBox)
|
|
||||||
selBox.focus()
|
|
||||||
selBox.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
document.body.removeChild(selBox)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToViewer() {
|
public goToViewer() {
|
||||||
@@ -599,10 +620,24 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onTableClick(libTable: any, library: any) {
|
public onTableClick(libTable: any, library: any) {
|
||||||
|
// 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.lib = library.LIBRARYREF
|
||||||
this.table = libTable
|
this.table = libTable
|
||||||
this.selectLibTable(libTable)
|
this.selectLibTable(libTable)
|
||||||
this.viewData(0)
|
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) {
|
public async selectTable(lib: string, initial?: boolean, library?: any) {
|
||||||
@@ -721,6 +756,14 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async viewData(filter_pk: number) {
|
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
|
this.loadingTableView = true
|
||||||
|
|
||||||
let libDataset: any
|
let libDataset: any
|
||||||
@@ -961,14 +1004,25 @@ export class ViewerComponent
|
|||||||
|
|
||||||
this.loadingTableView = false
|
this.loadingTableView = false
|
||||||
|
|
||||||
//If we try to setup hot when no data is returned it errors `isDestoryed`.
|
// Setup Handsontable after async operations complete
|
||||||
//That is intorduced by HOT update
|
// Original issue: setupHot() called before API responses populated headerPks array
|
||||||
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot()
|
// 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
|
// Fix ARIA accessibility issues after data loading
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.fixAriaAccessibility()
|
this.fixAriaAccessibility()
|
||||||
}, 1500)
|
}, 500)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
||||||
@@ -988,7 +1042,7 @@ export class ViewerComponent
|
|||||||
})
|
})
|
||||||
this.hotInstanceClickListener = true
|
this.hotInstanceClickListener = true
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,12 +1170,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() {
|
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(() => {
|
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
|
this.hotInstance = this.hotTableComponent?.hotInstance
|
||||||
|
|
||||||
if (this.hotInstance) {
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
this.hotInstance.updateSettings({
|
this.hotInstance.updateSettings({
|
||||||
height: this.hotTable.height,
|
height: this.hotTable.height,
|
||||||
modifyColWidth: (width: any, col: any) => {
|
modifyColWidth: (width: any, col: any) => {
|
||||||
@@ -1129,15 +1245,29 @@ export class ViewerComponent
|
|||||||
else return width
|
else return width
|
||||||
},
|
},
|
||||||
afterGetColHeader: (col: number, th: any) => {
|
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
|
try {
|
||||||
|
const column = this.hotInstance.colToProp(col) as string
|
||||||
|
|
||||||
|
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
|
||||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||||
|
|
||||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||||
|
|
||||||
// Dark mode
|
// DARK MODE: Apply to all headers
|
||||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
} catch (error) {
|
||||||
|
// SAFETY NET: Ensure basic styling is always applied
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1153,14 +1283,18 @@ export class ViewerComponent
|
|||||||
this.fixAriaAccessibility()
|
this.fixAriaAccessibility()
|
||||||
}, 50)
|
}, 50)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix ARIA accessibility issues after table setup
|
// 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(() => {
|
setTimeout(() => {
|
||||||
this.fixAriaAccessibility()
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
}, 500)
|
this.hotInstance.render()
|
||||||
}, 1000)
|
}
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
}, 50) // Optimized Angular component readiness delay
|
||||||
|
}, 200) // Optimized main setup delay (was 600ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadWithParameters() {
|
async loadWithParameters() {
|
||||||
@@ -1233,13 +1367,27 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
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) {
|
if (this.ariaObserver) {
|
||||||
this.ariaObserver.disconnect()
|
this.ariaObserver.disconnect()
|
||||||
this.ariaObserver = undefined
|
this.ariaObserver = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the interval
|
// Clear ARIA check intervals
|
||||||
if (this.ariaCheckInterval) {
|
if (this.ariaCheckInterval) {
|
||||||
clearInterval(this.ariaCheckInterval)
|
clearInterval(this.ariaCheckInterval)
|
||||||
this.ariaCheckInterval = undefined
|
this.ariaCheckInterval = undefined
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<sasjs
|
<sasjs
|
||||||
serverUrl="http://localhost:5000"
|
serverUrl=""
|
||||||
appLoc="/Public/app/devtest"
|
appLoc="/Public/app/devtest"
|
||||||
serverType="SASJS"
|
serverType="SASJS"
|
||||||
loginMechanism="Redirected"
|
loginMechanism="Redirected"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dcfrontend",
|
"name": "dcfrontend",
|
||||||
"version": "7.2.0",
|
"version": "7.2.4",
|
||||||
"description": "Data Controller",
|
"description": "Data Controller",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||||
|
|||||||
572
sas/package-lock.json
generated
572
sas/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/cli": "^4.12.10",
|
"@sasjs/cli": "^4.12.11",
|
||||||
"@sasjs/core": "^4.59.5"
|
"@sasjs/core": "^4.59.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user