Monday, 8 March 2021

Generic ABAP Value Helps using Fundamental Library for ABAP and Web Components

Exploring web development trends you might have already heard about Web Components, Custom Elements or Shadow DOM. These not so new standards (origins 2011) are still not always easy to try in own applications. Some web frameworks support Web Components more than others, some are less or not at all interested, providing only inevitable capabilities.

Aurelia web-standards based platform supports Web Components for years. Offering a great playground for hands-on experience what web components can do for ui framework and more important, for your application.

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.

Based on: Fundamental Library for ABAP, SAP/node-rfc, abap-value-help and Aurelia, doable with other ui frameworks as well.

Let start from the experience of web developer, implementing conventions’ based app.

Here we have a regular (web-components based) input, bound to READ_FLOC field of the View-Model ABAP structure ES_SPECIFIC.

<ui-input ddic-length="40" label="Functional Location Label"

  value.bind="equipment.ES_SPECIFIC.READ_FLOC" 

</ui-input>

SAP ABAP Exam Prep, SAP ABAP Certification, SAP ABAP Learning, SAP ABAP Career, SAP ABAP Preparation

Adding a custom attribute shlp turns any regular input into ABAP Value Help enabled input:

 <ui-input ddic-length="40" ddic-type="CHAR" label="Functional Location Label"
  value.bind="equipment.ES_SPECIFIC.READ_FLOC"
  shlp.bind="{type: 'SH', id: 'IFLM'}" mid="IFL">
</ui-input>

SAP ABAP Exam Prep, SAP ABAP Certification, SAP ABAP Learning, SAP ABAP Career, SAP ABAP Preparation

The visual difference is input add-on at the right hand side, indicating Value Help capability.

On Value Help user request (keyboard or click on icon), it happens something like in SAPGUI. The Value Help dialog is presented, to capture the selection criteria (user input). The selection criteria is then passed to Value Help generic search service and the search result is returned back to the dialog. User can cancel or confirm one search result value, to be captured as ui component input.

SAP ABAP Exam Prep, SAP ABAP Certification, SAP ABAP Learning, SAP ABAP Career, SAP ABAP Preparation

Combo box is the default ui representation of data elements with Fixed Domain Values or Check Tables (CT) Value Help type. No need for a dialog, only the code list coming from the same generic ABAP API is shown in a drop-down:

SAP ABAP Exam Prep, SAP ABAP Certification, SAP ABAP Learning, SAP ABAP Career, SAP ABAP Preparation

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


Generic ABAP API takes ca 150 lines of code (example implementation), exposing

◉ 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

SAP ABAP Exam Prep, SAP ABAP Certification, SAP ABAP Learning, SAP ABAP Career, SAP ABAP Preparation

No comments:

Post a Comment