From c20b5d0ab405c586e341f38233ec2fd2b1766178 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 20 Dec 2024 15:52:37 +0200 Subject: [PATCH 01/56] feat: introduce ADP FLP config generator --- .../.eslintignore | 3 + .../adp-flp-config-sub-generator/.eslintrc.js | 7 + .../adp-flp-config-sub-generator/CHANGELOG.md | 1 + packages/adp-flp-config-sub-generator/LICENSE | 201 ++++++++++++++++++ .../adp-flp-config-sub-generator/README.md | 20 ++ .../jest.config copy.js | 6 + .../jest.config.js | 6 + .../adp-flp-config-sub-generator/package.json | 65 ++++++ .../src/app/index.ts | 155 ++++++++++++++ .../src/app/types.ts | 15 ++ .../tsconfig.eslint.json | 4 + .../tsconfig.json | 46 ++++ tsconfig.json | 3 + 13 files changed, 532 insertions(+) create mode 100644 packages/adp-flp-config-sub-generator/.eslintignore create mode 100644 packages/adp-flp-config-sub-generator/.eslintrc.js create mode 100644 packages/adp-flp-config-sub-generator/CHANGELOG.md create mode 100644 packages/adp-flp-config-sub-generator/LICENSE create mode 100644 packages/adp-flp-config-sub-generator/README.md create mode 100644 packages/adp-flp-config-sub-generator/jest.config copy.js create mode 100644 packages/adp-flp-config-sub-generator/jest.config.js create mode 100644 packages/adp-flp-config-sub-generator/package.json create mode 100644 packages/adp-flp-config-sub-generator/src/app/index.ts create mode 100644 packages/adp-flp-config-sub-generator/src/app/types.ts create mode 100644 packages/adp-flp-config-sub-generator/tsconfig.eslint.json create mode 100644 packages/adp-flp-config-sub-generator/tsconfig.json diff --git a/packages/adp-flp-config-sub-generator/.eslintignore b/packages/adp-flp-config-sub-generator/.eslintignore new file mode 100644 index 0000000000..09379ed8fa --- /dev/null +++ b/packages/adp-flp-config-sub-generator/.eslintignore @@ -0,0 +1,3 @@ +/test/test-output/ +/test/unit/expected-output/ +generators diff --git a/packages/adp-flp-config-sub-generator/.eslintrc.js b/packages/adp-flp-config-sub-generator/.eslintrc.js new file mode 100644 index 0000000000..b717f83ae9 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['../../.eslintrc'], + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: __dirname + } +}; diff --git a/packages/adp-flp-config-sub-generator/CHANGELOG.md b/packages/adp-flp-config-sub-generator/CHANGELOG.md new file mode 100644 index 0000000000..ba973e6468 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/CHANGELOG.md @@ -0,0 +1 @@ +# @sap-ux/adp-flp-config-sub-generator diff --git a/packages/adp-flp-config-sub-generator/LICENSE b/packages/adp-flp-config-sub-generator/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/adp-flp-config-sub-generator/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/adp-flp-config-sub-generator/README.md b/packages/adp-flp-config-sub-generator/README.md new file mode 100644 index 0000000000..c5d15570ca --- /dev/null +++ b/packages/adp-flp-config-sub-generator/README.md @@ -0,0 +1,20 @@ +# @sap-ux/adp-flp-config-s + +## Features + +The SAP Adaptation Project FLP sub-generator enables users to create a FLP configuration for an Adaptation Project. + +## Installation + +The SAP Adaptation Project FLP Configuration sub-generator is included in the [@sap/generator-fiori](https://www.npmjs.com/package/@sap/generator-fiori) generator and cannot be used independently. Additionally, it requires the [SAP Fiori Tools Extension](https://marketplace.visualstudio.com/items?itemName=SAPSE.sap-ux-fiori-tools-extension-pack) pack from the VSCode marketplace. + + +## Launch the SAP Reference Library sub-generator + +Open the Command Palette in MS Visual Studio Code ( CMD/CTRL + Shift + P ) and execute the Fiori: Adaptation Project FLP configuration command. + + +## Keywords +SAP Fiori Elements +Yeoman +Generator diff --git a/packages/adp-flp-config-sub-generator/jest.config copy.js b/packages/adp-flp-config-sub-generator/jest.config copy.js new file mode 100644 index 0000000000..2f0a4db758 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/jest.config copy.js @@ -0,0 +1,6 @@ +const config = require('../../jest.base'); +config.snapshotFormat = { + escapeString: false, + printBasicPrototype: false +}; +module.exports = config; diff --git a/packages/adp-flp-config-sub-generator/jest.config.js b/packages/adp-flp-config-sub-generator/jest.config.js new file mode 100644 index 0000000000..2f0a4db758 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/jest.config.js @@ -0,0 +1,6 @@ +const config = require('../../jest.base'); +config.snapshotFormat = { + escapeString: false, + printBasicPrototype: false +}; +module.exports = config; diff --git a/packages/adp-flp-config-sub-generator/package.json b/packages/adp-flp-config-sub-generator/package.json new file mode 100644 index 0000000000..f381365b1d --- /dev/null +++ b/packages/adp-flp-config-sub-generator/package.json @@ -0,0 +1,65 @@ +{ + "name": "@sap-ux/adp-flp-config-sub-generator", + "description": "Generator for adding FLP configuration to an Adaptation Project", + "version": "0.0.1", + "repository": { + "type": "git", + "url": "https://github.com/SAP/open-ux-tools.git", + "directory": "packages/adp-flp-config-sub-generator" + }, + "bugs": { + "url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue" + }, + "license": "Apache-2.0", + "main": "generators/app/index.js", + "scripts": { + "build": "tsc --build", + "clean": "rimraf --glob generators test/test-output coverage *.tsbuildinfo", + "watch": "tsc --watch", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "test": "jest --ci --forceExit --detectOpenHandles --colors --passWithNoTests", + "test-u": "jest --ci --forceExit --detectOpenHandles --colors -u", + "link": "pnpm link --global", + "unlink": "pnpm unlink --global" + }, + "files": [ + "LICENSE", + "generators", + "!generators/*.map", + "!generators/**/*.map" + ], + "dependencies": { + "@sap-devx/yeoman-ui-types": "1.14.4", + "@sap-ux/adp-tooling": "workspace:*", + "@sap-ux/btp-utils": "workspace:*", + "@sap-ux/flp-config-inquirer": "workspace:*", + "@sap-ux/odata-service-inquirer": "workspace:*", + "@sap-ux/project-access": "workspace:*", + "@sap-ux/axios-extension": "workspace:*", + "@sap-ux/system-access": "workspace:*", + "@sap-ux/store": "workspace:*", + "@sap-ux/logger": "workspace:*", + "@sap-ux/fiori-generator-shared": "workspace:*", + "@sap-ux/feature-toggle": "workspace:*", + "i18next": "23.5.1", + "yeoman-generator": "5.10.0" + }, + "devDependencies": { + "@jest/types": "29.6.3", + "@types/fs-extra": "9.0.13", + "@types/vscode": "1.73.1", + "@types/yeoman-environment": "2.10.11", + "@types/yeoman-generator": "5.2.11", + "@types/yeoman-test": "4.0.6", + "@vscode-logging/logger": "2.0.0", + "@types/inquirer": "8.2.6", + "fs-extra": "10.0.0", + "rimraf": "5.0.5", + "typescript": "5.3.3", + "yeoman-test": "6.3.0" + }, + "engines": { + "node": ">=18.x" + } +} diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts new file mode 100644 index 0000000000..0c1eab1112 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -0,0 +1,155 @@ +import Generator from 'yeoman-generator'; +import type { Manifest } from '@sap-ux/project-access'; +import type { FlpConfigOptions } from './types'; +import type { Question } from 'inquirer'; +import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; + +import path, { join } from 'path'; +import { + ManifestService, + getVariant, + getAdpConfig, + getInboundsFromManifest, + getRegistrationIdFromManifest, + isCFEnvironment, + generateInboundConfig, + type InternalInboundNavigation +} from '@sap-ux/adp-tooling'; +import { ToolsLogger } from '@sap-ux/logger'; +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; +import { type AbapTarget } from '@sap-ux/system-access'; +import { getPrompts, type FLPConfigAnswers } from '@sap-ux/flp-config-inquirer'; +import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; +import { TelemetryHelper } from '@sap-ux/fiori-generator-shared'; +import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; +import { FileName } from '@sap-ux/project-access'; +import { isAppStudio } from '@sap-ux/btp-utils'; +import { SystemService, BackendSystemKey } from '@sap-ux/store'; + +/** + * Generator for adding a FLP configuration to an Adaptation Project. + * + * @extends Generator + */ +export default class extends Generator { + setPromptsCallback: (fn: object) => void; + prompts: Prompts; + existingProject: boolean; // Are we adding to an existing app or will a new app be generated in the writing phase of a parent generator + appWizard: AppWizard; + manifest: Manifest; + projectRootPath: string = ''; + answers: FLPConfigAnswers; + logger: ToolsLogger; + + /** + * Creates an instance of the generator. + * + * @param {string | string[]} args - The arguments passed to the generator. + * @param {FlpConfigOptions} opts - The options for the generator. + */ + constructor(args: string | string[], opts: FlpConfigOptions) { + super(args, opts); + this.appWizard = opts.appWizard ?? AppWizard.create(opts); + this.logger = new ToolsLogger(); + + this.projectRootPath = opts.data?.projectRootPath ?? this.destinationRoot(); + + // If launched standalone add nav steps + if (!opts.launchFlpConfigAsSubGenerator) { + this.prompts = new Prompts([ + { + name: 'FLP Configuration', + description: `FLP Configuration for ${path.basename(this.projectRootPath)}` + } + ]); + this.setPromptsCallback = (fn) => { + if (this.prompts) { + this.prompts.setCallback(fn); + } + }; + this.existingProject = true; // If this generator is composedWith Adaptation Project generator we assume the project is not written yet + } + } + + async initializing(): Promise { + // Generator does not support CF projects + if (isCFEnvironment(this.projectRootPath)) { + //TODO: Throw error in the UI + //handleErrorMessage(this.appWizard, t('ERROR_CF_PROJECT_NOT_SUPPORTED')); + return; + } + // Add telemetry to be sent once adp-flp-config is generated + await TelemetryHelper.initTelemetrySettings({ + consumerModule: { + name: '@sap/generator-fiori-deployment:adp-flp-config', + version: this.rootGeneratorVersion() + }, + internalFeature: isInternalFeaturesSettingEnabled(), + watchTelemetrySettingStore: false + }); + } + + public async prompting(): Promise { + await this._fetchManifest(); + const inbounds = getInboundsFromManifest(this.manifest); + const appId = getRegistrationIdFromManifest(this.manifest); + const prompts: Question[] = await getPrompts(inbounds, appId, { overwrite: { hide: true } }); + this.answers = (await this.prompt(prompts)) as FLPConfigAnswers; + } + + async writing(): Promise { + if (this.existingProject) { + const fs = await generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation); + fs.commit(() => this.appWizard?.showInformation('FLP Configuration complete', MessageType.notification)); + } + } + + public async end(): Promise { + // if (this.existingProject) { + // generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation); + // } + } + + /** + * Finds the configured system based on the provided target. + * + * @param {AbapTarget} target - The target ABAP system. + * @returns {Promise} The configured system. + */ + private async _findConfiguredSystem(target: AbapTarget): Promise { + let configuredSystem: string | undefined; + if (isAppStudio()) { + configuredSystem = target.destination; + } else { + const systemService = new SystemService(this.logger); + const backendSystem = new BackendSystemKey({ url: target.url as string, client: target.client ?? '' }); + configuredSystem = (await systemService.read(backendSystem))?.name; + } + + if (!configuredSystem) { + //TODO: Throw error in the UI + throw new Error("Couldn't find the configured system."); + } + + return configuredSystem; + } + + private async _fetchManifest(): Promise { + const { target } = await getAdpConfig(this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml)); + + const configuredSystem = await this._findConfiguredSystem(target); + const systemSelectionQuestions = await getSystemSelectionQuestions({ + systemSelection: { onlyShowDefaultChoice: true, defaultChoice: configuredSystem }, + serviceSelection: { hide: true } + }); + + const variant = getVariant(this.projectRootPath); + const manifestService = await ManifestService.initMergedManifest( + systemSelectionQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, + this.projectRootPath, + variant, + this.logger + ); + this.manifest = manifestService.getManifest(); + } +} diff --git a/packages/adp-flp-config-sub-generator/src/app/types.ts b/packages/adp-flp-config-sub-generator/src/app/types.ts new file mode 100644 index 0000000000..5005a87553 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/src/app/types.ts @@ -0,0 +1,15 @@ +import type { AppWizard } from '@sap-devx/yeoman-ui-types'; +import type { ManifestNamespace } from '@sap-ux/project-access'; +import type vscode from 'vscode'; +import type Generator from 'yeoman-generator'; + +export interface FlpConfigOptions extends Generator.GeneratorOptions { + vscode?: typeof vscode; + appWizard?: AppWizard; + destinationRoot?: string; + launchFlpConfigAsSubGenerator?: boolean; + inboundConfig: Partial; + data?: { + projectRootPath: string; + }; +} diff --git a/packages/adp-flp-config-sub-generator/tsconfig.eslint.json b/packages/adp-flp-config-sub-generator/tsconfig.eslint.json new file mode 100644 index 0000000000..d5f1aa3474 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "test", ".eslintrc.js"] +} diff --git a/packages/adp-flp-config-sub-generator/tsconfig.json b/packages/adp-flp-config-sub-generator/tsconfig.json new file mode 100644 index 0000000000..0e9aa5dc10 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/tsconfig.json @@ -0,0 +1,46 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src", + "src/**/*.json" + ], + "compilerOptions": { + "rootDir": "src", + "outDir": "generators" + }, + "references": [ + { + "path": "../adp-tooling" + }, + { + "path": "../axios-extension" + }, + { + "path": "../btp-utils" + }, + { + "path": "../feature-toggle" + }, + { + "path": "../fiori-generator-shared" + }, + { + "path": "../flp-config-inquirer" + }, + { + "path": "../logger" + }, + { + "path": "../odata-service-inquirer" + }, + { + "path": "../project-access" + }, + { + "path": "../store" + }, + { + "path": "../system-access" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index c9aeaa2c4b..2e89680cff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,9 @@ { "path": "packages/abap-deploy-config-writer" }, + { + "path": "packages/adp-flp-config-sub-generator" + }, { "path": "packages/adp-tooling" }, From 73b727f72cdbe66c1f1aff1e4434828235744a95 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 3 Jan 2025 16:37:12 +0200 Subject: [PATCH 02/56] fix: overwriting files --- .../src/app/index.ts | 42 ++++++++++--------- .../src/app/types.ts | 4 +- packages/adp-tooling/src/base/helper.ts | 8 +++- .../src/writer/inbound-navigation.ts | 4 +- packages/i18n/src/write/properties/create.ts | 2 +- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 0c1eab1112..28c685bc67 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -34,7 +34,7 @@ import { SystemService, BackendSystemKey } from '@sap-ux/store'; export default class extends Generator { setPromptsCallback: (fn: object) => void; prompts: Prompts; - existingProject: boolean; // Are we adding to an existing app or will a new app be generated in the writing phase of a parent generator + launchFlpConfigAsSubGenerator: boolean; appWizard: AppWizard; manifest: Manifest; projectRootPath: string = ''; @@ -50,13 +50,19 @@ export default class extends Generator { constructor(args: string | string[], opts: FlpConfigOptions) { super(args, opts); this.appWizard = opts.appWizard ?? AppWizard.create(opts); + this.launchFlpConfigAsSubGenerator = Boolean(opts.launchFlpConfigAsSubGenerator); + this.manifest = opts.manifest as Manifest; this.logger = new ToolsLogger(); this.projectRootPath = opts.data?.projectRootPath ?? this.destinationRoot(); // If launched standalone add nav steps - if (!opts.launchFlpConfigAsSubGenerator) { + if (!this.launchFlpConfigAsSubGenerator) { this.prompts = new Prompts([ + { + name: 'System Selection', + description: 'Select the system to use for the FLP Configuration' + }, { name: 'FLP Configuration', description: `FLP Configuration for ${path.basename(this.projectRootPath)}` @@ -67,15 +73,13 @@ export default class extends Generator { this.prompts.setCallback(fn); } }; - this.existingProject = true; // If this generator is composedWith Adaptation Project generator we assume the project is not written yet } } async initializing(): Promise { // Generator does not support CF projects if (isCFEnvironment(this.projectRootPath)) { - //TODO: Throw error in the UI - //handleErrorMessage(this.appWizard, t('ERROR_CF_PROJECT_NOT_SUPPORTED')); + this.appWizard.showError('FLP Configuration is not supported for CF projects', MessageType.notification); return; } // Add telemetry to be sent once adp-flp-config is generated @@ -90,23 +94,23 @@ export default class extends Generator { } public async prompting(): Promise { - await this._fetchManifest(); + if (!this.launchFlpConfigAsSubGenerator) { + await this._fetchManifest(); + } const inbounds = getInboundsFromManifest(this.manifest); const appId = getRegistrationIdFromManifest(this.manifest); - const prompts: Question[] = await getPrompts(inbounds, appId, { overwrite: { hide: true } }); + + const prompts: Question[] = await getPrompts(inbounds, appId, { + overwrite: { hide: true }, + createAnotherInbound: { hide: true } + }); this.answers = (await this.prompt(prompts)) as FLPConfigAnswers; } async writing(): Promise { - if (this.existingProject) { - const fs = await generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation); - fs.commit(() => this.appWizard?.showInformation('FLP Configuration complete', MessageType.notification)); - } - } - - public async end(): Promise { - // if (this.existingProject) { - // generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation); + // if (!this.launchFlpConfigAsSubGenerator) { + await generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation, this.fs); + // fs.commit(() => this.appWizard?.showInformation('FLP Configuration complete', MessageType.notification)); // } } @@ -127,8 +131,8 @@ export default class extends Generator { } if (!configuredSystem) { - //TODO: Throw error in the UI - throw new Error("Couldn't find the configured system."); + this.appWizard.showError('Couldn not find the configured system', MessageType.notification); + throw new Error('Could not find the configured system.'); } return configuredSystem; @@ -142,7 +146,7 @@ export default class extends Generator { systemSelection: { onlyShowDefaultChoice: true, defaultChoice: configuredSystem }, serviceSelection: { hide: true } }); - + await this.prompt(systemSelectionQuestions.prompts); const variant = getVariant(this.projectRootPath); const manifestService = await ManifestService.initMergedManifest( systemSelectionQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, diff --git a/packages/adp-flp-config-sub-generator/src/app/types.ts b/packages/adp-flp-config-sub-generator/src/app/types.ts index 5005a87553..95a2baeb7c 100644 --- a/packages/adp-flp-config-sub-generator/src/app/types.ts +++ b/packages/adp-flp-config-sub-generator/src/app/types.ts @@ -1,5 +1,5 @@ import type { AppWizard } from '@sap-devx/yeoman-ui-types'; -import type { ManifestNamespace } from '@sap-ux/project-access'; +import type { Manifest } from '@sap-ux/project-access'; import type vscode from 'vscode'; import type Generator from 'yeoman-generator'; @@ -8,7 +8,7 @@ export interface FlpConfigOptions extends Generator.GeneratorOptions { appWizard?: AppWizard; destinationRoot?: string; launchFlpConfigAsSubGenerator?: boolean; - inboundConfig: Partial; + manifest: Manifest; data?: { projectRootPath: string; }; diff --git a/packages/adp-tooling/src/base/helper.ts b/packages/adp-tooling/src/base/helper.ts index eb93b5634c..628446f948 100644 --- a/packages/adp-tooling/src/base/helper.ts +++ b/packages/adp-tooling/src/base/helper.ts @@ -10,9 +10,13 @@ import type { DescriptorVariant, AdpPreviewConfig } from '../types'; * Get the app descriptor variant. * * @param {string} basePath - The path to the adaptation project. + * @param {Editor} fs - The mem-fs editor instance. * @returns {DescriptorVariant} The app descriptor variant. */ -export function getVariant(basePath: string): DescriptorVariant { +export function getVariant(basePath: string, fs?: Editor): DescriptorVariant { + if (fs) { + return fs.readJSON(join(basePath, 'webapp', 'manifest.appdescr_variant')) as unknown as DescriptorVariant; + } return JSON.parse(readFileSync(join(basePath, 'webapp', 'manifest.appdescr_variant'), 'utf-8')); } @@ -24,7 +28,7 @@ export function getVariant(basePath: string): DescriptorVariant { * @param {Editor} fs - The mem-fs editor instance. */ export function updateVariant(basePath: string, variant: DescriptorVariant, fs: Editor) { - fs.writeJSON(join(basePath, 'webapp', 'manifest.appdescr_variant'), variant); + fs.extendJSON(join(basePath, 'webapp', 'manifest.appdescr_variant'), { content: variant.content }); } /** diff --git a/packages/adp-tooling/src/writer/inbound-navigation.ts b/packages/adp-tooling/src/writer/inbound-navigation.ts index bb52454277..620b295a0f 100644 --- a/packages/adp-tooling/src/writer/inbound-navigation.ts +++ b/packages/adp-tooling/src/writer/inbound-navigation.ts @@ -25,7 +25,7 @@ export async function generateInboundConfig( fs = create(createStorage()); } - const variant = getVariant(basePath); + const variant = getVariant(basePath, fs); if (!config?.inboundId) { config.addInboundId = true; @@ -53,7 +53,7 @@ export function getFlpI18nKeys(config: InternalInboundNavigation, appId: string) newEntries.push({ key: `${baseKey}.title`, value: config.title }); if (config?.subTitle) { - newEntries.push({ key: `${baseKey}.subTitle`, value: config.title }); + newEntries.push({ key: `${baseKey}.subTitle`, value: config.subTitle }); } return newEntries; diff --git a/packages/i18n/src/write/properties/create.ts b/packages/i18n/src/write/properties/create.ts index 1f4d54691c..0c295e644e 100644 --- a/packages/i18n/src/write/properties/create.ts +++ b/packages/i18n/src/write/properties/create.ts @@ -21,7 +21,7 @@ export async function createPropertiesI18nEntries( root?: string, fs?: Editor ): Promise { - if (!(await doesExist(i18nFilePath))) { + if (!fs?.exists(i18nFilePath) && !(await doesExist(i18nFilePath))) { let content = '# Resource bundle \n'; if (root) { content = `# This is the resource bundle for ${basename(root)}\n`; From ca379b360e78c4d5123d041a912902397a1f8aad Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 3 Jan 2025 17:54:45 +0200 Subject: [PATCH 03/56] fix: force generator to overwrite files --- packages/adp-flp-config-sub-generator/src/app/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 28c685bc67..a174fdb445 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -48,6 +48,8 @@ export default class extends Generator { * @param {FlpConfigOptions} opts - The options for the generator. */ constructor(args: string | string[], opts: FlpConfigOptions) { + // Force the generator to overwrite existing files without prompting + opts.force = true; super(args, opts); this.appWizard = opts.appWizard ?? AppWizard.create(opts); this.launchFlpConfigAsSubGenerator = Boolean(opts.launchFlpConfigAsSubGenerator); From a8fd14aa29368e021269246055d665ad0d334f19 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 6 Jan 2025 13:33:49 +0200 Subject: [PATCH 04/56] fix: add translations --- .../src/app/index.ts | 112 ++++++++++++------ .../src/app/types.ts | 4 +- .../src/telemetryEvents/index.ts | 6 + .../adp-flp-config-sub-generator.i18n.json | 21 ++++ .../src/utils/i18n.ts | 32 +++++ .../src/utils/logger.ts | 48 ++++++++ 6 files changed, 182 insertions(+), 41 deletions(-) create mode 100644 packages/adp-flp-config-sub-generator/src/telemetryEvents/index.ts create mode 100644 packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json create mode 100644 packages/adp-flp-config-sub-generator/src/utils/i18n.ts create mode 100644 packages/adp-flp-config-sub-generator/src/utils/logger.ts diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index a174fdb445..24f1ee6aa3 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -1,9 +1,10 @@ -import Generator from 'yeoman-generator'; import type { Manifest } from '@sap-ux/project-access'; import type { FlpConfigOptions } from './types'; import type { Question } from 'inquirer'; +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; +import type { AbapTarget } from '@sap-ux/system-access'; import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; - +import Generator from 'yeoman-generator'; import path, { join } from 'path'; import { ManifestService, @@ -16,15 +17,16 @@ import { type InternalInboundNavigation } from '@sap-ux/adp-tooling'; import { ToolsLogger } from '@sap-ux/logger'; -import type { AbapServiceProvider } from '@sap-ux/axios-extension'; -import { type AbapTarget } from '@sap-ux/system-access'; +import { EventName } from '../telemetryEvents'; import { getPrompts, type FLPConfigAnswers } from '@sap-ux/flp-config-inquirer'; import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; -import { TelemetryHelper } from '@sap-ux/fiori-generator-shared'; +import { TelemetryHelper, sendTelemetry } from '@sap-ux/fiori-generator-shared'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; import { isAppStudio } from '@sap-ux/btp-utils'; import { SystemService, BackendSystemKey } from '@sap-ux/store'; +import AdpFlpConfigLogger from '../utils/logger'; +import { t } from '../utils/i18n'; /** * Generator for adding a FLP configuration to an Adaptation Project. @@ -48,7 +50,7 @@ export default class extends Generator { * @param {FlpConfigOptions} opts - The options for the generator. */ constructor(args: string | string[], opts: FlpConfigOptions) { - // Force the generator to overwrite existing files without prompting + // Force the generator to overwrite existing files without additional prompting opts.force = true; super(args, opts); this.appWizard = opts.appWizard ?? AppWizard.create(opts); @@ -58,16 +60,16 @@ export default class extends Generator { this.projectRootPath = opts.data?.projectRootPath ?? this.destinationRoot(); - // If launched standalone add nav steps + // If launched standalone add navigation steps if (!this.launchFlpConfigAsSubGenerator) { this.prompts = new Prompts([ { - name: 'System Selection', - description: 'Select the system to use for the FLP Configuration' + name: t('yuiNavSteps.sysConfirmName'), + description: t('yuiNavSteps.sysConfirmDesc') }, { - name: 'FLP Configuration', - description: `FLP Configuration for ${path.basename(this.projectRootPath)}` + name: t('yuiNavSteps.flpConfigName'), + description: t('yuiNavSteps.flpConfigDesc', { projectName: path.basename(this.projectRootPath) }) } ]); this.setPromptsCallback = (fn) => { @@ -96,7 +98,7 @@ export default class extends Generator { } public async prompting(): Promise { - if (!this.launchFlpConfigAsSubGenerator) { + if (!this.manifest) { await this._fetchManifest(); } const inbounds = getInboundsFromManifest(this.manifest); @@ -110,52 +112,86 @@ export default class extends Generator { } async writing(): Promise { - // if (!this.launchFlpConfigAsSubGenerator) { - await generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation, this.fs); - // fs.commit(() => this.appWizard?.showInformation('FLP Configuration complete', MessageType.notification)); - // } + try { + await generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation, this.fs); + } catch (error) { + AdpFlpConfigLogger.logger.error(t('error.writingPhase', { error })); + throw new Error(t('error.updatingApp')); + } + } + + end(): void { + if (!this.launchFlpConfigAsSubGenerator) { + this.appWizard.showInformation(t('info.flpConfigAdded'), MessageType.notification); + } + try { + const telemetryData = TelemetryHelper.createTelemetryData(); + if (telemetryData) { + sendTelemetry(EventName.ADP_FLP_CONFIG_ADDED, telemetryData, this.projectRootPath).catch((error) => { + AdpFlpConfigLogger.logger.error(t('error.telemetry', { error })); + }); + } + } catch (error) { + AdpFlpConfigLogger.logger.error(t('error.endPhase', { error })); + } } /** - * Finds the configured system based on the provided target. + * Finds the configured system based on the provided target in ui5.yaml configuration. * * @param {AbapTarget} target - The target ABAP system. * @returns {Promise} The configured system. */ private async _findConfiguredSystem(target: AbapTarget): Promise { let configuredSystem: string | undefined; + if (isAppStudio()) { configuredSystem = target.destination; + if (!configuredSystem) { + throw new Error(t('error.destinationNotFound')); + } } else { + const { url, client } = target; + if (!url) { + throw new Error(t('error.systemNotFound')); + } const systemService = new SystemService(this.logger); - const backendSystem = new BackendSystemKey({ url: target.url as string, client: target.client ?? '' }); + const backendSystem = new BackendSystemKey({ url: url as string, client: client ?? '' }); configuredSystem = (await systemService.read(backendSystem))?.name; - } - - if (!configuredSystem) { - this.appWizard.showError('Couldn not find the configured system', MessageType.notification); - throw new Error('Could not find the configured system.'); + if (!configuredSystem) { + throw new Error(t('error.systemNotFoundInStore', { url })); + } } return configuredSystem; } + /** + * Fetches the manifest for the project. + * + * @returns {Promise} A promise that resolves when the manifest has been fetched. + */ private async _fetchManifest(): Promise { - const { target } = await getAdpConfig(this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml)); + try { + const { target } = await getAdpConfig(this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml)); - const configuredSystem = await this._findConfiguredSystem(target); - const systemSelectionQuestions = await getSystemSelectionQuestions({ - systemSelection: { onlyShowDefaultChoice: true, defaultChoice: configuredSystem }, - serviceSelection: { hide: true } - }); - await this.prompt(systemSelectionQuestions.prompts); - const variant = getVariant(this.projectRootPath); - const manifestService = await ManifestService.initMergedManifest( - systemSelectionQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, - this.projectRootPath, - variant, - this.logger - ); - this.manifest = manifestService.getManifest(); + const configuredSystem = await this._findConfiguredSystem(target); + const systemSelectionQuestions = await getSystemSelectionQuestions({ + systemSelection: { onlyShowDefaultChoice: true, defaultChoice: configuredSystem }, + serviceSelection: { hide: true } + }); + await this.prompt(systemSelectionQuestions.prompts); + const variant = getVariant(this.projectRootPath); + const manifestService = await ManifestService.initMergedManifest( + systemSelectionQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, + this.projectRootPath, + variant, + this.logger + ); + this.manifest = manifestService.getManifest(); + } catch (error) { + AdpFlpConfigLogger.logger.error(t('error.fetchingManifest', { error })); + throw new Error(t('error.fetchingManifest')); + } } } diff --git a/packages/adp-flp-config-sub-generator/src/app/types.ts b/packages/adp-flp-config-sub-generator/src/app/types.ts index 95a2baeb7c..0836e7e99f 100644 --- a/packages/adp-flp-config-sub-generator/src/app/types.ts +++ b/packages/adp-flp-config-sub-generator/src/app/types.ts @@ -1,12 +1,10 @@ import type { AppWizard } from '@sap-devx/yeoman-ui-types'; import type { Manifest } from '@sap-ux/project-access'; -import type vscode from 'vscode'; import type Generator from 'yeoman-generator'; export interface FlpConfigOptions extends Generator.GeneratorOptions { - vscode?: typeof vscode; + force?: boolean; appWizard?: AppWizard; - destinationRoot?: string; launchFlpConfigAsSubGenerator?: boolean; manifest: Manifest; data?: { diff --git a/packages/adp-flp-config-sub-generator/src/telemetryEvents/index.ts b/packages/adp-flp-config-sub-generator/src/telemetryEvents/index.ts new file mode 100644 index 0000000000..bda949558c --- /dev/null +++ b/packages/adp-flp-config-sub-generator/src/telemetryEvents/index.ts @@ -0,0 +1,6 @@ +/** + * Event names for telemetry for the adaptation project fiori launchpad configuration generator + */ +export enum EventName { + ADP_FLP_CONFIG_ADDED = 'ADP_FLP_CONFIG_ADDED' +} diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json new file mode 100644 index 0000000000..ae906b2751 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -0,0 +1,21 @@ +{ + "yuiNavSteps": { + "sysConfirmName": "System confirmation", + "sysConfirmDesc": "Confirm the system to use for FLP configuration", + "flpConfigName": "FLP Configuration", + "flpConfigDesc": "FLP Configuration for {{- projectName}}" + }, + "info": { + "flpConfigAdded": "FLP Configuration added successfully" + }, + "error": { + "fetchingManifest": "Error fetching merged manifest for app: {{- error}}", + "destinationNotFound": "Missing destination configuration in ui5.yaml", + "systemNotFound": "Missing system configuration in ui5.yaml", + "systemNotFoundInStore" : "System not found in the system store: {{- systemUrl}}", + "endPhase": "Error in end phase of the adaptation project FLP configuration: {{- error}}", + "writingPhase": "Error in writing phase of the adaptation project FLP configuration: {{- error}}", + "telemetry": "Error sending telemetry data: {{- error}}", + "updatingApp": "Error updating app with FLP configuration. Inspect the logs for full error." + } +} \ No newline at end of file diff --git a/packages/adp-flp-config-sub-generator/src/utils/i18n.ts b/packages/adp-flp-config-sub-generator/src/utils/i18n.ts new file mode 100644 index 0000000000..c5bbfe2010 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/src/utils/i18n.ts @@ -0,0 +1,32 @@ +import type { TOptions } from 'i18next'; +import i18next from 'i18next'; +import translations from '../translations/adp-flp-config-sub-generator.i18n.json'; + +const ui5LibI18nNamespace = 'ui5-library-reference-sub-generator'; + +/** + * Initialize i18next with the translations for this module. + */ +export async function initI18n(): Promise { + await i18next.init({ lng: 'en', fallbackLng: 'en' }, () => + i18next.addResourceBundle('en', ui5LibI18nNamespace, translations) + ); +} + +/** + * Helper function facading the call to i18next. Unless a namespace option is provided the local namespace will be used. + * + * @param key i18n key + * @param options additional options + * @returns {string} localized string stored for the given key + */ +export function t(key: string, options?: TOptions): string { + if (!options?.ns) { + options = Object.assign(options ?? {}, { ns: ui5LibI18nNamespace }); + } + return i18next.t(key, options); +} + +initI18n().catch(() => { + // Needed for lint +}); diff --git a/packages/adp-flp-config-sub-generator/src/utils/logger.ts b/packages/adp-flp-config-sub-generator/src/utils/logger.ts new file mode 100644 index 0000000000..2544cd0edc --- /dev/null +++ b/packages/adp-flp-config-sub-generator/src/utils/logger.ts @@ -0,0 +1,48 @@ +import { DefaultLogger, LogWrapper, type ILogWrapper } from '@sap-ux/fiori-generator-shared'; +import type { Logger } from 'yeoman-environment'; +import type { IVSCodeExtLogger, LogLevel } from '@vscode-logging/logger'; + +/** + * Static logger prevents passing of logger references through all functions, as this is a cross-cutting concern. + */ +export default class AdpFlpConfigLogger { + private static _logger: ILogWrapper = DefaultLogger; + + /** + * Get the logger. + * + * @returns the logger + */ + public static get logger(): ILogWrapper { + return AdpFlpConfigLogger._logger; + } + + /** + * Set the logger. + * + * @param value the logger to set + */ + public static set logger(value: ILogWrapper) { + AdpFlpConfigLogger._logger = value; + } + + /** + * Configures the vscode logger. + * + * @param vscLogger - the vscode logger + * @param loggerName - the logger name + * @param yoLogger - the yeoman logger + * @param vscode - the vscode instance + * @param logLevel - the log level + */ + static configureLogging( + vscLogger: IVSCodeExtLogger, + loggerName: string, + yoLogger: Logger, + vscode?: unknown, + logLevel?: LogLevel + ): void { + const logWrapper = new LogWrapper(loggerName, yoLogger, logLevel, vscLogger, vscode); + AdpFlpConfigLogger.logger = logWrapper; + } +} From db4e3f31883e139492edb782a2fffbde4f3a213a Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 6 Jan 2025 14:16:25 +0200 Subject: [PATCH 05/56] fix: added cf not supported error text --- packages/adp-flp-config-sub-generator/src/app/index.ts | 2 +- .../src/translations/adp-flp-config-sub-generator.i18n.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 24f1ee6aa3..c5f0107451 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -83,7 +83,7 @@ export default class extends Generator { async initializing(): Promise { // Generator does not support CF projects if (isCFEnvironment(this.projectRootPath)) { - this.appWizard.showError('FLP Configuration is not supported for CF projects', MessageType.notification); + this.appWizard.showError(t('error.cfNotSupported'), MessageType.notification); return; } // Add telemetry to be sent once adp-flp-config is generated diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index ae906b2751..e049ab789f 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -9,6 +9,7 @@ "flpConfigAdded": "FLP Configuration added successfully" }, "error": { + "cfNotSupported": "FLP Configuration is not supported for CF projects", "fetchingManifest": "Error fetching merged manifest for app: {{- error}}", "destinationNotFound": "Missing destination configuration in ui5.yaml", "systemNotFound": "Missing system configuration in ui5.yaml", From bacd976e7f80e63c793fdb735199f0844a3aa0c4 Mon Sep 17 00:00:00 2001 From: mmilko01 <162288787+mmilko01@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:23:04 +0200 Subject: [PATCH 06/56] chore: create changeset --- .changeset/tricky-pets-worry.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/tricky-pets-worry.md diff --git a/.changeset/tricky-pets-worry.md b/.changeset/tricky-pets-worry.md new file mode 100644 index 0000000000..03e059b8b5 --- /dev/null +++ b/.changeset/tricky-pets-worry.md @@ -0,0 +1,7 @@ +--- +"@sap-ux/adp-flp-config-sub-generator": patch +"@sap-ux/adp-tooling": patch +"@sap-ux/i18n": patch +--- + +Introduce ADP FLP config generator From cd9292f51849477c702473ce74c9d9d7a3051e3d Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 6 Jan 2025 15:27:53 +0200 Subject: [PATCH 07/56] chore: add lock file changes --- pnpm-lock.yaml | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ca99d90ba..d74dd318b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -478,6 +478,88 @@ importers: specifier: 10.0.0 version: 10.0.0 + packages/adp-flp-config-sub-generator: + dependencies: + '@sap-devx/yeoman-ui-types': + specifier: 1.14.4 + version: 1.14.4 + '@sap-ux/adp-tooling': + specifier: workspace:* + version: link:../adp-tooling + '@sap-ux/axios-extension': + specifier: workspace:* + version: link:../axios-extension + '@sap-ux/btp-utils': + specifier: workspace:* + version: link:../btp-utils + '@sap-ux/feature-toggle': + specifier: workspace:* + version: link:../feature-toggle + '@sap-ux/fiori-generator-shared': + specifier: workspace:* + version: link:../fiori-generator-shared + '@sap-ux/flp-config-inquirer': + specifier: workspace:* + version: link:../flp-config-inquirer + '@sap-ux/logger': + specifier: workspace:* + version: link:../logger + '@sap-ux/odata-service-inquirer': + specifier: workspace:* + version: link:../odata-service-inquirer + '@sap-ux/project-access': + specifier: workspace:* + version: link:../project-access + '@sap-ux/store': + specifier: workspace:* + version: link:../store + '@sap-ux/system-access': + specifier: workspace:* + version: link:../system-access + i18next: + specifier: 23.5.1 + version: 23.5.1 + yeoman-generator: + specifier: 5.10.0 + version: 5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3) + devDependencies: + '@jest/types': + specifier: 29.6.3 + version: 29.6.3 + '@types/fs-extra': + specifier: 9.0.13 + version: 9.0.13 + '@types/inquirer': + specifier: 8.2.6 + version: 8.2.6 + '@types/vscode': + specifier: 1.73.1 + version: 1.73.1 + '@types/yeoman-environment': + specifier: 2.10.11 + version: 2.10.11 + '@types/yeoman-generator': + specifier: 5.2.11 + version: 5.2.11 + '@types/yeoman-test': + specifier: 4.0.6 + version: 4.0.6 + '@vscode-logging/logger': + specifier: 2.0.0 + version: 2.0.0 + fs-extra: + specifier: 10.0.0 + version: 10.0.0 + rimraf: + specifier: 5.0.5 + version: 5.0.5 + typescript: + specifier: 5.3.3 + version: 5.3.3 + yeoman-test: + specifier: 6.3.0 + version: 6.3.0(mem-fs@2.1.0)(yeoman-environment@3.19.3)(yeoman-generator@5.10.0) + packages/adp-tooling: dependencies: '@sap-ux/axios-extension': From c51d930939c89ed169d24963249fdef230dd0964 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 6 Jan 2025 16:02:46 +0200 Subject: [PATCH 08/56] test: fix adp-tooling tests --- .../test/unit/app.test.ts | 0 packages/adp-tooling/src/base/helper.ts | 2 +- .../test/unit/writer/inbound-navigation.test.ts | 14 ++++++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 packages/adp-flp-config-sub-generator/test/unit/app.test.ts diff --git a/packages/adp-flp-config-sub-generator/test/unit/app.test.ts b/packages/adp-flp-config-sub-generator/test/unit/app.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/adp-tooling/src/base/helper.ts b/packages/adp-tooling/src/base/helper.ts index 628446f948..74bfaddffa 100644 --- a/packages/adp-tooling/src/base/helper.ts +++ b/packages/adp-tooling/src/base/helper.ts @@ -28,7 +28,7 @@ export function getVariant(basePath: string, fs?: Editor): DescriptorVariant { * @param {Editor} fs - The mem-fs editor instance. */ export function updateVariant(basePath: string, variant: DescriptorVariant, fs: Editor) { - fs.extendJSON(join(basePath, 'webapp', 'manifest.appdescr_variant'), { content: variant.content }); + fs.writeJSON(join(basePath, 'webapp', 'manifest.appdescr_variant'), variant); } /** diff --git a/packages/adp-tooling/test/unit/writer/inbound-navigation.test.ts b/packages/adp-tooling/test/unit/writer/inbound-navigation.test.ts index dcae37c191..67faf22cc9 100644 --- a/packages/adp-tooling/test/unit/writer/inbound-navigation.test.ts +++ b/packages/adp-tooling/test/unit/writer/inbound-navigation.test.ts @@ -48,7 +48,7 @@ describe('FLP Configuration Functions', () => { await generateInboundConfig(basePath, config, fs); - expect(getVariantMock).toHaveBeenCalledWith(basePath); + expect(getVariantMock).toHaveBeenCalledWith(basePath, expect.any(Object)); expect(fs.writeJSON).toHaveBeenCalledWith(join(basePath, 'webapp', 'manifest.appdescr_variant'), variant); expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith( join(basePath, 'webapp', 'i18n', 'i18n.properties'), @@ -64,7 +64,7 @@ describe('FLP Configuration Functions', () => { const fs = await generateInboundConfig(basePath, config); expect(fs).toBeDefined(); - expect(getVariantMock).toHaveBeenCalledWith(basePath); + expect(getVariantMock).toHaveBeenCalledWith(basePath, expect.any(Object)); expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith( join(basePath, 'webapp', 'i18n', 'i18n.properties'), expect.any(Array), @@ -91,7 +91,10 @@ describe('FLP Configuration Functions', () => { expect(keys).toEqual([ { key: `${appId}_sap.app.crossNavigation.inbounds.${config.inboundId}.title`, value: config.title }, - { key: `${appId}_sap.app.crossNavigation.inbounds.${config.inboundId}.subTitle`, value: config.title } + { + key: `${appId}_sap.app.crossNavigation.inbounds.${config.inboundId}.subTitle`, + value: config.subTitle + } ]); }); @@ -110,7 +113,10 @@ describe('FLP Configuration Functions', () => { const i18nPath = join(basePath, 'webapp', 'i18n', 'i18n.properties'); const expectedEntries = [ { key: `${appId}_sap.app.crossNavigation.inbounds.${config.inboundId}.title`, value: config.title }, - { key: `${appId}_sap.app.crossNavigation.inbounds.${config.inboundId}.subTitle`, value: config.title } + { + key: `${appId}_sap.app.crossNavigation.inbounds.${config.inboundId}.subTitle`, + value: config.subTitle + } ]; await updateI18n(basePath, appId, config, fs); From f0f98dc818bcfbd50f356e454607efbbe55a6072 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Tue, 7 Jan 2025 11:32:49 +0200 Subject: [PATCH 09/56] chore: add reminder --- .../prompts/datasources/sap-system/system-selection/questions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts index 712fb92ceb..5aff1bd817 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts @@ -188,6 +188,7 @@ export async function getSystemConnectionQuestions( if (promptOptions?.systemSelection?.useAutoComplete && (selectedSystem as ListChoiceOptions).value) { selectedSystemAnswer = (selectedSystem as ListChoiceOptions).value; } + // TODO: Show message when connection is successful return ( validateSystemSelection(selectedSystemAnswer, connectionValidator, requiredOdataVersion) ?? false ); From 239255375c58bf099943d54d5b4da074c92226ec Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 11:13:42 +0200 Subject: [PATCH 10/56] chore: fix readme type --- packages/adp-flp-config-sub-generator/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adp-flp-config-sub-generator/README.md b/packages/adp-flp-config-sub-generator/README.md index c5d15570ca..8597246d07 100644 --- a/packages/adp-flp-config-sub-generator/README.md +++ b/packages/adp-flp-config-sub-generator/README.md @@ -1,4 +1,4 @@ -# @sap-ux/adp-flp-config-s +# @sap-ux/adp-flp-config-sub-generator ## Features From dfd39c846aab0d51263f83d5b1db13cc9c81ceab Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 11:14:26 +0200 Subject: [PATCH 11/56] chore: remove duplicate file --- packages/adp-flp-config-sub-generator/jest.config copy.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 packages/adp-flp-config-sub-generator/jest.config copy.js diff --git a/packages/adp-flp-config-sub-generator/jest.config copy.js b/packages/adp-flp-config-sub-generator/jest.config copy.js deleted file mode 100644 index 2f0a4db758..0000000000 --- a/packages/adp-flp-config-sub-generator/jest.config copy.js +++ /dev/null @@ -1,6 +0,0 @@ -const config = require('../../jest.base'); -config.snapshotFormat = { - escapeString: false, - printBasicPrototype: false -}; -module.exports = config; From 575d3a5e06ff953c7b3ce6d1886e72fc4a7190b9 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 13:17:20 +0200 Subject: [PATCH 12/56] fix: rename property and use shorthand --- packages/adp-flp-config-sub-generator/src/app/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index c5f0107451..8fc016870a 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -36,7 +36,8 @@ import { t } from '../utils/i18n'; export default class extends Generator { setPromptsCallback: (fn: object) => void; prompts: Prompts; - launchFlpConfigAsSubGenerator: boolean; + // Flag to determine if the generator was launched as a sub-generator or standalone + launchAsSubGen: boolean; appWizard: AppWizard; manifest: Manifest; projectRootPath: string = ''; @@ -54,14 +55,14 @@ export default class extends Generator { opts.force = true; super(args, opts); this.appWizard = opts.appWizard ?? AppWizard.create(opts); - this.launchFlpConfigAsSubGenerator = Boolean(opts.launchFlpConfigAsSubGenerator); + this.launchAsSubGen = !!opts.launchAsSubGen; this.manifest = opts.manifest as Manifest; this.logger = new ToolsLogger(); this.projectRootPath = opts.data?.projectRootPath ?? this.destinationRoot(); // If launched standalone add navigation steps - if (!this.launchFlpConfigAsSubGenerator) { + if (!this.launchAsSubGen) { this.prompts = new Prompts([ { name: t('yuiNavSteps.sysConfirmName'), @@ -121,7 +122,7 @@ export default class extends Generator { } end(): void { - if (!this.launchFlpConfigAsSubGenerator) { + if (!this.launchAsSubGen) { this.appWizard.showInformation(t('info.flpConfigAdded'), MessageType.notification); } try { From 2948e91e8ae27c990d7f09ad9b0371a35d31edb7 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 13:24:18 +0200 Subject: [PATCH 13/56] fix: simplify logger usage --- .../src/app/index.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 8fc016870a..df397e2059 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -20,7 +20,7 @@ import { ToolsLogger } from '@sap-ux/logger'; import { EventName } from '../telemetryEvents'; import { getPrompts, type FLPConfigAnswers } from '@sap-ux/flp-config-inquirer'; import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; -import { TelemetryHelper, sendTelemetry } from '@sap-ux/fiori-generator-shared'; +import { TelemetryHelper, sendTelemetry, type ILogWrapper } from '@sap-ux/fiori-generator-shared'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; import { isAppStudio } from '@sap-ux/btp-utils'; @@ -42,7 +42,8 @@ export default class extends Generator { manifest: Manifest; projectRootPath: string = ''; answers: FLPConfigAnswers; - logger: ToolsLogger; + toolsLogger: ToolsLogger; + logger: ILogWrapper; /** * Creates an instance of the generator. @@ -57,7 +58,8 @@ export default class extends Generator { this.appWizard = opts.appWizard ?? AppWizard.create(opts); this.launchAsSubGen = !!opts.launchAsSubGen; this.manifest = opts.manifest as Manifest; - this.logger = new ToolsLogger(); + this.toolsLogger = new ToolsLogger(); + this.logger = AdpFlpConfigLogger.logger; this.projectRootPath = opts.data?.projectRootPath ?? this.destinationRoot(); @@ -116,7 +118,7 @@ export default class extends Generator { try { await generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation, this.fs); } catch (error) { - AdpFlpConfigLogger.logger.error(t('error.writingPhase', { error })); + this.logger.error(t('error.writingPhase', { error })); throw new Error(t('error.updatingApp')); } } @@ -129,11 +131,11 @@ export default class extends Generator { const telemetryData = TelemetryHelper.createTelemetryData(); if (telemetryData) { sendTelemetry(EventName.ADP_FLP_CONFIG_ADDED, telemetryData, this.projectRootPath).catch((error) => { - AdpFlpConfigLogger.logger.error(t('error.telemetry', { error })); + this.logger.error(t('error.telemetry', { error })); }); } } catch (error) { - AdpFlpConfigLogger.logger.error(t('error.endPhase', { error })); + this.logger.error(t('error.endPhase', { error })); } } @@ -156,7 +158,7 @@ export default class extends Generator { if (!url) { throw new Error(t('error.systemNotFound')); } - const systemService = new SystemService(this.logger); + const systemService = new SystemService(this.toolsLogger); const backendSystem = new BackendSystemKey({ url: url as string, client: client ?? '' }); configuredSystem = (await systemService.read(backendSystem))?.name; if (!configuredSystem) { @@ -187,11 +189,11 @@ export default class extends Generator { systemSelectionQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, this.projectRootPath, variant, - this.logger + this.toolsLogger ); this.manifest = manifestService.getManifest(); } catch (error) { - AdpFlpConfigLogger.logger.error(t('error.fetchingManifest', { error })); + this.logger.error(t('error.fetchingManifest', { error })); throw new Error(t('error.fetchingManifest')); } } From 8a84433d2f8c5e9900c47be8e2a4b07d031a8356 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 13:31:21 +0200 Subject: [PATCH 14/56] fix: extract prompt setup in method --- .../src/app/index.ts | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index df397e2059..187742447b 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -65,21 +65,7 @@ export default class extends Generator { // If launched standalone add navigation steps if (!this.launchAsSubGen) { - this.prompts = new Prompts([ - { - name: t('yuiNavSteps.sysConfirmName'), - description: t('yuiNavSteps.sysConfirmDesc') - }, - { - name: t('yuiNavSteps.flpConfigName'), - description: t('yuiNavSteps.flpConfigDesc', { projectName: path.basename(this.projectRootPath) }) - } - ]); - this.setPromptsCallback = (fn) => { - if (this.prompts) { - this.prompts.setCallback(fn); - } - }; + this._setupPrompts(); } } @@ -197,4 +183,25 @@ export default class extends Generator { throw new Error(t('error.fetchingManifest')); } } + + /** + * Adds navigations steps and callback function for the generator prompts. + */ + private _setupPrompts(): void { + this.prompts = new Prompts([ + { + name: t('yuiNavSteps.sysConfirmName'), + description: t('yuiNavSteps.sysConfirmDesc') + }, + { + name: t('yuiNavSteps.flpConfigName'), + description: t('yuiNavSteps.flpConfigDesc', { projectName: path.basename(this.projectRootPath) }) + } + ]); + this.setPromptsCallback = (fn) => { + if (this.prompts) { + this.prompts.setCallback(fn); + } + }; + } } From 2c6ee78c8417a257a670d60302ae59042efb3248 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 13:34:30 +0200 Subject: [PATCH 15/56] fix: rename i18n namespace --- packages/adp-flp-config-sub-generator/src/utils/i18n.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/utils/i18n.ts b/packages/adp-flp-config-sub-generator/src/utils/i18n.ts index c5bbfe2010..3654ea71fe 100644 --- a/packages/adp-flp-config-sub-generator/src/utils/i18n.ts +++ b/packages/adp-flp-config-sub-generator/src/utils/i18n.ts @@ -2,14 +2,14 @@ import type { TOptions } from 'i18next'; import i18next from 'i18next'; import translations from '../translations/adp-flp-config-sub-generator.i18n.json'; -const ui5LibI18nNamespace = 'ui5-library-reference-sub-generator'; +const adpFlpConfigI18nNamespace = 'adp-flp-config-sub-generator'; /** * Initialize i18next with the translations for this module. */ export async function initI18n(): Promise { await i18next.init({ lng: 'en', fallbackLng: 'en' }, () => - i18next.addResourceBundle('en', ui5LibI18nNamespace, translations) + i18next.addResourceBundle('en', adpFlpConfigI18nNamespace, translations) ); } @@ -22,7 +22,7 @@ export async function initI18n(): Promise { */ export function t(key: string, options?: TOptions): string { if (!options?.ns) { - options = Object.assign(options ?? {}, { ns: ui5LibI18nNamespace }); + options = Object.assign(options ?? {}, { ns: adpFlpConfigI18nNamespace }); } return i18next.t(key, options); } From 59b3ce91e4077e309adc852650d560c8b7a228ae Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 13:48:14 +0200 Subject: [PATCH 16/56] fix: get system name via system-access --- .../adp-flp-config-sub-generator/src/app/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 187742447b..4fb69970fb 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -2,7 +2,7 @@ import type { Manifest } from '@sap-ux/project-access'; import type { FlpConfigOptions } from './types'; import type { Question } from 'inquirer'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; -import type { AbapTarget } from '@sap-ux/system-access'; +import type { AbapTarget, UrlAbapTarget } from '@sap-ux/system-access'; import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; import Generator from 'yeoman-generator'; import path, { join } from 'path'; @@ -24,7 +24,7 @@ import { TelemetryHelper, sendTelemetry, type ILogWrapper } from '@sap-ux/fiori- import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; import { isAppStudio } from '@sap-ux/btp-utils'; -import { SystemService, BackendSystemKey } from '@sap-ux/store'; +import { getCredentialsFromStore } from '@sap-ux/system-access'; import AdpFlpConfigLogger from '../utils/logger'; import { t } from '../utils/i18n'; @@ -131,7 +131,7 @@ export default class extends Generator { * @param {AbapTarget} target - The target ABAP system. * @returns {Promise} The configured system. */ - private async _findConfiguredSystem(target: AbapTarget): Promise { + private async _findConfiguredSystem(target: AbapTarget): Promise { let configuredSystem: string | undefined; if (isAppStudio()) { @@ -140,13 +140,12 @@ export default class extends Generator { throw new Error(t('error.destinationNotFound')); } } else { - const { url, client } = target; + const { url } = target; if (!url) { throw new Error(t('error.systemNotFound')); } - const systemService = new SystemService(this.toolsLogger); - const backendSystem = new BackendSystemKey({ url: url as string, client: client ?? '' }); - configuredSystem = (await systemService.read(backendSystem))?.name; + + const configuredSystem = (await getCredentialsFromStore(target as UrlAbapTarget, this.toolsLogger))?.name; if (!configuredSystem) { throw new Error(t('error.systemNotFoundInStore', { url })); } From ef44a9ccfab8039e03c70cc1e265f2ad339c9866 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 17:01:27 +0200 Subject: [PATCH 17/56] feat: add connection successful message in system selection prompt --- .../sap-system/system-selection/questions.ts | 11 +++++- .../odata-service-inquirer.i18n.json | 3 +- packages/odata-service-inquirer/src/types.ts | 5 +++ .../system-selection/questions.test.ts | 37 +++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts index 5aff1bd817..395e04c93e 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts @@ -188,12 +188,21 @@ export async function getSystemConnectionQuestions( if (promptOptions?.systemSelection?.useAutoComplete && (selectedSystem as ListChoiceOptions).value) { selectedSystemAnswer = (selectedSystem as ListChoiceOptions).value; } - // TODO: Show message when connection is successful return ( validateSystemSelection(selectedSystemAnswer, connectionValidator, requiredOdataVersion) ?? false ); }, additionalMessages: async (selectedSystem: SystemSelectionAnswerType) => { + // Show message if connection to selected system is successful and showConnectionSuccessMessage is enabled + if ( + promptOptions?.systemSelection?.showConnectionSuccessMessage && + (connectionValidator.validity.authenticated || connectionValidator.validity.authRequired === false) + ) { + return { + message: t('prompts.systemSelection.connectionSuccessMessage'), + severity: Severity.information + }; + } // Backend systems credentials may need to be updated if ( selectedSystem.type === 'backendSystem' && diff --git a/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json b/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json index f259fa4189..58a75fa4e5 100644 --- a/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json +++ b/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json @@ -103,7 +103,8 @@ "newSystemChoiceLabel": "New system", "hint": "Select a system configuration", "message": "System", - "authenticationFailedUpdateCredentials": "Authentication failed. Please try updating the credentials." + "authenticationFailedUpdateCredentials": "Authentication failed. Please try updating the credentials.", + "connectionSuccessMessage": "Connection successful" }, "abapOnBTPType": { "message": "ABAP environment definition source", diff --git a/packages/odata-service-inquirer/src/types.ts b/packages/odata-service-inquirer/src/types.ts index 37c7a332c1..19c7f6e8ae 100644 --- a/packages/odata-service-inquirer/src/types.ts +++ b/packages/odata-service-inquirer/src/types.ts @@ -268,6 +268,11 @@ export type SystemSelectionPromptOptions = { * this option will not be applied and the full list of choices will be presented to the user. */ onlyShowDefaultChoice?: boolean; + + /** + * Shows a message when the connection to the system is successful. + */ + showConnectionSuccessMessage?: boolean; }; export type MetadataPromptOptions = { diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts index 21de01b1f9..fe546409f2 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts @@ -630,4 +630,41 @@ describe('Test system selection prompts', () => { ); expect(serviceSelectionPrompt).toBeDefined(); }); + + test('Should show connestion success message when showConnectionSuccessMessage is provided as true and connections is successful', async () => { + const connectValidator = new ConnectionValidator(); + connectValidator.validity.authRequired = false; + connectValidator.validity.authenticated = true; + const systemSelectionQuestions = await getSystemConnectionQuestions(connectValidator, { + [promptNames.systemSelection]: { showConnectionSuccessMessage: true } + }); + const systemSelectionPrompt = systemSelectionQuestions.find( + (question) => question.name === promptNames.systemSelection + ); + expect((systemSelectionPrompt as ListQuestion).additionalMessages).toBeDefined(); + const additionalMessages = await (systemSelectionPrompt as ListQuestion).additionalMessages?.({ + type: 'backendSystem', + system: backendSystemBasic + } as SystemSelectionAnswerType); + expect(additionalMessages).toMatchObject({ + message: t('prompts.systemSelection.connectionSuccessMessage'), + severity: 2 + }); + }); + + test('Should not show connestion success message when showConnectionSuccessMessage is not provided and connections is successful', async () => { + const connectValidator = new ConnectionValidator(); + connectValidator.validity.authRequired = false; + connectValidator.validity.authenticated = true; + const systemSelectionQuestions = await getSystemConnectionQuestions(connectValidator); + const systemSelectionPrompt = systemSelectionQuestions.find( + (question) => question.name === promptNames.systemSelection + ); + expect((systemSelectionPrompt as ListQuestion).additionalMessages).toBeDefined(); + const additionalMessages = await (systemSelectionPrompt as ListQuestion).additionalMessages?.({ + type: 'backendSystem', + system: backendSystemBasic + } as SystemSelectionAnswerType); + expect(additionalMessages).not.toBeDefined(); + }); }); From a36d96804d4ffdced3359932406dedcd2834ff5f Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 8 Jan 2025 17:23:10 +0200 Subject: [PATCH 18/56] fix: system validation navigation name and description --- .../src/translations/adp-flp-config-sub-generator.i18n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index e049ab789f..c39dc8752b 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -1,7 +1,7 @@ { "yuiNavSteps": { - "sysConfirmName": "System confirmation", - "sysConfirmDesc": "Confirm the system to use for FLP configuration", + "sysConfirmName": "System connection validation", + "sysConfirmDesc": "Validates connection to configured system", "flpConfigName": "FLP Configuration", "flpConfigDesc": "FLP Configuration for {{- projectName}}" }, From 55b5bb2c56016a365cb73fe765697b6e8885b9cb Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Thu, 9 Jan 2025 18:26:35 +0200 Subject: [PATCH 19/56] test: add generator tests --- .../src/app/index.ts | 26 +- .../src/app/types.ts | 2 +- .../test/__snapshots__/app.test.ts.snap | 351 ++++++++++++++++ .../test/app.test.ts | 397 ++++++++++++++++++ .../app.variant1/webapp/i18n/i18n.properties | 4 + .../webapp/manifest.appdescr_variant | 164 ++++++++ .../test/unit/app.test.ts | 0 .../test/unit/__snapshots__/app.test.ts.snap | 392 ----------------- pnpm-lock.yaml | 3 + 9 files changed, 931 insertions(+), 408 deletions(-) create mode 100644 packages/adp-flp-config-sub-generator/test/__snapshots__/app.test.ts.snap create mode 100644 packages/adp-flp-config-sub-generator/test/app.test.ts create mode 100644 packages/adp-flp-config-sub-generator/test/fixtures/app.variant1/webapp/i18n/i18n.properties create mode 100644 packages/adp-flp-config-sub-generator/test/fixtures/app.variant1/webapp/manifest.appdescr_variant delete mode 100644 packages/adp-flp-config-sub-generator/test/unit/app.test.ts delete mode 100644 packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 4fb69970fb..f3faf0547e 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -72,8 +72,7 @@ export default class extends Generator { async initializing(): Promise { // Generator does not support CF projects if (isCFEnvironment(this.projectRootPath)) { - this.appWizard.showError(t('error.cfNotSupported'), MessageType.notification); - return; + throw new Error(t('error.cfNotSupported')); } // Add telemetry to be sent once adp-flp-config is generated await TelemetryHelper.initTelemetrySettings({ @@ -111,17 +110,13 @@ export default class extends Generator { end(): void { if (!this.launchAsSubGen) { - this.appWizard.showInformation(t('info.flpConfigAdded'), MessageType.notification); + this.appWizard?.showInformation(t('info.flpConfigAdded'), MessageType.notification); } - try { - const telemetryData = TelemetryHelper.createTelemetryData(); - if (telemetryData) { - sendTelemetry(EventName.ADP_FLP_CONFIG_ADDED, telemetryData, this.projectRootPath).catch((error) => { - this.logger.error(t('error.telemetry', { error })); - }); - } - } catch (error) { - this.logger.error(t('error.endPhase', { error })); + const telemetryData = TelemetryHelper.createTelemetryData() ?? {}; + if (telemetryData) { + sendTelemetry(EventName.ADP_FLP_CONFIG_ADDED, telemetryData, this.projectRootPath).catch((error) => { + this.logger.error(t('error.telemetry', { error })); + }); } } @@ -160,10 +155,9 @@ export default class extends Generator { * @returns {Promise} A promise that resolves when the manifest has been fetched. */ private async _fetchManifest(): Promise { + const { target } = await getAdpConfig(this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml)); + const configuredSystem = await this._findConfiguredSystem(target); try { - const { target } = await getAdpConfig(this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml)); - - const configuredSystem = await this._findConfiguredSystem(target); const systemSelectionQuestions = await getSystemSelectionQuestions({ systemSelection: { onlyShowDefaultChoice: true, defaultChoice: configuredSystem }, serviceSelection: { hide: true } @@ -204,3 +198,5 @@ export default class extends Generator { }; } } + +export type { FlpConfigOptions }; diff --git a/packages/adp-flp-config-sub-generator/src/app/types.ts b/packages/adp-flp-config-sub-generator/src/app/types.ts index 0836e7e99f..2421fe1e01 100644 --- a/packages/adp-flp-config-sub-generator/src/app/types.ts +++ b/packages/adp-flp-config-sub-generator/src/app/types.ts @@ -5,7 +5,7 @@ import type Generator from 'yeoman-generator'; export interface FlpConfigOptions extends Generator.GeneratorOptions { force?: boolean; appWizard?: AppWizard; - launchFlpConfigAsSubGenerator?: boolean; + launchAsSubGen?: boolean; manifest: Manifest; data?: { projectRootPath: string; diff --git a/packages/adp-flp-config-sub-generator/test/__snapshots__/app.test.ts.snap b/packages/adp-flp-config-sub-generator/test/__snapshots__/app.test.ts.snap new file mode 100644 index 0000000000..873e610e92 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/test/__snapshots__/app.test.ts.snap @@ -0,0 +1,351 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FLPConfigGenerator Integration Tests should generate FLP configuration successfully - Application Studio 1`] = ` +"{ + "fileName": "manifest", + "layer": "CUSTOMER_BASE", + "fileType": "appdescr_variant", + "reference": "mockReference", + "id": "customer.app.variant1", + "namespace": "apps/mockReference/appVariants/customer.app.variant1/", + "version": "0.1.0", + "content": [ + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "i18n" + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "@i18n" + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "mockModel" + }, + "texts": { + "i18n": "mockI18n" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "mockModel" + }, + "texts": { + "i18n": "mockI18n" + } + }, + { + "changeType": "appdescr_ui5_setMinUI5Version", + "content": { + "minUI5Version": "1.120.25" + } + }, + { + "changeType": "appdescr_app_setTitle", + "content": {}, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_addNewInbound", + "content": { + "inbound": { + "customer.app.variant1.InboundID": { + "action": "testAction", + "semanticObject": "testSemanticObject", + "title": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.title}}", + "signature": { + "additionalParameters": "allowed", + "parameters": { + "param1": { + "required": true, + "defaultValue": { + "value": "test1", + "format": "plain" + } + }, + "param2": { + "required": true, + "defaultValue": { + "value": "test2", + "format": "plain" + } + }, + "sap-appvar-id": { + "required": true, + "filter": { + "value": "customer.app.variant1", + "format": "plain" + }, + "launcherValue": { + "value": "customer.app.variant1" + } + } + } + }, + "subTitle": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.subTitle}}" + } + } + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_removeAllInboundsExceptOne", + "content": { + "inboundId": "customer.app.variant1.InboundID" + }, + "texts": {} + }, + { + "changeType": "appdescr_app_addNewInbound", + "content": { + "inbound": { + "customer.app.variant1.InboundID": { + "action": "testAction", + "semanticObject": "testSemanticObject", + "title": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.title}}", + "signature": { + "additionalParameters": "allowed", + "parameters": { + "param1": { + "required": true, + "defaultValue": { + "value": "test1", + "format": "plain" + } + }, + "param2": { + "required": true, + "defaultValue": { + "value": "test2", + "format": "plain" + } + }, + "sap-appvar-id": { + "required": true, + "filter": { + "value": "customer.app.variant1", + "format": "plain" + }, + "launcherValue": { + "value": "customer.app.variant1" + } + } + } + }, + "subTitle": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.subTitle}}" + } + } + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_removeAllInboundsExceptOne", + "content": { + "inboundId": "customer.app.variant1.InboundID" + }, + "texts": {} + } + ] +} +" +`; + +exports[`FLPConfigGenerator Integration Tests should generate FLP configuration successfully - Application Studio 2`] = ` +"#Make sure you provide a unique prefix to the newly added keys in this file, to avoid overriding of SAP Fiori application keys. + +#XTIT: Application name +customer.app.variant1_sap.app.title=App Variant Title" +`; + +exports[`FLPConfigGenerator Integration Tests should generate FLP configuration successfully - VS Code 1`] = ` +"{ + "fileName": "manifest", + "layer": "CUSTOMER_BASE", + "fileType": "appdescr_variant", + "reference": "mockReference", + "id": "customer.app.variant1", + "namespace": "apps/mockReference/appVariants/customer.app.variant1/", + "version": "0.1.0", + "content": [ + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "i18n" + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "@i18n" + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "mockModel" + }, + "texts": { + "i18n": "mockI18n" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "mockModel" + }, + "texts": { + "i18n": "mockI18n" + } + }, + { + "changeType": "appdescr_ui5_setMinUI5Version", + "content": { + "minUI5Version": "1.120.25" + } + }, + { + "changeType": "appdescr_app_setTitle", + "content": {}, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_addNewInbound", + "content": { + "inbound": { + "customer.app.variant1.InboundID": { + "action": "testAction", + "semanticObject": "testSemanticObject", + "title": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.title}}", + "signature": { + "additionalParameters": "allowed", + "parameters": { + "param1": { + "required": true, + "defaultValue": { + "value": "test1", + "format": "plain" + } + }, + "param2": { + "required": true, + "defaultValue": { + "value": "test2", + "format": "plain" + } + }, + "sap-appvar-id": { + "required": true, + "filter": { + "value": "customer.app.variant1", + "format": "plain" + }, + "launcherValue": { + "value": "customer.app.variant1" + } + } + } + }, + "subTitle": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.subTitle}}" + } + } + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_removeAllInboundsExceptOne", + "content": { + "inboundId": "customer.app.variant1.InboundID" + }, + "texts": {} + }, + { + "changeType": "appdescr_app_addNewInbound", + "content": { + "inbound": { + "customer.app.variant1.InboundID": { + "action": "testAction", + "semanticObject": "testSemanticObject", + "title": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.title}}", + "signature": { + "additionalParameters": "allowed", + "parameters": { + "param1": { + "required": true, + "defaultValue": { + "value": "test1", + "format": "plain" + } + }, + "param2": { + "required": true, + "defaultValue": { + "value": "test2", + "format": "plain" + } + }, + "sap-appvar-id": { + "required": true, + "filter": { + "value": "customer.app.variant1", + "format": "plain" + }, + "launcherValue": { + "value": "customer.app.variant1" + } + } + } + }, + "subTitle": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.subTitle}}" + } + } + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_removeAllInboundsExceptOne", + "content": { + "inboundId": "customer.app.variant1.InboundID" + }, + "texts": {} + } + ] +} +" +`; + +exports[`FLPConfigGenerator Integration Tests should generate FLP configuration successfully - VS Code 2`] = ` +"#Make sure you provide a unique prefix to the newly added keys in this file, to avoid overriding of SAP Fiori application keys. + +#XTIT: Application name +customer.app.variant1_sap.app.title=App Variant Title" +`; diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts new file mode 100644 index 0000000000..4b98fea047 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -0,0 +1,397 @@ +import type { BackendSystem } from '@sap-ux/store'; +import type * as axios from '@sap-ux/axios-extension'; +import type { FLPConfigAnswers } from '@sap-ux/flp-config-inquirer'; +import type { ToolsLogger } from '@sap-ux/logger'; +import { join } from 'path'; +import yeomanTest from 'yeoman-test'; +import fs from 'fs'; +import fsextra from 'fs-extra'; +import adpFlpConfigGenerator from '../src/app'; +import * as adpTooling from '@sap-ux/adp-tooling'; +import * as btpUtils from '@sap-ux/btp-utils'; +import * as Logger from '@sap-ux/logger'; +import * as odataInquirer from '@sap-ux/odata-service-inquirer'; +import * as fioriGenShared from '@sap-ux/fiori-generator-shared'; +import { rimraf } from 'rimraf'; +import { EventName } from '../src/telemetryEvents'; +import * as sysAccess from '@sap-ux/system-access'; +import { t } from '../src/utils/i18n'; +import { MessageType } from '@sap-devx/yeoman-ui-types'; + +jest.mock('@sap-ux/system-access'); +jest.mock('@sap-ux/btp-utils'); +jest.mock('@sap-ux/adp-tooling', () => ({ + ...jest.requireActual('@sap-ux/adp-tooling'), + isCFEnvironment: jest.fn(), + getAdpConfig: jest.fn(), + generateInboundConfig: jest.fn() +})); + +jest.mock('../src/utils/logger', () => ({ + logger: { + error: jest.fn() + } +})); + +jest.mock('@sap-ux/fiori-generator-shared', () => ({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + ...(jest.requireActual('@sap-ux/fiori-generator-shared') as {}), + sendTelemetry: jest.fn().mockReturnValue(new Promise(() => {})), + TelemetryHelper: { + initTelemetrySettings: jest.fn(), + createTelemetryData: jest.fn().mockReturnValue({ + OperatingSystem: 'testOS', + Platform: 'testPlatform' + }) + }, + isExtensionInstalled: jest.fn().mockReturnValue(true), + getHostEnvironment: jest.fn() +})); + +const sendTelemetrySpy = fioriGenShared.sendTelemetry as jest.Mock; + +const loggerMock: ToolsLogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn() +} as Partial as ToolsLogger; +jest.spyOn(Logger, 'ToolsLogger').mockImplementation(() => loggerMock); + +describe('FLPConfigGenerator Integration Tests', () => { + jest.setTimeout(100000); + jest.spyOn(adpTooling, 'isCFEnvironment').mockReturnValue(false); + + const generatorPath = join(__dirname, '../../src/app/index.ts'); + const testOutputDir = join(__dirname, 'test-output'); + const systemSelectionPrompts = { + prompts: [ + { + name: 'systemSelection' + }, + { + name: 'systemUsername' + }, + { + name: 'systemPassword' + } + ], + answers: { + connectedSystem: { + serviceProvider: {} as axios.ServiceProvider + } + } + }; + let answers: + | FLPConfigAnswers + | { + systemSelection: string; + systemUsername: string; + systemPassword: string; + }; + + jest.spyOn(adpTooling.ManifestService, 'initMergedManifest').mockResolvedValue({ + getManifest: jest.fn().mockReturnValue({}) + } as unknown as adpTooling.ManifestService); + const showInformationSpy = jest.fn(); + const mockAppWizard = { + setHeaderTitle: jest.fn(), + showInformation: showInformationSpy + }; + jest.spyOn(odataInquirer, 'getSystemSelectionQuestions').mockResolvedValue(systemSelectionPrompts); + + beforeEach(() => { + answers = { + systemSelection: 'testSystem', + systemUsername: 'testUsername', + systemPassword: 'testPassword', + semanticObject: 'testSemanticObject', + emptyInboundsInfo: 'testEmptyInboundsInfo', + action: 'testAction', + title: 'testTitle', + subTitle: 'testSubTitle', + additionalParameters: 'param1=test1¶m2=test2' + }; + }); + + beforeAll(() => { + fs.mkdirSync(testOutputDir, { recursive: true }); + }); + + afterAll(() => { + rimraf.sync(testOutputDir); + }); + + it('should generate FLP configuration successfully - Application Studio', async () => { + const testPath = join(testOutputDir, 'test_project1'); + fs.mkdirSync(testPath, { recursive: true }); + fsextra.copySync(join(__dirname, 'fixtures/app.variant1'), join(testPath, 'app.variant1')); + const testProjectPath = join(testPath, 'app.variant1'); + + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const sendTelemetrySpy = jest.spyOn(fioriGenShared, 'sendTelemetry'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + appWizard: mockAppWizard, + launchAsSubGen: false + }) + .withPrompts(answers); + + await expect(runContext.run()).resolves.not.toThrow(); + const variant = fs.readFileSync(join(testProjectPath, 'webapp', 'manifest.appdescr_variant'), { + encoding: 'utf8' + }); + const i18n = fs.readFileSync(join(testProjectPath, 'webapp', 'i18n', 'i18n.properties'), { + encoding: 'utf8' + }); + expect(variant).toMatchSnapshot(); + expect(i18n).toMatchSnapshot(); + expect(sendTelemetrySpy).toBeCalledWith( + EventName.ADP_FLP_CONFIG_ADDED, + expect.objectContaining({ + OperatingSystem: 'testOS', + Platform: 'testPlatform' + }), + testProjectPath + ); + expect(showInformationSpy).toHaveBeenCalledWith(t('info.flpConfigAdded'), MessageType.notification); + }); + + it('should generate FLP configuration successfully - VS Code', async () => { + showInformationSpy.mockReset(); + const testPath = join(testOutputDir, 'test_project2'); + fs.mkdirSync(testPath, { recursive: true }); + fsextra.copySync(join(__dirname, 'fixtures/app.variant1'), join(testPath, 'app.variant1')); + const testProjectPath = join(testPath, 'app.variant1'); + jest.spyOn(sysAccess, 'getCredentialsFromStore').mockResolvedValue({ + name: 'testSystem' + } as unknown as BackendSystem); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + url: 'https://testUrl', + client: '000' + } + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(false); + const sendTelemetrySpy = jest.spyOn(fioriGenShared, 'sendTelemetry').mockRejectedValueOnce(new Error('Error')); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + appWizard: mockAppWizard, + launchAsSubGen: true + }) + .withPrompts(answers); + + await expect(runContext.run()).resolves.not.toThrow(); + const variant = fs.readFileSync(join(testProjectPath, 'webapp', 'manifest.appdescr_variant'), { + encoding: 'utf8' + }); + const i18n = fs.readFileSync(join(testProjectPath, 'webapp', 'i18n', 'i18n.properties'), { + encoding: 'utf8' + }); + expect(variant).toMatchSnapshot(); + expect(i18n).toMatchSnapshot(); + expect(sendTelemetrySpy).toBeCalledWith( + EventName.ADP_FLP_CONFIG_ADDED, + expect.objectContaining({ + OperatingSystem: 'testOS', + Platform: 'testPlatform' + }), + testProjectPath + ); + expect(showInformationSpy).not.toBeCalled(); + }); + + it('Should result in an error message if the project is a CF project', async () => { + jest.spyOn(adpTooling, 'isCFEnvironment').mockReturnValueOnce(true); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await expect(runContext.run()).rejects.toThrow(t('error.cfNotSupported')); + }); + + it('Should throw an error if writing phase fails', async () => { + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + jest.spyOn(adpTooling, 'generateInboundConfig').mockRejectedValueOnce(new Error('Error')); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await expect(runContext.run()).rejects.toThrow(t('error.updatingApp')); + }); + + it('Should throw an error when no destination is configured in Application Studio', async () => { + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: {} as unknown as sysAccess.AbapTarget + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await expect(runContext.run()).rejects.toThrow(t('error.destinationNotFound')); + }); + + it('Should throw an error when no url is configured for target in VS Code', async () => { + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: {} as unknown as sysAccess.AbapTarget + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(false); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await expect(runContext.run()).rejects.toThrow(t('error.systemNotFound')); + }); + + it('Should throw an error when system is not found in the store in VS Code', async () => { + const systemUrl = 'https://testUrl'; + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + url: systemUrl + } + }); + jest.spyOn(sysAccess, 'getCredentialsFromStore').mockResolvedValueOnce(undefined); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(false); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await expect(runContext.run()).rejects.toThrow(t('error.systemNotFoundInStore', { url: systemUrl })); + }); + + it('Should throw an error when fetching manifest fails', async () => { + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + url: 'https://testUrl' + } + }); + jest.spyOn(odataInquirer, 'getSystemSelectionQuestions').mockRejectedValueOnce(new Error('Error')); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(false); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await expect(runContext.run()).rejects.toThrow(t('error.fetchingManifest')); + }); +}); diff --git a/packages/adp-flp-config-sub-generator/test/fixtures/app.variant1/webapp/i18n/i18n.properties b/packages/adp-flp-config-sub-generator/test/fixtures/app.variant1/webapp/i18n/i18n.properties new file mode 100644 index 0000000000..29b745952a --- /dev/null +++ b/packages/adp-flp-config-sub-generator/test/fixtures/app.variant1/webapp/i18n/i18n.properties @@ -0,0 +1,4 @@ +#Make sure you provide a unique prefix to the newly added keys in this file, to avoid overriding of SAP Fiori application keys. + +#XTIT: Application name +customer.app.variant1_sap.app.title=App Variant Title \ No newline at end of file diff --git a/packages/adp-flp-config-sub-generator/test/fixtures/app.variant1/webapp/manifest.appdescr_variant b/packages/adp-flp-config-sub-generator/test/fixtures/app.variant1/webapp/manifest.appdescr_variant new file mode 100644 index 0000000000..517e5b6ce1 --- /dev/null +++ b/packages/adp-flp-config-sub-generator/test/fixtures/app.variant1/webapp/manifest.appdescr_variant @@ -0,0 +1,164 @@ +{ + "fileName": "manifest", + "layer": "CUSTOMER_BASE", + "fileType": "appdescr_variant", + "reference": "mockReference", + "id": "customer.app.variant1", + "namespace": "apps/mockReference/appVariants/customer.app.variant1/", + "version": "0.1.0", + "content": [ + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "i18n" + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "@i18n" + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "mockModel" + }, + "texts": { + "i18n": "mockI18n" + } + }, + { + "changeType": "appdescr_ui5_addNewModelEnhanceWith", + "content": { + "modelId": "mockModel" + }, + "texts": { + "i18n": "mockI18n" + } + }, + { + "changeType": "appdescr_ui5_setMinUI5Version", + "content": { + "minUI5Version": "1.120.25" + } + }, + { + "changeType": "appdescr_app_setTitle", + "content": {}, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_addNewInbound", + "content": { + "inbound": { + "customer.app.variant1.InboundID": { + "action": "testAction", + "semanticObject": "testSemanticObject", + "title": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.title}}", + "signature": { + "additionalParameters": "allowed", + "parameters": { + "param1": { + "required": true, + "defaultValue": { + "value": "test1", + "format": "plain" + } + }, + "param2": { + "required": true, + "defaultValue": { + "value": "test2", + "format": "plain" + } + }, + "sap-appvar-id": { + "required": true, + "filter": { + "value": "customer.app.variant1", + "format": "plain" + }, + "launcherValue": { + "value": "customer.app.variant1" + } + } + } + }, + "subTitle": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.subTitle}}" + } + } + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_removeAllInboundsExceptOne", + "content": { + "inboundId": "customer.app.variant1.InboundID" + }, + "texts": {} + }, + { + "changeType": "appdescr_app_addNewInbound", + "content": { + "inbound": { + "customer.app.variant1.InboundID": { + "action": "testAction", + "semanticObject": "testSemanticObject", + "title": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.title}}", + "signature": { + "additionalParameters": "allowed", + "parameters": { + "param1": { + "required": true, + "defaultValue": { + "value": "test1", + "format": "plain" + } + }, + "param2": { + "required": true, + "defaultValue": { + "value": "test2", + "format": "plain" + } + }, + "sap-appvar-id": { + "required": true, + "filter": { + "value": "customer.app.variant1", + "format": "plain" + }, + "launcherValue": { + "value": "customer.app.variant1" + } + } + } + }, + "subTitle": "{{customer.app.variant1_sap.app.crossNavigation.inbounds.customer.app.variant1.InboundID.subTitle}}" + } + } + }, + "texts": { + "i18n": "i18n/i18n.properties" + } + }, + { + "changeType": "appdescr_app_removeAllInboundsExceptOne", + "content": { + "inboundId": "customer.app.variant1.InboundID" + }, + "texts": {} + } + ] +} diff --git a/packages/adp-flp-config-sub-generator/test/unit/app.test.ts b/packages/adp-flp-config-sub-generator/test/unit/app.test.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap b/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap deleted file mode 100644 index dc54e78aaa..0000000000 --- a/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap +++ /dev/null @@ -1,392 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Test reference generator should run the generator 1`] = ` -{ - "_version": "1.17.0", - "sap.app": { - "applicationVersion": { - "version": "1.0.0", - }, - "dataSources": { - "SEPMRA_PROD_MAN_ANNO_MDL": { - "settings": { - "localUri": "localService/SEPMRA_PROD_MAN_ANNO_MDL.xml", - }, - "type": "ODataAnnotation", - "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='SEPMRA_PROD_MAN_ANNO_MDL',Version='0001')/$value/", - }, - "annotation": { - "settings": { - "localUri": "annotations/annotation.xml", - }, - "type": "ODataAnnotation", - "uri": "annotations/annotation.xml", - }, - "mainService": { - "settings": { - "annotations": [ - "SEPMRA_PROD_MAN_ANNO_MDL", - "annotation", - ], - "localUri": "localService/metadata.xml", - }, - "type": "OData", - "uri": "/sap/opu/odata/sap/SEPMRA_PROD_MAN/", - }, - }, - "description": "{{appDescription}}", - "i18n": "i18n/i18n.properties", - "id": "testNameSpace.testprojectlropv2", - "offline": false, - "resources": "resources.json", - "sourceTemplate": { - "id": "ui5template.smartTemplate", - "toolsId": "4e5poddc-a666-4465-89eb-092e768dfecc", - "version": "1.40.12", - }, - "tags": { - "keywords": [], - }, - "title": "{{appTitle}}", - "type": "application", - }, - "sap.fiori": { - "archeType": "transactional", - "registrationIds": [], - }, - "sap.platform.abap": { - "uri": "", - }, - "sap.platform.hcp": { - "uri": "", - }, - "sap.ui": { - "deviceTypes": { - "desktop": true, - "phone": true, - "tablet": true, - }, - "icons": { - "favIcon": "", - "icon": "", - "phone": "", - "phone@2": "", - "tablet": "", - "tablet@2": "", - }, - "supportedThemes": [ - "sap_hcb", - "sap_belize", - "sap_fiori_3", - ], - "technology": "UI5", - }, - "sap.ui.generic.app": { - "_version": "1.3.0", - "pages": { - "ListReport|SEPMRA_C_PD_Product": { - "component": { - "list": true, - "name": "sap.suite.ui.generic.template.ListReport", - "settings": { - "condensedTableLayout": true, - "enableTableFilterInPageVariant": true, - "filterSettings": { - "dateSettings": { - "useDateRange": true, - }, - }, - "smartVariantManagement": true, - }, - }, - "entitySet": "SEPMRA_C_PD_Product", - "pages": { - "ObjectPage|SEPMRA_C_PD_Product": { - "component": { - "name": "sap.suite.ui.generic.template.ObjectPage", - }, - "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", - "entitySet": "SEPMRA_C_PD_Product", - "pages": { - "ObjectPage|to_ProductSalesData": { - "component": { - "name": "sap.suite.ui.generic.template.ObjectPage", - }, - "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", - "entitySet": "SEPMRA_C_PD_ProductSalesData", - "navigationProperty": "to_ProductSalesData", - }, - }, - }, - }, - }, - }, - "settings": { - "considerAnalyticalParameters": true, - "forceGlobalRefresh": false, - "objectPageHeaderType": "Dynamic", - "showDraftToggle": false, - }, - }, - "sap.ui5": { - "contentDensities": { - "compact": true, - "cozy": true, - }, - "dependencies": { - "components": {}, - "libs": { - "sap.suite.ui.generic.template": { - "lazy": false, - }, - "sap.ui.core": { - "lazy": false, - }, - "sap.ui.generic.app": { - "lazy": false, - }, - "se.mi.plm.attachmentservice": { - "lazy": false, - }, - }, - "minUI5Version": "1.65.0", - }, - "extends": { - "extensions": {}, - }, - "models": { - "": { - "dataSource": "mainService", - "preload": true, - "settings": { - "defaultBindingMode": "TwoWay", - "defaultCountMode": "Inline", - "metadataUrlParams": { - "sap-value-list": "none", - }, - "refreshAfterChange": false, - }, - }, - "@i18n": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/i18n.properties", - }, - "i18n": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/i18n.properties", - }, - "i18n|sap.suite.ui.generic.template.ListReport|SEPMRA_C_PD_Product": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/ListReport/SEPMRA_C_PD_Product/i18n.properties", - }, - "i18n|sap.suite.ui.generic.template.ObjectPage|SEPMRA_C_PD_Product": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/ObjectPage/SEPMRA_C_PD_Product/i18n.properties", - }, - "i18n|sap.suite.ui.generic.template.ObjectPage|SEPMRA_C_PD_ProductSalesData": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/ObjectPage/SEPMRA_C_PD_ProductSalesData/i18n.properties", - }, - }, - "resources": { - "css": [], - "js": [], - }, - }, -} -`; - -exports[`Test reference generator should run the generator custom webapp path 1`] = ` -{ - "_version": "1.17.0", - "sap.app": { - "applicationVersion": { - "version": "1.0.0", - }, - "dataSources": { - "SEPMRA_PROD_MAN_ANNO_MDL": { - "settings": { - "localUri": "localService/SEPMRA_PROD_MAN_ANNO_MDL.xml", - }, - "type": "ODataAnnotation", - "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='SEPMRA_PROD_MAN_ANNO_MDL',Version='0001')/$value/", - }, - "annotation": { - "settings": { - "localUri": "annotations/annotation.xml", - }, - "type": "ODataAnnotation", - "uri": "annotations/annotation.xml", - }, - "mainService": { - "settings": { - "annotations": [ - "SEPMRA_PROD_MAN_ANNO_MDL", - "annotation", - ], - "localUri": "localService/metadata.xml", - }, - "type": "OData", - "uri": "/sap/opu/odata/sap/SEPMRA_PROD_MAN/", - }, - }, - "description": "{{appDescription}}", - "i18n": "i18n/i18n.properties", - "id": "testNameSpace.testprojectlropv2", - "offline": false, - "resources": "resources.json", - "sourceTemplate": { - "id": "ui5template.smartTemplate", - "version": "1.40.12", - }, - "tags": { - "keywords": [], - }, - "title": "{{appTitle}}", - "type": "application", - }, - "sap.fiori": { - "archeType": "transactional", - "registrationIds": [], - }, - "sap.platform.abap": { - "uri": "", - }, - "sap.platform.hcp": { - "uri": "", - }, - "sap.ui": { - "deviceTypes": { - "desktop": true, - "phone": true, - "tablet": true, - }, - "icons": { - "favIcon": "", - "icon": "", - "phone": "", - "phone@2": "", - "tablet": "", - "tablet@2": "", - }, - "supportedThemes": [ - "sap_hcb", - "sap_belize", - "sap_fiori_3", - ], - "technology": "UI5", - }, - "sap.ui.generic.app": { - "_version": "1.3.0", - "pages": { - "ListReport|SEPMRA_C_PD_Product": { - "component": { - "list": true, - "name": "sap.suite.ui.generic.template.ListReport", - "settings": { - "condensedTableLayout": true, - "enableTableFilterInPageVariant": true, - "filterSettings": { - "dateSettings": { - "useDateRange": true, - }, - }, - "smartVariantManagement": true, - }, - }, - "entitySet": "SEPMRA_C_PD_Product", - "pages": { - "ObjectPage|SEPMRA_C_PD_Product": { - "component": { - "name": "sap.suite.ui.generic.template.ObjectPage", - }, - "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", - "entitySet": "SEPMRA_C_PD_Product", - "pages": { - "ObjectPage|to_ProductSalesData": { - "component": { - "name": "sap.suite.ui.generic.template.ObjectPage", - }, - "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", - "entitySet": "SEPMRA_C_PD_ProductSalesData", - "navigationProperty": "to_ProductSalesData", - }, - }, - }, - }, - }, - }, - "settings": { - "considerAnalyticalParameters": true, - "forceGlobalRefresh": false, - "objectPageHeaderType": "Dynamic", - "showDraftToggle": false, - }, - }, - "sap.ui5": { - "contentDensities": { - "compact": true, - "cozy": true, - }, - "dependencies": { - "components": {}, - "libs": { - "sap.suite.ui.generic.template": { - "lazy": false, - }, - "sap.ui.core": { - "lazy": false, - }, - "sap.ui.generic.app": { - "lazy": false, - }, - "se.mi.plm.attachmentservice": { - "lazy": false, - }, - }, - "minUI5Version": "1.65.0", - }, - "extends": { - "extensions": {}, - }, - "models": { - "": { - "dataSource": "mainService", - "preload": true, - "settings": { - "defaultBindingMode": "TwoWay", - "defaultCountMode": "Inline", - "metadataUrlParams": { - "sap-value-list": "none", - }, - "refreshAfterChange": false, - }, - }, - "@i18n": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/i18n.properties", - }, - "i18n": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/i18n.properties", - }, - "i18n|sap.suite.ui.generic.template.ListReport|SEPMRA_C_PD_Product": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/ListReport/SEPMRA_C_PD_Product/i18n.properties", - }, - "i18n|sap.suite.ui.generic.template.ObjectPage|SEPMRA_C_PD_Product": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/ObjectPage/SEPMRA_C_PD_Product/i18n.properties", - }, - "i18n|sap.suite.ui.generic.template.ObjectPage|SEPMRA_C_PD_ProductSalesData": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/ObjectPage/SEPMRA_C_PD_ProductSalesData/i18n.properties", - }, - }, - "resources": { - "css": [], - "js": [], - }, - }, -} -`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97b7d49122..f3b2e64121 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -550,6 +550,9 @@ importers: fs-extra: specifier: 10.0.0 version: 10.0.0 + memfs: + specifier: 3.4.13 + version: 3.4.13 rimraf: specifier: 5.0.5 version: 5.0.5 From 59987cb43e1d3d281cf2dd198a1b802ed51d03c4 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Thu, 9 Jan 2025 18:29:03 +0200 Subject: [PATCH 20/56] chore: update pnpm-lock --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3b2e64121..97b7d49122 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -550,9 +550,6 @@ importers: fs-extra: specifier: 10.0.0 version: 10.0.0 - memfs: - specifier: 3.4.13 - version: 3.4.13 rimraf: specifier: 5.0.5 version: 5.0.5 From c40b1595fc63d4e89df365105c36ba93109ec5a0 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Thu, 9 Jan 2025 20:09:16 +0200 Subject: [PATCH 21/56] fix: restore snapshot --- .../test/unit/__snapshots__/app.test.ts.snap | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap diff --git a/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap b/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap new file mode 100644 index 0000000000..44995d5246 --- /dev/null +++ b/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap @@ -0,0 +1,392 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test reference generator should run the generator 1`] = ` +{ + "_version": "1.17.0", + "sap.app": { + "applicationVersion": { + "version": "1.0.0", + }, + "dataSources": { + "SEPMRA_PROD_MAN_ANNO_MDL": { + "settings": { + "localUri": "localService/SEPMRA_PROD_MAN_ANNO_MDL.xml", + }, + "type": "ODataAnnotation", + "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='SEPMRA_PROD_MAN_ANNO_MDL',Version='0001')/$value/", + }, + "annotation": { + "settings": { + "localUri": "annotations/annotation.xml", + }, + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + }, + "mainService": { + "settings": { + "annotations": [ + "SEPMRA_PROD_MAN_ANNO_MDL", + "annotation", + ], + "localUri": "localService/metadata.xml", + }, + "type": "OData", + "uri": "/sap/opu/odata/sap/SEPMRA_PROD_MAN/", + }, + }, + "description": "{{appDescription}}", + "i18n": "i18n/i18n.properties", + "id": "testNameSpace.testprojectlropv2", + "offline": false, + "resources": "resources.json", + "sourceTemplate": { + "id": "ui5template.smartTemplate", + "toolsId": "4e5poddc-a666-4465-89eb-092e768dfecc", + "version": "1.40.12", + }, + "tags": { + "keywords": [], + }, + "title": "{{appTitle}}", + "type": "application", + }, + "sap.fiori": { + "archeType": "transactional", + "registrationIds": [], + }, + "sap.platform.abap": { + "uri": "", + }, + "sap.platform.hcp": { + "uri": "", + }, + "sap.ui": { + "deviceTypes": { + "desktop": true, + "phone": true, + "tablet": true, + }, + "icons": { + "favIcon": "", + "icon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "", + }, + "supportedThemes": [ + "sap_hcb", + "sap_belize", + "sap_fiori_3", + ], + "technology": "UI5", + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "pages": { + "ListReport|SEPMRA_C_PD_Product": { + "component": { + "list": true, + "name": "sap.suite.ui.generic.template.ListReport", + "settings": { + "condensedTableLayout": true, + "enableTableFilterInPageVariant": true, + "filterSettings": { + "dateSettings": { + "useDateRange": true, + }, + }, + "smartVariantManagement": true, + }, + }, + "entitySet": "SEPMRA_C_PD_Product", + "pages": { + "ObjectPage|SEPMRA_C_PD_Product": { + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage", + }, + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "entitySet": "SEPMRA_C_PD_Product", + "pages": { + "ObjectPage|to_ProductSalesData": { + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage", + }, + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "entitySet": "SEPMRA_C_PD_ProductSalesData", + "navigationProperty": "to_ProductSalesData", + }, + }, + }, + }, + }, + }, + "settings": { + "considerAnalyticalParameters": true, + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "showDraftToggle": false, + }, + }, + "sap.ui5": { + "contentDensities": { + "compact": true, + "cozy": true, + }, + "dependencies": { + "components": {}, + "libs": { + "sap.suite.ui.generic.template": { + "lazy": false, + }, + "sap.ui.core": { + "lazy": false, + }, + "sap.ui.generic.app": { + "lazy": false, + }, + "se.mi.plm.attachmentservice": { + "lazy": false, + }, + }, + "minUI5Version": "1.65.0", + }, + "extends": { + "extensions": {}, + }, + "models": { + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "defaultCountMode": "Inline", + "metadataUrlParams": { + "sap-value-list": "none", + }, + "refreshAfterChange": false, + }, + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties", + }, + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties", + }, + "i18n|sap.suite.ui.generic.template.ListReport|SEPMRA_C_PD_Product": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ListReport/SEPMRA_C_PD_Product/i18n.properties", + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|SEPMRA_C_PD_Product": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/SEPMRA_C_PD_Product/i18n.properties", + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|SEPMRA_C_PD_ProductSalesData": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/SEPMRA_C_PD_ProductSalesData/i18n.properties", + }, + }, + "resources": { + "css": [], + "js": [], + }, + }, +} +`; + +exports[`Test reference generator should run the generator custom webapp path 1`] = ` +{ + "_version": "1.17.0", + "sap.app": { + "applicationVersion": { + "version": "1.0.0", + }, + "dataSources": { + "SEPMRA_PROD_MAN_ANNO_MDL": { + "settings": { + "localUri": "localService/SEPMRA_PROD_MAN_ANNO_MDL.xml", + }, + "type": "ODataAnnotation", + "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='SEPMRA_PROD_MAN_ANNO_MDL',Version='0001')/$value/", + }, + "annotation": { + "settings": { + "localUri": "annotations/annotation.xml", + }, + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + }, + "mainService": { + "settings": { + "annotations": [ + "SEPMRA_PROD_MAN_ANNO_MDL", + "annotation", + ], + "localUri": "localService/metadata.xml", + }, + "type": "OData", + "uri": "/sap/opu/odata/sap/SEPMRA_PROD_MAN/", + }, + }, + "description": "{{appDescription}}", + "i18n": "i18n/i18n.properties", + "id": "testNameSpace.testprojectlropv2", + "offline": false, + "resources": "resources.json", + "sourceTemplate": { + "id": "ui5template.smartTemplate", + "version": "1.40.12", + }, + "tags": { + "keywords": [], + }, + "title": "{{appTitle}}", + "type": "application", + }, + "sap.fiori": { + "archeType": "transactional", + "registrationIds": [], + }, + "sap.platform.abap": { + "uri": "", + }, + "sap.platform.hcp": { + "uri": "", + }, + "sap.ui": { + "deviceTypes": { + "desktop": true, + "phone": true, + "tablet": true, + }, + "icons": { + "favIcon": "", + "icon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "", + }, + "supportedThemes": [ + "sap_hcb", + "sap_belize", + "sap_fiori_3", + ], + "technology": "UI5", + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "pages": { + "ListReport|SEPMRA_C_PD_Product": { + "component": { + "list": true, + "name": "sap.suite.ui.generic.template.ListReport", + "settings": { + "condensedTableLayout": true, + "enableTableFilterInPageVariant": true, + "filterSettings": { + "dateSettings": { + "useDateRange": true, + }, + }, + "smartVariantManagement": true, + }, + }, + "entitySet": "SEPMRA_C_PD_Product", + "pages": { + "ObjectPage|SEPMRA_C_PD_Product": { + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage", + }, + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "entitySet": "SEPMRA_C_PD_Product", + "pages": { + "ObjectPage|to_ProductSalesData": { + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage", + }, + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "entitySet": "SEPMRA_C_PD_ProductSalesData", + "navigationProperty": "to_ProductSalesData", + }, + }, + }, + }, + }, + }, + "settings": { + "considerAnalyticalParameters": true, + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "showDraftToggle": false, + }, + }, + "sap.ui5": { + "contentDensities": { + "compact": true, + "cozy": true, + }, + "dependencies": { + "components": {}, + "libs": { + "sap.suite.ui.generic.template": { + "lazy": false, + }, + "sap.ui.core": { + "lazy": false, + }, + "sap.ui.generic.app": { + "lazy": false, + }, + "se.mi.plm.attachmentservice": { + "lazy": false, + }, + }, + "minUI5Version": "1.65.0", + }, + "extends": { + "extensions": {}, + }, + "models": { + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "defaultCountMode": "Inline", + "metadataUrlParams": { + "sap-value-list": "none", + }, + "refreshAfterChange": false, + }, + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties", + }, + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties", + }, + "i18n|sap.suite.ui.generic.template.ListReport|SEPMRA_C_PD_Product": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ListReport/SEPMRA_C_PD_Product/i18n.properties", + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|SEPMRA_C_PD_Product": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/SEPMRA_C_PD_Product/i18n.properties", + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|SEPMRA_C_PD_ProductSalesData": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/SEPMRA_C_PD_ProductSalesData/i18n.properties", + }, + }, + "resources": { + "css": [], + "js": [], + }, + }, +} +`; \ No newline at end of file From c05ecf28a54aa3b2e990183438166c3ab0fbe58b Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Thu, 9 Jan 2025 21:22:27 +0200 Subject: [PATCH 22/56] fix: logging --- .../src/app/index.ts | 32 +++++++++++++++---- .../src/app/types.ts | 24 ++++++++++++++ .../adp-flp-config-sub-generator.i18n.json | 5 ++- .../test/app.test.ts | 6 ---- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index f3faf0547e..9e752e9dd8 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -57,12 +57,12 @@ export default class extends Generator { super(args, opts); this.appWizard = opts.appWizard ?? AppWizard.create(opts); this.launchAsSubGen = !!opts.launchAsSubGen; - this.manifest = opts.manifest as Manifest; + this.manifest = opts.manifest; this.toolsLogger = new ToolsLogger(); - this.logger = AdpFlpConfigLogger.logger; - this.projectRootPath = opts.data?.projectRootPath ?? this.destinationRoot(); + this._configureLogging(); + // If launched standalone add navigation steps if (!this.launchAsSubGen) { this._setupPrompts(); @@ -103,7 +103,7 @@ export default class extends Generator { try { await generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation, this.fs); } catch (error) { - this.logger.error(t('error.writingPhase', { error })); + this.logger.error(`Writing phase failed: ${error}`); throw new Error(t('error.updatingApp')); } } @@ -140,7 +140,7 @@ export default class extends Generator { throw new Error(t('error.systemNotFound')); } - const configuredSystem = (await getCredentialsFromStore(target as UrlAbapTarget, this.toolsLogger))?.name; + configuredSystem = (await getCredentialsFromStore(target as UrlAbapTarget, this.toolsLogger))?.name; if (!configuredSystem) { throw new Error(t('error.systemNotFoundInStore', { url })); } @@ -159,7 +159,11 @@ export default class extends Generator { const configuredSystem = await this._findConfiguredSystem(target); try { const systemSelectionQuestions = await getSystemSelectionQuestions({ - systemSelection: { onlyShowDefaultChoice: true, defaultChoice: configuredSystem }, + systemSelection: { + onlyShowDefaultChoice: true, + defaultChoice: configuredSystem, + showConnectionSuccessMessage: true + }, serviceSelection: { hide: true } }); await this.prompt(systemSelectionQuestions.prompts); @@ -172,7 +176,7 @@ export default class extends Generator { ); this.manifest = manifestService.getManifest(); } catch (error) { - this.logger.error(t('error.fetchingManifest', { error })); + this.logger.error(`Manifest fetching failed: ${error}`); throw new Error(t('error.fetchingManifest')); } } @@ -197,6 +201,20 @@ export default class extends Generator { } }; } + + /** + * Configures logging for the generator. + */ + private _configureLogging(): void { + AdpFlpConfigLogger.configureLogging( + this.options.logger, + this.rootGeneratorName(), + this.log, + this.options.vscode, + this.options.logLevel + ); + this.logger = AdpFlpConfigLogger.logger; + } } export type { FlpConfigOptions }; diff --git a/packages/adp-flp-config-sub-generator/src/app/types.ts b/packages/adp-flp-config-sub-generator/src/app/types.ts index 2421fe1e01..5933afa8cd 100644 --- a/packages/adp-flp-config-sub-generator/src/app/types.ts +++ b/packages/adp-flp-config-sub-generator/src/app/types.ts @@ -1,12 +1,36 @@ import type { AppWizard } from '@sap-devx/yeoman-ui-types'; import type { Manifest } from '@sap-ux/project-access'; import type Generator from 'yeoman-generator'; +import type { TelemetryData } from '@sap-ux/fiori-generator-shared'; export interface FlpConfigOptions extends Generator.GeneratorOptions { + /** + * VSCode instance + */ + vscode?: unknown; + /** + * Option to force the conflicter property of the yeoman environment (prevents additional prompt for overwriting files) + */ force?: boolean; + /** + * AppWizard instance + */ appWizard?: AppWizard; + /** + * Whether the generator is launched as a subgenerator + */ launchAsSubGen?: boolean; + /** + * The manifest of the base application + */ manifest: Manifest; + /** + * Telemetry data to be send after deployment configuration has been added + */ + telemetryData?: TelemetryData; + /** + * Additional data for the generator + */ data?: { projectRootPath: string; }; diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index c39dc8752b..0424f8285f 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -10,12 +10,11 @@ }, "error": { "cfNotSupported": "FLP Configuration is not supported for CF projects", - "fetchingManifest": "Error fetching merged manifest for app: {{- error}}", + "fetchingManifest": "Error fetching merged manifest for base application", "destinationNotFound": "Missing destination configuration in ui5.yaml", "systemNotFound": "Missing system configuration in ui5.yaml", "systemNotFoundInStore" : "System not found in the system store: {{- systemUrl}}", - "endPhase": "Error in end phase of the adaptation project FLP configuration: {{- error}}", - "writingPhase": "Error in writing phase of the adaptation project FLP configuration: {{- error}}", + "writingPhase": "Error in writing phase of the adaptation project FLP configuration", "telemetry": "Error sending telemetry data: {{- error}}", "updatingApp": "Error updating app with FLP configuration. Inspect the logs for full error." } diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index 4b98fea047..e15629236e 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -27,12 +27,6 @@ jest.mock('@sap-ux/adp-tooling', () => ({ generateInboundConfig: jest.fn() })); -jest.mock('../src/utils/logger', () => ({ - logger: { - error: jest.fn() - } -})); - jest.mock('@sap-ux/fiori-generator-shared', () => ({ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion ...(jest.requireActual('@sap-ux/fiori-generator-shared') as {}), From 7805c6de21a4a53eecb5da6121f524987748a90d Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 10 Jan 2025 18:19:53 +0200 Subject: [PATCH 23/56] fix: logging --- .../adp-flp-config-sub-generator/src/app/index.ts | 11 +++++++++-- .../adp-flp-config-sub-generator/src/utils/logger.ts | 8 +++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 9e752e9dd8..81519a6a02 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -44,6 +44,7 @@ export default class extends Generator { answers: FLPConfigAnswers; toolsLogger: ToolsLogger; logger: ILogWrapper; + public options: FlpConfigOptions; /** * Creates an instance of the generator. @@ -60,6 +61,7 @@ export default class extends Generator { this.manifest = opts.manifest; this.toolsLogger = new ToolsLogger(); this.projectRootPath = opts.data?.projectRootPath ?? this.destinationRoot(); + this.options = opts; this._configureLogging(); @@ -112,7 +114,11 @@ export default class extends Generator { if (!this.launchAsSubGen) { this.appWizard?.showInformation(t('info.flpConfigAdded'), MessageType.notification); } - const telemetryData = TelemetryHelper.createTelemetryData() ?? {}; + const telemetryData = + TelemetryHelper.createTelemetryData({ + appType: 'adp-flp-config', + ...this.options.telemetryData + }) ?? {}; if (telemetryData) { sendTelemetry(EventName.ADP_FLP_CONFIG_ADDED, telemetryData, this.projectRootPath).catch((error) => { this.logger.error(t('error.telemetry', { error })); @@ -211,7 +217,8 @@ export default class extends Generator { this.rootGeneratorName(), this.log, this.options.vscode, - this.options.logLevel + this.options.logLevel, + this.options.logWrapper ); this.logger = AdpFlpConfigLogger.logger; } diff --git a/packages/adp-flp-config-sub-generator/src/utils/logger.ts b/packages/adp-flp-config-sub-generator/src/utils/logger.ts index 2544cd0edc..d2f075541b 100644 --- a/packages/adp-flp-config-sub-generator/src/utils/logger.ts +++ b/packages/adp-flp-config-sub-generator/src/utils/logger.ts @@ -34,15 +34,17 @@ export default class AdpFlpConfigLogger { * @param yoLogger - the yeoman logger * @param vscode - the vscode instance * @param logLevel - the log level + * @param logWrapper - log wrapper instance */ static configureLogging( vscLogger: IVSCodeExtLogger, loggerName: string, yoLogger: Logger, vscode?: unknown, - logLevel?: LogLevel + logLevel?: LogLevel, + logWrapper?: LogWrapper ): void { - const logWrapper = new LogWrapper(loggerName, yoLogger, logLevel, vscLogger, vscode); - AdpFlpConfigLogger.logger = logWrapper; + const logger = logWrapper ?? new LogWrapper(loggerName, yoLogger, logLevel, vscLogger, vscode); + AdpFlpConfigLogger.logger = logger; } } From f9c51ea75d09e3aa7dffc26804bb361deb91eec3 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Tue, 28 Jan 2025 16:43:46 +0200 Subject: [PATCH 24/56] refacor: Use @sap-ux/system-access module for system connection/validation --- .../adp-flp-config-sub-generator/package.json | 14 +- .../src/app/index.ts | 230 +++++++++++++----- .../src/app/questions.ts | 56 +++++ .../adp-flp-config-sub-generator.i18n.json | 20 +- .../tsconfig.json | 4 +- .../sap-system/system-selection/questions.ts | 10 - .../odata-service-inquirer.i18n.json | 3 +- packages/odata-service-inquirer/src/types.ts | 5 - .../system-selection/questions.test.ts | 37 --- pnpm-lock.yaml | 43 +--- 10 files changed, 256 insertions(+), 166 deletions(-) create mode 100644 packages/adp-flp-config-sub-generator/src/app/questions.ts diff --git a/packages/adp-flp-config-sub-generator/package.json b/packages/adp-flp-config-sub-generator/package.json index f381365b1d..85272a124a 100644 --- a/packages/adp-flp-config-sub-generator/package.json +++ b/packages/adp-flp-config-sub-generator/package.json @@ -32,28 +32,28 @@ "dependencies": { "@sap-devx/yeoman-ui-types": "1.14.4", "@sap-ux/adp-tooling": "workspace:*", + "@sap-ux/axios-extension": "workspace:*", "@sap-ux/btp-utils": "workspace:*", + "@sap-ux/feature-toggle": "workspace:*", + "@sap-ux/fiori-generator-shared": "workspace:*", "@sap-ux/flp-config-inquirer": "workspace:*", - "@sap-ux/odata-service-inquirer": "workspace:*", + "@sap-ux/inquirer-common": "workspace:*", + "@sap-ux/logger": "workspace:*", "@sap-ux/project-access": "workspace:*", - "@sap-ux/axios-extension": "workspace:*", - "@sap-ux/system-access": "workspace:*", "@sap-ux/store": "workspace:*", - "@sap-ux/logger": "workspace:*", - "@sap-ux/fiori-generator-shared": "workspace:*", - "@sap-ux/feature-toggle": "workspace:*", + "@sap-ux/system-access": "workspace:*", "i18next": "23.5.1", "yeoman-generator": "5.10.0" }, "devDependencies": { "@jest/types": "29.6.3", "@types/fs-extra": "9.0.13", + "@types/inquirer": "8.2.6", "@types/vscode": "1.73.1", "@types/yeoman-environment": "2.10.11", "@types/yeoman-generator": "5.2.11", "@types/yeoman-test": "4.0.6", "@vscode-logging/logger": "2.0.0", - "@types/inquirer": "8.2.6", "fs-extra": "10.0.0", "rimraf": "5.0.5", "typescript": "5.3.3", diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 81519a6a02..c232dde22a 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -1,11 +1,14 @@ import type { Manifest } from '@sap-ux/project-access'; import type { FlpConfigOptions } from './types'; import type { Question } from 'inquirer'; -import type { AbapServiceProvider } from '@sap-ux/axios-extension'; -import type { AbapTarget, UrlAbapTarget } from '@sap-ux/system-access'; -import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; import Generator from 'yeoman-generator'; import path, { join } from 'path'; +import { + type AxiosError, + type AxiosRequestConfig, + type ProviderConfiguration, + isAxiosError +} from '@sap-ux/axios-extension'; import { ManifestService, getVariant, @@ -23,10 +26,17 @@ import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; import { TelemetryHelper, sendTelemetry, type ILogWrapper } from '@sap-ux/fiori-generator-shared'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; -import { isAppStudio } from '@sap-ux/btp-utils'; -import { getCredentialsFromStore } from '@sap-ux/system-access'; import AdpFlpConfigLogger from '../utils/logger'; -import { t } from '../utils/i18n'; +import { t, initI18n } from '../utils/i18n'; +import { type Credentials, type ValidationResults, getCredentialsPrompts } from './questions'; +import { ErrorHandler } from '@sap-ux/inquirer-common'; +import { + createAbapServiceProvider, + type AbapTarget, + type UrlAbapTarget, + getCredentialsFromStore +} from '@sap-ux/system-access'; +import { isAppStudio, listDestinations } from '@sap-ux/btp-utils'; /** * Generator for adding a FLP configuration to an Adaptation Project. @@ -35,16 +45,21 @@ import { t } from '../utils/i18n'; */ export default class extends Generator { setPromptsCallback: (fn: object) => void; - prompts: Prompts; + private prompts: Prompts; // Flag to determine if the generator was launched as a sub-generator or standalone - launchAsSubGen: boolean; - appWizard: AppWizard; - manifest: Manifest; - projectRootPath: string = ''; - answers: FLPConfigAnswers; - toolsLogger: ToolsLogger; - logger: ILogWrapper; + private launchAsSubGen: boolean; + private appWizard: AppWizard; + private manifest: Manifest; + private projectRootPath: string = ''; + private answers: FLPConfigAnswers; + private toolsLogger: ToolsLogger; + private logger: ILogWrapper; public options: FlpConfigOptions; + private vscode: any; + private authenticationRequired: boolean = false; + // Flag to determine if the generator was aborted + private abort: boolean = false; + private configuredSystem: string | undefined; /** * Creates an instance of the generator. @@ -62,6 +77,7 @@ export default class extends Generator { this.toolsLogger = new ToolsLogger(); this.projectRootPath = opts.data?.projectRootPath ?? this.destinationRoot(); this.options = opts; + this.vscode = opts.vscode; this._configureLogging(); @@ -76,6 +92,11 @@ export default class extends Generator { if (isCFEnvironment(this.projectRootPath)) { throw new Error(t('error.cfNotSupported')); } + + await initI18n(); + if (!this.manifest) { + await this._fetchManifest(); + } // Add telemetry to be sent once adp-flp-config is generated await TelemetryHelper.initTelemetrySettings({ consumerModule: { @@ -88,8 +109,11 @@ export default class extends Generator { } public async prompting(): Promise { - if (!this.manifest) { - await this._fetchManifest(); + if (this.abort) { + return; + } + if (this.authenticationRequired) { + await this._promptAuthentication(); } const inbounds = getInboundsFromManifest(this.manifest); const appId = getRegistrationIdFromManifest(this.manifest); @@ -102,6 +126,9 @@ export default class extends Generator { } async writing(): Promise { + if (this.abort) { + return; + } try { await generateInboundConfig(this.projectRootPath, this.answers as InternalInboundNavigation, this.fs); } catch (error) { @@ -111,6 +138,9 @@ export default class extends Generator { } end(): void { + if (this.abort) { + return; + } if (!this.launchAsSubGen) { this.appWizard?.showInformation(t('info.flpConfigAdded'), MessageType.notification); } @@ -126,65 +156,94 @@ export default class extends Generator { } } - /** - * Finds the configured system based on the provided target in ui5.yaml configuration. - * - * @param {AbapTarget} target - The target ABAP system. - * @returns {Promise} The configured system. - */ - private async _findConfiguredSystem(target: AbapTarget): Promise { - let configuredSystem: string | undefined; - - if (isAppStudio()) { - configuredSystem = target.destination; - if (!configuredSystem) { - throw new Error(t('error.destinationNotFound')); - } - } else { - const { url } = target; - if (!url) { - throw new Error(t('error.systemNotFound')); - } - - configuredSystem = (await getCredentialsFromStore(target as UrlAbapTarget, this.toolsLogger))?.name; - if (!configuredSystem) { - throw new Error(t('error.systemNotFoundInStore', { url })); - } - } - - return configuredSystem; - } - /** * Fetches the manifest for the project. * + * @param {Credentials} credentials - The request options. + * @param {ValidationResults} validationResult - The validation results object used in password prompt validation. * @returns {Promise} A promise that resolves when the manifest has been fetched. */ - private async _fetchManifest(): Promise { - const { target } = await getAdpConfig(this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml)); - const configuredSystem = await this._findConfiguredSystem(target); + private async _fetchManifest(credentials?: Credentials, validationResult?: ValidationResults): Promise { + const { target, ignoreCertErrors = false } = await getAdpConfig( + this.projectRootPath, + join(this.projectRootPath, FileName.Ui5Yaml) + ); + this.configuredSystem = await this._findConfiguredSystem(target); + if (!this.configuredSystem) { + return; + } try { - const systemSelectionQuestions = await getSystemSelectionQuestions({ - systemSelection: { - onlyShowDefaultChoice: true, - defaultChoice: configuredSystem, - showConnectionSuccessMessage: true - }, - serviceSelection: { hide: true } - }); - await this.prompt(systemSelectionQuestions.prompts); + const requiestOptions: AxiosRequestConfig & Partial = { ignoreCertErrors }; + if (credentials) { + requiestOptions['auth'] = { username: credentials.username, password: credentials.password }; + } + const provider = await createAbapServiceProvider(target, requiestOptions, false, this.toolsLogger); const variant = getVariant(this.projectRootPath); const manifestService = await ManifestService.initMergedManifest( - systemSelectionQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, + provider, this.projectRootPath, variant, this.toolsLogger ); this.manifest = manifestService.getManifest(); + if (validationResult) { + validationResult.valid = true; + } } catch (error) { - this.logger.error(`Manifest fetching failed: ${error}`); - throw new Error(t('error.fetchingManifest')); + await this._handleFetchingError(error, validationResult); + } + } + + /** + * Prompts the user for authentication credentials. + * + * @returns {void} + */ + private async _promptAuthentication(): Promise { + const prompts = getCredentialsPrompts(this._fetchManifest.bind(this)); + this.prompts.splice(0, 0, [ + { + name: t('yuiNavSteps.flpCredentialsName'), + description: t('yuiNavSteps.flpCredentialsDesc', { system: this.configuredSystem }) + } + ]); + await this.prompt(prompts); + } + + /** + * Handles errors that occur during the fetching of the manifest. + * + * @param {Error | AxiosError} error - The error that occurred. + * @param {ValidationResults} [validationResult] - The validation results object used in password prompt validation. + * @returns {Promise} A promise that resolves when the error has been handled. + */ + private async _handleFetchingError(error: Error | AxiosError, validationResult?: ValidationResults): Promise { + if (isAxiosError(error)) { + this.logger.error( + `Manifest fetching failed: ${error}. Status: ${error.response?.status}. URI: ${error.request?.path}` + ); + if (error.response?.status === 401) { + if (validationResult) { + validationResult.valid = false; + validationResult.message = t('error.authenticationFailed'); + } + this.authenticationRequired = true; + return; + } + + const errorHandler = new ErrorHandler(); + const errorHelp = errorHandler.getValidationErrorHelp(error); + if (errorHelp) { + this._showErrorNotification( + typeof errorHelp === 'string' + ? errorHelp + : `${errorHelp?.message} ([${errorHelp.link.text}](${errorHelp.link.url}))` + ); + return; + } } + this.logger.error(`Manifest fetching failed: ${error}`); + throw new Error(t('error.fetchingManifest')); } /** @@ -192,10 +251,6 @@ export default class extends Generator { */ private _setupPrompts(): void { this.prompts = new Prompts([ - { - name: t('yuiNavSteps.sysConfirmName'), - description: t('yuiNavSteps.sysConfirmDesc') - }, { name: t('yuiNavSteps.flpConfigName'), description: t('yuiNavSteps.flpConfigDesc', { projectName: path.basename(this.projectRootPath) }) @@ -208,6 +263,53 @@ export default class extends Generator { }; } + /** + * Finds the configured system based on the provided target in ui5.yaml configuration. + * + * @param {AbapTarget} target - The target ABAP system. + * @returns {Promise} The configured system. + */ + private async _findConfiguredSystem(target: AbapTarget): Promise { + let configuredSystem: string | undefined; + if (isAppStudio()) { + configuredSystem = target.destination; + if (!configuredSystem) { + this._showErrorNotification(t('error.destinationNotFound')); + return; + } + + const destinations = await listDestinations(); + if (!(configuredSystem in destinations)) { + this._showErrorNotification(t('error.destinationNotFoundInStore', { destination: configuredSystem })); + return; + } + } else { + const { url } = target; + if (!url) { + this._showErrorNotification(t('error.systemUrlNotFound')); + return; + } + + configuredSystem = (await getCredentialsFromStore(target as UrlAbapTarget, this.toolsLogger))?.name; + if (!configuredSystem) { + this._showErrorNotification(t('error.systemNotFoundInStore', { systemUrl: url })); + return; + } + } + + return configuredSystem; + } + + /** + * Shows an error notification with the provided message and aborts the generator execution. + * + * @param {string} message - The error message to display. + */ + private _showErrorNotification(message: string): void { + this.vscode.window.showErrorMessage(message); + this.abort = true; + } + /** * Configures logging for the generator. */ diff --git a/packages/adp-flp-config-sub-generator/src/app/questions.ts b/packages/adp-flp-config-sub-generator/src/app/questions.ts new file mode 100644 index 0000000000..46fb564a1e --- /dev/null +++ b/packages/adp-flp-config-sub-generator/src/app/questions.ts @@ -0,0 +1,56 @@ +import type Generator from 'yeoman-generator'; +import { t } from '../utils/i18n'; + +export type Credentials = { username: string; password: string }; +export type ValidationResults = { valid: boolean; message?: string }; + +/** + * Prompts the user for credentials. + * + * @param {Function} callback - Optional callback function called in the validation phase of password prompt. + * Object containing the username and password is passed to the callback. Also, the validation results object is passed that can be used to set the validation status in the callback. + * @returns {Array} The prompts for username and password. + */ +export function getCredentialsPrompts( + callback?: (credentials: Credentials, validationResults: ValidationResults) => Promise +): Array { + return [ + { + type: 'input', + name: t('prompts.username.name'), + message: t('prompts.username.message'), + guiOptions: { + mandatory: true + }, + store: false, + validate: (value: string): string | boolean => { + return value ? true : t('error.cannotBeEmpty', { field: t('prompts.username.message') }); + } + }, + { + type: 'password', + guiType: 'login', + name: t('prompts.password.name'), + message: t('prompts.password.message'), + guiOptions: { + mandatory: true + }, + store: false, + validate: async (value: string, answers: Generator.Answers): Promise => { + const validationResults: ValidationResults = { valid: true }; + if (!value) { + return t('error.cannotBeEmpty', { field: t('prompts.password.message') }); + } + + if (!answers.username) { + return t('error.cannotBeEmpty', { field: t('prompts.username.message') }); + } + if (callback) { + await callback({ username: answers.username, password: value }, validationResults); + return validationResults.valid ? true : validationResults.message; + } + return true; + } + } + ]; +} diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index 0424f8285f..06f13625ff 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -1,6 +1,19 @@ { + "prompts": { + "username": { + "type": "input", + "name": "username", + "message": "Username" + }, + "password": { + "type": "password", + "name": "password", + "message": "Password" + } + }, "yuiNavSteps": { - "sysConfirmName": "System connection validation", + "flpCredentialsName": "FLP Configuration - Credentials", + "flpCredentialsDesc": "Enter credentials for your adaptation project's system ({{- system}})", "sysConfirmDesc": "Validates connection to configured system", "flpConfigName": "FLP Configuration", "flpConfigDesc": "FLP Configuration for {{- projectName}}" @@ -12,10 +25,13 @@ "cfNotSupported": "FLP Configuration is not supported for CF projects", "fetchingManifest": "Error fetching merged manifest for base application", "destinationNotFound": "Missing destination configuration in ui5.yaml", + "destinationNotInSubaccount": "Destination not found in the subaccount: {{- destination}}", "systemNotFound": "Missing system configuration in ui5.yaml", "systemNotFoundInStore" : "System not found in the system store: {{- systemUrl}}", "writingPhase": "Error in writing phase of the adaptation project FLP configuration", "telemetry": "Error sending telemetry data: {{- error}}", - "updatingApp": "Error updating app with FLP configuration. Inspect the logs for full error." + "updatingApp": "Error updating app with FLP configuration. Inspect the logs for full error.", + "cannotBeEmpty": "{{- field}} cannot be empty.", + "authenticationFailed": "Authentication incorrect." } } \ No newline at end of file diff --git a/packages/adp-flp-config-sub-generator/tsconfig.json b/packages/adp-flp-config-sub-generator/tsconfig.json index 0e9aa5dc10..f6bd55853a 100644 --- a/packages/adp-flp-config-sub-generator/tsconfig.json +++ b/packages/adp-flp-config-sub-generator/tsconfig.json @@ -28,10 +28,10 @@ "path": "../flp-config-inquirer" }, { - "path": "../logger" + "path": "../inquirer-common" }, { - "path": "../odata-service-inquirer" + "path": "../logger" }, { "path": "../project-access" diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts index 395e04c93e..712fb92ceb 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts @@ -193,16 +193,6 @@ export async function getSystemConnectionQuestions( ); }, additionalMessages: async (selectedSystem: SystemSelectionAnswerType) => { - // Show message if connection to selected system is successful and showConnectionSuccessMessage is enabled - if ( - promptOptions?.systemSelection?.showConnectionSuccessMessage && - (connectionValidator.validity.authenticated || connectionValidator.validity.authRequired === false) - ) { - return { - message: t('prompts.systemSelection.connectionSuccessMessage'), - severity: Severity.information - }; - } // Backend systems credentials may need to be updated if ( selectedSystem.type === 'backendSystem' && diff --git a/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json b/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json index 58a75fa4e5..f259fa4189 100644 --- a/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json +++ b/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json @@ -103,8 +103,7 @@ "newSystemChoiceLabel": "New system", "hint": "Select a system configuration", "message": "System", - "authenticationFailedUpdateCredentials": "Authentication failed. Please try updating the credentials.", - "connectionSuccessMessage": "Connection successful" + "authenticationFailedUpdateCredentials": "Authentication failed. Please try updating the credentials." }, "abapOnBTPType": { "message": "ABAP environment definition source", diff --git a/packages/odata-service-inquirer/src/types.ts b/packages/odata-service-inquirer/src/types.ts index 19c7f6e8ae..37c7a332c1 100644 --- a/packages/odata-service-inquirer/src/types.ts +++ b/packages/odata-service-inquirer/src/types.ts @@ -268,11 +268,6 @@ export type SystemSelectionPromptOptions = { * this option will not be applied and the full list of choices will be presented to the user. */ onlyShowDefaultChoice?: boolean; - - /** - * Shows a message when the connection to the system is successful. - */ - showConnectionSuccessMessage?: boolean; }; export type MetadataPromptOptions = { diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts index fe546409f2..21de01b1f9 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts @@ -630,41 +630,4 @@ describe('Test system selection prompts', () => { ); expect(serviceSelectionPrompt).toBeDefined(); }); - - test('Should show connestion success message when showConnectionSuccessMessage is provided as true and connections is successful', async () => { - const connectValidator = new ConnectionValidator(); - connectValidator.validity.authRequired = false; - connectValidator.validity.authenticated = true; - const systemSelectionQuestions = await getSystemConnectionQuestions(connectValidator, { - [promptNames.systemSelection]: { showConnectionSuccessMessage: true } - }); - const systemSelectionPrompt = systemSelectionQuestions.find( - (question) => question.name === promptNames.systemSelection - ); - expect((systemSelectionPrompt as ListQuestion).additionalMessages).toBeDefined(); - const additionalMessages = await (systemSelectionPrompt as ListQuestion).additionalMessages?.({ - type: 'backendSystem', - system: backendSystemBasic - } as SystemSelectionAnswerType); - expect(additionalMessages).toMatchObject({ - message: t('prompts.systemSelection.connectionSuccessMessage'), - severity: 2 - }); - }); - - test('Should not show connestion success message when showConnectionSuccessMessage is not provided and connections is successful', async () => { - const connectValidator = new ConnectionValidator(); - connectValidator.validity.authRequired = false; - connectValidator.validity.authenticated = true; - const systemSelectionQuestions = await getSystemConnectionQuestions(connectValidator); - const systemSelectionPrompt = systemSelectionQuestions.find( - (question) => question.name === promptNames.systemSelection - ); - expect((systemSelectionPrompt as ListQuestion).additionalMessages).toBeDefined(); - const additionalMessages = await (systemSelectionPrompt as ListQuestion).additionalMessages?.({ - type: 'backendSystem', - system: backendSystemBasic - } as SystemSelectionAnswerType); - expect(additionalMessages).not.toBeDefined(); - }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da9bf03c76..76d8928b90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -168,7 +168,7 @@ importers: version: link:../../packages/system-access yeoman-generator: specifier: 5.10.0 - version: 5.10.0 + version: 5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3) devDependencies: '@sap-ux/odata-service-writer': specifier: workspace:* @@ -501,12 +501,12 @@ importers: '@sap-ux/flp-config-inquirer': specifier: workspace:* version: link:../flp-config-inquirer + '@sap-ux/inquirer-common': + specifier: workspace:* + version: link:../inquirer-common '@sap-ux/logger': specifier: workspace:* version: link:../logger - '@sap-ux/odata-service-inquirer': - specifier: workspace:* - version: link:../odata-service-inquirer '@sap-ux/project-access': specifier: workspace:* version: link:../project-access @@ -1409,7 +1409,7 @@ importers: version: 20.6.1 yeoman-generator: specifier: 5.10.0 - version: 5.10.0 + version: 5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3) devDependencies: '@sap-ux/axios-extension': specifier: workspace:* @@ -22821,37 +22821,6 @@ packages: - bluebird - supports-color - /yeoman-generator@5.10.0: - resolution: {integrity: sha512-iDUKykV7L4nDNzeYSedRmSeJ5eMYFucnKDi6KN1WNASXErgPepKqsQw55TgXPHnmpcyOh2Dd/LAZkyc+f0qaAw==} - engines: {node: '>=12.10.0'} - peerDependencies: - yeoman-environment: ^3.2.0 - peerDependenciesMeta: - yeoman-environment: - optional: true - dependencies: - chalk: 4.1.2 - dargs: 7.0.0 - debug: 4.3.7 - execa: 5.1.1 - github-username: 6.0.0 - lodash: 4.17.21 - mem-fs-editor: 9.4.0(mem-fs@2.1.0) - minimist: 1.2.8 - pacote: 15.2.0 - read-pkg-up: 7.0.1 - run-async: 2.4.1 - semver: 7.5.4 - shelljs: 0.8.5 - sort-keys: 4.2.0 - text-table: 0.2.0 - transitivePeerDependencies: - - bluebird - - encoding - - mem-fs - - supports-color - dev: false - /yeoman-generator@5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3): resolution: {integrity: sha512-iDUKykV7L4nDNzeYSedRmSeJ5eMYFucnKDi6KN1WNASXErgPepKqsQw55TgXPHnmpcyOh2Dd/LAZkyc+f0qaAw==} engines: {node: '>=12.10.0'} @@ -22872,7 +22841,7 @@ packages: pacote: 15.2.0 read-pkg-up: 7.0.1 run-async: 2.4.1 - semver: 7.6.3 + semver: 7.5.4 shelljs: 0.8.5 sort-keys: 4.2.0 text-table: 0.2.0 From b957cbe3a7c7e664b6aae48c50e58fe08412a7ef Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Tue, 28 Jan 2025 18:06:31 +0200 Subject: [PATCH 25/56] fix: mask password prompt input --- .../src/app/index.ts | 16 ++++++++++------ .../src/app/questions.ts | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index c232dde22a..61b35c695f 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -23,7 +23,7 @@ import { ToolsLogger } from '@sap-ux/logger'; import { EventName } from '../telemetryEvents'; import { getPrompts, type FLPConfigAnswers } from '@sap-ux/flp-config-inquirer'; import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; -import { TelemetryHelper, sendTelemetry, type ILogWrapper } from '@sap-ux/fiori-generator-shared'; +import { TelemetryHelper, sendTelemetry, isCli, type ILogWrapper } from '@sap-ux/fiori-generator-shared'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; import AdpFlpConfigLogger from '../utils/logger'; @@ -256,7 +256,7 @@ export default class extends Generator { description: t('yuiNavSteps.flpConfigDesc', { projectName: path.basename(this.projectRootPath) }) } ]); - this.setPromptsCallback = (fn) => { + this.setPromptsCallback = (fn): void => { if (this.prompts) { this.prompts.setCallback(fn); } @@ -272,7 +272,7 @@ export default class extends Generator { private async _findConfiguredSystem(target: AbapTarget): Promise { let configuredSystem: string | undefined; if (isAppStudio()) { - configuredSystem = target.destination; + configuredSystem = target?.destination; if (!configuredSystem) { this._showErrorNotification(t('error.destinationNotFound')); return; @@ -284,9 +284,9 @@ export default class extends Generator { return; } } else { - const { url } = target; + const url = target?.url; if (!url) { - this._showErrorNotification(t('error.systemUrlNotFound')); + this._showErrorNotification(t('error.systemNotFound')); return; } @@ -306,7 +306,11 @@ export default class extends Generator { * @param {string} message - The error message to display. */ private _showErrorNotification(message: string): void { - this.vscode.window.showErrorMessage(message); + if (isCli()) { + this.toolsLogger.error(message); + } else { + this.vscode.window.showErrorMessage(message); + } this.abort = true; } diff --git a/packages/adp-flp-config-sub-generator/src/app/questions.ts b/packages/adp-flp-config-sub-generator/src/app/questions.ts index 46fb564a1e..d05871c190 100644 --- a/packages/adp-flp-config-sub-generator/src/app/questions.ts +++ b/packages/adp-flp-config-sub-generator/src/app/questions.ts @@ -32,6 +32,7 @@ export function getCredentialsPrompts( guiType: 'login', name: t('prompts.password.name'), message: t('prompts.password.message'), + mask: '*', guiOptions: { mandatory: true }, From 4812bb21d6a61f2851c08cfa8c733d4768ad1f63 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 29 Jan 2025 12:56:37 +0200 Subject: [PATCH 26/56] fix: callback return result --- .../src/app/index.ts | 26 +++++++------------ .../src/app/questions.ts | 16 +++++------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 61b35c695f..77e076568e 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -28,7 +28,7 @@ import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; import AdpFlpConfigLogger from '../utils/logger'; import { t, initI18n } from '../utils/i18n'; -import { type Credentials, type ValidationResults, getCredentialsPrompts } from './questions'; +import { type Credentials, getCredentialsPrompts } from './questions'; import { ErrorHandler } from '@sap-ux/inquirer-common'; import { createAbapServiceProvider, @@ -160,17 +160,16 @@ export default class extends Generator { * Fetches the manifest for the project. * * @param {Credentials} credentials - The request options. - * @param {ValidationResults} validationResult - The validation results object used in password prompt validation. - * @returns {Promise} A promise that resolves when the manifest has been fetched. + * @returns {Promise} A promise that resolves with a boolean or an error message. */ - private async _fetchManifest(credentials?: Credentials, validationResult?: ValidationResults): Promise { + private async _fetchManifest(credentials?: Credentials): Promise { const { target, ignoreCertErrors = false } = await getAdpConfig( this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml) ); this.configuredSystem = await this._findConfiguredSystem(target); if (!this.configuredSystem) { - return; + return false; } try { const requiestOptions: AxiosRequestConfig & Partial = { ignoreCertErrors }; @@ -186,11 +185,9 @@ export default class extends Generator { this.toolsLogger ); this.manifest = manifestService.getManifest(); - if (validationResult) { - validationResult.valid = true; - } + return true; } catch (error) { - await this._handleFetchingError(error, validationResult); + return (await this._handleFetchingError(error)) ?? false; } } @@ -214,21 +211,16 @@ export default class extends Generator { * Handles errors that occur during the fetching of the manifest. * * @param {Error | AxiosError} error - The error that occurred. - * @param {ValidationResults} [validationResult] - The validation results object used in password prompt validation. - * @returns {Promise} A promise that resolves when the error has been handled. + * @returns {Promise} A promise that resolves with an error message or undefined. */ - private async _handleFetchingError(error: Error | AxiosError, validationResult?: ValidationResults): Promise { + private async _handleFetchingError(error: Error | AxiosError): Promise { if (isAxiosError(error)) { this.logger.error( `Manifest fetching failed: ${error}. Status: ${error.response?.status}. URI: ${error.request?.path}` ); if (error.response?.status === 401) { - if (validationResult) { - validationResult.valid = false; - validationResult.message = t('error.authenticationFailed'); - } this.authenticationRequired = true; - return; + return t('error.authenticationFailed'); } const errorHandler = new ErrorHandler(); diff --git a/packages/adp-flp-config-sub-generator/src/app/questions.ts b/packages/adp-flp-config-sub-generator/src/app/questions.ts index d05871c190..17804c6aba 100644 --- a/packages/adp-flp-config-sub-generator/src/app/questions.ts +++ b/packages/adp-flp-config-sub-generator/src/app/questions.ts @@ -2,17 +2,15 @@ import type Generator from 'yeoman-generator'; import { t } from '../utils/i18n'; export type Credentials = { username: string; password: string }; -export type ValidationResults = { valid: boolean; message?: string }; /** * Prompts the user for credentials. * - * @param {Function} callback - Optional callback function called in the validation phase of password prompt. - * Object containing the username and password is passed to the callback. Also, the validation results object is passed that can be used to set the validation status in the callback. - * @returns {Array} The prompts for username and password. + * @param {Function} additionalValidation - Optional callback function called in the validation phase of password prompt. Callback function should return a boolean or a message. + * @returns {Array} An array of prompts. */ export function getCredentialsPrompts( - callback?: (credentials: Credentials, validationResults: ValidationResults) => Promise + additionalValidation?: (credentials: Credentials) => Promise ): Array { return [ { @@ -37,8 +35,7 @@ export function getCredentialsPrompts( mandatory: true }, store: false, - validate: async (value: string, answers: Generator.Answers): Promise => { - const validationResults: ValidationResults = { valid: true }; + validate: async (value: string, answers: Generator.Answers): Promise => { if (!value) { return t('error.cannotBeEmpty', { field: t('prompts.password.message') }); } @@ -46,9 +43,8 @@ export function getCredentialsPrompts( if (!answers.username) { return t('error.cannotBeEmpty', { field: t('prompts.username.message') }); } - if (callback) { - await callback({ username: answers.username, password: value }, validationResults); - return validationResults.valid ? true : validationResults.message; + if (additionalValidation) { + return await additionalValidation({ username: answers.username, password: value }); } return true; } From 158f3dd2cd4231055e10883e6bbe994e0ab371e2 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 29 Jan 2025 13:11:50 +0200 Subject: [PATCH 27/56] fix: add type for credential prompts --- .../adp-flp-config-sub-generator/src/app/index.ts | 6 +++--- .../adp-flp-config-sub-generator/src/app/questions.ts | 11 ++++++----- packages/inquirer-common/src/types.ts | 8 ++++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 77e076568e..19137fdb92 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -28,7 +28,7 @@ import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; import AdpFlpConfigLogger from '../utils/logger'; import { t, initI18n } from '../utils/i18n'; -import { type Credentials, getCredentialsPrompts } from './questions'; +import { type CredentialsAnswers, getCredentialsPrompts } from './questions'; import { ErrorHandler } from '@sap-ux/inquirer-common'; import { createAbapServiceProvider, @@ -159,10 +159,10 @@ export default class extends Generator { /** * Fetches the manifest for the project. * - * @param {Credentials} credentials - The request options. + * @param {CredentialsAnswers} credentials - The request options. * @returns {Promise} A promise that resolves with a boolean or an error message. */ - private async _fetchManifest(credentials?: Credentials): Promise { + private async _fetchManifest(credentials?: CredentialsAnswers): Promise { const { target, ignoreCertErrors = false } = await getAdpConfig( this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml) diff --git a/packages/adp-flp-config-sub-generator/src/app/questions.ts b/packages/adp-flp-config-sub-generator/src/app/questions.ts index 17804c6aba..2fa797f51e 100644 --- a/packages/adp-flp-config-sub-generator/src/app/questions.ts +++ b/packages/adp-flp-config-sub-generator/src/app/questions.ts @@ -1,7 +1,8 @@ import type Generator from 'yeoman-generator'; import { t } from '../utils/i18n'; +import type { YUIQuestion, InputQuestion, PasswordQuestion } from '@sap-ux/inquirer-common'; -export type Credentials = { username: string; password: string }; +export type CredentialsAnswers = { username: string; password: string }; /** * Prompts the user for credentials. @@ -10,8 +11,8 @@ export type Credentials = { username: string; password: string }; * @returns {Array} An array of prompts. */ export function getCredentialsPrompts( - additionalValidation?: (credentials: Credentials) => Promise -): Array { + additionalValidation?: (credentials: CredentialsAnswers) => Promise +): YUIQuestion[] { return [ { type: 'input', @@ -24,7 +25,7 @@ export function getCredentialsPrompts( validate: (value: string): string | boolean => { return value ? true : t('error.cannotBeEmpty', { field: t('prompts.username.message') }); } - }, + } as InputQuestion, { type: 'password', guiType: 'login', @@ -48,6 +49,6 @@ export function getCredentialsPrompts( } return true; } - } + } as PasswordQuestion ]; } diff --git a/packages/inquirer-common/src/types.ts b/packages/inquirer-common/src/types.ts index ed680798a7..0f0f310d79 100644 --- a/packages/inquirer-common/src/types.ts +++ b/packages/inquirer-common/src/types.ts @@ -7,6 +7,7 @@ import type { CheckboxQuestion as BaseCheckBoxQuestion, NumberQuestion as BaseNumberQuestion, EditorQuestion as BaseEditorQuestion, + PasswordQuestion as BasePasswordQuestion, ListChoiceOptions, PromptFunction, PromptModule, @@ -112,6 +113,13 @@ export interface InputQuestion extends BaseInputQue guiOptions?: YUIQuestion['guiOptions']; } +export interface PasswordQuestion extends BasePasswordQuestion { + name: YUIQuestion['name']; + guiOptions?: YUIQuestion['guiOptions']; + guiType?: string; + mask?: string; +} + export interface CheckBoxQuestion extends BaseCheckBoxQuestion { name: YUIQuestion['name']; guiOptions?: YUIQuestion['guiOptions']; From 4de715ac7c734722a5a383003b52612e4ea7cfdc Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 29 Jan 2025 17:44:01 +0200 Subject: [PATCH 28/56] refactor: extract credential prompts in inquirer-common --- .../src/app/index.ts | 15 ++++++----- .../adp-flp-config-sub-generator.i18n.json | 13 ---------- packages/inquirer-common/src/index.ts | 1 + .../src/prompts/credentials.ts} | 25 ++++++++++--------- .../translations/inquirer-common.i18n.json | 11 +++++++- 5 files changed, 31 insertions(+), 34 deletions(-) rename packages/{adp-flp-config-sub-generator/src/app/questions.ts => inquirer-common/src/prompts/credentials.ts} (62%) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 19137fdb92..2a48cc22f8 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -28,8 +28,7 @@ import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; import AdpFlpConfigLogger from '../utils/logger'; import { t, initI18n } from '../utils/i18n'; -import { type CredentialsAnswers, getCredentialsPrompts } from './questions'; -import { ErrorHandler } from '@sap-ux/inquirer-common'; +import { ErrorHandler, type CredentialsAnswers, getCredentialsPrompts } from '@sap-ux/inquirer-common'; import { createAbapServiceProvider, type AbapTarget, @@ -80,11 +79,6 @@ export default class extends Generator { this.vscode = opts.vscode; this._configureLogging(); - - // If launched standalone add navigation steps - if (!this.launchAsSubGen) { - this._setupPrompts(); - } } async initializing(): Promise { @@ -94,6 +88,11 @@ export default class extends Generator { } await initI18n(); + // If launched standalone add navigation steps + if (!this.launchAsSubGen) { + this._setupPrompts(); + } + if (!this.manifest) { await this._fetchManifest(); } @@ -197,7 +196,7 @@ export default class extends Generator { * @returns {void} */ private async _promptAuthentication(): Promise { - const prompts = getCredentialsPrompts(this._fetchManifest.bind(this)); + const prompts = await getCredentialsPrompts(this._fetchManifest.bind(this)); this.prompts.splice(0, 0, [ { name: t('yuiNavSteps.flpCredentialsName'), diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index 06f13625ff..2408013c3a 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -1,16 +1,4 @@ { - "prompts": { - "username": { - "type": "input", - "name": "username", - "message": "Username" - }, - "password": { - "type": "password", - "name": "password", - "message": "Password" - } - }, "yuiNavSteps": { "flpCredentialsName": "FLP Configuration - Credentials", "flpCredentialsDesc": "Enter credentials for your adaptation project's system ({{- system}})", @@ -31,7 +19,6 @@ "writingPhase": "Error in writing phase of the adaptation project FLP configuration", "telemetry": "Error sending telemetry data: {{- error}}", "updatingApp": "Error updating app with FLP configuration. Inspect the logs for full error.", - "cannotBeEmpty": "{{- field}} cannot be empty.", "authenticationFailed": "Authentication incorrect." } } \ No newline at end of file diff --git a/packages/inquirer-common/src/index.ts b/packages/inquirer-common/src/index.ts index e6379b8ab6..a8c0a65d17 100644 --- a/packages/inquirer-common/src/index.ts +++ b/packages/inquirer-common/src/index.ts @@ -4,4 +4,5 @@ export * from './prompts/helpers'; export * from './error-handler/error-handler'; export * from './prompts/cf-helper'; export * from './telemetry/telemetry'; +export * from './prompts/credentials'; export { addi18nResourceBundle } from './i18n'; diff --git a/packages/adp-flp-config-sub-generator/src/app/questions.ts b/packages/inquirer-common/src/prompts/credentials.ts similarity index 62% rename from packages/adp-flp-config-sub-generator/src/app/questions.ts rename to packages/inquirer-common/src/prompts/credentials.ts index 2fa797f51e..73e6dba22a 100644 --- a/packages/adp-flp-config-sub-generator/src/app/questions.ts +++ b/packages/inquirer-common/src/prompts/credentials.ts @@ -1,8 +1,8 @@ -import type Generator from 'yeoman-generator'; -import { t } from '../utils/i18n'; -import type { YUIQuestion, InputQuestion, PasswordQuestion } from '@sap-ux/inquirer-common'; +import { t, addi18nResourceBundle } from '../i18n'; +import type { YUIQuestion, InputQuestion, PasswordQuestion } from '../types'; export type CredentialsAnswers = { username: string; password: string }; +export type AdditionalValidation = (credentials: CredentialsAnswers) => Promise; /** * Prompts the user for credentials. @@ -10,39 +10,40 @@ export type CredentialsAnswers = { username: string; password: string }; * @param {Function} additionalValidation - Optional callback function called in the validation phase of password prompt. Callback function should return a boolean or a message. * @returns {Array} An array of prompts. */ -export function getCredentialsPrompts( - additionalValidation?: (credentials: CredentialsAnswers) => Promise -): YUIQuestion[] { +export async function getCredentialsPrompts( + additionalValidation?: AdditionalValidation +): Promise[]> { + addi18nResourceBundle(); return [ { type: 'input', - name: t('prompts.username.name'), + name: 'username', message: t('prompts.username.message'), guiOptions: { mandatory: true }, store: false, validate: (value: string): string | boolean => { - return value ? true : t('error.cannotBeEmpty', { field: t('prompts.username.message') }); + return value ? true : t('errors.cannotBeEmpty', { field: t('prompts.username.message') }); } } as InputQuestion, { type: 'password', guiType: 'login', - name: t('prompts.password.name'), + name: 'password', message: t('prompts.password.message'), mask: '*', guiOptions: { mandatory: true }, store: false, - validate: async (value: string, answers: Generator.Answers): Promise => { + validate: async (value: string, answers: CredentialsAnswers): Promise => { if (!value) { - return t('error.cannotBeEmpty', { field: t('prompts.password.message') }); + return t('errors.cannotBeEmpty', { field: t('prompts.password.message') }); } if (!answers.username) { - return t('error.cannotBeEmpty', { field: t('prompts.username.message') }); + return t('errors.cannotBeEmpty', { field: t('prompts.username.message') }); } if (additionalValidation) { return await additionalValidation({ username: answers.username, password: value }); diff --git a/packages/inquirer-common/src/translations/inquirer-common.i18n.json b/packages/inquirer-common/src/translations/inquirer-common.i18n.json index ce66886d13..3ba04b4222 100644 --- a/packages/inquirer-common/src/translations/inquirer-common.i18n.json +++ b/packages/inquirer-common/src/translations/inquirer-common.i18n.json @@ -1,4 +1,12 @@ { + "prompts": { + "username": { + "message": "Username" + }, + "password": { + "message": "Password" + } + }, "ui5VersionLabels": { "maintained": "Maintained", "outOfMaintenance": "Out of maintenance", @@ -37,7 +45,8 @@ "systemConnectionValidationFailed": "A connection to the selected system could not be established.", "internalServerError": "Internal server error{{-errorMsg, addMsgWithColonFormatter}}", "badGateway": "Bad gateway{{- errorMsg, addMsgWithColonFormatter}}", - "badRequest": "Bad request{{- errorMsg, addMsgWithColonFormatter}}" + "badRequest": "Bad request{{- errorMsg, addMsgWithColonFormatter}}", + "cannotBeEmpty": "{{- field}} cannot be empty." }, "guidedAnswers": { "validationErrorHelpText": "Need help with this error?" From 1b8bfb9ff3fee6203171d36c783e3dfa1154cd9d Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 29 Jan 2025 19:11:26 +0200 Subject: [PATCH 29/56] fix: yeoman ui page configuration --- .../src/app/index.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 2a48cc22f8..050a105cb6 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -78,6 +78,13 @@ export default class extends Generator { this.options = opts; this.vscode = opts.vscode; + this.prompts = new Prompts([]); + this.setPromptsCallback = (fn): void => { + if (this.prompts) { + this.prompts.setCallback(fn); + } + }; + this._configureLogging(); } @@ -90,7 +97,7 @@ export default class extends Generator { await initI18n(); // If launched standalone add navigation steps if (!this.launchAsSubGen) { - this._setupPrompts(); + this._setupFLPConfigPage(); } if (!this.manifest) { @@ -240,18 +247,13 @@ export default class extends Generator { /** * Adds navigations steps and callback function for the generator prompts. */ - private _setupPrompts(): void { - this.prompts = new Prompts([ + private _setupFLPConfigPage(): void { + this.prompts.splice(0, 0, [ { name: t('yuiNavSteps.flpConfigName'), description: t('yuiNavSteps.flpConfigDesc', { projectName: path.basename(this.projectRootPath) }) } ]); - this.setPromptsCallback = (fn): void => { - if (this.prompts) { - this.prompts.setCallback(fn); - } - }; } /** From bf91aa0e511dff404c9d801fdb96c2e1de9c45d1 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 29 Jan 2025 19:38:26 +0200 Subject: [PATCH 30/56] fix: extract prompt setup --- .../src/app/index.ts | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 050a105cb6..00a144111e 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -78,13 +78,7 @@ export default class extends Generator { this.options = opts; this.vscode = opts.vscode; - this.prompts = new Prompts([]); - this.setPromptsCallback = (fn): void => { - if (this.prompts) { - this.prompts.setCallback(fn); - } - }; - + this._setupPrompts(); this._configureLogging(); } @@ -95,10 +89,7 @@ export default class extends Generator { } await initI18n(); - // If launched standalone add navigation steps - if (!this.launchAsSubGen) { - this._setupFLPConfigPage(); - } + this._setupFLPConfigPage(); if (!this.manifest) { await this._fetchManifest(); @@ -248,12 +239,30 @@ export default class extends Generator { * Adds navigations steps and callback function for the generator prompts. */ private _setupFLPConfigPage(): void { - this.prompts.splice(0, 0, [ - { - name: t('yuiNavSteps.flpConfigName'), - description: t('yuiNavSteps.flpConfigDesc', { projectName: path.basename(this.projectRootPath) }) - } - ]); + // if launched as a sub-generator, the navigation steps will be set by the parent generator + if (!this.launchAsSubGen) { + this.prompts.splice(0, 0, [ + { + name: t('yuiNavSteps.flpConfigName'), + description: t('yuiNavSteps.flpConfigDesc', { projectName: path.basename(this.projectRootPath) }) + } + ]); + } + } + + /** + * Sets up the prompts for the generator. + */ + private _setupPrompts(): void { + // If launched as a sub-generator, the prompts will be set by the parent generator + if (!this.launchAsSubGen) { + this.prompts = new Prompts([]); + this.setPromptsCallback = (fn): void => { + if (this.prompts) { + this.prompts.setCallback(fn); + } + }; + } } /** From eedf3ed730e40302c3194dbcc629c2ea6e013718 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Thu, 30 Jan 2025 18:08:42 +0200 Subject: [PATCH 31/56] refactor: change callback and error handling --- .../src/app/index.ts | 154 ++++++++++++------ .../src/prompts/credentials.ts | 11 +- 2 files changed, 109 insertions(+), 56 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 00a144111e..87218b6bec 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -17,7 +17,8 @@ import { getRegistrationIdFromManifest, isCFEnvironment, generateInboundConfig, - type InternalInboundNavigation + type InternalInboundNavigation, + type AdpPreviewConfig } from '@sap-ux/adp-tooling'; import { ToolsLogger } from '@sap-ux/logger'; import { EventName } from '../telemetryEvents'; @@ -28,7 +29,13 @@ import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName } from '@sap-ux/project-access'; import AdpFlpConfigLogger from '../utils/logger'; import { t, initI18n } from '../utils/i18n'; -import { ErrorHandler, type CredentialsAnswers, getCredentialsPrompts } from '@sap-ux/inquirer-common'; +import { + ErrorHandler, + type CredentialsAnswers, + getCredentialsPrompts, + addi18nResourceBundle, + type ValidationLink +} from '@sap-ux/inquirer-common'; import { createAbapServiceProvider, type AbapTarget, @@ -59,6 +66,8 @@ export default class extends Generator { // Flag to determine if the generator was aborted private abort: boolean = false; private configuredSystem: string | undefined; + private ui5Yaml: AdpPreviewConfig; + private credentials: CredentialsAnswers; /** * Creates an instance of the generator. @@ -79,7 +88,7 @@ export default class extends Generator { this.vscode = opts.vscode; this._setupPrompts(); - this._configureLogging(); + this._setupLogging(); } async initializing(): Promise { @@ -89,10 +98,25 @@ export default class extends Generator { } await initI18n(); + addi18nResourceBundle(); this._setupFLPConfigPage(); + this.ui5Yaml = await getAdpConfig(this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml)); + this.configuredSystem = await this._findConfiguredSystem(this.ui5Yaml.target); + if (!this.configuredSystem) { + return; + } + if (!this.manifest) { - await this._fetchManifest(); + try { + this.manifest = await this._getManifest(); + } catch (error) { + this.authenticationRequired = this._checkAuthRequired(error); + if (this.authenticationRequired) { + return; + } + this._handleFetchingError(error); + } } // Add telemetry to be sent once adp-flp-config is generated await TelemetryHelper.initTelemetrySettings({ @@ -106,12 +130,13 @@ export default class extends Generator { } public async prompting(): Promise { - if (this.abort) { - return; - } if (this.authenticationRequired) { await this._promptAuthentication(); } + + if (this.abort) { + return; + } const inbounds = getInboundsFromManifest(this.manifest); const appId = getRegistrationIdFromManifest(this.manifest); @@ -154,38 +179,25 @@ export default class extends Generator { } /** - * Fetches the manifest for the project. + * Retrieves the manifest for the project. * - * @param {CredentialsAnswers} credentials - The request options. - * @returns {Promise} A promise that resolves with a boolean or an error message. + * @returns {Promise} The project manifest. */ - private async _fetchManifest(credentials?: CredentialsAnswers): Promise { - const { target, ignoreCertErrors = false } = await getAdpConfig( + private async _getManifest(): Promise { + const { target, ignoreCertErrors = false } = this.ui5Yaml; + const requiestOptions: AxiosRequestConfig & Partial = { ignoreCertErrors }; + if (this.credentials) { + requiestOptions['auth'] = { username: this.credentials.username, password: this.credentials.password }; + } + const provider = await createAbapServiceProvider(target, requiestOptions, false, this.toolsLogger); + const variant = getVariant(this.projectRootPath); + const manifestService = await ManifestService.initMergedManifest( + provider, this.projectRootPath, - join(this.projectRootPath, FileName.Ui5Yaml) + variant, + this.toolsLogger ); - this.configuredSystem = await this._findConfiguredSystem(target); - if (!this.configuredSystem) { - return false; - } - try { - const requiestOptions: AxiosRequestConfig & Partial = { ignoreCertErrors }; - if (credentials) { - requiestOptions['auth'] = { username: credentials.username, password: credentials.password }; - } - const provider = await createAbapServiceProvider(target, requiestOptions, false, this.toolsLogger); - const variant = getVariant(this.projectRootPath); - const manifestService = await ManifestService.initMergedManifest( - provider, - this.projectRootPath, - variant, - this.toolsLogger - ); - this.manifest = manifestService.getManifest(); - return true; - } catch (error) { - return (await this._handleFetchingError(error)) ?? false; - } + return manifestService.getManifest(); } /** @@ -194,7 +206,25 @@ export default class extends Generator { * @returns {void} */ private async _promptAuthentication(): Promise { - const prompts = await getCredentialsPrompts(this._fetchManifest.bind(this)); + const prompts = await getCredentialsPrompts( + async (credentials: CredentialsAnswers): Promise => { + this.credentials = credentials; + try { + this.manifest = await this._getManifest(); + } catch (error) { + if (!isAxiosError(error)) { + this.logger.error(`Manifest fetching failed: ${error}`); + throw new Error(t('error.fetchingManifest')); + } + this.authenticationRequired = this._checkAuthRequired(error); + if (this.authenticationRequired) { + return t('error.authenticationFailed'); + } + return this._getErrorHandlerMessage(error) ?? false; + } + return true; + } + ); this.prompts.splice(0, 0, [ { name: t('yuiNavSteps.flpCredentialsName'), @@ -208,27 +238,20 @@ export default class extends Generator { * Handles errors that occur during the fetching of the manifest. * * @param {Error | AxiosError} error - The error that occurred. - * @returns {Promise} A promise that resolves with an error message or undefined. */ - private async _handleFetchingError(error: Error | AxiosError): Promise { + private _handleFetchingError(error: AxiosError): void { if (isAxiosError(error)) { this.logger.error( `Manifest fetching failed: ${error}. Status: ${error.response?.status}. URI: ${error.request?.path}` ); - if (error.response?.status === 401) { - this.authenticationRequired = true; - return t('error.authenticationFailed'); - } - const errorHandler = new ErrorHandler(); - const errorHelp = errorHandler.getValidationErrorHelp(error); + const errorHelp = this._getErrorHandlerMessage(error); if (errorHelp) { - this._showErrorNotification( + this._abortExecution( typeof errorHelp === 'string' ? errorHelp : `${errorHelp?.message} ([${errorHelp.link.text}](${errorHelp.link.url}))` ); - return; } } this.logger.error(`Manifest fetching failed: ${error}`); @@ -276,25 +299,25 @@ export default class extends Generator { if (isAppStudio()) { configuredSystem = target?.destination; if (!configuredSystem) { - this._showErrorNotification(t('error.destinationNotFound')); + this._abortExecution(t('error.destinationNotFound')); return; } const destinations = await listDestinations(); if (!(configuredSystem in destinations)) { - this._showErrorNotification(t('error.destinationNotFoundInStore', { destination: configuredSystem })); + this._abortExecution(t('error.destinationNotFoundInStore', { destination: configuredSystem })); return; } } else { const url = target?.url; if (!url) { - this._showErrorNotification(t('error.systemNotFound')); + this._abortExecution(t('error.systemNotFound')); return; } configuredSystem = (await getCredentialsFromStore(target as UrlAbapTarget, this.toolsLogger))?.name; if (!configuredSystem) { - this._showErrorNotification(t('error.systemNotFoundInStore', { systemUrl: url })); + this._abortExecution(t('error.systemNotFoundInStore', { systemUrl: url })); return; } } @@ -307,7 +330,7 @@ export default class extends Generator { * * @param {string} message - The error message to display. */ - private _showErrorNotification(message: string): void { + private _abortExecution(message: string): void { if (isCli()) { this.toolsLogger.error(message); } else { @@ -316,10 +339,37 @@ export default class extends Generator { this.abort = true; } + /** + * Retrieves the error handler message for the provided error. + * + * @param {Error | AxiosError} error - The error to handle. + * @returns {ValidationLink | string | undefined} The validation link or error message. + */ + private _getErrorHandlerMessage(error: Error | AxiosError): ValidationLink | string | undefined { + const errorHandler = new ErrorHandler(); + return errorHandler.getValidationErrorHelp(error); + } + + /** + * Checks if authentication is required based on the provided error. + * + * @param {Error | AxiosError} error - The error to check. + * @returns {boolean} True if authentication is required, false otherwise. + */ + private _checkAuthRequired(error: Error | AxiosError): boolean { + if (isAxiosError(error)) { + this.authenticationRequired = false; + if (error.response?.status === 401) { + return true; + } + } + return false; + } + /** * Configures logging for the generator. */ - private _configureLogging(): void { + private _setupLogging(): void { AdpFlpConfigLogger.configureLogging( this.options.logger, this.rootGeneratorName(), diff --git a/packages/inquirer-common/src/prompts/credentials.ts b/packages/inquirer-common/src/prompts/credentials.ts index 73e6dba22a..201ffc1d22 100644 --- a/packages/inquirer-common/src/prompts/credentials.ts +++ b/packages/inquirer-common/src/prompts/credentials.ts @@ -1,8 +1,9 @@ -import { t, addi18nResourceBundle } from '../i18n'; +import { t } from '../i18n'; import type { YUIQuestion, InputQuestion, PasswordQuestion } from '../types'; +import type { ValidationLink } from '../types'; export type CredentialsAnswers = { username: string; password: string }; -export type AdditionalValidation = (credentials: CredentialsAnswers) => Promise; +export type AdditionalValidation = (credentials: CredentialsAnswers) => Promise; /** * Prompts the user for credentials. @@ -13,7 +14,6 @@ export type AdditionalValidation = (credentials: CredentialsAnswers) => Promise< export async function getCredentialsPrompts( additionalValidation?: AdditionalValidation ): Promise[]> { - addi18nResourceBundle(); return [ { type: 'input', @@ -37,7 +37,10 @@ export async function getCredentialsPrompts( mandatory: true }, store: false, - validate: async (value: string, answers: CredentialsAnswers): Promise => { + validate: async ( + value: string, + answers: CredentialsAnswers + ): Promise => { if (!value) { return t('errors.cannotBeEmpty', { field: t('prompts.password.message') }); } From f097c8faf53268064d22a68e00cda1f945ed73a1 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Thu, 30 Jan 2025 20:14:32 +0200 Subject: [PATCH 32/56] test: amend existing tests --- .../test/app.test.ts | 81 ++++++++++++------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index e15629236e..5daa5f2a0a 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -1,5 +1,5 @@ import type { BackendSystem } from '@sap-ux/store'; -import type * as axios from '@sap-ux/axios-extension'; +import type { YUIQuestion, CredentialsAnswers } from '@sap-ux/inquirer-common'; import type { FLPConfigAnswers } from '@sap-ux/flp-config-inquirer'; import type { ToolsLogger } from '@sap-ux/logger'; import { join } from 'path'; @@ -10,13 +10,13 @@ import adpFlpConfigGenerator from '../src/app'; import * as adpTooling from '@sap-ux/adp-tooling'; import * as btpUtils from '@sap-ux/btp-utils'; import * as Logger from '@sap-ux/logger'; -import * as odataInquirer from '@sap-ux/odata-service-inquirer'; import * as fioriGenShared from '@sap-ux/fiori-generator-shared'; import { rimraf } from 'rimraf'; import { EventName } from '../src/telemetryEvents'; import * as sysAccess from '@sap-ux/system-access'; import { t } from '../src/utils/i18n'; import { MessageType } from '@sap-devx/yeoman-ui-types'; +import * as inquirerCommon from '@sap-ux/inquirer-common'; jest.mock('@sap-ux/system-access'); jest.mock('@sap-ux/btp-utils'); @@ -26,7 +26,10 @@ jest.mock('@sap-ux/adp-tooling', () => ({ getAdpConfig: jest.fn(), generateInboundConfig: jest.fn() })); - +jest.mock('@sap-ux/inquirer-common', () => ({ + ...jest.requireActual('@sap-ux/inquirer-common'), + getCredentialsPrompts: jest.fn() +})); jest.mock('@sap-ux/fiori-generator-shared', () => ({ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion ...(jest.requireActual('@sap-ux/fiori-generator-shared') as {}), @@ -39,11 +42,10 @@ jest.mock('@sap-ux/fiori-generator-shared', () => ({ }) }, isExtensionInstalled: jest.fn().mockReturnValue(true), - getHostEnvironment: jest.fn() + getHostEnvironment: jest.fn(), + isCli: jest.fn().mockReturnValue(false) })); -const sendTelemetrySpy = fioriGenShared.sendTelemetry as jest.Mock; - const loggerMock: ToolsLogger = { debug: jest.fn(), info: jest.fn(), @@ -58,30 +60,21 @@ describe('FLPConfigGenerator Integration Tests', () => { const generatorPath = join(__dirname, '../../src/app/index.ts'); const testOutputDir = join(__dirname, 'test-output'); - const systemSelectionPrompts = { + const credentialsPrompts = { prompts: [ { - name: 'systemSelection' + username: 'systemUsername' }, { - name: 'systemUsername' - }, - { - name: 'systemPassword' - } - ], - answers: { - connectedSystem: { - serviceProvider: {} as axios.ServiceProvider + password: 'systemPassword' } - } + ] }; let answers: | FLPConfigAnswers | { - systemSelection: string; - systemUsername: string; - systemPassword: string; + username: string; + password: string; }; jest.spyOn(adpTooling.ManifestService, 'initMergedManifest').mockResolvedValue({ @@ -92,13 +85,20 @@ describe('FLPConfigGenerator Integration Tests', () => { setHeaderTitle: jest.fn(), showInformation: showInformationSpy }; - jest.spyOn(odataInquirer, 'getSystemSelectionQuestions').mockResolvedValue(systemSelectionPrompts); + jest.spyOn(inquirerCommon, 'getCredentialsPrompts').mockResolvedValue( + credentialsPrompts as unknown as YUIQuestion[] + ); + const vsCodeMessageSpy = jest.fn(); + const vscode = { + window: { + showErrorMessage: vsCodeMessageSpy + } + }; beforeEach(() => { answers = { - systemSelection: 'testSystem', - systemUsername: 'testUsername', - systemPassword: 'testPassword', + username: 'testUsername', + password: 'testPassword', semanticObject: 'testSemanticObject', emptyInboundsInfo: 'testEmptyInboundsInfo', action: 'testAction', @@ -128,6 +128,16 @@ describe('FLPConfigGenerator Integration Tests', () => { } }); jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({ + testDestination: { + Name: 'https://testUrl', + Host: '000', + Type: 'Type', + Authentication: 'Basic', + ProxyType: 'Internet', + Description: 'Description' + } + }); const sendTelemetrySpy = jest.spyOn(fioriGenShared, 'sendTelemetry'); const runContext = yeomanTest @@ -141,6 +151,7 @@ describe('FLPConfigGenerator Integration Tests', () => { } ) .withOptions({ + vscode, appWizard: mockAppWizard, launchAsSubGen: false }) @@ -195,6 +206,7 @@ describe('FLPConfigGenerator Integration Tests', () => { } ) .withOptions({ + vscode, appWizard: mockAppWizard, launchAsSubGen: true }) @@ -241,6 +253,7 @@ describe('FLPConfigGenerator Integration Tests', () => { } ) .withOptions({ + vscode, appWizard: mockAppWizard, launchFlpConfigAsSubGenerator: false }) @@ -270,6 +283,7 @@ describe('FLPConfigGenerator Integration Tests', () => { } ) .withOptions({ + vscode, appWizard: mockAppWizard, launchFlpConfigAsSubGenerator: false }) @@ -283,6 +297,7 @@ describe('FLPConfigGenerator Integration Tests', () => { target: {} as unknown as sysAccess.AbapTarget }); jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({}); const testProjectPath = join(__dirname, 'fixtures/app.variant1'); const runContext = yeomanTest @@ -296,12 +311,14 @@ describe('FLPConfigGenerator Integration Tests', () => { } ) .withOptions({ + vscode, appWizard: mockAppWizard, launchFlpConfigAsSubGenerator: false }) .withPrompts(answers); - await expect(runContext.run()).rejects.toThrow(t('error.destinationNotFound')); + await runContext.run(); + expect(vsCodeMessageSpy).toBeCalledWith(t('error.destinationNotFound')); }); it('Should throw an error when no url is configured for target in VS Code', async () => { @@ -322,12 +339,14 @@ describe('FLPConfigGenerator Integration Tests', () => { } ) .withOptions({ + vscode, appWizard: mockAppWizard, launchFlpConfigAsSubGenerator: false }) .withPrompts(answers); - await expect(runContext.run()).rejects.toThrow(t('error.systemNotFound')); + await runContext.run(); + expect(vsCodeMessageSpy).toBeCalledWith(t('error.systemNotFound')); }); it('Should throw an error when system is not found in the store in VS Code', async () => { @@ -352,12 +371,13 @@ describe('FLPConfigGenerator Integration Tests', () => { } ) .withOptions({ + vscode, appWizard: mockAppWizard, launchFlpConfigAsSubGenerator: false }) .withPrompts(answers); - - await expect(runContext.run()).rejects.toThrow(t('error.systemNotFoundInStore', { url: systemUrl })); + await runContext.run(); + expect(vsCodeMessageSpy).toBeCalledWith(t('error.systemNotFound', { systemUrl })); }); it('Should throw an error when fetching manifest fails', async () => { @@ -366,7 +386,7 @@ describe('FLPConfigGenerator Integration Tests', () => { url: 'https://testUrl' } }); - jest.spyOn(odataInquirer, 'getSystemSelectionQuestions').mockRejectedValueOnce(new Error('Error')); + jest.spyOn(adpTooling.ManifestService, 'initMergedManifest').mockRejectedValueOnce(new Error('Error')); jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(false); const testProjectPath = join(__dirname, 'fixtures/app.variant1'); @@ -381,6 +401,7 @@ describe('FLPConfigGenerator Integration Tests', () => { } ) .withOptions({ + vscode, appWizard: mockAppWizard, launchFlpConfigAsSubGenerator: false }) From 3dbea2650b84f95059c0c4c372bc2343eb948215 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 31 Jan 2025 12:50:30 +0200 Subject: [PATCH 33/56] fix: check project type --- .../adp-flp-config-sub-generator/src/app/index.ts | 12 +++++------- .../adp-flp-config-sub-generator.i18n.json | 4 ++-- .../adp-flp-config-sub-generator/test/app.test.ts | 4 +++- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 87218b6bec..de6aa1bf7d 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -26,7 +26,7 @@ import { getPrompts, type FLPConfigAnswers } from '@sap-ux/flp-config-inquirer'; import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; import { TelemetryHelper, sendTelemetry, isCli, type ILogWrapper } from '@sap-ux/fiori-generator-shared'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; -import { FileName } from '@sap-ux/project-access'; +import { FileName, getAppType } from '@sap-ux/project-access'; import AdpFlpConfigLogger from '../utils/logger'; import { t, initI18n } from '../utils/i18n'; import { @@ -45,7 +45,7 @@ import { import { isAppStudio, listDestinations } from '@sap-ux/btp-utils'; /** - * Generator for adding a FLP configuration to an Adaptation Project. + * Generator for adding a FLP configuration to an adaptation project. * * @extends Generator */ @@ -60,7 +60,6 @@ export default class extends Generator { private answers: FLPConfigAnswers; private toolsLogger: ToolsLogger; private logger: ILogWrapper; - public options: FlpConfigOptions; private vscode: any; private authenticationRequired: boolean = false; // Flag to determine if the generator was aborted @@ -92,9 +91,9 @@ export default class extends Generator { } async initializing(): Promise { - // Generator does not support CF projects - if (isCFEnvironment(this.projectRootPath)) { - throw new Error(t('error.cfNotSupported')); + // Check if the project is supported + if ((await getAppType(this.projectRootPath)) !== 'Fiori Adaptation' || isCFEnvironment(this.projectRootPath)) { + throw new Error(t('error.projectNotSUpported')); } await initI18n(); @@ -358,7 +357,6 @@ export default class extends Generator { */ private _checkAuthRequired(error: Error | AxiosError): boolean { if (isAxiosError(error)) { - this.authenticationRequired = false; if (error.response?.status === 401) { return true; } diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index 2408013c3a..763ec6169f 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -10,7 +10,7 @@ "flpConfigAdded": "FLP Configuration added successfully" }, "error": { - "cfNotSupported": "FLP Configuration is not supported for CF projects", + "projectNotSUpported": "FLP Configuration is not supported for project of this type", "fetchingManifest": "Error fetching merged manifest for base application", "destinationNotFound": "Missing destination configuration in ui5.yaml", "destinationNotInSubaccount": "Destination not found in the subaccount: {{- destination}}", @@ -19,6 +19,6 @@ "writingPhase": "Error in writing phase of the adaptation project FLP configuration", "telemetry": "Error sending telemetry data: {{- error}}", "updatingApp": "Error updating app with FLP configuration. Inspect the logs for full error.", - "authenticationFailed": "Authentication incorrect." + "authenticationFailed": "Authentication failed." } } \ No newline at end of file diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index 5daa5f2a0a..29595d072a 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -17,6 +17,7 @@ import * as sysAccess from '@sap-ux/system-access'; import { t } from '../src/utils/i18n'; import { MessageType } from '@sap-devx/yeoman-ui-types'; import * as inquirerCommon from '@sap-ux/inquirer-common'; +import * as projectAccess from '@sap-ux/project-access'; jest.mock('@sap-ux/system-access'); jest.mock('@sap-ux/btp-utils'); @@ -94,6 +95,7 @@ describe('FLPConfigGenerator Integration Tests', () => { showErrorMessage: vsCodeMessageSpy } }; + jest.spyOn(projectAccess, 'getAppType').mockResolvedValue('Fiori Adaptation'); beforeEach(() => { answers = { @@ -259,7 +261,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }) .withPrompts(answers); - await expect(runContext.run()).rejects.toThrow(t('error.cfNotSupported')); + await expect(runContext.run()).rejects.toThrow(t('error.projectNotSUpported')); }); it('Should throw an error if writing phase fails', async () => { From f79018a6a58d8ec026240dc7c32bae0884a7f322 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 31 Jan 2025 13:16:59 +0200 Subject: [PATCH 34/56] fix: spelling error --- packages/adp-flp-config-sub-generator/src/app/index.ts | 2 +- .../src/translations/adp-flp-config-sub-generator.i18n.json | 2 +- packages/adp-flp-config-sub-generator/test/app.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index de6aa1bf7d..5e71f60195 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -93,7 +93,7 @@ export default class extends Generator { async initializing(): Promise { // Check if the project is supported if ((await getAppType(this.projectRootPath)) !== 'Fiori Adaptation' || isCFEnvironment(this.projectRootPath)) { - throw new Error(t('error.projectNotSUpported')); + throw new Error(t('error.projectNotSupported')); } await initI18n(); diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index 763ec6169f..5adba87a81 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -10,7 +10,7 @@ "flpConfigAdded": "FLP Configuration added successfully" }, "error": { - "projectNotSUpported": "FLP Configuration is not supported for project of this type", + "projectNotSupported": "FLP Configuration is not supported for project of this type", "fetchingManifest": "Error fetching merged manifest for base application", "destinationNotFound": "Missing destination configuration in ui5.yaml", "destinationNotInSubaccount": "Destination not found in the subaccount: {{- destination}}", diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index 29595d072a..787d47cc5c 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -261,7 +261,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }) .withPrompts(answers); - await expect(runContext.run()).rejects.toThrow(t('error.projectNotSUpported')); + await expect(runContext.run()).rejects.toThrow(t('error.projectNotSupported')); }); it('Should throw an error if writing phase fails', async () => { From e7c619799e5b5a2f9d0058aa5ecb8e029dabc07c Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 31 Jan 2025 13:29:29 +0200 Subject: [PATCH 35/56] fix: error message for unsupported projects --- .../src/translations/adp-flp-config-sub-generator.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index 5adba87a81..75e4f9f5dc 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -10,7 +10,7 @@ "flpConfigAdded": "FLP Configuration added successfully" }, "error": { - "projectNotSupported": "FLP Configuration is not supported for project of this type", + "projectNotSupported": "FLP Configuration was not able to find a supported adaptation project", "fetchingManifest": "Error fetching merged manifest for base application", "destinationNotFound": "Missing destination configuration in ui5.yaml", "destinationNotInSubaccount": "Destination not found in the subaccount: {{- destination}}", From 8df055bffdfc913d04949b685c0f51e41eb3dab2 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 31 Jan 2025 13:33:16 +0200 Subject: [PATCH 36/56] fix: error message --- .../src/translations/adp-flp-config-sub-generator.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json index 75e4f9f5dc..9c3f7bce01 100644 --- a/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json +++ b/packages/adp-flp-config-sub-generator/src/translations/adp-flp-config-sub-generator.i18n.json @@ -10,7 +10,7 @@ "flpConfigAdded": "FLP Configuration added successfully" }, "error": { - "projectNotSupported": "FLP Configuration was not able to find a supported adaptation project", + "projectNotSupported": "Unable to find a supported adaptation project", "fetchingManifest": "Error fetching merged manifest for base application", "destinationNotFound": "Missing destination configuration in ui5.yaml", "destinationNotInSubaccount": "Destination not found in the subaccount: {{- destination}}", From 3bc8462e2d6ed5a46bcc3439d047567ef1e07da8 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 31 Jan 2025 13:36:29 +0200 Subject: [PATCH 37/56] fix: merge types --- packages/inquirer-common/src/prompts/credentials.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/inquirer-common/src/prompts/credentials.ts b/packages/inquirer-common/src/prompts/credentials.ts index 201ffc1d22..77ec4864f8 100644 --- a/packages/inquirer-common/src/prompts/credentials.ts +++ b/packages/inquirer-common/src/prompts/credentials.ts @@ -1,6 +1,5 @@ import { t } from '../i18n'; -import type { YUIQuestion, InputQuestion, PasswordQuestion } from '../types'; -import type { ValidationLink } from '../types'; +import type { YUIQuestion, InputQuestion, PasswordQuestion, ValidationLink } from '../types'; export type CredentialsAnswers = { username: string; password: string }; export type AdditionalValidation = (credentials: CredentialsAnswers) => Promise; From 9bd7b50d29001c6bbc99549c04f12a20165ddd6d Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 31 Jan 2025 13:55:15 +0200 Subject: [PATCH 38/56] fix: duplicate type --- packages/inquirer-common/src/types.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/inquirer-common/src/types.ts b/packages/inquirer-common/src/types.ts index 61fdcde576..7bfe5728d5 100644 --- a/packages/inquirer-common/src/types.ts +++ b/packages/inquirer-common/src/types.ts @@ -120,13 +120,6 @@ export interface InputQuestion extends BaseInputQue additionalMessages?: YUIQuestion['additionalMessages']; } -export interface PasswordQuestion extends BasePasswordQuestion { - name: YUIQuestion['name']; - guiOptions?: YUIQuestion['guiOptions']; - guiType?: string; - mask?: string; -} - export interface CheckBoxQuestion extends BaseCheckBoxQuestion { name: YUIQuestion['name']; guiOptions?: YUIQuestion['guiOptions']; From 93661e970c02c7a6a701b193f97fad4dbc748229 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 31 Jan 2025 20:58:09 +0200 Subject: [PATCH 39/56] test: add tests --- .../test/app.test.ts | 213 +++++++++++++++++- 1 file changed, 209 insertions(+), 4 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index 787d47cc5c..b1cef5d5c9 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -14,7 +14,7 @@ import * as fioriGenShared from '@sap-ux/fiori-generator-shared'; import { rimraf } from 'rimraf'; import { EventName } from '../src/telemetryEvents'; import * as sysAccess from '@sap-ux/system-access'; -import { t } from '../src/utils/i18n'; +import { t, initI18n } from '../src/utils/i18n'; import { MessageType } from '@sap-devx/yeoman-ui-types'; import * as inquirerCommon from '@sap-ux/inquirer-common'; import * as projectAccess from '@sap-ux/project-access'; @@ -58,7 +58,6 @@ jest.spyOn(Logger, 'ToolsLogger').mockImplementation(() => loggerMock); describe('FLPConfigGenerator Integration Tests', () => { jest.setTimeout(100000); jest.spyOn(adpTooling, 'isCFEnvironment').mockReturnValue(false); - const generatorPath = join(__dirname, '../../src/app/index.ts'); const testOutputDir = join(__dirname, 'test-output'); const credentialsPrompts = { @@ -97,7 +96,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }; jest.spyOn(projectAccess, 'getAppType').mockResolvedValue('Fiori Adaptation'); - beforeEach(() => { + beforeEach(async () => { answers = { username: 'testUsername', password: 'testPassword', @@ -110,7 +109,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }; }); - beforeAll(() => { + beforeAll(async () => { fs.mkdirSync(testOutputDir, { recursive: true }); }); @@ -411,4 +410,210 @@ describe('FLPConfigGenerator Integration Tests', () => { await expect(runContext.run()).rejects.toThrow(t('error.fetchingManifest')); }); + + it('Should require authentication if manifest fetching returns 401 and fail after authentication', async () => { + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({ + testDestination: { + Name: 'https://testUrl', + Host: '000', + Type: 'Type', + Authentication: 'Basic', + ProxyType: 'Internet', + Description: 'Description' + } + }); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + const initMergedManifestSpy = jest + .spyOn(adpTooling.ManifestService, 'initMergedManifest') + .mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 401 + } + }) + .mockRejectedValueOnce(new Error('Error')); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + jest.spyOn(inquirerCommon, 'getCredentialsPrompts').mockImplementationOnce( + async ( + callback?: inquirerCommon.AdditionalValidation + ): Promise[]> => { + await callback?.({ username: 'testUsername', password: 'testPassword' }); + return Promise.resolve([ + { + username: 'testUsername' + } as unknown as inquirerCommon.InputQuestion, + { + password: 'testPassword' + } as unknown as inquirerCommon.PasswordQuestion + ]); + } + ); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + vscode, + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + initMergedManifestSpy.mockClear(); + await expect(runContext.run()).rejects.toThrow(t('error.fetchingManifest')); + expect(initMergedManifestSpy).toBeCalledTimes(2); + }); + + it('Should require authentication again if credentials are wrong', async () => { + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({ + testDestination: { + Name: 'https://testUrl', + Host: '000', + Type: 'Type', + Authentication: 'Basic', + ProxyType: 'Internet', + Description: 'Description' + } + }); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + const initMergedManifestSpy = jest + .spyOn(adpTooling.ManifestService, 'initMergedManifest') + .mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 401 + } + }) + .mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 401 + } + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + let callbackResult: string = ''; + jest.spyOn(inquirerCommon, 'getCredentialsPrompts').mockImplementationOnce( + async ( + callback?: inquirerCommon.AdditionalValidation + ): Promise[]> => { + callbackResult = (await callback?.({ username: 'testUsername', password: 'testPassword' })) as string; + return Promise.resolve([ + { + username: 'testUsername' + } as unknown as inquirerCommon.InputQuestion, + { + password: 'testPassword' + } as unknown as inquirerCommon.PasswordQuestion + ]); + } + ); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + vscode, + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await runContext.run(); + await initI18n(); + expect(callbackResult).toEqual(t('error.authenticationFailed')); + }); + + it('Should show error message after authetication when manifest request fails with connection error', async () => { + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({ + testDestination: { + Name: 'https://testUrl', + Host: '000', + Type: 'Type', + Authentication: 'Basic', + ProxyType: 'Internet', + Description: 'Description' + } + }); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + const initMergedManifestSpy = jest + .spyOn(adpTooling.ManifestService, 'initMergedManifest') + .mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 401 + } + }) + .mockRejectedValueOnce({ + isAxiosError: true, + response: { + message: 'unable to get local issuer certificate' + } + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + let callbackResult: string = ''; + jest.spyOn(inquirerCommon, 'getCredentialsPrompts').mockImplementationOnce( + async ( + callback?: inquirerCommon.AdditionalValidation + ): Promise[]> => { + callbackResult = (await callback?.({ username: 'testUsername', password: 'testPassword' })) as string; + return Promise.resolve([ + { + username: 'testUsername' + } as unknown as inquirerCommon.InputQuestion, + { + password: 'testPassword' + } as unknown as inquirerCommon.PasswordQuestion + ]); + } + ); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + vscode, + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await runContext.run(); + expect(callbackResult).toEqual(t('error.authenticationFailed')); + }); }); From 005197b6d3b6f8cb1a25889cc9acdbafe3b8ceb0 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Fri, 31 Jan 2025 22:42:19 +0200 Subject: [PATCH 40/56] test: add tests --- .../src/app/index.ts | 1 + .../test/app.test.ts | 245 ++++++++++++++---- 2 files changed, 198 insertions(+), 48 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 5e71f60195..7176437cb6 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -252,6 +252,7 @@ export default class extends Generator { : `${errorHelp?.message} ([${errorHelp.link.text}](${errorHelp.link.url}))` ); } + return; } this.logger.error(`Manifest fetching failed: ${error}`); throw new Error(t('error.fetchingManifest')); diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index b1cef5d5c9..dd0d41aef6 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -29,7 +29,13 @@ jest.mock('@sap-ux/adp-tooling', () => ({ })); jest.mock('@sap-ux/inquirer-common', () => ({ ...jest.requireActual('@sap-ux/inquirer-common'), - getCredentialsPrompts: jest.fn() + getCredentialsPrompts: jest.fn(), + ErrorHandler: jest.fn().mockImplementation( + () => + ({ + getValidationErrorHelp: () => 'Network Error' + } as unknown as inquirerCommon.ErrorHandler) + ) })); jest.mock('@sap-ux/fiori-generator-shared', () => ({ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion @@ -47,11 +53,12 @@ jest.mock('@sap-ux/fiori-generator-shared', () => ({ isCli: jest.fn().mockReturnValue(false) })); +const toolsLoggerErrorSpy = jest.fn(); const loggerMock: ToolsLogger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), - error: jest.fn() + error: toolsLoggerErrorSpy } as Partial as ToolsLogger; jest.spyOn(Logger, 'ToolsLogger').mockImplementation(() => loggerMock); @@ -70,6 +77,16 @@ describe('FLPConfigGenerator Integration Tests', () => { } ] }; + const destinationList = { + testDestination: { + Name: 'https://testUrl', + Host: '000', + Type: 'Type', + Authentication: 'Basic', + ProxyType: 'Internet', + Description: 'Description' + } + }; let answers: | FLPConfigAnswers | { @@ -129,16 +146,7 @@ describe('FLPConfigGenerator Integration Tests', () => { } }); jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); - jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({ - testDestination: { - Name: 'https://testUrl', - Host: '000', - Type: 'Type', - Authentication: 'Basic', - ProxyType: 'Internet', - Description: 'Description' - } - }); + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue(destinationList); const sendTelemetrySpy = jest.spyOn(fioriGenShared, 'sendTelemetry'); const runContext = yeomanTest @@ -412,16 +420,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }); it('Should require authentication if manifest fetching returns 401 and fail after authentication', async () => { - jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({ - testDestination: { - Name: 'https://testUrl', - Host: '000', - Type: 'Type', - Authentication: 'Basic', - ProxyType: 'Internet', - Description: 'Description' - } - }); + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue(destinationList); jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ target: { destination: 'testDestination' @@ -477,16 +476,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }); it('Should require authentication again if credentials are wrong', async () => { - jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({ - testDestination: { - Name: 'https://testUrl', - Host: '000', - Type: 'Type', - Authentication: 'Basic', - ProxyType: 'Internet', - Description: 'Description' - } - }); + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue(destinationList); jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ target: { destination: 'testDestination' @@ -548,23 +538,13 @@ describe('FLPConfigGenerator Integration Tests', () => { }); it('Should show error message after authetication when manifest request fails with connection error', async () => { - jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({ - testDestination: { - Name: 'https://testUrl', - Host: '000', - Type: 'Type', - Authentication: 'Basic', - ProxyType: 'Internet', - Description: 'Description' - } - }); + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue(destinationList); jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ target: { destination: 'testDestination' } }); - const initMergedManifestSpy = jest - .spyOn(adpTooling.ManifestService, 'initMergedManifest') + jest.spyOn(adpTooling.ManifestService, 'initMergedManifest') .mockRejectedValueOnce({ isAxiosError: true, response: { @@ -573,9 +553,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }) .mockRejectedValueOnce({ isAxiosError: true, - response: { - message: 'unable to get local issuer certificate' - } + message: 'Network Error' }); jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); const testProjectPath = join(__dirname, 'fixtures/app.variant1'); @@ -614,6 +592,177 @@ describe('FLPConfigGenerator Integration Tests', () => { .withPrompts(answers); await runContext.run(); - expect(callbackResult).toEqual(t('error.authenticationFailed')); + expect(callbackResult).toEqual('Network Error'); + }); + + it('Should pass authentication successfully', async () => { + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue(destinationList); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + jest.spyOn(adpTooling.ManifestService, 'initMergedManifest').mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 401 + } + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + let callbackResult: string = ''; + jest.spyOn(inquirerCommon, 'getCredentialsPrompts').mockImplementationOnce( + async ( + callback?: inquirerCommon.AdditionalValidation + ): Promise[]> => { + callbackResult = (await callback?.({ username: 'testUsername', password: 'testPassword' })) as string; + return Promise.resolve([ + { + username: 'testUsername' + } as unknown as inquirerCommon.InputQuestion, + { + password: 'testPassword' + } as unknown as inquirerCommon.PasswordQuestion + ]); + } + ); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + vscode, + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await runContext.run(); + expect(callbackResult).toEqual(true); + }); + + it('Should fail manifest fetching with network error', async () => { + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue(destinationList); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + jest.spyOn(adpTooling.ManifestService, 'initMergedManifest').mockRejectedValueOnce({ + isAxiosError: true, + message: 'Network Error' + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + let callbackResult: string = ''; + jest.spyOn(inquirerCommon, 'getCredentialsPrompts').mockImplementationOnce( + async ( + callback?: inquirerCommon.AdditionalValidation + ): Promise[]> => { + callbackResult = (await callback?.({ username: 'testUsername', password: 'testPassword' })) as string; + return Promise.resolve([ + { + username: 'testUsername' + } as unknown as inquirerCommon.InputQuestion, + { + password: 'testPassword' + } as unknown as inquirerCommon.PasswordQuestion + ]); + } + ); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + vscode, + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await runContext.run(); + expect(vsCodeMessageSpy).toBeCalledWith('Network Error'); + }); + + it('Should fail if destination is not found in BTP subaccount', async () => { + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({}); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + vscode, + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await runContext.run(); + expect(vsCodeMessageSpy).toBeCalledWith( + t('error.destinationNotFoundInStore', { destination: 'testDestination' }) + ); + }); + + it('Should fail if destination is not found in BTP subaccount and log error in CLI', async () => { + jest.spyOn(fioriGenShared, 'isCli').mockReturnValue(true); + jest.spyOn(btpUtils, 'listDestinations').mockResolvedValue({}); + jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ + target: { + destination: 'testDestination' + } + }); + jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); + const testProjectPath = join(__dirname, 'fixtures/app.variant1'); + + const runContext = yeomanTest + .create( + adpFlpConfigGenerator, + { + resolved: generatorPath + }, + { + cwd: testProjectPath + } + ) + .withOptions({ + vscode, + appWizard: mockAppWizard, + launchFlpConfigAsSubGenerator: false + }) + .withPrompts(answers); + + await runContext.run(); + expect(toolsLoggerErrorSpy).toBeCalledWith( + t('error.destinationNotFoundInStore', { destination: 'testDestination' }) + ); }); }); From b98cda3f3eba8f2e0be9ecd9226876929383a84c Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 09:19:17 +0200 Subject: [PATCH 41/56] fix: Sonar issues --- .../adp-flp-config-sub-generator/src/app/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 7176437cb6..a5026a18cc 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -53,14 +53,14 @@ export default class extends Generator { setPromptsCallback: (fn: object) => void; private prompts: Prompts; // Flag to determine if the generator was launched as a sub-generator or standalone - private launchAsSubGen: boolean; - private appWizard: AppWizard; + private readonly launchAsSubGen: boolean; + private readonly appWizard: AppWizard; + private readonly vscode: any; + private readonly toolsLogger: ToolsLogger; + private readonly projectRootPath: string = ''; private manifest: Manifest; - private projectRootPath: string = ''; private answers: FLPConfigAnswers; - private toolsLogger: ToolsLogger; private logger: ILogWrapper; - private vscode: any; private authenticationRequired: boolean = false; // Flag to determine if the generator was aborted private abort: boolean = false; @@ -143,7 +143,7 @@ export default class extends Generator { overwrite: { hide: true }, createAnotherInbound: { hide: true } }); - this.answers = (await this.prompt(prompts)) as FLPConfigAnswers; + this.answers = await this.prompt(prompts); } async writing(): Promise { From 61566abca88b9ceac1f6844c6a09af44314223c8 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 09:30:25 +0200 Subject: [PATCH 42/56] fix: revert unneeded change --- .../test/unit/__snapshots__/app.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap b/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap index 44995d5246..dc54e78aaa 100644 --- a/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap +++ b/packages/ui5-library-reference-sub-generator/test/unit/__snapshots__/app.test.ts.snap @@ -389,4 +389,4 @@ exports[`Test reference generator should run the generator custom webapp path 1` }, }, } -`; \ No newline at end of file +`; From 06398abe8ec3f1ed0ffbdad6d1f88dec8273838a Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 10:05:42 +0200 Subject: [PATCH 43/56] test: add adp-tooling tests --- packages/adp-tooling/test/unit/base/helper.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/adp-tooling/test/unit/base/helper.test.ts b/packages/adp-tooling/test/unit/base/helper.test.ts index 1cec392005..029cef5ac5 100644 --- a/packages/adp-tooling/test/unit/base/helper.test.ts +++ b/packages/adp-tooling/test/unit/base/helper.test.ts @@ -45,6 +45,17 @@ describe('helper', () => { expect(getVariant(basePath)).toStrictEqual(JSON.parse(mockVariant)); }); + + test('should return variant using fs editor', () => { + const fs = { + readJSON: jest.fn().mockReturnValue(JSON.parse(mockVariant)) + } as unknown as Editor; + + const result = getVariant(basePath, fs); + + expect(fs.readJSON).toHaveBeenCalledWith(join(basePath, 'webapp', 'manifest.appdescr_variant')); + expect(result).toStrictEqual(JSON.parse(mockVariant)); + }); }); describe('updateVariant', () => { From e8bdee04093ce08657d7fa6cef8189659984daf7 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 11:28:17 +0200 Subject: [PATCH 44/56] fix: i18n fs condition --- packages/i18n/src/write/properties/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/i18n/src/write/properties/create.ts b/packages/i18n/src/write/properties/create.ts index 0c295e644e..00dfed0ccd 100644 --- a/packages/i18n/src/write/properties/create.ts +++ b/packages/i18n/src/write/properties/create.ts @@ -21,7 +21,7 @@ export async function createPropertiesI18nEntries( root?: string, fs?: Editor ): Promise { - if (!fs?.exists(i18nFilePath) && !(await doesExist(i18nFilePath))) { + if ((!fs && !(await doesExist(i18nFilePath))) || (fs && !fs.exists(i18nFilePath))) { let content = '# Resource bundle \n'; if (root) { content = `# This is the resource bundle for ${basename(root)}\n`; From 5a09fcc6e7419c47bc128f31ac784edf37e52a93 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 14:00:56 +0200 Subject: [PATCH 45/56] test: add inquirer-common tests --- .../test/unit/prompts/credentials.test.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 packages/inquirer-common/test/unit/prompts/credentials.test.ts diff --git a/packages/inquirer-common/test/unit/prompts/credentials.test.ts b/packages/inquirer-common/test/unit/prompts/credentials.test.ts new file mode 100644 index 0000000000..c72782f9ce --- /dev/null +++ b/packages/inquirer-common/test/unit/prompts/credentials.test.ts @@ -0,0 +1,100 @@ +import { + getCredentialsPrompts, + type CredentialsAnswers, + type AdditionalValidation +} from '../../../src/prompts/credentials'; +import { t, addi18nResourceBundle } from '../../../src/i18n'; + +import type { InputQuestion, PasswordQuestion } from '../../../src/types'; + +describe('getCredentialsPrompts', () => { + beforeAll(() => { + addi18nResourceBundle(); + }); + + it('should return an array of prompts', async () => { + const prompts = await getCredentialsPrompts(); + expect(prompts).toBeInstanceOf(Array); + expect(prompts).toEqual([ + { + type: 'input', + name: 'username', + message: 'Username', + guiOptions: { + mandatory: true + }, + store: false, + validate: expect.any(Function) + } as InputQuestion, + { + type: 'password', + guiType: 'login', + name: 'password', + message: 'Password', + mask: '*', + guiOptions: { + mandatory: true + }, + store: false, + validate: expect.any(Function) + } as PasswordQuestion + ]); + }); + + it('should validate username correctly', async () => { + const prompts = await getCredentialsPrompts(); + const validate = (prompts[0] as InputQuestion).validate; + + expect(typeof validate).toBe('function'); + expect(validate?.('')).toBe(t('errors.cannotBeEmpty', { field: t('prompts.username.message') })); + expect(validate?.('user')).toBe(true); + }); + + it('should validate password correctly', async () => { + const prompts = await getCredentialsPrompts(); + const validate = (prompts[1] as PasswordQuestion).validate; + + const answers: CredentialsAnswers = { + username: undefined + } as unknown as CredentialsAnswers; + + expect(await validate?.('', answers)).toBe(t('errors.cannotBeEmpty', { field: t('prompts.password.message') })); + expect(await validate?.('password', answers)).toBe( + t('errors.cannotBeEmpty', { field: t('prompts.username.message') }) + ); + answers.username = 'user'; + expect(await validate?.('pass', answers)).toBe(true); + }); + + it('should call additionalValidation if provided', async () => { + const additionalValidation: AdditionalValidation = jest.fn().mockResolvedValue(true); + const prompts = await getCredentialsPrompts(additionalValidation); + const validate = (prompts[1] as PasswordQuestion).validate; + + const answers: CredentialsAnswers = { username: 'user', password: 'pass' }; + + await validate?.('pass', answers); + expect(additionalValidation).toHaveBeenCalledWith({ username: 'user', password: 'pass' }); + }); + + it('should return validation message from additionalValidation if provided', async () => { + const additionalValidation: AdditionalValidation = jest.fn().mockResolvedValue('Additional validation failed'); + const prompts = await getCredentialsPrompts(additionalValidation); + const validate = (prompts[1] as PasswordQuestion).validate; + + const answers: CredentialsAnswers = { username: 'user', password: 'pass' }; + + const result = await validate?.('pass', answers); + expect(result).toBe('Additional validation failed'); + }); + + it('should handle missing username in password validation', async () => { + const prompts = await getCredentialsPrompts(); + const validate = (prompts[1] as PasswordQuestion).validate; + + const answers: CredentialsAnswers = { username: '', password: 'pass' }; + + const result = await validate?.('pass', answers); + expect(result).toBe(t('errors.cannotBeEmpty', { field: t('prompts.username.message') })); + }); +}); From ecd6340362f1736945876185f3719dfafc6db068 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 15:40:38 +0200 Subject: [PATCH 46/56] test: i18n tests --- .../test/unit/write/properties/create.test.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/i18n/test/unit/write/properties/create.test.ts b/packages/i18n/test/unit/write/properties/create.test.ts index b3d3eaf6e7..3997166543 100644 --- a/packages/i18n/test/unit/write/properties/create.test.ts +++ b/packages/i18n/test/unit/write/properties/create.test.ts @@ -3,6 +3,7 @@ import * as utilsWrite from '../../../../src/write/utils'; import * as utils from '../../../../src/utils'; import { create as createStorage } from 'mem-fs'; import { create } from 'mem-fs-editor'; +import { basename } from 'path'; describe('create', () => { describe('createPropertiesI18nEntries', () => { @@ -64,7 +65,7 @@ describe('create', () => { const result = await createPropertiesI18nEntries('i18n.properties', newEntries, undefined, memFs); expect(result).toEqual(true); - expect(doesExistSpy).toHaveBeenCalledTimes(1); + expect(doesExistSpy).toHaveBeenCalledTimes(0); expect(writeFileSpy).toHaveBeenNthCalledWith(1, 'i18n.properties', '# Resource bundle \n', memFs); expect(writeToExistingI18nPropertiesFileSpy).toHaveBeenNthCalledWith( 1, @@ -89,6 +90,50 @@ describe('create', () => { undefined ); }); + test('create a new i18n file if it does not exist in both real and virtual file systems', async () => { + const i18nFilePath = 'path/to/i18n.properties'; + const root = 'path/to/root'; + const doesExistSpy = jest.spyOn(utils, 'doesExist').mockResolvedValue(false); + const memFs = create(createStorage()); + memFs.exists = jest.fn().mockReturnValue(false); + const writeFileSpy = jest.spyOn(utils, 'writeFile').mockResolvedValue(); + const writeToExistingI18nPropertiesFileSpy = jest + .spyOn(utilsWrite, 'writeToExistingI18nPropertiesFile') + .mockResolvedValue(true); + + await createPropertiesI18nEntries(i18nFilePath, newEntries, root, memFs); + + expect(doesExistSpy).not.toHaveBeenCalled(); + expect(memFs.exists).toHaveBeenCalledWith(i18nFilePath); + expect(writeFileSpy).toHaveBeenCalledWith( + i18nFilePath, + `# This is the resource bundle for ${basename(root)}\n`, + memFs + ); + expect(writeToExistingI18nPropertiesFileSpy).toHaveBeenCalledWith(i18nFilePath, newEntries, memFs); + }); + test('create a new i18n file if it exists in the real file system, but does not exist in the passed virtual file system', async () => { + const i18nFilePath = 'path/to/i18n.properties'; + const root = 'path/to/root'; + const doesExistSpy = jest.spyOn(utils, 'doesExist').mockResolvedValue(true); + const memFs = create(createStorage()); + memFs.exists = jest.fn().mockReturnValue(false); + const writeFileSpy = jest.spyOn(utils, 'writeFile').mockResolvedValue(); + const writeToExistingI18nPropertiesFileSpy = jest + .spyOn(utilsWrite, 'writeToExistingI18nPropertiesFile') + .mockResolvedValue(true); + + await createPropertiesI18nEntries(i18nFilePath, newEntries, root, memFs); + + expect(doesExistSpy).not.toHaveBeenCalled(); + expect(memFs.exists).toHaveBeenCalledWith(i18nFilePath); + expect(writeFileSpy).toHaveBeenCalledWith( + i18nFilePath, + `# This is the resource bundle for ${basename(root)}\n`, + memFs + ); + expect(writeToExistingI18nPropertiesFileSpy).toHaveBeenCalledWith(i18nFilePath, newEntries, memFs); + }); test('exception / error case', async () => { jest.spyOn(utilsWrite, 'writeToExistingI18nPropertiesFile').mockImplementation(() => { throw new Error('should-throw-error'); From 164e55b0bcb8bdf253900d5a791f1f3d179e6421 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 15:52:38 +0200 Subject: [PATCH 47/56] chore: add new package to Sonar --- sonar-project.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sonar-project.properties b/sonar-project.properties index b48530a655..c4782d8d8d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -12,6 +12,7 @@ sonar.javascript.lcov.reportPaths=packages/abap-deploy-config-inquirer/coverage/ packages/adp-tooling/coverage/lcov.info, \ packages/annotation-generator/coverage/lcov.info, \ packages/app-config-writer/coverage/lcov.info, \ + packages/adp-flp-config-sub-generator/lcov.info, \ packages/axios-extension/coverage/lcov.info, \ packages/backend-proxy-middleware/coverage/lcov.info, \ packages/btp-utils/coverage/lcov.info, \ @@ -89,6 +90,7 @@ sonar.testExecutionReportPaths=packages/abap-deploy-config-inquirer/coverage/son packages/adp-tooling/coverage/sonar-report.xml, \ packages/annotation-generator/coverage/sonar-report.xml, \ packages/app-config-writer/coverage/sonar-report.xml, \ + packages/adp-flp-config-sub-generator/sonar-report.xml, \ packages/axios-extension/coverage/sonar-report.xml, \ packages/backend-proxy-middleware/coverage/sonar-report.xml, \ packages/btp-utils/coverage/sonar-report.xml, \ From 3f2c58c0eb7ee7438b2e20175825a17ceb56e555 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 16:16:15 +0200 Subject: [PATCH 48/56] chore: fix sonar report path --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index c4782d8d8d..6e6aa74163 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -90,7 +90,7 @@ sonar.testExecutionReportPaths=packages/abap-deploy-config-inquirer/coverage/son packages/adp-tooling/coverage/sonar-report.xml, \ packages/annotation-generator/coverage/sonar-report.xml, \ packages/app-config-writer/coverage/sonar-report.xml, \ - packages/adp-flp-config-sub-generator/sonar-report.xml, \ + packages/adp-flp-config-sub-generator/coverage/sonar-report.xml, \ packages/axios-extension/coverage/sonar-report.xml, \ packages/backend-proxy-middleware/coverage/sonar-report.xml, \ packages/btp-utils/coverage/sonar-report.xml, \ From dfe9764243687035f71a3f7be72bd34187a4a556 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 16:42:02 +0200 Subject: [PATCH 49/56] chore: fix sonar lcov path --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 6e6aa74163..f0244942ae 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -12,7 +12,7 @@ sonar.javascript.lcov.reportPaths=packages/abap-deploy-config-inquirer/coverage/ packages/adp-tooling/coverage/lcov.info, \ packages/annotation-generator/coverage/lcov.info, \ packages/app-config-writer/coverage/lcov.info, \ - packages/adp-flp-config-sub-generator/lcov.info, \ + packages/adp-flp-config-sub-generator/coverage/lcov.info, \ packages/axios-extension/coverage/lcov.info, \ packages/backend-proxy-middleware/coverage/lcov.info, \ packages/btp-utils/coverage/lcov.info, \ From eda533a9fe9b3251e7e3094b3040f5e067d76dd4 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Mon, 3 Feb 2025 17:20:46 +0200 Subject: [PATCH 50/56] doc: change JSDoc --- packages/adp-flp-config-sub-generator/src/app/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index a5026a18cc..13c129b6eb 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -178,7 +178,7 @@ export default class extends Generator { } /** - * Retrieves the manifest for the project. + * Retrieves the merged manifest for the project. * * @returns {Promise} The project manifest. */ From c4eef3dc7a13dd8705b676f0b31711cbdfdfaeab Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Tue, 4 Feb 2025 10:53:15 +0200 Subject: [PATCH 51/56] fix: revert process working dir after tests --- packages/adp-flp-config-sub-generator/test/app.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index dd0d41aef6..0d0e3cf613 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -19,6 +19,8 @@ import { MessageType } from '@sap-devx/yeoman-ui-types'; import * as inquirerCommon from '@sap-ux/inquirer-common'; import * as projectAccess from '@sap-ux/project-access'; +const originalCwd = process.cwd(); + jest.mock('@sap-ux/system-access'); jest.mock('@sap-ux/btp-utils'); jest.mock('@sap-ux/adp-tooling', () => ({ @@ -63,7 +65,8 @@ const loggerMock: ToolsLogger = { jest.spyOn(Logger, 'ToolsLogger').mockImplementation(() => loggerMock); describe('FLPConfigGenerator Integration Tests', () => { - jest.setTimeout(100000); + jest.setTimeout(60000); + jest.spyOn(adpTooling, 'isCFEnvironment').mockReturnValue(false); const generatorPath = join(__dirname, '../../src/app/index.ts'); const testOutputDir = join(__dirname, 'test-output'); @@ -131,6 +134,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }); afterAll(() => { + process.chdir(originalCwd); // Generation changes the cwd, this breaks sonar report so we restore later rimraf.sync(testOutputDir); }); From 1f81c49e037a234a3a4caee73a36a53eb23cddf6 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 5 Feb 2025 10:13:57 +0200 Subject: [PATCH 52/56] fix: change overwrite implementation --- .../src/app/index.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 13c129b6eb..aaeee88aa6 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -24,7 +24,13 @@ import { ToolsLogger } from '@sap-ux/logger'; import { EventName } from '../telemetryEvents'; import { getPrompts, type FLPConfigAnswers } from '@sap-ux/flp-config-inquirer'; import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; -import { TelemetryHelper, sendTelemetry, isCli, type ILogWrapper } from '@sap-ux/fiori-generator-shared'; +import { + TelemetryHelper, + sendTelemetry, + isCli, + type ILogWrapper, + type YeomanEnvironment +} from '@sap-ux/fiori-generator-shared'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { FileName, getAppType } from '@sap-ux/project-access'; import AdpFlpConfigLogger from '../utils/logger'; @@ -75,8 +81,6 @@ export default class extends Generator { * @param {FlpConfigOptions} opts - The options for the generator. */ constructor(args: string | string[], opts: FlpConfigOptions) { - // Force the generator to overwrite existing files without additional prompting - opts.force = true; super(args, opts); this.appWizard = opts.appWizard ?? AppWizard.create(opts); this.launchAsSubGen = !!opts.launchAsSubGen; @@ -98,6 +102,12 @@ export default class extends Generator { await initI18n(); addi18nResourceBundle(); + + // Force the generator to overwrite existing files without additional prompting + if ((this.env as unknown as YeomanEnvironment).conflicter) { + (this.env as unknown as YeomanEnvironment).conflicter.force = this.options.force ?? true; + } + this._setupFLPConfigPage(); this.ui5Yaml = await getAdpConfig(this.projectRootPath, join(this.projectRootPath, FileName.Ui5Yaml)); From 1befebb9d178fb9b8b91418f4a3837f5eb7605b7 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 5 Feb 2025 11:23:41 +0200 Subject: [PATCH 53/56] fix: initialize translations earlier --- .../src/app/index.ts | 6 ++---- .../src/utils/i18n.ts | 3 +++ .../test/app.test.ts | 18 +++++++++--------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index aaeee88aa6..778d0ab1bd 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -39,7 +39,6 @@ import { ErrorHandler, type CredentialsAnswers, getCredentialsPrompts, - addi18nResourceBundle, type ValidationLink } from '@sap-ux/inquirer-common'; import { @@ -95,14 +94,13 @@ export default class extends Generator { } async initializing(): Promise { + await initI18n(); + // Check if the project is supported if ((await getAppType(this.projectRootPath)) !== 'Fiori Adaptation' || isCFEnvironment(this.projectRootPath)) { throw new Error(t('error.projectNotSupported')); } - await initI18n(); - addi18nResourceBundle(); - // Force the generator to overwrite existing files without additional prompting if ((this.env as unknown as YeomanEnvironment).conflicter) { (this.env as unknown as YeomanEnvironment).conflicter.force = this.options.force ?? true; diff --git a/packages/adp-flp-config-sub-generator/src/utils/i18n.ts b/packages/adp-flp-config-sub-generator/src/utils/i18n.ts index 3654ea71fe..d6881cfaf4 100644 --- a/packages/adp-flp-config-sub-generator/src/utils/i18n.ts +++ b/packages/adp-flp-config-sub-generator/src/utils/i18n.ts @@ -1,6 +1,7 @@ import type { TOptions } from 'i18next'; import i18next from 'i18next'; import translations from '../translations/adp-flp-config-sub-generator.i18n.json'; +import { addi18nResourceBundle as addInquirerCommonResourceBundle } from '@sap-ux/inquirer-common'; const adpFlpConfigI18nNamespace = 'adp-flp-config-sub-generator'; @@ -11,6 +12,8 @@ export async function initI18n(): Promise { await i18next.init({ lng: 'en', fallbackLng: 'en' }, () => i18next.addResourceBundle('en', adpFlpConfigI18nNamespace, translations) ); + + addInquirerCommonResourceBundle(); } /** diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index 0d0e3cf613..eafe83d749 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -130,6 +130,7 @@ describe('FLPConfigGenerator Integration Tests', () => { }); beforeAll(async () => { + await initI18n(); fs.mkdirSync(testOutputDir, { recursive: true }); }); @@ -245,13 +246,13 @@ describe('FLPConfigGenerator Integration Tests', () => { expect(showInformationSpy).not.toBeCalled(); }); - it('Should result in an error message if the project is a CF project', async () => { - jest.spyOn(adpTooling, 'isCFEnvironment').mockReturnValueOnce(true); + it('Should throw an error if writing phase fails', async () => { jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ target: { destination: 'testDestination' } }); + jest.spyOn(adpTooling, 'generateInboundConfig').mockRejectedValueOnce(new Error('Error')); jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); const testProjectPath = join(__dirname, 'fixtures/app.variant1'); @@ -272,16 +273,16 @@ describe('FLPConfigGenerator Integration Tests', () => { }) .withPrompts(answers); - await expect(runContext.run()).rejects.toThrow(t('error.projectNotSupported')); + await expect(runContext.run()).rejects.toThrow(t('error.updatingApp')); }); - it('Should throw an error if writing phase fails', async () => { + it('Should result in an error message if the project is a CF project', async () => { + jest.spyOn(adpTooling, 'isCFEnvironment').mockReturnValueOnce(true); jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({ target: { destination: 'testDestination' } }); - jest.spyOn(adpTooling, 'generateInboundConfig').mockRejectedValueOnce(new Error('Error')); jest.spyOn(btpUtils, 'isAppStudio').mockReturnValue(true); const testProjectPath = join(__dirname, 'fixtures/app.variant1'); @@ -301,8 +302,8 @@ describe('FLPConfigGenerator Integration Tests', () => { launchFlpConfigAsSubGenerator: false }) .withPrompts(answers); - - await expect(runContext.run()).rejects.toThrow(t('error.updatingApp')); + await initI18n(); + await expect(runContext.run()).rejects.toThrow(t('error.projectNotSupported')); }); it('Should throw an error when no destination is configured in Application Studio', async () => { @@ -486,8 +487,7 @@ describe('FLPConfigGenerator Integration Tests', () => { destination: 'testDestination' } }); - const initMergedManifestSpy = jest - .spyOn(adpTooling.ManifestService, 'initMergedManifest') + jest.spyOn(adpTooling.ManifestService, 'initMergedManifest') .mockRejectedValueOnce({ isAxiosError: true, response: { From b73b3a7eefeca3d85941bf8c988930d20f76ade5 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 5 Feb 2025 11:31:00 +0200 Subject: [PATCH 54/56] fix: typo --- packages/adp-flp-config-sub-generator/src/app/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 778d0ab1bd..6a34acdb31 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -192,11 +192,11 @@ export default class extends Generator { */ private async _getManifest(): Promise { const { target, ignoreCertErrors = false } = this.ui5Yaml; - const requiestOptions: AxiosRequestConfig & Partial = { ignoreCertErrors }; + const requestOptions: AxiosRequestConfig & Partial = { ignoreCertErrors }; if (this.credentials) { - requiestOptions['auth'] = { username: this.credentials.username, password: this.credentials.password }; + requestOptions['auth'] = { username: this.credentials.username, password: this.credentials.password }; } - const provider = await createAbapServiceProvider(target, requiestOptions, false, this.toolsLogger); + const provider = await createAbapServiceProvider(target, requestOptions, false, this.toolsLogger); const variant = getVariant(this.projectRootPath); const manifestService = await ManifestService.initMergedManifest( provider, From bd4b61e3380dbe58d46415ca86f2fcec4f4a0370 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 5 Feb 2025 11:34:08 +0200 Subject: [PATCH 55/56] fix: clean inspy in afterEach --- packages/adp-flp-config-sub-generator/test/app.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/adp-flp-config-sub-generator/test/app.test.ts b/packages/adp-flp-config-sub-generator/test/app.test.ts index eafe83d749..8fd466a4d2 100644 --- a/packages/adp-flp-config-sub-generator/test/app.test.ts +++ b/packages/adp-flp-config-sub-generator/test/app.test.ts @@ -129,6 +129,10 @@ describe('FLPConfigGenerator Integration Tests', () => { }; }); + afterEach(() => { + showInformationSpy.mockReset(); + }); + beforeAll(async () => { await initI18n(); fs.mkdirSync(testOutputDir, { recursive: true }); @@ -192,7 +196,6 @@ describe('FLPConfigGenerator Integration Tests', () => { }); it('should generate FLP configuration successfully - VS Code', async () => { - showInformationSpy.mockReset(); const testPath = join(testOutputDir, 'test_project2'); fs.mkdirSync(testPath, { recursive: true }); fsextra.copySync(join(__dirname, 'fixtures/app.variant1'), join(testPath, 'app.variant1')); From b833f84b42c2b29f71e9b6c5dfd96af4b81f0577 Mon Sep 17 00:00:00 2001 From: mmilko01 Date: Wed, 5 Feb 2025 12:41:21 +0200 Subject: [PATCH 56/56] fix: hide label in CLI --- packages/adp-flp-config-sub-generator/src/app/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index 6a34acdb31..e6e155a73e 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -146,10 +146,10 @@ export default class extends Generator { } const inbounds = getInboundsFromManifest(this.manifest); const appId = getRegistrationIdFromManifest(this.manifest); - const prompts: Question[] = await getPrompts(inbounds, appId, { overwrite: { hide: true }, - createAnotherInbound: { hide: true } + createAnotherInbound: { hide: true }, + emptyInboundsInfo: { hide: isCli() } }); this.answers = await this.prompt(prompts); }