Adding toast notification

This commit is contained in:
Martin Stockhammer 2020-12-23 22:43:04 +01:00
parent 3012e2f76f
commit 7744b086d7
12 changed files with 343 additions and 18 deletions

View File

@ -35,6 +35,7 @@
<button type="button" class="btn btn-danger" (click)="modal.close('Save click')">{{'modal.close'|translate}}</button>
</div>
</ng-template>
<app-toasts aria-live="polite" aria-atomic="true"></app-toasts>
<div class="app d-flex flex-column">
<header>
<nav class="navbar navbar-expand-md fixed-top navbar-light " style="background-color: #c6cbd2;">

View File

@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {TemplateRef} from "@angular/core";
export class AppNotification {
origin: string;
header: string;
body: string | TemplateRef<any>;
timestamp: Date;
classname: string='';
delay:number=5000;
contextData:any;
type:string='normal'
constructor(origin: string, body: string|TemplateRef<any>, header:string="", options: any = {}, timestamp:Date = new Date()) {
this.origin = origin
this.header = header;
this.body = body;
this.timestamp = timestamp;
console.log("Options " + JSON.stringify(options));
if (options.classname) {
this.classname = options.classname;
}
if (options.delay) {
this.delay = options.delay;
}
if (options.contextData) {
this.contextData = options.contextData;
}
if (options.type) {
this.type = options.type;
}
}
public toString(): string {
return this.origin + ',classname:' + this.classname + ", delay:" + this.delay +", context: "+JSON.stringify(this.contextData);
}
}

View File

@ -20,4 +20,14 @@ export class ErrorMessage {
error_key: string;
args: string[];
message: string;
static of(messageString:string): ErrorMessage {
const msg = new ErrorMessage()
msg.message = messageString;
return msg;
}
public toString() {
return this.message;
}
}

View File

@ -54,6 +54,9 @@
<input type="password" class="form-control" formControlName="password" id="password"
[ngClass]="valid('password')"
placeholder="{{'users.input.password'|translate}}">
<div *ngFor="let error of getErrorsFor('password')" class="invalid-feedback">
{{error}}
</div>
</div>
<div class="form-group col-md-8">
<label for="confirm_password">{{'users.attributes.confirm_password' |translate}}</label>
@ -87,16 +90,21 @@
<button class="btn btn-primary" type="submit"
[attr.disabled]="userForm.valid?null:true">{{'users.add.submit'|translate}}</button>
</div>
<div *ngIf="success" class="alert alert-success" role="alert">
User <a [routerLink]="['/security','users','edit',result?.user_id]">{{result?.user_id}}</a> was added to the list.
<div class="form-group col-md-8">
<button class="btn btn-primary" (click)="showMessage()">Show Message</button>
</div>
<div *ngIf="error" class="alert alert-danger" role="alert" >
<h4 class="alert-heading">{{'users.add.errortitle'|translate}}</h4>
<ng-container *ngFor="let message of errorResult?.error_messages; first as isFirst" >
<hr *ngIf="!isFirst">
<ng-template #successTmpl let-userId="user_id">
User <a [routerLink]="['/security','users','edit',userId]">{{userId}}</a> was added to the list.
</ng-template>
<ng-template #errorTmpl let-messages="error_messages">
<h4 class="alert-heading">{{'users.add.errortitle1'|translate}}</h4>
<p>{{'users.add.errortitle2'|translate}}</p>
<ng-container *ngFor="let message of messages; first as isFirst" >
<hr>
<p>{{message.message}}</p>
</ng-container>
</div>
</ng-template>
</form>

View File

@ -16,13 +16,15 @@
* under the License.
*/
import {Component, OnInit} from '@angular/core';
import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn} from '@angular/forms';
import {UserService} from "../../../../services/user.service";
import {ErrorResult} from "../../../../model/error-result";
import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core';
import {FormBuilder} from '@angular/forms';
import {UserService} from "@app/services/user.service";
import {ErrorResult} from "@app/model/error-result";
import {catchError} from "rxjs/operators";
import {UserInfo} from "../../../../model/user-info";
import {UserInfo} from "@app/model/user-info";
import {ManageUsersBaseComponent} from "../manage-users-base.component";
import {ToastService} from "@app/services/toast.service";
import {ErrorMessage} from "@app/model/error-message";
@Component({
selector: 'app-manage-users-add',
@ -31,7 +33,10 @@ import {ManageUsersBaseComponent} from "../manage-users-base.component";
})
export class ManageUsersAddComponent extends ManageUsersBaseComponent implements OnInit {
constructor(userService: UserService, fb: FormBuilder) {
@ViewChild('errorTmpl') public errorTmpl: TemplateRef<any>;
@ViewChild('successTmpl') public successTmpl: TemplateRef<any>;
constructor(userService: UserService, fb: FormBuilder, private toastService: ToastService) {
super(userService, fb);
}
@ -61,12 +66,15 @@ export class ManageUsersAddComponent extends ManageUsersBaseComponent implements
this.errorResult = error;
this.success = false;
this.error = true;
this.toastService.showError('manage-users-add',this.errorTmpl,{contextData:this.errorResult})
return [];
// return throwError(error);
})).subscribe((user: UserInfo) => {
this.result = user;
this.success = true;
this.error = false;
this.toastService.showSuccess('manage-users-add',this.successTmpl,{contextData:this.result})
this.userForm.reset(this.formInitialValues);
});
}
@ -74,7 +82,18 @@ export class ManageUsersAddComponent extends ManageUsersBaseComponent implements
showMessage() {
this.result=new UserInfo()
this.result.user_id='XXXXX'
const errorResult : ErrorResult = new ErrorResult([
ErrorMessage.of('Not so good'),
ErrorMessage.of('Completely crap')
]);
console.log(JSON.stringify(errorResult));
errorResult.status=422;
this.toastService.showSuccess('manage-users-add',this.successTmpl,{contextData:this.result,delay:1000})
this.toastService.showError('manage-users-add',this.errorTmpl,{contextData:errorResult,delay:10000})
}
}

View File

