pull/2613/head
Halil İbrahim Kalkan 5 years ago
commit 4e118b11bf

@ -6,7 +6,7 @@ import { eLayoutType } from '@abp/ng.core';
template: `
<router-outlet></router-outlet>
<abp-confirmation></abp-confirmation>
<abp-toast></abp-toast>
<abp-toast-container right="30px" bottom="30px"></abp-toast-container>
`,
})
export class AccountLayoutComponent {

@ -34,7 +34,8 @@
<ng-template #defaultLink let-route>
<li class="nav-item" *abpPermission="route.requiredPolicy">
<a class="nav-link" [routerLink]="[route.url]"
><i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i> {{ route.name | abpLocalization }}</a
><i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i>
{{ route.name | abpLocalization }}</a
>
</li>
</ng-template>
@ -47,7 +48,9 @@
class="nav-item dropdown"
display="static"
(click)="
navbarRootDropdown.expand ? (navbarRootDropdown.expand = false) : (navbarRootDropdown.expand = true)
navbarRootDropdown.expand
? (navbarRootDropdown.expand = false)
: (navbarRootDropdown.expand = true)
"
>
<a
@ -57,7 +60,8 @@
aria-expanded="false"
href="javascript:void(0)"
>
<i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i> {{ route.name | abpLocalization }}
<i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i>
{{ route.name | abpLocalization }}
</a>
<div
#routeContainer
@ -146,12 +150,15 @@
</div>
</nav>
<div [@slideFromBottom]="outlet && outlet.activatedRoute && outlet.activatedRoute.routeConfig.path" class="container">
<div
[@slideFromBottom]="outlet && outlet.activatedRoute && outlet.activatedRoute.routeConfig.path"
class="container"
>
<router-outlet #outlet="outlet"></router-outlet>
</div>
<abp-confirmation></abp-confirmation>
<abp-toast></abp-toast>
<abp-toast-container right="30px" bottom="30px"></abp-toast-container>
<ng-template #appName>
{{ appInfo.name }}
@ -213,12 +220,12 @@
[class.d-block]="smallScreen"
[class.abp-mh-25]="smallScreen && currentUserDropdown.isOpen()"
>
<a class="dropdown-item" routerLink="/account/manage-profile"><i class="fa fa-cog mr-1"></i>{{
'AbpAccount::ManageYourProfile' | abpLocalization
}}</a>
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()"><i class="fa fa-power-off mr-1"></i>{{
'AbpUi::Logout' | abpLocalization
}}</a>
<a class="dropdown-item" routerLink="/account/manage-profile"
><i class="fa fa-cog mr-1"></i>{{ 'AbpAccount::ManageYourProfile' | abpLocalization }}</a
>
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()"
><i class="fa fa-power-off mr-1"></i>{{ 'AbpUi::Logout' | abpLocalization }}</a
>
</div>
</div>
</li>

