feat(mocks): run pre/post edit hook programs in the JS mock services
The JS mock services carried MPE_TABLES.pre_edit_hook / post_edit_hook values but never acted on them, so a table configured with hooks behaved differently in the mock than in production. Add mockHookSource() to dcMockUtils (resolving a hook program name to the source of its .js counterpart on the Drive) and run the hook where the SAS backend does: - editors/getdata runs the pre-edit hook after filtering, letting it reassign visibleRows / visibleColumns (the work.OUT contract) - so an empty mirror can display the live rows of the table it points at - editors/stagedata and editors/loadfile run the post-edit hook before the MPE_SUBMIT row is written, letting it reassign libref / dsn (the call symputx contract) - so a changeset submitted against a mirror is raised against the real table Seed a demo pair in makedata (TESTDATA.DEMO_ORDERS plus an empty TESTDATA.DEMO_MIRROR with both hooks) and cover it with a Cypress spec.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
// Marks this file as an ES module (rather than a global script) so its
|
||||
// top-level consts don't collide, under the TS type-checker, with the same
|
||||
// names declared in other spec files.
|
||||
export {}
|
||||
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
|
||||
context('hook program tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
// TESTDATA.DEMO_MIRROR is an empty mirror of TESTDATA.DEMO_ORDERS, registered
|
||||
// with a PRE_EDIT_HOOK and a POST_EDIT_HOOK (services/hooks/demo_mirror_*.js).
|
||||
// The mock services run those hooks the way the SAS backend %includes the
|
||||
// real hook programs - see dcMockUtils.mockHookSource.
|
||||
|
||||
it('1 | PRE_EDIT_HOOK: an empty mirror displays the live rows of its target table', () => {
|
||||
openTableFromTree('testdata', 'demo_mirror')
|
||||
|
||||
// The mirror holds no rows of its own: everything on screen comes from the
|
||||
// pre-edit hook, which loads TESTDATA.DEMO_ORDERS.
|
||||
cy.get('#hotTable .ht_master tbody tr', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should('have.length', 4)
|
||||
|
||||
cy.get('#hotTable .ht_master tbody tr')
|
||||
.first()
|
||||
.should('contain.text', 'Acme Corp')
|
||||
|
||||
cy.get('#hotTable .ht_master tbody tr')
|
||||
.last()
|
||||
.should('contain.text', 'Delta Systems')
|
||||
|
||||
// The grid is validated against the MIRROR's own rule set (AMOUNT <= 2000),
|
||||
// not the real table's (AMOUNT <= 100000) - that is the point of routing
|
||||
// edits through a mirror.
|
||||
clickOnEdit(() => {
|
||||
editCell(0, 'AMOUNT', '5000')
|
||||
})
|
||||
|
||||
cy.get('#hotTable .ht_master tbody tr')
|
||||
.first()
|
||||
.find('td.htInvalid')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('2 | POST_EDIT_HOOK: a change submitted against the mirror is raised against the target', () => {
|
||||
openTableFromTree('testdata', 'demo_mirror')
|
||||
|
||||
clickOnEdit(() => {
|
||||
editCell(0, 'AMOUNT', '750')
|
||||
})
|
||||
|
||||
submitTable(() => {
|
||||
cy.get('textarea.submit-reason', { timeout: longerCommandTimeout }).type(
|
||||
'hook routing test'
|
||||
)
|
||||
submitTableMessage()
|
||||
})
|
||||
|
||||
// A successful submit sends the app to the staged-data page; wait for that
|
||||
// before navigating away, or the app's own redirect wins the race.
|
||||
cy.url({ timeout: longerCommandTimeout }).should('include', '/stage/')
|
||||
|
||||
// The post-edit hook re-points the changeset at TESTDATA.DEMO_ORDERS, so the
|
||||
// approval queue names the real table rather than the mirror.
|
||||
visitPage('review/approve')
|
||||
|
||||
cy.get('clr-datagrid clr-dg-row', { timeout: longerCommandTimeout })
|
||||
.last()
|
||||
.should('contain.text', 'TESTDATA.DEMO_ORDERS')
|
||||
})
|
||||
})
|
||||
|
||||
// ─── helpers (declared at the bottom, per the other specs) ───────────────────
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.wait(300)
|
||||
|
||||
cy.get(
|
||||
'.clr-tree-node-content-container .clr-treenode-content p'
|
||||
).click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should('exist')
|
||||
}
|
||||
|
||||
const clickOnEdit = (callback?: any) => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
// Edits a single cell, locating the column by its header label. Handsontable
|
||||
// renders the frozen header in a separate clone pane (.ht_clone_top) - the
|
||||
// header row inside .ht_master is kept visibility:hidden - so the labels are
|
||||
// read from the clone.
|
||||
const editCell = (rowIndex: number, colName: string, value: string) => {
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead th', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((headers: any) => {
|
||||
let colIndex = -1
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
if (
|
||||
String(headers[i].innerText || '')
|
||||
.trim()
|
||||
.toUpperCase() === colName.toUpperCase()
|
||||
) {
|
||||
colIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
expect(colIndex, `column ${colName} present`).to.be.greaterThan(-1)
|
||||
|
||||
cy.get('#hotTable .ht_master tbody tr')
|
||||
.eq(rowIndex)
|
||||
.then((row: any) => {
|
||||
cy.get(row[0].childNodes[colIndex])
|
||||
.dblclick({ force: true })
|
||||
.then(() => {
|
||||
cy.focused()
|
||||
.clear()
|
||||
.type(value + '{enter}')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const submitTable = (callback?: any) => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const submitTableMessage = (callback?: any) => {
|
||||
cy.get('.modal-footer .btn.btn-sm.btn-success-outline', {
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
@@ -5259,12 +5259,40 @@ const testTables = {
|
||||
STATUS: 1,
|
||||
LAST_REVIEWED: 23621
|
||||
}
|
||||
]
|
||||
],
|
||||
// Hook demo pair: DEMO_MIRROR is an EMPTY mirror of DEMO_ORDERS, registered
|
||||
// with pre/post edit hooks (services/hooks/demo_mirror_*.js). The pre-edit
|
||||
// hook shows the live rows of DEMO_ORDERS; the post-edit hook routes a
|
||||
// changeset submitted against the mirror to DEMO_ORDERS.
|
||||
DEMO_ORDERS: [
|
||||
{ ORDER_ID: 1001, CUSTOMER: 'Acme Corp', REGION: 'EU', AMOUNT: 450, STATUS: 'NEW' },
|
||||
{ ORDER_ID: 1002, CUSTOMER: 'Borealis Ltd', REGION: 'EU', AMOUNT: 980.5, STATUS: 'OPEN' },
|
||||
{ ORDER_ID: 1003, CUSTOMER: 'Cobalt GmbH', REGION: 'EU', AMOUNT: 620, STATUS: 'CLOSED' },
|
||||
{ ORDER_ID: 1004, CUSTOMER: 'Delta Systems', REGION: 'US', AMOUNT: 4500, STATUS: 'NEW' }
|
||||
],
|
||||
DEMO_MIRROR: []
|
||||
}
|
||||
|
||||
// Schemas for the TESTDATA tables, matching the old inline getdata.js payload
|
||||
// that the Cypress editor specs were calibrated against.
|
||||
const testSchema = {
|
||||
// Hook demo pair (see DEMO_ORDERS / DEMO_MIRROR in testTables). The mirror
|
||||
// has the same shape as the real table, so the pre-edit hook can hand its rows
|
||||
// straight to the grid.
|
||||
DEMO_ORDERS: [
|
||||
{ name: 'ORDER_ID', type: 'N', length: 8, format: '', label: 'Order Id', notnull: true },
|
||||
{ name: 'CUSTOMER', type: 'C', length: 20, format: '', label: 'Customer', notnull: false },
|
||||
{ name: 'REGION', type: 'C', length: 8, format: '', label: 'Region', notnull: false },
|
||||
{ name: 'AMOUNT', type: 'N', length: 8, format: '', label: 'Amount', notnull: false },
|
||||
{ name: 'STATUS', type: 'C', length: 12, format: '', label: 'Status', notnull: false }
|
||||
],
|
||||
DEMO_MIRROR: [
|
||||
{ name: 'ORDER_ID', type: 'N', length: 8, format: '', label: 'Order Id', notnull: true },
|
||||
{ name: 'CUSTOMER', type: 'C', length: 20, format: '', label: 'Customer', notnull: false },
|
||||
{ name: 'REGION', type: 'C', length: 8, format: '', label: 'Region', notnull: false },
|
||||
{ name: 'AMOUNT', type: 'N', length: 8, format: '', label: 'Amount', notnull: false },
|
||||
{ name: 'STATUS', type: 'C', length: 12, format: '', label: 'Status', notnull: false }
|
||||
],
|
||||
MPE_X_NEW: [
|
||||
{
|
||||
name: 'PRIMARY_KEY_FIELD',
|
||||
@@ -5631,6 +5659,28 @@ tables.MPE_TABLES.push(
|
||||
loadtype: 'UPDATE',
|
||||
buskey: 'ID',
|
||||
notes: 'Compact rule demo table (eight columns)'
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
tx_to: 127490111999,
|
||||
libref: testDataLibref,
|
||||
dsn: 'DEMO_ORDERS',
|
||||
num_of_approvals_required: 1,
|
||||
loadtype: 'UPDATE',
|
||||
buskey: 'ORDER_ID',
|
||||
notes: 'Hook demo - the real table the mirror points at'
|
||||
},
|
||||
{
|
||||
tx_from: 0,
|
||||
tx_to: 127490111999,
|
||||
libref: testDataLibref,
|
||||
dsn: 'DEMO_MIRROR',
|
||||
num_of_approvals_required: 1,
|
||||
loadtype: 'UPDATE',
|
||||
buskey: 'ORDER_ID',
|
||||
notes: 'Hook demo - an empty mirror whose hooks show and route DEMO_ORDERS',
|
||||
pre_edit_hook: 'services/hooks/demo_mirror_preedit',
|
||||
post_edit_hook: 'services/hooks/demo_mirror_postedit'
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5854,6 +5904,22 @@ const testValidations = [
|
||||
base_col: 'STATUS',
|
||||
rule_type: 'SOFTSELECT',
|
||||
rule_value: testDataLibref + '.DEMO_01.STATUS'
|
||||
},
|
||||
// Hook demo: the mirror carries its OWN rule set, separate from the real
|
||||
// table's - which is the whole point of routing edits through a mirror.
|
||||
{
|
||||
base_lib: testDataLibref,
|
||||
base_ds: 'DEMO_MIRROR',
|
||||
base_col: 'AMOUNT',
|
||||
rule_type: 'MAXVAL',
|
||||
rule_value: '2000'
|
||||
},
|
||||
{
|
||||
base_lib: testDataLibref,
|
||||
base_ds: 'DEMO_ORDERS',
|
||||
base_col: 'AMOUNT',
|
||||
rule_type: 'MAXVAL',
|
||||
rule_value: '100000'
|
||||
}
|
||||
]
|
||||
for (const v of testValidations) {
|
||||
|
||||
@@ -336,6 +336,39 @@ function loadTableSchema(dataDir, tableName) {
|
||||
return (data && data.columns) ? data.columns : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a hook program name to the source of its mock implementation.
|
||||
*
|
||||
* MPE_TABLES.pre_edit_hook / post_edit_hook (and the SOFTSELECT_HOOK /
|
||||
* HARDSELECT_HOOK rule_value in MPE_VALIDATIONS) hold a hook program: either a
|
||||
* physical .sas path, or a path relative to the appLoc (e.g.
|
||||
* 'services/hooks/mytable_postedit'). The real backend %includes the program
|
||||
* into the running service, so the hook shares the service's macro variables
|
||||
* and work datasets and can modify them.
|
||||
*
|
||||
* The mock mirrors that: this returns the source of the hook's .js counterpart,
|
||||
* which the calling service eval()s in its OWN scope so the hook can read and
|
||||
* modify the service's variables. Returns null when the hook has no mock
|
||||
* implementation, so callers can fall back to an inline emulation or ignore it.
|
||||
*
|
||||
* Requires nodePath, driveRoot and appLoc in the caller's scope.
|
||||
*/
|
||||
function mockHookSource(hookValue) {
|
||||
if (!hookValue) return null
|
||||
let hook = String(hookValue).trim()
|
||||
if (!hook) return null
|
||||
// Reduce to the services-relative form, dropping any drive path or appLoc
|
||||
// prefix the config may carry.
|
||||
const idx = hook.indexOf('services/')
|
||||
if (idx > -1) hook = hook.slice(idx)
|
||||
// A physical hook is a .sas program on the server; its mock is a .js file
|
||||
// sitting in the same place on the Drive.
|
||||
hook = hook.replace(/\.sas$/i, '')
|
||||
const hookFile = nodePath.resolve(driveRoot, 'files', appLoc, hook + '.js')
|
||||
if (!fs.existsSync(hookFile)) return null
|
||||
return fs.readFileSync(hookFile, 'utf8')
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesises NOTNULL dqrules from the table schema, mirroring the
|
||||
* dictionary.columns union in getdata.sas:
|
||||
|
||||
@@ -204,8 +204,8 @@ if (tableData && tableData.columns && tableData.rows) {
|
||||
if (tableReg.var_busto) excludeCols.add(tableReg.var_busto.toUpperCase())
|
||||
}
|
||||
|
||||
const visibleColumns = tableData.columns.filter(c => !excludeCols.has(c.name.toUpperCase()))
|
||||
const visibleRows = tableData.rows
|
||||
let visibleColumns = tableData.columns.filter(c => !excludeCols.has(c.name.toUpperCase()))
|
||||
let visibleRows = tableData.rows
|
||||
.filter(r => filterPredicate(r))
|
||||
.map(r => {
|
||||
const copy = {}
|
||||
@@ -215,6 +215,23 @@ if (tableData && tableData.columns && tableData.rows) {
|
||||
return copy
|
||||
})
|
||||
|
||||
// ─── PRE_EDIT_HOOK ───────────────────────────────────────────────────────
|
||||
// Mirrors getdata.sas: %mpe_runhook(PRE_EDIT_HOOK) runs after the filter has
|
||||
// been applied and the rows sorted, with the data in work.OUT, and may
|
||||
// replace it. The mock eval()s the hook's .js counterpart in this scope, so
|
||||
// the hook may reassign visibleRows / visibleColumns - e.g. to show the live
|
||||
// rows of another table behind an empty mirror.
|
||||
if (tableReg && tableReg.pre_edit_hook) {
|
||||
const hookSrc = mockHookSource(tableReg.pre_edit_hook)
|
||||
if (hookSrc) {
|
||||
try {
|
||||
eval(hookSrc)
|
||||
} catch (err) {
|
||||
console.log('Error running pre_edit_hook ' + tableReg.pre_edit_hook + ': ' + err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Augment rows: add delete flag, normalise keys, convert temporal values to ISO
|
||||
const colLookup = {}
|
||||
for (const col of visibleColumns) {
|
||||
|
||||
@@ -59,8 +59,8 @@ for (let i = 1; i <= 10; i++) {
|
||||
|
||||
let libds = ''
|
||||
if (typeof table !== 'undefined' && table) libds = String(table).toUpperCase()
|
||||
const libref = libds.split('.')[0] || dcLibref
|
||||
const dsn = libds.split('.')[1] || ''
|
||||
let libref = libds.split('.')[0] || dcLibref
|
||||
let dsn = libds.split('.')[1] || ''
|
||||
|
||||
// ─── Validate (mirrors the mp_abort checks in loadfile.sas) ─────────────────
|
||||
|
||||
@@ -167,6 +167,29 @@ if (!msg) {
|
||||
}
|
||||
|
||||
if (!msg) {
|
||||
// ─── POST_EDIT_HOOK ──────────────────────────────────────────────────
|
||||
// Mirrors mpe_loader.sas: the hook runs before the MPE_SUBMIT record is
|
||||
// written, while LIBREF / DS are still ordinary macro variables, so it
|
||||
// can re-point the changeset at a different table. The mock eval()s the
|
||||
// hook's .js counterpart in this scope so it can reassign libref / dsn.
|
||||
const hookTables = mpeLoadTableData('MPE_TABLES')
|
||||
const hookReg = (hookTables && hookTables.rows)
|
||||
? hookTables.rows.find(r => r.libref === libref && r.dsn === dsn)
|
||||
: null
|
||||
if (hookReg && hookReg.post_edit_hook) {
|
||||
const hookSrc = mockHookSource(hookReg.post_edit_hook)
|
||||
if (hookSrc) {
|
||||
try {
|
||||
eval(hookSrc)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'post_edit_hook ' + hookReg.post_edit_hook + ' failed: ' + err.message
|
||||
)
|
||||
}
|
||||
libds = libref + '.' + dsn
|
||||
}
|
||||
}
|
||||
|
||||
tableId = makeTableId()
|
||||
stageSubmission({
|
||||
dataDir: libDataDir(libref),
|
||||
|
||||
@@ -20,8 +20,8 @@ const _sctRow = fetchTable('SASControlTable')[0] || {}
|
||||
let action = _sctRow.ACTION || 'LOAD'
|
||||
let message = _sctRow.MESSAGE || ''
|
||||
let libds = _sctRow.LIBDS || ''
|
||||
const libref = libds.split('.')[0] || dcLibref
|
||||
const dsn = libds.split('.')[1] || ''
|
||||
let libref = libds.split('.')[0] || dcLibref
|
||||
let dsn = libds.split('.')[1] || ''
|
||||
|
||||
// The base table lives in its own library (DC_JSLIB for control tables,
|
||||
// TESTDATA for demo user tables); the staging dir stays in the control lib.
|
||||
@@ -56,6 +56,31 @@ if (schema.length > 0) {
|
||||
})
|
||||
}
|
||||
|
||||
// ─── POST_EDIT_HOOK ──────────────────────────────────────────────────────────
|
||||
// Mirrors mpe_loader.sas: %mpe_runhook(POST_EDIT_HOOK) runs before the MPE_SUBMIT
|
||||
// record is written, while LIBREF / DS are still ordinary macro variables - which
|
||||
// is what lets a hook re-point a changeset at a different table. The mock
|
||||
// eval()s the hook's .js counterpart in this scope so it can reassign
|
||||
// libref / dsn / libds; the submit record then names the new base table, exactly
|
||||
// as the SAS hook does via call symputx.
|
||||
const hookTables = makeTableLoader(mpeDataDir)('MPE_TABLES')
|
||||
const hookReg = (hookTables && hookTables.rows)
|
||||
? hookTables.rows.find(r => r.libref === libref && r.dsn === dsn)
|
||||
: null
|
||||
if (hookReg && hookReg.post_edit_hook) {
|
||||
const hookSrc = mockHookSource(hookReg.post_edit_hook)
|
||||
if (hookSrc) {
|
||||
try {
|
||||
eval(hookSrc)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'post_edit_hook ' + hookReg.post_edit_hook + ' failed: ' + err.message
|
||||
)
|
||||
}
|
||||
libds = libref + '.' + dsn
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stage the data ──────────────────────────────────────────────────────────
|
||||
// One subfolder per submission (DSID) under the staging directory, so that the
|
||||
// staged data can sit alongside related artifacts (submit/approval logs,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Mock of a POST_EDIT_HOOK program
|
||||
//
|
||||
// Registered against TESTDATA.DEMO_MIRROR (MPE_TABLES.post_edit_hook).
|
||||
//
|
||||
// This file is eval'd by editors/stagedata.js and editors/loadfile.js
|
||||
// (mirroring how mpe_loader.sas %includes the hook program via %mpe_runhook),
|
||||
// so it shares the caller's scope: libref, dsn, libds and every dcMockUtils
|
||||
// function are available directly.
|
||||
//
|
||||
// The real hook runs in mpe_loader BEFORE the submit record is written, while
|
||||
// LIBREF / DS are still ordinary macro variables - which is what lets it
|
||||
// re-point a changeset at a different table:
|
||||
//
|
||||
// data _null_;
|
||||
// call symputx('libref','TESTDATA');
|
||||
// call symputx('ds','DEMO_ORDERS');
|
||||
// run;
|
||||
//
|
||||
// (call symputx reaches the calling service's variable because LIBREF/DS are
|
||||
// not declared %local in mpe_loader; a %let creates a new variable in the
|
||||
// hook's own scope and is discarded.)
|
||||
//
|
||||
// The mock's equivalent is to reassign libref / dsn, so the MPE_SUBMIT record
|
||||
// names the real table and the approval is raised against it rather than
|
||||
// against the empty mirror.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
libref = 'TESTDATA'
|
||||
dsn = 'DEMO_ORDERS'
|
||||
console.log('[HOOK DEBUG] demo_mirror_postedit: routing changeset to ' + libref + '.' + dsn)
|
||||
@@ -0,0 +1,35 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Mock of a PRE_EDIT_HOOK program
|
||||
//
|
||||
// Registered against TESTDATA.DEMO_MIRROR (MPE_TABLES.pre_edit_hook), which is
|
||||
// an EMPTY mirror of TESTDATA.DEMO_ORDERS.
|
||||
//
|
||||
// This file is eval'd by editors/getdata.js (mirroring how getdata.sas
|
||||
// %includes the hook program via %mpe_runhook), so it shares the caller's
|
||||
// scope: visibleRows, visibleColumns, tableReg, requestedLibref and every
|
||||
// dcMockUtils function (loadTableAnyLib, makeTableLoader, libDataDir, ...) are
|
||||
// available directly.
|
||||
//
|
||||
// The real hook is a .sas program which runs after the filter has been applied
|
||||
// and the table sorted, with the editor data in work.OUT:
|
||||
//
|
||||
// data work.out;
|
||||
// set TESTDATA.DEMO_ORDERS;
|
||||
// run;
|
||||
//
|
||||
// The mock's equivalent is to replace visibleRows / visibleColumns with the
|
||||
// rows and columns of the real table, so an empty mirror still shows live data.
|
||||
// (A real hook that must honour the user's filter has to re-apply it - the
|
||||
// filter has already been applied to the empty mirror by the time it runs.)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const realTable = loadTableAnyLib('DEMO_ORDERS')
|
||||
if (realTable && realTable.data) {
|
||||
visibleRows = realTable.data.rows.map((row) => ({ ...row }))
|
||||
visibleColumns = realTable.data.columns
|
||||
console.log(
|
||||
'[HOOK DEBUG] demo_mirror_preedit: loaded ' +
|
||||
visibleRows.length +
|
||||
' rows from DEMO_ORDERS'
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user