src/app/stage/stage.component.ts
| encapsulation | ViewEncapsulation.None |
| host | { |
| selector | app-stage |
| styleUrls | ./stage.component.scss |
| templateUrl | ./stage.component.html |
Properties |
|
Methods |
|
constructor(licenceService: LicenceService, sasStoreService: SasStoreService, eventService: EventService, route: Router, router: ActivatedRoute, sasService: SasService)
|
|||||||||||||||||||||
|
Defined in src/app/stage/stage.component.ts:90
|
|||||||||||||||||||||
|
Parameters :
|
| Public approveTableScreen |
approveTableScreen()
|
|
Defined in src/app/stage/stage.component.ts:105
|
|
Returns :
void
|
| Private buildHotSettings |
buildHotSettings()
|
|
Defined in src/app/stage/stage.component.ts:75
|
|
Returns :
Handsontable.GridSettings
|
| Public download | ||||||
download(id: any)
|
||||||
|
Defined in src/app/stage/stage.component.ts:133
|
||||||
|
Parameters :
Returns :
void
|
| Private fixFocusCatcherAccessibility |
fixFocusCatcherAccessibility()
|
|
Defined in src/app/stage/stage.component.ts:280
|
|
Returns :
void
|
| Public formattingChanged |
formattingChanged()
|
|
Defined in src/app/stage/stage.component.ts:125
|
|
Returns :
void
|
| Public goBack |
goBack()
|
|
Defined in src/app/stage/stage.component.ts:113
|
|
Returns :
void
|
| ngAfterViewInit |
ngAfterViewInit()
|
|
Defined in src/app/stage/stage.component.ts:237
|
|
Returns :
void
|
| Async ngOnInit |
ngOnInit()
|
|
Defined in src/app/stage/stage.component.ts:148
|
|
Returns :
any
|
| revertChanges |
revertChanges()
|
|
Defined in src/app/stage/stage.component.ts:244
|
|
Returns :
void
|
| Private setFocus |
setFocus()
|
|
Defined in src/app/stage/stage.component.ts:271
|
|
Returns :
void
|
| Public submittedTableScreen |
submittedTableScreen()
|
|
Defined in src/app/stage/stage.component.ts:101
|
|
Returns :
void
|
| Public viewerTableScreen |
viewerTableScreen()
|
|
Defined in src/app/stage/stage.component.ts:109
|
|
Returns :
void
|
| Private fmtStageTable |
Type : any[] | undefined
|
|
Defined in src/app/stage/stage.component.ts:40
|
| Public formattedValues |
Type : boolean
|
Default value : true
|
|
Defined in src/app/stage/stage.component.ts:38
|
| Public hotTable |
Type : HotTableInterface
|
Default value : {
data: [],
colHeaders: [],
columns: [],
height: 500,
settings: {
// Disable problematic ARIA attributes that cause accessibility issues
ariaTags: false,
// Use grid role instead of treegrid for better accessibility
tableClassName: 'htCenter',
// Disable focus management to avoid focus catcher issues
outsideClickDeselects: false,
// Use simpler accessibility mode
autoWrapRow: false,
autoWrapCol: false
},
licenseKey: undefined,
maxRows: this.licenceState.value.stage_rows_allowed || Infinity,
afterGetColHeader: (column, th, headerLevel) => {
// Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass)
},
afterInit: () => {
// Fix accessibility issues with focus catcher inputs
this.fixFocusCatcherAccessibility()
}
}
|
|
Defined in src/app/stage/stage.component.ts:42
|
| hotTableSettings |
Type : unknown
|
Default value : signal<Handsontable.GridSettings>(this.buildHotSettings())
|
|
Defined in src/app/stage/stage.component.ts:73
|
| Public jsParams |
Type : any
|
|
Defined in src/app/stage/stage.component.ts:33
|
| Public keysArray |
Type : any
|
|
Defined in src/app/stage/stage.component.ts:34
|
| Public licenceState |
Type : unknown
|
Default value : this.licenceService.licenceState
|
|
Defined in src/app/stage/stage.component.ts:41
|
| Public loaded |
Type : boolean
|
Default value : false
|
|
Defined in src/app/stage/stage.component.ts:36
|
| Private rawStageTable |
Type : any[]
|
Default value : []
|
|
Defined in src/app/stage/stage.component.ts:39
|
| Public revertingChanges |
Type : boolean
|
Default value : false
|
|
Defined in src/app/stage/stage.component.ts:37
|
| Public table_id |
Type : any
|
|
Defined in src/app/stage/stage.component.ts:32
|
| Public tableDetails |
Type : any
|
|
Defined in src/app/stage/stage.component.ts:35
|
import {
Component,
OnInit,
ViewEncapsulation,
AfterViewInit,
signal
} from '@angular/core'
import { SasStoreService } from '../services/sas-store.service'
import { Router } from '@angular/router'
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'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
import { selectFormattedRows } from '../shared/utils/select-formatted-rows'
@Component({
selector: 'app-stage',
templateUrl: './stage.component.html',
styleUrls: ['./stage.component.scss'],
host: {
class: 'content-container'
},
encapsulation: ViewEncapsulation.None,
standalone: false
})
export class StageComponent implements OnInit, AfterViewInit {
public table_id: any
public jsParams: any
public keysArray: any
public tableDetails: any
public loaded: boolean = false
public revertingChanges: boolean = false
public formattedValues: boolean = true
private rawStageTable: any[] = []
private fmtStageTable: any[] | undefined
public licenceState = this.licenceService.licenceState
public hotTable: HotTableInterface = {
data: [],
colHeaders: [],
columns: [],
height: 500,
settings: {
// Disable problematic ARIA attributes that cause accessibility issues
ariaTags: false,
// Use grid role instead of treegrid for better accessibility
tableClassName: 'htCenter',
// Disable focus management to avoid focus catcher issues
outsideClickDeselects: false,
// Use simpler accessibility mode
autoWrapRow: false,
autoWrapCol: false
},
licenseKey: undefined,
maxRows: this.licenceState.value.stage_rows_allowed || Infinity,
afterGetColHeader: (column, th, headerLevel) => {
// Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass)
},
afterInit: () => {
// Fix accessibility issues with focus catcher inputs
this.fixFocusCatcherAccessibility()
}
}
// Stable settings reference so the HOT v17 wrapper doesn't re-run
// updateSettings on every change-detection tick. Updated only when the data/
// columns or license change.
hotTableSettings = signal<Handsontable.GridSettings>(this.buildHotSettings())
private buildHotSettings(): 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',
theme: 'ht-theme-classic'
}
}
constructor(
private licenceService: LicenceService,
private sasStoreService: SasStoreService,
private eventService: EventService,
private route: Router,
private router: ActivatedRoute,
private sasService: SasService
) {}
public submittedTableScreen() {
this.route.navigateByUrl('/stage/' + this.table_id)
}
public approveTableScreen() {
this.route.navigateByUrl('/review/approveDet/' + this.table_id)
}
public viewerTableScreen() {
this.route.navigateByUrl('/view/data/' + this.tableDetails.BASE_TABLE)
}
public goBack() {
const xlmap = globals.xlmaps.find(
(xlmap) => xlmap.targetDS === this.tableDetails.BASE_TABLE
)
if (xlmap) {
const id = this.hotTable.data[0].XLMAP_ID
this.route.navigateByUrl('/home/excel-maps/' + id)
} else {
this.route.navigateByUrl('/editor/' + this.tableDetails.BASE_TABLE)
}
}
public formattingChanged() {
this.hotTable.data = selectFormattedRows(
this.rawStageTable,
this.fmtStageTable,
this.formattedValues
)
}
public download(id: any) {
let sasjsConfig = this.sasService.getSasjsConfig()
let storage = sasjsConfig.serverUrl
let metaData = sasjsConfig.appLoc
let path = this.sasService.getExecutionPath()
let downUrl =
storage +
path +
'/?_program=' +
metaData +
'/services/auditors/getauditfile&table=' +
id
window.open(downUrl)
}
async ngOnInit() {
this.licenceService.hot_license_key.subscribe(
(hot_license_key: string | undefined) => {
this.hotTable.licenseKey = hot_license_key
this.hotTableSettings.set(this.buildHotSettings())
}
)
if (typeof this.router.snapshot.params['tableId'] !== 'undefined') {
this.table_id = this.router.snapshot.params['tableId']
try {
let res = await this.sasStoreService.getChangeInfo(this.table_id)
if (!(res && res.jsparams))
throw new Error('jsparams property is missing from response.')
this.tableDetails = res.jsparams[0]
} catch (error: any) {
let errorObject: any = {}
if (!error.MESSAGE) {
errorObject.MESSAGE = error
} else {
errorObject = error
}
this.eventService.catchResponseError(
'public/getchangeinfo',
errorObject
)
}
try {
let res = await this.sasStoreService.openTable(this.table_id)
if (!(res && res.stagetable))
throw new Error('Stagetable property is missing from response.')
let cols = res.stagetable[0]
let colHeaders = []
let columns: any[] = []
for (let key in cols) {
if (cols) {
colHeaders.push(key)
}
}
for (let index = 0; index < colHeaders.length; index++) {
columns.push({
data: colHeaders[index]
})
}
let cells = function () {
let cellProperties = { readOnly: true }
return cellProperties
}
this.rawStageTable = res.stagetable
this.fmtStageTable = res.fmt_stagetable
this.hotTable.data = selectFormattedRows(
this.rawStageTable,
this.fmtStageTable,
this.formattedValues
)
this.hotTable.colHeaders = colHeaders
this.hotTable.columns = columns
this.hotTable.cells = cells
this.hotTableSettings.set(this.buildHotSettings())
this.loaded = true
this.setFocus()
} catch (error: any) {
let errorObject: any = {}
if (!error.MESSAGE) errorObject.MESSAGE = error
this.eventService.catchResponseError(
'auditors/getstagetable',
errorObject
)
this.loaded = false
}
}
}
ngAfterViewInit() {
// Additional accessibility fixes after view is initialized
setTimeout(() => {
this.fixFocusCatcherAccessibility()
}, 500)
}
revertChanges() {
this.revertingChanges = true
const data = {
restore_in: [
{
load_ref: this.table_id
}
]
}
this.sasService
.request('editors/restore', data)
.then((res: RequestWrapperResponse<EditorsRestoreServiceResponse>) => {
if (res.adapterResponse.restore_out) {
this.route.navigate([`/stage`]).then(() => {
this.route.navigate([
`/stage/${res.adapterResponse.restore_out[0].LOADREF}`
])
})
}
})
.finally(() => {
this.revertingChanges = false
})
}
private setFocus() {
setTimeout(() => {
let approvalBtn: any = window.document.getElementById('approval-btn')
if (!!approvalBtn) {
approvalBtn.focus()
}
}, 200)
}
private fixFocusCatcherAccessibility() {
// Add labels to focus catcher inputs to fix accessibility issues
setTimeout(() => {
const focusCatchers = document.querySelectorAll('.htFocusCatcher')
focusCatchers.forEach((input: any, index: number) => {
if (input) {
// Add proper accessibility attributes
input.setAttribute('aria-label', `Table focus catcher ${index + 1}`)
input.setAttribute('aria-hidden', 'true')
input.setAttribute('tabindex', '-1')
input.setAttribute('role', 'presentation')
// Add a hidden label element
const label = document.createElement('label')
label.setAttribute('for', input.id || `htFocusCatcher${index}`)
label.setAttribute('aria-hidden', 'true')
label.style.display = 'none'
label.textContent = `Table focus catcher ${index + 1}`
input.parentNode?.insertBefore(label, input)
}
})
}, 100)
}
}
<main class="content-area">
<div class="clr-col-md-12 card">
@if (!loaded) {
<div
class="h-80vh d-flex justify-content-center flex-column align-items-center"
>
@if (!loaded) {
<span class="spinner"> Loading... </span>
}
@if (!loaded) {
<div>
<h3>Loading submitted table</h3>
</div>
}
</div>
}
@if (loaded) {
<div>
<div class="card-header" ng-if="loaded">
<div class="card-title text-center">
<h4 class="mt-0">Staged Data</h4>
</div>
</div>
<div class="clr-row" ng-if="loaded">
<div class="card-block clr-col-md-6 no-borders">
<div class="card-header py-5">
<div class="card-title text-center">Basic Submitted Details</div>
</div>
<div>
<span>Table Id:</span>
<strong>
<span> {{ tableDetails?.TABLE_ID }}</span>
</strong>
</div>
<div>
<span>Base Table:</span>
<a (click)="viewerTableScreen()" class="baseTableLink">
{{ tableDetails?.BASE_TABLE }}
</a>
</div>
<div>
<span>Submit Date:</span>
<span class="mt-10">
{{ tableDetails?.SUBMITTED_ON_DTTM }}
</span>
</div>
<div>
<span>Review Status:</span>
<span
class="mt-10"
[ngClass]="{
rejected: tableDetails?.REVIEW_STATUS_ID === 'REJECTED',
accepted: tableDetails?.REVIEW_STATUS_ID === 'APPROVED'
}"
[attr.aria-label]="
'Review status: ' + tableDetails?.REVIEW_STATUS_ID
"
role="status"
>
{{ tableDetails?.REVIEW_STATUS_ID }}
</span>
</div>
</div>
<div class="clr-col-md-6 card-block no-borders">
<div class="card-header py-5">
<div class="card-title text-center">Actions</div>
</div>
<div class="mt-20">
<div class="row stage-actions-row">
<button
class="btn btn-sm btn-outline text-center mr-5i"
(click)="viewerTableScreen()"
[disabled]="revertingChanges"
aria-label="View base table"
>
View base table
</button>
@if (!(tableDetails?.['ALLOW_RESTORE'] === 'YES')) {
<button
id="approval-btn"
class="btn btn-sm btn-success-outline text-center mr-5i"
[disabled]="
tableDetails?.REVIEW_STATUS_ID === 'APPROVED' ||
tableDetails?.REVIEW_STATUS_ID === 'REJECTED'
"
(click)="approveTableScreen()"
[disabled]="revertingChanges"
aria-label="Approve table"
>
Approve
</button>
}
<button
class="btn btn-sm btn-info-outline text-center mr-5i"
(click)="goBack()"
[disabled]="revertingChanges"
aria-label="Edit base table"
>
Edit base table
</button>
<button
class="btn btn-sm btn-success text-center mr-5i min-w-0"
(click)="download(tableDetails?.TABLE_ID)"
aria-label="Download audit file"
>
<clr-icon shape="download" aria-hidden="true"></clr-icon>
</button>
<clr-toggle-container class="m-0 flex-shrink-0">
<clr-toggle-wrapper>
<input
type="checkbox"
clrToggle
checked
[(ngModel)]="formattedValues"
(change)="formattingChanged()"
/>
<label class="formatted-values-toggle">{{
formattedValues ? 'Formatted' : 'Unformatted'
}}</label>
</clr-toggle-wrapper>
</clr-toggle-container>
<clr-tooltip>
@if (tableDetails?.['ALLOW_RESTORE'] === 'YES') {
<button
(click)="revertChanges()"
clrTooltipTrigger
[clrLoading]="revertingChanges"
class="btn btn-sm btn-danger text-center mt-20"
aria-label="Revert this and all subsequent changes"
>
REVERT
<clr-tooltip-content
clrPosition="bottom-left"
clrSize="lg"
*clrIfOpen
>
<span> Revert this and all subsequent changes </span>
</clr-tooltip-content>
</button>
}
</clr-tooltip>
</div>
</div>
</div>
</div>
<div class="card-block">
<hot-table
id="hotTable"
[data]="hotTable.data"
[settings]="hotTableSettings()"
aria-label="Staged data table"
>
<!--[licenseKey]=null-->
</hot-table>
</div>
<div class="card-footer d-flex justify-content-center"></div>
</div>
}
</div>
</main>
./stage.component.scss
// Named specifically (not a generic utility) since this component uses
// ViewEncapsulation.None - a generic name would leak globally.
//
// .row here has no CSS establishing it as a flex/grid container (Clarity
// doesn't define one) - the buttons lay out via normal inline flow, since
// .btn is display: inline-flex (an INLINE outer box). flex-wrap/align-items
// would be no-ops on it; vertical-align is what actually governs alignment
// between inline-level boxes of different heights.
.stage-actions-row {
clr-toggle-container {
// clr-form-control (Clarity's own class here) sets display: flex, not
// inline-flex - a block-level box takes the full width of its
// containing block regardless of its own width: auto, which is why it
// wrapped onto its own line. inline-flex gives it an inline outer box
// (sized to content) so it flows with the buttons instead.
display: inline-flex;
width: auto;
margin-top: 0;
// .btn already sets vertical-align: middle - matching it here is what
// actually centers this against the buttons, since neither is a flex
// item of a shared flex container.
vertical-align: middle;
}
}