Set up ngrx store and added snackbar messages on error
This commit is contained in:
Generated
+78
@@ -18,6 +18,8 @@
|
|||||||
"@angular/platform-browser": "^16.1.0",
|
"@angular/platform-browser": "^16.1.0",
|
||||||
"@angular/platform-browser-dynamic": "^16.1.0",
|
"@angular/platform-browser-dynamic": "^16.1.0",
|
||||||
"@angular/router": "^16.1.0",
|
"@angular/router": "^16.1.0",
|
||||||
|
"@ngrx/effects": "^16.1.0",
|
||||||
|
"@ngrx/store": "^16.1.0",
|
||||||
"keycloak-angular": "^14.0.0",
|
"keycloak-angular": "^14.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
"@angular-devkit/build-angular": "^16.1.4",
|
"@angular-devkit/build-angular": "^16.1.4",
|
||||||
"@angular/cli": "~16.1.4",
|
"@angular/cli": "~16.1.4",
|
||||||
"@angular/compiler-cli": "^16.1.0",
|
"@angular/compiler-cli": "^16.1.0",
|
||||||
|
"@angular/localize": "^16.1.5",
|
||||||
"@types/jasmine": "~4.3.0",
|
"@types/jasmine": "~4.3.0",
|
||||||
"jasmine-core": "~4.6.0",
|
"jasmine-core": "~4.6.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
@@ -414,6 +417,29 @@
|
|||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/localize": {
|
||||||
|
"version": "16.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.1.5.tgz",
|
||||||
|
"integrity": "sha512-8ApTdmv4sH0VbW9kVNanze5DEmb3OPIGzbD19jzvUSb6mTVMfUcQrsf4h+H8+cT+epBhor8RgVeVbUJaUbaLNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "7.22.5",
|
||||||
|
"fast-glob": "3.2.12",
|
||||||
|
"yargs": "^17.2.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"localize-extract": "tools/bundles/src/extract/cli.js",
|
||||||
|
"localize-migrate": "tools/bundles/src/migrate/cli.js",
|
||||||
|
"localize-translate": "tools/bundles/src/translate/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.14.0 || >=18.10.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/compiler": "16.1.5",
|
||||||
|
"@angular/compiler-cli": "16.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular/material": {
|
"node_modules/@angular/material": {
|
||||||
"version": "16.1.5",
|
"version": "16.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.5.tgz",
|
||||||
@@ -3650,6 +3676,31 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ngrx/effects": {
|
||||||
|
"version": "16.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-16.1.0.tgz",
|
||||||
|
"integrity": "sha512-sdLzxjdQcYht3SYiuhchF4uOxxZIy3h9TEoyVp6PZJmiLJqhMAmV9/+s3tPD7yKd4+4o4n+HeiLDOTlMxn3Bxg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": "^16.0.0",
|
||||||
|
"@ngrx/store": "16.1.0",
|
||||||
|
"rxjs": "^6.5.3 || ^7.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ngrx/store": {
|
||||||
|
"version": "16.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngrx/store/-/store-16.1.0.tgz",
|
||||||
|
"integrity": "sha512-cGwI5wy+irIudQubfrbaKrDbJxCvu+lZYlBmsOlpzevQLCa+ZjNLrN05J025P3KvUmfzX4StEpAc8Ord089Kig==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": "^16.0.0",
|
||||||
|
"rxjs": "^6.5.3 || ^7.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ngtools/webpack": {
|
"node_modules/@ngtools/webpack": {
|
||||||
"version": "16.1.4",
|
"version": "16.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz",
|
||||||
@@ -12593,6 +12644,17 @@
|
|||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@angular/localize": {
|
||||||
|
"version": "16.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.1.5.tgz",
|
||||||
|
"integrity": "sha512-8ApTdmv4sH0VbW9kVNanze5DEmb3OPIGzbD19jzvUSb6mTVMfUcQrsf4h+H8+cT+epBhor8RgVeVbUJaUbaLNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/core": "7.22.5",
|
||||||
|
"fast-glob": "3.2.12",
|
||||||
|
"yargs": "^17.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@angular/material": {
|
"@angular/material": {
|
||||||
"version": "16.1.5",
|
"version": "16.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.5.tgz",
|
||||||
@@ -14998,6 +15060,22 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@ngrx/effects": {
|
||||||
|
"version": "16.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-16.1.0.tgz",
|
||||||
|
"integrity": "sha512-sdLzxjdQcYht3SYiuhchF4uOxxZIy3h9TEoyVp6PZJmiLJqhMAmV9/+s3tPD7yKd4+4o4n+HeiLDOTlMxn3Bxg==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@ngrx/store": {
|
||||||
|
"version": "16.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngrx/store/-/store-16.1.0.tgz",
|
||||||
|
"integrity": "sha512-cGwI5wy+irIudQubfrbaKrDbJxCvu+lZYlBmsOlpzevQLCa+ZjNLrN05J025P3KvUmfzX4StEpAc8Ord089Kig==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@ngtools/webpack": {
|
"@ngtools/webpack": {
|
||||||
"version": "16.1.4",
|
"version": "16.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
"@angular/platform-browser": "^16.1.0",
|
"@angular/platform-browser": "^16.1.0",
|
||||||
"@angular/platform-browser-dynamic": "^16.1.0",
|
"@angular/platform-browser-dynamic": "^16.1.0",
|
||||||
"@angular/router": "^16.1.0",
|
"@angular/router": "^16.1.0",
|
||||||
|
"@ngrx/effects": "^16.1.0",
|
||||||
|
"@ngrx/store": "^16.1.0",
|
||||||
"keycloak-angular": "^14.0.0",
|
"keycloak-angular": "^14.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
"@angular-devkit/build-angular": "^16.1.4",
|
"@angular-devkit/build-angular": "^16.1.4",
|
||||||
"@angular/cli": "~16.1.4",
|
"@angular/cli": "~16.1.4",
|
||||||
"@angular/compiler-cli": "^16.1.0",
|
"@angular/compiler-cli": "^16.1.0",
|
||||||
|
"@angular/localize": "^16.1.5",
|
||||||
"@types/jasmine": "~4.3.0",
|
"@types/jasmine": "~4.3.0",
|
||||||
"jasmine-core": "~4.6.0",
|
"jasmine-core": "~4.6.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
import {NgModule} from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import {RouterModule, Routes} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
import {DashboardComponent} from "./dashboard/dashboard.component";
|
import {DashboardComponent} from "./dashboard/dashboard.component";
|
||||||
import {ElectionsComponent} from "./elections/elections.component";
|
|
||||||
import {AppAuthGuard} from "./auth/auth.guard";
|
|
||||||
import {PermissionDeniedComponent} from "./permission-denied/permission-denied.component";
|
import {PermissionDeniedComponent} from "./permission-denied/permission-denied.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: DashboardComponent,
|
component: DashboardComponent,
|
||||||
canActivate: [AppAuthGuard],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'elections',
|
path: 'elections',
|
||||||
component: ElectionsComponent,
|
loadChildren: () => import("./elections/elections.module").then(mod => mod.ElectionsModule)
|
||||||
data: {
|
|
||||||
roles: ['USER']
|
|
||||||
},
|
|
||||||
canActivate: [AppAuthGuard]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'permission-denied',
|
path: 'permission-denied',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<app-navigation>
|
<app-navigation>
|
||||||
<app-messages></app-messages>
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</app-navigation>
|
</app-navigation>
|
||||||
|
|||||||
@@ -11,15 +11,17 @@ import {MatGridListModule} from "@angular/material/grid-list";
|
|||||||
import {MatIconModule} from "@angular/material/icon";
|
import {MatIconModule} from "@angular/material/icon";
|
||||||
import {MatListModule} from "@angular/material/list";
|
import {MatListModule} from "@angular/material/list";
|
||||||
import {KeycloakAngularModule, KeycloakService} from "keycloak-angular";
|
import {KeycloakAngularModule, KeycloakService} from "keycloak-angular";
|
||||||
import {ElectionsComponent} from './elections/elections.component';
|
|
||||||
import {PermissionDeniedComponent} from './permission-denied/permission-denied.component';
|
import {PermissionDeniedComponent} from './permission-denied/permission-denied.component';
|
||||||
import {MessagesComponent} from './messages/messages.component';
|
|
||||||
import {HttpClientModule} from "@angular/common/http";
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
import {environment} from "../environments/environment";
|
import {environment} from "../environments/environment";
|
||||||
import {NavigationComponent} from './navigation/navigation.component';
|
import {NavigationComponent} from './navigation/navigation.component';
|
||||||
import {MatSidenavModule} from '@angular/material/sidenav';
|
import {MatSidenavModule} from '@angular/material/sidenav';
|
||||||
import {MatMenuModule} from "@angular/material/menu";
|
import {MatMenuModule} from "@angular/material/menu";
|
||||||
import {MatTableModule} from "@angular/material/table";
|
import {MatTableModule} from "@angular/material/table";
|
||||||
|
import {StoreModule} from '@ngrx/store';
|
||||||
|
import {ElectionsModule} from "./elections/elections.module";
|
||||||
|
import {EffectsModule} from '@ngrx/effects';
|
||||||
|
import {MessagesModule} from "./messages/messages.module";
|
||||||
|
|
||||||
function initializeKeycloak(keycloak: KeycloakService) {
|
function initializeKeycloak(keycloak: KeycloakService) {
|
||||||
return () =>
|
return () =>
|
||||||
@@ -36,7 +38,7 @@ function initializeKeycloak(keycloak: KeycloakService) {
|
|||||||
flow: "standard"
|
flow: "standard"
|
||||||
},
|
},
|
||||||
shouldAddToken: (request) => {
|
shouldAddToken: (request) => {
|
||||||
const { url } = request;
|
const {url} = request;
|
||||||
return url.startsWith(environment.backendURL);
|
return url.startsWith(environment.backendURL);
|
||||||
},
|
},
|
||||||
loadUserProfileAtStartUp: true
|
loadUserProfileAtStartUp: true
|
||||||
@@ -47,9 +49,7 @@ function initializeKeycloak(keycloak: KeycloakService) {
|
|||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
DashboardComponent,
|
DashboardComponent,
|
||||||
ElectionsComponent,
|
|
||||||
PermissionDeniedComponent,
|
PermissionDeniedComponent,
|
||||||
MessagesComponent,
|
|
||||||
NavigationComponent
|
NavigationComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
@@ -66,7 +66,11 @@ function initializeKeycloak(keycloak: KeycloakService) {
|
|||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatTableModule
|
MatTableModule,
|
||||||
|
ElectionsModule,
|
||||||
|
MessagesModule,
|
||||||
|
StoreModule.forRoot({}, {}),
|
||||||
|
EffectsModule.forRoot([])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {MatTableDataSource} from "@angular/material/table";
|
import {MatTableDataSource} from "@angular/material/table";
|
||||||
import {ElectionService} from "../elections/election.service";
|
import {ElectionService} from "../elections/election.service";
|
||||||
import {Election} from "../elections/election";
|
import {Election} from "../elections/model/election";
|
||||||
import {Subscription} from "rxjs";
|
import {Subscription} from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {HttpClient, HttpHeaders} from "@angular/common/http";
|
import {HttpClient, HttpHeaders} from "@angular/common/http";
|
||||||
import {environment} from "../../environments/environment";
|
import {environment} from "../../environments/environment";
|
||||||
import {Election} from "./election";
|
import {Election} from "./model/election";
|
||||||
import {catchError, Observable, of, tap} from "rxjs";
|
import {catchError, Observable, of} from "rxjs";
|
||||||
import {MessageService} from "../messages/message.service";
|
import {Store} from "@ngrx/store";
|
||||||
|
import {electionByName} from "./store";
|
||||||
|
import {MessagesService} from "../messages/messages.service";
|
||||||
|
import {MessageType} from "../messages/model/message-type";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -17,16 +20,20 @@ export class ElectionService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(private http: HttpClient,
|
constructor(private http: HttpClient,
|
||||||
private messageService: MessageService) { }
|
private store: Store,
|
||||||
|
private messageService: MessagesService) { }
|
||||||
|
|
||||||
getElections(): Observable<Election[]> {
|
getElections(): Observable<Election[]> {
|
||||||
return this.http.get<Election[]>(this.electionsURL)
|
return this.http.get<Election[]>(this.electionsURL, this.httpOptions)
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(_ => this.log('fetched elections')),
|
|
||||||
catchError(this.handleError<Election[]>('getElections', []))
|
catchError(this.handleError<Election[]>('getElections', []))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectElection(name: string) {
|
||||||
|
return this.store.select(electionByName(name));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Http operation that failed.
|
* Handle Http operation that failed.
|
||||||
* Let the app continue.
|
* Let the app continue.
|
||||||
@@ -39,16 +46,22 @@ export class ElectionService {
|
|||||||
// TODO: send the error to remote logging infrastructure
|
// TODO: send the error to remote logging infrastructure
|
||||||
console.error(error); // log to console instead
|
console.error(error); // log to console instead
|
||||||
|
|
||||||
// TODO: better job of transforming error for user consumption
|
if (error.status == 401) {
|
||||||
this.log(`${operation} failed: ${error.message}`);
|
this.log(MessageType.UNAUTHENTICATED);
|
||||||
|
}
|
||||||
|
if (error.status == 403) {
|
||||||
|
this.log(MessageType.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
if (error.status == 500) {
|
||||||
|
this.log(MessageType.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
// Let the app keep running by returning an empty result.
|
// Let the app keep running by returning an empty result.
|
||||||
return of(result as T);
|
return of(result as T);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Log a HeroService message with the MessageService */
|
private log(type: MessageType) {
|
||||||
private log(message: string) {
|
this.messageService.logMessage('ElectionService', type);
|
||||||
this.messageService.add(`ElectionService: ${message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {RouterModule, Routes} from "@angular/router";
|
||||||
|
import {ElectionsComponent} from "./elections.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ElectionsComponent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class ElectionsRoutingModule {
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {KeycloakService} from "keycloak-angular";
|
import {KeycloakService} from "keycloak-angular";
|
||||||
import {Election} from "./election";
|
import {Election} from "./model/election";
|
||||||
import {ElectionService} from "./election.service";
|
|
||||||
import {Observable} from "rxjs";
|
import {Observable} from "rxjs";
|
||||||
|
import {Store} from "@ngrx/store";
|
||||||
|
import {allElections} from "./store";
|
||||||
|
import {loadAllElectionsAction} from "./store/elections.actions";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-elections',
|
selector: 'app-elections',
|
||||||
@@ -10,12 +12,13 @@ import {Observable} from "rxjs";
|
|||||||
styleUrls: ['./elections.component.scss']
|
styleUrls: ['./elections.component.scss']
|
||||||
})
|
})
|
||||||
export class ElectionsComponent implements OnInit{
|
export class ElectionsComponent implements OnInit{
|
||||||
$elections: Observable<Election[]> = this.electionService.getElections();
|
$elections: Observable<Election[]> = this.store.select<Election[]>(allElections());
|
||||||
|
|
||||||
constructor(private keycloakService: KeycloakService,
|
constructor(private keycloakService: KeycloakService,
|
||||||
private electionService: ElectionService) {
|
private store: Store) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.store.dispatch(loadAllElectionsAction());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {ElectionsComponent} from "./elections.component";
|
||||||
|
import {StoreModule} from "@ngrx/store";
|
||||||
|
import {ElectionsEffects} from "./store/elections.effects";
|
||||||
|
import {EffectsModule} from "@ngrx/effects";
|
||||||
|
import {electionsReducers, featureStateName} from "./store";
|
||||||
|
import {ElectionsRoutingModule} from "./elections-routing.module";
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
ElectionsComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ElectionsRoutingModule,
|
||||||
|
StoreModule.forFeature(featureStateName, electionsReducers),
|
||||||
|
EffectsModule.forFeature([ElectionsEffects]),
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ElectionsComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ElectionsModule {
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {createAction, props} from "@ngrx/store";
|
||||||
|
import {Election} from "../model/election";
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
LoadAllElections = '[Elections] Load All Elections',
|
||||||
|
LoadAllElectionsFinished = '[Elections] Load All Elections Finished',
|
||||||
|
|
||||||
|
LoadSingleElection = '[Elections] Load Single Election',
|
||||||
|
LoadSingleElectionFinished = '[Elections] Load Single Election Finished',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAllElectionsAction = createAction(
|
||||||
|
ActionTypes.LoadAllElections
|
||||||
|
)
|
||||||
|
|
||||||
|
export type LoadAllElectionsAction = ReturnType<typeof loadAllElectionsAction>;
|
||||||
|
|
||||||
|
export const loadAllElectionsFinishedAction = createAction(
|
||||||
|
ActionTypes.LoadAllElectionsFinished,
|
||||||
|
props<{payload: Election[]}>()
|
||||||
|
)
|
||||||
|
|
||||||
|
export type LoadAllElectionsFinishedAction = ReturnType<typeof loadAllElectionsFinishedAction>;
|
||||||
|
|
||||||
|
export const loadSingleElectionAction = createAction(
|
||||||
|
ActionTypes.LoadSingleElection,
|
||||||
|
props<{payload: string}>()
|
||||||
|
)
|
||||||
|
|
||||||
|
export type LoadSingleElectionAction = ReturnType<typeof loadSingleElectionAction>;
|
||||||
|
|
||||||
|
export const loadSingleElectionFinishedAction = createAction(
|
||||||
|
ActionTypes.LoadSingleElectionFinished,
|
||||||
|
props<{payload: Election|undefined}>()
|
||||||
|
)
|
||||||
|
|
||||||
|
export type LoadSingleElectionFinishedAction = ReturnType<typeof loadSingleElectionFinishedAction>;
|
||||||
|
|
||||||
|
export type ElectionsActions =
|
||||||
|
| LoadAllElectionsAction
|
||||||
|
| LoadAllElectionsFinishedAction
|
||||||
|
| LoadSingleElectionAction
|
||||||
|
| LoadSingleElectionFinishedAction;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import {ElectionService} from "../election.service";
|
||||||
|
import {
|
||||||
|
ActionTypes,
|
||||||
|
loadAllElectionsFinishedAction,
|
||||||
|
LoadSingleElectionAction,
|
||||||
|
loadSingleElectionFinishedAction
|
||||||
|
} from "./elections.actions";
|
||||||
|
import {map, switchMap} from "rxjs";
|
||||||
|
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ElectionsEffects {
|
||||||
|
constructor(private actions$: Actions,
|
||||||
|
private electionService: ElectionService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAllElections$ = createEffect(() => this.actions$.pipe(
|
||||||
|
ofType(ActionTypes.LoadAllElections),
|
||||||
|
switchMap(() => this.electionService.getElections()
|
||||||
|
.pipe(
|
||||||
|
map(elections => loadAllElectionsFinishedAction({payload: elections}))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
loadSingleElection$ = createEffect(() => {
|
||||||
|
return this.actions$.pipe(
|
||||||
|
ofType(ActionTypes.LoadSingleElection),
|
||||||
|
switchMap((action: LoadSingleElectionAction) => this.electionService.selectElection(action.payload)
|
||||||
|
.pipe(
|
||||||
|
map(election =>
|
||||||
|
loadSingleElectionFinishedAction({payload: election}))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {Election} from "../model/election";
|
||||||
|
import {
|
||||||
|
loadAllElectionsFinishedAction,
|
||||||
|
loadSingleElectionFinishedAction
|
||||||
|
} from "./elections.actions";
|
||||||
|
import {createReducer, on} from "@ngrx/store";
|
||||||
|
|
||||||
|
export interface ReducerElectionsState {
|
||||||
|
items: Election[];
|
||||||
|
selectedItem: Election|undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState: ReducerElectionsState = {
|
||||||
|
items: [],
|
||||||
|
selectedItem: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
export const electionsReducer = createReducer(
|
||||||
|
initialState,
|
||||||
|
on(loadAllElectionsFinishedAction, (state,
|
||||||
|
action) => ({
|
||||||
|
...state,
|
||||||
|
items: [...action.payload]
|
||||||
|
})),
|
||||||
|
on(loadSingleElectionFinishedAction, (state,
|
||||||
|
action) => ({
|
||||||
|
...state,
|
||||||
|
selectedItem: action.payload
|
||||||
|
}))
|
||||||
|
);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {electionsReducer, ReducerElectionsState} from "./elections.reducer";
|
||||||
|
import {ActionReducerMap, createFeatureSelector, createSelector} from "@ngrx/store";
|
||||||
|
|
||||||
|
export const featureStateName = 'electionsFeature';
|
||||||
|
|
||||||
|
export interface ElectionsState {
|
||||||
|
elections: ReducerElectionsState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const electionsReducers: ActionReducerMap<ElectionsState> = {
|
||||||
|
elections: electionsReducer,
|
||||||
|
};
|
||||||
|
|
||||||
|
// extract the main property 'electionsFeature' from the state object
|
||||||
|
export const getElectionsFeatureState = createFeatureSelector<ElectionsState>(
|
||||||
|
featureStateName
|
||||||
|
);
|
||||||
|
|
||||||
|
export const allElections = () => createSelector(
|
||||||
|
getElectionsFeatureState,
|
||||||
|
(state: ElectionsState) => state.elections.items
|
||||||
|
);
|
||||||
|
|
||||||
|
export const electionByName = (name: string) => createSelector(
|
||||||
|
getElectionsFeatureState,
|
||||||
|
(state: ElectionsState) =>
|
||||||
|
state.elections.items.find(election => election.name === name)
|
||||||
|
);
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import {Injectable} from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class MessageService {
|
|
||||||
messages: string[] = [];
|
|
||||||
|
|
||||||
add(message: string) {
|
|
||||||
this.messages.push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.messages = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<div *ngIf="messageService.messages.length">
|
|
||||||
|
|
||||||
<h2>Messages</h2>
|
|
||||||
<button class="clear"
|
|
||||||
(click)="messageService.clear()">clear</button>
|
|
||||||
<div *ngFor='let message of messageService.messages'> {{message}} </div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
h2 {
|
|
||||||
color: red;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 2em;
|
|
||||||
}
|
|
||||||
body, input[text], button {
|
|
||||||
color: crimson;
|
|
||||||
font-family: Cambria, Georgia, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.clear {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #eee;
|
|
||||||
border: none;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
background-color: #cfd8dc;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
background-color: #eee;
|
|
||||||
color: #aaa;
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
button.clear {
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
|
||||||
|
|
||||||
import {MessagesComponent} from './messages.component';
|
|
||||||
|
|
||||||
describe('MessagesComponent', () => {
|
|
||||||
let component: MessagesComponent;
|
|
||||||
let fixture: ComponentFixture<MessagesComponent>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [MessagesComponent]
|
|
||||||
});
|
|
||||||
fixture = TestBed.createComponent(MessagesComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import {Component} from '@angular/core';
|
|
||||||
import {MessageService} from "./message.service";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-messages',
|
|
||||||
templateUrl: './messages.component.html',
|
|
||||||
styleUrls: ['./messages.component.scss']
|
|
||||||
})
|
|
||||||
export class MessagesComponent {
|
|
||||||
constructor(public messageService: MessageService) {}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {StoreModule} from "@ngrx/store";
|
||||||
|
import {featureStateName, messagesReducers} from "./store";
|
||||||
|
import {EffectsModule} from "@ngrx/effects";
|
||||||
|
import {MessagesEffects} from "./store/messages.effects";
|
||||||
|
import {MatSnackBarModule} from "@angular/material/snack-bar";
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
StoreModule.forFeature(featureStateName, messagesReducers),
|
||||||
|
EffectsModule.forFeature([MessagesEffects])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class MessagesModule {
|
||||||
|
}
|
||||||
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {MessageService} from './message.service';
|
import {MessagesService} from './messages.service';
|
||||||
|
|
||||||
describe('MessageService', () => {
|
describe('MessagesService', () => {
|
||||||
let service: MessageService;
|
let service: MessagesService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(MessageService);
|
service = TestBed.inject(MessagesService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {Store} from "@ngrx/store";
|
||||||
|
import {addMessageAction} from "./store/messages.actions";
|
||||||
|
import {MessageType} from "./model/message-type";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MessagesService {
|
||||||
|
private static UNAUTHENTICATED = $localize `You are not logged in which prevents data from loading`;
|
||||||
|
private static UNAUTHORIZED = $localize `You don't have sufficient authorization to view the data`;
|
||||||
|
private static INTERNAL_SERVER_ERROR = $localize `An internal server error occurred. Sorry for the inconvenience.`;
|
||||||
|
private static UNKNOWN_ERROR = $localize `An unknown error occurred. Sorry for the inconvenience.`;
|
||||||
|
|
||||||
|
constructor(private store: Store) { }
|
||||||
|
|
||||||
|
logMessage(component: string, type: MessageType) {
|
||||||
|
let text = component + ": ";
|
||||||
|
switch (type) {
|
||||||
|
case MessageType.UNAUTHENTICATED:
|
||||||
|
text += MessagesService.UNAUTHENTICATED;
|
||||||
|
break;
|
||||||
|
case MessageType.UNAUTHORIZED:
|
||||||
|
text += MessagesService.UNAUTHORIZED;
|
||||||
|
break;
|
||||||
|
case MessageType.INTERNAL_SERVER_ERROR:
|
||||||
|
text += MessagesService.INTERNAL_SERVER_ERROR;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text += MessagesService.UNKNOWN_ERROR;
|
||||||
|
}
|
||||||
|
this.store.dispatch(addMessageAction({message: {text, type}}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum MessageType {
|
||||||
|
UNAUTHENTICATED,
|
||||||
|
UNAUTHORIZED,
|
||||||
|
INTERNAL_SERVER_ERROR
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {MessageType} from "./message-type";
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
text: string;
|
||||||
|
type: MessageType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {messagesReducer, ReducerMessagesState} from "./messages.reducer";
|
||||||
|
import {ActionReducerMap, createFeatureSelector} from "@ngrx/store";
|
||||||
|
|
||||||
|
export const featureStateName = 'messagesFeature';
|
||||||
|
|
||||||
|
export interface MessagesState {
|
||||||
|
messages: ReducerMessagesState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messagesReducers: ActionReducerMap<MessagesState> = {
|
||||||
|
messages: messagesReducer,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMessagesFeatureState = createFeatureSelector<MessagesState>(
|
||||||
|
featureStateName
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {createAction, props} from "@ngrx/store";
|
||||||
|
import {Message} from "../model/message";
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
AddMessage = '[Messages] Add Message',
|
||||||
|
AddMessageFinished = '[Messages] Add Message Finished',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addMessageAction = createAction(
|
||||||
|
ActionTypes.AddMessage,
|
||||||
|
props<{message: Message}>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export type AddMessageAction = ReturnType<typeof addMessageAction>;
|
||||||
|
|
||||||
|
export const addMessageFinishedAction = createAction(
|
||||||
|
ActionTypes.AddMessageFinished
|
||||||
|
);
|
||||||
|
|
||||||
|
export type AddMessageFinishedAction = ReturnType<typeof addMessageFinishedAction>;
|
||||||
|
|
||||||
|
export type MessagesActions =
|
||||||
|
| AddMessageAction
|
||||||
|
| AddMessageFinishedAction;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {MatSnackBar} from "@angular/material/snack-bar";
|
||||||
|
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||||
|
import {ActionTypes, AddMessageAction, addMessageFinishedAction} from "./messages.actions";
|
||||||
|
import {map} from "rxjs";
|
||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MessagesEffects {
|
||||||
|
constructor(private actions$: Actions,
|
||||||
|
private snackBar: MatSnackBar) {
|
||||||
|
}
|
||||||
|
|
||||||
|
showSnackbar$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(ActionTypes.AddMessage),
|
||||||
|
map((action: AddMessageAction) => {
|
||||||
|
this.snackBar.open(action.message.text, "OK");
|
||||||
|
return addMessageFinishedAction();
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {Message} from "../model/message";
|
||||||
|
import {createReducer, on} from "@ngrx/store";
|
||||||
|
import {addMessageAction, addMessageFinishedAction} from "./messages.actions";
|
||||||
|
|
||||||
|
export interface ReducerMessagesState {
|
||||||
|
displayedMessage: Message|undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ReducerMessagesState = {
|
||||||
|
displayedMessage: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messagesReducer = createReducer(
|
||||||
|
initialState,
|
||||||
|
on(addMessageAction, (state,
|
||||||
|
action) => ({
|
||||||
|
...state,
|
||||||
|
displayedMessage: action.message
|
||||||
|
})),
|
||||||
|
on(addMessageFinishedAction, (state) => ({
|
||||||
|
...state,
|
||||||
|
displayedMessage: undefined
|
||||||
|
}))
|
||||||
|
);
|
||||||
@@ -20,8 +20,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<span>Wahlrecht</span>
|
<span>Wahlrecht</span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
<ng-container *ngIf="isLoggedIn$ | async; else loginButton">
|
||||||
<span class="logged-user">Logged in as {{loggedUserName}}</span>
|
<span class="logged-user">Logged in as {{loggedUserName}}</span>
|
||||||
<button mat-raised-button class="app-nav-icon" (click)="logout()">Logout</button>
|
<button mat-raised-button class="app-nav-icon" (click)="logout()">Logout</button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loginButton>
|
||||||
|
<button mat-raised-button class="app-nav-icon" (click)="login()" i18n>Login</button>
|
||||||
|
</ng-template>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {KeycloakService} from "keycloak-angular";
|
import {KeycloakService} from "keycloak-angular";
|
||||||
|
import {from} from "rxjs";
|
||||||
|
import {ActivatedRoute} from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navigation',
|
selector: 'app-navigation',
|
||||||
@@ -8,13 +10,24 @@ import {KeycloakService} from "keycloak-angular";
|
|||||||
})
|
})
|
||||||
export class NavigationComponent {
|
export class NavigationComponent {
|
||||||
loggedUserName = '';
|
loggedUserName = '';
|
||||||
|
isLoggedIn$ = from(this.keycloakService.isLoggedIn());
|
||||||
|
url: string;
|
||||||
|
|
||||||
constructor(private keycloakService: KeycloakService) { }
|
constructor(private keycloakService: KeycloakService,
|
||||||
|
private route: ActivatedRoute) {
|
||||||
|
this.url = route.snapshot.url.join('');
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loggedUserName = this.keycloakService.getUsername();
|
this.loggedUserName = this.keycloakService.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login(): void {
|
||||||
|
this.keycloakService.login({
|
||||||
|
redirectUri: window.location.origin + this.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.keycloakService.logout(window.location.origin);
|
this.keycloakService.logout(window.location.origin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {ElectionsState} from "../elections/store";
|
||||||
|
import {MessagesState} from "../messages/store";
|
||||||
|
|
||||||
|
export interface AppState {
|
||||||
|
electionsFeature: ElectionsState;
|
||||||
|
messagesFeature: MessagesState;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/// <reference types="@angular/localize" />
|
||||||
|
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
import {AppModule} from './app/app.module';
|
import {AppModule} from './app/app.module';
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/app",
|
"outDir": "./out-tsc/app",
|
||||||
"types": []
|
"types": [
|
||||||
|
"@angular/localize"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/main.ts"
|
"src/main.ts"
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": [
|
||||||
"jasmine"
|
"jasmine",
|
||||||
|
"@angular/localize"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
Reference in New Issue
Block a user