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 7a25718..1c5acab 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 @@ -75,6 +75,25 @@ +
+
+ + + + +
+
+
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 d9d9569..7ee1914 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 @@ -45,9 +45,8 @@
-
+
- -
-
- - - - - - - -
-
+ + + + + +
-
- - - - -
@@ -124,16 +109,15 @@
- + - +
diff --git a/src/app/product-store/product-detail/product-props/product-props.component.spec.ts b/src/app/product-store/product-detail/product-props/product-props.component.spec.ts index 0a1522f..9c97579 100644 --- a/src/app/product-store/product-detail/product-props/product-props.component.spec.ts +++ b/src/app/product-store/product-detail/product-props/product-props.component.spec.ts @@ -18,7 +18,6 @@ const mockForm = new FormGroup({ Validators.maxLength(255), productNameValidator() ]), - operator: new FormControl(null), version: new FormControl(null, [Validators.required, Validators.maxLength(255)]), description: new FormControl(null, [Validators.maxLength(255)]), imageUrl: new FormControl(null, [Validators.maxLength(255)]), @@ -157,7 +156,6 @@ describe('ProductPropertyComponent', () => { const formGroup = new FormGroup({ id: new FormControl('id'), name: new FormControl('name'), - operator: new FormControl(null), version: new FormControl('version'), description: new FormControl(null), imageUrl: new FormControl(null), @@ -180,7 +178,6 @@ describe('ProductPropertyComponent', () => { const formGroup = new FormGroup({ id: new FormControl('id'), name: new FormControl('name'), - operator: new FormControl(null), version: new FormControl('version'), description: new FormControl(null), imageUrl: new FormControl(null), @@ -203,7 +200,6 @@ describe('ProductPropertyComponent', () => { const formGroup = new FormGroup({ id: new FormControl('id'), name: new FormControl('name'), - operator: new FormControl(null), version: new FormControl('version'), description: new FormControl(null), imageUrl: new FormControl(null), @@ -224,17 +220,26 @@ describe('ProductPropertyComponent', () => { }) }) - it('should display unique constraint error if error code points to it', () => { + it('should display unique constraint error if name already exists', () => { const error = { error: { - errorCode: 'PERSIST_ENTITY_FAILED' + errorCode: 'PERSIST_ENTITY_FAILED', + detail: + "could not execute statement [ERROR: duplicate key value violates unique constraint 'ui_product_name' ...", + params: [ + { + key: 'constraint', + value: + "could not execute statement [ERROR: duplicate key value violates unique constraint 'ui_product_name' ..." + }, + { key: 'constraintName', value: 'ui_product_name' } + ] } } apiServiceSpy.updateProduct.and.returnValue(throwError(() => error)) const formGroup = new FormGroup({ id: new FormControl('id'), name: new FormControl('name'), - operator: new FormControl(null), version: new FormControl('version'), description: new FormControl(null), imageUrl: new FormControl(null), @@ -244,15 +249,56 @@ describe('ProductPropertyComponent', () => { classifications: new FormControl(null) }) component.formGroup = formGroup as FormGroup + component.changeMode = 'EDIT' component.formGroup.controls['name'].setValue('') + + component.onSave() + + expect(component.formGroup.valid).toBeTrue() + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.PRODUCT.NOK', + detailKey: 'VALIDATION.PRODUCT.UNIQUE_CONSTRAINT.NAME' + }) + }) + + it('should display unique constraint error if basepath already exists', () => { + const error = { + error: { + errorCode: 'PERSIST_ENTITY_FAILED', + detail: + "could not execute statement [ERROR: duplicate key value violates unique constraint 'ui_product_base_path' ...", + params: [ + { + key: 'constraint', + value: + "could not execute statement [ERROR: duplicate key value violates unique constraint 'ui_product_base_path' ..." + }, + { key: 'constraintName', value: 'ui_product_base_path' } + ] + } + } + apiServiceSpy.updateProduct.and.returnValue(throwError(() => error)) + const formGroup = new FormGroup({ + id: new FormControl('id'), + name: new FormControl('name'), + version: new FormControl('version'), + description: new FormControl(null), + imageUrl: new FormControl(null), + basePath: new FormControl('path'), + displayName: new FormControl('display'), + iconName: new FormControl('icon'), + classifications: new FormControl(null) + }) + component.formGroup = formGroup as FormGroup component.changeMode = 'EDIT' + component.formGroup.controls['basePath'].setValue('') component.onSave() expect(component.formGroup.valid).toBeTrue() expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.PRODUCT.NOK', - detailKey: 'VALIDATION.PRODUCT.UNIQUE_CONSTRAINT' + detailKey: 'VALIDATION.PRODUCT.UNIQUE_CONSTRAINT.BASEPATH' }) }) @@ -274,7 +320,6 @@ describe('ProductPropertyComponent', () => { const formGroup = new FormGroup({ id: new FormControl('id'), name: new FormControl('name'), - operator: new FormControl(null), version: new FormControl('version'), description: new FormControl(null), imageUrl: new FormControl(null), @@ -298,7 +343,6 @@ describe('ProductPropertyComponent', () => { const formGroup = new FormGroup({ id: new FormControl(null, Validators.required), name: new FormControl('name'), - operator: new FormControl(null), version: new FormControl('version'), description: new FormControl(null), imageUrl: new FormControl(null), @@ -416,7 +460,7 @@ describe('ProductPropertyComponent', () => { it('should return an image url', () => { component.formGroup.controls['imageUrl'].setValue('url') - const result = component.getImageUrl() + const result = component.prepareImageUrl() expect(result).toEqual('url') }) diff --git a/src/app/product-store/product-detail/product-props/product-props.component.ts b/src/app/product-store/product-detail/product-props/product-props.component.ts index 03985b3..4db12ef 100644 --- a/src/app/product-store/product-detail/product-props/product-props.component.ts +++ b/src/app/product-store/product-detail/product-props/product-props.component.ts @@ -20,7 +20,6 @@ import { ChangeMode } from '../product-detail.component' export interface ProductDetailForm { id: FormControl name: FormControl - operator: FormControl version: FormControl description: FormControl imageUrl: FormControl @@ -74,7 +73,6 @@ export class ProductPropertyComponent implements OnChanges, OnInit { productNameValidator() ]), displayName: new FormControl(null, [Validators.required, Validators.minLength(2), Validators.maxLength(255)]), - operator: new FormControl(null), version: new FormControl(null, [Validators.required, Validators.maxLength(255)]), description: new FormControl(null, [Validators.maxLength(255)]), imageUrl: new FormControl(null, [Validators.maxLength(255)]), @@ -88,29 +86,19 @@ export class ProductPropertyComponent implements OnChanges, OnInit { ngOnInit(): void { let productName = this.formGroup.controls['name'].value! - let requestParametersGet: GetImageRequestParams = { - refId: productName, - refType: RefType.Logo - } - if ( - requestParametersGet.refId === undefined || - requestParametersGet.refId === '' || - requestParametersGet.refId === null - ) { + if (!productName) { this.logoImageWasUploaded = false } else { - this.imageApi.getImage(requestParametersGet).subscribe(() => { + this.imageApi.getImage({ refId: productName, refType: RefType.Logo }).subscribe(() => { this.logoImageWasUploaded = true }) } - this.fetchingLogoUrl = this.getImageUrl() + this.fetchingLogoUrl = this.prepareImageUrl() } ngOnChanges(): void { if (this.product) { - this.formGroup.patchValue({ - ...this.product - }) + this.formGroup.patchValue({ ...this.product }) this.productId = this.changeMode !== 'COPY' ? this.product.id : undefined this.productName = this.product.name // business key => manage the change! } else { @@ -135,17 +123,13 @@ export class ProductPropertyComponent implements OnChanges, OnInit { } private createProduct() { - let imgUrl = this.formGroup.controls['imageUrl'].value - if ((imgUrl == '' || imgUrl == null) && !this.logoImageWasUploaded) { - imgUrl = 'http://pragmaticscrum.info/wp-content/uploads/2016/06/t1.jpg' - } this.productApi .createProduct({ createProductRequest: { name: this.formGroup.value['name'], version: this.formGroup.value['version'], description: this.formGroup.value['description'], - imageUrl: imgUrl, + imageUrl: this.formGroup.controls['imageUrl'].value, basePath: this.formGroup.value['basePath'], displayName: this.formGroup.value['displayName'], iconName: this.formGroup.value['iconName'], @@ -162,10 +146,6 @@ export class ProductPropertyComponent implements OnChanges, OnInit { } private updateProduct() { - let imgUrl = this.formGroup.controls['imageUrl'].value - if ((imgUrl == '' || imgUrl == null) && !this.logoImageWasUploaded) { - imgUrl = 'http://pragmaticscrum.info/wp-content/uploads/2016/06/t1.jpg' - } this.productApi .updateProduct({ id: this.productId!, @@ -173,7 +153,7 @@ export class ProductPropertyComponent implements OnChanges, OnInit { name: this.formGroup.value['name'], version: this.formGroup.value['version'], description: this.formGroup.value['description'], - imageUrl: imgUrl, + imageUrl: this.formGroup.controls['imageUrl'].value, basePath: this.formGroup.value['basePath'], displayName: this.formGroup.value['displayName'], iconName: this.formGroup.value['iconName'], @@ -193,7 +173,9 @@ export class ProductPropertyComponent implements OnChanges, OnInit { if (err.error?.errorCode === 'PERSIST_ENTITY_FAILED') { this.msgService.error({ summaryKey: 'ACTIONS.' + this.changeMode + '.PRODUCT.NOK', - detailKey: 'VALIDATION.PRODUCT.UNIQUE_CONSTRAINT' + detailKey: + 'VALIDATION.PRODUCT.UNIQUE_CONSTRAINT.' + + (err.error?.detail.indexOf('ui_product_base_path') > 0 ? 'BASEPATH' : 'NAME') }) } else { this.msgService.error({ summaryKey: 'ACTIONS.' + this.changeMode + '.PRODUCT.NOK' }) @@ -259,7 +241,7 @@ export class ProductPropertyComponent implements OnChanges, OnInit { } } - getImageUrl(): string { + prepareImageUrl(): string { let imgUrl = this.formGroup.controls['imageUrl'].value if (imgUrl == '' || imgUrl == null) { return this.imageApi.configuration.basePath + '/images/' + this.formGroup.controls['name'].value + '/logo' diff --git a/src/app/product-store/product-search/product-search.component.html b/src/app/product-store/product-search/product-search.component.html index 4e6ad4d..5038fdb 100644 --- a/src/app/product-store/product-search/product-search.component.html +++ b/src/app/product-store/product-search/product-search.component.html @@ -86,7 +86,7 @@
@@ -118,7 +118,7 @@
diff --git a/src/app/product-store/product-search/product-search.component.spec.ts b/src/app/product-store/product-search/product-search.component.spec.ts index bc4136b..9d1ee49 100644 --- a/src/app/product-store/product-search/product-search.component.spec.ts +++ b/src/app/product-store/product-search/product-search.component.spec.ts @@ -195,20 +195,20 @@ describe('ProductSearchComponent', () => { expect(result).toBe(1) }) - it('should getImageUrl from existing product', () => { + it('should prepareImageUrl from existing product', () => { const product = { imageUrl: 'url' } - const result = component.getImageUrl(product) + const result = component.prepareImageUrl(product) expect(result).toEqual(product.imageUrl) }) - it('should getImageUrl from image api if not from existing product', () => { + it('should prepareImageUrl from image api if not from existing product', () => { const product = { id: 'id' } - const result = component.getImageUrl(product) + const result = component.prepareImageUrl(product) expect(result).toEqual('http://onecx-product-store-bff:8080/images/undefined/logo') }) diff --git a/src/app/product-store/product-search/product-search.component.ts b/src/app/product-store/product-search/product-search.component.ts index 4dfb934..1337427 100644 --- a/src/app/product-store/product-search/product-search.component.ts +++ b/src/app/product-store/product-search/product-search.component.ts @@ -179,7 +179,7 @@ export class ProductSearchComponent implements OnInit { this.router.navigate(['./new'], { relativeTo: this.route }) } - public getImageUrl(product: any): string { + public prepareImageUrl(product: any): string { if (product.imageUrl) { return product.imageUrl } else { diff --git a/src/app/shared/image-container/image-container.component.html b/src/app/shared/image-container/image-container.component.html index 3e3b1de..7f8e9d7 100644 --- a/src/app/shared/image-container/image-container.component.html +++ b/src/app/shared/image-container/image-container.component.html @@ -1,58 +1,15 @@ Logo - - - + [class]="'image-object image-' + (small ? 'sm' : 'lg') + ' sm:image-lg vertical-align-middle'" + [src]="defaultImageUrl" + alt="Logo" +/> diff --git a/src/app/shared/image-container/image-container.component.ts b/src/app/shared/image-container/image-container.component.ts index 5918a62..3161e76 100644 --- a/src/app/shared/image-container/image-container.component.ts +++ b/src/app/shared/image-container/image-container.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core' import { prepareUrl } from 'src/app/shared/utils' +import { environment } from 'src/environments/environment' @Component({ selector: 'app-image-container', @@ -11,6 +12,7 @@ export class ImageContainerComponent implements OnChanges { @Input() public imageUrl: string | undefined @Input() public small = false + public defaultImageUrl = environment.DEFAULT_LOGO_URL public displayPlaceHolder = false public onImageError() { diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 8dd9380..9d5f1a4 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -227,7 +227,7 @@ "OPERATOR": "Erzeugt durch", "OPERATOR.MANUAL": "Manuell", "OPERATOR.DEPLOYMENT": "Deployment Operator", - "IMAGE_URL": "Image URL", + "IMAGE_URL": "Externe Image URL", "BASE_PATH": "Basispfad", "ICON_NAME": "Iconname", "CLASSIFICATIONS": "Klassifizierungen", @@ -239,7 +239,7 @@ "DESCRIPTION": "Beschreibung des Produkts", "DISPLAY_NAME": "Anzeigename des Produkts", "OPERATOR": "Wie wurde das Produkt angelegt? Wird bei der Erstellung des Produkts bestimmt.", - "IMAGE_URL": "Image URL", + "IMAGE_URL": "URL für ein Image ausserhalb des Product Stores", "BASE_PATH": "Der Basispfad wie das Produkt erreichbar ist. Muss eindeutig über alle Produkte sein", "ICON_NAME": "Name eines Icons aus der PrimeNG Icon Bibliothek. Wird z.b. als Default bei Menüeinträgen benutzt", "CLASSIFICATIONS": "Kommaseparierte Liste von Begriffen zur Klassifizierungen des Produkts, z.b. Game, Tool etc.. Kann zur Filterung benutzt werden", @@ -268,7 +268,8 @@ "FORM_INVALID": "Die Daten sind nicht bereit zum Speichern.", "APP.UNIQUE_CONSTRAINT.REMOTE_MODULE": "Eine App mit dieser Remote Konfiguration existiert bereits für das Produkt.", "APP.UNIQUE_CONSTRAINT.APP_ID": "Eine App mit dieser ID existiert bereits für das Produkt.", - "PRODUCT.UNIQUE_CONSTRAINT": "Ein Produkt mit diesem Namen existiert bereits.", + "PRODUCT.UNIQUE_CONSTRAINT.NAME": "Ein Produkt mit diesem Namen existiert bereits.", + "PRODUCT.UNIQUE_CONSTRAINT.BASEPATH": "Ein Produkt mit diesem Basepath existiert bereits.", "PRODUCT.INVALID_NAME": "Dieser Name ist reserviert und kann nicht verwendet werden.", "ERRORS": { "PARSE_ERROR": "Parserfehler: Die enthaltene Struktur entspricht nicht dem erwarteten Format.", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ea5c2c3..ece8cd3 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -227,7 +227,7 @@ "OPERATOR": "Created by", "OPERATOR.MANUAL": "Manual", "OPERATOR.DEPLOYMENT": "Deployment Operator", - "IMAGE_URL": "Image URL", + "IMAGE_URL": "Extern Image URL", "BASE_PATH": "Base Path", "ICON_NAME": "Icon Name", "CLASSIFICATIONS": "Classifications", @@ -238,8 +238,8 @@ "VERSION": "Version of the Product", "DESCRIPTION": "Description of the Product", "DISPLAY_NAME": "Display Name of the Product", - "OPERATOR": "How the Product was created? Set at creation time.", - "IMAGE_URL": "Image URL", + "OPERATOR": "How the Product was created? Set at creation time", + "IMAGE_URL": "URL for an image outside the Product Store", "BASE_PATH": "The basic path through which the product can be accessed. It must be unique across all products", "ICON_NAME": "Name of the icons from the PrimeNG icon library. Used, for example, as a default icon for menu entries", "CLASSIFICATIONS": "Comma-separated list of terms to classify the product, e.g. Game, Tool etc. Can be used for filtering", @@ -268,7 +268,8 @@ "FORM_INVALID": "The data are not ready to store.", "APP.UNIQUE_CONSTRAINT.REMOTE_MODULE": "An app with this remote configuration already exists for the product.", "APP.UNIQUE_CONSTRAINT.APP_ID": "An App with this id exists already for the Product.", - "PRODUCT.UNIQUE_CONSTRAINT": "A Product with this name exists already.", + "PRODUCT.UNIQUE_CONSTRAINT.NAME": "A Product with this name exists already.", + "PRODUCT.UNIQUE_CONSTRAINT.BASEPATH": "A Product with this basepath exists already.", "PRODUCT.INVALID_NAME": "This name is a reserved term and cannot be used.", "ERRORS": { "PARSE_ERROR": "Parse error: The contained structure does not match the expected format.", diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 9c02573..5071f7e 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,5 +1,6 @@ export const environment = { production: true, BASE_PATH: '/bff', - apiPrefix: 'bff' + apiPrefix: 'bff', + DEFAULT_LOGO_URL: 'http://pragmaticscrum.info/wp-content/uploads/2016/06/t1.jpg' } diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 25bf5d6..4adbe3a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -8,5 +8,6 @@ export const environment = { KEYCLOAK_URL: 'http://keycloak-app/', KEYCLOAK_REALM: 'OneCX', skipRemoteConfigLoad: true, - apiPrefix: 'bff' + apiPrefix: 'bff', + DEFAULT_LOGO_URL: 'http://pragmaticscrum.info/wp-content/uploads/2016/06/t1.jpg' }