From 254e37c9c54f65ede35cc6d4fcfd8eeb89736753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20T=C3=A4schner?= <129834483+HenryT-CG@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:37:17 +0100 Subject: [PATCH] fix: product list on workspace creation (#455) * fix: product list on workspace creation * fix: dropdown lists on creation * fix: tests * fix: sonar --- sonar-local-project.properties | 20 +++-- .../horizontal-main-menu.component.spec.ts | 8 +- .../user-avatar-menu.component.spec.ts | 8 +- .../user-avatar-menu.component.ts | 12 ++- .../workspace-create.component.html | 76 ++++++++-------- .../workspace-create.component.spec.ts | 87 +++++++++++++----- .../workspace-create.component.ts | 88 ++++++++----------- .../workspace-detail.component.ts | 4 +- .../workspace-menu/menu.component.html | 7 -- .../workspace-menu/menu.component.ts | 1 - 10 files changed, 172 insertions(+), 139 deletions(-) diff --git a/sonar-local-project.properties b/sonar-local-project.properties index ab43eb67..2f6a5b40 100644 --- a/sonar-local-project.properties +++ b/sonar-local-project.properties @@ -7,19 +7,25 @@ # npm run sonar # sonar.host.url=http://localhost:9000 -sonar.login= +# sonar.verbose=true # sonar.organization=onecx sonar.projectKey=onecx-workspace-ui sonar.projectName=onecx-workspace-ui sonar.javascript.coveragePlugin=lcov -sonar.javascript.lcov.reportPaths=reports/coverage/lcov.info +sonar.javascript.lcov.reportPaths=reports/coverage/lcov.js.info +sonar.typescript.lcov.reportPaths=reports/coverage/lcov.ts.info sonar.testExecutionReportPaths=reports/sonarqube_report.xml sonar.sourceEncoding=UTF-8 +sonar.sources=src/app sonar.exclusions=node_modules/**/*,src/app/shared/generated/**/* +# duplication detection sonar.cpd.exclusions=src/app/shared/generated/**/* -sonar.coverage.exclusions=*.ts,*.js,src/*.ts,src/**/*.module.ts,src/environments/*,src/assets/**/*,src/app/shared/generated/**/* -sonar.sources=src/app -#sonar.working.directory=dist/sonar -#sonar.tests=src/app -sonar.test.inclusions=src/app/**/*.spec.ts \ No newline at end of file +# coverage +sonar.coverage.exclusions=*.ts,*.js,src/*.ts,src/**/*.module.ts,src/**/*.main.ts,src/**/*.bootstrap.ts,src/environments/*,src/assets/**/*,src/app/shared/generated/**/* +# sonar.working.directory=dist/sonar +sonar.test.inclusions=src/app/**/*.spec.ts +# sonar.scm.exclusions.disabled=true +sonar.language=ts,js,json,css,web +# sonar.lang.patterns.ts=**/*.ts +# sonar.typescript.tsconfigPath=tsconfig.json diff --git a/src/app/remotes/horizontal-main-menu/horizontal-main-menu.component.spec.ts b/src/app/remotes/horizontal-main-menu/horizontal-main-menu.component.spec.ts index 2537b8e3..5543ca14 100644 --- a/src/app/remotes/horizontal-main-menu/horizontal-main-menu.component.spec.ts +++ b/src/app/remotes/horizontal-main-menu/horizontal-main-menu.component.spec.ts @@ -23,7 +23,6 @@ describe('OneCXHorizontalMainMenuComponent', () => { const fixture = TestBed.createComponent(OneCXHorizontalMainMenuComponent) const component = fixture.componentInstance fixture.detectChanges() - return { fixture, component } } @@ -40,11 +39,8 @@ describe('OneCXHorizontalMainMenuComponent', () => { providers: [ provideHttpClient(), provideHttpClientTesting(), - { - provide: BASE_URL, - useValue: baseUrlSubject - }, - provideRouter([{ path: 'admin/welcome', component: OneCXHorizontalMainMenuComponent }]) + provideRouter([{ path: 'admin/welcome', component: OneCXHorizontalMainMenuComponent }]), + { provide: BASE_URL, useValue: baseUrlSubject } ] }) .overrideComponent(OneCXHorizontalMainMenuComponent, { diff --git a/src/app/remotes/user-avatar-menu/user-avatar-menu.component.spec.ts b/src/app/remotes/user-avatar-menu/user-avatar-menu.component.spec.ts index 6ac43557..f97f38d1 100644 --- a/src/app/remotes/user-avatar-menu/user-avatar-menu.component.spec.ts +++ b/src/app/remotes/user-avatar-menu/user-avatar-menu.component.spec.ts @@ -94,7 +94,7 @@ describe('OneCXUserAvatarMenuComponent', () => { }) .compileComponents() - baseUrlSubject.next('base_url') + baseUrlSubject.next('base_url_mock') menuItemApiSpy.getMenuItems.calls.reset() appConfigSpy.init.and.returnValue(Promise.resolve()) @@ -210,9 +210,7 @@ describe('OneCXUserAvatarMenuComponent', () => { spyOn(userService.profile$, 'asObservable').and.returnValue(of(profile) as any) const appStateService = TestBed.inject(AppStateService) spyOn(appStateService.currentWorkspace$, 'asObservable').and.returnValue( - of({ - workspaceName: 'test-workspace' - }) as any + of({ workspaceName: 'test-workspace' }) as any ) }) @@ -393,7 +391,7 @@ describe('OneCXUserAvatarMenuComponent', () => { }) xit('should have correct icon for logout', async () => { - menuItemApiSpy.getMenuItems.and.returnValue(of({ workspaceName: 'workspace', menu: [] } as any)) + menuItemApiSpy.getMenuItems.and.returnValue(of({ workspaceName: 'test-workspace', menu: [] } as any)) const { avatarMenuHarness } = await setUpWithHarness() const menuItems = await avatarMenuHarness.getMenuItems() diff --git a/src/app/remotes/user-avatar-menu/user-avatar-menu.component.ts b/src/app/remotes/user-avatar-menu/user-avatar-menu.component.ts index 54990915..4b6e4db1 100644 --- a/src/app/remotes/user-avatar-menu/user-avatar-menu.component.ts +++ b/src/app/remotes/user-avatar-menu/user-avatar-menu.component.ts @@ -1,7 +1,15 @@ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' +import { + APP_INITIALIZER, + AfterViewInit, + Component, + CUSTOM_ELEMENTS_SCHEMA, + Inject, + Input, + OnDestroy, + Renderer2 +} from '@angular/core' import { Location } from '@angular/common' import { HttpClient } from '@angular/common/http' -import { APP_INITIALIZER, AfterViewInit, Component, Inject, Input, OnDestroy, Renderer2 } from '@angular/core' import { FormsModule } from '@angular/forms' import { RouterModule } from '@angular/router' import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' diff --git a/src/app/workspace/workspace-create/workspace-create.component.html b/src/app/workspace/workspace-create/workspace-create.component.html index 6b6c283b..b85dcbd8 100644 --- a/src/app/workspace/workspace-create/workspace-create.component.html +++ b/src/app/workspace/workspace-create/workspace-create.component.html @@ -18,65 +18,65 @@ - + - -
+
- + + + + + + -
- - - - - -
@@ -84,23 +84,23 @@ - + -
@@ -108,7 +108,7 @@ - +
@@ -125,14 +125,14 @@ - +
@@ -143,13 +143,13 @@ pInputTextarea autoresize="true" rows="4" - id="ws_create_item_description" + id="ws_create_form_description" formControlName="description" [pTooltip]="'WORKSPACE.TOOLTIPS.DESCRIPTION' | translate" tooltipPosition="top" tooltipEvent="hover" > - + diff --git a/src/app/workspace/workspace-create/workspace-create.component.spec.ts b/src/app/workspace/workspace-create/workspace-create.component.spec.ts index cb87df2a..06da2308 100644 --- a/src/app/workspace/workspace-create/workspace-create.component.spec.ts +++ b/src/app/workspace/workspace-create/workspace-create.component.spec.ts @@ -26,7 +26,7 @@ const workspace: Workspace = { theme: 'theme', baseUrl: '/some/base/url', homePage: '/homepage', - displayName: '' + displayName: 'displayName' } class MockRouter { @@ -120,27 +120,6 @@ describe('WorkspaceCreateComponent', () => { expect(component).toBeTruthy() }) - describe('loadMfeUrls', () => { - it('should load product urls on init', () => { - productServiceSpy.searchAvailableProducts.and.returnValue(of({ stream: [{ baseUrl: 'baseUrl' }] })) - component.mfeRList = [] - - component.ngOnInit() - - expect(component.mfeRList).toContain('baseUrl') - }) - - it('should log error if api call fails', () => { - const errorResponse = { status: 400, statusText: 'Error on searching products' } - productServiceSpy.searchAvailableProducts.and.returnValue(throwError(() => errorResponse)) - spyOn(console, 'error') - - component.ngOnInit() - - expect(console.error).toHaveBeenCalledWith('getProductsByWorkspaceId', errorResponse) - }) - }) - it('should create a workspace', () => { wApiServiceSpy.createWorkspace.and.returnValue(of({ resource: workspace })) @@ -186,4 +165,68 @@ describe('WorkspaceCreateComponent', () => { expect(component.fetchingLogoUrl).toBe(url) })) + + describe('onOpenProductPathes', () => { + it('should load product urls', () => { + const products = [{ baseUrl: '/productBaseUrl-1' }, { baseUrl: '/productBaseUrl-2' }] + productServiceSpy.searchAvailableProducts.and.returnValue(of({ stream: products })) + + component.onOpenProductPathes([]) + + component.productPaths$.subscribe((paths) => { + expect(paths).toEqual([products[0].baseUrl, products[1].baseUrl]) + }) + }) + + it('should prevent loading product URLs again', () => { + const paths = ['/productBaseUrl-1', '/productBaseUrl-2'] + + component.onOpenProductPathes(paths) + }) + + it('should load product paths failed', () => { + const errorResponse = { status: 400, statusText: 'Error on loading product paths' } + productServiceSpy.searchAvailableProducts.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') + + component.onOpenProductPathes([]) + + component.productPaths$.subscribe((paths) => { + expect(paths).toEqual([]) + expect(console.error).toHaveBeenCalledWith('searchAvailableProducts', errorResponse) + }) + }) + }) + + describe('onOpenThemes', () => { + it('should load themes', () => { + const themes = ['theme-1', 'theme-2'] + wApiServiceSpy.getAllThemes.and.returnValue(of(themes)) + + component.onOpenThemes([]) + + component.themes$.subscribe((data) => { + expect(data).toEqual(themes) + }) + }) + + it('should prevent loading product URLs again', () => { + const themes = ['theme-1', 'theme-2'] + + component.onOpenThemes(themes) + }) + + it('should load themes failed', () => { + const errorResponse = { status: 400, statusText: 'Error on loading themes' } + wApiServiceSpy.getAllThemes.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') + + component.onOpenThemes([]) + + component.themes$.subscribe((data) => { + expect(data).toEqual([]) + expect(console.error).toHaveBeenCalledWith('getAllThemes', errorResponse) + }) + }) + }) }) diff --git a/src/app/workspace/workspace-create/workspace-create.component.ts b/src/app/workspace/workspace-create/workspace-create.component.ts index c924567d..3a9a167f 100644 --- a/src/app/workspace/workspace-create/workspace-create.component.ts +++ b/src/app/workspace/workspace-create/workspace-create.component.ts @@ -1,10 +1,8 @@ -import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core' +import { Component, Input, Output, EventEmitter } from '@angular/core' import { FormControl, FormGroup, Validators } from '@angular/forms' -import { Location } from '@angular/common' import { ActivatedRoute, Router } from '@angular/router' import { TranslateService } from '@ngx-translate/core' -import { Observable, Subject, catchError, map, of, takeUntil } from 'rxjs' -import { SelectItem } from 'primeng/api/selectitem' +import { Observable, catchError, map, of } from 'rxjs' import { PortalMessageService } from '@onecx/angular-integration-interface' @@ -16,23 +14,17 @@ import { WorkspaceAPIService, ProductAPIService } from 'src/app/shared/generated templateUrl: './workspace-create.component.html', styleUrls: ['./workspace-create.component.scss'] }) -export class WorkspaceCreateComponent implements OnInit { +export class WorkspaceCreateComponent { @Input() displayDialog = false @Output() toggleCreationDialogEvent = new EventEmitter() - private readonly destroy$ = new Subject() - public themes$!: Observable[]> + public themes$: Observable = of([]) + public productPaths$: Observable = of([]) public formGroup: FormGroup - public hasPermission = false public selectedLogoFile: File | undefined - public preview = false - public previewSrc: string | undefined public minimumImageWidth = 150 public minimumImageHeight = 150 - public workspaceCreationValidationMsg = false public fetchingLogoUrl?: string - public urlPattern = '/base-path-to-workspace' - public mfeRList: string[] = [] constructor( private readonly router: Router, @@ -54,29 +46,14 @@ export class WorkspaceCreateComponent implements OnInit { }) } - ngOnInit(): void { - this.themes$ = this.workspaceApi.getAllThemes().pipe( - map((val: any[]) => { - val.sort(sortByLocale) - return val - }) - ) - this.loadMfeUrls() - } - - closeDialog() { + public closeDialog(): void { this.formGroup.reset() this.fetchingLogoUrl = undefined this.selectedLogoFile = undefined this.toggleCreationDialogEvent.emit() } - saveWorkspace() { - if (this.formGroup.controls['homePage'].value) { - this.formGroup.controls['homePage'].setValue( - Location.joinWithSlash(this.formGroup.controls['baseUrl'].value, this.formGroup.controls['homePage'].value) - ) - } + public saveWorkspace(): void { this.workspaceApi .createWorkspace({ createWorkspaceRequest: { resource: this.formGroup.value } @@ -85,7 +62,6 @@ export class WorkspaceCreateComponent implements OnInit { .subscribe({ next: (fetchedWorkspace) => { this.message.success({ summaryKey: 'ACTIONS.CREATE.MESSAGE.CREATE_OK' }) - this.workspaceCreationValidationMsg = false this.closeDialog() this.router.navigate(['./' + fetchedWorkspace.resource?.name], { relativeTo: this.route }) }, @@ -100,24 +76,38 @@ export class WorkspaceCreateComponent implements OnInit { this.fetchingLogoUrl = (event.target as HTMLInputElement).value } - private loadMfeUrls(): void { - this.productApi - .searchAvailableProducts({ productStoreSearchCriteria: {} }) - .pipe( - map((result) => { - if (result.stream) { - for (const p of result.stream) { - if (p.baseUrl) this.mfeRList.push(p.baseUrl) - } - this.mfeRList.sort(sortByLocale) + public onOpenProductPathes(paths: string[]) { + // if paths already filled then prevent doing twice + if (paths.length > 0) return + this.productPaths$ = this.productApi.searchAvailableProducts({ productStoreSearchCriteria: {} }).pipe( + map((result) => { + const paths: string[] = [] + if (result.stream) { + for (const p of result.stream) { + if (p.baseUrl) paths.push(p.baseUrl) } - }), - catchError((err) => { - console.error('getProductsByWorkspaceId', err) - return of([] as string[]) - }) - ) - .pipe(takeUntil(this.destroy$)) - .subscribe() + paths.sort(sortByLocale) + } + return paths + }), + catchError((err) => { + console.error('searchAvailableProducts', err) + return of([] as string[]) + }) + ) + } + + public onOpenThemes(themes: string[]) { + // if paths already filled then prevent doing twice + if (themes.length > 0) return + this.themes$ = this.workspaceApi.getAllThemes().pipe( + map((data: string[]) => { + return data ? data.sort(sortByLocale) : [] + }), + catchError((err) => { + console.error('getAllThemes', err) + return of([] as string[]) + }) + ) } } diff --git a/src/app/workspace/workspace-detail/workspace-detail.component.ts b/src/app/workspace/workspace-detail/workspace-detail.component.ts index cd271ee8..3b8d89b6 100644 --- a/src/app/workspace/workspace-detail/workspace-detail.component.ts +++ b/src/app/workspace/workspace-detail/workspace-detail.component.ts @@ -221,7 +221,7 @@ export class WorkspaceDetailComponent implements OnInit, AfterViewInit { 'ACTIONS.EDIT.LABEL', 'ACTIONS.EDIT.TOOLTIP', 'ACTIONS.DELETE.LABEL', - 'ACTIONS.DELETE.WORKSPACE' + 'ACTIONS.DELETE.WORKSPACE.HEADER' ]) .pipe( map((data) => { @@ -288,7 +288,7 @@ export class WorkspaceDetailComponent implements OnInit, AfterViewInit { }, { label: data['ACTIONS.DELETE.LABEL'], - title: data['ACTIONS.DELETE.WORKSPACE'], + title: data['ACTIONS.DELETE.WORKSPACE.HEADER'], actionCallback: () => { this.workspaceDeleteVisible = true }, diff --git a/src/app/workspace/workspace-menu/menu.component.html b/src/app/workspace/workspace-menu/menu.component.html index 361858c7..8deb3059 100644 --- a/src/app/workspace/workspace-menu/menu.component.html +++ b/src/app/workspace/workspace-menu/menu.component.html @@ -29,13 +29,6 @@ - public workspaceName: string = this.route.snapshot.params['name'] - private mfeRUrls: Array = [] public wRoles$!: Observable public wRoles: WorkspaceRole[] = [] public wRolesFiltered: WorkspaceRole[] = []