@ -96,6 +96,16 @@ export class ManageUsersBaseComponent {
}
}
public getErrorsFor(formField:string) : string[] {
let field=this.userForm.get(formField)
if (field) {
if (field.errors) {
return Object.values(field.errors);
}
}
return []
}
/**
* Async validator with debounce time
* @constructor

View File

@ -26,7 +26,8 @@ import {
NgbModalModule,
NgbPaginationModule,
NgbTooltipModule,
NgbTypeaheadModule
NgbTypeaheadModule,
NgbToastModule
} from "@ng-bootstrap/ng-bootstrap";
import {TranslateCompiler, TranslateLoader, TranslateModule} from "@ngx-translate/core";
import {TranslateMessageFormatCompiler} from "ngx-translate-messageformat-compiler";
@ -35,6 +36,7 @@ import {TranslateHttpLoader} from "@ngx-translate/http-loader";
import {RouterModule} from "@angular/router";
import { WithLoadingPipe } from './with-loading.pipe';
import { StripLoadingPipe } from './strip-loading.pipe';
import { ToastComponent } from './toast/toast.component';
export { LoadingValue } from './model/loading-value';
export { PageQuery } from './model/page-query';
@ -45,7 +47,8 @@ export { PageQuery } from './model/page-query';
SortedTableHeaderComponent,
SortedTableHeaderRowComponent,
WithLoadingPipe,
StripLoadingPipe
StripLoadingPipe,
ToastComponent
],
exports: [
CommonModule,
@ -56,17 +59,20 @@ export { PageQuery } from './model/page-query';
NgbAccordionModule,
NgbModalModule,
NgbTypeaheadModule,
NgbToastModule,
PaginatedEntitiesComponent,
SortedTableHeaderComponent,
SortedTableHeaderRowComponent,
WithLoadingPipe,
StripLoadingPipe
StripLoadingPipe,
ToastComponent
],
imports: [
CommonModule,
RouterModule,
NgbPaginationModule,
NgbTooltipModule,
NgbToastModule,
TranslateModule.forChild({
compiler: {
provide: TranslateCompiler,

View File

@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToastComponent } from './toast.component';
describe('ToastComponent', () => {
let component: ToastComponent;
let fixture: ComponentFixture<ToastComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ToastComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ToastComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, OnInit } from '@angular/core';
import {ToastService} from "@app/services/toast.service";
import {TemplateRef} from "@angular/core";
import {AppNotification} from "@app/model/app-notification";
@Component({
selector: 'app-toasts',
template: `
<ngb-toast
*ngFor="let toast of toastService.toasts"
[class]="toast.classname"
[autohide]="autohide"
[delay]="toast.delay || 5000"
(hidden)="toastService.remove(toast); autohide=true;"
(mouseenter)="autohide = false"
(mouseleave)="autohide = true"
>
<i *ngIf="toast.type=='error'" class="fas fa-exclamation-triangle"></i>
<ng-template [ngIf]="isTemplate(toast)" [ngIfElse]="text">
<ng-template [ngTemplateOutlet]="toast.body" [ngTemplateOutletContext]="toast.contextData" ></ng-template>
</ng-template>
<ng-template #text>{{ toast.body }}</ng-template>
</ngb-toast>
`,
styles: [".ngb-toasts{margin:.5em;padding:0.5em;position:fixed;right:2px;top:2px;z-index:1200}"
],
host: {'[class.ngb-toasts]': 'true'}
})
export class ToastComponent implements OnInit {
autohide:boolean=true;
constructor(public toastService:ToastService) { }
ngOnInit(): void {
}
isTemplate(toast:AppNotification) {
console.log("Context data: "+JSON.stringify(toast.contextData))
return toast.body instanceof TemplateRef; }
}

View File

@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { TestBed } from '@angular/core/testing';
import { ToastService } from './toast.service';
describe('ToastService', () => {
let service: ToastService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ToastService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Injectable, TemplateRef } from '@angular/core';
import {AppNotification} from "@app/model/app-notification";
import {not} from "rxjs/internal-compatibility";
@Injectable({
providedIn: 'root'
})
export class ToastService {
maxNotifications:number=10
maxHistory:number=100
toasts:AppNotification[]=[]
toastHistory:AppNotification[]=[]
constructor() { }
show(origin:string, textOrTpl: string | TemplateRef<any>, options: any = {}) {
let notification = new AppNotification(origin, textOrTpl, "", options);
this.toasts.push(notification);
this.toastHistory.push(notification);
if (this.toasts.length>this.maxNotifications) {
this.toasts.splice(0, 1);
}
if (this.toastHistory.length>this.maxHistory) {
this.toastHistory.splice(0, 1);
}
console.log("Notification " + notification);
}
showStandard(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) {
options.classname='bg-primary'
if (!options.delay) {
options.delay=8000
}
this.show(origin,textOrTpl,options)
}
showError(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) {
options.classname='bg-warning'
options.type='error'
if (!options.delay) {
options.delay=10000
}
this.show(origin,textOrTpl,options)
}
showSuccess(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) {
options.classname='bg-info'
options.type='success'
if (!options.delay) {
options.delay=8000
}
this.show(origin,textOrTpl,options)
}
remove(toast) {
this.toasts = this.toasts.filter(t => t != toast);
}
}

View File

@ -99,7 +99,8 @@
"add": {
"head": "Add User",
"submit": "Add User",
"errortitle": "Could not add the user. Please check the following error messages."
"errortitle1": "Could not add the user!",
"errortitle2": "Please check the following error messages:"
},
"edit": {
"submit": "Save Changes",