From 964b3d0cfd18c93ef746c5e0f6ad7f0061757852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20T=C3=A4schner?= <129834483+HenryT-CG@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:55:24 +0100 Subject: [PATCH] fix: various things (#470) * fix: various things * fix: toggle-button in menu * fix: toggle-button in menu * fix: add role filter * fix: tests * fix: tuning menu layout * fix: tuning menu layout - colors * fix: tuning menu layout - opt * fix: tuning menu layout - switch buttons * fix: tuning menu layout - translations --- src/app/_ws-mixins.scss | 8 + src/app/shared/services/menu-item.service.ts | 19 +- .../workspace-create.component.html | 14 +- .../workspace-detail.component.spec.ts | 37 +- .../workspace-props.component.html | 12 +- .../workspace-roles.component.html | 6 +- .../workspace-roles.component.spec.ts | 15 +- .../workspace-roles.component.ts | 4 +- .../workspace-slots.component.spec.ts | 34 +- .../workspace-slots.component.ts | 6 +- .../preview/preview.component.html | 2 +- .../menu-detail/menu-detail.component.html | 12 +- .../menu-intern/menu-intern.component.html | 4 +- .../menu-preview/menu-preview.component.html | 4 +- .../menu-preview/menu-preview.component.ts | 3 +- .../workspace-menu/menu.component.html | 198 +++++----- .../workspace-menu/menu.component.scss | 47 ++- .../workspace-menu/menu.component.spec.ts | 351 ++++++++++-------- .../workspace-menu/menu.component.ts | 170 +++++---- .../services/menu-tree.service.ts | 1 + .../products.component.spec.ts | 44 ++- .../workspace-product/products.component.ts | 12 +- .../workspace-role-detail.component.html | 2 +- .../workspace-role-detail.component.spec.ts | 6 + .../workspace-role-detail.component.ts | 6 +- .../workspace-search.component.spec.ts | 49 ++- .../workspace-search.component.ts | 13 +- src/assets/i18n/de.json | 10 +- src/assets/i18n/en.json | 10 +- tsconfig.app.json | 2 +- 30 files changed, 641 insertions(+), 460 deletions(-) diff --git a/src/app/_ws-mixins.scss b/src/app/_ws-mixins.scss index 66c7bb94..5425e9e8 100644 --- a/src/app/_ws-mixins.scss +++ b/src/app/_ws-mixins.scss @@ -60,6 +60,14 @@ } } } +@mixin readable-disabled-components { + :host ::ng-deep { + .p-component:disabled, + .p-disabled { + opacity: unset; + } + } +} @mixin compact-dropdown-list-items { :host ::ng-deep { diff --git a/src/app/shared/services/menu-item.service.ts b/src/app/shared/services/menu-item.service.ts index cd13069d..22d31bd8 100644 --- a/src/app/shared/services/menu-item.service.ts +++ b/src/app/shared/services/menu-item.service.ts @@ -55,21 +55,20 @@ export class MenuItemService { } private expandCurrentMfeMenuItems(items: MenuItem[], currentMfePath: string): boolean { + let expanded = false for (const item of items) { - if (this.stripPath(item.routerLink) === currentMfePath) return true - else if (item.items && this.expandCurrentMfeMenuItems(item.items, currentMfePath)) { - item.expanded = true - return true - } + if (!expanded) + if (this.stripPath(item.routerLink) === currentMfePath) expanded = true + else if (item.items && this.expandCurrentMfeMenuItems(item.items, currentMfePath)) { + item.expanded = true + expanded = true + } } - return false + return expanded } private replaceUrlVariables(url: string | undefined): string | undefined { - if (!url) { - return - } - return url.replaceAll( + return url?.replaceAll( /\[\[(.+?)\]\]/g, //NOSONAR (_match, $1) => { return sessionStorage.getItem($1) ?? localStorage.getItem($1) ?? '' diff --git a/src/app/workspace/workspace-create/workspace-create.component.html b/src/app/workspace/workspace-create/workspace-create.component.html index 248d0e46..380e7c3a 100644 --- a/src/app/workspace/workspace-create/workspace-create.component.html +++ b/src/app/workspace/workspace-create/workspace-create.component.html @@ -25,7 +25,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'WORKSPACE.NAME' | translate }} + {{ 'WORKSPACE.NAME' | translate }} - {{ 'WORKSPACE.THEME' | translate }} + {{ 'WORKSPACE.THEME' | translate }} - {{ 'WORKSPACE.HOME_PAGE' | translate }} + {{ 'WORKSPACE.HOME_PAGE' | translate }} @@ -92,7 +92,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'WORKSPACE.BASE_URL' | translate }} + {{ 'WORKSPACE.BASE_URL' | translate }} {{ ('VALIDATION.HINTS.FORMAT_URL' | translate) + '/base-path-to-workspace' }} @@ -117,7 +117,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'IMAGE.LOGO_URL' | translate }} + {{ 'IMAGE.LOGO_URL' | translate }} @@ -133,7 +133,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'WORKSPACE.FOOTER_LABEL' | translate }} + {{ 'WORKSPACE.FOOTER_LABEL' | translate }} @@ -150,7 +150,7 @@ tooltipPosition="top" tooltipEvent="hover" > - {{ 'WORKSPACE.DESCRIPTION' | translate }} + {{ 'WORKSPACE.DESCRIPTION' | translate }} diff --git a/src/app/workspace/workspace-detail/workspace-detail.component.spec.ts b/src/app/workspace/workspace-detail/workspace-detail.component.spec.ts index f90bf5c2..477efe1a 100644 --- a/src/app/workspace/workspace-detail/workspace-detail.component.spec.ts +++ b/src/app/workspace/workspace-detail/workspace-detail.component.spec.ts @@ -84,13 +84,16 @@ describe('WorkspaceDetailComponent', () => { { provide: UserService, useValue: mockUserService } ] }).compileComponents() + // to spy data: reset + locationSpy.back.calls.reset() msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() apiServiceSpy.getWorkspaceByName.calls.reset() apiServiceSpy.deleteWorkspace.calls.reset() apiServiceSpy.exportWorkspaces.calls.reset() apiServiceSpy.updateWorkspace.calls.reset() - locationSpy.back.calls.reset() + // to spy data: refill with neutral data + apiServiceSpy.getWorkspaceByName.and.returnValue(of({})) })) function initializeComponent(): void { @@ -128,35 +131,15 @@ describe('WorkspaceDetailComponent', () => { }) component.onTabChange(event, component.workspace) - expect(component.selectedTabIndex).toEqual(1) - }) - - it('should set workspace for roles', () => { - const event = { index: 3 } - - component.onTabChange(event, component.workspace) + component.onTabChange({ index: 3 }, component.workspace) expect(component.workspaceForRoles).toBe(workspace) - }) - - it('should set workspace for slots', () => { - const event = { - index: 4 - } - - component.onTabChange(event, component.workspace) + component.onTabChange({ index: 4 }, component.workspace) expect(component.workspaceForSlots).toBe(workspace) - }) - - it('should set workspace for products', () => { - const event = { - index: 5 - } - - component.onTabChange(event, component.workspace) + component.onTabChange({ index: 5 }, component.workspace) expect(component.workspaceForProducts).toBe(workspace) }) }) @@ -391,8 +374,9 @@ describe('WorkspaceDetailComponent', () => { }) describe('update workspace data', () => { - it('it should display error on update workspace', (done) => { + it('it should display success on update workspace', (done) => { apiServiceSpy.updateWorkspace.and.returnValue(of(workspace)) + spyOn(console, 'error') component.selectedTabIndex = 99 component.ngOnInit() let actions: any = [] @@ -400,6 +384,7 @@ describe('WorkspaceDetailComponent', () => { actions[3].actionCallback() + expect(console.error).toHaveBeenCalledWith("Couldn't assign tab to component") expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.CHANGE_OK' }) component.workspace$.subscribe((data) => { expect(data).toEqual(workspace) @@ -412,10 +397,10 @@ describe('WorkspaceDetailComponent', () => { apiServiceSpy.updateWorkspace.and.returnValue(throwError(() => errorResponse)) component.selectedTabIndex = 99 spyOn(console, 'error') + component.ngOnInit() let actions: any = [] component.actions$!.subscribe((act) => (actions = act)) - actions[3].actionCallback() expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.CHANGE_NOK' }) diff --git a/src/app/workspace/workspace-detail/workspace-props/workspace-props.component.html b/src/app/workspace/workspace-detail/workspace-props/workspace-props.component.html index 993e703d..62dea139 100644 --- a/src/app/workspace/workspace-detail/workspace-props/workspace-props.component.html +++ b/src/app/workspace/workspace-detail/workspace-props/workspace-props.component.html @@ -85,7 +85,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'WORKSPACE.BASE_URL' | translate }} + {{ 'WORKSPACE.BASE_URL' | translate }} - {{ 'WORKSPACE.HOME_PAGE' | translate }} + {{ 'WORKSPACE.HOME_PAGE' | translate }} - {{ 'WORKSPACE.RSS_FEED_URL' | translate }} + {{ 'WORKSPACE.RSS_FEED_URL' | translate }} - {{ 'IMAGE.LOGO_URL' | translate }} + {{ 'IMAGE.LOGO_URL' | translate }} {{ ('VALIDATION.HINTS.FORMAT_URL' | translate) + externUrlPattern }} @@ -228,7 +228,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'WORKSPACE.FOOTER_LABEL' | translate }} + {{ 'WORKSPACE.FOOTER_LABEL' | translate }} @@ -244,7 +244,7 @@ tooltipPosition="top" tooltipEvent="hover" > - {{ 'WORKSPACE.DESCRIPTION' | translate }} + {{ 'WORKSPACE.DESCRIPTION' | translate }} diff --git a/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.html b/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.html index ce9956d9..bc31e5fb 100644 --- a/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.html +++ b/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.html @@ -53,9 +53,9 @@ {{ i.label | translate }} - {{ 'DIALOG.ROLE.QUICK_FILTER.LABEL' | translate }}: - {{ 'DIALOG.ROLE.QUICK_FILTER.' + quickFilterValue | translate }} - {{ onGetQuickFilterCount(quickFilterValue) }} + {{ 'DIALOG.ROLE.QUICK_FILTER.LABEL' | translate }}: + {{ 'DIALOG.ROLE.QUICK_FILTER.' + quickFilterValue | translate }} + {{ onGetQuickFilterCount(quickFilterValue) }} diff --git a/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.spec.ts b/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.spec.ts index 47b818e9..318c31bc 100644 --- a/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.spec.ts +++ b/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.spec.ts @@ -91,12 +91,15 @@ describe('WorkspaceRolesComponent', () => { component = fixture.componentInstance component.workspace = workspace fixture.detectChanges() + // to spy data: reset wRoleServiceSpy.searchWorkspaceRoles.calls.reset() wRoleServiceSpy.createWorkspaceRole.calls.reset() wRoleServiceSpy.deleteWorkspaceRole.calls.reset() iamRoleServiceSpy.searchAvailableRoles.calls.reset() msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() + // to spy data: refill with neutral data + wRoleServiceSpy.searchWorkspaceRoles.and.returnValue(of({})) }) it('should create', () => { @@ -161,6 +164,7 @@ describe('WorkspaceRolesComponent', () => { it('should display error on ws search', () => { const errorResponse = { status: 404, statusText: 'Workspace roles not found' } wRoleServiceSpy.searchWorkspaceRoles.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') const changes = { ['workspace']: { previousValue: 'ws0', @@ -172,10 +176,11 @@ describe('WorkspaceRolesComponent', () => { component.ngOnChanges(changes as unknown as SimpleChanges) + expect(console.error).toHaveBeenCalledWith('searchAvailableRoles', errorResponse) expect(component.exceptionKey).toEqual('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.WS_ROLES') }) - it('should populate iamRoles on search', () => { + it('should populate IAM roles on search', () => { iamRoleServiceSpy.searchAvailableRoles.and.returnValue(of({ stream: [{ name: 'role' }] as IAMRolePageResult })) const changes = { ['workspace']: { @@ -228,9 +233,10 @@ describe('WorkspaceRolesComponent', () => { expect(component.roles[0]).toBeUndefined() }) - it('should display error on iam search', () => { + it('should display error on IAM search', () => { const errorResponse = { status: 404, statusText: 'IAM roles not found' } iamRoleServiceSpy.searchAvailableRoles.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') const changes = { ['workspace']: { previousValue: 'ws0', @@ -242,6 +248,7 @@ describe('WorkspaceRolesComponent', () => { component.ngOnChanges(changes as unknown as SimpleChanges) + expect(console.error).toHaveBeenCalledWith('searchAvailableRoles', errorResponse) expect(component.exceptionKey).toEqual('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.IAM_ROLES') }) @@ -282,7 +289,7 @@ describe('WorkspaceRolesComponent', () => { component.onAddRole(mockEvent, wRole) - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.ROLE.MESSAGE_OK' }) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.ROLE_OK' }) }) it('should display error when creating a role onAddRole', () => { @@ -293,7 +300,7 @@ describe('WorkspaceRolesComponent', () => { component.onAddRole(mockEvent, wRole) - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.ROLE.MESSAGE_NOK' }) + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.ROLE_NOK' }) expect(console.error).toHaveBeenCalledWith('createWorkspaceRole', errorResponse) }) diff --git a/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.ts b/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.ts index c4bd5fdc..9cdf8e58 100644 --- a/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.ts +++ b/src/app/workspace/workspace-detail/workspace-roles/workspace-roles.component.ts @@ -208,14 +208,14 @@ export class WorkspaceRolesComponent implements OnInit, OnChanges { }) .subscribe({ next: (data) => { - this.msgService.success({ summaryKey: 'ACTIONS.CREATE.ROLE.MESSAGE_OK' }) + this.msgService.success({ summaryKey: 'ACTIONS.CREATE.ROLE_OK' }) role.id = data.id role.modificationCount = data.modificationCount role.modificationDate = data.modificationDate role.isWorkspaceRole = true }, error: (err) => { - this.msgService.error({ summaryKey: 'ACTIONS.CREATE.ROLE.MESSAGE_NOK' }) + this.msgService.error({ summaryKey: 'ACTIONS.CREATE.ROLE_NOK' }) console.error('createWorkspaceRole', err) } }) diff --git a/src/app/workspace/workspace-detail/workspace-slots/workspace-slots.component.spec.ts b/src/app/workspace/workspace-detail/workspace-slots/workspace-slots.component.spec.ts index 574c14d2..35d474fb 100644 --- a/src/app/workspace/workspace-detail/workspace-slots/workspace-slots.component.spec.ts +++ b/src/app/workspace/workspace-detail/workspace-slots/workspace-slots.component.spec.ts @@ -76,12 +76,18 @@ describe('WorkspaceSlotsComponent', () => { { provide: UserService, useValue: mockUserService } ] }).compileComponents() - msgServiceSpy.success.calls.reset() - msgServiceSpy.error.calls.reset() + // to spy data: reset wProductServiceSpy.getProductsByWorkspaceId.calls.reset() slotServiceSpy.getSlotsForWorkspace.calls.reset() slotServiceSpy.createSlot.calls.reset() productServiceSpy.searchAvailableProducts.calls.reset() + msgServiceSpy.success.calls.reset() + msgServiceSpy.error.calls.reset() + // to spy data: refill with neutral data + wProductServiceSpy.getProductsByWorkspaceId.and.returnValue(of({})) + slotServiceSpy.getSlotsForWorkspace.and.returnValue(of({})) + slotServiceSpy.createSlot.and.returnValue(of({})) + productServiceSpy.searchAvailableProducts.and.returnValue(of({})) })) function initializeComponent(): void { @@ -156,15 +162,17 @@ describe('WorkspaceSlotsComponent', () => { }) it('should display error when product names cannot be loaded', () => { - const err = { status: '404' } - wProductServiceSpy.getProductsByWorkspaceId.and.returnValue(throwError(() => err)) + const errorResponse = { status: '404', statusText: 'Not found' } + wProductServiceSpy.getProductsByWorkspaceId.and.returnValue(throwError(() => errorResponse)) spyOn(component as any, 'declareWorkspaceSlots').and.callFake(() => {}) spyOn(component as any, 'declarePsSlots').and.callFake(() => {}) + spyOn(console, 'error') component.workspace = workspace component.loadData() - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + err.status + '.PRODUCTS') + expect(console.error).toHaveBeenCalledWith('getProductsByWorkspaceId', errorResponse) + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.PRODUCTS') }) it('should get ws slots', () => { @@ -183,14 +191,16 @@ describe('WorkspaceSlotsComponent', () => { }) it('should display error when ws slots cannot be loaded', () => { - const err = { status: '404' } - slotServiceSpy.getSlotsForWorkspace.and.returnValue(throwError(() => err)) + const errorResponse = { status: '404', statusText: 'Not found' } + slotServiceSpy.getSlotsForWorkspace.and.returnValue(throwError(() => errorResponse)) spyOn(component as any, 'declareWorkspaceProducts').and.callFake(() => {}) spyOn(component as any, 'declarePsSlots').and.callFake(() => {}) + spyOn(console, 'error') component.loadData() - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + err.status + '.SLOTS') + expect(console.error).toHaveBeenCalledWith('getSlotsForWorkspace', errorResponse) + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.SLOTS') }) it('should get ps slots', () => { @@ -246,14 +256,16 @@ describe('WorkspaceSlotsComponent', () => { }) it('should display error when ps slots cannot be loaded', () => { - const err = { status: '404' } - productServiceSpy.searchAvailableProducts.and.returnValue(throwError(() => err)) + const errorResponse = { status: '404', statusText: 'Not found' } + productServiceSpy.searchAvailableProducts.and.returnValue(throwError(() => errorResponse)) spyOn(component as any, 'declareWorkspaceProducts').and.callFake(() => {}) spyOn(component as any, 'declareWorkspaceSlots').and.callFake(() => {}) + spyOn(console, 'error') component.loadData() - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + err.status + '.PRODUCTS') + expect(console.error).toHaveBeenCalledWith('searchAvailableProducts', errorResponse) + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.PRODUCTS') }) }) diff --git a/src/app/workspace/workspace-detail/workspace-slots/workspace-slots.component.ts b/src/app/workspace/workspace-detail/workspace-slots/workspace-slots.component.ts index 29138dc6..60229a02 100644 --- a/src/app/workspace/workspace-detail/workspace-slots/workspace-slots.component.ts +++ b/src/app/workspace/workspace-detail/workspace-slots/workspace-slots.component.ts @@ -133,7 +133,7 @@ export class WorkspaceSlotsComponent implements OnInit, OnChanges, OnDestroy { }), catchError((err) => { this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.PRODUCTS' - console.error('getProductsByWorkspaceId():', err) + console.error('getProductsByWorkspaceId', err) return of([] as string[]) }), finalize(() => (this.wpLoading = false)) @@ -170,7 +170,7 @@ export class WorkspaceSlotsComponent implements OnInit, OnChanges, OnDestroy { }), catchError((err) => { this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.SLOTS' - console.error('searchSlots():', err) + console.error('getSlotsForWorkspace', err) return of([]) }) ) @@ -263,7 +263,7 @@ export class WorkspaceSlotsComponent implements OnInit, OnChanges, OnDestroy { }), catchError((err) => { this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.PRODUCTS' - console.error('searchAvailableProducts():', err) + console.error('searchAvailableProducts', err) return of([]) }), finalize(() => (this.sLoading = false)) diff --git a/src/app/workspace/workspace-import/preview/preview.component.html b/src/app/workspace/workspace-import/preview/preview.component.html index b278b3ff..a4cafd08 100644 --- a/src/app/workspace/workspace-import/preview/preview.component.html +++ b/src/app/workspace/workspace-import/preview/preview.component.html @@ -33,7 +33,7 @@ tooltipEvent="hover" > - {{ 'WORKSPACE.THEME' | translate }} + {{ 'WORKSPACE.THEME' | translate }} diff --git a/src/app/workspace/workspace-menu/menu-detail/menu-detail.component.html b/src/app/workspace/workspace-menu/menu-detail/menu-detail.component.html index 3ceb9975..d7335d1c 100644 --- a/src/app/workspace/workspace-menu/menu-detail/menu-detail.component.html +++ b/src/app/workspace/workspace-menu/menu-detail/menu-detail.component.html @@ -69,7 +69,7 @@ tooltipEvent="hover" > - {{ 'MENU_ITEM.PARENT_ID' | translate }} + {{ 'MENU_ITEM.PARENT_ID' | translate }} @@ -111,7 +111,7 @@ - {{ 'MENU_ITEM.URL' | translate }} + {{ 'MENU_ITEM.URL' | translate }} @@ -159,7 +159,7 @@ - {{ 'MENU_ITEM.BADGE' | translate }} + {{ 'MENU_ITEM.BADGE' | translate }} @@ -176,7 +176,7 @@ tooltipEvent="hover" > - {{ 'MENU_ITEM.SCOPE' | translate }} + {{ 'MENU_ITEM.SCOPE' | translate }} @@ -208,7 +208,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'MENU_ITEM.DESCRIPTION' | translate }} + {{ 'MENU_ITEM.DESCRIPTION' | translate }} @@ -251,7 +251,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ getLanguageLabel(language.value) }} + {{ getLanguageLabel(language.value) }} 0"> diff --git a/src/app/workspace/workspace-menu/menu-intern/menu-intern.component.html b/src/app/workspace/workspace-menu/menu-intern/menu-intern.component.html index 905df53a..fcd960ea 100644 --- a/src/app/workspace/workspace-menu/menu-intern/menu-intern.component.html +++ b/src/app/workspace/workspace-menu/menu-intern/menu-intern.component.html @@ -48,7 +48,7 @@ tooltipPosition="top" tooltipEvent="focus" /> - {{ 'INTERNAL.MODIFICATION_DATE' | translate }} + {{ 'INTERNAL.MODIFICATION_DATE' | translate }} @@ -65,7 +65,7 @@ tooltipPosition="top" tooltipEvent="focus" /> - {{ 'INTERNAL.MODIFICATION_USER' | translate }} + {{ 'INTERNAL.MODIFICATION_USER' | translate }} diff --git a/src/app/workspace/workspace-menu/menu-preview/menu-preview.component.html b/src/app/workspace/workspace-menu/menu-preview/menu-preview.component.html index ba8a558c..9a57de7b 100644 --- a/src/app/workspace/workspace-menu/menu-preview/menu-preview.component.html +++ b/src/app/workspace/workspace-menu/menu-preview/menu-preview.component.html @@ -17,7 +17,7 @@ - {{ node.label }} + {{ node.label }} diff --git a/src/app/workspace/workspace-menu/menu-preview/menu-preview.component.ts b/src/app/workspace/workspace-menu/menu-preview/menu-preview.component.ts index 2ea7c5fd..fb4d9714 100644 --- a/src/app/workspace/workspace-menu/menu-preview/menu-preview.component.ts +++ b/src/app/workspace/workspace-menu/menu-preview/menu-preview.component.ts @@ -169,7 +169,8 @@ export class MenuPreviewComponent implements OnChanges { } public onHierarchyViewChange(event: TreeTableNodeExpandEvent): void { - this.stateService.getState().treeExpansionState.set(event.node.key ?? '', event.node.expanded ?? false) + if (event.node.key) + this.stateService.getState().treeExpansionState.set(event.node.key, event.node.expanded === true) } public onLanguagesPreviewChange(lang: string) { diff --git a/src/app/workspace/workspace-menu/menu.component.html b/src/app/workspace/workspace-menu/menu.component.html index 1b27f674..4ca49aa2 100644 --- a/src/app/workspace/workspace-menu/menu.component.html +++ b/src/app/workspace/workspace-menu/menu.component.html @@ -14,28 +14,18 @@ - - - - - - + + 0" #menuTree + *ngIf="!exceptionKey" id="ws_menu_table" styleClass="px-2 sm:px-3 mb-3" - [columns]="wRolesFiltered" [value]="menuNodes" + [columns]="wRolesFiltered" [globalFilterFields]="['name', 'url']" (onNodeCollapse)="onHierarchyViewChange($event)" (onNodeExpand)="onHierarchyViewChange($event)" @@ -49,6 +39,7 @@ icon="pi pi-sort" id="ws_menu_table_header_reorder" (onClick)="onDisplayMenuPreview()" + [disabled]="menuNodes.length === 0" [label]="'DIALOG.MENU.HEADER.TREE_MODAL' | translate" [ariaLabel]="'DIALOG.MENU.HEADER.TREE_MODAL' | translate" [pTooltip]="'DIALOG.MENU.HEADER.TREE_MODAL.TOOLTIP' | translate" @@ -75,7 +66,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'DIALOG.DATAVIEW.FILTER_PLACEHOLDER' | translate }} + {{ 'DIALOG.DATAVIEW.FILTER_PLACEHOLDER' | translate }} - - - - {{ item.label }} - - + + - + + + - - + + + + {{ item.label }} + + + - + - + + + + + 0" + pbutton + type="button" + [id]="'ws_menu_table_header_roles_action_filter_clear'" + class="p-button-rounded p-button-text p-button p-component p-button-icon-only" + (click)="onResetRoleFilter()" + [attr.aria-label]="'DIALOG.MENU.ROLE.FILTER.CLEAR' | translate" + [pTooltip]="'DIALOG.MENU.ROLE.FILTER.CLEAR' | translate" + tooltipPosition="top" + tooltipEvent="hover" + > + + + 5" - [pTooltip]="role.description ? role.description : ''" - tooltipPosition="top" - tooltipEvent="hover" + class="p-1 hidden-xs text-center vertical-align-bottom border-bottom-2 border-right-1 role-name" > - {{ role.name }} + + {{ role.name }} + - - - - - - - - - {{ rowNode.node.label }} - - - - - + + @@ -343,7 +357,7 @@ type="button" [id]="'ws_menu_table_row_' + rowData.key + '_goto_new'" class="p-button-rounded p-button-text p-button p-component p-button-icon-only" - (click)="onCreateMenu($event, rowData)" + (click)="onCreateMenu(rowData)" [attr.aria-label]="'ACTIONS.CREATE.MENU' | translate" [pTooltip]="'ACTIONS.CREATE.MENU.TOOLTIP' | translate" tooltipPosition="top" @@ -410,6 +424,7 @@ + + + + 0" + severity="info" + styleClass="m-3 p-2" + [text]="'ACTIONS.SEARCH.NO_DATA' | translate" + > diff --git a/src/app/workspace/workspace-menu/menu.component.scss b/src/app/workspace/workspace-menu/menu.component.scss index 959cb525..60392fa7 100644 --- a/src/app/workspace/workspace-menu/menu.component.scss +++ b/src/app/workspace/workspace-menu/menu.component.scss @@ -3,6 +3,7 @@ @include invisible; @include danger-action; @include displaying-text-responsive; +@include readable-disabled-components; @include correct-select-button; @include search-criteria-select-button-slim; @include correct-data-view-control; @@ -23,35 +24,49 @@ padding-bottom: 0; overflow-y: hidden; } - - .p-togglebutton { - text-align: center; - display: inline-block; - color: var(--emphasis-medium); - font-weight: var(--font-weight); - padding: 0.3rem 0.8rem; - } - .small-toggle-button.p-togglebutton.p-button:not(.p-disabled) { - background: var(--primary-color); - &:hover { - background: var(--button-hover-bg); - } - .p-button-label, - .p-button-icon-left, - .p-button-icon-right { + /* togglebuttons should look like buttons */ + .p-togglebutton.p-button:not(.p-highlight), + .p-togglebutton.p-button.p-highlight { + &:not(.p-disabled) { + background: var(--primary-color); color: var(--primary-text-color); + &:hover { + background: var(--button-hover-bg); + } + .p-button-label, + .p-button-icon-left, + .p-button-icon-right { + color: inherit; + } + } + &.p-disabled { + background: var(--emphasis-lower); + color: var(--emphasis-low); + .p-button-label, + .p-button-icon-left, + .p-button-icon-right { + color: inherit; + } } + text-align: center; + padding: 0.5rem 0.8rem; + font-weight: var(--font-weight); } .p-treetable { .p-treetable-header { padding: 1rem 0.5rem; // var(--table-header-padding); } + .p-treetable-thead > tr > th { + padding: 0 0.3rem 0.3rem 0.3rem; + } table { width: unset; + margin-top: 0.75rem; .p-treetable-thead { tr th.role-name { word-break: break-all; + min-width: 60px; } tr th:last-of-type { border-right: none !important; diff --git a/src/app/workspace/workspace-menu/menu.component.spec.ts b/src/app/workspace/workspace-menu/menu.component.spec.ts index c89a872a..cbf08f9d 100644 --- a/src/app/workspace/workspace-menu/menu.component.spec.ts +++ b/src/app/workspace/workspace-menu/menu.component.spec.ts @@ -1,7 +1,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' import { Location } from '@angular/common' -import { HttpErrorResponse, provideHttpClient } from '@angular/common/http' +import { provideHttpClient } from '@angular/common/http' import { provideHttpClientTesting } from '@angular/common/http/testing' import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router' import { of, throwError } from 'rxjs' @@ -95,7 +95,7 @@ const state: MenuState = { workspaceMenuItems: [] } -describe('MenuComponent', () => { +fdescribe('MenuComponent', () => { let component: MenuComponent let fixture: ComponentFixture @@ -125,9 +125,8 @@ describe('MenuComponent', () => { const mockUserService = jasmine.createSpyObj('UserService', ['hasPermission']) mockUserService.hasPermission.and.callFake((permission: string) => { - return ['MENU#VIEW', 'MENU#EDIT', 'MENU#GRANT', 'WORKSPACE_ROLE#EDIT'].includes(permission) + return ['MENU#VIEW', 'MENU#CREATE', 'MENU#EDIT', 'MENU#GRANT', 'WORKSPACE_ROLE#EDIT'].includes(permission) }) - const mockActivatedRouteSnapshot: Partial = { params: { id: 'mockId' } } @@ -159,6 +158,15 @@ describe('MenuComponent', () => { { provide: UserService, useValue: mockUserService } ] }).compileComponents() + })) + + beforeEach(() => { + stateServiceSpy.getState.and.returnValue(state) + fixture = TestBed.createComponent(MenuComponent) + component = fixture.componentInstance + component.workspace = workspace + fixture.detectChanges() + // to spy data: reset msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() apiServiceSpy.getWorkspaceByName.calls.reset() @@ -172,66 +180,82 @@ describe('MenuComponent', () => { assgmtApiServiceSpy.deleteAssignment.calls.reset() translateServiceSpy.get.calls.reset() stateServiceSpy.getState.calls.reset() - })) - - beforeEach(() => { - stateServiceSpy.getState.and.returnValue(state) - fixture = TestBed.createComponent(MenuComponent) - component = fixture.componentInstance - component.workspace = workspace - fixture.detectChanges() + // to spy data: refill with neutral data + apiServiceSpy.getWorkspaceByName.and.returnValue(of({})) + menuApiServiceSpy.getMenuStructure.and.returnValue(of({})) + wRoleServiceSpy.searchWorkspaceRoles.and.returnValue(of({})) + assgmtApiServiceSpy.searchAssignments.and.returnValue(of({})) }) - it('should create', () => { - expect(component).toBeTruthy() - }) + describe('Initialize:', () => { + it('should create', () => { + expect(component).toBeTruthy() + }) - it('it should push permissions to array if userService has them', () => { - expect(component.myPermissions).toContain('MENU#VIEW') - expect(component.myPermissions).toContain('MENU#EDIT') - expect(component.myPermissions).toContain('MENU#GRANT') - expect(component.myPermissions).toContain('WORKSPACE_ROLE#EDIT') + it('it should push permissions to array if userService has them', () => { + expect(component.myPermissions).toContain('MENU#VIEW') + expect(component.myPermissions).toContain('MENU#CREATE') + expect(component.myPermissions).toContain('MENU#EDIT') + expect(component.myPermissions).toContain('MENU#GRANT') + expect(component.myPermissions).toContain('WORKSPACE_ROLE#EDIT') + }) }) - describe('prepare page actions', () => { - it('should have prepared action buttons onInit: onClose, and called it', () => { + fdescribe('Page actions:', () => { + beforeEach(() => { component.ngOnInit() + }) + it('should have BACK navigation', () => { if (component.actions$) { component.actions$.subscribe((actions) => { const action = actions[0] action.actionCallback() + expect(locationSpy.back).toHaveBeenCalled() }) } }) - it('should have prepared action buttons onInit: hide Export button due to no menu items', () => { - spyOn(component, 'onExportMenu') - - component.ngOnInit() + it('should call CREATE', () => { + spyOn(component, 'onCreateMenu') if (component.actions$) { component.actions$.subscribe((actions) => { const action = actions[1] action.actionCallback() - expect(component.onExportMenu).toHaveBeenCalled() + + expect(action.permission).toEqual('MENU#CREATE') + //expect(component.changeMode).toEqual('CREATE') + expect(component.onCreateMenu).toHaveBeenCalled() + //expect(component.displayMenuDetail).toBeTrue() + }) + } + }) + + it('should call EXPORT: hide button if there are no menu items', () => { + spyOn(component, 'onExportMenu') + + if (component.actions$) { + component.actions$.subscribe((actions) => { + const action = actions[2] + action.actionCallback() + expect(action.permission).toEqual('MENU#EXPORT') expect(action.showCondition).toBeFalse() + expect(component.onExportMenu).toHaveBeenCalled() expect(component.menuItems).toEqual([]) expect(component.menuItems?.length).toBe(0) }) } }) - it('should have prepared action buttons onInit: hide Export button due to no menu items', () => { + it('should call EXPORT', () => { spyOn(component, 'onExportMenu') - - component.ngOnInit() component.menuItems = mockMenuItems if (component.actions$) { component.actions$.subscribe((actions) => { - const action = actions[1] + const action = actions[2] action.actionCallback() expect(component.onExportMenu).toHaveBeenCalled() expect(action.permission).toEqual('MENU#EXPORT') @@ -240,25 +264,24 @@ describe('MenuComponent', () => { }) } }) - it('should have exclude some actions', () => { + + it('should call EXPORT: hide on conditions', () => { component.menuItems = undefined component.prepareActionButtons() if (component.actions$) { component.actions$.subscribe((actions) => { - expect(actions[1].showCondition).toBeFalse() + expect(actions[2].showCondition).toBeFalse() }) } }) - it('should have prepared action buttons onInit: onImportMenu', () => { + it('should call IMPORT', () => { spyOn(component, 'onImportMenu') - component.ngOnInit() - if (component.actions$) { component.actions$.subscribe((actions) => { - const action = actions[2] + const action = actions[3] action.actionCallback() expect(component.onImportMenu).toHaveBeenCalled() }) @@ -269,8 +292,8 @@ describe('MenuComponent', () => { /** * UI ACTIONS */ - describe('on UI events', () => { - it('should call loadMenu onReload', () => { + describe('UI events', () => { + it('should call loadMenu on reload', () => { spyOn(component, 'loadMenu') component.onReload() @@ -311,14 +334,57 @@ describe('MenuComponent', () => { it('should call updateMenuItem and handle error', () => { const event = { stopPropagation: jasmine.createSpy('stopPropagation') } as any const errorResponse = { status: 400, statusText: 'error on updating a menu item' } - menuApiServiceSpy.updateMenuItem.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') component.onToggleDisable(event, mockMenuItems[0]) expect(event.stopPropagation).toHaveBeenCalled() + expect(console.error).toHaveBeenCalledWith('updateMenuItem', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.MENU_CHANGE_NOK' }) }) + + describe('Role Filter', () => { + it('should add role name to filter array and reload', () => { + menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: mockMenuItems })) + component.workspace = workspace + component.wRoles = [wRole] + component.wAssignments = [assgmt] + component.displayRoles = true + + spyOn(component, 'loadMenu') + //spyOn(component, 'assignNode2Role') + component.roleFilterValue = ['role1'] + + component.onChangeRoleFilter('role2') + + expect(component.roleFilterValue).toEqual(['role1', 'role2']) + expect(component.loadMenu).toHaveBeenCalledWith(false) + expect(component.wRoles).toEqual([wRole]) + //expect(component.menuItems).toEqual(mockMenuItems) + //expect(component['assignNode2Role']).toHaveBeenCalled() + }) + + it('should remove role name to filter array and reload', () => { + spyOn(component, 'loadMenu') + component.roleFilterValue = ['role1', 'role2'] + + component.onChangeRoleFilter('role2') + + expect(component.roleFilterValue).toEqual(['role1']) + expect(component.loadMenu).toHaveBeenCalledWith(false) + }) + + it('should reset filter array and reload', () => { + spyOn(component, 'loadMenu') + component.roleFilterValue = ['role1', 'role2'] + + component.onResetRoleFilter() + + expect(component.roleFilterValue).toEqual([]) + expect(component.loadMenu).toHaveBeenCalledWith(false) + }) + }) }) describe('TreeNodeLabelSwitch', () => { @@ -461,34 +527,30 @@ describe('MenuComponent', () => { expect(component.displayMenuDetail).toBeFalse() }) - it('should handle onCreateMenu correctly', () => { - const mockEvent = jasmine.createSpyObj('MouseEvent', ['stopPropagation']) - const mockParent = { - key: '1-1', - id: 'id1' - } - component.onCreateMenu(mockEvent, mockParent) + describe('create menu item', () => { + it('should handle onCreateMenu correctly: with parent', () => { + const mockParent = { key: '1-1', id: 'id1' } + component.onCreateMenu(mockParent) - expect(mockEvent.stopPropagation).toHaveBeenCalled() - expect(component.changeMode).toEqual('CREATE') - expect(component.menuItem).toEqual(mockParent) - expect(component.displayMenuDetail).toBeTrue() + expect(component.changeMode).toEqual('CREATE') + expect(component.menuItem).toEqual(mockParent) + expect(component.displayMenuDetail).toBeTrue() + }) + + it('should handle onCreateMenu correctly: without parent', () => { + component.onCreateMenu() + + expect(component.changeMode).toEqual('CREATE') + expect(component.menuItem).toEqual(undefined) + expect(component.displayMenuDetail).toBeTrue() + }) }) it('should removeNodeFromTree if key is present and refresh menuNodes if delete displayed onMenuItemChanged', () => { component.displayMenuDelete = true - const item = { - key: 'key' - } + const item = { key: 'key' } component.menuItem = item - const nodes = [ - { - key: 'key' - }, - { - key: 'key2' - } - ] + const nodes = [{ key: 'key' }, { key: 'key2' }] component.menuNodes = nodes component.onMenuItemChanged(true) @@ -498,16 +560,9 @@ describe('MenuComponent', () => { it('should removeNodeFromTree if key is present in node children', () => { component.displayMenuDelete = true - const item = { - key: 'child key' - } + const item = { key: 'child key' } component.menuItem = item - const nodes = [ - { - key: 'key2', - children: [{ key: 'child key' }] - } - ] + const nodes = [{ key: 'key2', children: [{ key: 'child key' }] }] component.menuNodes = nodes component.onMenuItemChanged(true) @@ -518,16 +573,9 @@ describe('MenuComponent', () => { it('should not removeNodeFromTree if no key present', () => { component.displayMenuDelete = true const key = undefined - const item = { - key: key - } + const item = { key: key } component.menuItem = item - const nodes = [ - item, - { - key: 'key2' - } - ] + const nodes = [item, { key: 'key2' }] component.menuNodes = nodes component.onMenuItemChanged(true) @@ -643,93 +691,86 @@ describe('MenuComponent', () => { /**************************************************************************** * DATA */ - it('should loadData', () => { - apiServiceSpy.getWorkspaceByName.and.returnValue(of({ resource: workspace })) - component.workspaceName = 'workspace-name' - - component.loadData() + describe('load data', () => { + it('should load all', () => { + apiServiceSpy.getWorkspaceByName.and.returnValue(of({ resource: workspace })) + menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: mockMenuItems })) + component.workspaceName = 'workspace-name' - expect(component.workspace).toEqual(workspace) - }) + component.loadData() - it('it should handle error response on loadData', () => { - const errorResponse = new HttpErrorResponse({ - error: 'test error', - status: 404, - statusText: 'Not Found' + expect(component.workspace).toEqual(workspace) + expect(component.menuNodes.length).toBe(2) + expect(component.menuNodes[0].expanded).toBeTrue() }) - apiServiceSpy.getWorkspaceByName.and.returnValue(throwError(() => errorResponse)) - component.loadData() + it('should display error message if loading workspace failed', () => { + const errorResponse = { status: 404, statusText: 'Workspace not found' } + apiServiceSpy.getWorkspaceByName.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_404.WORKSPACES') - }) + component.loadData() - it('it should handle exception on loadData', () => { - apiServiceSpy.getWorkspaceByName.and.returnValue(of(null)) + expect(console.error).toHaveBeenCalledWith('getWorkspaceByName', errorResponse) + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.WORKSPACE') + }) - component.loadData() + it('should reject menu loading if workspace is not available', () => { + component.workspace = undefined - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_0.WORKSPACES') - }) + component.loadMenu(false) - it('should loadMenu - restore', () => { - menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: mockMenuItems })) + expect(menuApiServiceSpy.getMenuStructure).not.toHaveBeenCalled() + }) - component.loadMenu(true) + it('should loadMenu - restore', () => { + menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: mockMenuItems })) + component.workspace = workspace - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.RELOAD.OK' }) - }) + component.loadMenu(true) - it('should loadMenu - not restore', () => { - menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: mockMenuItems })) - apiServiceSpy.getWorkspaceByName.and.returnValue(of({ resource: workspace })) - component.workspaceName = 'workspace-name' + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.RELOAD.OK' }) + }) - component.loadData() + it('should loadMenu: no node key in restoreRecursive', () => { + const mockMenuItems: WorkspaceMenuItem[] = [ + { + id: 'id', + key: undefined, + name: 'menu name', + i18n: { ['en']: 'en' }, + children: [{ name: 'child name', key: 'key', id: 'id' }], + url: '/workspace' + } + ] + menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: mockMenuItems })) + component.workspace = workspace - expect(component.workspace).toEqual(workspace) - expect(component.menuNodes.length).toBe(2) - expect(component.menuNodes[0].expanded).toBeTrue() - }) + component.loadMenu(true) - it('should loadMenu: no node key in restoreRecursive', () => { - const mockMenuItems: WorkspaceMenuItem[] = [ - { - id: 'id', - key: undefined, - name: 'menu name', - i18n: { ['en']: 'en' }, - children: [{ name: 'child name', key: 'key', id: 'id' }], - url: '/workspace' - } - ] - menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: mockMenuItems })) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.RELOAD.OK' }) + }) - component.loadMenu(true) + it('should display error message if loading menu failed', () => { + const errorResponse = { status: 404, statusText: 'Menu not found' } + menuApiServiceSpy.getMenuStructure.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') + component.workspace = workspace - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.RELOAD.OK' }) - }) + component.loadMenu(true) - it('should handle error response on loadMenu', () => { - const errorResponse = new HttpErrorResponse({ - error: 'test error', - status: 404, - statusText: 'Not Found' + expect(console.error).toHaveBeenCalledWith('getMenuStructure', errorResponse) + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.MENUS') }) - menuApiServiceSpy.getMenuStructure.and.returnValue(throwError(() => errorResponse)) - - component.loadMenu(true) - - expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_404.MENUS') - }) - it('should return an empty array from mapToTreeNodes if no menuItems onLoadMenu', () => { - menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: [] })) + it('should return an empty array from mapToTreeNodes if no menuItems onLoadMenu', () => { + menuApiServiceSpy.getMenuStructure.and.returnValue(of({ id: workspace.id, menuItems: [] })) + component.workspace = workspace - component.loadMenu(true) + component.loadMenu(true) - expect(component.menuNodes).toEqual([]) + expect(component.menuNodes).toEqual([]) + }) }) /**************************************************************************** @@ -737,6 +778,11 @@ describe('MenuComponent', () => { */ describe('load roles and assignments', () => { + beforeEach(() => { + component.workspace = workspace + component.displayRoles = true + }) + it('should loadRolesAndAssignments -> searchRoles and searchAssignments on loadMenu', () => { const wRole2: WorkspaceRole = { name: 'role name2', @@ -747,7 +793,6 @@ describe('MenuComponent', () => { wRoleServiceSpy.searchWorkspaceRoles.and.returnValue(of({ stream: [wRole, wRole2] })) assgmtApiServiceSpy.searchAssignments.and.returnValue(of({ stream: [assgmt] })) - component.displayRoles = true component.loadMenu(true) expect(component.wRoles).toEqual([wRole, wRole2]) @@ -796,6 +841,7 @@ describe('MenuComponent', () => { component.displayRoles = true component.loadMenu(true) + expect(component.wRoles).toEqual([wRole]) expect(component.wAssignments).toEqual([assgmt2]) }) @@ -817,16 +863,18 @@ describe('MenuComponent', () => { }) describe('onGrantPermission', () => { + beforeEach(() => { + component.workspace = workspace + }) + it('should create assignment when role is not assigned', () => { const roleId = 'role1' const menuItemId = 'menu1' const assignmentId = 'assignment1' - const rowData: MenuItemNodeData = { id: menuItemId, roles: {} } as MenuItemNodeData - const rowNode: TreeNode = { data: rowData, parent: undefined @@ -851,12 +899,10 @@ describe('MenuComponent', () => { id: parentMenuItemId, roles: {} } as MenuItemNodeData - const parentRowNode: TreeNode = { data: parentRowData, parent: undefined } - const rowData: MenuItemNodeData = { id: menuItemId, roles: {}, @@ -868,7 +914,6 @@ describe('MenuComponent', () => { positionPath: '', node: {} as TreeNode } - const rowNode: TreeNode = { data: rowData, parent: parentRowNode @@ -980,9 +1025,11 @@ describe('MenuComponent', () => { it('should display error onRevokePermission', () => { const errorResponse = { status: 400, statusText: 'Error on delete assignment' } assgmtApiServiceSpy.deleteAssignment.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') component.onRevokePermission(nodeData, 'role', assgmt.id!) + expect(console.error).toHaveBeenCalledWith('deleteAssignment', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'DIALOG.MENU.ASSIGNMENT.REVOKE_NOK' }) }) @@ -1048,7 +1095,7 @@ describe('MenuComponent', () => { }) it('should return the logoURL on getLogoUrl', () => { - const result = component.getLogoUrl({ name: 'name', displayName: 'name', logoUrl: 'url' }) + const result = component['getLogoUrl']({ name: 'name', displayName: 'name', logoUrl: 'url' }) expect(result).toBe('url') }) diff --git a/src/app/workspace/workspace-menu/menu.component.ts b/src/app/workspace/workspace-menu/menu.component.ts index 2aa8a040..0627902f 100644 --- a/src/app/workspace/workspace-menu/menu.component.ts +++ b/src/app/workspace/workspace-menu/menu.component.ts @@ -1,9 +1,8 @@ import { Component, ElementRef, OnInit, ViewChild, OnDestroy } from '@angular/core' -import { HttpErrorResponse } from '@angular/common/http' import { Location } from '@angular/common' import { ActivatedRoute, Router } from '@angular/router' import { TranslateService } from '@ngx-translate/core' -import { catchError, combineLatest, map, Observable, Subject, of } from 'rxjs' +import { catchError, combineLatest, finalize, map, Observable, Subject, of } from 'rxjs' import { saveAs } from 'file-saver' import { TreeTable, TreeTableNodeExpandEvent } from 'primeng/treetable' @@ -19,15 +18,13 @@ import { CreateAssignmentRequest, ImagesInternalAPIService, MenuItemAPIService, - MenuItemStructure, RefType, Workspace, WorkspaceMenuItem, WorkspaceRole, WorkspaceAPIService, WorkspaceRolesAPIService, - WorkspaceRolePageResult, - GetWorkspaceResponse + WorkspaceRolePageResult } from 'src/app/shared/generated' import { bffImageUrl, @@ -84,11 +81,10 @@ export class MenuComponent implements OnInit, OnDestroy { public treeNodeLabelSwitchValue = 'NAME' public treeNodeLabelSwitchValueOrg = '' // prevent bug in PrimeNG SelectButton public currentLogoUrl: string | undefined = undefined - public roleFilterValue = '' + public roleFilterValue: string[] = [] // workspace public workspace?: Workspace - private workspace$!: Observable public workspaceName: string = this.route.snapshot.params['name'] public wRoles$!: Observable public wRoles: WorkspaceRole[] = [] @@ -96,14 +92,13 @@ export class MenuComponent implements OnInit, OnDestroy { public wAssignments$!: Observable public wAssignments: Assignment[] = [] // menu - private menu$!: Observable public menuNodes: TreeNode[] = [] public menuItems: WorkspaceMenuItem[] | undefined public menuItem: WorkspaceMenuItem | undefined public parentItems!: SelectItem[] public usedLanguages: Map = new Map() // detail - public changeMode: ChangeMode = 'EDIT' + public changeMode: ChangeMode = 'VIEW' public displayMenuDetail = false public displayMenuImport = false public displayMenuDelete = false @@ -129,6 +124,7 @@ export class MenuComponent implements OnInit, OnDestroy { this.menuItems = state.workspaceMenuItems // simplify permission checks if (this.userService.hasPermission('MENU#VIEW')) this.myPermissions.push('MENU#VIEW') + if (this.userService.hasPermission('MENU#VIEW')) this.myPermissions.push('MENU#CREATE') if (this.userService.hasPermission('MENU#EDIT')) this.myPermissions.push('MENU#EDIT') if (this.userService.hasPermission('MENU#GRANT')) this.myPermissions.push('MENU#GRANT') if (this.userService.hasPermission('WORKSPACE_ROLE#EDIT')) this.myPermissions.push('WORKSPACE_ROLE#EDIT') @@ -172,6 +168,8 @@ export class MenuComponent implements OnInit, OnDestroy { .get([ 'ACTIONS.NAVIGATION.BACK', 'ACTIONS.NAVIGATION.BACK.TOOLTIP', + 'ACTIONS.CREATE.LABEL', + 'ACTIONS.CREATE.MENU', 'ACTIONS.EXPORT.LABEL', 'ACTIONS.EXPORT.MENU', 'ACTIONS.IMPORT.LABEL', @@ -187,6 +185,14 @@ export class MenuComponent implements OnInit, OnDestroy { icon: 'pi pi-arrow-left', show: 'always' }, + { + label: data['ACTIONS.CREATE.LABEL'], + title: data['ACTIONS.CREATE.MENU'], + actionCallback: () => this.onCreateMenu(), + icon: 'pi pi-plus', + show: 'always', + permission: 'MENU#CREATE' + }, { label: data['ACTIONS.EXPORT.LABEL'], title: data['ACTIONS.EXPORT.MENU'], @@ -217,7 +223,9 @@ export class MenuComponent implements OnInit, OnDestroy { this.location.back() } public onReload(): void { + if (this.loading) return this.wRoles = [] + this.wAssignments = [] this.loadMenu(true) } public onGoToWorkspacePermission(): void { @@ -269,6 +277,7 @@ export class MenuComponent implements OnInit, OnDestroy { public onToggleTreeTableContent(ev: any): void { this.displayRoles = ev.checked + if (!this.displayRoles) this.onResetRoleFilter() this.loadRolesAndAssignments() } public isObjectEmpty(obj: object) { @@ -324,12 +333,12 @@ export class MenuComponent implements OnInit, OnDestroy { this.menuItem = item this.displayMenuDetail = true } - public onCreateMenu($event: MouseEvent, parent?: WorkspaceMenuItem): void { - $event.stopPropagation() + public onCreateMenu(parent?: WorkspaceMenuItem): void { this.changeMode = 'CREATE' this.menuItem = parent this.displayMenuDetail = true } + // triggered by change event in menu detail dialog public onMenuItemChanged(changed: boolean): void { if (changed) { @@ -404,53 +413,64 @@ export class MenuComponent implements OnInit, OnDestroy { public loadData(): void { this.loading = true this.exceptionKey = undefined + this.workspace = undefined - this.workspace$ = this.workspaceApi + this.workspaceApi .getWorkspaceByName({ workspaceName: this.workspaceName }) - .pipe(catchError((error) => of(error))) - this.workspace$.subscribe((result) => { - if (result instanceof HttpErrorResponse) { - this.loading = false - this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + result.status + '.WORKSPACES' - console.error('getWorkspaceByName', result) - } else if (result instanceof Object) { - this.workspace = result.resource - this.currentLogoUrl = this.getLogoUrl(this.workspace) - this.loadMenu(false) - } else { - this.loading = false - this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_0.WORKSPACES' - } - }) + .pipe( + map((result) => result.resource), + catchError((err) => { + this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.WORKSPACE' + console.error('getWorkspaceByName', err) + return of(null) + }), + finalize(() => (this.loading = false)) + ) + .subscribe({ + next: (data) => { + if (data) { + this.workspace = data + this.currentLogoUrl = this.getLogoUrl(data) + this.loadMenu(false) + } + } + }) } public loadMenu(restore: boolean): void { if (!this.workspace) return this.menuItem = undefined - this.menu$ = this.menuApi - .getMenuStructure({ menuStructureSearchCriteria: { workspaceId: this.workspace.id!, roles: [] } }) - .pipe(catchError((error) => of(error))) - this.menu$.subscribe((result) => { - this.loading = true - if (result instanceof HttpErrorResponse) { - this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + result.status + '.MENUS' - console.error('getMenuStructure', result) - } else if (result.menuItems instanceof Array) { - this.menuItems = result.menuItems - this.menuNodes = this.mapToTreeNodes(this.menuItems) - this.prepareTreeNodeHelper(restore) - this.loadRolesAndAssignments() - this.prepareActionButtons() - if (restore) { - this.restoreTree() - this.msgService.success({ summaryKey: 'ACTIONS.SEARCH.RELOAD.OK' }) + this.loading = true + + this.menuApi + .getMenuStructure({ + menuStructureSearchCriteria: { workspaceId: this.workspace.id!, roles: this.roleFilterValue } + }) + .pipe( + map((result) => result.menuItems), + catchError((err) => { + this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.MENUS' + console.error('getMenuStructure', err) + return of(null) + }), + finalize(() => (this.loading = false)) + ) + .subscribe({ + next: (data) => { + if (data) { + this.menuItems = data + this.menuNodes = this.mapToTreeNodes(this.menuItems) + this.prepareTreeNodeHelper(restore) + if (this.wRoles.length > 0) this.assignNode2Role(this.wAssignments) + else this.loadRolesAndAssignments() + this.prepareActionButtons() + if (restore) { + this.restoreTree() + this.msgService.success({ summaryKey: 'ACTIONS.SEARCH.RELOAD.OK' }) + } + } } - } else { - this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_0.MENUS' - console.error('getMenuStructure', result) - } - this.loading = false - }) + }) } /**************************************************************************** @@ -502,18 +522,22 @@ export class MenuComponent implements OnInit, OnDestroy { roles.sort(this.sortRoleByName) this.wRoles = roles this.wRolesFiltered = roles - // principle: assignments(role.id, menu.id) => node.roles[role.id] = ass.id - ass.forEach((ass: Assignment) => { - // find affected node and assign role - const assignedNode = this.findTreeNodeById(this.menuNodes, ass.menuItemId) - if (assignedNode) { - assignedNode.data.roles[ass.roleId!] = ass.id - } - }) + this.assignNode2Role(ass) } this.loadingRoles = false }) } + private assignNode2Role(ass: Assignment[]) { + // principle: assignments(role.id, menu.id) => node.roles[role.id] = ass.id + ass.forEach((ass: Assignment) => { + // find affected node and assign role + const assignedNode = this.findTreeNodeById(this.menuNodes, ass.menuItemId) + if (assignedNode) { + assignedNode.data.roles[ass.roleId!] = ass.id + } + }) + } + private findTreeNodeById(source: TreeNode[], id?: string): TreeNode | undefined { let treeNode: TreeNode | undefined = undefined for (const node of source) { @@ -687,29 +711,41 @@ export class MenuComponent implements OnInit, OnDestroy { public onImportMenu(): void { this.displayMenuImport = true } - public onHideMenuImport() { + public onHideMenuImport(): void { this.displayMenuImport = false } - public onDisplayRoles() { + + public onResetRoleFilter(): void { + if (this.roleFilterValue.length > 0) { + this.roleFilterValue = [] + this.loadMenu(false) + } + } + public onChangeRoleFilter(role: string): void { + if (this.roleFilterValue.includes(role)) this.roleFilterValue = this.roleFilterValue.filter((r) => r !== role) + else this.roleFilterValue.push(role) + this.loadMenu(false) + } + public onDisplayRoles(): void { if (!this.displayRoles && this.wRoles.length === 0) { this.loadRolesAndAssignments() } this.displayRoles = !this.displayRoles } - public onDisplayMenuPreview() { + public onDisplayMenuPreview(): void { this.displayMenuPreview = true } - public onHideMenuPreview() { + public onHideMenuPreview(): void { this.displayMenuPreview = false } - // triggered by changes of tree structure in preview + // triggered by changes of tree structure in preview dialog public onUpdateMenuStructure(changed: boolean): void { this.loadMenu(true) } - public getLogoUrl(workspace: Workspace | undefined): string | undefined { - if (!workspace) return undefined - if (workspace.logoUrl) return workspace?.logoUrl - else return bffImageUrl(this.imageApi.configuration.basePath, workspace?.name, RefType.Logo) + + private getLogoUrl(workspace: Workspace): string | undefined { + if (workspace.logoUrl) return workspace.logoUrl + else return bffImageUrl(this.imageApi.configuration.basePath, workspace.name, RefType.Logo) } } diff --git a/src/app/workspace/workspace-menu/services/menu-tree.service.ts b/src/app/workspace/workspace-menu/services/menu-tree.service.ts index 2fc8cf12..fbfb9f03 100644 --- a/src/app/workspace/workspace-menu/services/menu-tree.service.ts +++ b/src/app/workspace/workspace-menu/services/menu-tree.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core' import { TreeNode } from 'primeng/api' + import { WorkspaceMenuItem } from 'src/app/shared/generated' export interface NewPosition { diff --git a/src/app/workspace/workspace-product/products.component.spec.ts b/src/app/workspace/workspace-product/products.component.spec.ts index 9b183dd9..98717829 100644 --- a/src/app/workspace/workspace-product/products.component.spec.ts +++ b/src/app/workspace/workspace-product/products.component.spec.ts @@ -155,15 +155,6 @@ describe('ProductComponent', () => { { provide: UserService, useValue: mockUserService } ] }).compileComponents() - msgServiceSpy.success.calls.reset() - msgServiceSpy.error.calls.reset() - wProductServiceSpy.getProductsByWorkspaceId.calls.reset() - wProductServiceSpy.getProductById.calls.reset() - wProductServiceSpy.updateProductById.calls.reset() - wProductServiceSpy.createProductInWorkspace.calls.reset() - wProductServiceSpy.deleteProductById.calls.reset() - productServiceSpy.searchAvailableProducts.calls.reset() - slotApiServiceSpy.createSlot.calls.reset() })) beforeEach(() => { @@ -174,6 +165,18 @@ describe('ProductComponent', () => { fb = TestBed.inject(FormBuilder) component.renderer = mockRenderer fixture.detectChanges() + // to spy data: reset + msgServiceSpy.success.calls.reset() + msgServiceSpy.error.calls.reset() + wProductServiceSpy.getProductsByWorkspaceId.calls.reset() + wProductServiceSpy.getProductById.calls.reset() + wProductServiceSpy.updateProductById.calls.reset() + wProductServiceSpy.createProductInWorkspace.calls.reset() + wProductServiceSpy.deleteProductById.calls.reset() + productServiceSpy.searchAvailableProducts.calls.reset() + slotApiServiceSpy.createSlot.calls.reset() + // to spy data: refill with neutral data + wProductServiceSpy.createProductInWorkspace.and.returnValue(of({})) }) it('should create', () => { @@ -215,6 +218,7 @@ describe('ProductComponent', () => { it('should log error if getProductsByWorkspaceId call fails', () => { const errorResponse = { status: 404, statusText: 'products not found for workspace' } wProductServiceSpy.getProductsByWorkspaceId.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') const changes = { ['workspace']: { previousValue: 'ws0', @@ -222,11 +226,10 @@ describe('ProductComponent', () => { firstChange: true } } - spyOn(console, 'error') component.ngOnChanges(changes as unknown as SimpleChanges) - expect(console.error).toHaveBeenCalledWith('getProductsByWorkspaceId():', errorResponse) + expect(console.error).toHaveBeenCalledWith('getProductsByWorkspaceId', errorResponse) }) describe('searchPsProducts', () => { @@ -287,14 +290,14 @@ describe('ProductComponent', () => { it('should loadData onChanges: searchPsProducts call error', () => { const errorResponse = { status: 404, statusText: 'product store products not found' } productServiceSpy.searchAvailableProducts.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') const changes = { ['workspace']: { previousValue: 'ws0', currentValue: 'ws1', firstChange: true } } - spyOn(console, 'error') component.ngOnChanges(changes as unknown as SimpleChanges) - expect(console.error).toHaveBeenCalledWith('searchAvailableProducts():', errorResponse) + expect(console.error).toHaveBeenCalledWith('searchAvailableProducts', errorResponse) }) it('prepare product app parts: mfe type is component', () => { @@ -729,12 +732,14 @@ describe('ProductComponent', () => { it('should call getWProduct when an item is selected: display error and hide detail panel', () => { const errorResponse = { status: 404, statusText: 'workspace product not found' } wProductServiceSpy.getProductById.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') const event = { items: [{ id: 1 }] } component.displayDetails = true component.onTargetSelect(event) expect(component.displayDetails).toBeFalse() + expect(console.error).toHaveBeenCalledWith('getProductById', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'DIALOG.PRODUCTS.MESSAGES.LOAD_ERROR' }) }) @@ -861,6 +866,7 @@ describe('ProductComponent', () => { it('should display error when trying to update a product by id', () => { const errorResponse = { status: 400, statusText: 'workspace product not updated' } wProductServiceSpy.updateProductById.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') const event: any = { items: [product] } component.formGroup = fb.group({ displayName: new FormControl(''), @@ -871,6 +877,7 @@ describe('ProductComponent', () => { component.onProductSave(event) + expect(console.error).toHaveBeenCalledWith('updateProductById', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'DIALOG.PRODUCTS.MESSAGES.UPDATE_NOK' }) }) @@ -999,18 +1006,21 @@ describe('ProductComponent', () => { it('should display error when trying to createProductInWorkspace onMoveToTarget', () => { const errorResponse = { status: 400, statusText: 'workspace product not created' } wProductServiceSpy.createProductInWorkspace.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') const event: any = { items: [product] } component.wProducts = [{ ...product, bucket: 'SOURCE', undeployed: false, changedComponents: false }] component.psProducts = [{ ...product, bucket: 'SOURCE', undeployed: false, changedComponents: false }] component.onMoveToTarget(event) + expect(console.error).toHaveBeenCalledWith('createProductInWorkspace', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'DIALOG.PRODUCTS.MESSAGES.REGISTRATION_NOK' }) }) it('should display error when trying to createProductInWorkspace onMoveToTarget', () => { const errorResponse = { status: 400, statusText: 'workspace product not created' } wProductServiceSpy.createProductInWorkspace.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') component.wProducts = [{ ...product, bucket: 'SOURCE', undeployed: false, changedComponents: false }] component.psProducts = [{ ...product, bucket: 'SOURCE', undeployed: false, changedComponents: false }] const product2: Product = { @@ -1024,6 +1034,7 @@ describe('ProductComponent', () => { component.onMoveToTarget(event) + expect(console.error).toHaveBeenCalledWith('createProductInWorkspace', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'DIALOG.PRODUCTS.MESSAGES.REGISTRATIONS_NOK' }) }) }) @@ -1069,6 +1080,7 @@ describe('ProductComponent', () => { it('should handle failed deregistration', () => { const errorResponse = { status: 400, statusText: 'workspace product could not be deregistered' } wProductServiceSpy.deleteProductById.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') component['deregisterItems'] = [product] component.psProducts = [prodStoreItem] component.wProducts = [] @@ -1076,11 +1088,9 @@ describe('ProductComponent', () => { component.onDeregisterConfirmation() + expect(console.error).toHaveBeenCalledWith('deleteProductById', errorResponse) expect(component.displayDeregisterConfirmation).toBeFalse() - expect(wProductServiceSpy.deleteProductById).toHaveBeenCalledWith({ - id: 'id', - productId: 'prod id' - }) + expect(wProductServiceSpy.deleteProductById).toHaveBeenCalledWith({ id: 'id', productId: 'prod id' }) expect(component.psProducts.length).toBe(0) expect(component.wProducts.length).toBe(1) expect(component.wProducts[0].productName).toBe('prod name') diff --git a/src/app/workspace/workspace-product/products.component.ts b/src/app/workspace/workspace-product/products.component.ts index de3441ef..559e843b 100644 --- a/src/app/workspace/workspace-product/products.component.ts +++ b/src/app/workspace/workspace-product/products.component.ts @@ -184,7 +184,7 @@ export class ProductComponent implements OnChanges, OnDestroy, AfterViewInit { }), catchError((err) => { this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.PRODUCTS' - console.error('getProductsByWorkspaceId():', err) + console.error('getProductsByWorkspaceId', err) return of([] as ExtendedProduct[]) }), finalize(() => (this.wpLoading = false)) @@ -221,7 +221,7 @@ export class ProductComponent implements OnChanges, OnDestroy, AfterViewInit { }), catchError((err) => { this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.PRODUCTS' - console.error('searchAvailableProducts():', err) + console.error('searchAvailableProducts', err) return of([] as ExtendedProduct[]) }), finalize(() => (this.psLoading = false)) @@ -342,7 +342,7 @@ export class ProductComponent implements OnChanges, OnDestroy, AfterViewInit { error: (err) => { this.displayedDetailItem = undefined this.displayDetails = false - console.error(err) + console.error('getProductById', err) this.msgService.error({ summaryKey: 'DIALOG.PRODUCTS.MESSAGES.LOAD_ERROR' }) }, complete() {} @@ -472,7 +472,7 @@ export class ProductComponent implements OnChanges, OnDestroy, AfterViewInit { this.msgService.success({ summaryKey: 'DIALOG.PRODUCTS.MESSAGES.UPDATE_OK' }) }, error: (err) => { - console.error(err) + console.error('updateProductById', err) this.msgService.error({ summaryKey: 'DIALOG.PRODUCTS.MESSAGES.UPDATE_NOK' }) }, complete() {} @@ -528,7 +528,7 @@ export class ProductComponent implements OnChanges, OnDestroy, AfterViewInit { this.displayRegisterMessages('REGISTRATION', successCounter, errorCounter) }, error: (err) => { - console.error(err) + console.error('createProductInWorkspace', err) errorCounter++ // Revert change: remove item in target + add item in source list this.wProducts = this.wProducts.filter((wp) => wp.productName !== p.productName) @@ -591,7 +591,7 @@ export class ProductComponent implements OnChanges, OnDestroy, AfterViewInit { // Revert change: remove item in source + add item in target list this.psProducts = this.psProducts.filter((psp) => psp.productName !== p.productName) this.wProducts.push(p) - console.error(err) + console.error('deleteProductById', err) if (itemCount === successCounter + errorCounter) this.displayRegisterMessages('DEREGISTRATION', successCounter, errorCounter) } diff --git a/src/app/workspace/workspace-role-detail/workspace-role-detail.component.html b/src/app/workspace/workspace-role-detail/workspace-role-detail.component.html index 2390dab2..8ff4d173 100644 --- a/src/app/workspace/workspace-role-detail/workspace-role-detail.component.html +++ b/src/app/workspace/workspace-role-detail/workspace-role-detail.component.html @@ -42,7 +42,7 @@ tooltipPosition="top" tooltipEvent="hover" /> - {{ 'ROLE.NAME' | translate }} + {{ 'ROLE.NAME' | translate }} diff --git a/src/app/workspace/workspace-role-detail/workspace-role-detail.component.spec.ts b/src/app/workspace/workspace-role-detail/workspace-role-detail.component.spec.ts index 9be81a3b..547a6d81 100644 --- a/src/app/workspace/workspace-role-detail/workspace-role-detail.component.spec.ts +++ b/src/app/workspace/workspace-role-detail/workspace-role-detail.component.spec.ts @@ -221,6 +221,7 @@ describe('WorkspaceRoleDetailComponent', () => { it('should display error if create role fails', () => { const errorResponse = { status: 400, statusText: 'error on creating a role' } wRoleServiceSpy.createWorkspaceRole.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') component.formGroupRole = { valid: true, controls: { @@ -234,6 +235,7 @@ describe('WorkspaceRoleDetailComponent', () => { component.onSaveRole() expect(wRoleServiceSpy.createWorkspaceRole).toHaveBeenCalled() + expect(console.error).toHaveBeenCalledWith('createWorkspaceRole', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.ROLE_NOK' }) }) @@ -283,6 +285,7 @@ describe('WorkspaceRoleDetailComponent', () => { it('should update the role successfully', () => { const errorResponse = { status: 400, statusText: 'error on updating a role' } wRoleServiceSpy.updateWorkspaceRole.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') component.formGroupRole = { valid: true, controls: { @@ -297,15 +300,18 @@ describe('WorkspaceRoleDetailComponent', () => { component.onSaveRole() expect(wRoleServiceSpy.updateWorkspaceRole).toHaveBeenCalled() + expect(console.error).toHaveBeenCalledWith('updateWorkspaceRole', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.ROLE_NOK' }) }) it('should display error message if delete api call fails', () => { const errorResponse = { status: 400, statusText: 'error on deleting a role' } wRoleServiceSpy.deleteWorkspaceRole.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') component.onDeleteRoleConfirmation() + expect(console.error).toHaveBeenCalledWith('deleteWorkspaceRole', errorResponse) expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.ROLE.MESSAGE_NOK' }) }) }) diff --git a/src/app/workspace/workspace-role-detail/workspace-role-detail.component.ts b/src/app/workspace/workspace-role-detail/workspace-role-detail.component.ts index d50cfc73..1411b327 100644 --- a/src/app/workspace/workspace-role-detail/workspace-role-detail.component.ts +++ b/src/app/workspace/workspace-role-detail/workspace-role-detail.component.ts @@ -97,7 +97,7 @@ export class WorkspaceRoleDetailComponent implements OnChanges { }, error: (err) => { this.msgService.error({ summaryKey: 'ACTIONS.CREATE.ROLE_NOK' }) - console.error(err) + console.error('createWorkspaceRole', err) } }) } else { @@ -114,7 +114,7 @@ export class WorkspaceRoleDetailComponent implements OnChanges { }, error: (err) => { this.msgService.error({ summaryKey: 'ACTIONS.EDIT.ROLE_NOK' }) - console.error(err) + console.error('updateWorkspaceRole', err) } }) } @@ -128,7 +128,7 @@ export class WorkspaceRoleDetailComponent implements OnChanges { }, error: (err) => { this.msgService.error({ summaryKey: 'ACTIONS.DELETE.ROLE.MESSAGE_NOK' }) - console.error(err) + console.error('deleteWorkspaceRole', err) } }) } diff --git a/src/app/workspace/workspace-search/workspace-search.component.spec.ts b/src/app/workspace/workspace-search/workspace-search.component.spec.ts index da633c7c..f22a9351 100644 --- a/src/app/workspace/workspace-search/workspace-search.component.spec.ts +++ b/src/app/workspace/workspace-search/workspace-search.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, provideRouter, Router } from '@angular/router' import { of, throwError } from 'rxjs' import { TranslateTestingModule } from 'ngx-translate-testing' -import { getLocation } from '@onecx/accelerator' +import * as Accelerator from '@onecx/accelerator' import { PortalMessageService } from '@onecx/portal-integration-angular' import { Workspace, WorkspaceAbstract, WorkspaceAPIService, SearchWorkspacesResponse } from 'src/app/shared/generated' @@ -16,9 +16,12 @@ import { WorkspaceSearchComponent } from './workspace-search.component' describe('WorkspaceSearchComponent', () => { let component: WorkspaceSearchComponent let fixture: ComponentFixture + let mockActivatedRoute: ActivatedRoute const mockRouter = { navigate: jasmine.createSpy('navigate') } - + const accSpy = { + getLocation: jasmine.createSpy('getLocation').and.returnValue({ deploymentPath: '/path' }) + } const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['info', 'error']) const wApiServiceSpy = { searchWorkspaces: jasmine.createSpy('searchWorkspaces').and.returnValue(of({})), @@ -42,22 +45,30 @@ describe('WorkspaceSearchComponent', () => { { provide: Router, useValue: mockRouter }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: PortalMessageService, useValue: msgServiceSpy }, - { provide: WorkspaceAPIService, useValue: wApiServiceSpy } + { provide: WorkspaceAPIService, useValue: wApiServiceSpy }, + { provide: Accelerator.getLocation, useValue: accSpy.getLocation } ] }).compileComponents() - msgServiceSpy.info.calls.reset() - msgServiceSpy.error.calls.reset() - wApiServiceSpy.searchWorkspaces.calls.reset() })) beforeEach(() => { fixture = TestBed.createComponent(WorkspaceSearchComponent) + accSpy.getLocation() component = fixture.componentInstance fixture.detectChanges() + // to spy data: reset + msgServiceSpy.info.calls.reset() + msgServiceSpy.error.calls.reset() + wApiServiceSpy.searchWorkspaces.calls.reset() + // to spy data: refill with neutral data + wApiServiceSpy.searchWorkspaces.and.returnValue(of({})) }) - it('should create', () => { - expect(component).toBeTruthy() + describe('initialize', () => { + it('should create', () => { + expect(component).toBeTruthy() + expect(component.deploymentPath).toEqual('') + }) }) it('should search workspaces with results', (done) => { @@ -136,9 +147,7 @@ describe('WorkspaceSearchComponent', () => { it('should behave correctly onGotoWorkspace', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const mockNewWorkspaceWindow = spyOn(window, 'open') - const mockEvent = { - stopPropagation: jasmine.createSpy() - } + const mockEvent = { stopPropagation: jasmine.createSpy() } const w: WorkspaceAbstract = { name: 'name', theme: 'theme', @@ -146,15 +155,15 @@ describe('WorkspaceSearchComponent', () => { displayName: '' } - const deploymentPath = getLocation().deploymentPath === '/' ? '' : getLocation().deploymentPath - component.onGotoWorkspace(mockEvent, w) expect(mockEvent.stopPropagation).toHaveBeenCalled() expect(window.open).toHaveBeenCalledWith( - Location.joinWithSlash(Location.joinWithSlash(window.document.location.origin, deploymentPath), w.baseUrl || ''), + Location.joinWithSlash(Location.joinWithSlash(window.document.location.origin, ''), w.baseUrl || ''), '_blank' ) + + component.onGotoWorkspace(mockEvent, { ...w, baseUrl: undefined }) }) it('should behave correctly onGotoMenu', () => { @@ -199,8 +208,8 @@ describe('WorkspaceSearchComponent', () => { if (component.actions$) { component.actions$.subscribe((actions) => { - const firstAction = actions[0] - firstAction.actionCallback() + const action = actions[0] + action.actionCallback() expect(component.toggleShowCreateDialog).toHaveBeenCalled() }) } @@ -213,8 +222,8 @@ describe('WorkspaceSearchComponent', () => { if (component.actions$) { component.actions$.subscribe((actions) => { - const firstAction = actions[1] - firstAction.actionCallback() + const action = actions[1] + action.actionCallback() expect(component.toggleShowImportDialog).toHaveBeenCalled() }) } @@ -237,8 +246,9 @@ describe('WorkspaceSearchComponent', () => { }) it('should search workspaces but display error if API call fails', (done) => { - const errorResponse = { status: 403, statusText: 'not authorized' } + const errorResponse = { status: 403, statusText: 'no permissions' } wApiServiceSpy.searchWorkspaces.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') component.search() @@ -247,6 +257,7 @@ describe('WorkspaceSearchComponent', () => { if (result) { expect(result.length).toBe(0) expect(component.exceptionKey).toEqual('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.WORKSPACES') + expect(console.error).toHaveBeenCalledWith('searchWorkspaces', errorResponse) } done() }, diff --git a/src/app/workspace/workspace-search/workspace-search.component.ts b/src/app/workspace/workspace-search/workspace-search.component.ts index 87e05a22..9697ddcf 100644 --- a/src/app/workspace/workspace-search/workspace-search.component.ts +++ b/src/app/workspace/workspace-search/workspace-search.component.ts @@ -7,7 +7,8 @@ import { Action } from '@onecx/angular-accelerator' import { DataViewControlTranslations } from '@onecx/portal-integration-angular' import { Location } from '@angular/common' -import { getLocation } from '@onecx/accelerator' +import * as Accelerator from '@onecx/accelerator' +//import { getLocation } from '@onecx/accelerator' import { ImagesInternalAPIService, RefType, @@ -36,7 +37,7 @@ export class WorkspaceSearchComponent implements OnInit { public sortField = 'displayName' public sortOrder = 1 public dataViewControlsTranslations: DataViewControlTranslations = {} - public deploymentPath = '' + public deploymentPath: string @ViewChild('table', { static: false }) table!: any @@ -46,7 +47,10 @@ export class WorkspaceSearchComponent implements OnInit { private readonly router: Router, private readonly translate: TranslateService, private readonly imageApi: ImagesInternalAPIService - ) {} + ) { + this.deploymentPath = + Accelerator.getLocation().deploymentPath === '/' ? '' : Accelerator.getLocation().deploymentPath + } ngOnInit() { this.prepareDialogTranslations() @@ -60,7 +64,7 @@ export class WorkspaceSearchComponent implements OnInit { map((data) => (data?.stream ? data.stream.sort(this.sortWorkspacesByName) : [])), catchError((err) => { this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.WORKSPACES' - console.error('searchWorkspaces():', err) + console.error('searchWorkspaces', err) return of([] as Workspace[]) }), finalize(() => (this.loading = false)) @@ -147,7 +151,6 @@ export class WorkspaceSearchComponent implements OnInit { } public onGotoWorkspace(ev: any, workspace: Workspace) { ev.stopPropagation() - this.deploymentPath = getLocation().deploymentPath === '/' ? '' : getLocation().deploymentPath window.open( Location.joinWithSlash( Location.joinWithSlash(window.document.location.origin, this.deploymentPath), diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 5738e319..489be69f 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -385,7 +385,7 @@ "DETAILS_N_ROLES.TOOLTIP": "Anzeigewechsel: Details / Rollen und Berechtigungen", "ROLES": "Rollen", "ROLES.LOADING": "...Rollen laden", - "ROLES.FILTER": "Rollenfilter", + "ROLES.FILTER": "Rollennamefilter", "ROLES.FILTER.TOOLTIP": "Filter für Rollenname", "ROLES.MANAGE": "Rollen verwalten", "ROLES.NOT_EXIST": "Keine Rollen für den Workspace definiert", @@ -394,7 +394,7 @@ "PREVIEW2.TOOLTIP": "Rot = keine Übersetzungen", "REORDER.TOOLTIP": "Umordnen mit Drag & Drop" }, - "SUBHEADER": "Menü verwalten", + "SUBHEADER": "Die Details und Rollenberechtigungen des Workspace Menüs verwalten", "ACTION": { "VIEW": "Details anzeigen", "EDIT": "Details bearbeiten" @@ -422,6 +422,12 @@ "EXTERN": "X", "EXTERN.TOOLTIP": "Wenn diese Option aktiviert ist, wird der Workspace verlassen" }, + "ROLE": { + "FILTER": "Rollenfilter: Anzeige der Menüeinträge entsprechend der gewählten Rollen", + "FILTER.LABEL": "Rollenfilter", + "FILTER.CLEAR": "Rollenfilter löschen", + "FILTER.NAME": "Mit Klick Rollenname zum Filter hinzufügen/entfernen" + }, "ASSIGNMENT": { "OK": "Zugeordnet: ", "NOK": "Nicht zugeordnet: ", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index afb02b77..7abb2f12 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -385,7 +385,7 @@ "DETAILS_N_ROLES.TOOLTIP": "Change display: Details / Roles and Permissions", "ROLES": "Roles", "ROLES.LOADING": "...loading Roles", - "ROLES.FILTER": "Role filter", + "ROLES.FILTER": "Role name filter", "ROLES.FILTER.TOOLTIP": "Filter for Role name", "ROLES.MANAGE": "Manage Roles", "ROLES.NOT_EXIST": "No Workspace Roles defined", @@ -394,7 +394,7 @@ "PREVIEW2.TOOLTIP": "Red = no translation", "REORDER.TOOLTIP": "Reorder via Drag & Drop" }, - "SUBHEADER": "Manage the Menu", + "SUBHEADER": "Manage details and Role assignments of the Workspace Menu", "ACTION": { "VIEW": "Display details", "EDIT": "Edit details" @@ -422,6 +422,12 @@ "EXTERN": "X", "EXTERN.TOOLTIP": "If checked then the Workspace is exit" }, + "ROLE": { + "FILTER": "Role filter: Display Menu Items according to the selected Roles", + "FILTER.LABEL": "Role filter", + "FILTER.CLEAR": "Clear Role filter", + "FILTER.NAME": "Click to add/remove role name to filter" + }, "ASSIGNMENT": { "OK": "Assigned: ", "NOK": "Not assigned: ", diff --git a/tsconfig.app.json b/tsconfig.app.json index 285ca208..61b100c4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -19,6 +19,6 @@ "src/app/remotes/list-workspaces-using-theme/list-workspaces-using-theme.component.main.ts", "src/app/remotes/list-workspaces-using-product/list-workspaces-using-product.component.main.ts" ], - "include": ["src/**/*.d.ts"], + "include": ["src/**/*.ts"], "exclude": ["src/test.ts", "src/**/*.spec.ts"] }