From 0c9b24fb5bbad62f1902610c65ef088ee111d67a Mon Sep 17 00:00:00 2001 From: Christian Badura <93912698+cbadura@users.noreply.github.com> Date: Wed, 7 Aug 2024 14:22:30 +0200 Subject: [PATCH] Restructure app detail dialog with tabs (#173) * fix: anchor tags for sonar * feat: add remote name and tag name fields in app detail form * feat: add two tab panels to dialog and show properties in first * feat: complete mfe version of dialog * feat: create app intern component, finish data binding * fix: search and detail tests * fix: bump deps * fix: intern tests * fix: almost finish product apps tests * fix: prod apps tests * fix: change styling * fix: translations, delete panel height ft in app detail * fix: app detail tests --------- Co-authored-by: Christian Badura --- package-lock.json | 134 ++- package.json | 4 +- src/_mixins.scss | 10 + .../app-detail/app-detail.component.html | 901 ++++++++---------- .../app-detail/app-detail.component.spec.ts | 594 ++++++------ .../app-detail/app-detail.component.ts | 44 +- .../app-intern/app-intern.component.html | 125 +++ .../app-intern/app-intern.component.scss | 39 + .../app-intern/app-intern.component.spec.ts | 93 ++ .../app-intern/app-intern.component.ts | 33 + .../app-search/app-search.component.html | 7 +- .../app-search/app-search.component.spec.ts | 127 ++- .../product-apps.component.spec.ts | 354 ++++--- .../product-apps/product-apps.component.ts | 2 +- .../product-intern.component.html | 2 +- .../product-props.component.html | 20 +- src/app/product-store/product-store.module.ts | 2 + .../slot-search/slot-search.component.html | 4 +- src/assets/i18n/de.json | 15 +- src/assets/i18n/en.json | 22 +- 20 files changed, 1451 insertions(+), 1081 deletions(-) create mode 100644 src/app/product-store/app-detail/app-intern/app-intern.component.html create mode 100644 src/app/product-store/app-detail/app-intern/app-intern.component.scss create mode 100644 src/app/product-store/app-detail/app-intern/app-intern.component.spec.ts create mode 100644 src/app/product-store/app-detail/app-intern/app-intern.component.ts diff --git a/package-lock.json b/package-lock.json index dffb9f0..d7421a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,13 +47,13 @@ "devDependencies": { "@angular-devkit/build-angular": "^18.1.2", "@angular-devkit/core": "^18.1.2", - "@angular-devkit/schematics": "^18.1.2", + "@angular-devkit/schematics": "^18.1.3", "@angular-eslint/builder": "^18.1.0", "@angular-eslint/eslint-plugin": "^18.1.0", "@angular-eslint/eslint-plugin-template": "^18.1.0", "@angular-eslint/schematics": "^18.1.0", "@angular-eslint/template-parser": "^18.1.0", - "@angular/cli": "~18.1.2", + "@angular/cli": "~18.1.3", "@angular/compiler-cli": "^18.1.2", "@angular/language-service": "^18.1.2", "@commitlint/cli": "^19.3.0", @@ -557,13 +557,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.1.2.tgz", - "integrity": "sha512-v8aCJ1tPPzXsdiCoZxkc6YzLGhzJgC/6QauT03/Z6wWo8uI6DKibQQwQBawRE5FN5lKDpuGlNDv40EDtVYkQSA==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.1.3.tgz", + "integrity": "sha512-ElzCfiYW9P3xPRNRbPRSrOTGm+G7X8ta1ce3srqi00yPX39Y0WSM95SACqqF8j9dxL6BqazBMyAgNQUaVSbWjw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.1.2", + "@angular-devkit/core": "18.1.3", "jsonc-parser": "3.3.1", "magic-string": "0.30.10", "ora": "5.4.1", @@ -575,6 +575,34 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "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", + "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", @@ -905,18 +933,18 @@ } }, "node_modules/@angular/cli": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.1.2.tgz", - "integrity": "sha512-5H0scWgJcDE3NSM6/j/xSwNfAQBVOhVjXuj+nZOaEkJC0Bxh6AoEdWpQdzmZ6qSlx4LMlJYI6P/sH0kiBlFfgA==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.1.3.tgz", + "integrity": "sha512-vsEc3cGDUYcc+adfvBHSqKdI8uiaa86Y9pLWGHfqaD+N0q/k17d/47AFvXTDKLmKucMZrto/4088Y1y+yM9eOg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1801.2", - "@angular-devkit/core": "18.1.2", - "@angular-devkit/schematics": "18.1.2", + "@angular-devkit/architect": "0.1801.3", + "@angular-devkit/core": "18.1.3", + "@angular-devkit/schematics": "18.1.3", "@inquirer/prompts": "5.0.7", "@listr2/prompt-adapter-inquirer": "2.0.13", - "@schematics/angular": "18.1.2", + "@schematics/angular": "18.1.3", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -938,6 +966,67 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1801.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1801.3.tgz", + "integrity": "sha512-4yba7x315GKim7OuBgv89ZtG50hE3hw64KuRLSGuW+RvwcwLV24VanmdWmFiLC4RKYNSH13E0wZqDNJkrMQepw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.1.3", + "rxjs": "7.8.1" + }, + "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" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "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", + "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/@schematics/angular": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.1.3.tgz", + "integrity": "sha512-VyoL7O+3eL+BazmoWzexFpVy9k0MoOAmff3XqKLhP3/V7eXPc9s7znIDpPp28QF0V/Y2xMaGDWhqTx2CFcz4Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.1.3", + "@angular-devkit/schematics": "18.1.3", + "jsonc-parser": "3.3.1" + }, + "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" + } + }, "node_modules/@angular/cli/node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -7411,6 +7500,25 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": { + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.1.2.tgz", + "integrity": "sha512-v8aCJ1tPPzXsdiCoZxkc6YzLGhzJgC/6QauT03/Z6wWo8uI6DKibQQwQBawRE5FN5lKDpuGlNDv40EDtVYkQSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.1.2", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.10", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "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" + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", diff --git a/package.json b/package.json index f37e7d0..94ea9fe 100644 --- a/package.json +++ b/package.json @@ -74,13 +74,13 @@ "devDependencies": { "@angular-devkit/build-angular": "^18.1.2", "@angular-devkit/core": "^18.1.2", - "@angular-devkit/schematics": "^18.1.2", + "@angular-devkit/schematics": "^18.1.3", "@angular-eslint/builder": "^18.1.0", "@angular-eslint/eslint-plugin": "^18.1.0", "@angular-eslint/eslint-plugin-template": "^18.1.0", "@angular-eslint/schematics": "^18.1.0", "@angular-eslint/template-parser": "^18.1.0", - "@angular/cli": "~18.1.2", + "@angular/cli": "~18.1.3", "@angular/compiler-cli": "^18.1.2", "@angular/language-service": "^18.1.2", "@commitlint/cli": "^19.3.0", diff --git a/src/_mixins.scss b/src/_mixins.scss index 83ff89c..d28c39f 100644 --- a/src/_mixins.scss +++ b/src/_mixins.scss @@ -221,3 +221,13 @@ } } } + +@mixin displaying-text-responsive { + :host ::ng-deep { + .text-responsive { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } +} diff --git a/src/app/product-store/app-detail/app-detail.component.html b/src/app/product-store/app-detail/app-detail.component.html index 97cd128..70f264c 100644 --- a/src/app/product-store/app-detail/app-detail.component.html +++ b/src/app/product-store/app-detail/app-detail.component.html @@ -8,7 +8,7 @@ [modal]="true" [showHeader]="true" [contentStyleClass]="'border-round'" - [style]="{ 'max-width': '750px' }" + [style]="{ 'max-width': '1050px', 'max-height': '800px' }" [breakpoints]="{ '992px': '80vw', '750px': '90vw', @@ -19,7 +19,7 @@
- {{ this.dialogTitelKey | translate }} + {{ this.dialogTitleKey | translate }}
-
- - {{ 'ACTIONS.' + this.changeMode + '.MFE.HEADER' | translate }} - - - - - {{ 'APP.GROUP.REMOTE_MODULE' | translate }} - - -
-
- - - - -
-
- - - - - -
-
- - - - - -
-
- - - - -
-
- - - - -
-
-
- - - - - {{ 'APP.GROUP.LOCAL_MODULE' | translate }} - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - -
- - {{ ico.label }} -
-
- -
- - {{ ico.label }} -
-
-
- -
-
-
- - - - -
-
- - - - -
-
-
- + - - - {{ 'APP.GROUP.INTERNALS' | translate }} - -
-
- -
-
- -
-
- -
- -
- - - - -
-
- - + + {{ 'ACTIONS.' + this.changeMode + '.MFE.HEADER' | translate }} + + + + - - -
-
-
-
+ > + {{ 'APP.GROUP.REMOTE_MODULE' | translate }} + + +
+
+ + + + +
+
+ + + + + +
+
+ + + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ -
- - {{ 'ACTIONS.' + this.changeMode + '.MS.HEADER' | translate }} - - - - - {{ 'APP.GROUP.LOCAL_MODULE' | translate }} - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
-
+ + + + {{ 'APP.GROUP.LOCAL_MODULE' | translate }} + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + +
+ + {{ ico.label }} +
+
+ +
+ + {{ ico.label }} +
+
+
+ +
+
+
+ + + + +
+
+ + + + +
+
+
+
- - - - {{ 'APP.GROUP.INTERNALS' | translate }} - -
-
- -
-
- -
+
+ + {{ 'ACTIONS.' + this.changeMode + '.MS.HEADER' | translate }} + + + + + {{ 'APP.GROUP.LOCAL_MODULE' | translate }} + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ -
- - - - -
-
- - - - -
-
-
- + + + + +
diff --git a/src/app/product-store/app-detail/app-detail.component.spec.ts b/src/app/product-store/app-detail/app-detail.component.spec.ts index d42664e..d6b1309 100644 --- a/src/app/product-store/app-detail/app-detail.component.spec.ts +++ b/src/app/product-store/app-detail/app-detail.component.spec.ts @@ -27,6 +27,8 @@ const form = new FormGroup({ type: new FormControl(''), remoteBaseUrl: new FormControl(''), remoteEntry: new FormControl(''), + remoteName: new FormControl(''), + tagName: new FormControl(''), exposedModule: new FormControl(''), classifications: new FormControl(''), contact: new FormControl(''), @@ -50,6 +52,8 @@ const mfe: Microfrontend = { productName: 'productName', appVersion: 'version', remoteEntry: 'entry', + remoteName: 'remoteName', + tagName: 'tagName', description: 'description', technology: 'technology', type: MicrofrontendType.Module, @@ -241,348 +245,362 @@ describe('AppDetailComponent', () => { expect(component.appChanged.emit).toHaveBeenCalledWith(false) }) - it('should display error if form is invalid onSave', () => { - component.appAbstract = appMfe - component.formGroupMfe = new FormGroup({ - appId: new FormControl('i', Validators.minLength(2)), - appName: new FormControl(''), - appVersion: new FormControl(''), - productName: new FormControl(''), - description: new FormControl(''), - technology: new FormControl(''), - type: new FormControl(''), - remoteBaseUrl: new FormControl(''), - remoteEntry: new FormControl(''), - exposedModule: new FormControl(''), - classifications: new FormControl(''), - contact: new FormControl(''), - iconName: new FormControl(''), - note: new FormControl('') + describe('onSave', () => { + it('should display error if form is invalid onSave', () => { + component.appAbstract = appMfe + component.formGroupMfe = new FormGroup({ + appId: new FormControl('i', Validators.minLength(2)), + appName: new FormControl(''), + appVersion: new FormControl(''), + productName: new FormControl(''), + description: new FormControl(''), + technology: new FormControl(''), + type: new FormControl(''), + remoteBaseUrl: new FormControl(''), + remoteEntry: new FormControl(''), + remoteName: new FormControl(''), + tagName: new FormControl(''), + exposedModule: new FormControl(''), + classifications: new FormControl(''), + contact: new FormControl(''), + iconName: new FormControl(''), + note: new FormControl('') + }) + component.changeMode = 'CREATE' + + component.onSave() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'VALIDATION.FORM_INVALID' }) }) - component.changeMode = 'CREATE' - - component.onSave() - - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'VALIDATION.FORM_INVALID' }) - }) - it('should call createApp onSave in create mode', () => { - mfeApiServiceSpy.createMicrofrontend.and.returnValue(of({})) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'CREATE' + it('should call createApp onSave in create mode', () => { + mfeApiServiceSpy.createMicrofrontend.and.returnValue(of({})) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'CREATE' - component.onSave() + component.onSave() - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.APP.OK' }) - }) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.APP.OK' }) + }) - it('should display save error in create mode', () => { - const err = { - error: { - detail: 'Error', - errorCode: 'PERSIST_ENTITY_FAILED' + it('should display save error in create mode', () => { + const err = { + error: { + detail: 'Error', + errorCode: 'PERSIST_ENTITY_FAILED' + } } - } - mfeApiServiceSpy.createMicrofrontend.and.returnValue(throwError(() => err)) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'CREATE' - - component.onSave() - - const expectedKey = '' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.CREATE.APP.NOK', - detailKey: expectedKey + mfeApiServiceSpy.createMicrofrontend.and.returnValue(throwError(() => err)) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'CREATE' + + component.onSave() + + const expectedKey = '' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.CREATE.APP.NOK', + detailKey: expectedKey + }) }) - }) - it('should call updateApp onSave in edit mode', () => { - mfeApiServiceSpy.updateMicrofrontend.and.returnValue(of({})) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'EDIT' + it('should call updateApp onSave in edit mode', () => { + mfeApiServiceSpy.updateMicrofrontend.and.returnValue(of({})) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'EDIT' - component.onSave() + component.onSave() - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.APP.OK' }) - }) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.APP.OK' }) + }) - it('should display save error in edit mode: unique constraint mfe id', () => { - const err = { - error: { - detail: 'error: microfrontend_app_id', - errorCode: 'PERSIST_ENTITY_FAILED' + it('should display save error in edit mode: unique constraint mfe id', () => { + const err = { + error: { + detail: 'error: microfrontend_app_id', + errorCode: 'PERSIST_ENTITY_FAILED' + } } - } - mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'EDIT' - - component.onSave() - - const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.APP_ID' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.EDIT.APP.NOK', - detailKey: expectedKey + mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.APP_ID' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) }) - }) - it('should display save error in edit mode: unique constraint mfe id', () => { - const err = { - error: { - detail: 'error: microfrontend_remote_module', - errorCode: 'PERSIST_ENTITY_FAILED' + it('should display save error in edit mode: unique constraint mfe id', () => { + const err = { + error: { + detail: 'error: microfrontend_remote_module', + errorCode: 'PERSIST_ENTITY_FAILED' + } } - } - mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'EDIT' - - component.onSave() - - const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.REMOTE_MODULE' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.EDIT.APP.NOK', - detailKey: expectedKey + mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.REMOTE_MODULE' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) }) - }) - it('should display save error in edit mode: other internal error', () => { - const err = { - error: { - detail: 'error: microfrontend_remote_module', - errorCode: 'other' + it('should display save error in edit mode: other internal error', () => { + const err = { + error: { + detail: 'error: microfrontend_remote_module', + errorCode: 'other' + } } - } - mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'EDIT' - - component.onSave() - - const expectedKey = 'VALIDATION.ERRORS.INTERNAL_ERROR' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.EDIT.APP.NOK', - detailKey: expectedKey + mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.ERRORS.INTERNAL_ERROR' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) }) - }) - it('should display error if form is invalid onSave', () => { - component.appAbstract = appMfe - component.formGroupMfe = new FormGroup({ - appId: new FormControl('i', Validators.minLength(2)), - appName: new FormControl(''), - appVersion: new FormControl(''), - productName: new FormControl(''), - description: new FormControl(''), - technology: new FormControl(''), - type: new FormControl(''), - remoteBaseUrl: new FormControl(''), - remoteEntry: new FormControl(''), - exposedModule: new FormControl(''), - classifications: new FormControl(''), - contact: new FormControl(''), - iconName: new FormControl(''), - note: new FormControl('') + it('should display error if form is invalid onSave', () => { + component.appAbstract = appMfe + component.formGroupMfe = new FormGroup({ + appId: new FormControl('i', Validators.minLength(2)), + appName: new FormControl(''), + appVersion: new FormControl(''), + productName: new FormControl(''), + description: new FormControl(''), + technology: new FormControl(''), + type: new FormControl(''), + remoteBaseUrl: new FormControl(''), + remoteEntry: new FormControl(''), + remoteName: new FormControl(''), + tagName: new FormControl(''), + exposedModule: new FormControl(''), + classifications: new FormControl(''), + contact: new FormControl(''), + iconName: new FormControl(''), + note: new FormControl('') + }) + component.changeMode = 'CREATE' + + component.onSave() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'VALIDATION.FORM_INVALID' }) }) - component.changeMode = 'CREATE' - - component.onSave() - - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'VALIDATION.FORM_INVALID' }) - }) - it('should call createApp onSave in create mode', () => { - mfeApiServiceSpy.createMicrofrontend.and.returnValue(of({})) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'CREATE' + it('should call createApp onSave in create mode', () => { + mfeApiServiceSpy.createMicrofrontend.and.returnValue(of({})) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'CREATE' - component.onSave() + component.onSave() - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.APP.OK' }) - }) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.APP.OK' }) + }) - it('should display save error in create mode', () => { - const err = { - error: { - detail: 'Error', - errorCode: 'PERSIST_ENTITY_FAILED' + it('should display save error in create mode', () => { + const err = { + error: { + detail: 'Error', + errorCode: 'PERSIST_ENTITY_FAILED' + } } - } - mfeApiServiceSpy.createMicrofrontend.and.returnValue(throwError(() => err)) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'CREATE' - - component.onSave() - - const expectedKey = '' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.CREATE.APP.NOK', - detailKey: expectedKey + mfeApiServiceSpy.createMicrofrontend.and.returnValue(throwError(() => err)) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'CREATE' + + component.onSave() + + const expectedKey = '' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.CREATE.APP.NOK', + detailKey: expectedKey + }) }) - }) - it('should call updateApp onSave in edit mode', () => { - mfeApiServiceSpy.updateMicrofrontend.and.returnValue(of({})) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'EDIT' + it('should call updateApp onSave in edit mode', () => { + mfeApiServiceSpy.updateMicrofrontend.and.returnValue(of({})) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'EDIT' - component.onSave() + component.onSave() - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.APP.OK' }) - }) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.APP.OK' }) + }) - it('should display save error in edit mode: unique constraint mfe id', () => { - const err = { - error: { - detail: 'error: microfrontend_app_id', - errorCode: 'PERSIST_ENTITY_FAILED' + it('should display save error in edit mode: unique constraint mfe id', () => { + const err = { + error: { + detail: 'error: microfrontend_app_id', + errorCode: 'PERSIST_ENTITY_FAILED' + } } - } - mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'EDIT' - - component.onSave() - - const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.APP_ID' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.EDIT.APP.NOK', - detailKey: expectedKey + mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.APP_ID' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) }) - }) - it('should display save error in edit mode: unique constraint mfe id', () => { - const err = { - error: { - detail: 'error: microfrontend_remote_module', - errorCode: 'PERSIST_ENTITY_FAILED' + it('should display save error in edit mode: unique constraint mfe id', () => { + const err = { + error: { + detail: 'error: microfrontend_remote_module', + errorCode: 'PERSIST_ENTITY_FAILED' + } } - } - mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'EDIT' - - component.onSave() - - const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.REMOTE_MODULE' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.EDIT.APP.NOK', - detailKey: expectedKey + mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.REMOTE_MODULE' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) }) - }) - it('should display save error in edit mode: other internal error', () => { - const err = { - error: { - detail: 'error: microfrontend_remote_module', - errorCode: 'other' + it('should display save error in edit mode: other internal error', () => { + const err = { + error: { + detail: 'error: microfrontend_remote_module', + errorCode: 'other' + } } - } - mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) - component.appAbstract = appMfe - component.formGroupMfe = form - component.changeMode = 'EDIT' - - component.onSave() - - const expectedKey = 'VALIDATION.ERRORS.INTERNAL_ERROR' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.EDIT.APP.NOK', - detailKey: expectedKey + mfeApiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.appAbstract = appMfe + component.formGroupMfe = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.ERRORS.INTERNAL_ERROR' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) }) - }) - // ONSAVE TESTS FOR MSS - it('should display error if form is invalid onSave', () => { - component.appAbstract = appMs - component.formGroupMs = new FormGroup({ - appId: new FormControl('i', Validators.minLength(2)), - appName: new FormControl(''), - appVersion: new FormControl(''), - productName: new FormControl(''), - description: new FormControl('') + // ONSAVE TESTS FOR MSS + it('should display error if form is invalid onSave', () => { + component.appAbstract = appMs + component.formGroupMs = new FormGroup({ + appId: new FormControl('i', Validators.minLength(2)), + appName: new FormControl(''), + appVersion: new FormControl(''), + productName: new FormControl(''), + description: new FormControl('') + }) + component.changeMode = 'CREATE' + + component.onSave() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'VALIDATION.FORM_INVALID' }) }) - component.changeMode = 'CREATE' - - component.onSave() - - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'VALIDATION.FORM_INVALID' }) - }) - it('should call createApp onSave in create mode', () => { - msApiServiceSpy.createMicroservice.and.returnValue(of({})) - component.appAbstract = appMs - component.formGroupMs = msForm - component.changeMode = 'CREATE' + it('should call createApp onSave in create mode', () => { + msApiServiceSpy.createMicroservice.and.returnValue(of({})) + component.appAbstract = appMs + component.formGroupMs = msForm + component.changeMode = 'CREATE' - component.onSave() + component.onSave() - expect(component.formGroupMs.valid).toBeTrue() - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.APP.OK' }) - }) + expect(component.formGroupMs.valid).toBeTrue() + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.APP.OK' }) + }) - it('should display save error in create mode', () => { - const err = { - error: { - detail: 'Error', - errorCode: 'PERSIST_ENTITY_FAILED' + it('should display save error in create mode', () => { + const err = { + error: { + detail: 'Error', + errorCode: 'PERSIST_ENTITY_FAILED' + } } - } - msApiServiceSpy.createMicroservice.and.returnValue(throwError(() => err)) - component.appAbstract = appMs - component.formGroupMs = msForm - component.changeMode = 'CREATE' - - component.onSave() - - const expectedKey = '' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.CREATE.APP.NOK', - detailKey: expectedKey + msApiServiceSpy.createMicroservice.and.returnValue(throwError(() => err)) + component.appAbstract = appMs + component.formGroupMs = msForm + component.changeMode = 'CREATE' + + component.onSave() + + const expectedKey = '' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.CREATE.APP.NOK', + detailKey: expectedKey + }) }) - }) - it('should call updateApp onSave in edit mode', () => { - msApiServiceSpy.updateMicroservice.and.returnValue(of({})) - component.appAbstract = appMs - component.formGroupMs = msForm - component.changeMode = 'EDIT' + it('should call updateApp onSave in edit mode', () => { + msApiServiceSpy.updateMicroservice.and.returnValue(of({})) + component.appAbstract = appMs + component.formGroupMs = msForm + component.changeMode = 'EDIT' - component.onSave() + component.onSave() - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.APP.OK' }) - }) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.APP.OK' }) + }) - it('should display save error in edit mode: other internal error', () => { - const err = { - error: { - detail: 'error: microservice_remote_module', - errorCode: 'other' + it('should display save error in edit mode: other internal error', () => { + const err = { + error: { + detail: 'error: microservice_remote_module', + errorCode: 'other' + } } - } - msApiServiceSpy.updateMicroservice.and.returnValue(throwError(() => err)) - component.appAbstract = appMs - component.formGroupMs = msForm - component.changeMode = 'EDIT' + msApiServiceSpy.updateMicroservice.and.returnValue(throwError(() => err)) + component.appAbstract = appMs + component.formGroupMs = msForm + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.ERRORS.INTERNAL_ERROR' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) + }) + }) - component.onSave() + it('should update tabIndex onTabPanelChange', () => { + const mockEvent = { index: 3 } - const expectedKey = 'VALIDATION.ERRORS.INTERNAL_ERROR' - expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.EDIT.APP.NOK', - detailKey: expectedKey - }) + component.onTabPanelChange(mockEvent) + + expect(component.tabIndex).toBe(mockEvent.index) }) it('should call this.user.lang$ from the constructor and set this.dateFormat to the default format if user.lang$ is not de', () => { diff --git a/src/app/product-store/app-detail/app-detail.component.ts b/src/app/product-store/app-detail/app-detail.component.ts index 8c5c233..dc78487 100644 --- a/src/app/product-store/app-detail/app-detail.component.ts +++ b/src/app/product-store/app-detail/app-detail.component.ts @@ -1,8 +1,9 @@ -import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core' +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, Renderer2, ViewChild } from '@angular/core' import { FormControl, FormGroup, Validators } from '@angular/forms' import { TranslateService } from '@ngx-translate/core' import { finalize } from 'rxjs' import { SelectItem } from 'primeng/api' +import { TabView } from 'primeng/tabview' import { PortalMessageService, UserService } from '@onecx/portal-integration-angular' import { IconService } from 'src/app/shared/iconservice' @@ -31,7 +32,9 @@ export interface MfeForm { technology: FormControl type: FormControl remoteBaseUrl: FormControl + remoteName: FormControl remoteEntry: FormControl + tagName: FormControl classifications: FormControl contact?: FormControl iconName?: FormControl @@ -51,28 +54,27 @@ export interface MsForm { templateUrl: './app-detail.component.html', styleUrls: ['./app-detail.component.scss'] }) -export class AppDetailComponent implements OnChanges { +export class AppDetailComponent implements OnInit, OnChanges { @Input() appAbstract: AppAbstract | undefined @Input() dateFormat = 'medium' @Input() changeMode: ChangeMode = 'VIEW' @Input() displayDialog = false @Output() appChanged = new EventEmitter() + @ViewChild('panelDetail') panelDetail: TabView | undefined public mfe: Microfrontend | undefined public ms: Microservice | undefined public formGroupMfe: FormGroup public formGroupMs: FormGroup - public dialogTitelKey = '' + public tabIndex = 0 + public dialogTitleKey = '' public loading = false public operator = false public undeployed = false public deprecated = false public hasCreatePermission = false public hasEditPermission = false - public technologies: SelectItem[] = [ - { label: 'Angular', value: 'ANGULAR' }, - { label: 'WebComponent', value: 'WEBCOMPONENTMODULE' } - ] + public technologies: SelectItem[] = [] public types: SelectItem[] = [ { label: 'Module', value: 'MODULE' }, { label: 'Component', value: 'COMPONENT' } @@ -86,7 +88,8 @@ export class AppDetailComponent implements OnChanges { private msApi: MicroservicesAPIService, private mfeApi: MicrofrontendsAPIService, private msgService: PortalMessageService, - private translate: TranslateService + private translate: TranslateService, + private renderer: Renderer2 ) { this.hasCreatePermission = this.user.hasPermission('APP#CREATE') this.hasEditPermission = this.user.hasPermission('APP#EDIT') @@ -104,6 +107,8 @@ export class AppDetailComponent implements OnChanges { type: new FormControl(null), remoteBaseUrl: new FormControl(null, [Validators.maxLength(255)]), remoteEntry: new FormControl(null, [Validators.maxLength(255)]), + remoteName: new FormControl(null, [Validators.maxLength(255)]), + tagName: new FormControl(null, [Validators.maxLength(255)]), exposedModule: new FormControl(null, [Validators.maxLength(255)]), classifications: new FormControl(null, [Validators.maxLength(255)]), contact: new FormControl(null, [Validators.maxLength(255)]), @@ -119,6 +124,10 @@ export class AppDetailComponent implements OnChanges { }) } + ngOnInit() { + this.getDropdownTranslations() + } + ngOnChanges() { this.enableForms() if (this.changeMode === 'CREATE') { @@ -174,7 +183,7 @@ export class AppDetailComponent implements OnChanges { } this.enableForms() } - this.dialogTitelKey = 'ACTIONS.' + this.changeMode + '.MFE.HEADER' + this.dialogTitleKey = 'ACTIONS.' + this.changeMode + '.MFE.HEADER' } }) } @@ -200,7 +209,7 @@ export class AppDetailComponent implements OnChanges { } this.enableForms() } - this.dialogTitelKey = 'ACTIONS.' + this.changeMode + '.MS.HEADER' + this.dialogTitleKey = 'ACTIONS.' + this.changeMode + '.MS.HEADER' } }) } @@ -216,6 +225,8 @@ export class AppDetailComponent implements OnChanges { type: mfe['type'], remoteBaseUrl: mfe['remoteBaseUrl'], remoteEntry: mfe['remoteEntry'], + remoteName: mfe['remoteName'], + tagName: mfe['tagName'], exposedModule: mfe['exposedModule'], classifications: mfe['classifications'], contact: mfe['contact'], @@ -258,6 +269,9 @@ export class AppDetailComponent implements OnChanges { } } + public onTabPanelChange(e: any): void { + this.tabIndex = e.index + } private createMfe() { this.mfeApi.createMicrofrontend({ createMicrofrontendRequest: this.mfe as CreateMicrofrontendRequest }).subscribe({ next: () => { @@ -330,4 +344,14 @@ export class AppDetailComponent implements OnChanges { }) console.error('err', err) } + + private getDropdownTranslations() { + this.translate.get(['APP.WEBCOMPONENT.MODULE', 'APP.WEBCOMPONENT.SCRIPT']).subscribe((data) => { + this.technologies = [ + { label: 'Angular', value: 'ANGULAR' }, + { label: data['APP.WEBCOMPONENT.MODULE'], value: 'WEBCOMPONENTMODULE' }, + { label: data['APP.WEBCOMPONENT.SCRIPT'], value: 'WEBCOMPONENTSCRIPT' } + ] + }) + } } diff --git a/src/app/product-store/app-detail/app-intern/app-intern.component.html b/src/app/product-store/app-detail/app-intern/app-intern.component.html new file mode 100644 index 0000000..d5b2460 --- /dev/null +++ b/src/app/product-store/app-detail/app-intern/app-intern.component.html @@ -0,0 +1,125 @@ +
+ + + + {{ 'APP.GROUP.INTERNALS' | translate }} + +
+
+ +
+
+ +
+ +
+ +
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+
diff --git a/src/app/product-store/app-detail/app-intern/app-intern.component.scss b/src/app/product-store/app-detail/app-intern/app-intern.component.scss new file mode 100644 index 0000000..949c7c0 --- /dev/null +++ b/src/app/product-store/app-detail/app-intern/app-intern.component.scss @@ -0,0 +1,39 @@ +@import '/src/_mixins.scss'; + +@include danger-action; + +/* limit deletion dialog */ +:host ::ng-deep { + // popup dialogues + @media (max-width: 991px) { + .p-dialog.p-dialog { + max-height: unset !important; + height: unset !important; + } + } + + .p-dialog { + background: var(--panel-content-bg); + .p-dialog-content { + border-radius: unset !important; + padding-bottom: 0.5rem; + margin-bottom: 0.5rem; + } + .p-dialog-footer button { + margin: unset; + } + .p-dialog-header .p-dialog-header-icons { + display: none; + } + } + .p-fieldset { + &.p-fieldset-toggleable .p-fieldset-legend a, + .p-fieldset-legend { + background-color: transparent; + padding: 0.3rem 0.4rem; + } + .p-fieldset-content { + padding: 1rem 0.2rem 0rem; + } + } +} diff --git a/src/app/product-store/app-detail/app-intern/app-intern.component.spec.ts b/src/app/product-store/app-detail/app-intern/app-intern.component.spec.ts new file mode 100644 index 0000000..8b4d242 --- /dev/null +++ b/src/app/product-store/app-detail/app-intern/app-intern.component.spec.ts @@ -0,0 +1,93 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { RouterTestingModule } from '@angular/router/testing' +import { TranslateTestingModule } from 'ngx-translate-testing' + +import { AppInternComponent } from './app-intern.component' +import { Microfrontend, Microservice } from 'src/app/shared/generated' + +const appMfe: Microfrontend = { + operator: true, + deprecated: false, + undeployed: true +} as Microfrontend + +const appMs: Microservice = { + operator: true, + undeployed: true +} + +describe('AppInternComponent', () => { + let component: AppInternComponent + let fixture: ComponentFixture + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [AppInternComponent], + imports: [ + RouterTestingModule, + HttpClientTestingModule, + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(AppInternComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('ngOnChanges', () => { + it('should set relevant values correctly when viewed app is Microfrontend', () => { + component.app = appMfe + + component.ngOnChanges() + + expect(component.undeployed).toBe(true) + expect(component.operator).toBe(true) + expect(component.deprecated).toBe(false) + }) + + it('should set relevant values to false when viewed app is Microservice', () => { + component.app = appMs + + component.ngOnChanges() + + expect(component.undeployed).toBe(true) + expect(component.operator).toBe(true) + expect(component.deprecated).toBe(false) + }) + + it('should set all properties to false when app is undefined', () => { + component.app = undefined + + component.ngOnChanges() + + expect(component.undeployed).toBe(false) + expect(component.operator).toBe(false) + expect(component.deprecated).toBe(false) + }) + + it('should set deprecated to false if app is a Microfrontend and deprecated property is missing', () => { + const appMfeNoDeprecated: Partial = { + operator: true, + undeployed: true + } + component.app = appMfeNoDeprecated as Microfrontend + + component.ngOnChanges() + + expect(component.deprecated).toBe(false) + }) + }) +}) diff --git a/src/app/product-store/app-detail/app-intern/app-intern.component.ts b/src/app/product-store/app-detail/app-intern/app-intern.component.ts new file mode 100644 index 0000000..1766d3f --- /dev/null +++ b/src/app/product-store/app-detail/app-intern/app-intern.component.ts @@ -0,0 +1,33 @@ +import { Component, Input, OnChanges } from '@angular/core' +import { TranslateService } from '@ngx-translate/core' + +import { Microfrontend, Microservice } from 'src/app/shared/generated' + +@Component({ + selector: 'app-app-intern', + templateUrl: './app-intern.component.html' +}) +export class AppInternComponent implements OnChanges { + @Input() app: (Microfrontend | Microservice) | undefined + @Input() dateFormat = 'medium' + + public undeployed = false + public operator = false + public deprecated = false + + constructor(private translate: TranslateService) {} + + public ngOnChanges(): void { + this.undeployed = this.app?.undeployed ?? false + this.operator = this.app?.operator ?? false + if (this.isMicrofrontend(this.app)) { + this.deprecated = this.app.deprecated ?? false + } else { + this.deprecated = false + } + } + + private isMicrofrontend(app: Microfrontend | Microservice | undefined): app is Microfrontend { + return (app as Microfrontend)?.deprecated !== undefined + } +} diff --git a/src/app/product-store/app-search/app-search.component.html b/src/app/product-store/app-search/app-search.component.html index 75fc9ea..cd90077 100644 --- a/src/app/product-store/app-search/app-search.component.html +++ b/src/app/product-store/app-search/app-search.component.html @@ -152,8 +152,9 @@
-
@@ -248,7 +249,7 @@ {{ limitText(app.productName, 25) }}
-
+
diff --git a/src/app/product-store/app-search/app-search.component.spec.ts b/src/app/product-store/app-search/app-search.component.spec.ts index d0d3ac2..b3817b1 100644 --- a/src/app/product-store/app-search/app-search.component.spec.ts +++ b/src/app/product-store/app-search/app-search.component.spec.ts @@ -193,48 +193,6 @@ describe('AppSearchComponent', () => { } }) - it('should update viewMode onLayoutChange', () => { - component.onLayoutChange('list') - - expect(component.viewMode).toBe('list') - }) - - it('should update filter and call dv.filter onFilterChange', () => { - const filter = 'testFilter' - - component.onFilterChange(filter) - - expect(component.filter).toBe(filter) - }) - - it('should update filterBy and filterValue onQuickFilterChange: ALL', () => { - component.onQuickFilterChange({ value: 'ALL' }) - - expect(component.filterBy).toBe(component.filterValueDefault) - expect(component.filterValue).toBe('') - }) - - it('should update filterBy and filterValue onQuickFilterChange: other', () => { - component.onQuickFilterChange({ value: 'other' }) - - expect(component.filterValue).toBe('other') - expect(component.filterBy).toBe('appType') - }) - - it('should update sortField onSortChange', () => { - component.onSortChange('field') - - expect(component.sortField).toBe('field') - }) - - it('should update sortOrder based on asc boolean onSortDirChange', () => { - component.onSortDirChange(true) - expect(component.sortOrder).toBe(-1) - - component.onSortDirChange(false) - expect(component.sortOrder).toBe(1) - }) - it('should call searchApps onSearch', () => { spyOn(component, 'searchApps') @@ -363,13 +321,85 @@ describe('AppSearchComponent', () => { }) }) - it('should reset appSearchCriteriaGroup onSearchReset is called', () => { - component.appSearchCriteriaGroup = form - spyOn(form, 'reset').and.callThrough() + /* + * UI ACTIONS + */ + it('should update viewMode onLayoutChange', () => { + component.onLayoutChange('list') - component.onSearchReset() + expect(component.viewMode).toBe('list') + }) - expect(component.appSearchCriteriaGroup.reset).toHaveBeenCalled() + it('should update filter and call dv.filter onFilterChange', () => { + const filter = 'testFilter' + + component.onFilterChange(filter) + + expect(component.filter).toBe(filter) + }) + + describe('onAppTypeFilterChange', () => { + it('should set appTypeFilterValue when ev.value is provided', () => { + const event = { value: 'testValue' } + component.onAppTypeFilterChange(event) + expect(component.appTypeFilterValue).toBe('testValue') + }) + + it('should not change appTypeFilterValue when ev.value is not provided', () => { + component.appTypeFilterValue = 'initialValue' + const event = { value: null } + component.onAppTypeFilterChange(event) + expect(component.appTypeFilterValue).toBe('initialValue') + }) + }) + + describe('onQuickFilterChange', () => { + it('should update filterBy and filterValue onQuickFilterChange: ALL', () => { + component.onQuickFilterChange({ value: 'ALL' }) + + expect(component.filterBy).toBe(component.filterValueDefault) + expect(component.filterValue).toBe('') + }) + + it('should update filterBy and filterValue onQuickFilterChange: other', () => { + component.onQuickFilterChange({ value: 'other' }) + + expect(component.filterValue).toBe('other') + expect(component.filterBy).toBe('appType') + }) + + it('should set to quickFulterVaule to the original one if there is no current value', () => { + component.quickFilterValueOld = 'old' + + component.onQuickFilterChange({}) + + expect(component.quickFilterValue).toBe('old') + }) + }) + + it('should update sortField onSortChange', () => { + component.onSortChange('field') + + expect(component.sortField).toBe('field') + }) + + describe('onSortDirChange', () => { + it('should update sortOrder based on asc boolean onSortDirChange', () => { + component.onSortDirChange(true) + expect(component.sortOrder).toBe(-1) + + component.onSortDirChange(false) + expect(component.sortOrder).toBe(1) + }) + + it('should reset appSearchCriteriaGroup onSearchReset is called', () => { + component.appSearchCriteriaGroup = form + spyOn(form, 'reset').and.callThrough() + + component.onSearchReset() + + expect(component.appSearchCriteriaGroup.reset).toHaveBeenCalled() + }) }) it('should navigate back onBack', () => { @@ -431,6 +461,9 @@ describe('AppSearchComponent', () => { expect(component.app).toBe(msApp) }) + /** + * MODAL Dialog feedback + */ it('should call searchApps if app changed', () => { spyOn(component, 'searchApps') diff --git a/src/app/product-store/product-detail/product-apps/product-apps.component.spec.ts b/src/app/product-store/product-detail/product-apps/product-apps.component.spec.ts index 73fbbe2..e20d14d 100644 --- a/src/app/product-store/product-detail/product-apps/product-apps.component.spec.ts +++ b/src/app/product-store/product-detail/product-apps/product-apps.component.spec.ts @@ -1,6 +1,4 @@ -/* import { NO_ERRORS_SCHEMA } from '@angular/core' -//import { ComponentFixture, TestBed, fakeAsync, waitForAsync } from '@angular/core/testing' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' import { HttpClientTestingModule } from '@angular/common/http/testing' import { RouterTestingModule } from '@angular/router/testing' @@ -8,15 +6,17 @@ import { of, throwError } from 'rxjs' import { TranslateTestingModule } from 'ngx-translate-testing' import { PortalMessageService } from '@onecx/portal-integration-angular' -import { ProductAppsComponent } from './product-apps.component' +import { AppType, ProductAppsComponent } from './product-apps.component' import { MicrofrontendAbstract, - MicrofrontendsAPIService, MicrofrontendPageResult, + MicrofrontendType, Microservice, - MicroservicesAPIService, - MicroservicePageResult, - Product + Product, + ProductDetails, + ProductsAPIService, + Slot, + SlotPageItem } from 'src/app/shared/generated' import { AppAbstract } from '../../app-search/app-search.component' @@ -52,11 +52,8 @@ describe('ProductAppsComponent', () => { const ms: Microservice = { id: 'id', appId: 'appId', appName: 'microservice', productName: 'prodName' } const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) - const apiMfeServiceSpy = { - searchMicrofrontends: jasmine.createSpy('searchMicrofrontends').and.returnValue(of({})) - } - const apiMsServiceSpy = { - searchMicroservice: jasmine.createSpy('searchMicroservice').and.returnValue(of({})) + const productServiceSpy = { + getProductDetailsByCriteria: jasmine.createSpy('getProductDetailsByCriteria').and.returnValue(of({})) } beforeEach(waitForAsync(() => { @@ -71,8 +68,7 @@ describe('ProductAppsComponent', () => { }).withDefaultLanguage('en') ], providers: [ - { provide: MicrofrontendsAPIService, useValue: apiMfeServiceSpy }, - { provide: MicroservicesAPIService, useValue: apiMsServiceSpy }, + { provide: ProductsAPIService, useValue: productServiceSpy }, { provide: PortalMessageService, useValue: msgServiceSpy } ], schemas: [NO_ERRORS_SCHEMA] @@ -83,15 +79,13 @@ describe('ProductAppsComponent', () => { fixture = TestBed.createComponent(ProductAppsComponent) component = fixture.componentInstance fixture.detectChanges() - apiMfeServiceSpy.searchMicrofrontends.and.returnValue(of({} as MicrofrontendPageResult)) - apiMsServiceSpy.searchMicroservice.and.returnValue(of({} as MicroservicePageResult)) + productServiceSpy.getProductDetailsByCriteria.and.returnValue(of({} as MicrofrontendPageResult)) component.product = product component.exceptionKey = '' }) afterEach(() => { - apiMfeServiceSpy.searchMicrofrontends.calls.reset() - apiMsServiceSpy.searchMicroservice.calls.reset() + productServiceSpy.getProductDetailsByCriteria.calls.reset() msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() msgServiceSpy.info.calls.reset() @@ -103,202 +97,159 @@ describe('ProductAppsComponent', () => { it('should call searchApps onChanges if product exists', () => { component.product = product - spyOn(component, 'searchApps') + spyOn(component, 'searchProducts') component.ngOnChanges() - expect(component.searchApps).toHaveBeenCalled() - }) - - it('should search microfrontends and microservices on searchApps', (done) => { - component.product = product - apiMfeServiceSpy.searchMicrofrontends.and.returnValue(of({ stream: [mfe] } as MicrofrontendPageResult)) - apiMsServiceSpy.searchMicroservice.and.returnValue(of({ stream: [ms] } as MicroservicePageResult)) - - component.searchApps() - - component.apps$.subscribe({ - next: (result) => { - expect(result.length).toBe(2) - result.forEach((result, i) => { - if (i === 0) expect(result.appType).toEqual('MFE') - if (i === 1) expect(result.appType).toEqual('MS') - }) - done() - }, - error: done.fail - }) - }) - - it('should search microfrontends only on searchApps', (done) => { - component.product = product - apiMfeServiceSpy.searchMicrofrontends.and.returnValue(of({ stream: [mfe] } as MicrofrontendPageResult)) - - component.searchApps() - - component.apps$.subscribe({ - next: (result) => { - expect(result.length).toBe(1) - result.forEach((result, i) => { - if (i === 0) expect(result.appType).toEqual('MFE') - }) - done() - }, - error: done.fail - }) + expect(component.searchProducts).toHaveBeenCalled() }) - it('should search microservices only on searchApps', (done) => { - component.product = product - apiMsServiceSpy.searchMicroservice.and.returnValue(of({ stream: [ms] } as MicroservicePageResult)) - - component.searchApps() - - component.apps$.subscribe({ - next: (result) => { - expect(result.length).toBe(1) - result.forEach((result, i) => { - if (i === 0) expect(result.appType).toEqual('MS') - }) - done() - }, - error: done.fail + /** + * SEARCH + */ + describe('searchProducts', () => { + it('should search microfrontends and microservices', (done) => { + component.product = product + productServiceSpy.getProductDetailsByCriteria.and.returnValue( + of({ microfrontends: [mfe], microservices: [ms] } as ProductDetails) + ) + + component.searchProducts() + + component.productDetails$.subscribe({ + next: (result) => { + expect(result.microfrontends?.length).toBe(1) + expect(result.microservices?.length).toBe(1) + done() + }, + error: done.fail + }) }) - }) - it('should catch error on searchApps: mfes', (done) => { - const err = { status: 404 } - apiMfeServiceSpy.searchMicrofrontends.and.returnValue(throwError(() => err)) + it('should catch error on searchProducts', (done) => { + const err = { status: 404 } + productServiceSpy.getProductDetailsByCriteria.and.returnValue(throwError(() => err)) - component.searchApps() + component.searchProducts() - component.apps$.subscribe({ - next: (result) => { - expect(result.length).toBe(0) - expect(component.exceptionKey).toEqual('EXCEPTIONS.HTTP_STATUS_404.APPS') - done() - }, - error: done.fail + component.productDetails$.subscribe({ + next: (result) => { + expect(component.exceptionKey).toEqual('EXCEPTIONS.HTTP_STATUS_404.APPS') + done() + }, + error: done.fail + }) }) }) - it('should catch error on searchApps: ms', (done) => { - const err = { status: 404 } - apiMsServiceSpy.searchMicroservice.and.returnValue(throwError(() => err)) - - component.searchApps() - - component.apps$.subscribe({ - next: (result) => { - expect(result.length).toBe(0) - expect(component.exceptionKey).toEqual('EXCEPTIONS.HTTP_STATUS_404.APPS') - done() - }, - error: done.fail + describe('sortMfesByTypeAndExposedModule', () => { + it('should sort by type and then by exposedModule', () => { + const mfeA: MicrofrontendAbstract = { + type: MicrofrontendType.Component, + exposedModule: 'moduleA' + } as MicrofrontendAbstract + const mfeB: MicrofrontendAbstract = { + type: MicrofrontendType.Component, + exposedModule: 'moduleB' + } as MicrofrontendAbstract + const mfeC: MicrofrontendAbstract = { + type: MicrofrontendType.Module, + exposedModule: 'moduleC' + } as MicrofrontendAbstract + + expect(component.sortMfesByTypeAndExposedModule(mfeA, mfeB)).toBeLessThan(0) + expect(component.sortMfesByTypeAndExposedModule(mfeB, mfeA)).toBeGreaterThan(0) + expect(component.sortMfesByTypeAndExposedModule(mfeA, mfeC)).toBeLessThan(0) + expect(component.sortMfesByTypeAndExposedModule(mfeC, mfeA)).toBeGreaterThan(0) + expect(component.sortMfesByTypeAndExposedModule(mfeA, mfeA)).toBe(0) }) - }) - it('should search microfrontends and microservices on searchApps with ms error', (done) => { - const err = { status: 404 } - apiMfeServiceSpy.searchMicrofrontends.and.returnValue(of({ stream: [mfe] } as MicrofrontendPageResult)) - apiMsServiceSpy.searchMicroservice.and.returnValue(of(throwError(() => err))) - - component.searchApps() - - component.apps$.subscribe({ - next: (result) => { - expect(result.length).toBe(1) - result.forEach((result, i) => { - if (i === 0) expect(result.appType).toEqual('MFE') - }) - done() - }, - error: done.fail - }) - }) + it('should handle undefined or empty values', () => { + const mfeA: MicrofrontendAbstract = { type: MicrofrontendType.Component } as MicrofrontendAbstract + const mfeB: MicrofrontendAbstract = { exposedModule: 'moduleB' } as MicrofrontendAbstract - it('should search microfrontends and microservices on searchApps with mfe and ms error', (done) => { - component.product = product - const err = { status: 404 } - apiMfeServiceSpy.searchMicrofrontends.and.returnValue(of(throwError(() => err))) - apiMsServiceSpy.searchMicroservice.and.returnValue(of(throwError(() => err))) - - component.searchApps() - - component.apps$.subscribe({ - next: (result) => { - expect(result.length).toBe(0) - done() - }, - error: done.fail + expect(component.sortMfesByTypeAndExposedModule(mfeA, mfeB)).toBeGreaterThan(0) }) }) - it('should set correct value onLayoutChange', () => { - const viewMode = 'EDIT' - - component.onLayoutChange(viewMode) + describe('sortMssByAppId', () => { + it('should sort by appId', () => { + const msA: Microservice = { appId: 'a' } as Microservice + const msB: Microservice = { appId: 'b' } as Microservice + const msC: Microservice = { appId: 'a' } as Microservice - expect(component.viewMode).toEqual('EDIT') - }) - - it('should set correct values onFilterChange', () => { - const filter = 'filter' + expect(component.sortMssByAppId(msA, msB)).toBeLessThan(0) + expect(component.sortMssByAppId(msB, msA)).toBeGreaterThan(0) + expect(component.sortMssByAppId(msA, msC)).toBe(0) + }) - component.onFilterChange(filter) + it('should handle undefined or empty values', () => { + const msA: Microservice = { appId: 'a' } as Microservice + const msB: Microservice = {} as Microservice + const msC: Microservice = { appId: '' } as Microservice - expect(component.filter).toEqual(filter) + expect(component.sortMssByAppId(msA, msB)).toBeGreaterThan(0) + expect(component.sortMssByAppId(msB, msA)).toBeLessThan(0) + expect(component.sortMssByAppId(msB, msC)).toBe(0) + }) }) - it('should set correct value onSortChange', () => { - const sortField = 'field' - - component.onSortChange(sortField) + describe('sortSlotsByName', () => { + it('should sort by name', () => { + const slotA: SlotPageItem = { name: 'a' } as SlotPageItem + const slotB: SlotPageItem = { name: 'b' } as SlotPageItem + const slotC: SlotPageItem = { name: 'a' } as SlotPageItem - expect(component.sortField).toEqual(sortField) - }) + expect(component.sortSlotsByName(slotA, slotB)).toBeLessThan(0) + expect(component.sortSlotsByName(slotB, slotA)).toBeGreaterThan(0) + expect(component.sortSlotsByName(slotA, slotC)).toBe(0) + }) - it('should set correct value onSortDirChange', () => { - let asc = true - component.onSortDirChange(asc) - expect(component.sortOrder).toEqual(-1) + it('should handle undefined or empty values', () => { + const slotA: SlotPageItem = { name: 'a' } as SlotPageItem + const slotB: SlotPageItem = {} as SlotPageItem + const slotC: SlotPageItem = { name: '' } as SlotPageItem - asc = false - component.onSortDirChange(asc) - expect(component.sortOrder).toEqual(1) + expect(component.sortSlotsByName(slotA, slotB)).toBeGreaterThan(0) + expect(component.sortSlotsByName(slotB, slotA)).toBeLessThan(0) + expect(component.sortSlotsByName(slotB, slotC)).toBe(0) + }) }) - it('should behave correctly onDetail for MFE', () => { + /** + * UI EVENTS + */ + describe('onDetail', () => { const mockEvent = { stopPropagation: jasmine.createSpy() } - component.onDetail(mockEvent, mfeApp) + it('should display details of an mfe', () => { + component.onDetail(mockEvent, mfeApp, AppType.MFE) - expect(component.app).toEqual(mfeApp) - expect(component.changeMode).toEqual('EDIT') - expect(component.displayDetailDialog).toBeTrue() - }) - - it('should behave correctly onDetail for MS', () => { - const mockEvent = { stopPropagation: jasmine.createSpy() } + expect(component.app).toEqual(mfeApp) + expect(component.changeMode).toEqual('EDIT') + expect(component.displayDetailDialog).toBeTrue() + }) - component.onDetail(mockEvent, msApp) + it('should display details of an ms', () => { + component.onDetail(mockEvent, msApp, AppType.MS) - expect(component.app).toEqual(msApp) - expect(component.changeMode).toEqual('EDIT') - expect(component.displayDetailDialog).toBeTrue() + expect(component.app).toEqual(msApp) + expect(component.changeMode).toEqual('EDIT') + expect(component.displayDetailDialog).toBeTrue() + }) }) - it('should behave correctly onCopy', () => { + it('should display details to copy', () => { const mockEvent = { stopPropagation: jasmine.createSpy() } - component.onCopy(mockEvent, mfeApp) + component.onCopy(mockEvent, mfeApp, AppType.MFE) expect(component.app).toEqual(mfeApp) expect(component.changeMode).toEqual('COPY') expect(component.displayDetailDialog).toBeTrue() }) - it('should should behave correctly onCreate', () => { + it('should should show create dialog', () => { component.onCreate() expect(component.changeMode).toEqual('CREATE') @@ -306,32 +257,73 @@ describe('ProductAppsComponent', () => { expect(component.displayDetailDialog).toBeTrue() }) - it('should behave correctly onDelete', () => { + it('should display delete dialog', () => { const mockEvent = { stopPropagation: jasmine.createSpy() } - component.onDelete(mockEvent, mfeApp) + component.onDelete(mockEvent, mfeApp, AppType.MFE) expect(component.app).toEqual(mfeApp) expect(component.displayDeleteDialog).toBeTrue() }) - it('should call searchApps if app changed', () => { - spyOn(component, 'searchApps') + it('should call searchProducts if app changed', () => { + spyOn(component, 'searchProducts') component.appChanged(true) - expect(component.searchApps).toHaveBeenCalled() + expect(component.searchProducts).toHaveBeenCalled() expect(component.displayDetailDialog).toBeFalse() }) - it('should call searchApps if app deleted', () => { - spyOn(component, 'searchApps') + it('should call searchProducts if app deleted', () => { + spyOn(component, 'searchProducts') component.appDeleted(true) - expect(component.searchApps).toHaveBeenCalled() + expect(component.searchProducts).toHaveBeenCalled() expect(component.displayDetailDialog).toBeFalse() }) -}) -*/ + describe('onSlotDelete', () => { + it('should prepare slot deletion', () => { + const event = { stopPropagation: jasmine.createSpy('stopPropagation') } + const slot: Slot = { id: 'id', name: 'Test Slot' } as Slot + + component.onSlotDelete(event, slot) + + expect(event.stopPropagation).toHaveBeenCalled() + expect(component.slot).toEqual(slot) + expect(component.displaySlotDeleteDialog).toBe(true) + }) + }) + + describe('slotDeleted', () => { + it('should set displaySlotDeleteDialog to false', () => { + component.displaySlotDeleteDialog = true + + component.slotDeleted(false) + + expect(component.displaySlotDeleteDialog).toBe(false) + }) + + it('should call searchProducts when slot has been deleted', () => { + spyOn(component, 'searchProducts') + component.displaySlotDeleteDialog = true + + component.slotDeleted(true) + + expect(component.displaySlotDeleteDialog).toBe(false) + expect(component.searchProducts).toHaveBeenCalled() + }) + + it('should not call searchProducts when slot has not been deleted', () => { + spyOn(component, 'searchProducts') + component.displaySlotDeleteDialog = true + + component.slotDeleted(false) + + expect(component.displaySlotDeleteDialog).toBe(false) + expect(component.searchProducts).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/app/product-store/product-detail/product-apps/product-apps.component.ts b/src/app/product-store/product-detail/product-apps/product-apps.component.ts index 6a18f89..33e70c1 100644 --- a/src/app/product-store/product-detail/product-apps/product-apps.component.ts +++ b/src/app/product-store/product-detail/product-apps/product-apps.component.ts @@ -17,7 +17,7 @@ import { IconService } from 'src/app/shared/iconservice' import { AppAbstract, ChangeMode } from '../../app-search/app-search.component' -enum AppType { +export enum AppType { MS = 'MS', MFE = 'MFE' } diff --git a/src/app/product-store/product-detail/product-intern/product-intern.component.html b/src/app/product-store/product-detail/product-intern/product-intern.component.html index 693af7a..f86f44c 100644 --- a/src/app/product-store/product-detail/product-intern/product-intern.component.html +++ b/src/app/product-store/product-detail/product-intern/product-intern.component.html @@ -9,7 +9,7 @@ inputId="product_detail_intern_field_operator" [(ngModel)]="operator" [binary]="true" - [label]="'PRODUCT.OPERATOR' | translate" + [label]="'INTERNAL.OPERATOR' | translate" [pTooltip]="'PRODUCT.TOOLTIPS.OPERATOR' | translate" tooltipPosition="top" tooltipEvent="hover" diff --git a/src/app/product-store/product-detail/product-props/product-props.component.html b/src/app/product-store/product-detail/product-props/product-props.component.html index a8e579b..7a65182 100644 --- a/src/app/product-store/product-detail/product-props/product-props.component.html +++ b/src/app/product-store/product-detail/product-props/product-props.component.html @@ -2,15 +2,17 @@
- - > - + + diff --git a/src/app/product-store/product-store.module.ts b/src/app/product-store/product-store.module.ts index 88352bf..3619331 100644 --- a/src/app/product-store/product-store.module.ts +++ b/src/app/product-store/product-store.module.ts @@ -15,6 +15,7 @@ import { SharedModule } from 'src/app/shared/shared.module' import { AppSearchComponent } from './app-search/app-search.component' import { AppDeleteComponent } from './app-delete/app-delete.component' import { AppDetailComponent } from './app-detail/app-detail.component' +import { AppInternComponent } from './app-detail/app-intern/app-intern.component' import { ProductSearchComponent } from './product-search/product-search.component' import { ProductDetailComponent } from './product-detail/product-detail.component' import { ProductPropertyComponent } from './product-detail/product-props/product-props.component' @@ -59,6 +60,7 @@ const routes: Routes = [ AppSearchComponent, AppDeleteComponent, AppDetailComponent, + AppInternComponent, ProductSearchComponent, ProductDetailComponent, ProductPropertyComponent, diff --git a/src/app/product-store/slot-search/slot-search.component.html b/src/app/product-store/slot-search/slot-search.component.html index 4c6b8e8..313255e 100644 --- a/src/app/product-store/slot-search/slot-search.component.html +++ b/src/app/product-store/slot-search/slot-search.component.html @@ -95,7 +95,7 @@ tooltipPosition="top" tooltipEvent="hover" > - + >