feat: As a tester I want to edit my profile
This commit is contained in:
parent
e0e23f7383
commit
b4bf6de8f8
|
@ -28,9 +28,12 @@
|
|||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/assets/@theme/styles/styles.scss"
|
||||
"src/assets/@theme/styles/styles.scss",
|
||||
"node_modules/@fortawesome/fontawesome-free/css/all.css"
|
||||
],
|
||||
"scripts": [
|
||||
"node_modules/@fortawesome/fontawesome-free/js/all.js"
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"buffer",
|
||||
"crypto-js/hmac-sha256",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -42,6 +42,7 @@
|
|||
"keycloak-js": "^13.0.1",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "latest",
|
||||
"font-awesome": "^4.7.0",
|
||||
"ng-mocks": "^13.4.2",
|
||||
"ngx-moment": "^5.0.0",
|
||||
"ngx-take-until-destroy": "^5.4.0",
|
||||
|
@ -58,12 +59,12 @@
|
|||
"@angular/cli": "^12.2.16",
|
||||
"@angular/compiler-cli": "~12.2.16",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@briebug/jest-schematic": "^3.0.0",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@schematics/angular": "^10.2.4",
|
||||
"@types/jest": "28.1.1",
|
||||
"@types/node": "^12.20.47",
|
||||
"@briebug/jest-schematic": "^3.0.0",
|
||||
"codelyzer": "^6.0.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"jest": "28.1.1",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
|
|
|
@ -9,6 +9,8 @@ import {SessionState, SessionStateModel} from '@shared/stores/session-state/sess
|
|||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
|
||||
import {filter} from 'rxjs/operators';
|
||||
import {NbIconLibraries} from '@nebular/theme';
|
||||
import {FaIconLibrary} from '@fortawesome/angular-fontawesome';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
|
@ -24,6 +26,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
|
||||
constructor(private translateService: TranslateService,
|
||||
private store: Store,
|
||||
private iconLibraries: FaIconLibrary,
|
||||
private nebularIconLibraries: NbIconLibraries,
|
||||
@Inject(LOCALE_ID) private localeId: string) {
|
||||
this.initApp();
|
||||
}
|
||||
|
@ -44,10 +48,14 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
initApp(): void {
|
||||
// for global language
|
||||
this.translateService.use(this.localeId);
|
||||
|
||||
// for number, date and time
|
||||
registerLocaleData(localeDe, 'de-DE');
|
||||
|
||||
// for font-awesome icons
|
||||
this.nebularIconLibraries.registerFontPack('fas', { packClass: 'fas', iconClassPrefix: 'fa' });
|
||||
this.nebularIconLibraries.registerFontPack('far', { packClass: 'far', iconClassPrefix: 'fa' });
|
||||
this.nebularIconLibraries.registerFontPack('fab', { packClass: 'fab', iconClassPrefix: 'fa' });
|
||||
this.nebularIconLibraries.setDefaultPack('far');
|
||||
// for country codes
|
||||
this.setupCountryCode();
|
||||
}
|
||||
|
||||
|
|
|
@ -12,15 +12,12 @@ import {
|
|||
NbSelectModule,
|
||||
NbThemeModule,
|
||||
NbOverlayContainerAdapter,
|
||||
NbDialogModule, NbMenuModule,
|
||||
NbDialogModule, NbMenuModule, NbIconLibraries,
|
||||
} from '@nebular/theme';
|
||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||
import {HttpLoaderFactory} from './common-app.module';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
import {fas} from '@fortawesome/free-solid-svg-icons';
|
||||
import {far} from '@fortawesome/free-regular-svg-icons';
|
||||
import {NgxsModule} from '@ngxs/store';
|
||||
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||
import {environment} from '../environments/environment';
|
||||
|
@ -37,6 +34,9 @@ import {CustomOverlayContainer} from '@shared/modules/custom-overlay-container.c
|
|||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {RetryDialogModule} from '@shared/modules/retry-dialog/retry-dialog.module';
|
||||
import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
import {fas} from '@fortawesome/free-solid-svg-icons';
|
||||
import {far} from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -94,9 +94,11 @@ import {RetryDialogModule} from '@shared/modules/retry-dialog/retry-dialog.modul
|
|||
]
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(library: FaIconLibrary, faConfig: FaConfig) {
|
||||
library.addIconPacks(fas, far);
|
||||
constructor(library: FaIconLibrary, faConfig: FaConfig, libraries: NbIconLibraries) {
|
||||
library.addIconPacks(far, fas);
|
||||
libraries.registerFontPack('solid', {packClass: 'fas', iconClassPrefix: 'fa'});
|
||||
faConfig.defaultPrefix = 'fas';
|
||||
libraries.setDefaultPack('solid');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<img *ngIf="currentTheme === 'corporate', else changeImage"
|
||||
src="../../assets/images/favicons/favicon.ico" alt="logo dark" class="header-icon" width="60rem" height="60rem">
|
||||
<ng-template #changeImage>
|
||||
<img src="../../assets/images/favicons/favicon_corporate.ico" alt="logo light" class="header-icon" width="60rem" height="60rem">
|
||||
<img src="../../assets/images/favicons/favicon_corporate.ico" alt="logo light" class="header-icon" width="60rem"
|
||||
height="60rem">
|
||||
</ng-template>
|
||||
|
||||
<div class="logo-container">
|
||||
|
@ -11,41 +12,31 @@
|
|||
<div class="filler"></div>
|
||||
<div fxLayoutGap="4rem">
|
||||
<nb-actions size="medium">
|
||||
<!--Info Action-->
|
||||
<nb-action>
|
||||
<fa-icon title="Info" [icon]="fa.faCircleInfo" (click)="onClickShowInfo()" class="action-element-icon fa-2x">
|
||||
</fa-icon>
|
||||
</nb-action>
|
||||
<!--OWASP Action-->
|
||||
<nb-action>
|
||||
<fa-icon title="OWASP Testing Guide"
|
||||
(click)="onClickGoToLink('https://owasp.org/www-project-web-security-testing-guide/v42/')"
|
||||
[icon]="fa.faFileInvoice" class="action-element-icon fa-2x">
|
||||
</fa-icon>
|
||||
</nb-action>
|
||||
<!--Theme Action-->
|
||||
<nb-action class="toggle-theme">
|
||||
<button nbButton
|
||||
(click)="onClickGoToLink('https://owasp.org/www-project-web-security-testing-guide/v42/')">
|
||||
<fa-icon [icon]="fa.faFileInvoice" class="new-element-icon" href="https://www.google.com">
|
||||
<nb-action>
|
||||
<div (click)="onClickSwitchTheme()" class="action-element-icon">
|
||||
<fa-icon *ngIf="currentTheme === 'corporate', else changeIcon"
|
||||
title="Darktheme" [icon]="fa.faMoon" class="fa-2x">
|
||||
</fa-icon>
|
||||
<span class="owasp-redirect-button">OWASP</span>
|
||||
</button>
|
||||
</nb-action>
|
||||
<nb-action class="toggle-theme">
|
||||
<button nbButton
|
||||
(click)="onClickSwitchTheme()">
|
||||
<fa-icon *ngIf="currentTheme === 'corporate', else changeIcon" [icon]="fa.faMoon"
|
||||
class="new-element-icon"></fa-icon>
|
||||
<ng-template #changeIcon>
|
||||
<fa-icon [icon]="fa.faSun" class="new-element-icon"></fa-icon>
|
||||
<fa-icon title="Lighttheme" [icon]="fa.faSun" class="fa-2x"></fa-icon>
|
||||
</ng-template>
|
||||
</button>
|
||||
</nb-action>
|
||||
<!--Language Action-->
|
||||
<nb-action class="language-menu">
|
||||
<div *ngIf="selectedLanguage && languages" class="languageContainer">
|
||||
<nb-select selected="{{selectedLanguage}}" fullWidth>
|
||||
<nb-option *ngFor="let language of languages"
|
||||
value="{{language}}" (click)="onClickLanguage(language)" fxLayoutAlign="start center">
|
||||
<img src="../../assets/images/flags/{{language}}.svg" class="flag" width="25rem" height="16rem" alt="">
|
||||
<span fxFlexOffset="0.5rem"> {{'languageKeys.' + language | translate}} </span>
|
||||
</nb-option>
|
||||
</nb-select>
|
||||
</div>
|
||||
</nb-action>
|
||||
<!--User Action-->
|
||||
<nb-action class="user">
|
||||
<!--<fa-icon [icon]="fa.faUser" class="user-icon">
|
||||
</fa-icon>-->
|
||||
<nb-action class="user-action">
|
||||
<nb-user [nbContextMenu]="userMenu"
|
||||
[picture]="FALLBACK_IMG"
|
||||
name="{{user?.getValue()?.username}}"
|
||||
|
|
|
@ -8,17 +8,26 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.action-element-icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.owasp-redirect-button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.languageContainer {
|
||||
display: flex;
|
||||
max-width: 8rem;
|
||||
min-width: 8rem;
|
||||
.user-action {
|
||||
// width: 4rem;
|
||||
z-index: 10;
|
||||
// height: 3rem;
|
||||
.user-action-accordion-header {
|
||||
|
||||
.flag {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,14 +42,6 @@
|
|||
align-items: center;
|
||||
width: auto;
|
||||
|
||||
.logo-container {
|
||||
font-style: oblique;
|
||||
color: #e74c3c;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
nb-action {
|
||||
height: auto;
|
||||
display: flex;
|
||||
|
|
|
@ -14,6 +14,8 @@ import {NgxsModule, Store} from '@ngxs/store';
|
|||
import {KeycloakService} from 'keycloak-angular';
|
||||
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state';
|
||||
import {User} from '@shared/models/user.model';
|
||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
|
||||
|
||||
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
|
||||
userAccount: {
|
||||
|
@ -52,6 +54,7 @@ describe('HeaderComponent', () => {
|
|||
NgxsModule.forRoot([SessionState])
|
||||
],
|
||||
providers: [
|
||||
{provide: DialogService, useClass: DialogServiceMock},
|
||||
NbMenuService,
|
||||
KeycloakService
|
||||
]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||
import {NbMenuItem, NbMenuService, NbThemeService} from '@nebular/theme';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {filter, map} from 'rxjs/operators';
|
||||
import {GlobalTitlesVariables} from '@shared/config/global-variables';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||
|
@ -14,6 +14,8 @@ import {BehaviorSubject} from 'rxjs';
|
|||
import {Route} from '@shared/models/route.enum';
|
||||
import {environment} from '../../environments/environment';
|
||||
import {Router} from '@angular/router';
|
||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {ProfileSettingsComponent} from '@shared/modules/profile-settings/profile-settings.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
|
@ -26,18 +28,21 @@ export class HeaderComponent implements OnInit {
|
|||
// HTML only
|
||||
readonly fa = FA;
|
||||
readonly SECURITYC4PO_TITLE: string = GlobalTitlesVariables.SECURITYC4PO_TITLE;
|
||||
// Menu only
|
||||
readonly settingsIcon = 'gear';
|
||||
readonly logoutIcon = 'right-from-bracket';
|
||||
|
||||
currentTheme = '';
|
||||
languages = ['en-US', 'de-DE'];
|
||||
selectedLanguage = '';
|
||||
|
||||
// User Menu Properties
|
||||
userPictureOnly = false;
|
||||
user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
||||
userMenu: NbMenuItem[] = [
|
||||
{
|
||||
title: '',
|
||||
pathMatch: 'prefix'
|
||||
title: 'settings',
|
||||
icon: { icon: this.settingsIcon, pack: 'fas' }
|
||||
},
|
||||
{
|
||||
title: 'logout',
|
||||
icon: { icon: this.logoutIcon, pack: 'fas'}
|
||||
}
|
||||
];
|
||||
readonly FALLBACK_IMG = 'assets/images/demo/anon-user-icon.png';
|
||||
|
@ -47,6 +52,7 @@ export class HeaderComponent implements OnInit {
|
|||
private router: Router,
|
||||
private themeService: NbThemeService,
|
||||
private translateService: TranslateService,
|
||||
private dialogService: DialogService,
|
||||
private menuService: NbMenuService,
|
||||
private userService: UserService,
|
||||
protected keycloakService: KeycloakService) {
|
||||
|
@ -60,7 +66,6 @@ export class HeaderComponent implements OnInit {
|
|||
untilDestroyed(this),
|
||||
).subscribe(themeName => this.currentTheme = themeName);
|
||||
|
||||
this.selectedLanguage = this.translateService.currentLang;
|
||||
// Load user profile
|
||||
this.userService.loadUserProfile().pipe(
|
||||
untilDestroyed(this)
|
||||
|
@ -78,16 +83,44 @@ export class HeaderComponent implements OnInit {
|
|||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((menuBag) => {
|
||||
if (menuBag.item.pathMatch === 'prefix') {
|
||||
this.onClickLogOut();
|
||||
// Makes sure that other menus without icon won't trigger
|
||||
if (menuBag.item.icon) {
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
if (menuBag.item.icon['icon'] === this.settingsIcon) {
|
||||
console.warn('Profile');
|
||||
this.dialogService.openCustomDialog(
|
||||
ProfileSettingsComponent,
|
||||
{
|
||||
user: this.user.getValue(),
|
||||
}
|
||||
).onClose.pipe(
|
||||
filter((confirm) => !!confirm),
|
||||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: () => {
|
||||
console.warn('New Settings confirmed');
|
||||
}
|
||||
});
|
||||
}
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
else if (menuBag.item.icon['icon'] === this.logoutIcon) {
|
||||
this.onClickLogOut();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Setup stream to translate menu item
|
||||
this.translateService.stream('global.action.profile')
|
||||
.pipe(
|
||||
untilDestroyed(this)
|
||||
).subscribe((text: string) => {
|
||||
this.userMenu[0].title = text;
|
||||
});
|
||||
// Setup stream to translate menu item
|
||||
this.translateService.stream('global.action.logout')
|
||||
.pipe(
|
||||
untilDestroyed(this)
|
||||
).subscribe((text: string) => {
|
||||
this.userMenu[0].title = text;
|
||||
this.userMenu[1].title = text;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -96,6 +129,10 @@ export class HeaderComponent implements OnInit {
|
|||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
onClickShowInfo(): void {
|
||||
console.info('To be implemented..');
|
||||
}
|
||||
|
||||
onClickSwitchTheme(): void {
|
||||
if (this.currentTheme === 'corporate') {
|
||||
this.themeService.changeTheme('dark');
|
||||
|
@ -120,8 +157,4 @@ export class HeaderComponent implements OnInit {
|
|||
console.error(err);
|
||||
});*/
|
||||
}
|
||||
|
||||
onClickLanguage(language: string): void {
|
||||
this.translateService.use(language);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,14 @@ import {
|
|||
NbActionsModule,
|
||||
NbButtonModule,
|
||||
NbCardModule,
|
||||
NbContextMenuModule, NbMenuModule,
|
||||
NbContextMenuModule,
|
||||
NbSelectModule,
|
||||
NbUserModule
|
||||
} from '@nebular/theme';
|
||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {ProfileSettingsModule} from '@shared/modules/profile-settings/profile-settings.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -30,7 +31,8 @@ import {TranslateModule} from '@ngx-translate/core';
|
|||
NbSelectModule,
|
||||
TranslateModule,
|
||||
NbUserModule,
|
||||
NbContextMenuModule
|
||||
NbContextMenuModule,
|
||||
ProfileSettingsModule,
|
||||
],
|
||||
providers: [
|
||||
]
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<!--Actions for mobile devices-->
|
||||
<nb-actions size="medium" fxHide fxShow.lt-lg>
|
||||
<nb-action>
|
||||
<nb-user [nbContextMenu]="objectiveActionItems" shape="rectangle" [picture]="BARS_IMG" name="" [onlyPicture]></nb-user> <!--[picture]="fa.faRedoAlt"-->
|
||||
<nb-user [nbContextMenu]="objectiveActionItems" shape="rectangle" [picture]="BARS_IMG" name="" [onlyPicture]></nb-user>
|
||||
</nb-action>
|
||||
</nb-actions>
|
||||
</div>
|
||||
|
|
|
@ -29,19 +29,18 @@ import {TranslateService} from '@ngx-translate/core';
|
|||
export class ObjectiveHeaderComponent implements OnInit {
|
||||
|
||||
selectedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
|
||||
// Menu only
|
||||
readonly editIcon = 'edit';
|
||||
readonly fileExportIcon = 'file-export';
|
||||
// Mobile menu properties
|
||||
objectiveActionItems: NbMenuItem[] = [
|
||||
{
|
||||
title: 'global.action.edit',
|
||||
badge: {
|
||||
status: 'warning'
|
||||
}
|
||||
icon: { icon: this.editIcon, pack: 'fas' }
|
||||
},
|
||||
{
|
||||
title: 'global.action.report',
|
||||
badge: {
|
||||
status: 'info'
|
||||
}
|
||||
icon: { icon: this.fileExportIcon, pack: 'fas' }
|
||||
},
|
||||
];
|
||||
// HTML only
|
||||
|
@ -83,10 +82,16 @@ export class ObjectiveHeaderComponent implements OnInit {
|
|||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((menuBag) => {
|
||||
if (menuBag.item.badge && menuBag.item.badge.status === 'warning') {
|
||||
this.onClickEditPentestProject();
|
||||
} else if (menuBag.item.badge && menuBag.item.badge.status === 'info') {
|
||||
this.onClickGeneratePentestReport();
|
||||
// Makes sure that other menus without icon won't trigger
|
||||
if (menuBag.item.icon) {
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
if (menuBag.item.icon['icon'] === this.editIcon) {
|
||||
this.onClickEditPentestProject();
|
||||
}
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
else if (menuBag.item.icon['icon'] === this.fileExportIcon) {
|
||||
this.onClickGeneratePentestReport();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Setup stream to translate menu action item
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
.content {
|
||||
height: 95%;
|
||||
overflow: auto !important;
|
||||
// overflow: hidden !important;
|
||||
|
||||
// ToDo: Fixes tab header but also disables scrolling for content
|
||||
/*nb-tab {
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
<h4>
|
||||
{{ getPentestHeaderForObjective(pentestInfo$.getValue().refNumber) | translate}}
|
||||
</h4>
|
||||
<p class="description">
|
||||
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
|
||||
</p>
|
||||
<div class="description">
|
||||
<div>
|
||||
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<!--ToDo: Add tooling hints after description (maybe in pentest-header component)-->
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
.pentest-info {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
// overflow: auto !important;
|
||||
overflow: hidden !important;
|
||||
position: relative !important;
|
||||
|
||||
.description {
|
||||
// ToDo: Make only description scrollable
|
||||
// Scrollbar
|
||||
overflow-y: scroll !important;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
width: 60vw;
|
||||
font-size: 1rem;
|
||||
white-space: pre-line;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"global": {
|
||||
"actions": "Aktionen",
|
||||
"action.profile": "Profil",
|
||||
"action.login": "Einloggen",
|
||||
"action.logout": "Ausloggen",
|
||||
"action.retry": "Erneut Versuchen",
|
||||
|
@ -56,6 +57,35 @@
|
|||
"failed": "Benutzername oder Passwort falsch",
|
||||
"unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen"
|
||||
},
|
||||
"profile": {
|
||||
"header": "Nutzerprofil",
|
||||
"username": {
|
||||
"title": "Nutzername",
|
||||
"placeholder": "Nutzername"
|
||||
},
|
||||
"firstName": {
|
||||
"title": "Vorname",
|
||||
"placeholder": "Vorname"
|
||||
},
|
||||
"lastName": {
|
||||
"title": "Nachname",
|
||||
"placeholder": "Nachname"
|
||||
},
|
||||
"validationMessage": {
|
||||
"firstNameRequired": "Vorname ist erforderlich.",
|
||||
"lastNameRequired": "Nachname ist erforderlich."
|
||||
},
|
||||
"languageLabel": "Sprache ändern:",
|
||||
"password": {
|
||||
"title": "Passwort ändern:",
|
||||
"old": "Altes Passwort",
|
||||
"new": "Neues Passwort",
|
||||
"confirmNew": "Neues Passwort bestätigen",
|
||||
"validationMessage": {
|
||||
"passwordRequired": "Passwort ist erforderlich."
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"new": "Neu",
|
||||
"needs_more_info": "Benötigt mehr Informationen",
|
||||
|
@ -64,7 +94,7 @@
|
|||
"triaged": "Ausstehend",
|
||||
"retesting": "Erneutes Testen",
|
||||
"resolved": "Aufgeklärt",
|
||||
"informative": "Informatif",
|
||||
"informative": "Informativ",
|
||||
"duplicate": "Duplikat",
|
||||
"not_applicable": "Unzutreffend",
|
||||
"spam": "Spam",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"global": {
|
||||
"actions": "Actions",
|
||||
"action.profile": "Profile",
|
||||
"action.login": "Login",
|
||||
"action.logout": "Logout",
|
||||
"action.retry": "Try again",
|
||||
|
@ -56,6 +57,35 @@
|
|||
"failed": "Wrong username or password",
|
||||
"unauthorized": "User not found. Please register and try again"
|
||||
},
|
||||
"profile": {
|
||||
"header": "Userprofile",
|
||||
"username": {
|
||||
"title": "Username",
|
||||
"placeholder": "Username"
|
||||
},
|
||||
"firstName": {
|
||||
"title": "Firstname",
|
||||
"placeholder": "Firstname"
|
||||
},
|
||||
"lastName": {
|
||||
"title": "Lastname",
|
||||
"placeholder": "Lastname"
|
||||
},
|
||||
"validationMessage": {
|
||||
"firstNameRequired": "Firstname is required.",
|
||||
"lastNameRequired": "Lastname is required."
|
||||
},
|
||||
"languageLabel": "Change language:",
|
||||
"password": {
|
||||
"title": "Change password:",
|
||||
"old": "Old password",
|
||||
"new": "New password",
|
||||
"confirmNew": "Confirm new password",
|
||||
"validationMessage": {
|
||||
"passwordRequired": "Password is required."
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"new": "New",
|
||||
"needs_more_info": "Needs More Info",
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
float: left;
|
||||
clear: none;
|
||||
margin-left: 1rem;
|
||||
|
||||
}
|
||||
|
||||
nb-form-field {
|
||||
|
|
|
@ -32,15 +32,14 @@ export class ExportReportDialogComponent implements OnInit {
|
|||
private dialogService: DialogService
|
||||
) {
|
||||
}
|
||||
|
||||
// HTML
|
||||
readonly fa = FA;
|
||||
// form control elements
|
||||
exportReportFormatControl = new FormControl(ExportFormatOptions.PDF);
|
||||
exportReportLanguageControl = new FormControl(ExportLanguageOptions.ENGLISH);
|
||||
exportReportLanguageControl = new FormControl(LanguageOptions.ENGLISH);
|
||||
// exports
|
||||
exportFormats = ExportFormatOptions;
|
||||
exportLanguages = ExportLanguageOptions;
|
||||
exportLanguages = LanguageOptions;
|
||||
|
||||
dialogData: GenericDialogData;
|
||||
|
||||
|
@ -159,7 +158,7 @@ export enum ExportFormatOptions {
|
|||
HTML = 'HTML'
|
||||
}
|
||||
|
||||
export enum ExportLanguageOptions {
|
||||
export enum LanguageOptions {
|
||||
ENGLISH = 'en-US',
|
||||
GERMAN = 'de-DE'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<form [formGroup]="passwordFormGroup">
|
||||
<div class="password-form" fxLayout="row" fxLayoutGap="2rem" fxLayoutAlign="start start">
|
||||
<!--Old password-->
|
||||
<nb-form-field class="password-form-field">
|
||||
<label for="oldPassword" class="label">
|
||||
{{'profile.password.old' | translate}}
|
||||
</label>
|
||||
<input formControlName="oldPassword"
|
||||
type="{{getInputType('oldPassword')}}" required
|
||||
id="oldPassword" nbInput
|
||||
class="form-field"
|
||||
[status]="passwordFormGroup.get('oldPassword').dirty ? (passwordFormGroup.get('oldPassword').invalid ? 'danger' : 'basic') : 'basic'"
|
||||
placeholder="{{'******'}}">
|
||||
<button nbSuffix nbButton ghost class="form-field-button" (click)="toggleShowOldPassword()">
|
||||
<fa-icon [icon]="showOldPassword ? fa.faEye : fa.faEyeSlash"></fa-icon>
|
||||
</button>
|
||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||
<ng-template *ngIf="passwordFormGroup.get('oldPassword').dirty">
|
||||
<span class="error-text"
|
||||
*ngIf="passwordFormGroup.get('oldPassword')?.hasError('required')">
|
||||
{{'profile.password.validationMessage.passwordRequired' | translate}}
|
||||
</span>
|
||||
</ng-template>
|
||||
</nb-form-field>
|
||||
<div fxLayout="column" fxLayoutGap="2rem">
|
||||
<!--New password-->
|
||||
<nb-form-field class="password-form-field">
|
||||
<label for="newPassword" class="label">
|
||||
{{'profile.password.new' | translate}}
|
||||
</label>
|
||||
<input formControlName="newPassword"
|
||||
type="{{getInputType('newPassword')}}" required
|
||||
id="newPassword" nbInput
|
||||
class="form-field"
|
||||
[status]="passwordFormGroup.get('newPassword').dirty ? (passwordFormGroup.get('newPassword').invalid ? 'danger' : 'basic') : 'basic'"
|
||||
placeholder="{{'******'}}">
|
||||
<button nbSuffix nbButton ghost class="form-field-button" (click)="toggleShowNewPasswords()">
|
||||
<fa-icon [icon]="showNewPasswords ? fa.faEye : fa.faEyeSlash"></fa-icon>
|
||||
</button>
|
||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||
<ng-template *ngIf="passwordFormGroup.get('newPassword').dirty">
|
||||
<span class="error-text"
|
||||
*ngIf="passwordFormGroup.get('newPassword')?.hasError('required')">
|
||||
{{'profile.password.validationMessage.passwordRequired' | translate}}
|
||||
</span>
|
||||
</ng-template>
|
||||
</nb-form-field>
|
||||
<!--New password-->
|
||||
<nb-form-field class="password-form-field">
|
||||
<label for="confirmNewPassword" class="label">
|
||||
{{'profile.password.confirmNew' | translate}}
|
||||
</label>
|
||||
<input formControlName="confirmNewPassword"
|
||||
type="{{getInputType('confirmNewPassword')}}" required
|
||||
id="confirmNewPassword" nbInput
|
||||
class="form-field"
|
||||
[status]="passwordFormGroup.get('confirmNewPassword').dirty ? (passwordFormGroup.get('confirmNewPassword').invalid ? 'danger' : 'basic') : 'basic'"
|
||||
placeholder="{{'******'}}">
|
||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||
<ng-template *ngIf="passwordFormGroup.get('confirmNewPassword').dirty">
|
||||
<span class="error-text"
|
||||
*ngIf="passwordFormGroup.get('confirmNewPassword')?.hasError('required')">
|
||||
{{'profile.password.validationMessage.passwordRequired' | translate}}
|
||||
</span>
|
||||
</ng-template>
|
||||
</nb-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,29 @@
|
|||
@import '../../../../assets/@theme/styles/themes';
|
||||
|
||||
.password-form {
|
||||
padding-top: 1.5rem;
|
||||
|
||||
.password-form-field {
|
||||
width: 20rem !important;
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field-button {
|
||||
margin-top: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
float: left;
|
||||
color: nb-theme(color-danger-default);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {PasswordInputFromComponent} from './password-input-from.component';
|
||||
import {NbFormFieldModule} from '@nebular/theme';
|
||||
import {ReactiveFormsModule} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ThemeModule} from '@assets/@theme/theme.module';
|
||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {HttpLoaderFactory} from '../../../../app/common-app.module';
|
||||
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||
import {NgxsModule} from '@ngxs/store';
|
||||
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
describe('PasswordInputFromComponent', () => {
|
||||
let component: PasswordInputFromComponent;
|
||||
let fixture: ComponentFixture<PasswordInputFromComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
PasswordInputFromComponent
|
||||
],
|
||||
imports: [
|
||||
NbFormFieldModule,
|
||||
ReactiveFormsModule,
|
||||
BrowserAnimationsModule,
|
||||
ThemeModule.forRoot(),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
NgxsModule.forRoot([SessionState]),
|
||||
HttpClientModule,
|
||||
HttpClientTestingModule
|
||||
],
|
||||
providers: [
|
||||
KeycloakService
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordInputFromComponent);
|
||||
component = fixture.componentInstance;
|
||||
// ToDo: fix detectChanges() when from control accessor is defined
|
||||
// fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
import {Component, EventEmitter, forwardRef, Input, OnInit, Output} from '@angular/core';
|
||||
import {AbstractControl, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors} from '@angular/forms';
|
||||
import {
|
||||
OldPasswordEmptyError,
|
||||
PasswordErrorStateMatcher
|
||||
} from '@shared/modules/profile-settings/password-input-from/util/password-error-state-matcher';
|
||||
import {passwordValidator} from '@shared/modules/profile-settings/password-input-from/util/password.validator';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {ProfilePasswordFormData} from '@shared/modules/profile-settings/password-input-from/util/profile-password-form-data.model';
|
||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-input-from',
|
||||
templateUrl: './password-input-from.component.html',
|
||||
styleUrls: ['./password-input-from.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => PasswordInputFromComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => PasswordInputFromComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class PasswordInputFromComponent implements OnInit {
|
||||
|
||||
constructor(private fb: FormBuilder) { }
|
||||
// Emits true if password is strong
|
||||
@Output() passwordStrong: EventEmitter<boolean> = new EventEmitter();
|
||||
// Handle erros
|
||||
@Input() userServiceError$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
public passwordErrorStateMatcher: PasswordErrorStateMatcher;
|
||||
public oldPasswordEmptyError: OldPasswordEmptyError;
|
||||
// show the criteria of the new password
|
||||
showDetails = false;
|
||||
|
||||
passwordFormGroup: FormGroup;
|
||||
// form control elements
|
||||
oldPasswordCtrl: AbstractControl;
|
||||
newPasswordCtrl: AbstractControl;
|
||||
confirmNewPasswordCtrl: AbstractControl;
|
||||
// HTML only
|
||||
readonly fa = FA;
|
||||
showOldPassword = false;
|
||||
showNewPasswords = false;
|
||||
|
||||
/**
|
||||
* tests value of control for empty-spaces
|
||||
* @param control FormControl
|
||||
*/
|
||||
static containsBlankSpaceValidator(control: FormControl): ValidationErrors | null {
|
||||
const containsEmptySpacesRegex: RegExp = new RegExp(/\s+/);
|
||||
return containsEmptySpacesRegex.test(control.value) ? {passwordContainsBlankSpace: true} : null;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.passwordFormGroup = this.fb.group(
|
||||
{
|
||||
oldPassword: ['', [PasswordInputFromComponent.containsBlankSpaceValidator]],
|
||||
newPassword: ['', [PasswordInputFromComponent.containsBlankSpaceValidator]],
|
||||
confirmNewPassword: ''
|
||||
},
|
||||
{validators: [passwordValidator]}
|
||||
);
|
||||
// get form controls
|
||||
this.oldPasswordCtrl = this.passwordFormGroup.get('oldPassword');
|
||||
this.newPasswordCtrl = this.passwordFormGroup.get('newPassword');
|
||||
this.confirmNewPasswordCtrl = this.passwordFormGroup.get('confirmNewPassword');
|
||||
|
||||
this.passwordErrorStateMatcher = new PasswordErrorStateMatcher(this.userServiceError$);
|
||||
this.oldPasswordEmptyError = new OldPasswordEmptyError();
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.passwordFormGroup.valueChanges.subscribe(() => {
|
||||
fn(this.passwordFormGroup.value);
|
||||
});
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
}
|
||||
|
||||
writeValue(value: ProfilePasswordFormData): void {
|
||||
if (value) {
|
||||
this.oldPasswordCtrl.setValue(value.oldPassword);
|
||||
this.newPasswordCtrl.setValue(value.newPassword);
|
||||
this.confirmNewPasswordCtrl.setValue(value.confirmNewPassword);
|
||||
}
|
||||
}
|
||||
|
||||
validate(control: AbstractControl): ValidationErrors | null {
|
||||
return this.passwordFormGroup.valid ? null : passwordValidator(this.passwordFormGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* This Function is triggered as soon as a change in the strength of the password happens
|
||||
*
|
||||
* @param strength the new strength as a percentage number. Only 100% denotes a strong password
|
||||
*/
|
||||
onStrengthChanged(strength: number): void {
|
||||
this.passwordStrong.emit(strength === 100);
|
||||
}
|
||||
|
||||
getInputType(passwordField: string): string {
|
||||
if (passwordField === 'oldPassword') {
|
||||
if (this.showOldPassword) {
|
||||
return 'text';
|
||||
}
|
||||
return 'password';
|
||||
} else {
|
||||
if (this.showNewPasswords) {
|
||||
return 'text';
|
||||
}
|
||||
return 'password';
|
||||
}
|
||||
}
|
||||
|
||||
toggleShowOldPassword(): void {
|
||||
this.showOldPassword = !this.showOldPassword;
|
||||
}
|
||||
|
||||
toggleShowNewPasswords(): void {
|
||||
this.showNewPasswords = !this.showNewPasswords;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
export class PasswordErrorStateMatcher {
|
||||
public passwordsDoNotMatch = true;
|
||||
public userServiceError;
|
||||
|
||||
constructor(userServiceError: BehaviorSubject<boolean>) {
|
||||
this.userServiceError = userServiceError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if new and confirm password do not match.
|
||||
*/
|
||||
isErrorState(ctrl: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const passwordCtrl = form.control.get('newPassword');
|
||||
this.passwordsDoNotMatch = passwordCtrl.value !== ctrl.value;
|
||||
return this.passwordsDoNotMatch || this.userServiceError.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class OldPasswordEmptyError {
|
||||
public oldPasswordEmpty = true;
|
||||
|
||||
/**
|
||||
* @return true if old password is empty and new password is not.
|
||||
*/
|
||||
isErrorState(ctrl: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const passwordCtrl = form.control.get('newPassword');
|
||||
this.oldPasswordEmpty = passwordCtrl.value.length !== 0 && ctrl.value.length === 0;
|
||||
return this.oldPasswordEmpty;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import {AbstractControl, ValidationErrors} from '@angular/forms';
|
||||
|
||||
/**
|
||||
* passwordValidator validates the password based on 3 conditions:
|
||||
* - newPassword === confirmNewPassword
|
||||
* - MIN_PASSWORD_LENGTH <= newPassword
|
||||
* - oldPassword is NOT empty
|
||||
*
|
||||
* @param formGroup AbstractControl containing [oldPassword, newPassword, confirmPassword]
|
||||
*/
|
||||
export function passwordValidator(formGroup: AbstractControl): ValidationErrors | null {
|
||||
const oldPasswordCtrl = formGroup.get('oldPassword');
|
||||
const newPasswordCtrl = formGroup.get('newPassword');
|
||||
const confirmNewPasswordCtrl = formGroup.get('confirmNewPassword');
|
||||
if (newPasswordCtrl.value !== confirmNewPasswordCtrl.value) {
|
||||
return {passwordsDoNotMatch: true};
|
||||
} else if (oldPasswordCtrl.value === '') {
|
||||
oldPasswordCtrl.setErrors({oldPasswordEmpty: true});
|
||||
return {oldPasswordEmpty: true};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface ProfilePasswordFormData {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export const Patterns = {
|
||||
NOT_BLANK: '^(?!(?:\\s+)$).*$',
|
||||
NO_WHITESPACES: '^(?!(?:\\s+)$).*$'
|
||||
};
|
|
@ -0,0 +1,135 @@
|
|||
<nb-card accent="control" status="info" class="profile-setting-dialog">
|
||||
<nb-card-header fxLayoutAlign="start center" class="dialog-header">
|
||||
<fa-icon [icon]="fa.faUserCog"
|
||||
class="header-icon fa-lg">
|
||||
</fa-icon>
|
||||
<span class="header-text"> {{ 'profile.header' | translate }} </span>
|
||||
</nb-card-header>
|
||||
<nb-card-body class="dialog-body">
|
||||
<div class="user-properties" fxLayout="row" fxLayoutGap="2rem" fxLayoutAlign="center center">
|
||||
<div fxFlex="20">
|
||||
<img src="{{USER_IMG}}" alt="userimage" class="user-img">
|
||||
</div>
|
||||
<div fxFlex class="properties">
|
||||
<form [formGroup]="userFormGroup" fxLayout="column" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||
<!--Username-->
|
||||
<nb-form-field class="user-form-field">
|
||||
<label for="username" class="label">
|
||||
{{'profile.username.title' | translate}}
|
||||
</label>
|
||||
<fa-icon nbPrefix class="prefix-icon" [icon]="fa.faAt"></fa-icon>
|
||||
<input formControlName="username"
|
||||
type="text" required
|
||||
id="username" nbInput
|
||||
fullWidth
|
||||
class="form-field untouchable"
|
||||
status="control"
|
||||
placeholder="{{'profile.username.placeholder' | translate}}">
|
||||
</nb-form-field>
|
||||
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||
<!--Firstname-->
|
||||
<nb-form-field class="user-form-field">
|
||||
<label for="firstName" class="label">
|
||||
{{'profile.firstName.title' | translate}}
|
||||
</label>
|
||||
<input formControlName="firstName"
|
||||
type="text" required
|
||||
id="firstName" nbInput
|
||||
fullWidth
|
||||
class="form-field"
|
||||
[status]="userFormGroup.get('firstName').dirty ? (userFormGroup.get('firstName').invalid ? 'danger' : 'basic') : 'basic'"
|
||||
placeholder="{{'profile.firstName.placeholder' | translate}} *">
|
||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||
<ng-template *ngIf="userFormGroup.get('firstName').dirty">
|
||||
<span class="error-text"
|
||||
*ngIf="userFormGroup.get('firstName')?.hasError('required')">
|
||||
{{'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'}}
|
||||
{{'profile.validationMessage.firstNameRequired' | translate}}
|
||||
</span>
|
||||
<span class="error-text"
|
||||
*ngIf="userFormGroup.get('firstName').invalid">
|
||||
{{'Firstname is invalid' | translate}}
|
||||
</span>
|
||||
</ng-template>
|
||||
</nb-form-field>
|
||||
<!--Lastname-->
|
||||
<nb-form-field class="user-form-field">
|
||||
<label for="lastName" class="label">
|
||||
{{'profile.lastName.title' | translate}}
|
||||
</label>
|
||||
<input formControlName="lastName"
|
||||
type="text" required
|
||||
id="lastName" nbInput
|
||||
fullWidth
|
||||
class="form-field"
|
||||
[status]="userFormGroup.get('lastName').dirty ? (userFormGroup.get('lastName').invalid ? 'danger' : 'basic') : 'basic'"
|
||||
placeholder="{{'profile.lastName.placeholder' | translate}} *">
|
||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||
<ng-template *ngIf="userFormGroup.get('lastName').dirty">
|
||||
<span class="error-text"
|
||||
*ngIf="userFormGroup.get('lastName')?.hasError('required')">
|
||||
{{'profile.validationMessage.lastNameRequired' | translate}}
|
||||
</span>
|
||||
<span class="error-text"
|
||||
*ngIf="userFormGroup.get('lastName').invalid">
|
||||
{{'Lastname is invalid' | translate}}
|
||||
</span>
|
||||
</ng-template>
|
||||
</nb-form-field>
|
||||
</div>
|
||||
<!--ToDo: Email?-->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="language-settings">
|
||||
<!--Language Radio Selection-->
|
||||
<label class="language-selection-label">
|
||||
{{ 'profile.languageLabel' | translate }}
|
||||
</label>
|
||||
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start center">
|
||||
<nb-radio-group name="language" [formControl]="profileLanguageControl"
|
||||
class="language-radio-buttons languageContainer" status="info">
|
||||
<nb-radio value="{{profileLanguages.ENGLISH}}" (click)="onClickLanguage(profileLanguages.ENGLISH)">
|
||||
<img src="../../assets/images/flags/{{profileLanguages.ENGLISH}}.svg" class="flag" width="25rem"
|
||||
height="16rem"
|
||||
alt="">
|
||||
</nb-radio>
|
||||
<nb-radio value="{{profileLanguages.GERMAN}}" (click)="onClickLanguage(profileLanguages.GERMAN)">
|
||||
<img src="../../assets/images/flags/{{profileLanguages.GERMAN}}.svg" class="flag" width="25rem"
|
||||
height="16rem"
|
||||
alt="">
|
||||
</nb-radio>
|
||||
</nb-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<!--ToDo: Add accordion for keycloak values-->
|
||||
<div class="user-password-change">
|
||||
<label class="password-selection-label">
|
||||
{{'profile.password.title' | translate}}
|
||||
</label>
|
||||
<div>
|
||||
<form class="password-form" [formGroup]="passwordFormGroup">
|
||||
<app-password-input-from
|
||||
formControlName="passwordInput"
|
||||
[userServiceError$]="userServiceError"
|
||||
(passwordStrong)="passwordStrong = $event">
|
||||
</app-password-input-from>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nb-card-body>
|
||||
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
|
||||
<button nbButton size="small"
|
||||
class="dialog-button"
|
||||
status="info"
|
||||
[disabled]="!allowConfirm()"
|
||||
(click)="onClickConfirm()">
|
||||
{{ 'global.action.confirm' | translate }}
|
||||
</button>
|
||||
<button nbButton size="small"
|
||||
class="dialog-button"
|
||||
(click)="onClickCancel()">
|
||||
{{ 'global.action.cancel' | translate }}
|
||||
</button>
|
||||
</nb-card-footer>
|
||||
</nb-card>
|
|
@ -0,0 +1,98 @@
|
|||
@import "../../../assets/@theme/styles/_dialog.scss";
|
||||
@import '../../../assets/@theme/styles/themes';
|
||||
|
||||
.profile-setting-dialog {
|
||||
width: 45.25rem !important;
|
||||
height: 46rem;
|
||||
|
||||
.dialog-header {
|
||||
height: 8vh;
|
||||
|
||||
.header-text {
|
||||
font-size: 1.5rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
|
||||
.user-properties {
|
||||
padding-top: 1rem;
|
||||
|
||||
.user-img {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-img:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.properties {
|
||||
padding-left: 2rem;
|
||||
|
||||
.prefix-icon {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field:disabled {
|
||||
background-color: nb-theme(color-basic-transparent-focus);
|
||||
}
|
||||
|
||||
.untouchable {
|
||||
width: 30rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
float: left;
|
||||
color: nb-theme(color-danger-default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.language-settings{
|
||||
padding-top: 2rem;
|
||||
|
||||
.language-selection-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.language-radio-buttons {
|
||||
float: left;
|
||||
clear: none;
|
||||
padding-top: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.languageContainer {
|
||||
display: flex;
|
||||
max-width: 8rem;
|
||||
min-width: 8rem;
|
||||
|
||||
.flag {
|
||||
object-fit: contain;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-password-change{
|
||||
padding-top: 1rem;
|
||||
|
||||
.password-selection-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProfileSettingsComponent } from './profile-settings.component';
|
||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
|
||||
import {NbDialogRef, NbFormFieldModule} from '@nebular/theme';
|
||||
import {createSpyObj} from '@shared/modules/project-dialog/project-dialog.component.spec';
|
||||
import {ReactiveFormsModule} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ThemeModule} from '@assets/@theme/theme.module';
|
||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {HttpLoaderFactory} from '../../../app/common-app.module';
|
||||
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
import {NgxsModule} from '@ngxs/store';
|
||||
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
describe('ProfileSettingsComponent', () => {
|
||||
let component: ProfileSettingsComponent;
|
||||
let fixture: ComponentFixture<ProfileSettingsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ProfileSettingsComponent
|
||||
],
|
||||
imports: [
|
||||
NbFormFieldModule,
|
||||
ReactiveFormsModule,
|
||||
BrowserAnimationsModule,
|
||||
ThemeModule.forRoot(),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
NgxsModule.forRoot([SessionState]),
|
||||
HttpClientModule,
|
||||
HttpClientTestingModule
|
||||
],
|
||||
providers: [
|
||||
{provide: DialogService, useClass: DialogServiceMock},
|
||||
{provide: NbDialogRef, useValue: dialogSpy},
|
||||
KeycloakService
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProfileSettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
// ToDo: fix detectChanges() when from control accessor is defined
|
||||
// fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,262 @@
|
|||
import {ChangeDetectionStrategy, Component, Input, OnInit, Output} from '@angular/core';
|
||||
import {NbDialogRef} from '@nebular/theme';
|
||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {AbstractControl, FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms';
|
||||
import {ProfilePasswordFormData} from '@shared/modules/profile-settings/password-input-from/util/profile-password-form-data.model';
|
||||
import {Patterns} from '@shared/modules/profile-settings/patterns';
|
||||
import {LanguageOptions} from '@shared/modules/export-report-dialog/export-report-dialog.component';
|
||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||
import {User} from '@shared/models/user.model';
|
||||
import {UserService} from '@shared/services/user-service/user.service';
|
||||
import {BehaviorSubject, Observable, of} from 'rxjs';
|
||||
import deepEqual from 'deep-equal';
|
||||
import {UpdateUser, UpdateUserSettings} from '@shared/stores/session-state/session-state.actions';
|
||||
import {mapTo, tap} from 'rxjs/operators';
|
||||
import {Store} from '@ngxs/store';
|
||||
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile-settings',
|
||||
templateUrl: './profile-settings.component.html',
|
||||
styleUrls: ['./profile-settings.component.scss']
|
||||
})
|
||||
@UntilDestroy()
|
||||
export class ProfileSettingsComponent implements OnInit {
|
||||
/**
|
||||
* @param data contains all relevant information the dialog needs
|
||||
* @param data.title The translation key for the dialog title
|
||||
* @param data.key The translation key for the shown message
|
||||
* @param data.data The data that may be used in the message translation key
|
||||
*/
|
||||
@Input() data: any;
|
||||
user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
||||
|
||||
@Output() userServiceError: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
// HTML only
|
||||
readonly fa = FA;
|
||||
readonly USER_IMG = 'assets/images/demo/anon-user-icon.png';
|
||||
|
||||
// User form
|
||||
userFormGroup: FormGroup;
|
||||
userNameControl: AbstractControl;
|
||||
userFirstNameControl: AbstractControl;
|
||||
userLastNameControl: AbstractControl;
|
||||
userEmailControl: AbstractControl;
|
||||
oldUserData: UserProfileFormData;
|
||||
|
||||
// Password form
|
||||
passwordFormGroup: FormGroup;
|
||||
userPasswordInputControl: AbstractControl;
|
||||
private readonly initialPasswordInput: ProfilePasswordFormData = {oldPassword: '', newPassword: '', confirmNewPassword: ''};
|
||||
passwordStrong: boolean;
|
||||
oldPasswordData: ProfilePasswordFormData;
|
||||
|
||||
|
||||
// Language change
|
||||
profileLanguageControl = new FormControl(LanguageOptions.ENGLISH);
|
||||
profileLanguages = LanguageOptions;
|
||||
|
||||
constructor(protected dialogRef: NbDialogRef<any>,
|
||||
private fb: FormBuilder,
|
||||
private store: Store,
|
||||
private userService: UserService,
|
||||
private translateService: TranslateService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.profileLanguageControl.setValue(this.translateService.currentLang);
|
||||
this.setupUserFormGroup();
|
||||
// Load user profile
|
||||
this.userService.loadUserProfile().pipe(
|
||||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (user: User) => {
|
||||
this.user.next(user);
|
||||
this.userNameControl.setValue(user.username);
|
||||
this.userFirstNameControl.setValue(user.firstName);
|
||||
this.userLastNameControl.setValue(user.lastName);
|
||||
console.warn(this.user.getValue());
|
||||
},
|
||||
error: err => {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
this.oldUserData = Object.assign({}, this.userFormGroup.getRawValue());
|
||||
// Setup password
|
||||
this.setupPasswordFormGroup();
|
||||
this.oldPasswordData = Object.assign({}, this.passwordFormGroup.getRawValue());
|
||||
}
|
||||
|
||||
setupUserFormGroup(): void {
|
||||
this.userFormGroup = this.fb.group({
|
||||
username: [{value: '', disabled: true}, [Validators.required, Validators.pattern(Patterns.NO_WHITESPACES)]],
|
||||
firstName: [{value: '', disabled: false}, [Validators.required, Validators.pattern(Patterns.NO_WHITESPACES)]],
|
||||
lastName: [{value: '', disabled: false}, [Validators.required, Validators.pattern(Patterns.NO_WHITESPACES)]],
|
||||
eMail: [{value: '', disabled: true}, [Validators.required, Validators.email, Validators.pattern(Patterns.NO_WHITESPACES)]],
|
||||
avatarUploader: null
|
||||
});
|
||||
// Get form controls
|
||||
this.userNameControl = this.userFormGroup.get('username');
|
||||
this.userFirstNameControl = this.userFormGroup.get('firstName');
|
||||
this.userLastNameControl = this.userFormGroup.get('lastName');
|
||||
this.userEmailControl = this.userFormGroup.get('eMail');
|
||||
}
|
||||
|
||||
setupPasswordFormGroup(): void {
|
||||
this.passwordFormGroup = this.fb.group({
|
||||
passwordInput: this.initialPasswordInput
|
||||
});
|
||||
// get password-form control
|
||||
this.userPasswordInputControl = this.passwordFormGroup.get('passwordInput');
|
||||
}
|
||||
|
||||
onClickLanguage(language: string): void {
|
||||
this.translateService.use(language);
|
||||
}
|
||||
|
||||
onClickConfirm(): void {
|
||||
// ToDo: use handleUserUpdate() here
|
||||
const userFormData: UserProfileFormData = this.userFormGroup.getRawValue();
|
||||
// ToDo: use handlePasswordChange() here
|
||||
const passwordFormData: ProfilePasswordFormData = this.passwordFormGroup.getRawValue();
|
||||
// tslint:disable-next-line:no-console
|
||||
console.info('User', userFormData);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.info('Password', passwordFormData);
|
||||
// ToDo: Fix?
|
||||
if (this.allowChangeProfileData() && this.allowChangePassword()) {
|
||||
const formAndUser: [UserProfileFormData, User] = this.extractFormAndUser();
|
||||
this.handleUserUpdate(formAndUser['1']).subscribe({
|
||||
complete: () => {
|
||||
this.handlePasswordChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
// Changes occur only on the profile data without the password
|
||||
else if (this.allowChangeProfileData()) {
|
||||
const formAndUser: [UserProfileFormData, User] = this.extractFormAndUser();
|
||||
this.handleUserUpdate(formAndUser['1']).subscribe({
|
||||
complete: () => this.dialogRef.close({confirm: true})
|
||||
});
|
||||
}
|
||||
// Changes occur only on the password
|
||||
else if (this.allowChangePassword()) {
|
||||
this.handlePasswordChange();
|
||||
}
|
||||
// this.dialogRef.close({confirm: true});
|
||||
}
|
||||
|
||||
private handleUserUpdate(user: User): Observable<void> {
|
||||
/* return this.userService.updateUser(user, this.timeOfChange)
|
||||
.pipe(
|
||||
tap({
|
||||
next: resultingUser => {
|
||||
this.store.dispatch(new UpdateUserSettings(resultingUser));
|
||||
this.store.dispatch(new UpdateUser(resultingUser, true));
|
||||
},
|
||||
error: error => {
|
||||
console.error(error);
|
||||
this.onFailedUpdate(error);
|
||||
}
|
||||
}),
|
||||
mapTo(void 0),
|
||||
untilDestroyed(this)
|
||||
);*/
|
||||
return of();
|
||||
}
|
||||
|
||||
private handlePasswordChange(): void {
|
||||
const formData = this.passwordFormGroup.getRawValue();
|
||||
if (formData && formData.passwordInput) {
|
||||
const oldPassword = formData.passwordInput.oldPassword;
|
||||
const newPassword = formData.passwordInput.newPassword;
|
||||
// ToDo: Fix connection to keycloak
|
||||
/*this.userService.getCurrentAuthenticatedUser().pipe(
|
||||
switchMap((currentUser: CognitoUser) => {
|
||||
return this.userService.changePassword(currentUser, oldPassword, newPassword).pipe(
|
||||
catchError((err) => {
|
||||
if (err && 'message' in err) {
|
||||
if (err.code === 'LimitExceededException') {
|
||||
this.notificationService.showPopup('userProfile.password.limitExceeded', PopupType.FAILURE);
|
||||
} else {
|
||||
this.notificationService.showPopup('userProfile.password.invalidPassword', PopupType.FAILURE);
|
||||
}
|
||||
}
|
||||
this.userServiceError.next(true);
|
||||
return throwError(err);
|
||||
}),
|
||||
tap(() => {
|
||||
this.notificationService.showPopup('userProfile.password.changePasswordSuccess', PopupType.SUCCESS);
|
||||
this.userDialogRef.close('confirm');
|
||||
})
|
||||
);
|
||||
}),
|
||||
).subscribe({
|
||||
error: err => console.error(err)
|
||||
});*/
|
||||
}
|
||||
}
|
||||
|
||||
onClickCancel(): void {
|
||||
console.log(this.userFormGroup.get('firstName').dirty);
|
||||
console.log(this.userFormGroup.get('firstName')?.hasError('required'));
|
||||
console.log(this.userFirstNameControl.hasError('required'));
|
||||
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
extractFormAndUser(): [UserProfileFormData, User] {
|
||||
const user: User = Object.assign(new User(), this.store.selectSnapshot(SessionState.userAccount));
|
||||
const formData: UserProfileFormData = this.userFormGroup.getRawValue();
|
||||
user.username = formData.username;
|
||||
user.firstName = formData.firstName;
|
||||
user.lastName = formData.lastName;
|
||||
user.mailAddress = formData.email;
|
||||
|
||||
return [formData, user];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for valid changes on both the change-profile-data and the change-password forms.
|
||||
* @return true if we have one of the followings:
|
||||
* 1. Valid changes on both forms
|
||||
* 2. Valid changes on change-password form
|
||||
* 3. Valid changes on change-profile-data form with empty old, new and confirm passwords
|
||||
*/
|
||||
allowConfirm(): boolean {
|
||||
const userFormValid = this.allowChangeProfileData();
|
||||
|
||||
const passwordFormData: ProfilePasswordFormData = this.passwordFormGroup.getRawValue().passwordInput;
|
||||
const passwordFormValid = this.allowChangePassword();
|
||||
const passwordFormEmpty = (deepEqual(passwordFormData.oldPassword, '')
|
||||
&& deepEqual(passwordFormData.newPassword, '')
|
||||
&& deepEqual(passwordFormData.confirmNewPassword, ''));
|
||||
|
||||
return (userFormValid && passwordFormValid) || (passwordFormValid) || (userFormValid && passwordFormEmpty);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the change-profile-data form is valid and there are actual changes.
|
||||
*/
|
||||
allowChangeProfileData(): boolean {
|
||||
const formData: UserProfileFormData = this.userFormGroup.getRawValue();
|
||||
return this.userFormGroup.dirty && this.userFormGroup.valid && !deepEqual(formData, this.oldUserData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the change-password form is valid and the password is strong.
|
||||
*/
|
||||
allowChangePassword(): boolean {
|
||||
return this.passwordStrong && this.passwordFormGroup.valid && this.userFormGroup.dirty;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UserProfileFormData {
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
avatarUploader: FileList;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ProfileSettingsComponent} from '@shared/modules/profile-settings/profile-settings.component';
|
||||
import {NbButtonModule, NbCardModule, NbFormFieldModule, NbIconModule, NbInputModule, NbRadioModule, NbSelectModule} from '@nebular/theme';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
import {ReactiveFormsModule} from '@angular/forms';
|
||||
import { PasswordInputFromComponent } from './password-input-from/password-input-from.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ProfileSettingsComponent,
|
||||
PasswordInputFromComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NbCardModule,
|
||||
TranslateModule,
|
||||
FlexLayoutModule,
|
||||
NbButtonModule,
|
||||
FontAwesomeModule,
|
||||
NbSelectModule,
|
||||
ReactiveFormsModule,
|
||||
NbFormFieldModule,
|
||||
NbInputModule,
|
||||
NbRadioModule,
|
||||
NbIconModule
|
||||
],
|
||||
exports: [
|
||||
ProfileSettingsComponent
|
||||
]
|
||||
})
|
||||
export class ProfileSettingsModule {
|
||||
}
|
|
@ -5,6 +5,7 @@ import {DialogMessage, SecurityDialogMessage} from '@shared/services/dialog-serv
|
|||
import {ConfirmDialogComponent} from '@shared/modules/confirm-dialog/confirm-dialog.component';
|
||||
import {SecurityConfirmDialogComponent} from '@shared/modules/security-confirm-dialog/security-confirm-dialog.component';
|
||||
import {RetryDialogComponent} from '@shared/modules/retry-dialog/retry-dialog.component';
|
||||
import {any} from 'codelyzer/util/function';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -19,14 +20,15 @@ export class DialogService {
|
|||
*/
|
||||
openCustomDialog<T>(
|
||||
componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
|
||||
config?: Partial<NbDialogConfig<Partial<T> | string>>
|
||||
additionalData: any
|
||||
): NbDialogRef<T> {
|
||||
return this.dialog.open<T>(componentOrTemplateRef, {
|
||||
context: config?.context || undefined,
|
||||
closeOnEsc: config?.closeOnEsc || false,
|
||||
hasScroll: config?.hasScroll || false,
|
||||
autoFocus: config?.autoFocus || true,
|
||||
closeOnBackdropClick: config?.closeOnBackdropClick || false
|
||||
closeOnEsc: false,
|
||||
hasScroll: false,
|
||||
autoFocus: true,
|
||||
closeOnBackdropClick: false,
|
||||
// @ts-ignore
|
||||
context: {data: additionalData}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
min-height: 6rem;
|
||||
|
||||
.header-title {
|
||||
@include multiLineEllipsis($font-size: 1.5rem, $font-weight: bold, $line-height: 1.5rem, $lines-to-show: 2, $max-width: 14rem);
|
||||
@include multiLineEllipsis($font-size: 1.5rem, $font-weight: bold, $line-height: 2rem, $lines-to-show: 4, $max-width: 14rem);
|
||||
}
|
||||
|
||||
.state-tag {
|
||||
|
|
Loading…
Reference in New Issue