mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'remotes/origin/master' into authentication
# Conflicts: # src/app/app.effects.ts # src/app/app.module.ts # src/app/app.reducer.ts # src/app/core/core.module.ts # src/app/shared/shared.module.ts
This commit is contained in:
@@ -22,6 +22,17 @@ module.exports = {
|
|||||||
// msToLive: 1000, // 15 minutes
|
// msToLive: 1000, // 15 minutes
|
||||||
control: 'max-age=60' // revalidate browser
|
control: 'max-age=60' // revalidate browser
|
||||||
},
|
},
|
||||||
|
// Notifications
|
||||||
|
notifications: {
|
||||||
|
rtl: false,
|
||||||
|
position: ['top', 'right'],
|
||||||
|
maxStack: 8,
|
||||||
|
// NOTE: after how many seconds notification is closed automatically. If set to zero notifications are not closed automatically
|
||||||
|
timeOut: 5000, // 5 second
|
||||||
|
clickToClose: true,
|
||||||
|
// NOTE: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale'
|
||||||
|
animate: 'scale'
|
||||||
|
},
|
||||||
// Angular Universal settings
|
// Angular Universal settings
|
||||||
universal: {
|
universal: {
|
||||||
preboot: true,
|
preboot: true,
|
||||||
|
@@ -16,7 +16,7 @@ import { TopLevelCommunityListComponent } from './top-level-community-list/top-l
|
|||||||
declarations: [
|
declarations: [
|
||||||
HomePageComponent,
|
HomePageComponent,
|
||||||
TopLevelCommunityListComponent,
|
TopLevelCommunityListComponent,
|
||||||
HomeNewsComponent
|
HomeNewsComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class HomePageModule {
|
export class HomePageModule {
|
||||||
|
@@ -2,6 +2,10 @@
|
|||||||
<div class="inner-wrapper">
|
<div class="inner-wrapper">
|
||||||
<ds-header></ds-header>
|
<ds-header></ds-header>
|
||||||
|
|
||||||
|
<ds-notifications-board
|
||||||
|
[options]="config.notifications">
|
||||||
|
</ds-notifications-board>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
@@ -9,3 +13,5 @@
|
|||||||
<ds-footer></ds-footer>
|
<ds-footer></ds-footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
import { HeaderEffects } from './header/header.effects';
|
import { HeaderEffects } from './header/header.effects';
|
||||||
import { StoreEffects } from './store.effects';
|
import { StoreEffects } from './store.effects';
|
||||||
import { AuthEffects } from './core/auth/auth.effects';
|
import { NotificationsEffects } from './shared/notifications/notifications.effects';
|
||||||
|
|
||||||
export const appEffects = [
|
export const appEffects = [
|
||||||
StoreEffects,
|
StoreEffects,
|
||||||
HeaderEffects
|
HeaderEffects,
|
||||||
|
NotificationsEffects
|
||||||
];
|
];
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowserTransferStateModule } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
@@ -29,6 +28,8 @@ import { HeaderComponent } from './header/header.component';
|
|||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
|
|
||||||
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
||||||
|
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||||
|
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||||
import { SharedModule } from './shared/shared.module';
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
@@ -87,7 +88,9 @@ if (!ENV_CONFIG.production) {
|
|||||||
AppComponent,
|
AppComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
PageNotFoundComponent
|
PageNotFoundComponent,
|
||||||
|
NotificationComponent,
|
||||||
|
NotificationsBoardComponent
|
||||||
],
|
],
|
||||||
exports: [AppComponent]
|
exports: [AppComponent]
|
||||||
})
|
})
|
||||||
|
@@ -11,12 +11,14 @@ import {
|
|||||||
filterReducer,
|
filterReducer,
|
||||||
SearchFiltersState
|
SearchFiltersState
|
||||||
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
||||||
|
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
|
||||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: fromRouter.RouterReducerState;
|
router: fromRouter.RouterReducerState;
|
||||||
hostWindow: HostWindowState;
|
hostWindow: HostWindowState;
|
||||||
header: HeaderState;
|
header: HeaderState;
|
||||||
|
notifications: NotificationsState;
|
||||||
searchSidebar: SearchSidebarState;
|
searchSidebar: SearchSidebarState;
|
||||||
searchFilter: SearchFiltersState;
|
searchFilter: SearchFiltersState;
|
||||||
truncatable: TruncatablesState;
|
truncatable: TruncatablesState;
|
||||||
@@ -26,6 +28,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
router: fromRouter.routerReducer,
|
router: fromRouter.routerReducer,
|
||||||
hostWindow: hostWindowReducer,
|
hostWindow: hostWindowReducer,
|
||||||
header: headerReducer,
|
header: headerReducer,
|
||||||
|
notifications: notificationsReducer,
|
||||||
searchSidebar: sidebarReducer,
|
searchSidebar: sidebarReducer,
|
||||||
searchFilter: filterReducer,
|
searchFilter: filterReducer,
|
||||||
truncatable: truncatableReducer
|
truncatable: truncatableReducer
|
||||||
|
@@ -50,6 +50,7 @@ import { HALEndpointService } from './shared/hal-endpoint.service';
|
|||||||
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
||||||
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
||||||
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
|
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
|
||||||
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -99,13 +100,14 @@ const PROVIDERS = [
|
|||||||
SubmissionFormsConfigService,
|
SubmissionFormsConfigService,
|
||||||
SubmissionSectionsConfigService,
|
SubmissionSectionsConfigService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
useClass: AuthInterceptor,
|
useClass: AuthInterceptor,
|
||||||
multi: true
|
multi: true
|
||||||
}
|
}
|
||||||
|
NotificationsService,
|
||||||
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -1,14 +1,24 @@
|
|||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
|
export const fadeInState = state('fadeIn', style({opacity: 1}));
|
||||||
|
export const fadeInEnter = transition('* => fadeIn', [
|
||||||
|
style({ opacity: 0 }),
|
||||||
|
animate(300, style({ opacity: 1 }))
|
||||||
|
]);
|
||||||
const fadeEnter = transition(':enter', [
|
const fadeEnter = transition(':enter', [
|
||||||
style({ opacity: 0 }),
|
style({ opacity: 0 }),
|
||||||
animate(300, style({ opacity: 1 }))
|
animate(300, style({ opacity: 1 }))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fadeLeave = transition(':leave', [
|
export const fadeOutState = state('fadeOut', style({opacity: 0}));
|
||||||
|
export const fadeOutLeave = transition('fadeIn => fadeOut', [
|
||||||
style({ opacity: 1 }),
|
style({ opacity: 1 }),
|
||||||
animate(400, style({ opacity: 0 }))
|
animate(400, style({ opacity: 0 }))
|
||||||
]);
|
]);
|
||||||
|
const fadeLeave = transition(':leave', [
|
||||||
|
style({ opacity: 0 }),
|
||||||
|
animate(300, style({ opacity: 1 }))
|
||||||
|
]);
|
||||||
|
|
||||||
export const fadeIn = trigger('fadeIn', [
|
export const fadeIn = trigger('fadeIn', [
|
||||||
fadeEnter
|
fadeEnter
|
||||||
|
26
src/app/shared/animations/fromBottom.ts
Normal file
26
src/app/shared/animations/fromBottom.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
|
export const fromBottomInState = state('fromBottomIn', style({opacity: 1, transform: 'translateY(0)'}));
|
||||||
|
export const fromBottomEnter = transition('* => fromBottomIn', [
|
||||||
|
style({opacity: 0, transform: 'translateY(5%)'}),
|
||||||
|
animate('400ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromBottomOutState = state('fromBottomOut', style({opacity: 0, transform: 'translateY(-5%)'}));
|
||||||
|
export const fromBottomLeave = transition('fromBottomIn => fromBottomOut', [
|
||||||
|
style({opacity: 1, transform: 'translateY(0)'}),
|
||||||
|
animate('300ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromBottomIn = trigger('fromBottomIn', [
|
||||||
|
fromBottomEnter
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromBottomOut = trigger('fromBottomOut', [
|
||||||
|
fromBottomLeave
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromBottomInOut = trigger('fromBottomInOut', [
|
||||||
|
fromBottomEnter,
|
||||||
|
fromBottomLeave
|
||||||
|
]);
|
26
src/app/shared/animations/fromLeft.ts
Normal file
26
src/app/shared/animations/fromLeft.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
|
export const fromLeftInState = state('fromLeftIn', style({opacity: 1, transform: 'translateX(0)'}));
|
||||||
|
export const fromLeftEnter = transition('* => fromLeftIn', [
|
||||||
|
style({opacity: 0, transform: 'translateX(-5%)'}),
|
||||||
|
animate('400ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromLeftOutState = state('fromLeftOut', style({opacity: 0, transform: 'translateX(5%)'}));
|
||||||
|
export const fromLeftLeave = transition('fromLeftIn => fromLeftOut', [
|
||||||
|
style({opacity: 1, transform: 'translateX(0)'}),
|
||||||
|
animate('300ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromLeftIn = trigger('fromLeftIn', [
|
||||||
|
fromLeftEnter
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromLeftOut = trigger('fromLeftOut', [
|
||||||
|
fromLeftLeave
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromLeftInOut = trigger('fromLeftInOut', [
|
||||||
|
fromLeftEnter,
|
||||||
|
fromLeftLeave
|
||||||
|
]);
|
26
src/app/shared/animations/fromRight.ts
Normal file
26
src/app/shared/animations/fromRight.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
|
export const fromRightInState = state('fromRightIn', style({opacity: 1, transform: 'translateX(0)'}));
|
||||||
|
export const fromRightEnter = transition('* => fromRightIn', [
|
||||||
|
style({opacity: 0, transform: 'translateX(5%)'}),
|
||||||
|
animate('400ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromRightOutState = state('fromRightOut', style({opacity: 0, transform: 'translateX(-5%)'}));
|
||||||
|
export const fromRightLeave = transition('fromRightIn => fromRightOut', [
|
||||||
|
style({opacity: 1, transform: 'translateX(0)'}),
|
||||||
|
animate('300ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromRightIn = trigger('fromRightIn', [
|
||||||
|
fromRightEnter
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromRightOut = trigger('fromRightOut', [
|
||||||
|
fromRightLeave
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromRightInOut = trigger('fromRightInOut', [
|
||||||
|
fromRightEnter,
|
||||||
|
fromRightLeave
|
||||||
|
]);
|
26
src/app/shared/animations/fromTop.ts
Normal file
26
src/app/shared/animations/fromTop.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
|
export const fromTopInState = state('fromTopIn', style({opacity: 1, transform: 'translateY(0)'}));
|
||||||
|
export const fromTopEnter = transition('* => fromTopIn', [
|
||||||
|
style({opacity: 0, transform: 'translateY(-5%)'}),
|
||||||
|
animate('400ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromTopOutState = state('fromTopOut', style({opacity: 0, transform: 'translateY(5%)'}));
|
||||||
|
export const fromTopLeave = transition('fromTopIn => fromTopOut', [
|
||||||
|
style({opacity: 1, transform: 'translateY(0)'}),
|
||||||
|
animate('300ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromTopIn = trigger('fromTopIn', [
|
||||||
|
fromTopEnter
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromTopOut = trigger('fromTopOut', [
|
||||||
|
fromTopLeave
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fromTopInOut = trigger('fromTopInOut', [
|
||||||
|
fromTopEnter,
|
||||||
|
fromTopLeave
|
||||||
|
]);
|
26
src/app/shared/animations/rotate.ts
Normal file
26
src/app/shared/animations/rotate.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
|
export const rotateInState = state('rotateIn', style({opacity: 1, transform: 'rotate(0deg)'}));
|
||||||
|
export const rotateEnter = transition('* => rotateIn', [
|
||||||
|
style({opacity: 0, transform: 'rotate(5deg)'}),
|
||||||
|
animate('400ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const rotateOutState = state('rotateOut', style({opacity: 0, transform: 'rotate(5deg)'}));
|
||||||
|
export const rotateLeave = transition('rotateIn => rotateOut', [
|
||||||
|
style({opacity: 1, transform: 'rotate(0deg)'}),
|
||||||
|
animate('400ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const rotateIn = trigger('rotateIn', [
|
||||||
|
rotateEnter
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const rotateOut = trigger('rotateOut', [
|
||||||
|
rotateLeave
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const rotateInOut = trigger('rotateInOut', [
|
||||||
|
rotateEnter,
|
||||||
|
rotateLeave
|
||||||
|
]);
|
26
src/app/shared/animations/scale.ts
Normal file
26
src/app/shared/animations/scale.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
|
export const scaleInState = state('scaleIn', style({opacity: 1, transform: 'scale(1)'}));
|
||||||
|
export const scaleEnter = transition('* => scaleIn', [
|
||||||
|
style({opacity: 0, transform: 'scale(0)'}),
|
||||||
|
animate('400ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const scaleOutState = state('scaleOut', style({opacity: 0, transform: 'scale(0)'}));
|
||||||
|
export const scaleLeave = transition('scaleIn => scaleOut', [
|
||||||
|
style({opacity: 1, transform: 'scale(1)'}),
|
||||||
|
animate('400ms ease-in-out')
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const scaleIn = trigger('scaleIn', [
|
||||||
|
scaleEnter
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const scaleOut = trigger('scaleOut', [
|
||||||
|
scaleLeave
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const scaleInOut = trigger('scaleInOut', [
|
||||||
|
scaleEnter,
|
||||||
|
scaleLeave
|
||||||
|
]);
|
23
src/app/shared/mocks/mock-admin-guard.service.ts
Normal file
23
src/app/shared/mocks/mock-admin-guard.service.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router';
|
||||||
|
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MockAdminGuard implements CanActivate, CanActivateChild {
|
||||||
|
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
|
||||||
|
// if being run in browser, enforce 'isAdmin' requirement
|
||||||
|
if (typeof window === 'object' && hasValue(window.localStorage)) {
|
||||||
|
if (window.localStorage.getItem('isAdmin') === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
|
||||||
|
return this.canActivate(route, state);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
// 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale'
|
||||||
|
|
||||||
|
export enum NotificationAnimationsType {
|
||||||
|
Fade = 'fade',
|
||||||
|
FromTop = 'fromTop',
|
||||||
|
FromRight = 'fromRight',
|
||||||
|
FromBottom = 'fromBottom',
|
||||||
|
FromLeft = 'fromLeft',
|
||||||
|
Rotate = 'rotate',
|
||||||
|
Scale = 'scale'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NotificationAnimationsStatus {
|
||||||
|
In = 'In',
|
||||||
|
Out = 'Out'
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
import { NotificationAnimationsType } from './notification-animations-type';
|
||||||
|
|
||||||
|
export interface INotificationOptions {
|
||||||
|
timeOut: number;
|
||||||
|
clickToClose: boolean;
|
||||||
|
animate: NotificationAnimationsType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationOptions implements INotificationOptions {
|
||||||
|
public timeOut: number;
|
||||||
|
public clickToClose: boolean;
|
||||||
|
public animate: any;
|
||||||
|
|
||||||
|
constructor(timeOut = 5000,
|
||||||
|
clickToClose = true,
|
||||||
|
animate = NotificationAnimationsType.Scale) {
|
||||||
|
|
||||||
|
this.timeOut = timeOut;
|
||||||
|
this.clickToClose = clickToClose;
|
||||||
|
this.animate = animate;
|
||||||
|
}
|
||||||
|
}
|
7
src/app/shared/notifications/models/notification-type.ts
Normal file
7
src/app/shared/notifications/models/notification-type.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export enum NotificationType {
|
||||||
|
Success = 'alert-success',
|
||||||
|
Error = 'alert-danger',
|
||||||
|
Info = 'alert-info',
|
||||||
|
Warning = 'alert-warning',
|
||||||
|
// Bare = 'bare'
|
||||||
|
}
|
38
src/app/shared/notifications/models/notification.model.ts
Normal file
38
src/app/shared/notifications/models/notification.model.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { INotificationOptions, NotificationOptions } from './notification-options.model';
|
||||||
|
import { NotificationType } from './notification-type';
|
||||||
|
import { isEmpty } from '../../empty.util';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
export interface INotification {
|
||||||
|
id: string;
|
||||||
|
type: NotificationType;
|
||||||
|
title?: Observable<string> | string;
|
||||||
|
content?: Observable<string> | string;
|
||||||
|
options?: INotificationOptions;
|
||||||
|
html?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Notification implements INotification {
|
||||||
|
public id: string;
|
||||||
|
public type: NotificationType;
|
||||||
|
public title: Observable<string> | string;
|
||||||
|
public content: Observable<string> | string;
|
||||||
|
public options: INotificationOptions;
|
||||||
|
public html: boolean;
|
||||||
|
|
||||||
|
constructor(id: string,
|
||||||
|
type: NotificationType,
|
||||||
|
title?: Observable<string> | string,
|
||||||
|
content?: Observable<string> | string,
|
||||||
|
options?: NotificationOptions,
|
||||||
|
html?: boolean) {
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.type = type;
|
||||||
|
this.title = title;
|
||||||
|
this.content = content;
|
||||||
|
this.options = isEmpty(options) ? new NotificationOptions() : options;
|
||||||
|
this.html = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,58 @@
|
|||||||
|
<div class="alert {{notification.type}} alert-dismissible p-3" role="alert"
|
||||||
|
[@enterLeave]="animate">
|
||||||
|
|
||||||
|
<div class="notification-progress-loader position-absolute w-100" *ngIf="showProgressBar">
|
||||||
|
<span [ngStyle]="{'width': progressWidth + '%'}" class="h-100 float-left"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button *ngIf="notification.options.clickToClose"
|
||||||
|
(click)="remove()"
|
||||||
|
type="button" class="close pt-0 pr-1 pl-0 pb-0" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div class="d-flex flex-column justify-content-center align-items-center">
|
||||||
|
<div class="notification-icon d-flex justify-content-center"><i
|
||||||
|
[ngClass]="{'fa fa-2x': true,
|
||||||
|
'fa-check': notification.type == 'alert-success',
|
||||||
|
'fa-times-circle': notification.type == 'alert-danger',
|
||||||
|
'fa-exclamation-triangle': notification.type == 'alert-warning',
|
||||||
|
'fa-info': notification.type == 'alert-info'
|
||||||
|
}"></i></div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column justify-content-center align-content-stretch">
|
||||||
|
<div class="p-2 mr-3" *ngIf="title">
|
||||||
|
<strong>
|
||||||
|
<div class="notification-title pl-1" *ngIf="titleIsTemplate; else regularTitle">
|
||||||
|
<ng-container *ngTemplateOutlet="title"></ng-container>
|
||||||
|
</div>
|
||||||
|
<ng-template #regularTitle>
|
||||||
|
<div class="notification-title pl-1">{{(title | async)}}</div>
|
||||||
|
</ng-template>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2 mr-3" *ngIf="content && !html">
|
||||||
|
<div class="notification-content pl-1" *ngIf="contentIsTemplate; else regularContent">
|
||||||
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
|
</div>
|
||||||
|
<ng-template #regularContent>
|
||||||
|
<div class="notification-content pl-1">{{(content | async)}}</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2 mr-3" *ngIf="content && html">
|
||||||
|
<div class="notification-html pl-1" *ngIf="contentIsTemplate; else regularHtml">
|
||||||
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
|
</div>
|
||||||
|
<ng-template #regularHtml>
|
||||||
|
<div class="notification-html pl-1" [innerHTML]="content"></div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1,28 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
.close {
|
||||||
|
outline: none !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
min-width: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-progress-loader {
|
||||||
|
top: -1px;
|
||||||
|
left: 0;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success .notification-progress-loader span {
|
||||||
|
background: darken(adjust-hue(map-get($theme-colors, success), -10), 10%);
|
||||||
|
}
|
||||||
|
.alert-danger .notification-progress-loader span {
|
||||||
|
background: darken(adjust-hue(map-get($theme-colors, danger), -10), 10%);
|
||||||
|
}
|
||||||
|
.alert-info .notification-progress-loader span {
|
||||||
|
background: darken(adjust-hue(map-get($theme-colors, info), -10), 10%);
|
||||||
|
}
|
||||||
|
.alert-warning .notification-progress-loader span {
|
||||||
|
background: darken(adjust-hue(map-get($theme-colors, warning), -10), 10%);
|
||||||
|
}
|
@@ -0,0 +1,119 @@
|
|||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
|
import { ChangeDetectorRef, DebugElement } from '@angular/core';
|
||||||
|
|
||||||
|
import { NotificationComponent } from './notification.component';
|
||||||
|
import { NotificationsService } from '../notifications.service';
|
||||||
|
import { NotificationType } from '../models/notification-type';
|
||||||
|
import { notificationsReducer } from '../notifications.reducers';
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { NotificationOptions } from '../models/notification-options.model';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { NotificationsServiceStub } from '../../testing/notifications-service-stub';
|
||||||
|
import { AppState } from '../../../app.reducer';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { SearchPageComponent } from '../../../+search-page/search-page.component';
|
||||||
|
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
|
||||||
|
import { GlobalConfig } from '../../../../config/global-config.interface';
|
||||||
|
import { Notification } from '../models/notification.model';
|
||||||
|
|
||||||
|
describe('NotificationComponent', () => {
|
||||||
|
|
||||||
|
let comp: NotificationComponent;
|
||||||
|
let fixture: ComponentFixture<NotificationComponent>;
|
||||||
|
let deTitle: DebugElement;
|
||||||
|
let elTitle: HTMLElement;
|
||||||
|
let deContent: DebugElement;
|
||||||
|
let elContent: HTMLElement;
|
||||||
|
let elType: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
const store: Store<Notification> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
notifications: []
|
||||||
|
});
|
||||||
|
const envConfig: GlobalConfig = {
|
||||||
|
notifications: {
|
||||||
|
rtl: false,
|
||||||
|
position: ['top', 'right'],
|
||||||
|
maxStack: 8,
|
||||||
|
timeOut: 5000,
|
||||||
|
clickToClose: true,
|
||||||
|
animate: 'scale'
|
||||||
|
}as INotificationBoardOptions,
|
||||||
|
} as any;
|
||||||
|
const service = new NotificationsService(envConfig, store);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
StoreModule.forRoot({notificationsReducer})],
|
||||||
|
declarations: [NotificationComponent], // declare the test component
|
||||||
|
providers: [
|
||||||
|
{ provide: NotificationsService, useValue: service },
|
||||||
|
ChangeDetectorRef]
|
||||||
|
}).compileComponents(); // compile template and css
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NotificationComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.notification = {
|
||||||
|
id: '1',
|
||||||
|
type: NotificationType.Info,
|
||||||
|
title: 'Notif. title',
|
||||||
|
content: 'Notif. content',
|
||||||
|
options: new NotificationOptions()
|
||||||
|
};
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
deTitle = fixture.debugElement.query(By.css('.notification-title'));
|
||||||
|
elTitle = deTitle.nativeElement;
|
||||||
|
deContent = fixture.debugElement.query(By.css('.notification-content'));
|
||||||
|
elContent = deContent.nativeElement;
|
||||||
|
elType = fixture.debugElement.query(By.css('.notification-icon')).nativeElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create component', () => {
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set Title', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(elTitle.textContent).toBe(comp.notification.title as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set Content', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(elContent.textContent).toBe(comp.notification.content as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set type', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(elType).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shuld has html content', () => {
|
||||||
|
fixture = TestBed.createComponent(NotificationComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
const htmlContent = `<a class="btn btn-link p-0 m-0 pb-1" href="/test"><strong>test</strong></a>`
|
||||||
|
comp.notification = {
|
||||||
|
id: '1',
|
||||||
|
type: NotificationType.Info,
|
||||||
|
title: 'Notif. title',
|
||||||
|
content: htmlContent,
|
||||||
|
options: new NotificationOptions(),
|
||||||
|
html: true
|
||||||
|
};
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
deContent = fixture.debugElement.query(By.css('.notification-html'));
|
||||||
|
elContent = deContent.nativeElement;
|
||||||
|
expect(elContent.innerHTML).toEqual(htmlContent);
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
NgZone,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
TemplateRef,
|
||||||
|
ViewEncapsulation
|
||||||
|
} from '@angular/core';
|
||||||
|
import { trigger } from '@angular/animations';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
import { NotificationsService } from '../notifications.service';
|
||||||
|
import { INotification } from '../models/notification.model';
|
||||||
|
import { scaleEnter, scaleInState, scaleLeave, scaleOutState } from '../../animations/scale';
|
||||||
|
import { rotateEnter, rotateInState, rotateLeave, rotateOutState } from '../../animations/rotate';
|
||||||
|
import { fromBottomEnter, fromBottomInState, fromBottomLeave, fromBottomOutState } from '../../animations/fromBottom';
|
||||||
|
import { fromRightEnter, fromRightInState, fromRightLeave, fromRightOutState } from '../../animations/fromRight';
|
||||||
|
import { fromLeftEnter, fromLeftInState, fromLeftLeave, fromLeftOutState } from '../../animations/fromLeft';
|
||||||
|
import { fromTopEnter, fromTopInState, fromTopLeave, fromTopOutState } from '../../animations/fromTop';
|
||||||
|
import { fadeInEnter, fadeInState, fadeOutLeave, fadeOutState } from '../../animations/fade';
|
||||||
|
import { NotificationAnimationsStatus } from '../models/notification-animations-type';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { isNotEmpty } from '../../empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-notification',
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
animations: [
|
||||||
|
trigger('enterLeave', [
|
||||||
|
fadeInEnter, fadeInState, fadeOutLeave, fadeOutState,
|
||||||
|
fromBottomEnter, fromBottomInState, fromBottomLeave, fromBottomOutState,
|
||||||
|
fromRightEnter, fromRightInState, fromRightLeave, fromRightOutState,
|
||||||
|
fromLeftEnter, fromLeftInState, fromLeftLeave, fromLeftOutState,
|
||||||
|
fromTopEnter, fromTopInState, fromTopLeave, fromTopOutState,
|
||||||
|
rotateInState, rotateEnter, rotateOutState, rotateLeave,
|
||||||
|
scaleInState, scaleEnter, scaleOutState, scaleLeave
|
||||||
|
])
|
||||||
|
],
|
||||||
|
templateUrl: './notification.component.html',
|
||||||
|
styleUrls: ['./notification.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
|
||||||
|
export class NotificationComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@Input() public notification: INotification;
|
||||||
|
|
||||||
|
// Progress bar variables
|
||||||
|
public title: Observable<string>;
|
||||||
|
public content: Observable<string>;
|
||||||
|
public html: any;
|
||||||
|
public showProgressBar = false;
|
||||||
|
public titleIsTemplate = false;
|
||||||
|
public contentIsTemplate = false;
|
||||||
|
public htmlIsTemplate = false;
|
||||||
|
|
||||||
|
public progressWidth = 0;
|
||||||
|
|
||||||
|
private stopTime = false;
|
||||||
|
private timer: any;
|
||||||
|
private steps: number;
|
||||||
|
private speed: number;
|
||||||
|
private count = 0;
|
||||||
|
private start: any;
|
||||||
|
private diff: any;
|
||||||
|
public animate: string;
|
||||||
|
|
||||||
|
constructor(private notificationService: NotificationsService,
|
||||||
|
private domSanitizer: DomSanitizer,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private zone: NgZone) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.animate = this.notification.options.animate + NotificationAnimationsStatus.In;
|
||||||
|
|
||||||
|
if (this.notification.options.timeOut !== 0) {
|
||||||
|
this.startTimeOut();
|
||||||
|
this.showProgressBar = true;
|
||||||
|
}
|
||||||
|
this.html = this.notification.html;
|
||||||
|
this.contentType(this.notification.title, 'title');
|
||||||
|
this.contentType(this.notification.content, 'content');
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTimeOut(): void {
|
||||||
|
this.steps = this.notification.options.timeOut / 10;
|
||||||
|
this.speed = this.notification.options.timeOut / this.steps;
|
||||||
|
this.start = new Date().getTime();
|
||||||
|
this.zone.runOutsideAngular(() => this.timer = setTimeout(this.instance, this.speed));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private instance = () => {
|
||||||
|
this.diff = (new Date().getTime() - this.start) - (this.count * this.speed);
|
||||||
|
|
||||||
|
if (this.count++ === this.steps) {
|
||||||
|
this.remove();
|
||||||
|
// this.item.timeoutEnd!.emit();
|
||||||
|
} else if (!this.stopTime) {
|
||||||
|
if (this.showProgressBar) {
|
||||||
|
this.progressWidth += 100 / this.steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timer = setTimeout(this.instance, (this.speed - this.diff));
|
||||||
|
}
|
||||||
|
this.zone.run(() => this.cdr.detectChanges());
|
||||||
|
};
|
||||||
|
|
||||||
|
private remove() {
|
||||||
|
if (this.animate) {
|
||||||
|
this.setAnimationOut();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.notificationService.remove(this.notification);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
this.notificationService.remove(this.notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private contentType(item: any, key: string) {
|
||||||
|
if (item instanceof TemplateRef) {
|
||||||
|
this[key] = item;
|
||||||
|
} else if (key === 'title' || (key === 'content' && !this.html)) {
|
||||||
|
let value = null;
|
||||||
|
if (isNotEmpty(item)) {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
value = Observable.of(item);
|
||||||
|
} else if (item instanceof Observable) {
|
||||||
|
value = item;
|
||||||
|
} else if (typeof item === 'object' && isNotEmpty(item.value)) {
|
||||||
|
// when notifications state is transferred from SSR to CSR,
|
||||||
|
// Observables Object loses the instance type and become simply object,
|
||||||
|
// so converts it again to Observable
|
||||||
|
value = Observable.of(item.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this[key] = value
|
||||||
|
} else {
|
||||||
|
this[key] = this.domSanitizer.bypassSecurityTrustHtml(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
this[key + 'IsTemplate'] = item instanceof TemplateRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setAnimationOut() {
|
||||||
|
this.animate = this.notification.options.animate + NotificationAnimationsStatus.Out;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="notifications-wrapper position-fixed" [ngClass]="position">
|
||||||
|
<ds-notification
|
||||||
|
*ngFor="let a of notifications; let i = index"
|
||||||
|
[notification]="a">
|
||||||
|
</ds-notification>
|
||||||
|
</div>
|
@@ -0,0 +1,45 @@
|
|||||||
|
@import '../../../../styles/variables';
|
||||||
|
@import '../../../../styles/mixins';
|
||||||
|
|
||||||
|
.notifications-wrapper {
|
||||||
|
width: 300px;
|
||||||
|
z-index: 1051;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-wrapper.left {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-wrapper.top {
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-wrapper.right {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-wrapper.bottom {
|
||||||
|
bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-wrapper.center {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-wrapper.middle {
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-wrapper.middle.center {
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
|
||||||
|
.notifications-wrapper {
|
||||||
|
width: auto;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { ChangeDetectorRef } from '@angular/core';
|
||||||
|
|
||||||
|
import { NotificationsService } from '../notifications.service';
|
||||||
|
import { notificationsReducer } from '../notifications.reducers';
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { NotificationsBoardComponent } from './notifications-board.component';
|
||||||
|
import { AppState } from '../../../app.reducer';
|
||||||
|
import { NotificationComponent } from '../notification/notification.component';
|
||||||
|
import { Notification } from '../models/notification.model';
|
||||||
|
import { NotificationType } from '../models/notification-type';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
|
||||||
|
import { NotificationsServiceStub } from '../../testing/notifications-service-stub';
|
||||||
|
|
||||||
|
describe('NotificationsBoardComponent', () => {
|
||||||
|
let comp: NotificationsBoardComponent;
|
||||||
|
let fixture: ComponentFixture<NotificationsBoardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
StoreModule.forRoot({notificationsReducer})],
|
||||||
|
declarations: [NotificationsBoardComponent, NotificationComponent], // declare the test component
|
||||||
|
providers: [
|
||||||
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
|
ChangeDetectorRef]
|
||||||
|
}).compileComponents(); // compile template and css
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([NotificationsService, Store], (service: NotificationsService, store: Store<AppState>) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
const notifications = [
|
||||||
|
new Notification(uniqueId(), NotificationType.Success, 'title1', 'content1'),
|
||||||
|
new Notification(uniqueId(), NotificationType.Info, 'title2', 'content2')
|
||||||
|
];
|
||||||
|
state.notifications = notifications;
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(NotificationsBoardComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.options = {
|
||||||
|
rtl: false,
|
||||||
|
position: ['top', 'right'],
|
||||||
|
maxStack: 5,
|
||||||
|
timeOut: 5000,
|
||||||
|
clickToClose: true,
|
||||||
|
animate: 'scale'
|
||||||
|
} as INotificationBoardOptions;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create component', () => {
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have two notifications', () => {
|
||||||
|
expect(comp.notifications.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewEncapsulation
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
|
import { difference } from 'lodash';
|
||||||
|
|
||||||
|
import { NotificationsService } from '../notifications.service';
|
||||||
|
import { AppState } from '../../../app.reducer';
|
||||||
|
import { notificationsStateSelector } from '../selectors';
|
||||||
|
import { INotification } from '../models/notification.model';
|
||||||
|
import { NotificationsState } from '../notifications.reducers';
|
||||||
|
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-notifications-board',
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
templateUrl: './notifications-board.component.html',
|
||||||
|
styleUrls: ['./notifications-board.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class NotificationsBoardComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set options(opt: INotificationBoardOptions) {
|
||||||
|
this.attachChanges(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public notifications: INotification[] = [];
|
||||||
|
public position: ['top' | 'bottom' | 'middle', 'right' | 'left' | 'center'] = ['bottom', 'right'];
|
||||||
|
|
||||||
|
// Received values
|
||||||
|
private maxStack = 8;
|
||||||
|
private sub: Subscription;
|
||||||
|
|
||||||
|
// Sent values
|
||||||
|
public rtl = false;
|
||||||
|
public animate: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale' = 'fromRight';
|
||||||
|
|
||||||
|
constructor(private service: NotificationsService,
|
||||||
|
private store: Store<AppState>,
|
||||||
|
private cdr: ChangeDetectorRef) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.sub = this.store.select(notificationsStateSelector)
|
||||||
|
.subscribe((state: NotificationsState) => {
|
||||||
|
if (state.length === 0) {
|
||||||
|
this.notifications = [];
|
||||||
|
} else if (state.length > this.notifications.length) {
|
||||||
|
// Add
|
||||||
|
const newElem = difference(state, this.notifications);
|
||||||
|
newElem.forEach((notification) => {
|
||||||
|
this.add(notification);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove
|
||||||
|
const delElem = difference(this.notifications, state);
|
||||||
|
delElem.forEach((notification) => {
|
||||||
|
this.notifications = this.notifications.filter((item: INotification) => item.id !== notification.id);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new notification to the notification array
|
||||||
|
add(item: INotification): void {
|
||||||
|
const toBlock: boolean = this.block(item);
|
||||||
|
if (!toBlock) {
|
||||||
|
if (this.notifications.length >= this.maxStack) {
|
||||||
|
this.notifications.splice(this.notifications.length - 1, 1);
|
||||||
|
}
|
||||||
|
this.notifications.splice(0, 0, item);
|
||||||
|
} else {
|
||||||
|
// Remove the notification from the store
|
||||||
|
// This notification was in the store, but not in this.notifications
|
||||||
|
// because it was a blocked duplicate
|
||||||
|
this.service.remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private block(item: INotification): boolean {
|
||||||
|
const toCheck = item.html ? this.checkHtml : this.checkStandard;
|
||||||
|
this.notifications.forEach((notification) => {
|
||||||
|
if (toCheck(notification, item)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.notifications.length > 0) {
|
||||||
|
this.notifications.forEach((notification) => {
|
||||||
|
if (toCheck(notification, item)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let comp: INotification;
|
||||||
|
if (this.notifications.length > 0) {
|
||||||
|
comp = this.notifications[0];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return toCheck(comp, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkStandard(checker: INotification, item: INotification): boolean {
|
||||||
|
return checker.type === item.type && checker.title === item.title && checker.content === item.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkHtml(checker: INotification, item: INotification): boolean {
|
||||||
|
return checker.html ? checker.type === item.type && checker.title === item.title && checker.content === item.content && checker.html === item.html : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach all the changes received in the options object
|
||||||
|
private attachChanges(options: any): void {
|
||||||
|
Object.keys(options).forEach((a) => {
|
||||||
|
if (this.hasOwnProperty(a)) {
|
||||||
|
(this as any)[a] = options[a];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.sub) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
src/app/shared/notifications/notifications.actions.ts
Normal file
61
src/app/shared/notifications/notifications.actions.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
import { type } from '../../shared/ngrx/type';
|
||||||
|
import { INotification } from './models/notification.model';
|
||||||
|
|
||||||
|
export const NotificationsActionTypes = {
|
||||||
|
NEW_NOTIFICATION: type('dspace/notifications/NEW_NOTIFICATION'),
|
||||||
|
REMOVE_ALL_NOTIFICATIONS: type('dspace/notifications/REMOVE_ALL_NOTIFICATIONS'),
|
||||||
|
REMOVE_NOTIFICATION: type('dspace/notifications/REMOVE_NOTIFICATION'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New notification.
|
||||||
|
* @class NewNotificationAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class NewNotificationAction implements Action {
|
||||||
|
public type: string = NotificationsActionTypes.NEW_NOTIFICATION;
|
||||||
|
payload: INotification;
|
||||||
|
|
||||||
|
constructor(notification: INotification) {
|
||||||
|
this.payload = notification;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all notifications.
|
||||||
|
* @class RemoveAllNotificationsAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RemoveAllNotificationsAction implements Action {
|
||||||
|
public type: string = NotificationsActionTypes.REMOVE_ALL_NOTIFICATIONS;
|
||||||
|
|
||||||
|
constructor(public payload?: any) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a notification.
|
||||||
|
* @class RemoveNotificationAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RemoveNotificationAction implements Action {
|
||||||
|
public type: string = NotificationsActionTypes.REMOVE_NOTIFICATION;
|
||||||
|
payload: any;
|
||||||
|
|
||||||
|
constructor(notificationId: any) {
|
||||||
|
this.payload = notificationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions type.
|
||||||
|
* @type {NotificationsActions}
|
||||||
|
*/
|
||||||
|
export type NotificationsActions
|
||||||
|
= NewNotificationAction
|
||||||
|
| RemoveAllNotificationsAction
|
||||||
|
| RemoveNotificationAction;
|
32
src/app/shared/notifications/notifications.effects.ts
Normal file
32
src/app/shared/notifications/notifications.effects.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Actions } from '@ngrx/effects';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NotificationsEffects {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate user.
|
||||||
|
* @method authenticate
|
||||||
|
*/
|
||||||
|
/* @Effect()
|
||||||
|
public timer: Observable<Action> = this.actions$
|
||||||
|
.ofType(NotificationsActionTypes.NEW_NOTIFICATION_WITH_TIMER)
|
||||||
|
// .debounceTime((action: any) => action.payload.options.timeOut)
|
||||||
|
.debounceTime(3000)
|
||||||
|
.map(() => new RemoveNotificationAction());
|
||||||
|
.switchMap((action: NewNotificationWithTimerAction) => Observable
|
||||||
|
.timer(30000)
|
||||||
|
.mapTo(() => new RemoveNotificationAction())
|
||||||
|
);*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {Actions} actions$
|
||||||
|
* @param {Store} store
|
||||||
|
*/
|
||||||
|
constructor(private actions$: Actions,
|
||||||
|
private store: Store<AppState>) {
|
||||||
|
}
|
||||||
|
}
|
140
src/app/shared/notifications/notifications.reducers.spec.ts
Normal file
140
src/app/shared/notifications/notifications.reducers.spec.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { notificationsReducer } from './notifications.reducers';
|
||||||
|
import { NewNotificationAction, RemoveAllNotificationsAction, RemoveNotificationAction } from './notifications.actions';
|
||||||
|
import { NotificationsService } from './notifications.service';
|
||||||
|
import { fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing';
|
||||||
|
import { NotificationsBoardComponent } from './notifications-board/notifications-board.component';
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { NotificationComponent } from './notification/notification.component';
|
||||||
|
import { NotificationOptions } from './models/notification-options.model';
|
||||||
|
import { NotificationAnimationsType } from './models/notification-animations-type';
|
||||||
|
import { NotificationType } from './models/notification-type';
|
||||||
|
import { Notification } from './models/notification.model';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
import { ChangeDetectorRef } from '@angular/core';
|
||||||
|
|
||||||
|
describe('Notifications reducer', () => {
|
||||||
|
|
||||||
|
let notification1;
|
||||||
|
let notification2;
|
||||||
|
let notification3;
|
||||||
|
let notificationHtml;
|
||||||
|
let options;
|
||||||
|
let html;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [NotificationComponent, NotificationsBoardComponent],
|
||||||
|
providers: [NotificationsService],
|
||||||
|
imports: [
|
||||||
|
ChangeDetectorRef,
|
||||||
|
StoreModule.forRoot({notificationsReducer}),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
options = new NotificationOptions(
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
NotificationAnimationsType.Rotate);
|
||||||
|
notification1 = new Notification(uniqueId(), NotificationType.Success, 'title1', 'content1', options, null);
|
||||||
|
notification2 = new Notification(uniqueId(), NotificationType.Info, 'title2', 'content2', options, null);
|
||||||
|
notification3 = new Notification(uniqueId(), NotificationType.Warning, 'title3', 'content3', options, null);
|
||||||
|
html = '<p>I\'m a mock test</p>';
|
||||||
|
notificationHtml = new Notification(uniqueId(), NotificationType.Error, null, null, options, html);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add 4 notifications and verify fields and length', () => {
|
||||||
|
const state1 = notificationsReducer(undefined, new NewNotificationAction(notification1));
|
||||||
|
const n1 = state1[0];
|
||||||
|
expect(n1.title).toBe('title1');
|
||||||
|
expect(n1.content).toBe('content1');
|
||||||
|
expect(n1.type).toBe(NotificationType.Success);
|
||||||
|
expect(n1.options).toBe(options);
|
||||||
|
expect(n1.html).toBeNull();
|
||||||
|
expect(state1.length).toEqual(1);
|
||||||
|
|
||||||
|
const state2 = notificationsReducer(state1, new NewNotificationAction(notification2));
|
||||||
|
const n2 = state2[1];
|
||||||
|
expect(n2.title).toBe('title2');
|
||||||
|
expect(n2.content).toBe('content2');
|
||||||
|
expect(n2.type).toBe(NotificationType.Info);
|
||||||
|
expect(n2.options).toBe(options);
|
||||||
|
expect(n2.html).toBeNull();
|
||||||
|
expect(state2.length).toEqual(2);
|
||||||
|
|
||||||
|
const state3 = notificationsReducer(state2, new NewNotificationAction(notification3));
|
||||||
|
const n3 = state3[2];
|
||||||
|
expect(n3.title).toBe('title3');
|
||||||
|
expect(n3.content).toBe('content3');
|
||||||
|
expect(n3.type).toBe(NotificationType.Warning);
|
||||||
|
expect(n3.options).toBe(options);
|
||||||
|
expect(n3.html).toBeNull();
|
||||||
|
expect(state3.length).toEqual(3);
|
||||||
|
|
||||||
|
const state4 = notificationsReducer(state3, new NewNotificationAction(notificationHtml));
|
||||||
|
const n4 = state4[3];
|
||||||
|
expect(n4.title).toBeNull();
|
||||||
|
expect(n4.content).toBeNull();
|
||||||
|
expect(n4.type).toBe(NotificationType.Error);
|
||||||
|
expect(n4.options).toBe(options);
|
||||||
|
expect(n4.html).toBe(html);
|
||||||
|
expect(state4.length).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add 2 notifications and remove only the first', () => {
|
||||||
|
const state1 = notificationsReducer(undefined, new NewNotificationAction(notification1));
|
||||||
|
expect(state1.length).toEqual(1);
|
||||||
|
|
||||||
|
const state2 = notificationsReducer(state1, new NewNotificationAction(notification2));
|
||||||
|
expect(state2.length).toEqual(2);
|
||||||
|
|
||||||
|
const state3 = notificationsReducer(state2, new RemoveNotificationAction(notification1.id));
|
||||||
|
expect(state3.length).toEqual(1);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add 2 notifications and later remove all', () => {
|
||||||
|
const state1 = notificationsReducer(undefined, new NewNotificationAction(notification1));
|
||||||
|
expect(state1.length).toEqual(1);
|
||||||
|
|
||||||
|
const state2 = notificationsReducer(state1, new NewNotificationAction(notification2));
|
||||||
|
expect(state2.length).toEqual(2);
|
||||||
|
|
||||||
|
const state3 = notificationsReducer(state2, new RemoveAllNotificationsAction());
|
||||||
|
expect(state3.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create 2 notifications and check they close after different timeout', fakeAsync(() => {
|
||||||
|
inject([ChangeDetectorRef], (cdr: ChangeDetectorRef) => {
|
||||||
|
const optionsWithTimeout = new NotificationOptions(
|
||||||
|
1000,
|
||||||
|
true,
|
||||||
|
NotificationAnimationsType.Rotate);
|
||||||
|
// Timeout 1000ms
|
||||||
|
const notification = new Notification(uniqueId(), NotificationType.Success, 'title', 'content', optionsWithTimeout, null);
|
||||||
|
const state = notificationsReducer(undefined, new NewNotificationAction(notification));
|
||||||
|
expect(state.length).toEqual(1);
|
||||||
|
|
||||||
|
// Timeout default 5000ms
|
||||||
|
const notificationBis = new Notification(uniqueId(), NotificationType.Success, 'title', 'content');
|
||||||
|
const stateBis = notificationsReducer(state, new NewNotificationAction(notification));
|
||||||
|
expect(stateBis.length).toEqual(2);
|
||||||
|
|
||||||
|
tick(1000);
|
||||||
|
cdr.detectChanges();
|
||||||
|
|
||||||
|
const action = new NewNotificationAction(notification);
|
||||||
|
action.type = 'NothingToDo, return only the state';
|
||||||
|
|
||||||
|
const lastState = notificationsReducer(stateBis, action);
|
||||||
|
expect(lastState.length).toEqual(1);
|
||||||
|
|
||||||
|
flush();
|
||||||
|
cdr.detectChanges();
|
||||||
|
|
||||||
|
const finalState = notificationsReducer(lastState, action);
|
||||||
|
expect(finalState.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
43
src/app/shared/notifications/notifications.reducers.ts
Normal file
43
src/app/shared/notifications/notifications.reducers.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { NotificationsActions, NotificationsActionTypes, RemoveNotificationAction } from './notifications.actions';
|
||||||
|
import { INotification } from './models/notification.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth state.
|
||||||
|
* @interface State
|
||||||
|
*/
|
||||||
|
export interface NotificationsState extends Array<INotification> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial state.
|
||||||
|
*/
|
||||||
|
const initialState: NotificationsState = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reducer function.
|
||||||
|
* @function reducer
|
||||||
|
* @param {State} state Current state
|
||||||
|
* @param {NotificationsActions} action Incoming action
|
||||||
|
*/
|
||||||
|
export function notificationsReducer(state: any = initialState, action: NotificationsActions): NotificationsState {
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case NotificationsActionTypes.NEW_NOTIFICATION:
|
||||||
|
return [...state, action.payload];
|
||||||
|
|
||||||
|
case NotificationsActionTypes.REMOVE_ALL_NOTIFICATIONS:
|
||||||
|
return [];
|
||||||
|
|
||||||
|
case NotificationsActionTypes.REMOVE_NOTIFICATION:
|
||||||
|
return removeNotification(state, action as RemoveNotificationAction);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeNotification = (state: NotificationsState, action: RemoveNotificationAction): NotificationsState => {
|
||||||
|
const newState = state.filter((item: INotification) => item.id !== action.payload);
|
||||||
|
return newState;
|
||||||
|
};
|
80
src/app/shared/notifications/notifications.service.spec.ts
Normal file
80
src/app/shared/notifications/notifications.service.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { NotificationsService } from './notifications.service';
|
||||||
|
import { NotificationsBoardComponent } from './notifications-board/notifications-board.component';
|
||||||
|
import { NotificationComponent } from './notification/notification.component';
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { notificationsReducer } from './notifications.reducers';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import 'rxjs/add/observable/of';
|
||||||
|
import { NewNotificationAction, RemoveAllNotificationsAction, RemoveNotificationAction } from './notifications.actions';
|
||||||
|
import { Notification } from './models/notification.model';
|
||||||
|
import { NotificationType } from './models/notification-type';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
|
||||||
|
describe('NotificationsService test', () => {
|
||||||
|
const store: Store<Notification> = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
select: Observable.of(true)
|
||||||
|
});
|
||||||
|
let service;
|
||||||
|
let envConfig: GlobalConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [NotificationComponent, NotificationsBoardComponent],
|
||||||
|
providers: [NotificationsService],
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({notificationsReducer}),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
envConfig = {
|
||||||
|
notifications: {
|
||||||
|
rtl: false,
|
||||||
|
position: ['top', 'right'],
|
||||||
|
maxStack: 8,
|
||||||
|
timeOut: 5000,
|
||||||
|
clickToClose: true,
|
||||||
|
animate: 'scale'
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
service = new NotificationsService(envConfig, store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Success method should dispatch NewNotificationAction with proper parameter', () => {
|
||||||
|
const notification = service.success('Title', Observable.of('Content'));
|
||||||
|
expect(notification.type).toBe(NotificationType.Success);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new NewNotificationAction(notification));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Warning method should dispatch NewNotificationAction with proper parameter', () => {
|
||||||
|
const notification = service.warning('Title', Observable.of('Content'));
|
||||||
|
expect(notification.type).toBe(NotificationType.Warning);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new NewNotificationAction(notification));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Info method should dispatch NewNotificationAction with proper parameter', () => {
|
||||||
|
const notification = service.info('Title', Observable.of('Content'));
|
||||||
|
expect(notification.type).toBe(NotificationType.Info);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new NewNotificationAction(notification));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error method should dispatch NewNotificationAction with proper parameter', () => {
|
||||||
|
const notification = service.error('Title', Observable.of('Content'));
|
||||||
|
expect(notification.type).toBe(NotificationType.Error);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new NewNotificationAction(notification));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Remove method should dispatch RemoveNotificationAction with proper id', () => {
|
||||||
|
const notification = new Notification('1234', NotificationType.Info, 'title...', 'description');
|
||||||
|
service.remove(notification);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new RemoveNotificationAction(notification.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('RemoveAll method should dispatch RemoveAllNotificationsAction', () => {
|
||||||
|
service.removeAll();
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new RemoveAllNotificationsAction());
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
77
src/app/shared/notifications/notifications.service.ts
Normal file
77
src/app/shared/notifications/notifications.service.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { INotification, Notification } from './models/notification.model';
|
||||||
|
import { NotificationType } from './models/notification-type';
|
||||||
|
import { NotificationOptions } from './models/notification-options.model';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NewNotificationAction, RemoveAllNotificationsAction, RemoveNotificationAction } from './notifications.actions';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NotificationsService {
|
||||||
|
|
||||||
|
constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||||
|
private store: Store<Notification>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private add(notification: Notification) {
|
||||||
|
let notificationAction;
|
||||||
|
notificationAction = new NewNotificationAction(notification);
|
||||||
|
this.store.dispatch(notificationAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
success(title: any = Observable.of(''),
|
||||||
|
content: any = Observable.of(''),
|
||||||
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
|
html: boolean = false): INotification {
|
||||||
|
const notification = new Notification(uniqueId(), NotificationType.Success, title, content, options, html);
|
||||||
|
this.add(notification);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
error(title: any = Observable.of(''),
|
||||||
|
content: any = Observable.of(''),
|
||||||
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
|
html: boolean = false): INotification {
|
||||||
|
const notification = new Notification(uniqueId(), NotificationType.Error, title, content, options, html);
|
||||||
|
this.add(notification);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
info(title: any = Observable.of(''),
|
||||||
|
content: any = Observable.of(''),
|
||||||
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
|
html: boolean = false): INotification {
|
||||||
|
const notification = new Notification(uniqueId(), NotificationType.Info, title, content, options, html);
|
||||||
|
this.add(notification);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(title: any = Observable.of(''),
|
||||||
|
content: any = Observable.of(''),
|
||||||
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
|
html: boolean = false): INotification {
|
||||||
|
const notification = new Notification(uniqueId(), NotificationType.Warning, title, content, options, html);
|
||||||
|
this.add(notification);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(notification: INotification) {
|
||||||
|
const actionRemove = new RemoveNotificationAction(notification.id);
|
||||||
|
this.store.dispatch(actionRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAll() {
|
||||||
|
const actionRemoveAll = new RemoveAllNotificationsAction();
|
||||||
|
this.store.dispatch(actionRemoveAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDefaultOptions(): NotificationOptions {
|
||||||
|
return new NotificationOptions(
|
||||||
|
this.config.notifications.timeOut,
|
||||||
|
this.config.notifications.clickToClose,
|
||||||
|
this.config.notifications.animate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
9
src/app/shared/notifications/selectors.ts
Normal file
9
src/app/shared/notifications/selectors.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Returns the user state.
|
||||||
|
* @function getUserState
|
||||||
|
* @param {AppState} state Top level state.
|
||||||
|
* @return {AuthState}
|
||||||
|
*/
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
|
||||||
|
export const notificationsStateSelector = (state: AppState) => state.notifications;
|
@@ -20,11 +20,11 @@ import { SearchResultListElementComponent } from './object-list/search-result-li
|
|||||||
import { WrapperListElementComponent } from './object-list/wrapper-list-element/wrapper-list-element.component';
|
import { WrapperListElementComponent } from './object-list/wrapper-list-element/wrapper-list-element.component';
|
||||||
import { ObjectListComponent } from './object-list/object-list.component';
|
import { ObjectListComponent } from './object-list/object-list.component';
|
||||||
|
|
||||||
import { CollectionGridElementComponent} from './object-grid/collection-grid-element/collection-grid-element.component'
|
import { CollectionGridElementComponent } from './object-grid/collection-grid-element/collection-grid-element.component';
|
||||||
import { CommunityGridElementComponent} from './object-grid/community-grid-element/community-grid-element.component'
|
import { CommunityGridElementComponent } from './object-grid/community-grid-element/community-grid-element.component';
|
||||||
import { ItemGridElementComponent} from './object-grid/item-grid-element/item-grid-element.component'
|
import { ItemGridElementComponent } from './object-grid/item-grid-element/item-grid-element.component';
|
||||||
import { AbstractListableElementComponent} from './object-collection/shared/object-collection-element/abstract-listable-element.component'
|
import { AbstractListableElementComponent } from './object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||||
import { WrapperGridElementComponent} from './object-grid/wrapper-grid-element/wrapper-grid-element.component'
|
import { WrapperGridElementComponent } from './object-grid/wrapper-grid-element/wrapper-grid-element.component';
|
||||||
import { ObjectGridComponent } from './object-grid/object-grid.component';
|
import { ObjectGridComponent } from './object-grid/object-grid.component';
|
||||||
import { ObjectCollectionComponent } from './object-collection/object-collection.component';
|
import { ObjectCollectionComponent } from './object-collection/object-collection.component';
|
||||||
import { ComcolPageContentComponent } from './comcol-page-content/comcol-page-content.component';
|
import { ComcolPageContentComponent } from './comcol-page-content/comcol-page-content.component';
|
||||||
@@ -43,11 +43,14 @@ import { VarDirective } from './utils/var.directive';
|
|||||||
import { LogInComponent } from './log-in/log-in.component';
|
import { LogInComponent } from './log-in/log-in.component';
|
||||||
import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
|
import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
|
||||||
import { LogOutComponent } from './log-out/log-out.component';
|
import { LogOutComponent } from './log-out/log-out.component';
|
||||||
|
import { NotificationComponent } from './notifications/notification/notification.component';
|
||||||
|
import { NotificationsBoardComponent } from './notifications/notifications-board/notifications-board.component';
|
||||||
import { DragClickDirective } from './utils/drag-click.directive';
|
import { DragClickDirective } from './utils/drag-click.directive';
|
||||||
import { TruncatePipe } from './utils/truncate.pipe';
|
import { TruncatePipe } from './utils/truncate.pipe';
|
||||||
import { TruncatableComponent } from './truncatable/truncatable.component';
|
import { TruncatableComponent } from './truncatable/truncatable.component';
|
||||||
import { TruncatableService } from './truncatable/truncatable.service';
|
import { TruncatableService } from './truncatable/truncatable.service';
|
||||||
import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component';
|
import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component';
|
||||||
|
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -103,11 +106,12 @@ const ENTRY_COMPONENTS = [
|
|||||||
ItemGridElementComponent,
|
ItemGridElementComponent,
|
||||||
CollectionGridElementComponent,
|
CollectionGridElementComponent,
|
||||||
CommunityGridElementComponent,
|
CommunityGridElementComponent,
|
||||||
SearchResultGridElementComponent
|
SearchResultGridElementComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
TruncatableService
|
TruncatableService,
|
||||||
|
MockAdminGuard
|
||||||
];
|
];
|
||||||
|
|
||||||
const DIRECTIVES = [
|
const DIRECTIVES = [
|
||||||
|
46
src/app/shared/testing/notifications-service-stub.ts
Normal file
46
src/app/shared/testing/notifications-service-stub.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { INotification } from '../notifications/models/notification.model';
|
||||||
|
import { NotificationOptions } from '../notifications/models/notification-options.model';
|
||||||
|
|
||||||
|
export class NotificationsServiceStub {
|
||||||
|
|
||||||
|
success(title: any = Observable.of(''),
|
||||||
|
content: any = Observable.of(''),
|
||||||
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
|
html?: any): INotification {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
error(title: any = Observable.of(''),
|
||||||
|
content: any = Observable.of(''),
|
||||||
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
|
html?: any): INotification {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info(title: any = Observable.of(''),
|
||||||
|
content: any = Observable.of(''),
|
||||||
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
|
html?: any): INotification {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(title: any = Observable.of(''),
|
||||||
|
content: any = Observable.of(''),
|
||||||
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
|
html?: any): INotification {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(notification: INotification) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAll() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDefaultOptions(): NotificationOptions {
|
||||||
|
return new NotificationOptions();
|
||||||
|
}
|
||||||
|
}
|
@@ -2,12 +2,14 @@ import { Config } from './config.interface';
|
|||||||
import { ServerConfig } from './server-config.interface';
|
import { ServerConfig } from './server-config.interface';
|
||||||
import { CacheConfig } from './cache-config.interface';
|
import { CacheConfig } from './cache-config.interface';
|
||||||
import { UniversalConfig } from './universal-config.interface';
|
import { UniversalConfig } from './universal-config.interface';
|
||||||
|
import { INotificationBoardOptions } from './notifications-config.interfaces';
|
||||||
|
|
||||||
export interface GlobalConfig extends Config {
|
export interface GlobalConfig extends Config {
|
||||||
ui: ServerConfig;
|
ui: ServerConfig;
|
||||||
rest: ServerConfig;
|
rest: ServerConfig;
|
||||||
production: boolean;
|
production: boolean;
|
||||||
cache: CacheConfig;
|
cache: CacheConfig;
|
||||||
|
notifications: INotificationBoardOptions;
|
||||||
universal: UniversalConfig;
|
universal: UniversalConfig;
|
||||||
gaTrackingId: string;
|
gaTrackingId: string;
|
||||||
logDirectory: string;
|
logDirectory: string;
|
||||||
|
11
src/config/notifications-config.interfaces.ts
Normal file
11
src/config/notifications-config.interfaces.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Config } from './config.interface';
|
||||||
|
import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type';
|
||||||
|
|
||||||
|
export interface INotificationBoardOptions extends Config {
|
||||||
|
rtl: boolean;
|
||||||
|
position: ['top' | 'bottom' | 'middle', 'right' | 'left' | 'center'];
|
||||||
|
maxStack: number;
|
||||||
|
timeOut: number;
|
||||||
|
clickToClose: boolean;
|
||||||
|
animate: NotificationAnimationsType;
|
||||||
|
}
|
Reference in New Issue
Block a user