Skip to content
Snippets Groups Projects
Commit bd404c82 authored by OZGCloud's avatar OZGCloud
Browse files

Merge pull request 'OZG-6102-admin-app-header' (#745) from OZG-6102-admin-app-header into master

parents 6d990407 d166ec52
Branches
Tags
No related merge requests found
Showing
with 386 additions and 75 deletions
<ng-container *ngIf="(apiRootStateResource$ | async)?.resource as apiRoot"> <ng-container *ngIf="(apiRootStateResource$ | async)?.resource as apiRoot">
<header class="flex items-center justify-between bg-white p-6" data-test-id="admin-header"> <header
<div class="font-extrabold text-ozgblue">OZG-Cloud Administration</div> class="flex h-16 items-center justify-between border-b border-b-ozggray-300 bg-white px-9 py-2"
data-test-id="admin-header"
>
<a
class="rounded border-2 border-transparent p-1 outline-2 outline-offset-2 hover:border-primary focus-visible:border-gray-200 focus-visible:outline-focus"
aria-label="OZG-Cloud Administration"
routerLink="/"
data-test-id="logo-link"
>
<ods-admin-logo-icon />
</a>
<div> <div>
<user-profile-button-container <user-profile-button-container
data-test-id="user-profile-button" data-test-id="user-profile-button"
......
...@@ -6,19 +6,20 @@ import { ...@@ -6,19 +6,20 @@ import {
} from '@alfa-client/tech-shared'; } from '@alfa-client/tech-shared';
import { import {
Mock, Mock,
dispatchEventFromFixture,
existsAsHtmlElement, existsAsHtmlElement,
getElementFromFixture, getElementFromFixture,
mock, mock,
notExistsAsHtmlElement, notExistsAsHtmlElement,
} from '@alfa-client/test-utils'; } from '@alfa-client/test-utils';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router'; import { Router, RouterOutlet } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { AdminLogoIconComponent } from '@ods/system';
import { AuthenticationService } from 'authentication'; import { AuthenticationService } from 'authentication';
import { NavigationComponent } from 'libs/admin-settings/src/lib/navigation/navigation.component'; import { NavigationComponent } from 'libs/admin-settings/src/lib/navigation/navigation.component';
import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root';
import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test';
import { MockComponent } from 'ng-mocks'; import { MockComponent, MockDirective } from 'ng-mocks';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component';
import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component';
...@@ -32,6 +33,7 @@ describe('AppComponent', () => { ...@@ -32,6 +33,7 @@ describe('AppComponent', () => {
const buildVersionSelector: string = getDataTestIdOf('build-version'); const buildVersionSelector: string = getDataTestIdOf('build-version');
const userProfileButtonSelector: string = getDataTestIdOf('user-profile-button'); const userProfileButtonSelector: string = getDataTestIdOf('user-profile-button');
const navigationSelector: string = getDataTestIdOf('navigation'); const navigationSelector: string = getDataTestIdOf('navigation');
const logoLink: string = getDataTestIdOf('logo-link');
const routerOutletSelector: string = getDataTestIdOf('router-outlet'); const routerOutletSelector: string = getDataTestIdOf('router-outlet');
const authenticationService: Mock<AuthenticationService> = { const authenticationService: Mock<AuthenticationService> = {
...@@ -47,11 +49,12 @@ describe('AppComponent', () => { ...@@ -47,11 +49,12 @@ describe('AppComponent', () => {
declarations: [ declarations: [
AppComponent, AppComponent,
MockComponent(NavigationComponent), MockComponent(NavigationComponent),
MockComponent(AdminLogoIconComponent),
MockComponent(UserProfileButtonContainerComponent), MockComponent(UserProfileButtonContainerComponent),
MockComponent(UnavailablePageComponent), MockComponent(UnavailablePageComponent),
HasLinkPipe, HasLinkPipe,
MockDirective(RouterOutlet),
], ],
imports: [RouterTestingModule],
providers: [ providers: [
{ {
provide: AuthenticationService, provide: AuthenticationService,
...@@ -129,6 +132,21 @@ describe('AppComponent', () => { ...@@ -129,6 +132,21 @@ describe('AppComponent', () => {
}); });
}); });
describe('administration logo', () => {
const apiResource: ApiRootResource = createApiRootResource();
beforeEach(() => {
component.apiRootStateResource$ = of(createStateResource(apiResource));
fixture.detectChanges();
});
it('should navigate to start page on click', () => {
dispatchEventFromFixture(fixture, logoLink, 'click');
expect(router.navigate).toHaveBeenCalledWith(['/']);
});
});
describe('navigation', () => { describe('navigation', () => {
beforeEach(() => {}); beforeEach(() => {});
it('should exist if configuration link exists', () => { it('should exist if configuration link exists', () => {
......
...@@ -13,7 +13,12 @@ import { EffectsModule } from '@ngrx/effects'; ...@@ -13,7 +13,12 @@ import { EffectsModule } from '@ngrx/effects';
import { StoreRouterConnectingModule } from '@ngrx/router-store'; import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { StoreModule } from '@ngrx/store'; import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { TestbtnComponent } from '@ods/system'; import {
AdminLogoIconComponent,
LogoutIconComponent,
PopupComponent,
PopupListItemComponent,
} from '@ods/system';
import { OAuthModule } from 'angular-oauth2-oidc'; import { OAuthModule } from 'angular-oauth2-oidc';
import { HttpUnauthorizedInterceptor } from 'libs/authentication/src/lib/http-unauthorized.interceptor'; import { HttpUnauthorizedInterceptor } from 'libs/authentication/src/lib/http-unauthorized.interceptor';
import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component';
...@@ -34,7 +39,10 @@ import { appRoutes } from './app.routes'; ...@@ -34,7 +39,10 @@ import { appRoutes } from './app.routes';
], ],
imports: [ imports: [
CommonModule, CommonModule,
TestbtnComponent, AdminLogoIconComponent,
PopupComponent,
PopupListItemComponent,
LogoutIconComponent,
RouterModule.forRoot(appRoutes), RouterModule.forRoot(appRoutes),
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
......
<div class="dropdown"> <ods-popup buttonClass="rounded-full">
<button (click)="showDropDown()" class="dropbtn" data-test-id="drop-down-button"> <div
{{ currentUserInitials }} button-content
</button> role="img"
<div id="myDropdown" class="dropdown-content"> class="flex size-9 items-center justify-center rounded-full border-2 border-transparent bg-ozggray-900 hover:border-primary"
<span style="cursor: pointer" (click)="authenticationService.logout()" data-test-id="logout"
>Abmelden</span
> >
<p class="font-semibold text-whitetext" data-test-id="popup-button-content">
{{ currentUserInitials }}
</p>
</div> </div>
</div> <ods-popup-list-item
caption="Abmelden"
(itemClicked)="authenticationService.logout()"
data-test-id="popup-logout-button"
>
<ods-logout-icon icon />
</ods-popup-list-item>
</ods-popup>
.dropbtn {
background-color: #666666;
color: white;
padding: 16px;
cursor: pointer;
border-radius: 30px;
}
.dropbtn:hover,
.dropbtn:focus {
background-color: #666666;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
z-index: 1;
}
.show {
display: block;
}
...@@ -6,8 +6,10 @@ import { ...@@ -6,8 +6,10 @@ import {
} from '@alfa-client/test-utils'; } from '@alfa-client/test-utils';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { LogoutIconComponent, PopupComponent, PopupListItemComponent } from '@ods/system';
import { AuthenticationService } from 'authentication'; import { AuthenticationService } from 'authentication';
import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test';
import { MockComponent } from 'ng-mocks';
import { UserProfileButtonContainerComponent } from './user-profile.button-container.component'; import { UserProfileButtonContainerComponent } from './user-profile.button-container.component';
describe('UserProfileButtonContainerComponent', () => { describe('UserProfileButtonContainerComponent', () => {
...@@ -16,13 +18,18 @@ describe('UserProfileButtonContainerComponent', () => { ...@@ -16,13 +18,18 @@ describe('UserProfileButtonContainerComponent', () => {
const authenticationService: Mock<AuthenticationService> = mock(AuthenticationService); const authenticationService: Mock<AuthenticationService> = mock(AuthenticationService);
const dropDownButton: string = getDataTestIdOf('drop-down-button'); const popupButtonContent: string = getDataTestIdOf('popup-button-content');
const logout: string = getDataTestIdOf('logout'); const popupLogoutButton: string = getDataTestIdOf('popup-logout-button');
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [UserProfileButtonContainerComponent], declarations: [UserProfileButtonContainerComponent],
imports: [RouterTestingModule], imports: [
RouterTestingModule,
MockComponent(PopupComponent),
MockComponent(PopupListItemComponent),
MockComponent(LogoutIconComponent),
],
providers: [ providers: [
{ {
provide: AuthenticationService, provide: AuthenticationService,
...@@ -50,29 +57,23 @@ describe('UserProfileButtonContainerComponent', () => { ...@@ -50,29 +57,23 @@ describe('UserProfileButtonContainerComponent', () => {
}); });
}); });
describe('button', () => { describe('popup button', () => {
it('should call showDropDown on click', () => {
component.showDropDown = jest.fn();
dispatchEventFromFixture(fixture, dropDownButton, 'click');
expect(component.showDropDown).toHaveBeenCalled();
});
it('should show initials', () => { it('should show initials', () => {
const userInitials: string = 'AV'; component.currentUserInitials = 'AV';
component.currentUserInitials = userInitials;
fixture.detectChanges(); fixture.detectChanges();
const buttonElement: HTMLElement = getElementFromFixture(fixture, dropDownButton); const popupButtonContentElement: HTMLElement = getElementFromFixture(
expect(buttonElement.textContent.trim()).toEqual('AV'); fixture,
popupButtonContent,
);
expect(popupButtonContentElement.textContent.trim()).toEqual('AV');
}); });
}); });
describe('abmelden', () => { describe('logout', () => {
it('should call authService logout', () => { it('should call authService logout', () => {
dispatchEventFromFixture(fixture, logout, 'click'); dispatchEventFromFixture(fixture, popupLogoutButton, 'itemClicked');
expect(authenticationService.logout).toHaveBeenCalled(); expect(authenticationService.logout).toHaveBeenCalled();
}); });
......
...@@ -4,7 +4,6 @@ import { AuthenticationService } from 'libs/authentication/src/lib/authenticatio ...@@ -4,7 +4,6 @@ import { AuthenticationService } from 'libs/authentication/src/lib/authenticatio
@Component({ @Component({
selector: 'user-profile-button-container', selector: 'user-profile-button-container',
templateUrl: './user-profile-button-container.component.html', templateUrl: './user-profile-button-container.component.html',
styleUrls: ['./user-profile-button-container.component.scss'],
}) })
export class UserProfileButtonContainerComponent implements OnInit { export class UserProfileButtonContainerComponent implements OnInit {
public currentUserInitials: string; public currentUserInitials: string;
...@@ -14,8 +13,4 @@ export class UserProfileButtonContainerComponent implements OnInit { ...@@ -14,8 +13,4 @@ export class UserProfileButtonContainerComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.currentUserInitials = this.authenticationService.getCurrentUserInitials(); this.currentUserInitials = this.authenticationService.getCurrentUserInitials();
} }
public showDropDown(): void {
document.getElementById('myDropdown').classList.toggle('show');
}
} }
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* You can add global styles to this file, and also import other style files */ @import 'libs/design-system/src/lib/tailwind-preset/root.css';
...@@ -10,6 +10,7 @@ export * from './lib/form/file-upload-button/file-upload-button.component'; ...@@ -10,6 +10,7 @@ export * from './lib/form/file-upload-button/file-upload-button.component';
export * from './lib/form/radio-button-card/radio-button-card.component'; export * from './lib/form/radio-button-card/radio-button-card.component';
export * from './lib/form/text-input/text-input.component'; export * from './lib/form/text-input/text-input.component';
export * from './lib/form/textarea/textarea.component'; export * from './lib/form/textarea/textarea.component';
export * from './lib/icons/admin-logo-icon/admin-logo-icon.component';
export * from './lib/icons/attachment-icon/attachment-icon.component'; export * from './lib/icons/attachment-icon/attachment-icon.component';
export * from './lib/icons/bescheid-generate-icon/bescheid-generate-icon.component'; export * from './lib/icons/bescheid-generate-icon/bescheid-generate-icon.component';
export * from './lib/icons/bescheid-upload-icon/bescheid-upload-icon.component'; export * from './lib/icons/bescheid-upload-icon/bescheid-upload-icon.component';
...@@ -18,6 +19,7 @@ export * from './lib/icons/collaboration-icon/collaboration-icon.component'; ...@@ -18,6 +19,7 @@ export * from './lib/icons/collaboration-icon/collaboration-icon.component';
export * from './lib/icons/exclamation-icon/exclamation-icon.component'; export * from './lib/icons/exclamation-icon/exclamation-icon.component';
export * from './lib/icons/file-icon/file-icon.component'; export * from './lib/icons/file-icon/file-icon.component';
export * from './lib/icons/iconVariants'; export * from './lib/icons/iconVariants';
export * from './lib/icons/logout-icon/logout-icon.component';
export * from './lib/icons/office-icon/office-icon.component'; export * from './lib/icons/office-icon/office-icon.component';
export * from './lib/icons/save-icon/save-icon.component'; export * from './lib/icons/save-icon/save-icon.component';
export * from './lib/icons/search-icon/search-icon.component'; export * from './lib/icons/search-icon/search-icon.component';
......
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminLogoIconComponent } from './admin-logo-icon.component';
describe('AdminLogoIconComponent', () => {
let component: AdminLogoIconComponent;
let fixture: ComponentFixture<AdminLogoIconComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AdminLogoIconComponent],
}).compileComponents();
fixture = TestBed.createComponent(AdminLogoIconComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
@Component({
selector: 'ods-admin-logo-icon',
standalone: true,
imports: [CommonModule],
templateUrl: `./admin-logo-icon.component.html`,
})
export class AdminLogoIconComponent {}
import type { Meta, StoryObj } from '@storybook/angular';
import { AdminLogoIconComponent } from './admin-logo-icon.component';
const meta: Meta<AdminLogoIconComponent> = {
title: 'Icons/Admin logo icon',
component: AdminLogoIconComponent,
excludeStories: /.*Data$/,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<AdminLogoIconComponent>;
export const Default: Story = {};
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LogoutIconComponent } from './logout-icon.component';
describe('LogoutIconComponent', () => {
let component: LogoutIconComponent;
let fixture: ComponentFixture<LogoutIconComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LogoutIconComponent],
}).compileComponents();
fixture = TestBed.createComponent(LogoutIconComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { NgClass } from '@angular/common';
import { Component, Input } from '@angular/core';
import { twMerge } from 'tailwind-merge';
import { IconVariants, iconVariants } from '../iconVariants';
@Component({
selector: 'ods-logout-icon',
standalone: true,
imports: [NgClass],
template: `<svg
[ngClass]="twMerge(iconVariants({ size }), 'fill-text', class)"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 21C4.45 21 3.97917 20.8042 3.5875 20.4125C3.19583 20.0208 3 19.55 3 19V5C3 4.45 3.19583 3.97917 3.5875 3.5875C3.97917 3.19583 4.45 3 5 3H12V5H5V19H12V21H5ZM16 17L14.625 15.55L17.175 13H9V11H17.175L14.625 8.45L16 7L21 12L16 17Z"
/>
</svg>`,
})
export class LogoutIconComponent {
@Input() size: IconVariants['size'] = 'medium';
@Input() class: string = '';
readonly iconVariants = iconVariants;
readonly twMerge = twMerge;
}
import type { Meta, StoryObj } from '@storybook/angular';
import { LogoutIconComponent } from './logout-icon.component';
const meta: Meta<LogoutIconComponent> = {
title: 'Icons/Logout icon',
component: LogoutIconComponent,
excludeStories: /.*Data$/,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<LogoutIconComponent>;
export const Default: Story = {
args: { size: 'medium' },
argTypes: {
size: {
control: 'select',
options: ['small', 'medium', 'large', 'extra-large', 'full'],
description: 'Size of icon. Property "full" means 100%',
table: {
defaultValue: { summary: 'medium' },
},
},
},
};
...@@ -13,7 +13,6 @@ type Story = StoryObj<InstantSearchComponent>; ...@@ -13,7 +13,6 @@ type Story = StoryObj<InstantSearchComponent>;
export const Default: Story = { export const Default: Story = {
args: { args: {
label: '',
placeholder: 'zuständige Stelle suchen', placeholder: 'zuständige Stelle suchen',
headerText: 'In der OZG-Cloud', headerText: 'In der OZG-Cloud',
}, },
...@@ -21,7 +20,6 @@ export const Default: Story = { ...@@ -21,7 +20,6 @@ export const Default: Story = {
export const SearchResults: Story = { export const SearchResults: Story = {
args: { args: {
label: '',
placeholder: 'zuständige Stelle suchen', placeholder: 'zuständige Stelle suchen',
headerText: 'In der OZG-Cloud', headerText: 'In der OZG-Cloud',
searchResults: [ searchResults: [
......
...@@ -13,7 +13,6 @@ type Story = StoryObj<SearchFieldComponent>; ...@@ -13,7 +13,6 @@ type Story = StoryObj<SearchFieldComponent>;
export const Default: Story = { export const Default: Story = {
args: { args: {
label: '',
placeholder: 'search something...', placeholder: 'search something...',
}, },
}; };
...@@ -9,7 +9,6 @@ import { twMerge } from 'tailwind-merge'; ...@@ -9,7 +9,6 @@ import { twMerge } from 'tailwind-merge';
imports: [CommonModule, CdkTrapFocus], imports: [CommonModule, CdkTrapFocus],
template: `<div class="relative w-fit"> template: `<div class="relative w-fit">
<button <button
class="w-fit outline-2 outline-offset-2 outline-focus"
[ngClass]="[twMerge('w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]" [ngClass]="[twMerge('w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]"
(click)="togglePopup()" (click)="togglePopup()"
[attr.aria-expanded]="isPopupOpen" [attr.aria-expanded]="isPopupOpen"
...@@ -18,11 +17,11 @@ import { twMerge } from 'tailwind-merge'; ...@@ -18,11 +17,11 @@ import { twMerge } from 'tailwind-merge';
data-test-id="popup-button" data-test-id="popup-button"
#button #button
> >
<ng-content select="[button]" /> <ng-content select="[button-content]" />
</button> </button>
<ul <ul
*ngIf="isPopupOpen" *ngIf="isPopupOpen"
class="max-h-120 animate-fadeIn absolute min-w-44 max-w-80 overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none" class="absolute max-h-120 min-w-44 max-w-80 animate-fadeIn overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none"
[ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'" [ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
......
...@@ -43,7 +43,7 @@ export const Default: Story = { ...@@ -43,7 +43,7 @@ export const Default: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: `<ods-popup ${argsToTemplate(args)}> template: `<ods-popup ${argsToTemplate(args)}>
<ods-user-icon button /> <ods-user-icon button-content />
<ods-popup-list-item caption="Lorem" /> <ods-popup-list-item caption="Lorem" />
<ods-popup-list-item caption="Ipsum" /> <ods-popup-list-item caption="Ipsum" />
<ods-popup-list-item caption="Dolor" /> <ods-popup-list-item caption="Dolor" />
...@@ -55,7 +55,7 @@ export const LongText: Story = { ...@@ -55,7 +55,7 @@ export const LongText: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: `<ods-popup ${argsToTemplate(args)}> template: `<ods-popup ${argsToTemplate(args)}>
<p button>Trigger popup</p> <p button-content>Trigger popup</p>
<ods-popup-list-item caption="Lorem" /> <ods-popup-list-item caption="Lorem" />
<ods-popup-list-item caption="Lorem ipsum dolor sit amet" /> <ods-popup-list-item caption="Lorem ipsum dolor sit amet" />
</ods-popup>`, </ods-popup>`,
...@@ -66,7 +66,7 @@ export const ItemsWithIcons: Story = { ...@@ -66,7 +66,7 @@ export const ItemsWithIcons: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: `<ods-popup ${argsToTemplate(args)}> template: `<ods-popup ${argsToTemplate(args)}>
<p button>Trigger popup</p> <p button-content>Trigger popup</p>
<ods-popup-list-item caption="Lorem"> <ods-popup-list-item caption="Lorem">
<ods-save-icon icon size="small" /> <ods-save-icon icon size="small" />
</ods-popup-list-item> </ods-popup-list-item>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment