This blog describes one real-life prototyping case, how the “magic” of generic ABAP Value Helps “happens” in conventions’ based web applications, leveraging web components super-powers.
Here we have a regular (web-components based) input, bound to READ_FLOC field of the View-Model ABAP structure ES_SPECIFIC.
Like everything else in
conventions’ based apps, the Value Help “magic” comes from four simple layers: ABAP, server (Node/Java/Python), View-Model and View. Let have a look in detail.
ABAP API
◉ ABAP Domain Values list (code list) based on ABAP domain name
◉ Value Help selection descriptor, based on Help id, used for dynamic selection form creation
◉ Search service, returning the search result descriptor and search result, based on selection criteria (user input)
Server
The server
example takes ca 100 lines of code, based on Node express:
const express = require("express");
const ValueHelp = require("abap-value-help").ValueInputHelp;
const Client = require("abap-value-help").Client;
// Generic ABAP Value Help API used in prototyping system
const shlpApi = {
rfm_domvalues_get: "/COE/SHLP_DOMVALUES_GET",
rfm_metadata_get: "/COE/SHLP_METADATA_GET",
rfm_search: "/COE/SHLP_VALUES_GET",
};
const PORT = 3000;
const app = express();
let client;
let valueHelp;
app.use(express.json());
// Authenticate and open client connection
// Closed in /logout or automatically
app.route("/login").all(async (req, res) => {
try {
client = new Client(Object.keys(req.body).length > 0 ? req.body : { dest: "MME" });
await client.open();
const user = await client.call("BAPI_USER_GET_DETAIL", {
USERNAME: req.body.username || "DEMO",
});
// User parameters (SU3) passed to Value Helps handler
// make user defaults appear in Value Help web forms just like in SAPGUI
valueHelp = new ValueHelp(client, shlpApi, user.PARAMETER);
res.json("connected");
} catch (ex) {
res.json(ex.message);
}
});
// Fixed domain values
app.route("/fieldvalues").all(async (req, res) => {
if (!(client && client.alive)) {
return res.json("Do the login first");
}
const result = await valueHelp.getDomainValues(
Object.keys(req.body).length > 0 ? req.body : "RET_TYPE"
);
res.json(result);
});
// Complex/elementary Value Help descriptor (SH type)
// used to dynamically build the frontend Value Input dialog
app.route("/helpselect").all(async (req, res) => {
if (!(client && client.alive)) {
return res.json("Do the login first");
}
const descriptor = await valueHelp.getShlpDescriptor(
Object.keys(req.body).length > 0 ? req.body : { type: "SH", name: "CC_VBELN" }
);
res.json(descriptor);
});
// Run the search using selection parameters from Value Input Dialog
app.route("/search").all(async (req, res) => {
if (!(client && client.alive)) {
return res.json("Do the login first");
}
const shlpId = req.body.shlpId
? req.body.shlpId
: { type: "SH", name: "VMVAA" };
const selection = req.body.selection
? req.body.selection
: [
//["AUART", "I", "EQ", "OR", ""],
["BSTKD", "I", "EQ", "212345678", ""],
["VKORG", "I", "EQ", "1000", ""],
];
const result = await valueHelp.search(shlpId, selection);
res.json(result);
});
// Close the connection (optional)
app.route("/logout").all(async (req, res) => {
if (client && client.alive) await client.close();
res.json("disconnected");
});
app.listen(PORT, () =>
console.log(
"ABAP Value Help server ready:",
`\nhttp://localhost:${PORT}/login`,
`\nhttp://localhost:${PORT}/fieldvalues`,
`\nhttp://localhost:${PORT}/helpselect`,
`\nhttp://localhost:${PORT}/search`,
`\nhttp://localhost:${PORT}/logout`
)
);
Server routes can be locally tested by POSTing Value Help ids or selection criteria ,or from the web browser, opening below listed routes (GET requests). Web browser tests send fixed requests to ABAP system, getting therefore fixed results:
gh repo clone SAP/fundamental-tools
cd abap-value-help/doc/server
npm install
node index
ABAP Value Help server ready:
http://localhost:3000/login
http://localhost:3000/fieldvalues
http://localhost:3000/helpselect
http://localhost:3000/search
http://localhost:3000/logout
In either case, unit tests and test data provide more insight into choreography and exchanged data structures.
View and View-Model
Value Input dialog is dynamically created, based on so called Value Help descriptors. One descriptor describes one “elementary” Value Help and array of descriptors describes a “composite” Value Help. The dialog is identical for both, except the composite Value Help has one dropdown ui element at the top (combo box) to select one particular elementary Value Help, for selection and search.
Aurelia View and View-Model implementation require ca 300 lines of code in total:
◉ doc/dialog/ui-search-help.html
◉ doc/dialog/ui-search-help.js
Hardcopy below, just in case sources are moved or changed,
View
<template>
<ai-dialog class="ui-search-help-dialog">
<ai-dialog-header>
<ui-row stretch>
<ui-combo options.bind="helpSelector" value.bind="shlpname" disabled.bind="helpSelectorDisabled"
select.delegate="selectHelp($event.detail.id)"></ui-combo>
</ui-row>
</ai-dialog-header>
<ai-dialog-body>
<!-- Search Parameters -->
<ui-row middle repeat.for="searchParam of searchParams" if.bind="!shlp.hide" class="ui-f4-param">
<ui-column size="md-4" class="ui-param-text">${searchParam.FIELDTEXT}</ui-column>
<ui-combo options.bind="helpSign" value.bind="searchParam.SIGN"></ui-combo>
<ui-combo options.bind="helpOption" value.bind="searchParam.OPTION"></ui-combo>
<ui-column fill>
<ui-input clear if.bind="searchParam.DATATYPE !== 'DATS' && searchParam.DATATYPE !== 'TIMS' "
id="${searchParam.FIELDNAME}_low" ddic-type="${searchParam.DATATYPE}"
ddic-length="${searchParam.LENG}" value.two-way="searchParam.LOW">
</ui-input>
<ui-input clear if.bind="searchParam.DATATYPE !== 'DATS' && searchParam.DATATYPE !== 'TIMS' "
show.bind="(searchParam.OPTION ==='BT') || (searchParam.OPTION ==='NB')"
id="${searchParam.FIELDNAME}_high" ddic-type="${searchParam.DATATYPE}"
ddic-length="${searchParam.LENG}" value.two-way="searchParam.HIGH">
</ui-input>
<ui-date clear if.bind="searchParam.DATATYPE === 'DATS' || searchParam.DATATYPE === 'TIMS' "
id="${searchParam.FIELDNAME}_low" ddic-type="${searchParam.DATATYPE}"
ddic-length="${searchParam.LENG}" date.two-way="searchParam.LOW">
</ui-date>
<ui-date clear if.bind="searchParam.DATATYPE === 'DATS' || searchParam.DATATYPE === 'TIMS' "
show.bind="(searchParam.OPTION ==='BT') || (searchParam.OPTION ==='NB')"
id="${searchParam.FIELDNAME}_high" ddic-type="${searchParam.DATATYPE}"
ddic-length="${searchParam.LENG}" date.two-way="searchParam.HIGH">
</ui-date>
</ui-column>
</ui-row>
<ui-row middle class="ui-f4-param">
<ui-column size="md-4" class="ui-param-text">Max. rows</ui-column>
<ui-input ddic-type="INT2" ddic-length="4" value.bind="maxRows"></ui-input>
</ui-column>
</ui-row>
<ui-row middle>
<ui-button reject click.delegate="cancel()" icon-suffix="sap-icon sap-icon-delete" label="Cancel">
</ui-button>
<ui-button default click.delegate="search()" icon-suffix="sap-icon sap-icon-search" label="Search">
</ui-button>
<ui-button accept click.delegate="confirm()" icon-suffix="sap-icon sap-icon-accept"
label="Confirm: ${selectedValue}" show.bind="selectedValue"></ui-button>
<!--ui-column auto class="ui-selected-value ui-pull-right">${selectedValue}</ui-column-->
<ui-column auto class="ui-pull-right" show.bind="searchResult.length">Found ${searchResult.length}
</ui-column>
</ui-row>
<!-- Search Result -->
<ui-row column class="ui-f4-result">
<ui-body scroll>
<ui-datagrid __title="${searchResult.length} records" selectable
rowselect.trigger="rowSelect($event)" empty-text="No records found" ref="__result">
</ui-datagrid>
</ui-body>
</ui-row>
</ai-dialog-body>
<ai-dialog-footer>
<ui-row>
</ui-row>
</ai-dialog-footer>
</ai-dialog>
</template>
View-Model
import { DialogController } from 'aurelia-dialog';
import { UIApplication } from '../../utils/ui-application';
import { UIHttpService } from '../../utils/ui-http-service';
export class UISearchHelp {
searchParams = [];
searchResult = [];
helpSelector = [];
helpSelectorDisabled = false;
helpSign = [{ id: 'I', name: 'Include' }, { id: 'E', name: 'Exclude' }];
helpOption = [
{ id: 'EQ', name: 'is' },
{ id: 'NE', name: 'is not' },
{ id: 'GT', name: 'greater than' },
{ id: 'LT', name: 'less than' },
{ id: 'GE', name: 'not less' },
{ id: 'LE', name: 'not greater' },
{ id: 'BT', name: 'between' },
{ id: 'NB', name: 'not between' },
{ id: 'CP', name: 'with pattern' },
{ id: 'NP', name: 'w/o pattern' }
];
static inject = [UIApplication, UIHttpService, DialogController, Element];
constructor(app, httpService, controller, element) {
this.app = app;
this.userParams = this.app.User.params;
this.httpService = httpService;
this.controller = controller;
this.element = element;
}
activate(shlp) {
this.shlp = shlp;
// get the collective help list and open the first elementary help search form
this.httpService
.backend('/helpselect', shlp)
.then(FROM_ABAP => {
this.elementaryHelps = FROM_ABAP.elementary_helps;
// update help selection list
let helpList = [];
if (this.shlp.blacklist) this.shlp.blacklist = this.shlp.blacklist.toString();
else this.shlp.blacklist = '';
for (let shlpname of FROM_ABAP.sort_order) {
if (this.shlp.blacklist.indexOf(shlpname) !== -1) continue; // no no
helpList.push({ id: shlpname, name: this.elementaryHelps[shlpname].INTDESCR.DDTEXT });
}
this.helpSelector = helpList;
this.helpSelectorDisabled = helpList.length === 1;
if (this.shlp.autoselect) this.selectHelp(this.shlp.autoselect);
else this.selectHelp(helpList[0].id);
})
.catch(error => {
this.app.toastError(error);
});
}
clearSearchResult() {
this.selectedRow = [];
this.searchResult = [];
this.valueColumn = '';
this.selectedValue = null;
this.maxRowsExceeded = false;
}
selectHelp(shlpname = null) {
if (shlpname === null) return; // todo
this.maxRows = 500;
this.clearSearchResult();
//selected Help
this.shlpname = shlpname;
//if (Boolean(this.elementaryHelps[shlpname])) {
// this.shlptitle = this.elementaryHelps[shlpname].INTDESCR.DDTEXT;
//}
// set search params
this.searchParams = this.elementaryHelps[shlpname].FIELDDESCR;
for (let param of this.searchParams) {
if (this.shlp.selection && this.shlp.selection[param.FIELDNAME] !== undefined) {
param.PARVA = this.shlp.selection[param.FIELDNAME];
}
param.OPTION = 'EQ';
param.SIGN = 'I';
param.HIGH = '';
param.LOW = param.PARVA;
param.STYLE = {};
// param.STYLE.width = param.STYLE.width;
// FIXME: to takeover all user defaults, uncomment following check
if (param.MEMORYID && this.userParams) {
if (this.userParams[param.MEMORYID]) {
let midValue = this.userParams[param.MEMORYID].value;
param.LOW = midValue !== null ? midValue : '';
}
}
}
// console.log('elementary Params', this.shlpname, 'Params:', this.searchParams);
if (this.shlp.run) {
// autorun
// this.search(); todo
}
else {
setTimeout(() => {
let firstInput = this.element.querySelector('.ui-f4-param .ui-input-group:not(.ui-combo) input');
if (firstInput) firstInput.focus();
}, 200);
}
}
search() {
// init the result
this.clearSearchResult();
// prepare selection parameters
let selection = [];
let selectionLine = [];
for (let param of this.searchParams) {
selectionLine = [];
// console.log(i, this.searchParams[i]);
if (param.OPTION !== 'BT' && param.OPTION !== 'NB') {
if (param.LOW.length > 0) {
selectionLine.push(param.FIELDNAME);
selectionLine.push(param.SIGN);
selectionLine.push(param.OPTION);
selectionLine.push(param.LOW);
selectionLine.push('');
}
}
else if (param.LOW.length > 0 || param.HIGH.length > 0) {
selectionLine.push(param.FIELDNAME);
selectionLine.push(param.SIGN);
selectionLine.push(param.OPTION);
selectionLine.push(param.LOW);
selectionLine.push(param.HIGH);
}
if (selectionLine.length > 0) {
selection.push(selectionLine);
}
}
this.httpService
.backend('/search', {
shlpname: this.shlpname,
selection: selection,
maxrows: this.maxRows,
compact: false
})
.then(FROM_ABAP => {
this.maxRowsExceeded = isTrue(FROM_ABAP.maxrows_exceeded);
// result value column index (used in compact mode)
//for (let i = 0; i < this.Headers.length; i++) {
// if (this.Headers[i][0] === this.valueColumn) {
// this.valueColumnIndex = i;
// break;
// }
//}
// search result columns
let columns = [];
for (let h of FROM_ABAP.headers) {
let c = {
__title: h.title,
ddicType: h.abaptype,
dataLength: h.len,
dataId: h.field,
align: h.text_align,
__sortable: true
};
columns.push(c);
}
// search result data
this.searchResult = FROM_ABAP.search_result;
this.__result.__handle.refresh(columns, this.searchResult);
// output column
this.valueColumn = this.shlp.valueColumn || FROM_ABAP.shlpoutput;
})
.catch(error => {
this.app.toastError(error);
});
}
rowSelect(evt) {
this.selectedRow = evt.detail;
this.selectedValue = evt.detail[this.valueColumn];
}
cancel() {
this.controller.cancel();
}
confirm() {
let result = {
selectedValue: this.selectedValue,
selectedRow: this.selectedRow
};
if (this.shlp.textColumn) result.selectedText = this.selectedRow[this.shlp.textColumn];
this.controller.ok(result);
}
}
The Flow
No comments:
Post a Comment