From 205e291a660c8ce0e39cbe035f58b34543fcbcf9 Mon Sep 17 00:00:00 2001 From: Christian Badura <93912698+cbadura@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:30:12 +0200 Subject: [PATCH] Enable import and export (#139) * feat: update api, prepare import and export * feat: make dialogs visible, write import functions * fix: disable lint for explicit any * fix: add buttons and text in export dialog, fix translations * fix: make export work * fix: make import also work * fix: use display names in export, show only used products * fix: bump deps * fix: tests * fix: remove fdescribe * fix: add date time to filename, add tooltips, and cancel button, fix tests --------- Co-authored-by: Christian Badura --- package-lock.json | 284 ++++++- package.json | 7 +- src/_mixins.scss | 10 + .../help-detail/help-detail.component.html | 6 + .../help-search/help-search.component.html | 101 +++ .../help-search/help-search.component.scss | 16 + .../help-search/help-search.component.spec.ts | 727 +++++++++++------- .../help/help-search/help-search.component.ts | 160 +++- .../help-item-editor.component.ts | 1 + .../remotes/show-help/show-help.component.ts | 1 + .../shared/generated/.openapi-generator/FILES | 1 + .../generated/api/helpsInternal.service.ts | 218 ++++++ .../generated/model/exportHelpsRequest.ts | 17 + src/app/shared/generated/model/help.ts | 1 + src/app/shared/generated/model/models.ts | 1 + src/app/shared/shared.module.ts | 3 + src/app/types/index.d.ts | 1 + src/assets/api/openapi-bff.yaml | 92 +++ src/assets/i18n/de.json | 23 + src/assets/i18n/en.json | 24 +- 20 files changed, 1376 insertions(+), 318 deletions(-) create mode 100644 src/app/shared/generated/model/exportHelpsRequest.ts create mode 100644 src/app/types/index.d.ts diff --git a/package-lock.json b/package-lock.json index 294ec35..ea05e69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@onecx/portal-integration-angular": "^5.2.0", "@onecx/portal-layout-styles": "^5.2.0", "@webcomponents/webcomponentsjs": "^2.8.0", + "file-saver": "^2.0.5", "keycloak-angular": "^16.0.1", "ngx-color": "^9.0.0", "primeflex": "^3.3.1", @@ -49,7 +50,7 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.1", - "@angular-devkit/core": "^18.1.1", + "@angular-devkit/core": "^18.1.3", "@angular-devkit/schematics": "^18.1.1", "@angular-eslint/builder": "^18.1.0", "@angular-eslint/eslint-plugin": "^18.1.0", @@ -67,7 +68,7 @@ "@schematics/angular": "^18.1.1", "@storybook/addon-essentials": "8.2.5", "@storybook/angular": "8.2.5", - "@storybook/core-server": "8.2.5 ", + "@storybook/core-server": "8.2.6 ", "@svgr/webpack": "^8.1.0", "@swc-node/register": "^1.10.9", "@swc/cli": "~0.4.0", @@ -106,7 +107,7 @@ "sonar-scanner": "^3.1.0", "sonarqube-scanner": "^4.0.1", "style-loader": "^4.0.0", - "tailwindcss": "3.4.6", + "tailwindcss": "3.4.7", "ts-node": "10.9.2", "typescript": "5.5", "url-loader": "^4.1.1", @@ -178,6 +179,34 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.1.tgz", + "integrity": "sha512-YFzn/+8LezX7ZJhMQisvrqfkxJm6+JOtbWFj8K/luK0rTDmE8Z9n9r6kJ36FnHcLJ5MvvVaBc7n1v1wnzdqXpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.16.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-devkit/build-angular": { "version": "18.1.1", "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.1.1.tgz", @@ -307,6 +336,58 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.1.tgz", + "integrity": "sha512-YFzn/+8LezX7ZJhMQisvrqfkxJm6+JOtbWFj8K/luK0rTDmE8Z9n9r6kJ36FnHcLJ5MvvVaBc7n1v1wnzdqXpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.16.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@angular-devkit/build-angular/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -505,10 +586,11 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.1.tgz", - "integrity": "sha512-YFzn/+8LezX7ZJhMQisvrqfkxJm6+JOtbWFj8K/luK0rTDmE8Z9n9r6kJ36FnHcLJ5MvvVaBc7n1v1wnzdqXpg==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.3.tgz", + "integrity": "sha512-S0UzNNVLbHPaiSVXHjCd2wX+eERj/YR7jJCc40PHs1gINA7Gtd2q3VDm3bUEWe4P6fP6GNp43qSXmWJFQD0+Yg==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "8.16.0", "ajv-formats": "3.0.1", @@ -549,6 +631,34 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.1.tgz", + "integrity": "sha512-YFzn/+8LezX7ZJhMQisvrqfkxJm6+JOtbWFj8K/luK0rTDmE8Z9n9r6kJ36FnHcLJ5MvvVaBc7n1v1wnzdqXpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.16.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-eslint/builder": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.1.0.tgz", @@ -893,6 +1003,34 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.1.tgz", + "integrity": "sha512-YFzn/+8LezX7ZJhMQisvrqfkxJm6+JOtbWFj8K/luK0rTDmE8Z9n9r6kJ36FnHcLJ5MvvVaBc7n1v1wnzdqXpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.16.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular/cli/node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -1685,6 +1823,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz", "integrity": "sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -2144,13 +2283,14 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz", - "integrity": "sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.2.tgz", + "integrity": "sha512-InBZ0O8tew5V0K6cHcQ+wgxlrjOw1W4wDXLkOTjLRD8GYhTSkxTVBtdy3MMtvYBrbAWa1Qm3hNoTc1620Yj+Mg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/plugin-syntax-flow": "^7.24.7" }, "engines": { @@ -2922,6 +3062,7 @@ "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.24.7.tgz", "integrity": "sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -2993,6 +3134,7 @@ "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.24.6.tgz", "integrity": "sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "clone-deep": "^4.0.1", @@ -3013,6 +3155,7 @@ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "commondir": "^1.0.1", @@ -3028,6 +3171,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "locate-path": "^3.0.0" @@ -3041,6 +3185,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-locate": "^3.0.0", @@ -3055,6 +3200,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "pify": "^4.0.1", @@ -3069,6 +3215,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-try": "^2.0.0" @@ -3085,6 +3232,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-limit": "^2.0.0" @@ -3098,6 +3246,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=4" @@ -3108,6 +3257,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -3118,6 +3268,7 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "find-up": "^3.0.0" @@ -3131,6 +3282,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "peer": true, "bin": { "semver": "bin/semver" @@ -7792,6 +7944,34 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.1.tgz", + "integrity": "sha512-YFzn/+8LezX7ZJhMQisvrqfkxJm6+JOtbWFj8K/luK0rTDmE8Z9n9r6kJ36FnHcLJ5MvvVaBc7n1v1wnzdqXpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.16.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -8475,16 +8655,17 @@ } }, "node_modules/@storybook/codemod": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.5.tgz", - "integrity": "sha512-bUCvOqW3LUjz6epmTfocWBm0S7Ae52xmHvhVqgAUsKp9bVw2CGt9uaPR8dVE4IfI1yJZKRjf3u7Y60OTfWew4g==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.7.tgz", + "integrity": "sha512-D2sJcZMUO6Y7DNja4LvdT6uBee4bZbQKB904kEG9Kpr0XF20IHAP9BbkfG8HEFaS0GbJwvGvE03Sg+S1y+vO6Q==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.24.4", "@babel/preset-env": "^7.24.4", "@babel/types": "^7.24.0", - "@storybook/core": "8.2.5", + "@storybook/core": "8.2.7", "@storybook/csf": "0.1.11", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", @@ -8505,6 +8686,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", @@ -8526,6 +8708,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=12" @@ -8539,6 +8722,7 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=14.16" @@ -8561,10 +8745,11 @@ } }, "node_modules/@storybook/core": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.5.tgz", - "integrity": "sha512-KjaeIkbdcog4Jmx3MoSjQZpfESin1qHEcFiLoOkICOpuKsj37xdMFcuSre8IbcVGCJPkt1RvEmfeu1N90jOgww==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.7.tgz", + "integrity": "sha512-vgw5MYN9Bq2/ZsObCOEHbBHwi4RpbYCHPFtKkr4kTnWID++FCSiSVd7jY3xPvcNxWqCxOyH6dThpBi+SsB/ZAA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@storybook/csf": "0.1.11", @@ -8585,16 +8770,17 @@ } }, "node_modules/@storybook/core-server": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-8.2.5.tgz", - "integrity": "sha512-NjfAiKV0BIhxi8ByGMZ04iAkI+UkxvwKk2p2OepkHUcDZS+FOedx4jo420oKkBqppnYMJnEaTl7cUnCKPJtMMA==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-8.2.6.tgz", + "integrity": "sha512-L8wT5C9D33gk8Y6fV9Gak52V/pzm60+TXXFRW2+YYMyRwyjC1c/eDePlrRIu7jAJiEs9UmdxxUwM4R/iEhOHzg==", "dev": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.2.5" + "storybook": "^8.2.6" } }, "node_modules/@storybook/core-webpack": { @@ -8628,6 +8814,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "undici-types": "~5.26.4" @@ -9524,6 +9711,7 @@ "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@types/node": "*" @@ -10856,6 +11044,7 @@ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "tslib": "^2.0.1" @@ -10968,6 +11157,7 @@ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", "dev": true, + "license": "MIT", "peer": true, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -14444,10 +14634,11 @@ } }, "node_modules/esbuild-register": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", - "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "debug": "^4.3.4" @@ -15575,6 +15766,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/file-type": { "version": "17.1.6", "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", @@ -15765,10 +15962,11 @@ "dev": true }, "node_modules/flow-parser": { - "version": "0.241.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.241.0.tgz", - "integrity": "sha512-82yKXpz7iWknWFsognZUf5a6mBQLnVrYoYSU9Nbu7FTOpKlu3v9ehpiI9mYXuaIO3J0ojX1b83M/InXvld9HUw==", + "version": "0.242.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.242.1.tgz", + "integrity": "sha512-E3ml21Q1S5cMAyPbtYslkvI6yZO5oCS/S2EoteeFH8Kx9iKOv/YOJ+dGd/yMf+H3YKfhMKjnOpyNwrO7NdddWA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.4.0" @@ -18388,6 +18586,7 @@ "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.23.0", @@ -18428,6 +18627,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "color-convert": "^2.0.1" @@ -18444,6 +18644,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "ansi-styles": "^4.1.0", @@ -18461,6 +18662,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -18471,6 +18673,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "has-flag": "^4.0.0" @@ -21885,6 +22088,7 @@ "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "minimatch": "^3.0.2" @@ -21898,6 +22102,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "balanced-match": "^1.0.0", @@ -21909,6 +22114,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -24955,6 +25161,7 @@ "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "ast-types": "^0.16.1", @@ -24972,6 +25179,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -26548,16 +26756,17 @@ } }, "node_modules/storybook": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.5.tgz", - "integrity": "sha512-nfcly5CY3D6KuHbsfhScPaGeraRA9EJhO9GF00/dnI0GXW4ILS8Kwket515IkKAuKcdjdZis6maEuosbG//Kbg==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.7.tgz", + "integrity": "sha512-Jb9DXue1sr3tKkpuq66VP5ItOKTpxL6t99ze1wXDbjCvPiInTdPA5AyFEjBuKjOBIh28bayYoOZa6/xbMJV+Wg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.24.4", "@babel/types": "^7.24.0", - "@storybook/codemod": "8.2.5", - "@storybook/core": "8.2.5", + "@storybook/codemod": "8.2.7", + "@storybook/core": "8.2.7", "@types/semver": "^7.3.4", "@yarnpkg/fslib": "2.10.3", "@yarnpkg/libzip": "2.3.0", @@ -27391,10 +27600,11 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", - "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==", + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", + "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -27563,6 +27773,7 @@ "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "rimraf": "~2.6.2" @@ -27587,6 +27798,7 @@ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "glob": "^7.1.3" @@ -29577,6 +29789,7 @@ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "graceful-fs": "^4.1.11", @@ -29589,6 +29802,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, + "license": "ISC", "peer": true }, "node_modules/ws": { diff --git a/package.json b/package.json index d8a8168..070a7e6 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@onecx/portal-integration-angular": "^5.2.0", "@onecx/portal-layout-styles": "^5.2.0", "@webcomponents/webcomponentsjs": "^2.8.0", + "file-saver": "^2.0.5", "keycloak-angular": "^16.0.1", "ngx-color": "^9.0.0", "primeflex": "^3.3.1", @@ -75,7 +76,7 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.1", - "@angular-devkit/core": "^18.1.1", + "@angular-devkit/core": "^18.1.3", "@angular-devkit/schematics": "^18.1.1", "@angular-eslint/builder": "^18.1.0", "@angular-eslint/eslint-plugin": "^18.1.0", @@ -93,7 +94,7 @@ "@schematics/angular": "^18.1.1", "@storybook/addon-essentials": "8.2.5", "@storybook/angular": "8.2.5", - "@storybook/core-server": "8.2.5 ", + "@storybook/core-server": "8.2.6 ", "@svgr/webpack": "^8.1.0", "@swc-node/register": "^1.10.9", "@swc/cli": "~0.4.0", @@ -132,7 +133,7 @@ "sonar-scanner": "^3.1.0", "sonarqube-scanner": "^4.0.1", "style-loader": "^4.0.0", - "tailwindcss": "3.4.6", + "tailwindcss": "3.4.7", "ts-node": "10.9.2", "typescript": "5.5", "url-loader": "^4.1.1", diff --git a/src/_mixins.scss b/src/_mixins.scss index 30b89eb..813217d 100644 --- a/src/_mixins.scss +++ b/src/_mixins.scss @@ -140,3 +140,13 @@ } } } + +@mixin listbox-zebra-rows { + :host ::ng-deep { + .p-listbox:not(.p-disabled) .p-listbox-item:not(.p-highlight):not(.p-disabled) { + &:nth-child(odd) { + background-color: #f8f9fa; + } + } + } +} diff --git a/src/app/help/help-detail/help-detail.component.html b/src/app/help/help-detail/help-detail.component.html index e6eaa4f..0646926 100644 --- a/src/app/help/help-detail/help-detail.component.html +++ b/src/app/help/help-detail/help-detail.component.html @@ -26,6 +26,9 @@ [label]="'GENERAL.CANCEL' | translate" (onClick)="onDialogHide()" icon="pi pi-times" + [pTooltip]="'GENERAL.CANCEL' | translate" + tooltipPosition="top" + tooltipEvent="hover" > diff --git a/src/app/help/help-search/help-search.component.html b/src/app/help/help-search/help-search.component.html index 29cb231..3f6b7d0 100644 --- a/src/app/help/help-search/help-search.component.html +++ b/src/app/help/help-search/help-search.component.html @@ -196,6 +196,107 @@ + +
+ +
+ +
+ +
+
+
+ + +
+
{{ 'HELP_ITEM.APPLICATION_LIST' | translate }}
+ +
+ +
+ + +
+
+
+ { let component: HelpSearchComponent @@ -18,6 +24,8 @@ describe('HelpSearchComponent', () => { searchHelps: jasmine.createSpy('searchHelps').and.returnValue(of({})), addHelpItem: jasmine.createSpy('addHelpItem').and.returnValue(of({})), deleteHelp: jasmine.createSpy('deleteHelp').and.returnValue(of({})), + importHelps: jasmine.createSpy('importHelps').and.returnValue(of({})), + exportHelps: jasmine.createSpy('exportHelps').and.returnValue(of({})), searchProductsByCriteria: jasmine.createSpy('searchProductsByCriteria').and.returnValue(of({})) } const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['get']) @@ -65,10 +73,12 @@ describe('HelpSearchComponent', () => { apiServiceSpy.searchHelps.calls.reset() apiServiceSpy.addHelpItem.calls.reset() apiServiceSpy.deleteHelp.calls.reset() + apiServiceSpy.importHelps.calls.reset() + apiServiceSpy.exportHelps.calls.reset() apiServiceSpy.searchProductsByCriteria.calls.reset() }) - it('should create component and set columns', () => { + it('should create component and set columns for displaying results', () => { expect(component).toBeTruthy() expect(component.filteredColumns[0].field).toBe('productDisplayName') expect(component.filteredColumns[1].field).toBe('itemId') @@ -77,227 +87,250 @@ describe('HelpSearchComponent', () => { expect(component.filteredColumns[4].field).toBe('context') }) - it('should call search OnInit and populate filteredColumns/actions correctly', () => { - translateServiceSpy.get.and.returnValue(of({ 'ACTIONS.CREATE.LABEL': 'Create' })) - component.columns = [ - { - field: 'productName', - header: 'APPLICATION_NAME', - active: false - }, - { - field: 'context', - header: 'CONTEXT', - active: true - } - ] - spyOn(component, 'search') - - component.ngOnInit() - - expect(component.search).toHaveBeenCalled() - expect(component.filteredColumns[0].field).toEqual('context') - expect(component.actions[0].label).toEqual('ACTIONS.CREATE.LABEL') - }) - - it('should process products onInit', () => { - const helpPageResultMock = { - totalElements: 0, - number: 0, - size: 0, - totalPages: 0, - stream: [ + describe('ngOnInit', () => { + it('should call search OnInit and populate filteredColumns/actions correctly', () => { + translateServiceSpy.get.and.returnValue(of({ 'ACTIONS.CREATE.LABEL': 'Create' })) + component.columns = [ { - itemId: 'id', - productName: 'help-mgmt-ui' - } - ] - } - apiServiceSpy.searchProductsByCriteria.and.returnValue(of(helpPageResultMock)) - component.products = [ - { name: 'help-mgmt-ui', displayName: 'Help Mgmt UI' }, - { name: '2', displayName: '2dn' } - ] as Product[] - spyOn(component, 'search') - - component.ngOnInit() - - expect(component.productsLoaded).toBeTrue() - }) - - it('should correctly assign results if API call returns some data', () => { - const helpPageResultMock = { - totalElements: 0, - number: 0, - size: 0, - totalPages: 0, - stream: [ + field: 'productName', + header: 'APPLICATION_NAME', + active: false + }, { - itemId: 'id', - productName: 'help-mgmt-ui' + field: 'context', + header: 'CONTEXT', + active: true } ] - } - apiServiceSpy.searchHelps.and.returnValue(of(helpPageResultMock)) - component.products = [ - { name: 'help-mgmt-ui', displayName: 'Help Mgmt UI' }, - { name: '2', displayName: '2dn' } - ] as Product[] - component.resultsForDisplay = [] + spyOn(component, 'search') - component.search({}) + component.ngOnInit() - expect(component.resultsForDisplay[0].productDisplayName).toEqual('Help Mgmt UI') - expect(component.resultsForDisplay[0].itemId).toEqual('id') - }) - - it('should handle empty results on search', () => { - const helpPageResultMock = { - totalElements: 0, - number: 0, - size: 0, - totalPages: 0, - stream: [] - } - apiServiceSpy.searchHelps.and.returnValue(of(helpPageResultMock)) - apiServiceSpy.searchProductsByCriteria.and.returnValue(of(helpPageResultMock)) - component.resultsForDisplay = [] - - component.search({}) - - expect(component.resultsForDisplay.length).toEqual(0) - expect(msgServiceSpy.info).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.MSG_NO_RESULTS' }) - }) + expect(component.search).toHaveBeenCalled() + expect(component.filteredColumns[0].field).toEqual('context') + expect(component.actions[0].label).toEqual('ACTIONS.CREATE.LABEL') + }) - it('should reuse criteria if reuseCriteria is true', () => { - apiServiceSpy.searchHelps.and.returnValue(of([])) - component.criteria = { - helpSearchCriteria: { - productName: 'help-mgmt-ui', - itemId: 'id' - } - } - const newCriteria = { - helpSearchCriteria: { - productName: 'ap-mgmt', - itemId: 'newId' + it('should process products onInit', () => { + const helpPageResultMock = { + totalElements: 0, + number: 0, + size: 0, + totalPages: 0, + stream: [ + { + itemId: 'id', + productName: 'help-mgmt-ui' + } + ] } - } + apiServiceSpy.searchProductsByCriteria.and.returnValue(of(helpPageResultMock)) + component.products = [ + { name: 'help-mgmt-ui', displayName: 'Help Mgmt UI' }, + { name: '2', displayName: '2dn' } + ] as Product[] + spyOn(component, 'search') - const reuseCriteria = true + component.ngOnInit() - component.search(component.criteria.helpSearchCriteria, reuseCriteria) - - expect(component.criteria).not.toBe(newCriteria) + expect(component.productsLoaded).toBeTrue() + }) }) - describe('searchHelps Error', () => { - it('should handle 401 Exception result on search', () => { - const helpPageResultMock: HttpErrorResponse = { - status: 401, - statusText: 'Not Found', - name: 'HttpErrorResponse', - message: '', - error: undefined, - ok: false, - headers: new HttpHeaders(), - url: null, - type: HttpEventType.ResponseHeader + describe('search', () => { + it('should correctly assign results if API call returns some data', () => { + const helpPageResultMock = { + totalElements: 0, + number: 0, + size: 0, + totalPages: 0, + stream: [ + { + itemId: 'id', + productName: 'help-mgmt-ui' + } + ] } - apiServiceSpy.searchHelps.and.returnValue(throwError(() => helpPageResultMock)) + apiServiceSpy.searchHelps.and.returnValue(of(helpPageResultMock)) + component.products = [ + { name: 'help-mgmt-ui', displayName: 'Help Mgmt UI' }, + { name: '2', displayName: '2dn' } + ] as Product[] component.resultsForDisplay = [] component.search({}) - expect(component.exceptionKey).toBeDefined() - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_401.HELP_ITEM') - expect(msgServiceSpy.info).toHaveBeenCalledWith({ summaryKey: 'HELP_SEARCH.NO_APPLICATION_AVAILABLE' }) + expect(component.resultsForDisplay[0].productDisplayName).toEqual('Help Mgmt UI') + expect(component.resultsForDisplay[0].itemId).toEqual('id') }) - it('should handle 403 Exception result on search', () => { - const helpPageResultMock: HttpErrorResponse = { - status: 403, - statusText: 'Not Found', - name: 'HttpErrorResponse', - message: '', - error: undefined, - ok: false, - headers: new HttpHeaders(), - url: null, - type: HttpEventType.ResponseHeader + it('should handle empty results on search', () => { + const helpPageResultMock = { + totalElements: 0, + number: 0, + size: 0, + totalPages: 0, + stream: [] } - apiServiceSpy.searchHelps.and.returnValue(throwError(() => helpPageResultMock)) + apiServiceSpy.searchHelps.and.returnValue(of(helpPageResultMock)) + apiServiceSpy.searchProductsByCriteria.and.returnValue(of(helpPageResultMock)) component.resultsForDisplay = [] component.search({}) - expect(component.exceptionKey).toBeDefined() - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_403.HELP_ITEM') - expect(msgServiceSpy.info).toHaveBeenCalledWith({ summaryKey: 'HELP_SEARCH.NO_APPLICATION_AVAILABLE' }) + expect(component.resultsForDisplay.length).toEqual(0) + expect(msgServiceSpy.info).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.MSG_NO_RESULTS' }) }) - it('should handle 404 Exception result on search', () => { - const helpPageResultMock: HttpErrorResponse = { - status: 404, - statusText: 'Not Found', - name: 'HttpErrorResponse', - message: '', - error: undefined, - ok: false, - headers: new HttpHeaders(), - url: null, - type: HttpEventType.ResponseHeader + it('should reuse criteria if reuseCriteria is true', () => { + apiServiceSpy.searchHelps.and.returnValue(of([])) + component.criteria = { + helpSearchCriteria: { + productName: 'help-mgmt-ui', + itemId: 'id' + } } - apiServiceSpy.searchHelps.and.returnValue(throwError(() => helpPageResultMock)) - component.resultsForDisplay = [] + const newCriteria = { + helpSearchCriteria: { + productName: 'ap-mgmt', + itemId: 'newId' + } + } + const reuseCriteria = true - component.search({}) + component.search(component.criteria.helpSearchCriteria, reuseCriteria) - expect(component.exceptionKey).toBeDefined() - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_404.HELP_ITEM') - expect(msgServiceSpy.info).toHaveBeenCalledWith({ summaryKey: 'HELP_SEARCH.NO_APPLICATION_AVAILABLE' }) + expect(component.criteria).not.toBe(newCriteria) }) - }) - it('should handle API call error', () => { - apiServiceSpy.searchHelps.and.returnValue(throwError(() => new Error())) + describe('searchHelps Error', () => { + it('should handle 401 Exception result on search', () => { + const helpPageResultMock: HttpErrorResponse = { + status: 401, + statusText: 'Not Found', + name: 'HttpErrorResponse', + message: '', + error: undefined, + ok: false, + headers: new HttpHeaders(), + url: null, + type: HttpEventType.ResponseHeader + } + apiServiceSpy.searchHelps.and.returnValue(throwError(() => helpPageResultMock)) + component.resultsForDisplay = [] + + component.search({}) + + expect(component.exceptionKey).toBeDefined() + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_401.HELP_ITEM') + expect(msgServiceSpy.info).toHaveBeenCalledWith({ summaryKey: 'HELP_SEARCH.NO_APPLICATION_AVAILABLE' }) + }) + + it('should handle 403 Exception result on search', () => { + const helpPageResultMock: HttpErrorResponse = { + status: 403, + statusText: 'Not Found', + name: 'HttpErrorResponse', + message: '', + error: undefined, + ok: false, + headers: new HttpHeaders(), + url: null, + type: HttpEventType.ResponseHeader + } + apiServiceSpy.searchHelps.and.returnValue(throwError(() => helpPageResultMock)) + component.resultsForDisplay = [] + + component.search({}) + + expect(component.exceptionKey).toBeDefined() + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_403.HELP_ITEM') + expect(msgServiceSpy.info).toHaveBeenCalledWith({ summaryKey: 'HELP_SEARCH.NO_APPLICATION_AVAILABLE' }) + }) + + it('should handle 404 Exception result on search', () => { + const helpPageResultMock: HttpErrorResponse = { + status: 404, + statusText: 'Not Found', + name: 'HttpErrorResponse', + message: '', + error: undefined, + ok: false, + headers: new HttpHeaders(), + url: null, + type: HttpEventType.ResponseHeader + } + apiServiceSpy.searchHelps.and.returnValue(throwError(() => helpPageResultMock)) + component.resultsForDisplay = [] - component.search({}) + component.search({}) - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.MSG_SEARCH_FAILED' }) - }) + expect(component.exceptionKey).toBeDefined() + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_404.HELP_ITEM') + expect(msgServiceSpy.info).toHaveBeenCalledWith({ summaryKey: 'HELP_SEARCH.NO_APPLICATION_AVAILABLE' }) + }) - it('should delete help item', () => { - apiServiceSpy.deleteHelp({ productName: newHelpItemArr[0].productName, itemId: newHelpItemArr[0].id }) - component.resultsForDisplay = newHelpItemArr - component.helpItem = { - id: newHelpItemArr[0].id, - productName: newHelpItemArr[0].productName, - itemId: newHelpItemArr[0].itemId - } + it('should handle API call error', () => { + apiServiceSpy.searchHelps.and.returnValue(throwError(() => new Error())) + + component.search({}) - component.onDeleteConfirmation() + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.MSG_SEARCH_FAILED' }) + }) + }) - expect(apiServiceSpy.deleteHelp).toHaveBeenCalled() - expect(component.resultsForDisplay.length).toBe(0) - expect(component.resultsForDisplay.length).toBe(0) - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGE.HELP_ITEM_OK' }) + it('should set productName and itemId as undefined if criteria strings empty', () => { + const criteria: SearchHelpsRequestParams = { + helpSearchCriteria: { + productName: '', + itemId: '' + } + } + const reuseCriteria = false + + component.search(criteria.helpSearchCriteria, reuseCriteria) + + expect(component.criteria.helpSearchCriteria.productName).not.toBeDefined() + expect(component.criteria.helpSearchCriteria.itemId).not.toBeDefined() + }) }) - it('should display error on deleteHelpItem failure', () => { - apiServiceSpy.deleteHelp.and.returnValue(throwError(() => new Error())) - component.resultsForDisplay = newHelpItemArr - component.helpItem = { - id: newHelpItemArr[0].id, - productName: newHelpItemArr[0].productName, - itemId: newHelpItemArr[0].itemId - } + describe('onDeleteConfirmation', () => { + it('should delete help item', () => { + apiServiceSpy.deleteHelp({ productName: newHelpItemArr[0].productName, itemId: newHelpItemArr[0].id }) + component.resultsForDisplay = newHelpItemArr + component.helpItem = { + id: newHelpItemArr[0].id, + productName: newHelpItemArr[0].productName, + itemId: newHelpItemArr[0].itemId + } + + component.onDeleteConfirmation() + + expect(apiServiceSpy.deleteHelp).toHaveBeenCalled() + expect(component.resultsForDisplay.length).toBe(0) + expect(component.resultsForDisplay.length).toBe(0) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGE.HELP_ITEM_OK' }) + }) + + it('should display error on deleteHelpItem failure', () => { + apiServiceSpy.deleteHelp.and.returnValue(throwError(() => new Error())) + component.resultsForDisplay = newHelpItemArr + component.helpItem = { + id: newHelpItemArr[0].id, + productName: newHelpItemArr[0].productName, + itemId: newHelpItemArr[0].itemId + } - component.onDeleteConfirmation() + component.onDeleteConfirmation() - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGE.HELP_ITEM_NOK' }) + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGE.HELP_ITEM_NOK' }) + }) }) + /* + * UI ACTIONS + */ it('should set correct values onSearch', () => { spyOn(component, 'search') component.onSearch() @@ -352,79 +385,6 @@ describe('HelpSearchComponent', () => { expect(component.displayDeleteDialog).toBeTrue() }) - it('should correctly sort productNames using sortHelpItemsByDefault 1', () => { - component.resultsForDisplay = [ - { productName: 'B', itemId: '2' }, - { productName: 'A', itemId: '1' } - ] - component.resultsForDisplay.sort(component['sortHelpItemByDefault']) - - expect(component.resultsForDisplay).toEqual([ - { productName: 'A', itemId: '1' }, - { productName: 'B', itemId: '2' } - ]) - }) - - it('should correctly sort productNames using sortHelpItemsByDefault 2', () => { - component.resultsForDisplay = [ - { productName: '', itemId: '1' }, - { productName: 'A', itemId: '2' } - ] - component.resultsForDisplay.sort(component['sortHelpItemByDefault']) - - expect(component.resultsForDisplay).toEqual([ - { productName: '', itemId: '1' }, - { productName: 'A', itemId: '2' } - ]) - }) - - it('should correctly sort productNames using sortHelpItemsByDefault 3', () => { - component.resultsForDisplay = [ - { productName: 'A', itemId: '2' }, - { productName: '', itemId: '1' } - ] - component.resultsForDisplay.sort(component['sortHelpItemByDefault']) - - expect(component.resultsForDisplay).toEqual([ - { productName: '', itemId: '1' }, - { productName: 'A', itemId: '2' } - ]) - }) - - it('should correctly sort productNames using sortHelpItemsByDefault 4', () => { - component.resultsForDisplay = [ - { productName: 'A', itemId: '' }, - { productName: 'A', itemId: '2' }, - { productName: 'A', itemId: '1' }, - { productName: '', itemId: '1' }, - { productName: '', itemId: '2' }, - { productName: '', itemId: '' } - ] - component.resultsForDisplay.sort(component['sortHelpItemByDefault']) - - expect(component.resultsForDisplay).toEqual([ - { productName: '', itemId: '' }, - { productName: '', itemId: '1' }, - { productName: '', itemId: '2' }, - { productName: 'A', itemId: '' }, - { productName: 'A', itemId: '1' }, - { productName: 'A', itemId: '2' } - ]) - }) - - it('should correctly sort itemIds using sortHelpItemsByDefault', () => { - component.resultsForDisplay = [ - { productName: 'A', itemId: '2' }, - { productName: 'A', itemId: '1' } - ] - component.resultsForDisplay.sort(component['sortHelpItemByDefault']) - - expect(component.resultsForDisplay).toEqual([ - { productName: 'A', itemId: '1' }, - { productName: 'A', itemId: '2' } - ]) - }) - it('should update filteredColumns onColumnsChange', () => { const columns: Column[] = [ { @@ -452,7 +412,6 @@ describe('HelpSearchComponent', () => { }) it('should call onCreate when actionCallback is executed', () => { - translateServiceSpy.get.and.returnValue(of({ 'ACTIONS.CREATE.LABEL': 'Create' })) spyOn(component, 'onCreate') component.ngOnInit() @@ -462,19 +421,257 @@ describe('HelpSearchComponent', () => { expect(component.onCreate).toHaveBeenCalled() }) - it('should set productName and itemId as undefined if criteria strings empty', () => { - const criteria: SearchHelpsRequestParams = { - helpSearchCriteria: { - productName: '', - itemId: '' + it('should call onImport when actionCallback is executed', () => { + spyOn(component, 'onImport') + component.ngOnInit() + + const action = component.actions[2] + action.actionCallback() + + expect(component.onImport).toHaveBeenCalled() + }) + + it('should call onExport when actionCallback is executed', () => { + spyOn(component, 'onExport') + component.ngOnInit() + + const action = component.actions[1] + action.actionCallback() + + expect(component.onExport).toHaveBeenCalled() + }) + + /* + * IMPORT + */ + it('should display import dialog when import button is clicked', () => { + component.displayImportDialog = false + + component.onImport() + + expect(component.displayImportDialog).toBeTrue() + }) + + describe('onSelect', () => { + let file: File + let event: any = {} + + beforeEach(() => { + translateServiceSpy.get.and.returnValue(of({})) + file = new File(['file content'], 'test.txt', { type: 'text/plain' }) + const fileList: FileList = { + 0: file, + length: 1, + item: (index: number) => file } - } - const reuseCriteria = false + event = { files: fileList } + }) + + it('should select a file to upload', (done) => { + spyOn(file, 'text').and.returnValue(Promise.resolve('{ "itemId": "id", "productName": "onecx-help" }')) + + component.onSelect(event as any as FileSelectEvent) + + setTimeout(() => { + expect(file.text).toHaveBeenCalled() + expect(component.importHelpItem).toEqual(helpItem) + done() + }) + }) - component.search(criteria.helpSearchCriteria, reuseCriteria) + it('should handle JSON parse error', (done) => { + spyOn(file, 'text').and.returnValue(Promise.resolve('Invalid Json')) + spyOn(console, 'error') + + component.onSelect(event) + + setTimeout(() => { + expect(console.error).toHaveBeenCalled() + expect(component.importError).toBeTrue() + expect(component.validationErrorCause).toBe('') + done() + }) + }) + }) + + it('should reset errors when clear button is clicked', () => { + component.importError = true + component.validationErrorCause = 'Some error' + + component.onClear() + + expect(component.importError).toBeFalse() + expect(component.validationErrorCause).toBe('') + }) + + describe('onImportConfirmation', () => { + it('should import help items', (done) => { + apiServiceSpy.importHelps.and.returnValue(of({})) + component.importHelpItem = helpItem + + component.onImportConfirmation() + + setTimeout(() => { + expect(component.displayImportDialog).toBeFalse() + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.IMPORT.MESSAGE.HELP_ITEM.IMPORT_OK' }) + done() + }) + }) + + it('should call importHelps and handle error', (done) => { + apiServiceSpy.importHelps.and.returnValue(throwError(() => 'Error')) + component.importHelpItem = helpItem + + component.onImportConfirmation() + + setTimeout(() => { + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.IMPORT.MESSAGE.HELP_ITEM.IMPORT_NOK' }) + done() + }, 0) + }) + + it('should not call importHelps if importHelpItem is not defined', () => { + component.importHelpItem = null + + component.onImportConfirmation() + + expect(apiServiceSpy.importHelps).not.toHaveBeenCalled() + }) + }) - expect(component.criteria.helpSearchCriteria.productName).not.toBeDefined() - expect(component.criteria.helpSearchCriteria.itemId).not.toBeDefined() + it('should validate a file', () => { + component.importError = false + + expect(component.isFileValid()).toBeTrue() + }) + + it('should close displayImportDialog', () => { + component.displayImportDialog = true + + component.onCloseImportDialog() + + expect(component.displayImportDialog).toBeFalse() + }) + + /* + * EXPORT + */ + it('should display export dialog', () => { + component.onExport() + + expect(component.displayExportDialog).toBeTrue() + }) + + describe('onExportConfirmation', () => { + it('should export help items', () => { + apiServiceSpy.exportHelps.and.returnValue(of(helpItem)) + const selectedResults = [{ productName: 'Product1' }, { productName: 'Product2' }] + component.selectedResults = selectedResults as HelpForDisplay[] + + component.onExportConfirmation() + + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EXPORT.MESSAGE.HELP_ITEM.EXPORT_OK' }) + }) + + it('should display error msg when export fails', () => { + apiServiceSpy.exportHelps.and.returnValue(throwError(() => 'Error')) + const selectedResults = [{ productName: 'Product1' }, { productName: 'Product2' }] + component.selectedResults = selectedResults as HelpForDisplay[] + + component.onExportConfirmation() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EXPORT.MESSAGE.HELP_ITEM.EXPORT_NOK' }) + }) + }) + + it('should reset displayExportDialog, selectedResults, and selectedProductNames', () => { + component.displayExportDialog = true + component.selectedResults = [{ productName: 'Product1' }] as HelpForDisplay[] + component.selectedProductNames = ['Product1'] + + component.onCloseExportDialog() + + expect(component.displayExportDialog).toBeFalse() + expect(component.selectedResults).toEqual([]) + expect(component.selectedProductNames).toEqual([]) + }) + + /* + * SORTING + */ + describe('sortHelpItemsByDefault', () => { + it('should correctly sort productNames using sortHelpItemsByDefault 1', () => { + component.resultsForDisplay = [ + { productName: 'B', itemId: '2' }, + { productName: 'A', itemId: '1' } + ] + component.resultsForDisplay.sort(component['sortHelpItemByDefault']) + + expect(component.resultsForDisplay).toEqual([ + { productName: 'A', itemId: '1' }, + { productName: 'B', itemId: '2' } + ]) + }) + + it('should correctly sort productNames using sortHelpItemsByDefault 2', () => { + component.resultsForDisplay = [ + { productName: '', itemId: '1' }, + { productName: 'A', itemId: '2' } + ] + component.resultsForDisplay.sort(component['sortHelpItemByDefault']) + + expect(component.resultsForDisplay).toEqual([ + { productName: '', itemId: '1' }, + { productName: 'A', itemId: '2' } + ]) + }) + + it('should correctly sort productNames using sortHelpItemsByDefault 3', () => { + component.resultsForDisplay = [ + { productName: 'A', itemId: '2' }, + { productName: '', itemId: '1' } + ] + component.resultsForDisplay.sort(component['sortHelpItemByDefault']) + + expect(component.resultsForDisplay).toEqual([ + { productName: '', itemId: '1' }, + { productName: 'A', itemId: '2' } + ]) + }) + + it('should correctly sort productNames using sortHelpItemsByDefault 4', () => { + component.resultsForDisplay = [ + { productName: 'A', itemId: '' }, + { productName: 'A', itemId: '2' }, + { productName: 'A', itemId: '1' }, + { productName: '', itemId: '1' }, + { productName: '', itemId: '2' }, + { productName: '', itemId: '' } + ] + component.resultsForDisplay.sort(component['sortHelpItemByDefault']) + + expect(component.resultsForDisplay).toEqual([ + { productName: '', itemId: '' }, + { productName: '', itemId: '1' }, + { productName: '', itemId: '2' }, + { productName: 'A', itemId: '' }, + { productName: 'A', itemId: '1' }, + { productName: 'A', itemId: '2' } + ]) + }) + + it('should correctly sort itemIds using sortHelpItemsByDefault', () => { + component.resultsForDisplay = [ + { productName: 'A', itemId: '2' }, + { productName: 'A', itemId: '1' } + ] + component.resultsForDisplay.sort(component['sortHelpItemByDefault']) + + expect(component.resultsForDisplay).toEqual([ + { productName: 'A', itemId: '1' }, + { productName: 'A', itemId: '2' } + ]) + }) }) it('should sort products', () => { diff --git a/src/app/help/help-search/help-search.component.ts b/src/app/help/help-search/help-search.component.ts index 4de99c6..3be4561 100644 --- a/src/app/help/help-search/help-search.component.ts +++ b/src/app/help/help-search/help-search.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit, ViewChild } from '@angular/core' import { TranslateService } from '@ngx-translate/core' -import { catchError, finalize, Observable, of } from 'rxjs' +import { catchError, finalize, of } from 'rxjs' import { Table } from 'primeng/table' +import FileSaver from 'file-saver' import { Action, Column, PortalMessageService } from '@onecx/portal-integration-angular' import { @@ -14,10 +15,11 @@ import { Product, HelpPageResult } from 'src/app/shared/generated' +import { FileSelectEvent } from 'primeng/fileupload' type ExtendedColumn = Column & { css?: string; limit?: boolean } type ChangeMode = 'VIEW' | 'NEW' | 'EDIT' -type HelpForDisplay = Help & { productDisplayName?: string; product?: { name?: string; displayName?: string } } +export type HelpForDisplay = Help & { productDisplayName?: string; product?: { name?: string; displayName?: string } } @Component({ selector: 'app-help-search', @@ -42,10 +44,17 @@ export class HelpSearchComponent implements OnInit { public loadingResults = false public displayDeleteDialog = false public displayDetailDialog = false + public displayImportDialog = false + public displayExportDialog = false public productsChanged = false public rowsPerPage = 10 public rowsPerPageOptions = [10, 20, 50] - public items$!: Observable + + importHelpItem: Help | null = null + public importError = false + public validationErrorCause: string + public selectedResults: HelpForDisplay[] | undefined + public selectedProductNames: string[] | undefined public filteredColumns: Column[] = [] public columns: ExtendedColumn[] = [ @@ -72,7 +81,9 @@ export class HelpSearchComponent implements OnInit { private helpInternalAPIService: HelpsInternalAPIService, private translate: TranslateService, private msgService: PortalMessageService - ) {} + ) { + this.validationErrorCause = '' + } ngOnInit(): void { this.filteredColumns = this.columns.filter((a) => { @@ -123,7 +134,7 @@ export class HelpSearchComponent implements OnInit { .pipe( catchError((err) => { this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.HELP_ITEM' - console.error('searchSlots():', err) + console.error('searchHelps():', err) this.msgService.error({ summaryKey: 'ACTIONS.SEARCH.MSG_SEARCH_FAILED' }) return of({ stream: [] } as HelpPageResult) }), @@ -222,16 +233,137 @@ export class HelpSearchComponent implements OnInit { } } - private prepareDialogTranslations() { - this.translate.get(['ACTIONS.CREATE.LABEL', 'ACTIONS.CREATE.HELP_ITEM.TOOLTIP']).subscribe((data) => { - this.actions.push({ - label: data['ACTIONS.CREATE.LABEL'], - title: data['ACTIONS.CREATE.HELP_ITEM.TOOLTIP'], - actionCallback: () => this.onCreate(), - icon: 'pi pi-plus', - show: 'always', - permission: 'HELP#EDIT' + /**************************************************************************** + * IMPORT + */ + public onImport(): void { + this.displayImportDialog = true + } + public onSelect(event: FileSelectEvent): void { + event.files[0].text().then((text) => { + this.importError = false + this.validationErrorCause = '' + + this.translate.get(['IMPORT.VALIDATION_RESULT']).subscribe((data) => { + try { + const importHelp = JSON.parse(text) + this.importHelpItem = importHelp + } catch (err) { + console.error('Import Error', err) + this.importError = true + } }) }) } + public onImportConfirmation(): void { + if (this.importHelpItem) { + this.helpInternalAPIService.importHelps({ body: this.importHelpItem }).subscribe({ + next: () => { + this.displayImportDialog = false + this.productsChanged = true + this.msgService.success({ summaryKey: 'ACTIONS.IMPORT.MESSAGE.HELP_ITEM.IMPORT_OK' }) + }, + error: () => this.msgService.error({ summaryKey: 'ACTIONS.IMPORT.MESSAGE.HELP_ITEM.IMPORT_NOK' }) + }) + this.loadData() + } + } + public isFileValid(): boolean { + return !this.importError + } + public onCloseImportDialog(): void { + this.displayImportDialog = false + } + public onClear(): void { + this.importError = false + this.validationErrorCause = '' + } + + /**************************************************************************** + * EXPORT + */ + public onExport(): void { + this.displayExportDialog = true + } + public onExportConfirmation(): void { + if (this.selectedResults && this.selectedResults.length > 0) { + this.selectedProductNames = this.selectedResults.map((item) => item.productName!) + this.helpInternalAPIService + .exportHelps({ exportHelpsRequest: { productNames: this.selectedProductNames } }) + .subscribe({ + next: (item) => { + const helpsJson = JSON.stringify(item, null, 2) + FileSaver.saveAs( + new Blob([helpsJson], { type: 'text/json' }), + 'onecx-help-items_' + this.getCurrentDateTime() + '.json' + ) + this.msgService.success({ summaryKey: 'ACTIONS.EXPORT.MESSAGE.HELP_ITEM.EXPORT_OK' }) + this.displayExportDialog = false + this.selectedResults = [] + this.selectedProductNames = [] + }, + error: (err) => { + this.msgService.error({ summaryKey: 'ACTIONS.EXPORT.MESSAGE.HELP_ITEM.EXPORT_NOK' }) + console.error(err) + } + }) + } + } + public onCloseExportDialog(): void { + this.displayExportDialog = false + this.selectedResults = [] + this.selectedProductNames = [] + } + + private prepareDialogTranslations() { + this.translate + .get([ + 'ACTIONS.CREATE.LABEL', + 'ACTIONS.CREATE.HELP_ITEM.TOOLTIP', + 'ACTIONS.IMPORT.LABEL', + 'ACTIONS.IMPORT.HELP_ITEM.TOOLTIP', + 'ACTIONS.EXPORT.LABEL', + 'ACTIONS.EXPORT.HELP_ITEM.TOOLTIP' + ]) + .subscribe((data) => { + this.actions.push( + { + label: data['ACTIONS.CREATE.LABEL'], + title: data['ACTIONS.CREATE.HELP_ITEM.TOOLTIP'], + actionCallback: () => this.onCreate(), + icon: 'pi pi-plus', + show: 'always', + permission: 'HELP#EDIT' + }, + { + label: data['ACTIONS.EXPORT.LABEL'], + title: data['ACTIONS.EXPORT.HELP_ITEM.TOOLTIP'], + actionCallback: () => this.onExport(), + icon: 'pi pi-download', + show: 'always', + permission: 'HELP#EDIT' + }, + { + label: data['ACTIONS.IMPORT.LABEL'], + title: data['ACTIONS.IMPORT.HELP_ITEM.TOOLTIP'], + actionCallback: () => this.onImport(), + icon: 'pi pi-upload', + show: 'always', + permission: 'HELP#EDIT' + } + ) + }) + } + + private getCurrentDateTime(): string { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const hours = String(now.getHours()).padStart(2, '0') + const minutes = String(now.getMinutes()).padStart(2, '0') + const seconds = String(now.getSeconds()).padStart(2, '0') + + return `${year}-${month}-${day}_${hours}${minutes}${seconds}` + } } diff --git a/src/app/remotes/help-item-editor/help-item-editor.component.ts b/src/app/remotes/help-item-editor/help-item-editor.component.ts index b402e38..07ff734 100644 --- a/src/app/remotes/help-item-editor/help-item-editor.component.ts +++ b/src/app/remotes/help-item-editor/help-item-editor.component.ts @@ -206,6 +206,7 @@ export class OneCXHelpItemEditorComponent implements ocxRemoteComponent, ocxRemo return this.editHelpPage({}) } + // eslint-disable-next-line @typescript-eslint/no-explicit-any public editHelpPage(event: any) { combineLatest([this.helpArticleId$, this.productName$, this.helpDataItem$, this.products$]) .pipe( diff --git a/src/app/remotes/show-help/show-help.component.ts b/src/app/remotes/show-help/show-help.component.ts index 9a6c40c..fbf405a 100644 --- a/src/app/remotes/show-help/show-help.component.ts +++ b/src/app/remotes/show-help/show-help.component.ts @@ -136,6 +136,7 @@ export class OneCXShowHelpComponent implements ocxRemoteComponent, ocxRemoteWebc return this.openHelpPage({}) } + // eslint-disable-next-line @typescript-eslint/no-explicit-any public openHelpPage(event: any) { this.helpDataItem$?.pipe(withLatestFrom(this.helpArticleId$), first()).subscribe({ next: ([helpDataItem, helpArticleId]) => { diff --git a/src/app/shared/generated/.openapi-generator/FILES b/src/app/shared/generated/.openapi-generator/FILES index 6b909a0..e7a2e6f 100644 --- a/src/app/shared/generated/.openapi-generator/FILES +++ b/src/app/shared/generated/.openapi-generator/FILES @@ -9,6 +9,7 @@ encoder.ts git_push.sh index.ts model/createHelp.ts +model/exportHelpsRequest.ts model/help.ts model/helpPageResult.ts model/helpProductNames.ts diff --git a/src/app/shared/generated/api/helpsInternal.service.ts b/src/app/shared/generated/api/helpsInternal.service.ts index 5dd9f06..78a85f2 100644 --- a/src/app/shared/generated/api/helpsInternal.service.ts +++ b/src/app/shared/generated/api/helpsInternal.service.ts @@ -21,6 +21,8 @@ import { Observable } from 'rxjs'; // @ts-ignore import { CreateHelp } from '../model/createHelp'; // @ts-ignore +import { ExportHelpsRequest } from '../model/exportHelpsRequest'; +// @ts-ignore import { Help } from '../model/help'; // @ts-ignore import { HelpPageResult } from '../model/helpPageResult'; @@ -50,10 +52,25 @@ export interface DeleteHelpRequestParams { id: string; } +export interface ExportHelpsRequestParams { + exportHelpsRequest: ExportHelpsRequest; +} + export interface GetHelpByIdRequestParams { id: string; } +export interface GetHelpByProductNameItemIdRequestParams { + /** product name */ + productName: string; + /** help item ID */ + helpItemId: string; +} + +export interface ImportHelpsRequestParams { + body: object; +} + export interface SearchHelpsRequestParams { helpSearchCriteria: HelpSearchCriteria; } @@ -259,6 +276,75 @@ export class HelpsInternalAPIService { ); } + /** + * Export helps + * @param requestParameters + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public exportHelps(requestParameters: ExportHelpsRequestParams, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public exportHelps(requestParameters: ExportHelpsRequestParams, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public exportHelps(requestParameters: ExportHelpsRequestParams, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public exportHelps(requestParameters: ExportHelpsRequestParams, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + const exportHelpsRequest = requestParameters.exportHelpsRequest; + if (exportHelpsRequest === null || exportHelpsRequest === undefined) { + throw new Error('Required parameter exportHelpsRequest was null or undefined when calling exportHelps.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/helps/export`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: exportHelpsRequest, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * Get all product names to which help items are assigned * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. @@ -372,6 +458,138 @@ export class HelpsInternalAPIService { ); } + /** + * search help item by product name and item id + * @param requestParameters + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getHelpByProductNameItemId(requestParameters: GetHelpByProductNameItemIdRequestParams, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public getHelpByProductNameItemId(requestParameters: GetHelpByProductNameItemIdRequestParams, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public getHelpByProductNameItemId(requestParameters: GetHelpByProductNameItemIdRequestParams, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public getHelpByProductNameItemId(requestParameters: GetHelpByProductNameItemIdRequestParams, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + const productName = requestParameters.productName; + if (productName === null || productName === undefined) { + throw new Error('Required parameter productName was null or undefined when calling getHelpByProductNameItemId.'); + } + const helpItemId = requestParameters.helpItemId; + if (helpItemId === null || helpItemId === undefined) { + throw new Error('Required parameter helpItemId was null or undefined when calling getHelpByProductNameItemId.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/helps/${this.configuration.encodeParam({name: "productName", value: productName, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/${this.configuration.encodeParam({name: "helpItemId", value: helpItemId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + + /** + * Import helps + * @param requestParameters + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public importHelps(requestParameters: ImportHelpsRequestParams, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public importHelps(requestParameters: ImportHelpsRequestParams, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public importHelps(requestParameters: ImportHelpsRequestParams, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public importHelps(requestParameters: ImportHelpsRequestParams, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + const body = requestParameters.body; + if (body === null || body === undefined) { + throw new Error('Required parameter body was null or undefined when calling importHelps.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/helps/import`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: body, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * Search for helps * @param requestParameters diff --git a/src/app/shared/generated/model/exportHelpsRequest.ts b/src/app/shared/generated/model/exportHelpsRequest.ts new file mode 100644 index 0000000..fed5090 --- /dev/null +++ b/src/app/shared/generated/model/exportHelpsRequest.ts @@ -0,0 +1,17 @@ +/** + * onecx-help bff + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ExportHelpsRequest { + productNames?: Array; +} + diff --git a/src/app/shared/generated/model/help.ts b/src/app/shared/generated/model/help.ts index 35035b0..77ece87 100644 --- a/src/app/shared/generated/model/help.ts +++ b/src/app/shared/generated/model/help.ts @@ -12,6 +12,7 @@ export interface Help { + operator?: boolean; modificationCount?: number; creationDate?: string; creationUser?: string; diff --git a/src/app/shared/generated/model/models.ts b/src/app/shared/generated/model/models.ts index e85c1d6..6550c68 100644 --- a/src/app/shared/generated/model/models.ts +++ b/src/app/shared/generated/model/models.ts @@ -1,4 +1,5 @@ export * from './createHelp'; +export * from './exportHelpsRequest'; export * from './help'; export * from './helpPageResult'; export * from './helpProductNames'; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index ef9a6cb..417bf0e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -11,6 +11,7 @@ import { ConfirmationService } from 'primeng/api' import { DataViewModule } from 'primeng/dataview' import { DialogModule } from 'primeng/dialog' import { DropdownModule } from 'primeng/dropdown' +import { FileUploadModule } from 'primeng/fileupload' import { InputTextModule } from 'primeng/inputtext' import { InputTextareaModule } from 'primeng/inputtextarea' import { KeyFilterModule } from 'primeng/keyfilter' @@ -35,6 +36,7 @@ import { LabelResolver } from './label.resolver' DataViewModule, DialogModule, DropdownModule, + FileUploadModule, FormsModule, InputTextModule, InputTextareaModule, @@ -55,6 +57,7 @@ import { LabelResolver } from './label.resolver' DataViewModule, DialogModule, DropdownModule, + FileUploadModule, FormsModule, InputTextModule, InputTextareaModule, diff --git a/src/app/types/index.d.ts b/src/app/types/index.d.ts new file mode 100644 index 0000000..7777575 --- /dev/null +++ b/src/app/types/index.d.ts @@ -0,0 +1 @@ +declare module 'file-saver' diff --git a/src/assets/api/openapi-bff.yaml b/src/assets/api/openapi-bff.yaml index 643e7d7..2214852 100644 --- a/src/assets/api/openapi-bff.yaml +++ b/src/assets/api/openapi-bff.yaml @@ -192,8 +192,98 @@ paths: application/json: schema: $ref: '#/components/schemas/HelpPageResult' + /helps/{productName}/{helpItemId}: + get: + x-onecx: + permissions: + help: + - read + tags: + - helpsInternal + description: search help item by product name and item id + operationId: getHelpByProductNameItemId + parameters: + - name: productName + in: path + required: true + description: product name + schema: + type: string + - name: helpItemId + in: path + required: true + description: help item ID + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Help' + 404: + description: Help not found + /helps/export: + x-onecx: + permissions: + help: + - read + post: + tags: + - helpsInternal + description: Export helps + operationId: exportHelps + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportHelpsRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + '404': + description: No helps founds + /helps/import: + x-onecx: + permissions: + help: + - write + post: + tags: + - helpsInternal + description: Import helps + operationId: importHelps + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Import result + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' components: schemas: + ExportHelpsRequest: + type: object + properties: + productNames: + type: array + uniqueItems: true + items: + type: string HelpSearchCriteria: type: object properties: @@ -243,6 +333,8 @@ components: - itemId type: object properties: + operator: + type: boolean modificationCount: format: int32 type: integer diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index a3fe82e..e1c8883 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1,5 +1,6 @@ { "ACTIONS": { + "CANCEL": "Abbrechen", "CONFIRMATION": { "NO": "Nein", "YES": "Ja", @@ -40,6 +41,27 @@ "HELP_ITEM_NOK": "Ein Fehler ist aufgetreten. Der Hilfeartikel wurde nicht gespeichert." } }, + "EXPORT": { + "LABEL": "Export", + "HELP_ITEM": "Hilfeartikel exportieren", + "HELP_ITEM.TOOLTIP": "Eine Gruppe von Hilfeartikeln exportieren", + "MESSAGE": { + "HELP_ITEM.EXPORT_OK": "Die Hilfeartikel wurde erfolgreich exportiert", + "HELP_ITEM.EXPORT_NOK": "Ein Fehler ist aufgetreten. Die Hilfeartikel wurden nicht exportiert." + } + }, + "IMPORT": { + "LABEL": "Import", + "CHOOSE": "Datei auswählen", + "UPLOAD": "Datei hochladen", + "HELP_ITEM": "Hilfeartikel importieren", + "HELP_ITEM.TOOLTIP": "Eine Gruppe von Hilfeartikeln importieren", + "MESSAGE": { + "HELP_ITEM.IMPORT_OK": "Die Hilfeartikel wurde erfolgreich importiert", + "HELP_ITEM.IMPORT_NOK": "Ein Fehler ist aufgetreten. Die Hilfeartikel wurden nicht importiert." + }, + "VALIDATION_RESULT": "Die Datei wurde geprüft." + }, "SEARCH": { "ACTION": "Aktionen", "SEARCH": "Suchen", @@ -89,6 +111,7 @@ }, "HELP_ITEM": { "APPLICATION_NAME": "Applikation", + "APPLICATION_LIST": "Liste der verfügbaren Applikationen", "HELP_ITEM_ID": "Hilfeartikel ID", "CONTEXT": "Kontext", "BASE_URL": "Basis URL", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 14a49ed..44da1bd 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -13,7 +13,7 @@ }, "CREATE": { "LABEL": "Create", - "HELP_ITEM": "Create Theme", + "HELP_ITEM": "Create Help Item", "HELP_ITEM.TOOLTIP": "Create a new Help Item", "MESSAGE": { "HELP_ITEM_ALREADY_EXIST": "A Help Item with that ID already exists", @@ -21,6 +21,27 @@ "HELP_ITEM.CREATE_NOK": "An error has occurred. The Help Item was not created." } }, + "IMPORT": { + "LABEL": "Import", + "CHOOSE": "Choose file", + "UPLOAD": "Upload file", + "HELP_ITEM": "Import Help Items", + "HELP_ITEM.TOOLTIP": "Import a set of Help Items", + "MESSAGE": { + "HELP_ITEM.IMPORT_OK": "Help Items were imported successfully", + "HELP_ITEM.IMPORT_NOK": "An error has occurred. Help Items were not imported." + }, + "VALIDATION_RESULT": "The file has been checked." + }, + "EXPORT": { + "LABEL": "Export", + "HELP_ITEM": "Export Help Items", + "HELP_ITEM.TOOLTIP": "Export a set of Help Items", + "MESSAGE": { + "HELP_ITEM.EXPORT_OK": "Help Items were exported successfully", + "HELP_ITEM.EXPORT_NOK": "An error has occurred. Help Items were not exported." + } + }, "DELETE": { "LABEL": "Delete", "HELP_ITEM": "Delete this Help Item", @@ -89,6 +110,7 @@ }, "HELP_ITEM": { "APPLICATION_NAME": "Application", + "APPLICATION_LIST": "List of available applications", "HELP_ITEM_ID": "Help Item ID", "CONTEXT": "Context", "BASE_URL": "Base URL",