Merge remote-tracking branch 'remotes/origin/master' into dynamic_forms

# Conflicts:
#	src/app/app.reducer.ts
#	src/app/core/core.module.ts
#	src/app/shared/shared.module.ts
This commit is contained in:
Giuseppe Digilio
2018-05-15 18:44:51 +02:00
40 changed files with 1446 additions and 33 deletions

View File

@@ -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,

View File

@@ -6,5 +6,4 @@ import { Component } from '@angular/core';
templateUrl: './home-page.component.html' templateUrl: './home-page.component.html'
}) })
export class HomePageComponent { export class HomePageComponent {
} }

View File

@@ -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 {

View File

@@ -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>

View File

@@ -1,11 +1,4 @@
import { import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, ViewEncapsulation } from '@angular/core';
ChangeDetectionStrategy,
Component,
HostListener,
Inject,
OnInit,
ViewEncapsulation
} from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';

View File

@@ -1,8 +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 { NotificationsEffects } from './shared/notifications/notifications.effects';
export const appEffects = [ export const appEffects = [
StoreEffects, StoreEffects,
HeaderEffects HeaderEffects,
NotificationsEffects
]; ];

View File

@@ -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';
export function getConfig() { export function getConfig() {
return ENV_CONFIG; return ENV_CONFIG;
@@ -85,7 +86,9 @@ if (!ENV_CONFIG.production) {
AppComponent, AppComponent,
HeaderComponent, HeaderComponent,
FooterComponent, FooterComponent,
PageNotFoundComponent PageNotFoundComponent,
NotificationComponent,
NotificationsBoardComponent
], ],
exports: [AppComponent] exports: [AppComponent]
}) })

View File

@@ -12,6 +12,7 @@ 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 {
@@ -19,6 +20,7 @@ export interface AppState {
hostWindow: HostWindowState; hostWindow: HostWindowState;
header: HeaderState; header: HeaderState;
forms: FormState; forms: FormState;
notifications: NotificationsState;
searchSidebar: SearchSidebarState; searchSidebar: SearchSidebarState;
searchFilter: SearchFiltersState; searchFilter: SearchFiltersState;
truncatable: TruncatablesState; truncatable: TruncatablesState;
@@ -29,6 +31,7 @@ export const appReducers: ActionReducerMap<AppState> = {
hostWindow: hostWindowReducer, hostWindow: hostWindowReducer,
header: headerReducer, header: headerReducer,
forms: formReducer, forms: formReducer,
notifications: notificationsReducer,
searchSidebar: sidebarReducer, searchSidebar: sidebarReducer,
searchFilter: filterReducer, searchFilter: filterReducer,
truncatable: truncatableReducer truncatable: truncatableReducer

View File

@@ -49,6 +49,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';
import { UploaderService } from '../shared/uploader/uploader.service'; import { UploaderService } from '../shared/uploader/uploader.service';
const IMPORTS = [ const IMPORTS = [
@@ -103,6 +104,7 @@ const PROVIDERS = [
IntegrationResponseParsingService, IntegrationResponseParsingService,
UploaderService, UploaderService,
UUIDService, UUIDService,
NotificationsService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory } { provide: NativeWindowService, useFactory: NativeWindowFactory }
]; ];

View File

@@ -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

View 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
]);

View 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
]);

View 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
]);

View 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
]);

View 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
]);

View 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
]);

View 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);
}
}

View File

@@ -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'
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
export enum NotificationType {
Success = 'alert-success',
Error = 'alert-danger',
Info = 'alert-info',
Warning = 'alert-warning',
// Bare = 'bare'
}

View 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;
}
}

View File

@@ -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">&times;</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>

View File

@@ -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%);
}

View File

@@ -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);
})
});

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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);
});
})
;

View File

@@ -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();
}
}
}

View 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;

View 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>) {
}
}

View 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);
});
}));
});

View 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;
};

View 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());
});
});

View 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
);
}
}

View 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;

View File

@@ -25,11 +25,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';
@@ -52,6 +52,8 @@ import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dyn
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core'; import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { TextMaskModule } from 'angular2-text-mask'; import { TextMaskModule } from 'angular2-text-mask';
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';
@@ -66,6 +68,7 @@ import { SortablejsModule } from 'angular-sortablejs';
import { NumberPickerComponent } from './number-picker/number-picker.component'; import { NumberPickerComponent } from './number-picker/number-picker.component';
import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component'; import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component'; import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.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
@@ -141,11 +144,12 @@ const ENTRY_COMPONENTS = [
ItemGridElementComponent, ItemGridElementComponent,
CollectionGridElementComponent, CollectionGridElementComponent,
CommunityGridElementComponent, CommunityGridElementComponent,
SearchResultGridElementComponent SearchResultGridElementComponent,
]; ];
const PROVIDERS = [ const PROVIDERS = [
TruncatableService TruncatableService,
MockAdminGuard
]; ];
const DIRECTIVES = [ const DIRECTIVES = [

View 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();
}
}

View File

@@ -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;

View 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;
}