Pull request #142: RED-1265

Merge in RED/ui from RED-1265 to master

* commit '813b887cfba4cc6ae1f36b87929a8c02cef9fbb6':
  refactor user profile
  add user profile screen
This commit is contained in:
Timo Bejan 2021-04-08 13:39:29 +02:00
commit 95710964dc
10 changed files with 307 additions and 87 deletions

View File

@ -9,6 +9,7 @@ import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { DownloadsListScreenComponent } from './components/downloads-list-screen/downloads-list-screen.component';
import { AppStateGuard } from './state/app-state.guard';
import { UserProfileScreenComponent } from './components/user-profile/user-profile-screen.component';
const routes = [
{
@ -37,6 +38,20 @@ const routes = [
routeGuards: [AuthGuard, RedRoleGuard]
}
},
{
path: 'ui/my-profile',
component: BaseScreenComponent,
children: [
{
path: '',
component: UserProfileScreenComponent
}
],
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'ui/admin',
component: BaseScreenComponent,

View File

@ -29,12 +29,13 @@ import { DownloadsListScreenComponent } from './components/downloads-list-screen
import { AppRoutingModule } from './app-routing.module';
import { SharedModule } from './modules/shared/shared.module';
import { FileUploadDownloadModule } from './modules/upload-download/file-upload-download.module';
import { UserProfileScreenComponent } from './components/user-profile/user-profile-screen.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
}
const screens = [BaseScreenComponent, PdfViewerScreenComponent, HtmlDebugScreenComponent, DownloadsListScreenComponent];
const screens = [BaseScreenComponent, PdfViewerScreenComponent, HtmlDebugScreenComponent, DownloadsListScreenComponent, UserProfileScreenComponent];
const components = [AppComponent, LogoComponent, AuthErrorComponent, ToastComponent, NotificationsComponent, ...screens];

View File

@ -1,17 +1,18 @@
<div class="red-top-bar">
<div class="top-bar-row">
<div class="menu-placeholder" *ngIf="!permissionsService.isUser()"></div>
<div class="menu visible-lt-lg" *ngIf="permissionsService.isUser()">
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
<mat-icon svgIcon="red:menu"></mat-icon>
<div class='red-top-bar'>
<div class='top-bar-row'>
<div class='menu-placeholder' *ngIf='!permissionsService.isUser()'></div>
<div class='menu visible-lt-lg' *ngIf='permissionsService.isUser()'>
<button [matMenuTriggerFor]='menuNav' mat-flat-button>
<mat-icon svgIcon='red:menu'></mat-icon>
</button>
<mat-menu #menuNav="matMenu">
<button mat-menu-item routerLink="/ui/projects" translate="top-bar.navigation-items.projects"></button>
<button *ngIf="appStateService.activeProject" [routerLink]="'/ui/projects/' + appStateService.activeProjectId" mat-menu-item>
<mat-menu #menuNav='matMenu'>
<button mat-menu-item routerLink='/ui/projects' translate='top-bar.navigation-items.projects'></button>
<button *ngIf='appStateService.activeProject'
[routerLink]="'/ui/projects/' + appStateService.activeProjectId" mat-menu-item>
{{ appStateService.activeProject.project.projectName }}
</button>
<button
*ngIf="appStateService.activeFile"
*ngIf='appStateService.activeFile'
[routerLink]="'/ui/projects/' + appStateService.activeProjectId + '/file/' + appStateService.activeFile.fileId"
mat-menu-item
>
@ -19,84 +20,97 @@
</button>
</mat-menu>
</div>
<div class="menu flex-2 visible-lg breadcrumbs-container" *ngIf="permissionsService.isUser()">
<div class='menu flex-2 visible-lg breadcrumbs-container' *ngIf='permissionsService.isUser()'>
<a
class="breadcrumb"
routerLink="/ui/projects"
translate="top-bar.navigation-items.projects"
routerLinkActive="active"
*ngIf="projectsView"
[routerLinkActiveOptions]="{ exact: true }"
class='breadcrumb'
routerLink='/ui/projects'
translate='top-bar.navigation-items.projects'
routerLinkActive='active'
*ngIf='projectsView'
[routerLinkActiveOptions]='{ exact: true }'
></a>
<a
class="breadcrumb back-to-projects"
routerLink="/ui/projects"
routerLinkActive="active"
*ngIf="settingsView"
[routerLinkActiveOptions]="{ exact: true }"
class='breadcrumb back-to-projects'
routerLink='/ui/projects'
routerLinkActive='active'
*ngIf='settingsView'
[routerLinkActiveOptions]='{ exact: true }'
>
<mat-icon svgIcon="red:expand"></mat-icon>
<mat-icon svgIcon='red:expand'></mat-icon>
{{ 'top-bar.navigation-items.back-to-projects' | translate }}
</a>
<ng-container *ngIf="projectsView">
<mat-icon class="primary" *ngIf="!appStateService.activeProject" svgIcon="red:arrow-down"></mat-icon>
<mat-icon *ngIf="appStateService.activeProject" svgIcon="red:arrow-right"></mat-icon>
<ng-container *ngIf='projectsView'>
<mat-icon class='primary' *ngIf='!appStateService.activeProject' svgIcon='red:arrow-down'></mat-icon>
<mat-icon *ngIf='appStateService.activeProject' svgIcon='red:arrow-right'></mat-icon>
<a
*ngIf="appStateService.activeProject"
class="breadcrumb"
*ngIf='appStateService.activeProject'
class='breadcrumb'
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
routerLinkActive='active'
[routerLinkActiveOptions]='{ exact: true }'
>
{{ appStateService.activeProject.project.projectName }}
</a>
<mat-icon svgIcon="red:arrow-right" *ngIf="appStateService.activeFile"></mat-icon>
<mat-icon svgIcon='red:arrow-right' *ngIf='appStateService.activeFile'></mat-icon>
<a
*ngIf="appStateService.activeFile"
class="breadcrumb"
*ngIf='appStateService.activeFile'
class='breadcrumb'
[routerLink]="'/ui/projects/' + appStateService.activeProjectId + '/file/' + appStateService.activeFile.fileId"
routerLinkActive="active"
routerLinkActive='active'
>
{{ appStateService.activeFile.filename }}
</a>
</ng-container>
</div>
<div class="center flex-1">
<redaction-hidden-action (action)="userPreferenceService.toggleDevFeatures()">
<div class='center flex-1'>
<redaction-hidden-action (action)='userPreferenceService.toggleDevFeatures()'>
<redaction-logo></redaction-logo>
</redaction-hidden-action>
<div class="app-name">{{ titleService.getTitle() }}</div>
<span class="dev-mode" *ngIf="userPreferenceService.areDevFeaturesEnabled" translate="dev-mode"></span>
<div class='app-name'>{{ titleService.getTitle() }}</div>
<span class='dev-mode' *ngIf='userPreferenceService.areDevFeaturesEnabled' translate='dev-mode'></span>
</div>
<div class="menu right flex-2">
<redaction-notifications class="mr-8" *ngIf="userPreferenceService.areDevFeaturesEnabled"></redaction-notifications>
<redaction-user-button [user]="user" [matMenuTriggerFor]="userMenu" [showDot]="showPendingDownloadsDot"></redaction-user-button>
<mat-menu #userMenu="matMenu">
<div class='menu right flex-2'>
<redaction-notifications class='mr-8'
*ngIf='userPreferenceService.areDevFeaturesEnabled'></redaction-notifications>
<redaction-user-button [user]='user' [matMenuTriggerFor]='userMenu'
[showDot]='showPendingDownloadsDot'></redaction-user-button>
<mat-menu #userMenu='matMenu'>
<button
*ngIf="permissionsService.isManager()"
(click)="appStateService.reset()"
*ngIf='permissionsService.isUser()'
[routerLink]="'/ui/my-profile'"
mat-menu-item
translate='top-bar.navigation-items.my-account.children.my-profile'
></button>
<button
*ngIf='permissionsService.isManager()'
(click)='appStateService.reset()'
[routerLink]="'/ui/admin'"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.admin"
translate='top-bar.navigation-items.my-account.children.admin'
></button>
<button
*ngIf="permissionsService.isUser()"
*ngIf='permissionsService.isUser()'
[routerLink]="'/ui/downloads'"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.downloads"
translate='top-bar.navigation-items.my-account.children.downloads'
></button>
<button [matMenuTriggerFor]="language" mat-menu-item translate="top-bar.navigation-items.my-account.children.language.label"></button>
<mat-menu #language="matMenu">
<button (click)="changeLanguage('en')" mat-menu-item translate="top-bar.navigation-items.my-account.children.language.english"></button>
<button (click)="changeLanguage('de')" mat-menu-item translate="top-bar.navigation-items.my-account.children.language.german"></button>
<button [matMenuTriggerFor]='language' mat-menu-item
translate='top-bar.navigation-items.my-account.children.language.label'></button>
<mat-menu #language='matMenu'>
<button
*ngFor='let lang of languages'
(click)='changeLanguage(lang)'
mat-menu-item
translate
>top-bar.navigation-items.my-account.children.language.{{lang}}</button>
</mat-menu>
<button (click)="logout()" mat-menu-item>
<mat-icon svgIcon="red:logout"></mat-icon>
<span translate="top-bar.navigation-items.my-account.children.logout"> </span>
<button (click)='logout()' mat-menu-item>
<mat-icon svgIcon='red:logout'></mat-icon>
<span translate='top-bar.navigation-items.my-account.children.logout'> </span>
</button>
</mat-menu>
</div>
</div>
<div class="divider"></div>
<div class='divider'></div>
</div>
<router-outlet></router-outlet>

View File

@ -9,6 +9,7 @@ import { AppConfigService } from '../../modules/app-config/app-config.service';
import { Title } from '@angular/platform-browser';
import { FileDownloadService } from '../../modules/upload-download/services/file-download.service';
import { StatusOverlayService } from '../../modules/upload-download/services/status-overlay.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'redaction-base-screen',
@ -32,7 +33,8 @@ export class BaseScreenComponent {
private readonly _appConfigService: AppConfigService,
private readonly _router: Router,
private readonly _languageService: LanguageService,
private readonly _userService: UserService
private readonly _userService: UserService,
private readonly _translateService: TranslateService
) {
_router.events.subscribe(() => {
this._projectsView = this._router.url.indexOf('/ui/projects') === 0;
@ -51,6 +53,10 @@ export class BaseScreenComponent {
return this.fileDownloadService.hasPendingDownloads;
}
get languages(): string[] {
return this._translateService.langs;
}
logout() {
this._userService.logout();
}

View File

@ -0,0 +1,45 @@
<section class="red-content-inner">
<div class="left-container full-height">
<div class="overlay-shadow"></div>
<div class="dialog">
<div class="dialog-header">
<div class="heading-l" [translate]="'user-profile.title'"></div>
</div>
<form [formGroup]="formGroup" (submit)="save()">
<div class="dialog-content">
<div class="dialog-content-left">
<div class="red-input-group required">
<label [translate]="'user-profile.form.email'"></label>
<input formControlName="email" name="email" type="email" />
</div>
<div class="red-input-group">
<label [translate]="'user-profile.form.first-name'"></label>
<input formControlName="firstName" name="firstName" type="text" />
</div>
<div class="red-input-group">
<label [translate]="'user-profile.form.last-name'"></label>
<input formControlName="lastName" name="lastName" type="text" />
</div>
<div class="red-input-group">
<label [translate]="'top-bar.navigation-items.my-account.children.language.label'"></label>
<mat-select formControlName="language">
<mat-option *ngFor="let language of languages" [value]="language">
{{ 'top-bar.navigation-items.my-account.children.language.' + language | translate }}
</mat-option>
</mat-select>
</div>
</div>
</div>
<div class="dialog-actions">
<button [disabled]="formGroup.invalid || !(profileChanged || languageChanged)" color="primary" mat-flat-button type="submit">
{{ 'user-profile.actions.save' | translate }}
</button>
</div>
</form>
</div>
</div>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -0,0 +1,18 @@
@import '../../../assets/styles/red-mixins';
.left-container {
background-color: $grey-2;
justify-content: center;
@include scroll-bar;
overflow: auto;
}
.full-height {
display: flex;
flex-direction: row;
position: absolute;
bottom: 0;
width: 100%;
height: calc(100% + 50px);
z-index: 1;
}

View File

@ -0,0 +1,109 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserService } from '../../services/user.service';
import { PermissionsService } from '../../services/permissions.service';
import { LanguageService } from '../../i18n/language.service';
import { TranslateService } from '@ngx-translate/core';
import { User, UserControllerService } from '@redaction/red-ui-http';
interface ProfileModel {
email: string;
firstName: string;
lastName: string;
language: string;
}
@Component({
selector: 'redaction-user-profile-screen',
templateUrl: './user-profile-screen.component.html',
styleUrls: ['./user-profile-screen.component.scss']
})
export class UserProfileScreenComponent implements OnInit {
public viewReady = false;
public formGroup: FormGroup;
private _initialValue: ProfileModel;
private _user: User;
constructor(
public readonly permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
private readonly _userService: UserService,
private readonly _userControllerService: UserControllerService,
private readonly _languageService: LanguageService,
private readonly _translateService: TranslateService
) {
this.formGroup = this._formBuilder.group({
email: [undefined, [Validators.required, Validators.email]],
firstName: [undefined],
lastName: [undefined],
language: [undefined]
});
}
ngOnInit() {
this._loadData();
}
private _loadData(): void {
try {
this._user = this._userService.getUserById(this._userService.userId);
this._initialValue = {
email: this._user.email,
firstName: this._user.firstName,
lastName: this._user.lastName,
language: this._languageService.currentLanguage
};
this.formGroup.patchValue(this._initialValue, { emitEvent: false });
} catch (e) {
} finally {
this.viewReady = true;
}
}
public get languageChanged(): boolean {
return this._initialValue['language'] !== this.formGroup.get('language').value;
}
public get profileChanged(): boolean {
const keys = Object.keys(this.formGroup.getRawValue());
keys.splice(keys.indexOf('language'), 1);
for (const key of keys) {
if (this._initialValue[key] !== this.formGroup.get(key).value) {
return true;
}
}
return false;
}
public get languages(): string[] {
return this._translateService.langs;
}
public async save(): Promise<void> {
this.viewReady = false;
if (this.languageChanged) {
this._languageService.changeLanguage(this.formGroup.get('language').value);
}
if (this.profileChanged) {
const value = this.formGroup.value as ProfileModel;
delete value.language;
await this._userControllerService
.updateProfile(
{
...value,
roles: this._user.roles
},
this._user.userId
)
.toPromise();
}
this._initialValue = this.formGroup.value;
this.viewReady = true;
}
}

View File

@ -8,34 +8,6 @@
justify-content: center;
@include scroll-bar;
overflow: auto;
.dialog {
border-radius: 8px;
margin-top: 40px;
margin-bottom: 70px;
background-color: $white;
max-width: 650px;
height: fit-content;
box-shadow: 0 1px 5px 0 rgba(40, 50, 65, 0.19);
position: unset;
.heading-l {
margin-bottom: 16px;
}
.dialog-content {
display: flex;
.dialog-content-left {
min-width: 300px;
margin-right: 64px;
}
.link-action {
margin-top: 8px;
}
}
}
}
.w-100 {

View File

@ -63,10 +63,11 @@
"children": {
"admin": "Settings",
"downloads": "My Downloads",
"my-profile": "My Profile",
"language": {
"label": "Language",
"english": "English",
"german": "German"
"en": "English",
"de": "German"
},
"logout": "Logout"
}
@ -791,6 +792,17 @@
"save": "Save Document Info",
"save-approval": "Save and Send for Approval"
},
"user-profile": {
"title": "My profile",
"form": {
"email": "Email",
"first-name": "First name",
"last-name": "Last name"
},
"actions": {
"save": "Save profile"
}
},
"user-listing": {
"table-header": {
"title": "{{length}} users"

View File

@ -120,6 +120,34 @@ body {
}
}
.dialog {
border-radius: 8px;
margin-top: 40px;
margin-bottom: 70px;
background-color: $white;
max-width: 650px;
height: fit-content;
box-shadow: 0 1px 5px 0 rgba(40, 50, 65, 0.19);
position: unset;
.heading-l {
margin-bottom: 16px;
}
.dialog-content {
display: flex;
.dialog-content-left {
min-width: 300px;
margin-right: 64px;
}
.link-action {
margin-top: 8px;
}
}
}
@media only screen and (max-width: 1600px) {
redaction-initials-avatar .username:not(.always-visible) {
display: none;