fix: remaining hot migrations - handsontable/angular-wrapper
This commit is contained in:
24
client/package-lock.json
generated
24
client/package-lock.json
generated
@@ -20,7 +20,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": "^16.0.1",
|
||||
"@handsontable/angular-wrapper": "16.0.1",
|
||||
"@sasjs/adapter": "^4.12.2",
|
||||
"@sasjs/utils": "^3.4.0",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
@@ -4714,23 +4714,23 @@
|
||||
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@handsontable/angular": {
|
||||
"node_modules/@handsontable/angular-wrapper": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@handsontable/angular/-/angular-16.0.1.tgz",
|
||||
"integrity": "sha512-AOXPntQOwga8vI7KblvlBUx/sPc60PhXDGTqAdZhji1KPUvCrd7hyjR2R1YlffETe0zcCLDptmfbUEGXjaQtEw==",
|
||||
"resolved": "https://registry.npmjs.org/@handsontable/angular-wrapper/-/angular-wrapper-16.0.1.tgz",
|
||||
"integrity": "sha512-1yK5ES5l6+uG3KjXvfd9L0RupfPC8Rq5AR0D8tYBAG+Fyhr7oVHKbBONNSS/nzZHifgr/YLrnAvutQ+EZb0FdA==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optionalDependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": ">=12.0.0",
|
||||
"@angular/common": ">=12.0.0",
|
||||
"@angular/compiler": ">=12.0.0",
|
||||
"@angular/core": ">=12.0.0",
|
||||
"@angular/forms": ">=12.0.0",
|
||||
"@angular/platform-browser": ">=12.0.0",
|
||||
"@angular/platform-browser-dynamic": ">=12.0.0",
|
||||
"@angular/router": ">=12.0.0",
|
||||
"@angular/animations": ">=16.0.0",
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/compiler": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@angular/forms": ">=16.0.0",
|
||||
"@angular/platform-browser": ">=16.0.0",
|
||||
"@angular/platform-browser-dynamic": ">=16.0.0",
|
||||
"@angular/router": ">=16.0.0",
|
||||
"handsontable": "^16.0.0"
|
||||
}
|
||||
},
|
||||
|
@@ -48,7 +48,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": "^16.0.1",
|
||||
"@handsontable/angular-wrapper": "16.0.1",
|
||||
"@sasjs/adapter": "^4.12.2",
|
||||
"@sasjs/utils": "^3.4.0",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
|
@@ -408,12 +408,11 @@
|
||||
<div class="hot-wrapper clr-flex-1">
|
||||
<hot-table
|
||||
#hotInstance
|
||||
hotId="hotInstance"
|
||||
id="hotTable"
|
||||
class="edit-hot"
|
||||
className="htDark"
|
||||
[class.hidden]="hotTable.hidden"
|
||||
[licenseKey]="hotTable.licenseKey"
|
||||
[data]="hotTable.data"
|
||||
[settings]="hotTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</div>
|
||||
|
@@ -17,7 +17,7 @@ import { SasStoreService } from '../services/sas-store.service'
|
||||
|
||||
type AOA = any[][]
|
||||
|
||||
import { HotTableRegisterer } from '@handsontable/angular'
|
||||
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'
|
||||
@@ -77,8 +77,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList()
|
||||
@ViewChildren('queryFilter')
|
||||
queryFilterCompList: QueryList<QueryComponent> = new QueryList()
|
||||
@ViewChildren('hotInstance')
|
||||
hotInstanceCompList: QueryList<Handsontable> = new QueryList()
|
||||
@ViewChild(HotTableComponent, { static: false })
|
||||
hotTableComponent!: HotTableComponent
|
||||
@ViewChildren('fileUploadInput')
|
||||
fileUploadInputCompList: QueryList<ElementRef> = new QueryList()
|
||||
|
||||
@@ -120,13 +120,26 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
public hotInstance!: Handsontable
|
||||
public dcValidator: DcValidator | undefined
|
||||
|
||||
public hotTableSettings: Handsontable.GridSettings = {}
|
||||
|
||||
private updateHotTableSettings(): void {
|
||||
this.hotTableSettings = {
|
||||
colHeaders: this.hotTable.colHeaders,
|
||||
columns: this.hotTable.columns,
|
||||
height: this.hotTable.height,
|
||||
licenseKey: this.hotTable.licenseKey,
|
||||
readOnly: this.hotTable.readOnly,
|
||||
copyPaste: this.hotTable.copyPaste,
|
||||
contextMenu: true
|
||||
}
|
||||
}
|
||||
|
||||
public hotTable: HotTableInterface = {
|
||||
data: [],
|
||||
colHeaders: [],
|
||||
hidden: true,
|
||||
columns: [],
|
||||
height: 'calc(100vh - 160px)',
|
||||
minSpareRows: 1,
|
||||
licenseKey: undefined,
|
||||
readOnly: true,
|
||||
copyPaste: {
|
||||
@@ -163,10 +176,30 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
},
|
||||
row_above: {
|
||||
name: 'Insert Row above'
|
||||
name: 'Insert Row above',
|
||||
callback: (
|
||||
key: string,
|
||||
selection: any[],
|
||||
clickEvent: MouseEvent
|
||||
) => {
|
||||
const firstSelection = selection[0]
|
||||
const targetRow = firstSelection.start.row
|
||||
|
||||
this.insertRowAtPosition(targetRow)
|
||||
}
|
||||
},
|
||||
row_below: {
|
||||
name: 'Insert Row below'
|
||||
name: 'Insert Row below',
|
||||
callback: (
|
||||
key: string,
|
||||
selection: any[],
|
||||
clickEvent: MouseEvent
|
||||
) => {
|
||||
const firstSelection = selection[0]
|
||||
const targetRow = firstSelection.start.row + 1
|
||||
|
||||
this.insertRowAtPosition(targetRow)
|
||||
}
|
||||
},
|
||||
remove_row: {
|
||||
name: 'Ignore row'
|
||||
@@ -364,15 +397,12 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
private sasService: SasService,
|
||||
private cdf: ChangeDetectorRef,
|
||||
private hotRegisterer: HotTableRegisterer,
|
||||
private spreadsheetService: SpreadsheetService
|
||||
) {
|
||||
const lang = languages[window.navigator.language]
|
||||
if (lang)
|
||||
numbro.default.registerLanguage(languages[window.navigator.language])
|
||||
|
||||
this.hotRegisterer = new HotTableRegisterer()
|
||||
|
||||
this.parseRestrictions()
|
||||
this.setRestrictions()
|
||||
}
|
||||
@@ -931,6 +961,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.cellValidationSource = []
|
||||
|
||||
// Clear custom validation styling
|
||||
this.clearDuplicateValidation()
|
||||
|
||||
const hot = this.hotInstance
|
||||
const columnSorting = hot.getPlugin('multiColumnSorting')
|
||||
const columnSortConfig = columnSorting.getSortConfig()
|
||||
@@ -991,22 +1024,54 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
setTimeout(() => {
|
||||
const hot = this.hotInstance
|
||||
|
||||
const dsInsertIndex = this.dataSource.length
|
||||
hot.alter('insert_row_below', dsInsertIndex, 1)
|
||||
// Create a new empty row object with proper structure
|
||||
const newRow = this.createEmptyRow()
|
||||
|
||||
// Add the new row to the data source
|
||||
this.dataSource.push(newRow)
|
||||
|
||||
// Update the hot table with the new data
|
||||
hot.updateSettings({ data: this.dataSource }, false)
|
||||
|
||||
// Select the newly added row
|
||||
hot.selectCell(this.dataSource.length - 1, 0)
|
||||
hot.render()
|
||||
|
||||
if (this.dataSource[dsInsertIndex]) {
|
||||
this.dataSource[dsInsertIndex]['noLinkOption'] = true
|
||||
}
|
||||
|
||||
this.addingNewRow = false
|
||||
|
||||
this.reSetCellValidationValues()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty row object with proper structure
|
||||
*/
|
||||
private createEmptyRow(): any {
|
||||
const newRow: any = {}
|
||||
this.headerColumns.forEach((col: string) => {
|
||||
newRow[col] = ''
|
||||
})
|
||||
newRow['noLinkOption'] = true
|
||||
return newRow
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new row at the specified position and updates the table
|
||||
*/
|
||||
private insertRowAtPosition(targetRow: number): void {
|
||||
const newRow = this.createEmptyRow()
|
||||
|
||||
// Insert the new row at the target position
|
||||
this.dataSource.splice(targetRow, 0, newRow)
|
||||
|
||||
// Update the hot table
|
||||
const hot = this.hotInstance
|
||||
hot.updateSettings({ data: this.dataSource }, false)
|
||||
hot.render()
|
||||
|
||||
this.reSetCellValidationValues()
|
||||
}
|
||||
|
||||
public cancelSubmit() {
|
||||
this.dataSource = this.helperService.deepClone(this.dataSourceBeforeSubmit)
|
||||
this.dataSourceBeforeSubmit = []
|
||||
@@ -1086,51 +1151,96 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public validatePrimaryKeys() {
|
||||
private clearDuplicateValidation() {
|
||||
const hot = this.hotInstance
|
||||
|
||||
const myTable = hot.getData()
|
||||
this.pkFields = []
|
||||
for (let index = 0; index < myTable.length; index++) {
|
||||
let pkRow = ''
|
||||
for (let ind = 1; ind < this.readOnlyFields + 1; ind++) {
|
||||
pkRow = pkRow + '|' + myTable[index][ind]
|
||||
}
|
||||
this.pkFields.push(pkRow)
|
||||
}
|
||||
|
||||
const results = []
|
||||
const rows = this.dataSource.length
|
||||
|
||||
for (let j = 0; j < this.pkFields.length; j++) {
|
||||
for (let i = 0; i < this.pkFields.length; i++) {
|
||||
if (this.pkFields[j] === this.pkFields[i] && i !== j) {
|
||||
results.push(i)
|
||||
// Clear previous duplicate validation styling
|
||||
for (const rowIndex of this.duplicatePkIndexes) {
|
||||
for (let col = 1; col <= this.readOnlyFields; col++) {
|
||||
hot.removeCellMeta(rowIndex, col, 'valid')
|
||||
hot.removeCellMeta(rowIndex, col, 'dupKey')
|
||||
// Remove our custom class from cell metadata
|
||||
const cellMeta = hot.getCellMeta(rowIndex, col)
|
||||
if (cellMeta.className) {
|
||||
let cleanedClassName: string
|
||||
if (Array.isArray(cellMeta.className)) {
|
||||
cleanedClassName = cellMeta.className
|
||||
.filter((c) => c !== 'dc-invalid-cell')
|
||||
.join(' ')
|
||||
} else {
|
||||
cleanedClassName = cellMeta.className
|
||||
.replace('dc-invalid-cell', '')
|
||||
.trim()
|
||||
}
|
||||
hot.setCellMeta(rowIndex, col, 'className', cleanedClassName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pkFields.length > rows) {
|
||||
for (let n = rows; n < this.pkFields.length; n++) {
|
||||
for (let p = rows; p < this.pkFields.length; p++) {
|
||||
if (n < p && this.pkFields[n] === this.pkFields[p]) {
|
||||
results.push(p)
|
||||
this.duplicatePkIndexes = []
|
||||
hot.render()
|
||||
}
|
||||
|
||||
public validatePrimaryKeys() {
|
||||
const hot = this.hotInstance
|
||||
|
||||
// Clear previous validation before applying new ones
|
||||
this.clearDuplicateValidation()
|
||||
|
||||
// Get data from the data source instead of hot.getData() to ensure consistency
|
||||
const myTable = this.dataSource
|
||||
this.pkFields = []
|
||||
|
||||
for (let index = 0; index < myTable.length; index++) {
|
||||
let pkRow = ''
|
||||
for (let ind = 1; ind < this.readOnlyFields + 1; ind++) {
|
||||
const colName = this.headerColumns[ind]
|
||||
const value = myTable[index][colName] || ''
|
||||
pkRow = pkRow + '|' + value
|
||||
}
|
||||
this.pkFields.push(pkRow)
|
||||
}
|
||||
|
||||
const results: any = []
|
||||
const rows = this.dataSource.length
|
||||
|
||||
// Only check for duplicates if we have data
|
||||
if (this.pkFields.length > 0) {
|
||||
for (let j = 0; j < this.pkFields.length; j++) {
|
||||
for (let i = 0; i < this.pkFields.length; i++) {
|
||||
if (
|
||||
this.pkFields[j] === this.pkFields[i] &&
|
||||
i !== j &&
|
||||
this.pkFields[j] !== '|'
|
||||
) {
|
||||
results.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cellMeta
|
||||
// Clear any existing validation marks for all cells
|
||||
for (let row = 0; row < myTable.length; row++) {
|
||||
for (let col = 0; col < this.headerColumns.length; col++) {
|
||||
const cellMeta = hot.getCellMeta(row, col)
|
||||
if (cellMeta) {
|
||||
cellMeta.valid = true
|
||||
cellMeta.dupKey = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark duplicate cells as invalid
|
||||
for (let k = 0; k < results.length; k++) {
|
||||
for (let index = 1; index < this.readOnlyFields + 1; index++) {
|
||||
cellMeta = hot.getCellMeta(results[k], index)
|
||||
cellMeta.valid = false
|
||||
cellMeta.dupKey = true
|
||||
hot.render()
|
||||
hot.setCellMeta(results[k], index, 'valid', false)
|
||||
hot.setCellMeta(results[k], index, 'dupKey', true)
|
||||
hot.setCellMeta(results[k], index, 'className', 'dc-invalid-cell')
|
||||
}
|
||||
}
|
||||
|
||||
this.duplicatePkIndexes = [...new Set(results.sort())]
|
||||
hot.render()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1425,10 +1535,26 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.dataSourceBeforeSubmit = this.helperService.deepClone(this.dataSource)
|
||||
|
||||
// Clean up the data source by removing noLinkOption property
|
||||
for (let i = 0; i < this.dataSource.length; i++) {
|
||||
delete this.dataSource[i].noLinkOption
|
||||
}
|
||||
|
||||
// Remove any completely empty rows from the end
|
||||
while (this.dataSource.length > 0) {
|
||||
const lastRow = this.dataSource[this.dataSource.length - 1]
|
||||
const isEmpty = Object.keys(lastRow).every((key) => {
|
||||
if (key === '_____DELETE__THIS__RECORD_____') return true
|
||||
return !lastRow[key] || lastRow[key] === ''
|
||||
})
|
||||
|
||||
if (isEmpty) {
|
||||
this.dataSource.pop()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hot.updateSettings(
|
||||
{
|
||||
data: this.dataSource,
|
||||
@@ -1446,17 +1572,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
EditorComponent.cnt = 0
|
||||
EditorComponent.nonPkCnt = 0
|
||||
// this.saveLoading = true;
|
||||
|
||||
/**
|
||||
* Below code should be analized, not sure what is the purpose of exceedCells
|
||||
*/
|
||||
const myTableData = hot.getData()
|
||||
|
||||
// If the last row is empty, remove it before validation
|
||||
if (myTableData.length > 1 && hot.isEmptyRow(myTableData.length - 1)) {
|
||||
hot.alter('remove_row', myTableData.length - 1)
|
||||
}
|
||||
|
||||
this.validatePrimaryKeys()
|
||||
|
||||
@@ -1486,15 +1601,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (txt) txt.focus()
|
||||
}, 200)
|
||||
})
|
||||
|
||||
// let cnt = 0;
|
||||
// hot.addHook("afterValidate", () => {
|
||||
// this.updateSoftSelectColumns(true);
|
||||
// cnt++;
|
||||
// if (cnt === long) {
|
||||
// this.validationDone = 1;
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
public async saveTable(data: any) {
|
||||
@@ -1648,11 +1754,20 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
public checkInvalid() {
|
||||
const hotElement = (this.hotInstanceCompList.first.container as any)
|
||||
.nativeElement
|
||||
const invalidCells = hotElement.querySelectorAll('.htInvalid')
|
||||
// Use Angular wrapper to access Handsontable element instead of DOM queries
|
||||
if (!this.hotTableComponent || !this.hotTableComponent.hotInstance)
|
||||
return false
|
||||
|
||||
return invalidCells.length > 0
|
||||
const hotElement = this.hotTableComponent.hotInstance.rootElement
|
||||
if (!hotElement) return false
|
||||
|
||||
// Check for standard Handsontable validation failures
|
||||
const standardInvalidCells = hotElement.querySelectorAll('.htInvalid')
|
||||
|
||||
// Check for our custom duplicate primary key validation failures
|
||||
const customInvalidCells = hotElement.querySelectorAll('.dc-invalid-cell')
|
||||
|
||||
return standardInvalidCells.length > 0 || customInvalidCells.length > 0
|
||||
}
|
||||
|
||||
public goToEditor() {
|
||||
@@ -2192,6 +2307,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
*/
|
||||
private setCellFilter(filter: boolean) {
|
||||
const hotSelected = this.hotInstance.getSelected()
|
||||
if (!hotSelected) return
|
||||
const selection = hotSelected ? hotSelected[0] : hotSelected
|
||||
|
||||
// When we open a dropdown we want filter disabled so value in cell
|
||||
@@ -2216,9 +2332,13 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// Initialize hot table settings
|
||||
this.updateHotTableSettings()
|
||||
|
||||
this.licenceService.hot_license_key.subscribe(
|
||||
(hot_license_key: string | undefined) => {
|
||||
this.hotTable.licenseKey = hot_license_key
|
||||
this.updateHotTableSettings() // Update settings when license key changes
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2276,6 +2396,27 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 1000)
|
||||
|
||||
// Set up event listener for hot table element
|
||||
// Double click to edit
|
||||
setTimeout(() => {
|
||||
if (this.hotTableComponent && this.hotTableComponent.hotInstance) {
|
||||
const hotElement = this.hotTableComponent.hotInstance.rootElement
|
||||
if (hotElement) {
|
||||
hotElement.addEventListener('mousedown', (event: MouseEvent) => {
|
||||
if (!this.uploadPreview) {
|
||||
this.hotClicked()
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const menuDebugItem: any =
|
||||
document.querySelector('.debug-switch-item') || undefined
|
||||
if (menuDebugItem) menuDebugItem.click()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -2440,11 +2581,12 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
initSetup(response: EditorsGetDataServiceResponse) {
|
||||
this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
|
||||
this.hotInstance = this.hotTableComponent!.hotInstance!
|
||||
|
||||
if (this.getdataError) return
|
||||
if (!response) return
|
||||
if (!response.data) return
|
||||
if (!this.hotInstance) return
|
||||
|
||||
this.cols = response.data.cols
|
||||
this.dsmeta = response.data.dsmeta
|
||||
@@ -2464,7 +2606,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.dsNote = ''
|
||||
}
|
||||
|
||||
const hot: Handsontable = this.hotInstance
|
||||
const hot = this.hotInstance
|
||||
|
||||
const approvers: Approver[] = response.data.approvers
|
||||
|
||||
@@ -2585,6 +2727,11 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
rowHeights: 24,
|
||||
maxRows: this.licenceState.value.editor_rows_allowed || Infinity,
|
||||
invalidCellClassName: 'htInvalid',
|
||||
// Prevent automatic row creation
|
||||
autoWrapRow: false,
|
||||
autoWrapCol: false,
|
||||
// Ensure proper data binding
|
||||
bindRowsWithHeaders: false,
|
||||
dropdownMenu: {
|
||||
items: {
|
||||
make_read_only: {
|
||||
@@ -2659,7 +2806,51 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
cellProperties: Handsontable.CellProperties
|
||||
) => {
|
||||
const isReadonlyCol = col && this.isReadonlyCol(col)
|
||||
if (isReadonlyCol) cellProperties.className = 'readonlyCell'
|
||||
|
||||
// Check if this cell should be marked as invalid due to duplicate primary key values
|
||||
// Only applies to primary key columns (col 1 through readOnlyFields)
|
||||
const isDuplicateCell =
|
||||
this.duplicatePkIndexes.includes(row) &&
|
||||
col >= 1 &&
|
||||
col <= this.readOnlyFields
|
||||
|
||||
// Handle existing CSS classes - Handsontable can provide className as string or array
|
||||
const existingClasses = cellProperties.className || ''
|
||||
let classes: string[]
|
||||
|
||||
if (Array.isArray(existingClasses)) {
|
||||
// If already an array, create a copy
|
||||
classes = [...existingClasses]
|
||||
} else {
|
||||
// If string, split by spaces and filter out empty strings
|
||||
classes = existingClasses
|
||||
.split(' ')
|
||||
.filter((c: string) => c.length > 0)
|
||||
}
|
||||
|
||||
// Add readonlyCell class for readonly columns to maintain original styling
|
||||
if (isReadonlyCol && !classes.includes('readonlyCell')) {
|
||||
classes.push('readonlyCell')
|
||||
}
|
||||
|
||||
// Apply custom validation styling for duplicate primary key cells
|
||||
// Note: Uses 'dc-invalid-cell' instead of Handsontable's 'htInvalid' class
|
||||
// because Handsontable's internal validation system was removing 'htInvalid'
|
||||
// causing flickering. Our custom class persists reliably.
|
||||
if (isDuplicateCell) {
|
||||
if (!classes.includes('dc-invalid-cell')) {
|
||||
classes.push('dc-invalid-cell')
|
||||
}
|
||||
// Mark cell as invalid to prevent form submission
|
||||
cellProperties.valid = false
|
||||
// Custom flag to identify this as a duplicate key cell for cleanup
|
||||
cellProperties.dupKey = true
|
||||
}
|
||||
|
||||
// Apply the combined CSS classes back to the cell
|
||||
if (classes.length > 0) {
|
||||
cellProperties.className = classes.join(' ')
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
@@ -2678,22 +2869,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.columnHeader[0] = 'Delete?'
|
||||
this.readOnlyFields = response.data.sasparams[0].PKCNT
|
||||
|
||||
const hotInstaceEl = document.getElementById('hotInstance')
|
||||
|
||||
if (hotInstaceEl) {
|
||||
hotInstaceEl.addEventListener('mousedown', (event) => {
|
||||
if (!this.uploadPreview) {
|
||||
this.hotClicked()
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const menuDebugItem: any =
|
||||
document.querySelector('.debug-switch-item') || undefined
|
||||
if (menuDebugItem) menuDebugItem.click()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
hot.addHook(
|
||||
'afterSelection',
|
||||
(
|
||||
@@ -2796,6 +2971,21 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
})
|
||||
|
||||
// Add hook to prevent unwanted row creation
|
||||
hot.addHook(
|
||||
'beforeCreateRow',
|
||||
(index: number, amount: number, source?: any) => {
|
||||
// Only allow row creation through the Add Row button or context menu
|
||||
if (
|
||||
!this.addingNewRow &&
|
||||
source !== 'ContextMenu.insert_row_above' &&
|
||||
source !== 'ContextMenu.insert_row_below'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
hot.addHook('beforePaste', (data: any, cords: any) => {
|
||||
const startCol = cords[0].startCol
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
import { AppSharedModule } from '../app-shared.module'
|
||||
import { DirectivesModule } from '../directives/directives.module'
|
||||
@@ -28,7 +28,7 @@ registerAllModules()
|
||||
FormsModule,
|
||||
EditorRoutingModule,
|
||||
ClarityModule,
|
||||
HotTableModule.forRoot(),
|
||||
HotTableModule,
|
||||
AppSharedModule,
|
||||
DirectivesModule,
|
||||
SharedModule,
|
||||
|
@@ -166,13 +166,9 @@
|
||||
>
|
||||
|
||||
<hot-table
|
||||
hotId="hotInstanceUserDataset"
|
||||
id="hotTableUserDataset"
|
||||
class="mt-15"
|
||||
[afterGetColHeader]="afterGetColHeader"
|
||||
[settings]="hotUserDatasets"
|
||||
[licenseKey]="hotTableLicenseKey"
|
||||
stretchH="all"
|
||||
[settings]="hotUserDatasetsSettings"
|
||||
>
|
||||
</hot-table>
|
||||
|
||||
@@ -360,17 +356,9 @@
|
||||
</div>
|
||||
|
||||
<hot-table
|
||||
hotId="hotInstance"
|
||||
id="hotTable"
|
||||
class="mt-15"
|
||||
[afterGetColHeader]="afterGetColHeader"
|
||||
[className]="['htDark', 'htCustomHidden']"
|
||||
[licenseKey]="hotTableLicenseKey"
|
||||
[multiColumnSorting]="true"
|
||||
[viewportRowRenderingOffset]="50"
|
||||
[manualColumnResize]="true"
|
||||
[filters]="true"
|
||||
stretchH="all"
|
||||
[settings]="hotMainTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</ng-container>
|
||||
|
@@ -22,7 +22,7 @@ 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 { HotTableRegisterer } from '@handsontable/angular'
|
||||
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'
|
||||
@@ -89,7 +89,10 @@ export class MultiDatasetComponent implements OnInit {
|
||||
|
||||
public hotInstance!: Handsontable
|
||||
public hotInstanceUserDataset!: Handsontable
|
||||
private hotRegisterer: HotTableRegisterer
|
||||
@ViewChild('hotInstanceMain', { static: false })
|
||||
hotTableMainComponent!: HotTableComponent
|
||||
@ViewChild('hotInstanceUserDataset', { static: false })
|
||||
hotTableUserDatasetComponent!: HotTableComponent
|
||||
|
||||
public showSubmitReasonModal: boolean = false
|
||||
public submitReasonMessage: string = ''
|
||||
@@ -136,7 +139,29 @@ export class MultiDatasetComponent implements OnInit {
|
||||
}
|
||||
},
|
||||
manualRowMove: true,
|
||||
columnSorting: true
|
||||
columnSorting: true,
|
||||
afterGetColHeader: baseAfterGetColHeader,
|
||||
stretchH: 'all'
|
||||
}
|
||||
|
||||
get hotMainTableSettings(): Handsontable.GridSettings {
|
||||
return {
|
||||
className: ['htDark', 'htCustomHidden'],
|
||||
licenseKey: this.hotTableLicenseKey,
|
||||
multiColumnSorting: true,
|
||||
viewportRowRenderingOffset: 50,
|
||||
manualColumnResize: true,
|
||||
filters: true,
|
||||
stretchH: 'all',
|
||||
afterGetColHeader: baseAfterGetColHeader
|
||||
}
|
||||
}
|
||||
|
||||
get hotUserDatasetsSettings(): Handsontable.GridSettings {
|
||||
return {
|
||||
...this.hotUserDatasets,
|
||||
licenseKey: this.hotTableLicenseKey
|
||||
}
|
||||
}
|
||||
|
||||
public afterGetColHeader = baseAfterGetColHeader
|
||||
@@ -149,9 +174,7 @@ export class MultiDatasetComponent implements OnInit {
|
||||
private spreadsheetService: SpreadsheetService,
|
||||
private sasService: SasService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {
|
||||
this.hotRegisterer = new HotTableRegisterer()
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.licenceService.hot_license_key.subscribe(
|
||||
@@ -392,7 +415,7 @@ export class MultiDatasetComponent implements OnInit {
|
||||
|
||||
initHot() {
|
||||
setTimeout(() => {
|
||||
this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
|
||||
this.hotInstance = this.hotTableMainComponent!.hotInstance!
|
||||
|
||||
// Set height of parsed data to full height of the page content area
|
||||
const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight
|
||||
@@ -413,9 +436,8 @@ export class MultiDatasetComponent implements OnInit {
|
||||
|
||||
initUserInputHot() {
|
||||
setTimeout(() => {
|
||||
this.hotInstanceUserDataset = this.hotRegisterer.getInstance(
|
||||
'hotInstanceUserDataset'
|
||||
)
|
||||
this.hotInstanceUserDataset =
|
||||
this.hotTableUserDatasetComponent!.hotInstance!
|
||||
|
||||
this.hotInstanceUserDataset.addHook(
|
||||
'beforeChange',
|
||||
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
import { AppSharedModule } from '../app-shared.module'
|
||||
import { DirectivesModule } from '../directives/directives.module'
|
||||
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { DirectivesModule } from '../directives/directives.module'
|
||||
import { SharedModule } from '../shared/shared.module'
|
||||
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
||||
@@ -23,7 +23,7 @@ import { HistoryComponent } from './history/history.component'
|
||||
FormsModule,
|
||||
ReviewRoutingModule,
|
||||
ClarityModule,
|
||||
HotTableModule.forRoot(),
|
||||
HotTableModule,
|
||||
DirectivesModule,
|
||||
SharedModule
|
||||
]
|
||||
|
@@ -365,13 +365,18 @@ export class SasService {
|
||||
}
|
||||
},
|
||||
(err: any) => {
|
||||
if (err.error.includes('Unauthorized')) {
|
||||
const errorMessage =
|
||||
typeof err.error === 'string'
|
||||
? err.error
|
||||
: JSON.stringify(err.error || err)
|
||||
|
||||
if (errorMessage.includes('Unauthorized')) {
|
||||
this.shouldLogin.next(true)
|
||||
|
||||
this.shouldLogin.subscribe((res: boolean) => {
|
||||
if (res === false) location.reload()
|
||||
})
|
||||
} else if (err.error.includes(`Folder doesn't exist.`)) {
|
||||
} else if (errorMessage.includes(`Folder doesn't exist.`)) {
|
||||
console.warn(
|
||||
'SASjs SAS services are not present on the current appLoc.'
|
||||
)
|
||||
@@ -419,7 +424,11 @@ export class SasService {
|
||||
}
|
||||
},
|
||||
(err: any) => {
|
||||
if (err.error.includes(`Folder doesn't exist.`)) {
|
||||
const errorMessage =
|
||||
typeof err.error === 'string'
|
||||
? err.error
|
||||
: JSON.stringify(err.error || err)
|
||||
if (errorMessage.includes(`Folder doesn't exist.`)) {
|
||||
reject()
|
||||
}
|
||||
}
|
||||
|
@@ -386,27 +386,9 @@
|
||||
</div>
|
||||
|
||||
<hot-table
|
||||
*ngIf="viewboxTableIndex > -1"
|
||||
[hotId]="'hotInstance_viewbox_' + viewbox.id"
|
||||
id="hotTable"
|
||||
className="htDark"
|
||||
[readOnly]="true"
|
||||
[modifyColWidth]="maxWidthCheker"
|
||||
[copyPaste]="viewboxTables[viewboxTableIndex].hotTable.copyPaste"
|
||||
[contextMenu]="viewboxTables[viewboxTableIndex].hotTable.contextMenu"
|
||||
[multiColumnSorting]="true"
|
||||
[viewportRowRenderingOffset]="50"
|
||||
[data]="viewboxTables[viewboxTableIndex].hotTable.data"
|
||||
[colHeaders]="viewboxTables[viewboxTableIndex].hotTable.colHeaders"
|
||||
[columns]="viewboxTables[viewboxTableIndex].hotTable.columns"
|
||||
[filters]="true"
|
||||
[dropdownMenu]="viewboxTables[viewboxTableIndex].hotTable.dropdownMenu"
|
||||
[height]="calculateTableHeight(viewbox)"
|
||||
stretchH="all"
|
||||
[cells]="viewboxTables[viewboxTableIndex].hotTable.cells"
|
||||
[maxRows]="viewboxTables[viewboxTableIndex].hotTable.maxRows"
|
||||
[manualColumnResize]="true"
|
||||
[licenseKey]="viewboxTables[viewboxTableIndex].hotTable.licenseKey"
|
||||
*ngIf="viewboxTableIndex > -1 && viewboxHotSettings.get(viewbox.id)"
|
||||
[settings]="viewboxHotSettings.get(viewbox.id) || {}"
|
||||
[id]="'hotTable_' + viewbox.id"
|
||||
></hot-table>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -21,9 +21,9 @@ import {
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { HotTableRegisterer } from '@handsontable/angular'
|
||||
import { SASjsConfig } from '@sasjs/adapter'
|
||||
import Handsontable from 'handsontable'
|
||||
import { HotTableComponent } from '@handsontable/angular-wrapper'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { FilterQuery, FilterGroup } from 'src/app/models/FilterQuery'
|
||||
@@ -54,6 +54,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
|
||||
@ViewChildren('dragHandleCorner')
|
||||
dragHandleCornerQuery!: QueryList<ElementRef>
|
||||
@ViewChildren(HotTableComponent)
|
||||
hotTableComponents!: QueryList<HotTableComponent>
|
||||
|
||||
private _viewboxModal: boolean = false
|
||||
get viewboxModal(): boolean {
|
||||
@@ -119,8 +121,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
licenseKey: undefined,
|
||||
dropdownMenu: undefined
|
||||
}
|
||||
|
||||
public viewboxHotSettings: Map<number, Handsontable.GridSettings> = new Map()
|
||||
public viewboxTables: ViewboxTable[] = []
|
||||
private hotTableRegisterer: HotTableRegisterer
|
||||
|
||||
public filteringViewbox: Viewbox | undefined
|
||||
|
||||
@@ -150,9 +153,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private cdf: ChangeDetectorRef
|
||||
) {
|
||||
this.hotTableRegisterer = new HotTableRegisterer()
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Load libraries
|
||||
@@ -207,7 +208,17 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
//set handles for box resize
|
||||
// Set handles for box resize and ensure HOT components are properly initialized
|
||||
setTimeout(() => {
|
||||
this.setAllHandleTransform()
|
||||
|
||||
// Force refresh of any existing HOT instances after view init
|
||||
this.viewboxes.forEach((viewbox) => {
|
||||
if (this.getViewboxTableIndex(viewbox) > -1) {
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Maximum number of open viewboxes reached
|
||||
@@ -304,6 +315,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (viewboxTable) {
|
||||
viewboxTable.hotTable.data = res.viewdata
|
||||
|
||||
// Update settings with new data
|
||||
this.createViewboxTableSettings(viewbox)
|
||||
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve(null)
|
||||
@@ -413,6 +427,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
viewbox.query = this.helperService.deepClone(res.query)
|
||||
viewbox.filterText = res.sasparams[0].FILTER_TEXT
|
||||
|
||||
// Create settings for this viewbox
|
||||
this.createViewboxTableSettings(viewbox)
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateHotColumns(
|
||||
viewboxTable!.hotTable.colHeadersHidden || [],
|
||||
@@ -421,30 +438,34 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// HOT Settings are bound in HTML but some settings due to timing issues
|
||||
// requires to be updated after the HOT is instanced
|
||||
// after the update `render` method is called
|
||||
const hotInstance = this.getViewboxHotInstance(viewbox.id)
|
||||
// Use a longer timeout to ensure the HOT component is fully initialized
|
||||
setTimeout(() => {
|
||||
const hotInstance = this.getViewboxHotInstance(viewbox.id)
|
||||
|
||||
hotInstance?.updateSettings({
|
||||
manualColumnMove: viewboxTable!.hotTable.manualColumnMove,
|
||||
afterGetColHeader: (col: number, th: any) => {
|
||||
const column = hotInstance?.colToProp(col) as string
|
||||
if (hotInstance) {
|
||||
hotInstance.updateSettings({
|
||||
manualColumnMove: viewboxTable!.hotTable.manualColumnMove,
|
||||
afterGetColHeader: (col: number, th: any) => {
|
||||
const column = hotInstance?.colToProp(col) as string
|
||||
|
||||
// header columns styling - primary keys
|
||||
const isPKCol =
|
||||
column &&
|
||||
viewboxTable!.hotTable.headerPks.indexOf(column) > -1
|
||||
// header columns styling - primary keys
|
||||
const isPKCol =
|
||||
column &&
|
||||
viewboxTable!.hotTable.headerPks.indexOf(column) > -1
|
||||
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
}
|
||||
})
|
||||
hotInstance.render()
|
||||
}
|
||||
})
|
||||
hotInstance?.render()
|
||||
|
||||
if (this.selectedViewbox) {
|
||||
this.resetSelectedViewbox(viewbox)
|
||||
}
|
||||
})
|
||||
if (this.selectedViewbox) {
|
||||
this.resetSelectedViewbox(viewbox)
|
||||
}
|
||||
}, 500)
|
||||
}, 100)
|
||||
|
||||
resolve()
|
||||
})
|
||||
@@ -490,6 +511,68 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and store Handsontable settings for a specific viewbox
|
||||
* @param viewbox
|
||||
*/
|
||||
private createViewboxTableSettings(viewbox: Viewbox): void {
|
||||
const viewboxTableIndex = this.getViewboxTableIndex(viewbox)
|
||||
if (viewboxTableIndex === -1) {
|
||||
this.viewboxHotSettings.set(viewbox.id, {})
|
||||
return
|
||||
}
|
||||
|
||||
const viewboxTable = this.viewboxTables[viewboxTableIndex]
|
||||
const calculatedHeight = this.calculateTableHeight(viewbox)
|
||||
|
||||
// HOT v16 settings - data will be loaded manually after initialization
|
||||
const settings: Handsontable.GridSettings = {
|
||||
colHeaders: viewboxTable.hotTable.colHeaders,
|
||||
columns: viewboxTable.hotTable.columns,
|
||||
height: calculatedHeight,
|
||||
readOnly: true,
|
||||
modifyColWidth: this.maxWidthCheker,
|
||||
copyPaste: viewboxTable.hotTable.copyPaste,
|
||||
contextMenu: viewboxTable.hotTable.contextMenu,
|
||||
multiColumnSorting: true,
|
||||
viewportRowRenderingOffset: 50,
|
||||
filters: true,
|
||||
dropdownMenu: viewboxTable.hotTable.dropdownMenu,
|
||||
stretchH: 'all',
|
||||
cells: viewboxTable.hotTable.cells,
|
||||
maxRows: viewboxTable.hotTable.maxRows || Infinity,
|
||||
manualColumnResize: true,
|
||||
rowHeaders: true,
|
||||
licenseKey: viewboxTable.hotTable.licenseKey
|
||||
}
|
||||
|
||||
// Force a new object reference to trigger change detection
|
||||
this.viewboxHotSettings.set(viewbox.id, { ...settings })
|
||||
|
||||
// Force change detection to ensure the template updates
|
||||
setTimeout(() => {
|
||||
this.cdf.detectChanges()
|
||||
|
||||
// Try to get the HOT instance and force a render
|
||||
setTimeout(() => {
|
||||
const hotInstance = this.getViewboxHotInstance(viewbox.id)
|
||||
if (hotInstance) {
|
||||
// Load data manually - this is required for HOT v16 Angular wrapper
|
||||
hotInstance.loadData(viewboxTable.hotTable.data)
|
||||
hotInstance.render()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored Handsontable settings for a specific viewbox
|
||||
* @param viewbox
|
||||
*/
|
||||
getViewboxTableSettings(viewbox: Viewbox): Handsontable.GridSettings {
|
||||
return this.viewboxHotSettings.get(viewbox.id) || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewbox resize
|
||||
* @param dragHandle
|
||||
@@ -514,8 +597,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.viewboxChanged()
|
||||
this.eventService.dispatchEvent('resize')
|
||||
|
||||
// Refresh all viewbox tables after resize
|
||||
// Refresh all viewbox tables after resize and update their settings
|
||||
this.viewboxes.forEach((viewbox) => {
|
||||
// Settings will include updated heights when accessed
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
})
|
||||
})
|
||||
@@ -680,6 +764,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// Refresh all tables after snap to grid
|
||||
this.viewboxes.forEach((viewbox) => {
|
||||
// Settings will include correct height when accessed
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
})
|
||||
})
|
||||
@@ -726,6 +811,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// Refresh table after restoring
|
||||
setTimeout(() => {
|
||||
// Settings will include correct height when accessed
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
}, 100)
|
||||
}
|
||||
@@ -741,6 +827,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// Refresh table after expanding
|
||||
setTimeout(() => {
|
||||
// Settings will include correct height when accessed
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
}, 100)
|
||||
}
|
||||
@@ -759,6 +846,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (index > -1) this.viewboxes.splice(index, 1)
|
||||
if (viewtableIndex > -1) this.viewboxTables.splice(viewtableIndex, 1)
|
||||
|
||||
// Clean up settings for this viewbox
|
||||
this.viewboxHotSettings.delete(viewbox.id)
|
||||
|
||||
if (this.selectedViewbox?.id === viewbox.id) {
|
||||
this.unsetSelectedViewbox()
|
||||
}
|
||||
@@ -1056,6 +1146,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
viewboxTable.hotTable.data = res.viewdata
|
||||
|
||||
// Update settings with new data
|
||||
this.createViewboxTableSettings(viewbox)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.loggerService.error(err)
|
||||
@@ -1084,6 +1177,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.updateHiddenColumnsHot(hiddenColProps, viewboxId)
|
||||
|
||||
this.setColumnOrder(viewboxId)
|
||||
|
||||
// Settings will be regenerated when accessed
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1179,8 +1274,6 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* WORKAROUND: This is a workaround to calculate the height of the table since `100%`
|
||||
* makes hot not load
|
||||
* Calculate available height for Handsontable
|
||||
* @param viewbox The viewbox to calculate height for
|
||||
* @returns Available height in pixels
|
||||
@@ -1188,9 +1281,13 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
calculateTableHeight(viewbox: Viewbox): number {
|
||||
// Calculate the exact height of the content div
|
||||
const dragHandleHeight = 20
|
||||
const searchFormHeight = 38
|
||||
// Return the exact remaining height for the table
|
||||
return viewbox.height - dragHandleHeight - searchFormHeight
|
||||
const searchFormHeight = 36
|
||||
const padding = 2
|
||||
// Return the exact remaining height for the table with minimum height
|
||||
const calculatedHeight =
|
||||
viewbox.height - dragHandleHeight - searchFormHeight - padding
|
||||
|
||||
return calculatedHeight
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1202,7 +1299,30 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (hotInstance) {
|
||||
// Force the table to recalculate its dimensions
|
||||
setTimeout(() => {
|
||||
hotInstance.refreshDimensions()
|
||||
try {
|
||||
// Update height setting and refresh
|
||||
hotInstance.updateSettings({
|
||||
height: this.calculateTableHeight(viewbox)
|
||||
})
|
||||
hotInstance.refreshDimensions()
|
||||
hotInstance.render()
|
||||
} catch (error) {
|
||||
// If refresh fails, try again later
|
||||
setTimeout(() => {
|
||||
try {
|
||||
hotInstance.updateSettings({
|
||||
height: this.calculateTableHeight(viewbox)
|
||||
})
|
||||
hotInstance.refreshDimensions()
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Failed to refresh HOT dimensions for viewbox',
|
||||
viewbox.id,
|
||||
e
|
||||
)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
@@ -1213,13 +1333,27 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
* @returns HOT Instance from the given Viewbox
|
||||
*/
|
||||
private getViewboxHotInstance(viewboxId?: number): Handsontable | undefined {
|
||||
if (!viewboxId) return
|
||||
if (!viewboxId || !this.hotTableComponents) return
|
||||
|
||||
const hotInstance = this.hotTableRegisterer.getInstance(
|
||||
`hotInstance_viewbox_${viewboxId}`
|
||||
)
|
||||
// Find the component corresponding to this viewbox
|
||||
// Since we only show one table per viewbox and they're rendered in order,
|
||||
// we can match by the viewbox's position in the array
|
||||
const viewboxIndex = this.viewboxes.findIndex((vb) => vb.id === viewboxId)
|
||||
if (viewboxIndex === -1) return
|
||||
|
||||
return hotInstance
|
||||
// Get the HOT component at this index
|
||||
const hotComponents = this.hotTableComponents.toArray()
|
||||
let hotComponentIndex = 0
|
||||
|
||||
// Count how many viewboxes before this one have loaded tables
|
||||
for (let i = 0; i < viewboxIndex; i++) {
|
||||
if (this.getViewboxTableIndex(this.viewboxes[i]) > -1) {
|
||||
hotComponentIndex++
|
||||
}
|
||||
}
|
||||
|
||||
const hotTableComponent = hotComponents[hotComponentIndex]
|
||||
return hotTableComponent?.hotInstance || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,7 +4,11 @@ import { ClarityModule } from '@clr/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ViewboxesComponent } from './viewboxes.component'
|
||||
import { QueryModule } from 'src/app/query/query.module'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
|
||||
// register Handsontable's modules
|
||||
registerAllModules()
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { AutocompleteModule } from '../autocomplete/autocomplete.module'
|
||||
import { DcTreeModule } from '../dc-tree/dc-tree.module'
|
||||
|
@@ -125,19 +125,9 @@
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<hot-table
|
||||
hotId="hotInstance"
|
||||
id="hotTable"
|
||||
className="htDark"
|
||||
[data]="hotTable.data"
|
||||
[colHeaders]="hotTable.colHeaders"
|
||||
[columns]="hotTable.columns"
|
||||
[maxRows]="hotTable.maxRows"
|
||||
[height]="hotTable.height"
|
||||
[licenseKey]="hotTable.licenseKey"
|
||||
[afterGetColHeader]="hotTable.afterGetColHeader"
|
||||
stretchH="all"
|
||||
[cells]="hotTable.cells"
|
||||
[settings]="hotTable.settings"
|
||||
[settings]="hotTableSettings"
|
||||
aria-label="Staged data table"
|
||||
>
|
||||
<!--[licenseKey]=null-->
|
||||
|
@@ -11,6 +11,7 @@ import { ActivatedRoute } from '@angular/router'
|
||||
import { SasService } from '../services/sas.service'
|
||||
import { EventService } from '../services/event.service'
|
||||
import { HotTableInterface } from '../models/HotTable.interface'
|
||||
import Handsontable from 'handsontable'
|
||||
import { LicenceService } from '../services/licence.service'
|
||||
import { globals } from '../_globals'
|
||||
import { EditorsRestoreServiceResponse } from '../models/sas/editors-restore.model'
|
||||
@@ -61,6 +62,22 @@ export class StageComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
get hotTableSettings(): Handsontable.GridSettings {
|
||||
return {
|
||||
...this.hotTable.settings,
|
||||
colHeaders: this.hotTable.colHeaders,
|
||||
columns: this.hotTable.columns,
|
||||
maxRows: this.hotTable.maxRows,
|
||||
height: this.hotTable.height,
|
||||
licenseKey: this.hotTable.licenseKey,
|
||||
afterGetColHeader: this.hotTable.afterGetColHeader,
|
||||
afterInit: this.hotTable.afterInit,
|
||||
stretchH: 'all',
|
||||
cells: this.hotTable.cells,
|
||||
className: 'htDark'
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private licenceService: LicenceService,
|
||||
private sasStoreService: SasStoreService,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { StageComponent } from './stage.component'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
|
@@ -621,33 +621,16 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="!noData && !noDataReqErr && table" class="clr-flex-1">
|
||||
<hot-table
|
||||
#hotInstance
|
||||
hotId="hotInstance"
|
||||
id="hotTable"
|
||||
className="htDark"
|
||||
[multiColumnSorting]="true"
|
||||
[viewportRowRenderingOffset]="50"
|
||||
[data]="hotTable.data"
|
||||
[colHeaders]="hotTable.colHeaders"
|
||||
[columns]="hotTable.columns"
|
||||
[copyPaste]="hotTable.copyPaste"
|
||||
[contextMenu]="hotTable.contextMenu"
|
||||
[filters]="true"
|
||||
[dropdownMenu]="hotTable.dropdownMenu"
|
||||
[height]="hotTable.height"
|
||||
stretchH="all"
|
||||
[modifyColWidth]="maxWidthCheker"
|
||||
[cells]="hotTable.cells"
|
||||
[maxRows]="hotTable.maxRows"
|
||||
[manualColumnResize]="true"
|
||||
[afterGetColHeader]="hotTable.afterGetColHeader"
|
||||
[rowHeaders]="hotTable.rowHeaders"
|
||||
[rowHeaderWidth]="hotTable.rowHeaderWidth"
|
||||
[rowHeights]="hotTable.rowHeights"
|
||||
[licenseKey]="hotTable.licenseKey"
|
||||
>
|
||||
</hot-table>
|
||||
<div class="hot-wrapper clr-flex-1">
|
||||
<hot-table
|
||||
#hotInstance
|
||||
id="hotTable"
|
||||
class="view-hot"
|
||||
[data]="hotTable.data"
|
||||
[settings]="hotTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@@ -18,7 +18,7 @@ import { globals } from '../_globals'
|
||||
|
||||
import { EventService } from '../services/event.service'
|
||||
import { HelperService } from '../services/helper.service'
|
||||
import { HotTableRegisterer } from '@handsontable/angular'
|
||||
import { HotTableComponent } from '@handsontable/angular-wrapper'
|
||||
import { SasService } from '../services/sas.service'
|
||||
import { SASjsConfig } from '@sasjs/adapter'
|
||||
import { QueryComponent } from '../query/query.component'
|
||||
@@ -102,7 +102,35 @@ export class ViewerComponent
|
||||
public sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||
public searchLoading: boolean = false
|
||||
public searchNumeric: boolean = false
|
||||
private hotTableRegisterer: HotTableRegisterer
|
||||
@ViewChild(HotTableComponent, { static: false })
|
||||
hotTableComponent!: HotTableComponent
|
||||
|
||||
public hotTableSettings: Handsontable.GridSettings = {}
|
||||
|
||||
private updateHotTableSettings(): void {
|
||||
this.hotTableSettings = {
|
||||
multiColumnSorting: true,
|
||||
viewportRowRenderingOffset: 30,
|
||||
colHeaders: this.hotTable.colHeaders,
|
||||
columns: this.hotTable.columns,
|
||||
copyPaste: this.hotTable.copyPaste,
|
||||
contextMenu: this.hotTable.contextMenu,
|
||||
filters: true,
|
||||
dropdownMenu: this.hotTable.dropdownMenu,
|
||||
height: this.hotTable.height,
|
||||
stretchH: 'all',
|
||||
modifyColWidth: this.maxWidthCheker,
|
||||
cells: this.hotTable.cells,
|
||||
maxRows: this.hotTable.maxRows,
|
||||
manualColumnResize: true,
|
||||
afterGetColHeader: this.hotTable.afterGetColHeader,
|
||||
rowHeaders: this.hotTable.rowHeaders,
|
||||
rowHeaderWidth: this.hotTable.rowHeaderWidth,
|
||||
rowHeights: this.hotTable.rowHeights,
|
||||
licenseKey: this.hotTable.licenseKey,
|
||||
className: 'htDark'
|
||||
}
|
||||
}
|
||||
public numberOfRows: number | null = null
|
||||
public headerPks: string[] = []
|
||||
public $dataFormats: $DataFormats | null = null
|
||||
@@ -129,6 +157,13 @@ export class ViewerComponent
|
||||
return ' '
|
||||
},
|
||||
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
||||
const column = this.hotInstance?.colToProp(col) as string
|
||||
|
||||
// header columns styling - primary keys
|
||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
},
|
||||
@@ -203,7 +238,6 @@ export class ViewerComponent
|
||||
private location: Location,
|
||||
private cdf: ChangeDetectorRef
|
||||
) {
|
||||
this.hotTableRegisterer = new HotTableRegisterer()
|
||||
this.sasjsConfig = this.sasService.getSasjsConfig()
|
||||
}
|
||||
|
||||
@@ -223,6 +257,7 @@ export class ViewerComponent
|
||||
this.licenceService.hot_license_key.subscribe(
|
||||
(hot_license_key: string | undefined) => {
|
||||
this.hotTable.licenseKey = hot_license_key
|
||||
this.updateHotTableSettings() // Update settings when license key changes
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -857,6 +892,9 @@ export class ViewerComponent
|
||||
return { readOnly: true }
|
||||
}
|
||||
|
||||
// Update hot table settings after data is loaded
|
||||
this.updateHotTableSettings()
|
||||
|
||||
this.tableFlag = false
|
||||
let ds = []
|
||||
ds = libDataset.split('.')
|
||||
@@ -1081,7 +1119,7 @@ export class ViewerComponent
|
||||
private setupHot() {
|
||||
setTimeout(() => {
|
||||
if (!this.loadingTableView && this.libDataset) {
|
||||
this.hotInstance = this.hotTableRegisterer.getInstance('hotInstance')
|
||||
this.hotInstance = this.hotTableComponent?.hotInstance
|
||||
|
||||
if (this.hotInstance) {
|
||||
this.hotInstance.updateSettings({
|
||||
|
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ViewerComponent } from './viewer.component'
|
||||
import { ViewRouteComponent } from '../routes/view-route/view-route.component'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { ViewerRoutingModule } from './viewer-routing.module'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
@@ -36,7 +36,7 @@ import { MetadataComponent } from '../metadata/metadata.component'
|
||||
ClipboardModule,
|
||||
FormsModule,
|
||||
ClarityModule,
|
||||
HotTableModule.forRoot(),
|
||||
HotTableModule,
|
||||
AppSharedModule,
|
||||
SharedModule,
|
||||
PipesModule,
|
||||
|
@@ -125,30 +125,9 @@
|
||||
|
||||
<div class="clr-flex-1">
|
||||
<hot-table
|
||||
hotId="hotInstance"
|
||||
id="hot-table"
|
||||
className="htDark"
|
||||
[multiColumnSorting]="true"
|
||||
[viewportRowRenderingOffset]="50"
|
||||
[data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData"
|
||||
[colHeaders]="
|
||||
selectedTab === TabsEnum.Rules ? xlmapRulesHeaders : xlUploadHeader
|
||||
"
|
||||
[columns]="
|
||||
selectedTab === TabsEnum.Rules ? xlmapRulesColumns : xlUploadColumns
|
||||
"
|
||||
[filters]="true"
|
||||
[height]="'100%'"
|
||||
stretchH="all"
|
||||
[afterGetColHeader]="afterGetColHeader"
|
||||
[modifyColWidth]="maxWidthChecker"
|
||||
[cells]="getCellConfiguration"
|
||||
[maxRows]="hotTableMaxRows"
|
||||
[manualColumnResize]="true"
|
||||
[rowHeaders]="rowHeaders"
|
||||
[rowHeaderWidth]="15"
|
||||
[rowHeights]="20"
|
||||
[licenseKey]="hotTableLicenseKey"
|
||||
[settings]="hotTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</div>
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
} from '../services'
|
||||
import { getCellAddress, getFinishingCell } from './utils/xl.utils'
|
||||
import { blobToFile, byteArrayToBinaryString } from './utils/file.utils'
|
||||
import Handsontable from 'handsontable'
|
||||
import { UploadFileResponse } from '../models/UploadFile'
|
||||
|
||||
interface XLMapRule {
|
||||
@@ -136,6 +137,34 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
|
||||
public hotTableMaxRows =
|
||||
this.licenceState.value.viewer_rows_allowed || Infinity
|
||||
|
||||
get hotTableSettings(): Handsontable.GridSettings {
|
||||
return {
|
||||
multiColumnSorting: true,
|
||||
viewportRowRenderingOffset: 50,
|
||||
colHeaders:
|
||||
this.selectedTab === this.TabsEnum.Rules
|
||||
? this.xlmapRulesHeaders
|
||||
: this.xlUploadHeader,
|
||||
columns:
|
||||
this.selectedTab === this.TabsEnum.Rules
|
||||
? this.xlmapRulesColumns
|
||||
: this.xlUploadColumns,
|
||||
filters: true,
|
||||
height: '100%',
|
||||
stretchH: 'all',
|
||||
afterGetColHeader: this.afterGetColHeader,
|
||||
modifyColWidth: this.maxWidthChecker,
|
||||
cells: this.getCellConfiguration,
|
||||
maxRows: this.hotTableMaxRows,
|
||||
manualColumnResize: true,
|
||||
rowHeaders: this.rowHeaders,
|
||||
rowHeaderWidth: 15,
|
||||
rowHeights: 20,
|
||||
licenseKey: this.hotTableLicenseKey,
|
||||
className: 'htDark'
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private eventService: EventService,
|
||||
private licenceService: LicenceService,
|
||||
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
import { AppSharedModule } from '../app-shared.module'
|
||||
import { DirectivesModule } from '../directives/directives.module'
|
||||
|
@@ -4781,6 +4781,12 @@ body[cds-theme="dark"] {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.handsontable td.dc-invalid-cell {
|
||||
background: #e62700ad !important;
|
||||
border: 1px solid red !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.handsontable .numericListbox {
|
||||
text-align: right;
|
||||
}
|
||||
|
@@ -13,6 +13,12 @@ if (fs.existsSync(sessionStoragePath)){
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
let controlTableText = ''
|
||||
|
||||
if (_WEBIN_FILENAME1.includes('SASControlTable')) controlTableText = _WEBIN_FILEREF1.toString()
|
||||
|
||||
|
||||
|
||||
let webouts = {
|
||||
MPE_X_TEST: `{"SYSDATE" : "26SEP22"
|
||||
,"SYSTIME" : "08:48"
|
||||
|
Reference in New Issue
Block a user