feat(stage): add Formatted/Unformatted toggle to the staging page
- Mirrors the review page's toggle: getstagetable.sas now also webouts fmt_stagetable (SAS-formatted), selectFormattedRows() picks between it and the raw stagetable, falling back to raw if fmt data is absent - Adds stage.cy.ts e2e coverage and wires it into both build.yaml and release.yaml's cypress --spec lists (previously out of sync with each other on viewbox.cy.ts too)
This commit is contained in:
@@ -146,7 +146,7 @@ jobs:
|
||||
# Start frontend and run cypress
|
||||
# timeout 1800: SIGTERM after 30 min so Cypress can flush video/screenshots
|
||||
# before the outer timeout-minutes hard-kills the step (avoids silent multi-hour hangs)
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 1800 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/viewbox.cy.ts"
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 1800 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/viewbox.cy.ts,cypress/e2e/stage.cy.ts"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts"
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/viewbox.cy.ts,cypress/e2e/stage.cy.ts"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
|
||||
context('stage tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
visitPage('stage/DC20221007T122326121_612316_7259')
|
||||
})
|
||||
|
||||
// getstagetable's mock ignores the table_id param entirely, so any id in
|
||||
// the URL resolves to the same fixture row - no need to submit a real
|
||||
// table first just to reach this page.
|
||||
it('1 | Formatted/Unformatted toggle switches between fmt_stagetable and stagetable, defaulting to formatted', () => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
|
||||
'not.exist'
|
||||
)
|
||||
|
||||
getCellByHeaderAndRow(0, 'SOME_DATE').should('have.text', '12FEB1960')
|
||||
|
||||
cy.get('.formatted-values-toggle').click()
|
||||
|
||||
getCellByHeaderAndRow(0, 'SOME_DATE').should('have.text', '42')
|
||||
|
||||
// Toggling back reverts to the formatted view.
|
||||
cy.get('.formatted-values-toggle').click()
|
||||
|
||||
getCellByHeaderAndRow(0, 'SOME_DATE').should('have.text', '12FEB1960')
|
||||
})
|
||||
})
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
// Locates a body cell by its column's header text rather than a hardcoded
|
||||
// childNodes index - same helper as editor.cy.ts's own.
|
||||
const getCellByHeaderAndRow = (rowIndex: number, headerText: string) => {
|
||||
return cy
|
||||
.get('.ht_clone_top .htCore thead tr th')
|
||||
.should(($ths) => {
|
||||
const texts = [...$ths].map((th) => th.innerText.trim())
|
||||
expect(texts).to.include(headerText)
|
||||
})
|
||||
.then(($ths) => {
|
||||
const index = [...$ths].findIndex(
|
||||
(th) => th.innerText.trim() === headerText
|
||||
)
|
||||
|
||||
return cy
|
||||
.get('.ht_master tbody tr')
|
||||
.then((rows: any) => rows[rowIndex].childNodes[index])
|
||||
.then((cell) => cy.get(cell))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { selectFormattedRows } from './select-formatted-rows'
|
||||
|
||||
describe('selectFormattedRows', () => {
|
||||
const rawRows = [{ SOME_NUM: 42, SOME_DATE: 42 }]
|
||||
const formattedRows = [{ SOME_NUM: '42', SOME_DATE: '11FEB1960' }]
|
||||
|
||||
it('returns the formatted rows when showFormatted is true and formatted rows exist', () => {
|
||||
expect(selectFormattedRows(rawRows, formattedRows, true)).toBe(
|
||||
formattedRows
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the raw rows when showFormatted is false, even if formatted rows exist', () => {
|
||||
expect(selectFormattedRows(rawRows, formattedRows, false)).toBe(rawRows)
|
||||
})
|
||||
|
||||
it('falls back to the raw rows when showFormatted is true but no formatted rows were provided', () => {
|
||||
expect(selectFormattedRows(rawRows, undefined, true)).toBe(rawRows)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Picks which row set to render - raw or SAS-formatted - the same
|
||||
* formatted/unformatted toggle already used on the review page, extracted
|
||||
* so it isn't duplicated inline. Falls back to raw rows if the backend
|
||||
* hasn't sent a formatted variant (e.g. an older service response).
|
||||
*/
|
||||
export function selectFormattedRows(
|
||||
rawRows: any[],
|
||||
formattedRows: any[] | undefined,
|
||||
showFormatted: boolean
|
||||
): any[] {
|
||||
return showFormatted && formattedRows ? formattedRows : rawRows
|
||||
}
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="card-title text-center">Actions</div>
|
||||
</div>
|
||||
<div class="mt-20">
|
||||
<div class="row">
|
||||
<div class="row stage-actions-row">
|
||||
<button
|
||||
class="btn btn-sm btn-outline text-center mr-5i"
|
||||
(click)="viewerTableScreen()"
|
||||
@@ -105,6 +105,20 @@
|
||||
>
|
||||
<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
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ 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',
|
||||
@@ -34,6 +35,9 @@ export class StageComponent implements OnInit, AfterViewInit {
|
||||
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: [],
|
||||
@@ -118,6 +122,14 @@ export class StageComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -193,7 +205,14 @@ export class StageComponent implements OnInit, AfterViewInit {
|
||||
return cellProperties
|
||||
}
|
||||
|
||||
this.hotTable.data = res.stagetable
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { StageComponent } from './stage.component'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
@@ -11,6 +12,7 @@ const routes: Routes = [{ path: ':tableId', component: StageComponent }]
|
||||
declarations: [StageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ClarityModule,
|
||||
RouterModule.forChild(routes),
|
||||
HotTableModule
|
||||
|
||||
@@ -4,6 +4,10 @@ _webout=`{"SYSDATE" : "26SEP22"
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":0 ,"SOME_BESTNUM":44 ,"SOME_CHAR":"this is dummy datass" ,"SOME_DATE":42 ,"SOME_DATETIME":42 ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":42 ,"SOME_SHORTNUM":3 ,"SOME_TIME":42 ,"_____DELETE__THIS__RECORD_____":"No" }
|
||||
]
|
||||
, "fmt_stagetable":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":"0" ,"SOME_BESTNUM":"44" ,"SOME_CHAR":"this is dummy datass" ,"SOME_DATE":"12FEB1960" ,"SOME_DATETIME":"01JAN1960:00:00:42" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"42" ,"SOME_SHORTNUM":"3" ,"SOME_TIME":"0:00:42" ,"_____DELETE__THIS__RECORD_____":"No" }
|
||||
]
|
||||
,"_DEBUG" : ""
|
||||
,"_METAUSER": "sasdemo@SAS"
|
||||
,"_METAPERSON": "sasdemo"
|
||||
|
||||
@@ -4,6 +4,10 @@ _webout = `{"SYSDATE" : "26SEP22"
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":0 ,"SOME_BESTNUM":44 ,"SOME_CHAR":"this is changed data" ,"SOME_DATE":42 ,"SOME_DATETIME":42 ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":42 ,"SOME_SHORTNUM":3 ,"SOME_TIME":42 ,"_____DELETE__THIS__RECORD_____":"No" }
|
||||
]
|
||||
, "fmt_stagetable":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":"0" ,"SOME_BESTNUM":"44" ,"SOME_CHAR":"this is changed data" ,"SOME_DATE":"12FEB1960" ,"SOME_DATETIME":"01JAN1960:00:00:42" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"42" ,"SOME_SHORTNUM":"3" ,"SOME_TIME":"0:00:42" ,"_____DELETE__THIS__RECORD_____":"No" }
|
||||
]
|
||||
,"_DEBUG" : ""
|
||||
,"_METAUSER": "sasdemo@SAS"
|
||||
,"_METAPERSON": "sasdemo"
|
||||
|
||||
@@ -29,6 +29,9 @@ run;
|
||||
|
||||
%webout(OPEN)
|
||||
%webout(OBJ,stagetable,missing=STRING)
|
||||
/* same table again with SAS formats applied, for the frontend's
|
||||
Formatted/Unformatted toggle - same pattern as postdata.sas */
|
||||
%webout(OBJ,stagetable,dslabel=fmt_stagetable,fmt=Y,missing=STRING)
|
||||
%webout(CLOSE)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user