@ -6,7 +6,7 @@ import { eLayoutType } from '@abp/ng.core';
template: `
<router-outlet></router-outlet>
<abp-confirmation></abp-confirmation>
<abp-toast></abp-toast>
<abp-toast-container right="30px" bottom="30px"></abp-toast-container>
`,
})
export class EmptyLayoutComponent {

@ -48,12 +48,6 @@ export default `
.container > .card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
}
.abp-confirm .abp-confirm-footer {
background-color: #f4f4f7 !important;
}
.abp-confirm .ui-toast-message-content {
background-color: #fff !important;
}
@media screen and (min-width: 768px) {
.navbar .dropdown:hover > .dropdown-menu {

@ -1,56 +0,0 @@
import { MessageService } from 'primeng/components/common/messageservice';
import { Observable, Subject } from 'rxjs';
import { Toaster } from '../models/toaster';
import { Config } from '@abp/ng.core';
export abstract class AbstractToaster<T = Toaster.Options> {
status$: Subject<Toaster.Status>;
key = 'abpToast';
sticky = false;
constructor(protected messageService: MessageService) {}
info(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'info', options);
}
success(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'success', options);
}
warn(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'warn', options);
}
error(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'error', options);
}
protected show(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
severity: Toaster.Severity,
options?: T,
): Observable<Toaster.Status> {
this.messageService.clear(this.key);
this.messageService.add({
severity,
detail: message || '',
summary: title || '',
...options,
key: this.key,
...(typeof (options || ({} as any)).sticky === 'undefined' && { sticky: this.sticky }),
});
this.status$ = new Subject<Toaster.Status>();
return this.status$;
}
clear(status?: Toaster.Status) {
this.messageService.clear(this.key);
this.status$.next(status || Toaster.Status.dismiss);
this.status$.complete();
}
}

@ -3,3 +3,4 @@ export * from './collapse.animations';
export * from './fade.animations';
export * from './modal.animations';
export * from './slide.animations';
export * from './toast.animations';

@ -0,0 +1,17 @@
import { animate, query, style, transition, trigger } from '@angular/animations';
export const toastInOut = trigger('toastInOut', [
transition('* <=> *', [
query(
':enter',
[
style({ opacity: 0, transform: 'translateY(20px)' }),
animate('350ms ease', style({ opacity: 1, transform: 'translateY(0)' })),
],
{ optional: true },
),
query(':leave', animate('450ms ease', style({ opacity: 0 })), {
optional: true,
}),
]),
]);

@ -0,0 +1,34 @@
<div class="confirmation show" *ngIf="visible">
<div class="confirmation-backdrop"></div>
<div class="confirmation-dialog">
<div class="icon-container" [ngClass]="data.severity" *ngIf="data.severity">
<i class="fa icon" [ngClass]="iconClass"></i>
</div>
<div class="content">
<h1 class="title" *ngIf="data.title">
{{ data.title | abpLocalization: data.options?.titleLocalizationParams }}
</h1>
<p class="message" *ngIf="data.message">
{{ data.message | abpLocalization: data.options?.messageLocalizationParams }}
</p>
</div>
<div class="footer">
<button
id="cancel"
class="confirmation-button confirmation-button-reject"
*ngIf="!data?.options?.hideCancelBtn"
(click)="close(reject)"
>
{{ data.options?.cancelText || 'AbpUi::Cancel' | abpLocalization }}
</button>
<button
id="confirm"
class="confirmation-button confirmation-button-approve"
*ngIf="!data?.options?.hideYesBtn"
(click)="close(confirm)"
>
{{ data.options?.yesText || 'AbpUi::Yes' | abpLocalization }}
</button>
</div>
</div>
</div>

@ -0,0 +1,120 @@
.confirmation {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
place-items: center;
z-index: 1060;
&.show {
display: grid;
}
.confirmation-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(#000, 0.7);
z-index: 1061 !important;
}
.confirmation-dialog {
display: flex;
flex-direction: column;
margin: 20px auto;
padding: 0;
border: none;
border-radius: 10px;
min-width: 450px;
min-height: 300px;
background-color: #fff;
box-shadow: 0 0 10px -5px rgba(#000, 0.5);
z-index: 1062 !important;
.icon-container {
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 10px 0;
padding: 20px;
.icon {
width: 100px;
height: 100px;
stroke-width: 1;
fill: #fff;
font-size: 80px;
text-align: center;
}
&.neutral .icon {
}
&.info .icon {
stroke: #2f96b4;
color: #2f96b4;
}
&.success .icon {
stroke: #51a351;
color: #51a351;
}
&.warning .icon {
stroke: #f89406;
color: #f89406;
}
&.error .icon {
stroke: #bd362f;
color: #bd362f;
}
}
.content {
flex-grow: 1;
display: block;
.title {
display: block;
margin: 0;
padding: 0;
font-size: 27px;
font-weight: 600;
text-align: center;
}
.message {
display: block;
margin: 10px auto;
padding: 0;
color: #777;
font-size: 16px;
font-weight: 400;
text-align: center;
}
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
margin: 10px 0 0 0;
padding: 20px;
width: 100%;
.confirmation-button {
display: inline-block;
margin: 0px 5px;
padding: 10px 20px;
border: none;
border-radius: 6px;
color: #777;
font-size: 14px;
font-weight: 600;
background-color: #eee;
&:hover {
background-color: darken(#eee, 5);
}
&-reject {
}
&-approve {
background-color: #2f96b4;
color: #fff;
&:hover {
background-color: darken(#2f96b4, 5);
}
}
}
}
}
}

@ -1,59 +1,46 @@
import { Component } from '@angular/core';
import { ConfirmationService } from '../../services/confirmation.service';
import { Toaster } from '../../models/toaster';
import { Confirmation, Toaster } from '../../models';
import { LocalizationService } from '@abp/ng.core';
@Component({
selector: 'abp-confirmation',
// tslint:disable-next-line: component-max-inline-declarations
template: `
<p-toast
position="center"
key="abpConfirmation"
(onClose)="close(dismiss)"
[modal]="true"
[baseZIndex]="1000"
styleClass="abp-confirm"
>
<ng-template let-message pTemplate="message">
<i class="fa fa-exclamation-circle abp-confirm-icon"></i>
<div *ngIf="message.summary" class="abp-confirm-summary">
{{ message.summary | abpLocalization: message.titleLocalizationParams }}
</div>
<div class="abp-confirm-body">
{{ message.detail | abpLocalization: message.messageLocalizationParams }}
</div>
<div class="abp-confirm-footer justify-content-center">
<button
*ngIf="!message.hideCancelBtn"
id="cancel"
type="button"
class="btn btn-sm btn-primary"
(click)="close(reject)"
>
{{ message.cancelText || message.cancelCopy || 'AbpIdentity::Cancel' | abpLocalization }}
</button>
<button
*ngIf="!message.hideYesBtn"
id="confirm"
type="button"
class="btn btn-sm btn-primary"
(click)="close(confirm)"
autofocus
>
<span>{{ message.yesText || message.yesCopy || 'AbpIdentity::Yes' | abpLocalization }}</span>
</button>
</div>
</ng-template>
</p-toast>
`,
templateUrl: './confirmation.component.html',
styleUrls: ['./confirmation.component.scss'],
})
export class ConfirmationComponent {
confirm = Toaster.Status.confirm;
reject = Toaster.Status.reject;
dismiss = Toaster.Status.dismiss;
constructor(private confirmationService: ConfirmationService) {}
visible = false;
data: Confirmation.DialogData;
get iconClass(): string {
switch (this.data.severity) {
case 'info':
return 'fa-info-circle';
case 'success':
return 'fa-check-circle';
case 'warning':
return 'fa-exclamation-triangle';
case 'error':
return 'fa-times-circle';
default:
return 'fa-question-circle';
}
}
constructor(
private confirmationService: ConfirmationService,
private localizationService: LocalizationService,
) {
this.confirmationService.confirmation$.subscribe(confirmation => {
this.data = confirmation;
this.visible = !!confirmation;
});
}
close(status: Toaster.Status) {
this.confirmationService.clear(status);

@ -0,0 +1,25 @@
.modal {
&.show {
display: block !important;
}
&-backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
&::-webkit-scrollbar {
width: 7px;
}
&::-webkit-scrollbar-track {
background: #ddd;
}
&::-webkit-scrollbar-thumb {
background: #8a8686;
}
&-dialog {
z-index: 1050;
}
}

@ -24,6 +24,7 @@ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
selector: 'abp-modal',
templateUrl: './modal.component.html',
animations: [fadeAnimation],
styleUrls: ['./modal.component.scss'],
})
export class ModalComponent implements OnDestroy {
@Input()
@ -115,7 +116,8 @@ export class ModalComponent implements OnDestroy {
}
const nodes = getFlatNodes(
((node || this.modalContent.nativeElement).querySelector('#abp-modal-body') as HTMLElement).childNodes,
((node || this.modalContent.nativeElement).querySelector('#abp-modal-body') as HTMLElement)
.childNodes,
);
if (hasNgDirty(nodes)) {
@ -123,7 +125,10 @@ export class ModalComponent implements OnDestroy {
this.isConfirmationOpen = true;
this.confirmationService
.warn('AbpAccount::AreYouSureYouWantToCancelEditingWarningMessage', 'AbpAccount::AreYouSure')
.warn(
'AbpAccount::AreYouSureYouWantToCancelEditingWarningMessage',
'AbpAccount::AreYouSure',
)
.subscribe((status: Toaster.Status) => {
this.isConfirmationOpen = false;
if (status === Toaster.Status.confirm) {
@ -162,7 +167,10 @@ export class ModalComponent implements OnDestroy {
function getFlatNodes(nodes: NodeList): HTMLElement[] {
return Array.from(nodes).reduce(
(acc, val) => [...acc, ...(val.childNodes && val.childNodes.length ? getFlatNodes(val.childNodes) : [val])],
(acc, val) => [
...acc,
...(val.childNodes && val.childNodes.length ? getFlatNodes(val.childNodes) : [val]),
],
[],
);
}

@ -0,0 +1,11 @@
<div
class="toast-container"
[style.top]="top || 'auto'"
[style.right]="right || 'auto'"
[style.bottom]="bottom || 'auto'"
[style.left]="left || 'auto'"
[style.display]="toasts.length ? 'flex' : 'none'"
[@toastInOut]="toasts.length - 1 || 0"
>
<abp-toast [toast]="toast" *ngFor="let toast of toasts; trackBy: trackByFunc"></abp-toast>
</div>

@ -0,0 +1,12 @@
.toast-container {
position: fixed;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
min-width: 350px;
min-height: 80px;
&.new-on-top {
flex-direction: column-reverse;
}
}

@ -0,0 +1,46 @@
import { Component, Input, OnInit } from '@angular/core';
import { Toaster } from '../../models';
import { toastInOut } from '../../animations';
import { ToasterService } from '../../services/toaster.service';
@Component({
selector: 'abp-toast-container',
templateUrl: './toast-container.component.html',
styleUrls: ['./toast-container.component.scss'],
animations: [toastInOut],
})
export class ToastContainerComponent implements OnInit {
toasts = [] as Toaster.Toast[];
@Input()
top: number;
@Input()
right: number;
@Input()
bottom: number;
@Input()
left: number;
@Input()
toastKey: string;
constructor(private toastService: ToasterService) {}
ngOnInit() {
this.toastService.toasts$.subscribe(toasts => {
this.toasts = this.toastKey
? toasts.filter(t => {
return t.options && t.options.containerKey !== this.toastKey;
})
: toasts;
});
}
trackByFunc(index, toast) {
if (!toast) return null;
return toast.options.id;
}
}

@ -0,0 +1,16 @@
<div class="toast" [ngClass]="severityClass" (click)="tap()">
<div class="toast-icon">
<i class="fa icon" [ngClass]="iconClass"></i>
</div>
<div class="toast-content">
<button class="close-button" (click)="close()" *ngIf="toast.options.closable">
<i class="fa fa-times"></i>
</button>
<div class="toast-title">
{{ toast.title | abpLocalization: toast.options?.titleLocalizationParams }}
</div>
<div class="toast-message">
{{ toast.message | abpLocalization: toast.options?.messageLocalizationParams }}
</div>
</div>
</div>

@ -0,0 +1,80 @@
@mixin fillColor($background, $color) {
border: 2px solid $background;
background-color: $background;
color: $color;
box-shadow: 0 0 10px -5px rgba(#000, 0.4);
&:hover {
border: 2px solid darken($background, 5);
background-color: darken($background, 5);
box-shadow: 0 0 15px -5px rgba(#000, 0.4);
}
}
.toast {
display: grid;
grid-template-columns: 50px 1fr;
gap: 10px;
margin: 5px 0;
padding: 10px;
border-radius: 0px;
width: 350px;
user-select: none;
box-shadow: 0 0 10px -5px rgba(#000, 0.4);
z-index: 9999;
@include fillColor(#f0f0f0, #000);
opacity: 1;
&.toast-success {
@include fillColor(#51a351, #fff);
}
&.toast-info {
@include fillColor(#2f96b4, #fff);
}
&.toast-warning {
@include fillColor(#f89406, #fff);
}
&.toast-error {
@include fillColor(#bd362f, #fff);
}
.toast-icon {
display: flex;
align-items: center;
justify-content: center;
.icon {
font-size: 36px;
}
}
.toast-content {
position: relative;
.close-button {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 5px 10px 5px 5px;
width: 25px;
height: 25px;
border: none;
border-radius: 50%;
background: transparent;
&:focus {
outline: none;
}
.close-icon {
width: 16px;
height: 16px;
stroke: #000;
}
}
.toast-title {
margin: 0;
padding: 0;
font-size: 1rem;
font-weight: 600;
}
.toast-message {
}
}
}

@ -1,26 +1,56 @@
import { Component } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { Toaster } from '../../models';
import { ToasterService } from '../../services/toaster.service';
import { LocalizationService } from '@abp/ng.core';
import snq from 'snq';
@Component({
selector: 'abp-toast',
// tslint:disable-next-line: component-max-inline-declarations
template: `
<p-toast position="bottom-right" key="abpToast" styleClass="abp-toast" [baseZIndex]="1000">
<ng-template let-message pTemplate="message">
<span
class="ui-toast-icon pi"
[ngClass]="{
'pi-info-circle': message.severity === 'info',
'pi-exclamation-triangle': message.severity === 'warn',
'pi-times': message.severity === 'error',
'pi-check': message.severity === 'success'
}"
></span>
<div class="ui-toast-message-text-content">
<div class="ui-toast-summary">{{ message.summary | abpLocalization: message.titleLocalizationParams }}</div>
<div class="ui-toast-detail">{{ message.detail | abpLocalization: message.messageLocalizationParams }}</div>
</div>
</ng-template>
</p-toast>
`,
templateUrl: './toast.component.html',
styleUrls: ['./toast.component.scss'],
})
export class ToastComponent {}
export class ToastComponent implements OnInit {
@Input()
toast: Toaster.Toast;
get severityClass(): string {
if (!this.toast || !this.toast.severity) return '';
return `toast-${this.toast.severity}`;
}
get iconClass(): string {
switch (this.toast.severity) {
case 'success':
return 'fa-check-circle';
case 'info':
return 'fa-info-circle';
case 'warning':
return 'fa-exclamation-triangle';
case 'error':
return 'fa-times-circle';
default:
return 'fa-exclamation-circle';
}
}
constructor(
private toastService: ToasterService,
private localizationService: LocalizationService,
) {}
ngOnInit() {
if (snq(() => this.toast.options.sticky)) return;
const timeout = snq(() => this.toast.options.life) || 5000;
setTimeout(() => {
this.close();
}, timeout);
}
close() {
this.toastService.remove(this.toast.options.id);
}
tap() {
if (this.toast.options && this.toast.options.tapToDismiss) this.close();
}
}

@ -41,30 +41,6 @@ export default `
background: #8a8686;
}
.modal.show {
display: block !important;
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
.modal::-webkit-scrollbar {
width: 7px;
}
.modal::-webkit-scrollbar-track {
background: #ddd;
}
.modal::-webkit-scrollbar-thumb {
background: #8a8686;
}
.modal-dialog {
z-index: 1050;
}
.abp-ellipsis-inline {
display: inline-block;
overflow: hidden;
@ -78,112 +54,6 @@ export default `
white-space: nowrap;
}
.abp-toast .ui-toast-message {
box-sizing: border-box;
border: 2px solid transparent;
border-radius: 4px;
color: #1b1d29;
}
.abp-toast .ui-toast-message-content {
padding: 10px;
}
.abp-toast .ui-toast-message-content .ui-toast-icon {
top: 0;
left: 0;
padding: 10px;
}
.abp-toast .ui-toast-summary {
margin: 0;
font-weight: 700;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-error {
border: 2px solid #ba1659;
background-color: #f4f4f7;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-error .ui-toast-message-content .ui-toast-icon {
color: #ba1659;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-warn {
border: 2px solid #ed5d98;
background-color: #f4f4f7;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-warn .ui-toast-message-content .ui-toast-icon {
color: #ed5d98;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-success {
border: 2px solid #1c9174;
background-color: #f4f4f7;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-success .ui-toast-message-content .ui-toast-icon {
color: #1c9174;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-info {
border: 2px solid #fccb31;
background-color: #f4f4f7;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-info .ui-toast-message-content .ui-toast-icon {
color: #fccb31;
}
.abp-confirm .ui-toast-message {
box-sizing: border-box;
padding: 0px;
border:0 none;
border-radius: 4px;
background-color: transparent !important;
font-family: "Poppins", sans-serif;
text-align: center;
}
.abp-confirm .ui-toast-message-content {
padding: 0px;
}
.abp-confirm .abp-confirm-icon {
margin: 32px 50px 5px !important;
color: #f8bb86 !important;
font-size: 52px !important;
}
.abp-confirm .ui-toast-close-icon {
display: none !important;
}
.abp-confirm .abp-confirm-summary {
display: block !important;
margin-bottom: 13px !important;
padding: 13px 16px 0px !important;
font-weight: 600 !important;
font-size: 18px !important;
}
.abp-confirm .abp-confirm-body {
display: inline-block !important;
padding: 0px 10px !important;
}
.abp-confirm .abp-confirm-footer {
display: block;
margin-top: 30px;
padding: 16px;
text-align: right;
}
.abp-confirm .abp-confirm-footer .btn {
margin-left: 10px !important;
}
.ui-widget-overlay {
z-index: 1000;
}

@ -1,11 +1,23 @@
import { Toaster } from './toaster';
import { Config } from '@abp/ng.core';
export namespace Confirmation {
export interface Options extends Toaster.Options {
export interface Options {
id?: any;
closable?: boolean;
messageLocalizationParams?: string[];
titleLocalizationParams?: string[];
hideCancelBtn?: boolean;
hideYesBtn?: boolean;
cancelText?: Config.LocalizationParam;
yesText?: Config.LocalizationParam;
}
export interface DialogData {
message: Config.LocalizationParam;
title?: Config.LocalizationParam;
severity?: Severity;
options?: Partial<Options>;
}
export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error';
}

@ -1,17 +1,27 @@
import { Config } from '@abp/ng.core';
export namespace Toaster {
export interface Options {
id?: any;
closable?: boolean;
export interface ToastOptions {
life?: number;
sticky?: boolean;
data?: any;
closable?: boolean;
tapToDismiss?: boolean;
messageLocalizationParams?: string[];
titleLocalizationParams?: string[];
id: any;
containerKey?: string;
}
export interface Toast {
message: Config.LocalizationParam;
title?: Config.LocalizationParam;
severity?: string;
options?: ToastOptions;
}
export type Severity = 'success' | 'info' | 'warn' | 'error';
export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error';
export const enum Status {
export enum Status {
confirm = 'confirm',
reject = 'reject',
dismiss = 'dismiss',

@ -1,43 +1,73 @@
import { Injectable } from '@angular/core';
import { AbstractToaster } from '../abstracts/toaster';
import { Confirmation } from '../models/confirmation';
import { MessageService } from 'primeng/components/common/messageservice';
import { fromEvent, Observable, Subject } from 'rxjs';
import { fromEvent, Observable, Subject, ReplaySubject } from 'rxjs';
import { takeUntil, debounceTime, filter } from 'rxjs/operators';
import { Toaster } from '../models/toaster';
import { Config } from '@abp/ng.core';
@Injectable({ providedIn: 'root' })
export class ConfirmationService extends AbstractToaster<Confirmation.Options> {
key = 'abpConfirmation';
export class ConfirmationService {
status$: Subject<Toaster.Status>;
confirmation$ = new ReplaySubject<Confirmation.DialogData>(1);
sticky = true;
info(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
return this.show(message, title, 'info', options);
}
destroy$ = new Subject();
success(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
return this.show(message, title, 'success', options);
}
constructor(protected messageService: MessageService) {
super(messageService);
warn(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
return this.show(message, title, 'warning', options);
}
error(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
return this.show(message, title, 'error', options);
}
show(
message: string,
title: string,
severity: Toaster.Severity,
options?: Confirmation.Options,
message: Config.LocalizationParam,
title: Config.LocalizationParam,
severity?: Toaster.Severity,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
this.confirmation$.next({
message,
title: title || 'AbpUi:AreYouSure',
severity: severity || 'neutral',
options,
});
this.status$ = new Subject();
this.listenToEscape();
return super.show(message, title, severity, options);
return this.status$;
}
clear(status?: Toaster.Status) {
super.clear(status);
this.destroy$.next();
this.confirmation$.next();
this.status$.next(status || Toaster.Status.dismiss);
}
listenToEscape() {
fromEvent(document, 'keyup')
.pipe(
takeUntil(this.destroy$),
takeUntil(this.status$),
debounceTime(150),
filter((key: KeyboardEvent) => key && key.key === 'Escape'),
)

@ -1,15 +1,116 @@
import { Injectable } from '@angular/core';
import { AbstractToaster } from '../abstracts/toaster';
import { Message } from 'primeng/components/common/message';
import { MessageService } from 'primeng/components/common/messageservice';
import { Toaster } from '../models';
import { ReplaySubject } from 'rxjs';
import { Config } from '@abp/ng.core';
import snq from 'snq';
@Injectable({ providedIn: 'root' })
export class ToasterService extends AbstractToaster {
constructor(protected messageService: MessageService) {
super(messageService);
@Injectable({
providedIn: 'root',
})
export class ToasterService {
toasts$ = new ReplaySubject<Toaster.Toast[]>(1);
private lastId = -1;
private toasts = [] as Toaster.Toast[];
/**
* Creates an info toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param options Spesific style or structural options for individual toast
*/
info(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
return this.show(message, title, 'info', options);
}
/**
* Creates a success toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param options Spesific style or structural options for individual toast
*/
success(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
return this.show(message, title, 'success', options);
}
/**
* Creates a warning toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param options Spesific style or structural options for individual toast
*/
warn(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
return this.show(message, title, 'warning', options);
}
/**
* Creates an error toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param options Spesific style or structural options for individual toast
*/
error(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
return this.show(message, title, 'error', options);
}
/**
* Creates a toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param severity Sets color of the toast. "success", "warning" etc.
* @param options Spesific style or structural options for individual toast
*/
show(
message: Config.LocalizationParam,
title: Config.LocalizationParam = null,
severity: Toaster.Severity = 'neutral',
options = {} as Partial<Toaster.ToastOptions>,
) {
const id = ++this.lastId;
this.toasts.push({
message,
title,
severity,
options: { closable: true, id, ...options },
});
this.toasts$.next(this.toasts);
return id;
}
/**
* Removes the toast with given id.
* @param id ID of the toast to be removed.
*/
remove(id: number) {
this.toasts = this.toasts.filter(toast => snq(() => toast.options.id) !== id);
this.toasts$.next(this.toasts);
}
addAll(messages: Message[]): void {
this.messageService.addAll(messages.map(message => ({ key: this.key, ...message })));
/**
* Removes all open toasts at once.
*/
clear(key?: string) {
this.toasts = !key
? []
: this.toasts.filter(toast => snq(() => toast.options.containerKey) !== key);
this.toasts$.next(this.toasts);
}
}

@ -33,45 +33,44 @@ describe('ConfirmationService', () => {
service = spectator.get(ConfirmationService);
});
it('should display a confirmation popup', () => {
test('should display a confirmation popup', () => {
service.info('test', 'title');
spectator.detectChanges();
expect(spectator.query('p-toast')).toBeTruthy();
expect(spectator.query('p-toastitem')).toBeTruthy();
expect(spectator.query('div.abp-confirm-summary')).toHaveText('title');
expect(spectator.query('div.abp-confirm-body')).toHaveText('test');
expect(spectator.query('div.confirmation .title')).toHaveText('title');
expect(spectator.query('div.confirmation .message')).toHaveText('test');
});
it('should close with ESC key', done => {
service.info('test', 'title');
spectator.detectChanges();
test('should close with ESC key', done => {
service.info('test', 'title').subscribe(() => {
setTimeout(() => {
spectator.detectComponentChanges();
expect(spectator.query('div.confirmation')).toBeFalsy();
done();
}, 0);
});
expect(spectator.query('p-toastitem')).toBeTruthy();
spectator.detectChanges();
expect(spectator.query('div.confirmation')).toBeTruthy();
spectator.dispatchKeyboardEvent('div.confirmation', 'keyup', 'Escape');
});
spectator.dispatchKeyboardEvent('abp-confirmation', 'keyup', 'Escape');
service.destroy$.subscribe(() => {
// expect(spectator.query('p-toastitem')).toBeFalsy();
test('should close when click cancel button', done => {
service.info('test', 'title', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(() => {
spectator.detectComponentChanges();
expect(spectator.query('p-toastitem')).toBeFalsy();
done();
setTimeout(() => {
expect(spectator.query('div.confirmation')).toBeFalsy();
done();
}, 0);
});
});
it('should close when click cancel button', done => {
service.info('test', 'title', { yesText: 'Sure', cancelText: 'Exit' });
spectator.detectChanges();
expect(spectator.query('p-toastitem')).toBeTruthy();
expect(spectator.query('div.confirmation')).toBeTruthy();
expect(spectator.query('button#cancel')).toHaveText('Exit');
expect(spectator.query('button#confirm')).toHaveText('Sure');
service.status$.subscribe(() => {
spectator.detectComponentChanges();
expect(spectator.query('p-toastitem')).toBeFalsy();
done();
});
spectator.click('button#cancel');
});
});

@ -73,10 +73,10 @@ describe('ErrorHandler', () => {
it('should display the confirmation when not found error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
spectator.detectChanges();
expect(spectator.query('.abp-confirm-summary')).toHaveText(
expect(spectator.query('.confirmation .title')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError404.title,
);
expect(spectator.query('.abp-confirm-body')).toHaveText(
expect(spectator.query('.confirmation .message')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError404.details,
);
});
@ -84,10 +84,10 @@ describe('ErrorHandler', () => {
it('should display the confirmation when default error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 412 })));
spectator.detectChanges();
expect(spectator.query('.abp-confirm-summary')).toHaveText(
expect(spectator.query('.confirmation .title')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.title,
);
expect(spectator.query('.abp-confirm-body')).toHaveText(
expect(spectator.query('.confirmation .message')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.details,
);
});
@ -128,8 +128,8 @@ describe('ErrorHandler', () => {
);
spectator.detectChanges();
expect(spectator.query('.abp-confirm-summary')).toHaveText('test message');
expect(spectator.query('.abp-confirm-body')).toHaveText('test detail');
expect(spectator.query('.title')).toHaveText('test message');
expect(spectator.query('.confirmation .message')).toHaveText('test detail');
});
});

@ -5,14 +5,18 @@ import { MessageService } from 'primeng/components/common/messageservice';
import { ToastModule } from 'primeng/toast';
import { timer } from 'rxjs';
import { ButtonComponent, ConfirmationComponent, ModalComponent } from '../components';
import { RouterTestingModule } from '@angular/router/testing';
describe('ModalComponent', () => {
let spectator: SpectatorHost<ModalComponent, { visible: boolean; busy: boolean; ngDirty: boolean }>;
let spectator: SpectatorHost<
ModalComponent,
{ visible: boolean; busy: boolean; ngDirty: boolean }
>;
let appearFn;
let disappearFn;
const createHost = createHostFactory({
component: ModalComponent,
imports: [ToastModule],
imports: [ToastModule, RouterTestingModule],
declarations: [ConfirmationComponent, LocalizationPipe, ButtonComponent],
providers: [MessageService],
mocks: [Store],
@ -82,7 +86,7 @@ describe('ModalComponent', () => {
spectator.click('#abp-modal-close-button');
expect(disappearFn).not.toHaveBeenCalled();
expect(spectator.query('p-toast')).toBeTruthy();
expect(spectator.query('div.confirmation')).toBeTruthy();
spectator.click('button#cancel');
expect(spectator.query('div.modal')).toBeTruthy();

@ -11,7 +11,7 @@ import { OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'abp-dummy',
template: `
<abp-toast></abp-toast>
<abp-toast-container></abp-toast-container>
`,
})
class DummyComponent {
@ -33,58 +33,57 @@ describe('ToasterService', () => {
service = spectator.get(ToasterService);
});
it('should display an error toast', () => {
test('should display an error toast', () => {
service.error('test', 'title');
spectator.detectChanges();
expect(spectator.query('p-toast')).toBeTruthy();
expect(spectator.query('p-toastitem')).toBeTruthy();
expect(spectator.query('span.ui-toast-icon')).toHaveClass('pi-times');
expect(spectator.query('div.ui-toast-summary')).toHaveText('title');
expect(spectator.query('div.ui-toast-detail')).toHaveText('test');
expect(spectator.query('div.toast')).toBeTruthy();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle');
expect(spectator.query('div.toast-title')).toHaveText('title');
expect(spectator.query('div.toast-message')).toHaveText('test');
});
it('should display a warning toast', () => {
test('should display a warning toast', () => {
service.warn('test', 'title');
spectator.detectChanges();
expect(spectator.query('span.ui-toast-icon')).toHaveClass('pi-exclamation-triangle');
expect(spectator.query('.toast-icon i')).toHaveClass('fa-exclamation-triangle');
});
it('should display a success toast', () => {
test('should display a success toast', () => {
service.success('test', 'title');
spectator.detectChanges();
expect(spectator.query('span.ui-toast-icon')).toHaveClass('pi-check');
expect(spectator.query('.toast-icon i')).toHaveClass('fa-check-circle');
});
it('should display an info toast', () => {
test('should display an info toast', () => {
service.info('test', 'title');
spectator.detectChanges();
expect(spectator.query('span.ui-toast-icon')).toHaveClass('pi-info-circle');
expect(spectator.query('.toast-icon i')).toHaveClass('fa-info-circle');
});
it('should display multiple toasts', () => {
service.addAll([
{ summary: 'summary1', detail: 'detail1' },
{ summary: 'summary2', detail: 'detail2' },
]);
test('should display multiple toasts', () => {
service.info('detail1', 'summary1');
service.info('detail2', 'summary2');
spectator.detectChanges();
expect(
spectator.queryAll('div.ui-toast-summary').map(node => node.textContent.trim()),
).toEqual(['summary1', 'summary2']);
expect(spectator.queryAll('div.ui-toast-detail').map(node => node.textContent.trim())).toEqual([
expect(spectator.queryAll('div.toast-title').map(node => node.textContent.trim())).toEqual([
'summary1',
'summary2',
]);
expect(spectator.queryAll('div.toast-message').map(node => node.textContent.trim())).toEqual([
'detail1',
'detail2',
]);
});
it('should remove the opened toast', () => {
test('should remove the opened toasts', () => {
service.info('test', 'title');
spectator.detectChanges();
expect(spectator.query('p-toastitem')).toBeTruthy();
expect(spectator.query('div.toast')).toBeTruthy();
service.clear();
spectator.detectChanges();
expect(spectator.query('p-toastitem')).toBeFalsy();
expect(spectator.query('p-div.toast')).toBeFalsy();
});
});

@ -14,6 +14,7 @@ import { LoaderBarComponent } from './components/loader-bar/loader-bar.component
import { ModalComponent } from './components/modal/modal.component';
import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component';
import { TableEmptyMessageComponent } from './components/table-empty-message/table-empty-message.component';
import { ToastContainerComponent } from './components/toast-container/toast-container.component';
import { TableComponent } from './components/table/table.component';
import { ToastComponent } from './components/toast/toast.component';
import styles from './constants/styles';
@ -53,6 +54,7 @@ export function appendScript(injector: Injector) {
TableComponent,
TableEmptyMessageComponent,
ToastComponent,
ToastContainerComponent,
SortOrderIconComponent,
LoadingDirective,
TableSortDirective,
@ -69,6 +71,7 @@ export function appendScript(injector: Injector) {
TableComponent,
TableEmptyMessageComponent,
ToastComponent,
ToastContainerComponent,
SortOrderIconComponent,
LoadingDirective,
TableSortDirective,

Loading…
Cancel
Save