src/app/stage/stage.component.ts

Implements

OnInit AfterViewInit

Metadata

Relationships

Used by

Depends on

Index

Properties
Methods

Constructor

constructor(licenceService: LicenceService, sasStoreService: SasStoreService, eventService: EventService, route: Router, router: ActivatedRoute, sasService: SasService)
Parameters :
Name Type Optional
licenceService LicenceService No
sasStoreService SasStoreService No
eventService EventService No
route Router No
router ActivatedRoute No
sasService SasService No

Methods

Public approveTableScreen
approveTableScreen()
Returns : void
Private buildHotSettings
buildHotSettings()
Returns : Handsontable.GridSettings
Public download
download(id: any)
Parameters :
Name Type Optional
id any No
Returns : void
Private fixFocusCatcherAccessibility
fixFocusCatcherAccessibility()
Returns : void
Public formattingChanged
formattingChanged()
Returns : void
Public goBack
goBack()
Returns : void
ngAfterViewInit
ngAfterViewInit()
Returns : void
Async ngOnInit
ngOnInit()
Returns : any
revertChanges
revertChanges()
Returns : void
Private setFocus
setFocus()
Returns : void
Public submittedTableScreen
submittedTableScreen()
Returns : void
Public viewerTableScreen
viewerTableScreen()
Returns : void

Properties

Private fmtStageTable
Type : any[] | undefined
Public formattedValues
Type : boolean
Default value : true
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() } }
hotTableSettings
Type : unknown
Default value : signal<Handsontable.GridSettings>(this.buildHotSettings())
Public jsParams
Type : any
Public keysArray
Type : any
Public licenceState
Type : unknown
Default value : this.licenceService.licenceState
Public loaded
Type : boolean
Default value : false
Private rawStageTable
Type : any[]
Default value : []
Public revertingChanges
Type : boolean
Default value : false
Public table_id
Type : any
Public tableDetails
Type : any
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;
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""