mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 18:44:14 +00:00
Merge branch 'master' into w2p-50632_Replace-mock-registry-service-with-implementation
Conflicts: src/app/core/core.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,
|
||||||
|
@@ -1,5 +1,60 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
|
Default configuration file is located in `config/` folder. All configuration options should be listed in the default configuration file `config/environment.default.js`. Please do not change this file directly! To change the default configuration values, create local files that override the parameters you need to change:
|
||||||
|
|
||||||
|
- Create a new `environment.dev.js` file in `config/` for `devel` environment;
|
||||||
|
- Create a new `environment.prod.js` file in `config/` for `production` environment;
|
||||||
|
|
||||||
|
Some few configuration options can be overridden by setting environment variables. These and the variable names are listed below.
|
||||||
|
|
||||||
|
## Nodejs server
|
||||||
|
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itsself to, and if ssl should be enabled not. By default it listens on `localhost:3000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
|
||||||
|
|
||||||
|
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
||||||
|
```
|
||||||
|
module.exports = {
|
||||||
|
// Angular Universal server settings.
|
||||||
|
ui: {
|
||||||
|
ssl: false,
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3000,
|
||||||
|
nameSpace: '/'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||||
|
```
|
||||||
|
DSPACE_SSL=true
|
||||||
|
DSPACE_HOST=localhost
|
||||||
|
DSPACE_PORT=3000
|
||||||
|
DSPACE_NAMESPACE=/
|
||||||
|
```
|
||||||
|
|
||||||
|
## DSpace's REST endpoint
|
||||||
|
dspace-angular connects to your DSpace installation by using its REST endpoint. To do so, you have to define the ip address, port and if ssl should be enabled. You can do this in a configuration file (see above) by adding the following options:
|
||||||
|
|
||||||
|
```
|
||||||
|
module.exports = {
|
||||||
|
// The REST API server settings.
|
||||||
|
rest: {
|
||||||
|
ssl: true,
|
||||||
|
host: 'dspace7.4science.it',
|
||||||
|
port: 443,
|
||||||
|
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
|
nameSpace: '/dspace-spring-rest/api'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||||
|
```
|
||||||
|
DSPACE_REST_SSL=true
|
||||||
|
DSPACE_REST_HOST=localhost
|
||||||
|
DSPACE_REST_PORT=3000
|
||||||
|
DSPACE_REST_NAMESPACE=/
|
||||||
|
```
|
||||||
|
|
||||||
## Supporting analytics services other than Google Analytics
|
## Supporting analytics services other than Google Analytics
|
||||||
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.
|
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -1,11 +1,17 @@
|
|||||||
<div class="outer-wrapper">
|
<div class="outer-wrapper">
|
||||||
<div class="inner-wrapper">
|
<div class="inner-wrapper">
|
||||||
<ds-header></ds-header>
|
<ds-header></ds-header>
|
||||||
|
|
||||||
<main class="main-content">
|
<ds-notifications-board
|
||||||
<router-outlet></router-outlet>
|
[options]="config.notifications">
|
||||||
</main>
|
</ds-notifications-board>
|
||||||
|
|
||||||
<ds-footer></ds-footer>
|
<main class="main-content">
|
||||||
</div>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
|
<ds-footer></ds-footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
|
@@ -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
|
||||||
];
|
];
|
||||||
|
@@ -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]
|
||||||
})
|
})
|
||||||
|
@@ -1,32 +1,35 @@
|
|||||||
import { ActionReducerMap } from '@ngrx/store';
|
import { ActionReducerMap } from '@ngrx/store';
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
import * as fromRouter from '@ngrx/router-store';
|
||||||
|
|
||||||
import { headerReducer, HeaderState } from './header/header.reducer';
|
import { headerReducer, HeaderState } from './header/header.reducer';
|
||||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||||
import {
|
import {
|
||||||
SearchSidebarState,
|
SearchSidebarState,
|
||||||
sidebarReducer
|
sidebarReducer
|
||||||
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
||||||
import {
|
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 { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
|
||||||
|
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||||
export interface AppState {
|
|
||||||
router: fromRouter.RouterReducerState;
|
export interface AppState {
|
||||||
hostWindow: HostWindowState;
|
router: fromRouter.RouterReducerState;
|
||||||
header: HeaderState;
|
hostWindow: HostWindowState;
|
||||||
searchSidebar: SearchSidebarState;
|
header: HeaderState;
|
||||||
searchFilter: SearchFiltersState;
|
notifications: NotificationsState;
|
||||||
truncatable: TruncatablesState;
|
searchSidebar: SearchSidebarState;
|
||||||
}
|
searchFilter: SearchFiltersState;
|
||||||
|
truncatable: TruncatablesState;
|
||||||
export const appReducers: ActionReducerMap<AppState> = {
|
}
|
||||||
router: fromRouter.routerReducer,
|
|
||||||
hostWindow: hostWindowReducer,
|
export const appReducers: ActionReducerMap<AppState> = {
|
||||||
header: headerReducer,
|
router: fromRouter.routerReducer,
|
||||||
searchSidebar: sidebarReducer,
|
hostWindow: hostWindowReducer,
|
||||||
searchFilter: filterReducer,
|
header: headerReducer,
|
||||||
truncatable: truncatableReducer
|
notifications: notificationsReducer,
|
||||||
};
|
searchSidebar: sidebarReducer,
|
||||||
|
searchFilter: filterReducer,
|
||||||
|
truncatable: truncatableReducer
|
||||||
|
};
|
||||||
|
@@ -49,6 +49,7 @@ import { RegistryMetadataschemasResponseParsingService } from './data/registry-m
|
|||||||
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
||||||
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
|
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
|
||||||
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
||||||
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -99,6 +100,7 @@ const PROVIDERS = [
|
|||||||
SubmissionFormsConfigService,
|
SubmissionFormsConfigService,
|
||||||
SubmissionSectionsConfigService,
|
SubmissionSectionsConfigService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
|
NotificationsService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -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';
|
||||||
@@ -40,11 +40,14 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
|
|||||||
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||||
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
||||||
import { VarDirective } from './utils/var.directive';
|
import { VarDirective } from './utils/var.directive';
|
||||||
|
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
|
||||||
@@ -97,11 +100,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();
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,7 @@ import { InjectionToken } from '@angular/core';
|
|||||||
import { Config } from './config/config.interface';
|
import { Config } from './config/config.interface';
|
||||||
import { ServerConfig } from './config/server-config.interface';
|
import { ServerConfig } from './config/server-config.interface';
|
||||||
import { GlobalConfig } from './config/global-config.interface';
|
import { GlobalConfig } from './config/global-config.interface';
|
||||||
|
import { hasValue } from './app/shared/empty.util';
|
||||||
|
|
||||||
const GLOBAL_CONFIG: InjectionToken<GlobalConfig> = new InjectionToken<GlobalConfig>('config');
|
const GLOBAL_CONFIG: InjectionToken<GlobalConfig> = new InjectionToken<GlobalConfig>('config');
|
||||||
|
|
||||||
@@ -55,6 +56,41 @@ if (envConfigFile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow to override a few important options by environment variables
|
||||||
|
function createServerConfig(host?: string, port?: string, nameSpace?: string, ssl?: string): ServerConfig {
|
||||||
|
const result = { host, nameSpace } as any;
|
||||||
|
|
||||||
|
if (hasValue(port)) {
|
||||||
|
result.port = Number(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValue(ssl)) {
|
||||||
|
result.ssl = ssl.trim().match(/^(true|1|yes)$/i) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processEnv = {
|
||||||
|
ui: createServerConfig(
|
||||||
|
process.env.DSPACE_HOST,
|
||||||
|
process.env.DSPACE_PORT,
|
||||||
|
process.env.DSPACE_NAMESPACE,
|
||||||
|
process.env.DSPACE_SSL),
|
||||||
|
rest: createServerConfig(
|
||||||
|
process.env.DSPACE_REST_HOST,
|
||||||
|
process.env.DSPACE_REST_PORT,
|
||||||
|
process.env.DSPACE_REST_NAMESPACE,
|
||||||
|
process.env.DSPACE_REST_SSL)
|
||||||
|
} as GlobalConfig;
|
||||||
|
|
||||||
|
// merge the environment variables with our configuration.
|
||||||
|
try {
|
||||||
|
merge(processEnv)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Unable to merge environment variable into the configuration')
|
||||||
|
}
|
||||||
|
|
||||||
buildBaseUrls();
|
buildBaseUrls();
|
||||||
|
|
||||||
// set config for whether running in production
|
// set config for whether running in production
|
||||||
|
@@ -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;
|
||||||
|
}
|
@@ -21,8 +21,6 @@ import { ENV_CONFIG } from './config';
|
|||||||
export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
|
export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const port = ENV_CONFIG.ui.port ? ENV_CONFIG.ui.port : 80;
|
|
||||||
|
|
||||||
if (ENV_CONFIG.production) {
|
if (ENV_CONFIG.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
@@ -90,7 +88,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
|
|||||||
https.createServer({
|
https.createServer({
|
||||||
key: keys.serviceKey,
|
key: keys.serviceKey,
|
||||||
cert: keys.certificate
|
cert: keys.certificate
|
||||||
}, app).listen(port, ENV_CONFIG.ui.host, () => {
|
}, app).listen(ENV_CONFIG.ui.port, ENV_CONFIG.ui.host, () => {
|
||||||
serverStarted();
|
serverStarted();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,7 +125,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.listen(port, ENV_CONFIG.ui.host, () => {
|
app.listen(ENV_CONFIG.ui.port, ENV_CONFIG.ui.host, () => {
|
||||||
serverStarted();
|
serverStarted();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
Reference in New Issue
Block a user