From 8d56175437466c72569251a27128fa1423c20eef Mon Sep 17 00:00:00 2001 From: bsrdjan Date: Wed, 17 Mar 2021 16:34:10 +0100 Subject: [PATCH] abap-api-tools Value Help descriptors added to backend output --- README.md | 15 ++- abap-api-tools/README.md | 9 +- abap-api-tools/package-lock.json | 29 +++++- abap-api-tools/package.json | 5 +- abap-api-tools/src/ts/abap.ts | 19 +++- abap-api-tools/src/ts/backend.ts | 172 +++++++++++++++++++++++++++---- abap-api-tools/src/ts/utils.ts | 6 +- doc/app.md | 16 +-- 8 files changed, 221 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 0b018ad9..05697148 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,8 @@ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](https://www.typescriptlang.org/) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-f8bc45.svg)](https://github.com/prettier/prettier) -- Minimalistic, unobtrusive, powerful - Web standards based applications with ABAP programming model -- Front-end frameworks: +- Standard front-ends: - [Aurelia](http://aurelia.io/) - Angular, React and Vue by [SAP Fundamenal Library](https://sap.github.io/fundamental/) - Angular, React and Vue by [Microsoft FAST](https://www.fast.design/docs/introduction/) @@ -18,16 +17,16 @@ - Servers - SAP Cloud Platform - Koa, express, Spark, Jooby, Sanic, Django, Pyramid, Flask, Tornado ... -- Application frameworks: electron, NW.js ... -- Building [pattern based application](./doc/app.md) +- Frameworks: electron, NW.js ... +- [Conventions based application](./doc/app.md) -Pattern based apps components +Components -- [**abap-api-tools**](./abap-api-tools/README.md) node CLI tool for pattern based applications +- [abap-api-tools](./abap-api-tools/README.md) node CLI tool, app elements generator -- [**abap-value-help**](./abap-value-help/README.md) generic Value Help runtime component +- [abap-value-help](./abap-value-help/README.md) Generic Value Helps server API -:star: Your star is appreciated, it helps! +:star: Your star helps! ## Content diff --git a/abap-api-tools/README.md b/abap-api-tools/README.md index e6349ded..c1d42044 100644 --- a/abap-api-tools/README.md +++ b/abap-api-tools/README.md @@ -1,19 +1,20 @@ # abap api tools -![NPM](https://img.shields.io/npm/l/abap-api-tools) +![License](https://img.shields.io/npm/l/abap-api-tools) -Command line tool for building [pattern based applications](https://github.com/SAP/fundamental-tools/blob/main/doc/app.md): +CLI tool for [conventions' based applications](https://github.com/SAP/fundamental-tools/blob/main/doc/app.md) - BAPI/RFM call templates ([What is BAPI/RFM?](https://sap.github.io/cloud-sdk/docs/java/features/bapi-and-rfc/bapi-and-rfc-overview/)) -- Ui components with ABAP data annotations: +- Ui components with ABAP annotations: - [Aurelia](http://aurelia.io/) - Angular, React and Vue by [SAP Fundamenal Library](https://sap.github.io/fundamental/) - Angular, React and Vue by [Microsoft FAST](https://www.fast.design/docs/introduction/) - UI5 web components for [React](https://sap.github.io/ui5-webcomponents-react/?path=/story/getting-started--page) - Custom configurations, open for integration - Minimalistic, unobtrusive, powerful +- [Conventions' based app](https://github.com/SAP/fundamental-tools/blob/main/doc/app.md) -:gift: Generic Value Help runtime component: [**abap-value-help**](https://github.com/SAP/fundamental-tools/blob/main/abap-value-help/README.md) +:gift: Generic Value Helps: [abap-value-help](https://github.com/SAP/fundamental-tools/blob/main/abap-value-help/README.md) ## Content diff --git a/abap-api-tools/package-lock.json b/abap-api-tools/package-lock.json index d7b17875..7df79b71 100644 --- a/abap-api-tools/package-lock.json +++ b/abap-api-tools/package-lock.json @@ -1,16 +1,17 @@ { "name": "abap-api-tools", - "version": "1.9.3", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.9.3", + "version": "2.0.0", "cpu": [ "!arm" ], "license": "Apache-2.0", "dependencies": { + "abap-value-help": "^0.9.6", "chalk": "^4.1.0", "js-yaml": "^4.0.0", "loglevel": "^1.7.1", @@ -1260,6 +1261,21 @@ "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "dev": true }, + "node_modules/abap-value-help": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/abap-value-help/-/abap-value-help-0.9.6.tgz", + "integrity": "sha512-EVb8IJZUhS5sCmPxbPgk7rUJ7tRkQVmwrb6PqMjkzXQDm5FAsF1VuKk78AmPAbPP7IFa6/k6P8CScxQCOL1YUQ==", + "cpu": [ + "!arm" + ], + "dependencies": { + "loglevel": "^1.7.1", + "node-rfc": "^2.4.0" + }, + "engines": { + "node": "~10 >=10.23 || ~12 >=12.17 || >= 14.0" + } + }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -8311,6 +8327,15 @@ "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "dev": true }, + "abap-value-help": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/abap-value-help/-/abap-value-help-0.9.6.tgz", + "integrity": "sha512-EVb8IJZUhS5sCmPxbPgk7rUJ7tRkQVmwrb6PqMjkzXQDm5FAsF1VuKk78AmPAbPP7IFa6/k6P8CScxQCOL1YUQ==", + "requires": { + "loglevel": "^1.7.1", + "node-rfc": "^2.4.0" + } + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", diff --git a/abap-api-tools/package.json b/abap-api-tools/package.json index b3a7f237..89f6cba8 100644 --- a/abap-api-tools/package.json +++ b/abap-api-tools/package.json @@ -1,7 +1,7 @@ { "name": "abap-api-tools", "description": "ABAP api tools", - "version": "1.9.3", + "version": "2.0.0", "homepage": "https://github.com/sap/fundamental-tools", "author": "SAP", "license": "Apache-2.0", @@ -28,7 +28,7 @@ "scripts": { "ts": "tsc -p src/ts && cp -r src/configuration dist/.", "lint": "eslint src/ts", - "dependencies": "npm i --save chalk js-yaml loglevel sprintf-js yargs node-rfc", + "dependencies": "npm i --save chalk js-yaml loglevel sprintf-js yargs node-rfc abap-value-help", "devDependencies": "npm i --save-dev @types/node @types/js-yaml @types/sprintf-js @types/yargs @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint jest typescript", "build": "reuse lint && rm -rf dist && npm run ts && npm run lint", "lock": "npm install --package-lock-only", @@ -45,6 +45,7 @@ "nodejs" ], "dependencies": { + "abap-value-help": "^0.9.6", "chalk": "^4.1.0", "js-yaml": "^4.0.0", "loglevel": "^1.7.1", diff --git a/abap-api-tools/src/ts/abap.ts b/abap-api-tools/src/ts/abap.ts index 5d8fc596..bcd9bd68 100644 --- a/abap-api-tools/src/ts/abap.ts +++ b/abap-api-tools/src/ts/abap.ts @@ -50,7 +50,12 @@ export type Destination = | string | { connectionParameters: RfcConnectionParameters; - searchHelpApi?: { determine: string; dom_values: string }; + searchHelpApi?: { + determine: string; + dom_values: string; + metadata?: string; + search?: string; + }; }; export { @@ -78,6 +83,7 @@ export type Arguments = { ui?: string | AbapCliUiConfig; "sort-fields"?: boolean; runInBg?: boolean; + helps?: boolean; }; export type AbapCliResult = { @@ -182,6 +188,7 @@ export class AbapCliApi { lang: DefaultLanguage, "sort-fields": false, debug: false, + helps: false, }; async call( @@ -219,7 +226,7 @@ export class AbapCliApi { async get( dest: Destination, rfm_names: string | string[], - options?: { lang?: string; debug?: boolean } + options?: { lang?: string; debug?: boolean; helps?: boolean } ): Promise { if (options) { Object.assign(this.options, options); @@ -239,6 +246,7 @@ export class AbapCliApi { lang: this.options.lang, debug: this.options.debug, runInBg: true, + helps: this.options.helps, }; const cli = new CliHandler(args); @@ -382,6 +390,13 @@ if (require.main === module) type: "boolean", default: false, nargs: 0, + }) + .option("h", { + alias: "helps", + describe: "Value Helps descriptors", + type: "boolean", + default: false, + nargs: 0, }); }, handler: (argv) => { diff --git a/abap-api-tools/src/ts/backend.ts b/abap-api-tools/src/ts/backend.ts index 922d540f..35693b07 100644 --- a/abap-api-tools/src/ts/backend.ts +++ b/abap-api-tools/src/ts/backend.ts @@ -4,6 +4,7 @@ // check search_help_api +import { ValueInputHelp, ElementaryHelpType } from "abap-value-help"; import chalk from "chalk"; import fs from "fs"; import path from "path"; @@ -93,20 +94,15 @@ type FieldValuesHelpType = { type: string; count: number; values: Record; + descriptor?: RfcTable; }; -type HelpsType = Record< - string, - | { - // search helps - title: string; - valueProperty?: string[]; - displayProperty?: string[]; - selection?: Record[]; - requestedFields?: string[]; - } - | FieldValuesHelpType // field values ->; +type ElementaryHelpsList = Record[]; +type StandardHelpType = { + title: string; + elementaryHelps?: ElementaryHelpsList; +}; +type HelpsType = Record; type StatType = Record< string, @@ -120,6 +116,17 @@ type StatType = Record< type UsageType = Record; +type ValueHelpError = Record; + +type ValueHelpElementary = { + blacklist?: "selection" | "search"; + selectionDescriptor?: ElementaryHelpType | ValueHelpError; + resultDescriptor?: RfcTable | ValueHelpError; + resultLine?: RfcTable; +}; + +type DescriptorsType = Record; + export type AnnotationsType = { alpha?: { all: string[]; rfm: Record }; parameters: yamlParametersType; @@ -127,11 +134,17 @@ export type AnnotationsType = { stat: StatType; helps?: HelpsType; usage?: UsageType; + descriptors?: DescriptorsType; }; // search help typings -type SearchHelpApiType = { determine: string; dom_values: string }; +type SearchHelpApiType = { + determine: string; + dom_values: string; + metadata?: string; + search?: string; +}; type SystemsYamlType = Record; export class Backend { private argv: Arguments; @@ -144,6 +157,7 @@ export class Backend { private getSearchHelps = false; private SPRAS: string; private Helps: HelpsType; + private Descriptors: DescriptorsType; private Stat: StatType; private Texts = {} as yamlTextsType; @@ -159,6 +173,7 @@ export class Backend { this.alpha = new Alpha(); this.SPRAS = Languages[this.argv.lang].spras; this.Helps = {}; + this.Descriptors = {}; this.Stat = {}; this.clientConnectionParameters = {}; @@ -174,7 +189,7 @@ export class Backend { } else { this.clientConnectionParameters = this.argv.dest.connectionParameters; if (this.argv.dest.searchHelpApi) { - this.search_help_api = this.argv.dest.searchHelpApi; + Object.assign(this.search_help_api, this.argv.dest.searchHelpApi); } this.systemId = this.clientConnectionParameters.ashost || @@ -214,14 +229,22 @@ export class Backend { // search help api plausibility check if (!isEmpty(this.search_help_api)) { for (const [apiKey, apiName] of Object.entries(this.search_help_api)) { - if (!["determine", "dom_values"].includes(apiKey)) { + if ( + !["determine", "dom_values", "metadata", "search"].includes(apiKey) + ) { throw new Error(`Search Help API key not supported: "${apiKey}"`); } - if (apiName.length > 30) { + if ((apiName as string).length > 30) { throw new Error(`Search help API name too long: "${apiName}"`); } } } + for (const apiName of ["metadata", "search"]) { + if (!this.search_help_api[apiName]) { + log.info(`Search help API not maintained: ${apiName}`); + this.argv.helps = false; + } + } // search helps processing this.getSearchHelps = @@ -318,7 +341,7 @@ export class Backend { } // - // value input help + // Value Helps // let shlp: RfcParameterValue = "", @@ -433,10 +456,10 @@ export class Backend { sh_val_fields.push(tf["FIELDNAME"] as string); } this.Helps[shlp_key] = { - valueProperty: sh_val_fields, - displayProperty: [], - selection: [], - requestedFields: [], + //valueProperty: sh_val_fields, + //displayProperty: [], + //selection: [], + //requestedFields: [], title: tab_text, }; } @@ -470,9 +493,108 @@ export class Backend { delete result.input; } + // Value Help descriptors + if (shlp_key && this.argv.helps && !this.Descriptors[shlp_key]) { + this.valueHelps(shlp_key); + } + return result as FieldType; } + async valueHelps(shlp_key: string): Promise { + if (shlp_key && this.argv.helps && !this.Descriptors[shlp_key]) { + const [stype, sname] = shlp_key.split(" "); + // + // selection + // + log.debug(`Value Help selection: ${shlp_key}`); + const elementary_helps: ElementaryHelpsList = []; + try { + const descriptors = await this.client.call( + this.search_help_api.metadata as string, + { + IV_SHLPTYPE: stype, + IV_SHLPNAME: sname, + } + ); + // parse elementary helps + for (const desc of descriptors.ET_SHLP as RfcTable) { + const skey = `${desc.SHLPTYPE} ${desc.SHLPNAME}`; + elementary_helps.push({ + [skey]: (desc.INTDESCR as RfcStructure).DDTEXT as string, + }); + if (!this.Descriptors[skey]) { + this.Descriptors[skey] = { + selectionDescriptor: ValueInputHelp.elementary(desc), + }; + } + } + if (elementary_helps.length > 1) { + if (!(this.Helps[shlp_key] as StandardHelpType).elementaryHelps) { + (this.Helps[ + shlp_key + ] as StandardHelpType).elementaryHelps = elementary_helps; + } + } + } catch (ex) { + log.debug(`Value Help metadata error: ${shlp_key}`, ex); + this.Descriptors[shlp_key] = { + blacklist: "selection", + selectionDescriptor: ex, + }; + } + + // + // search result + // + for (const eh of elementary_helps) { + const skey = Object.keys(eh)[0]; + const [stype, sname] = shlp_key.split(" "); + if (!this.Descriptors[skey].blacklist) { + try { + const searchResult = await this.client.call( + this.search_help_api.search as string, + { + IV_SHLPTYPE: stype, + IV_SHLPNAME: sname, + } + ); + // value descriptor + this.Descriptors[ + skey + ].resultDescriptor = searchResult.ET_VALUE_DESC as RfcTable; + + const value_list = searchResult.ET_VALUE_LIST as RfcTable; + if (value_list.length > 0) { + const record_pos = value_list[0].RECORDPOS; + let count = 0; + for (const value_line of value_list) { + if (record_pos !== value_line.RECORDPOS) { + break; + } else { + count++; + //delete value_line.SHLPNAME; + //delete value_line.RECORDPOS; + } + } + // search result "descriptor" + this.Descriptors[ + skey + ].resultLine = (searchResult.ET_VALUE_LIST as RfcTable).slice( + 0, + count + ); + } + } catch (ex) { + this.Descriptors[skey].blacklist = "search"; + this.Descriptors[skey].resultDescriptor = ex; + log.debug(`Value Help search error: ${skey} `, ex); + } + } + } + } + } + async annotations( param: ParameterType, langu = this.SPRAS @@ -608,7 +730,9 @@ export class Backend { log.info( `\n${chalk.bold(this.api_name)} ${this.systemId} (${this.argv.lang}) ${ this.argv.textOnly ? "only texts" : "" - } ${this.getSearchHelps ? "search helps" : ""}\n`.replace(/ +/g, " ") + } ${this.getSearchHelps ? "search helps" : ""}${ + this.argv.helps ? " w. descriptors" : "" + }\n`.replace(/ +/g, " ") ); await this.client.open(); @@ -848,6 +972,7 @@ export class Backend { helps: this.Helps, stat: this.Stat, usage: usage, + descriptors: this.Descriptors, }; if (!this.argv.runInBg && this.argv.cmd === Command.get) { @@ -893,6 +1018,8 @@ export class Backend { } fileSave(path.join(folder_yaml, "stat.yaml"), abap.stat); fileSave(path.join(folder_yaml, "usage.yaml"), abap.usage); + if (!isEmpty(abap.descriptors)) + fileSave(path.join(folder_yaml, "descriptors.yaml"), abap.descriptors); } if (!isEmpty(texts)) { fileSave(path.join(folder_yaml, "texts.yaml"), texts, { @@ -918,6 +1045,7 @@ export class Backend { "alpha", "usage", "texts", + "descriptors", ]) { deleteFile(path.join(folder_yaml, `${fileName}.yaml`)); } diff --git a/abap-api-tools/src/ts/utils.ts b/abap-api-tools/src/ts/utils.ts index f722a5cc..c5bbe93c 100644 --- a/abap-api-tools/src/ts/utils.ts +++ b/abap-api-tools/src/ts/utils.ts @@ -28,7 +28,7 @@ export function fileLoad(fileName: string): unknown { flag: "r", }); return fileName.match(/\.(yaml|yml)$/) - ? yaml.load(content, { schema: yaml.JSON_SCHEMA }) + ? yaml.load(content) //, { schema: yaml.JSON_SCHEMA }) : content; } @@ -40,7 +40,7 @@ export function fileSave( log.debug(`file save ${fileName}`); if (typeof content !== "string") { - options.schema = yaml.JSON_SCHEMA; + //options.schema = yaml.JSON_SCHEMA; content = yaml.dump(content, options); if (!fileName.match(/\.(yaml|yml)$/)) fileName += ".yaml"; } @@ -63,7 +63,7 @@ export function makeDir(dir: string): void { log.debug(`mkdir ${dir}`); } -export function isEmpty(obj?: any[] | Record): boolean { +export function isEmpty(obj?: unknown[] | Record): boolean { if (obj === undefined) return true; if (Array.isArray(obj)) return obj.length === 0; return Object.keys(obj).length === 0; diff --git a/doc/app.md b/doc/app.md index d0fc014d..f8611ecb 100644 --- a/doc/app.md +++ b/doc/app.md @@ -4,7 +4,7 @@ Conventions' based apps solve complex problems by re-usable patterns, rather tha Without frameworks in between you and your application, what remains are re-usable solution-patterns and basic programming skills of JavaScript or ABAP, you can start with. -- [Why Patterns?](#why-patterns) +- [Why Conventions?](#why-conventions) - [Technical Landscape](#technical-landscape) - [Components](#components) - [ABAP API](#abap-api) @@ -14,14 +14,16 @@ Without frameworks in between you and your application, what remains are re-usab - [App = ABAP API + Server Model + View Model + View](#app--abap-api--server-model--view-model--view) - [Deployment options](#deployment-options) -## Why Patterns? +## Why Conventions? -- Conventions' based model results in so little code, that pattern copy & adapt appears more handy than learning, configuring and modifying a "wizzard" or generator like "master/detail", doing the same. -- Diversity requirements is hard to cover by generic framework, wizzard, generator ... +- By factors less code +- Convention copy & adapt is a way more effective then learning, configuring and modifying any "wizzard" or generator output like "master/detail", doing the same. +- Diverse functional/visual requirements are hard to cover by generic framework, wizzard, generator ... - The model works the same way with any ui framework. Mostly used with standards-based [Aurelia](https://aurelia.io/), which is - - Based on simple conventions, easy to learn - - Practically invisible in applications, developers can entirely focus on application, not the framework + - Also conventions based, easy to learn + - Practically invisible in applications so that developers can focus on application, not the framework - Support other ui frameworks' templating systems +- Don't like conventions? Frame your conventions into your owm framework, always staying in full control. ## Technical Landscape @@ -205,7 +207,7 @@ abap make aurelia -c my-app # from the first ABAP API step above - Standard ui components' templates (default ui configurations) can be pure HTML, pure JS or the combination of both. It depends on ui framework and standard usage pattern can be easy changed (custom ui configurations). -- Generic and custom Value Helps are supported by `shlp` custom attribute: [abap-value-help](../abap-value-help/README.md) +- Generic and custom Value Helps can be generated and adapted in design time, or dynamically generated in run-time. Can be attched to any ui component by `shlp` custom attribute: [abap-value-help](../abap-value-help/README.md) One example with input, datepicker and checkbox: