diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts
index e5986e5..300136b 100644
--- a/src/app/app-routes.ts
+++ b/src/app/app-routes.ts
@@ -3,6 +3,7 @@ import { LoginComponent } from './modules/login/login.component';
import { ErrorComponent } from './components/error/error.component';
import { VotingEventSelectComponent } from './components/voting-event-select/voting-event-select.component';
import { LoginVotingEventComponent } from './modules/login/login-voting-event/login-voting-event.component';
+import { NicknameComponent } from './modules/login/nickname/nickname.component';
export const appRoutes: Routes = [
{
@@ -26,6 +27,10 @@ export const appRoutes: Routes = [
path: 'login-voting-event',
component: LoginVotingEventComponent
},
+ {
+ path: 'nickname',
+ component: NicknameComponent
+ },
{
path: 'vote',
loadChildren: './modules/vote/vote.module#VoteModule'
diff --git a/src/app/app-session.service.ts b/src/app/app-session.service.ts
index 827442c..b464b5d 100644
--- a/src/app/app-session.service.ts
+++ b/src/app/app-session.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { VotingEvent } from 'src/app/models/voting-event';
import { Technology } from './models/technology';
+import { Credentials } from './models/credentials';
@Injectable({
providedIn: 'root'
@@ -9,6 +10,7 @@ export class AppSessionService {
private votingEvents: VotingEvent[];
private selectedVotingEvent: VotingEvent;
private selectedTechnology: Technology;
+ private credentials: Credentials;
constructor() {}
@@ -32,4 +34,11 @@ export class AppSessionService {
setSelectedTechnology(technology: Technology) {
this.selectedTechnology = technology;
}
+
+ getCredentials() {
+ return this.credentials;
+ }
+ setCredentials(credentials: Credentials) {
+ this.credentials = credentials;
+ }
}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 6876b4a..5f11298 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -5,7 +5,8 @@ import { BackendService } from './services/backend.service';
import { ErrorService } from './services/error.service';
import { AppSessionService } from './app-session.service';
import { getIdentificationRoute } from './utils/voting-event-flow.util';
-import { map } from 'rxjs/operators';
+import { map, tap } from 'rxjs/operators';
+import { ConfigurationService } from './services/configuration.service';
@Component({
selector: 'byor-root',
@@ -19,27 +20,39 @@ export class AppComponent implements OnInit {
private router: Router,
private backend: BackendService,
public errorService: ErrorService,
- private appSession: AppSessionService
+ private appSession: AppSessionService,
+ private configurationService: ConfigurationService
) {}
ngOnInit() {
- this.backend
- .getVotingEvents()
- .pipe(map((votingEvents) => votingEvents.filter((ve) => ve.status === 'open')))
- .subscribe((votingEvents) => {
- if (!votingEvents || votingEvents.length === 0) {
- this.errorService.setError(new Error('There are no Voting Events open'));
- this.router.navigate(['error']);
- } else if (votingEvents.length === 1) {
- const votingEvent = votingEvents[0];
- this.appSession.setSelectedVotingEvent(votingEvent);
- const route = getIdentificationRoute(votingEvent);
- this.router.navigate([route]);
- } else {
- this.appSession.setVotingEvents(votingEvents);
- this.router.navigate(['selectVotingEvent']);
- }
- });
+ this.configurationService
+ .defaultConfiguration()
+ .pipe(
+ tap((config) => {
+ if (config.enableVotingEventFlow) {
+ this.backend
+ .getVotingEvents()
+ .pipe(map((votingEvents) => votingEvents.filter((ve) => ve.status === 'open')))
+ .subscribe((votingEvents) => {
+ if (!votingEvents || votingEvents.length === 0) {
+ this.errorService.setError(new Error('There are no Voting Events open'));
+ this.router.navigate(['error']);
+ } else if (votingEvents.length === 1) {
+ const votingEvent = votingEvents[0];
+ this.appSession.setSelectedVotingEvent(votingEvent);
+ const route = getIdentificationRoute(votingEvent);
+ this.router.navigate([route]);
+ } else {
+ this.appSession.setVotingEvents(votingEvents);
+ this.router.navigate(['selectVotingEvent']);
+ }
+ });
+ } else {
+ this.router.navigate(['vote']);
+ }
+ })
+ )
+ .subscribe();
}
goToAdminPage() {
diff --git a/src/app/models/credentials.ts b/src/app/models/credentials.ts
new file mode 100644
index 0000000..5056be8
--- /dev/null
+++ b/src/app/models/credentials.ts
@@ -0,0 +1,4 @@
+export interface Credentials {
+ userId?: string;
+ nickname?: string;
+}
diff --git a/src/app/modules/login/login.module.ts b/src/app/modules/login/login.module.ts
index 997d0d9..e451f64 100644
--- a/src/app/modules/login/login.module.ts
+++ b/src/app/modules/login/login.module.ts
@@ -5,9 +5,10 @@ import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
import { AppMaterialModule } from '../../app-material.module';
import { LoginVotingEventComponent } from './login-voting-event/login-voting-event.component';
+import { NicknameComponent } from './nickname/nickname.component';
@NgModule({
- declarations: [LoginComponent, LoginVotingEventComponent],
+ declarations: [LoginComponent, LoginVotingEventComponent, NicknameComponent],
providers: [AuthGuard, AuthService],
imports: [CommonModule, AppMaterialModule],
exports: [LoginComponent]
diff --git a/src/app/modules/login/nickname/nickname.component.html b/src/app/modules/login/nickname/nickname.component.html
new file mode 100644
index 0000000..317c12a
--- /dev/null
+++ b/src/app/modules/login/nickname/nickname.component.html
@@ -0,0 +1,26 @@
+
+
+ Voting for event: {{appSession.getSelectedVotingEvent().name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/modules/login/nickname/nickname.component.scss b/src/app/modules/login/nickname/nickname.component.scss
new file mode 100644
index 0000000..ece851c
--- /dev/null
+++ b/src/app/modules/login/nickname/nickname.component.scss
@@ -0,0 +1,53 @@
+@import "../../../styles/abstracts/mixins";
+
+$login-sections-max-width: 480px;
+
+.nickname-section {
+ margin: 1.3rem auto;
+ padding: 0 1.3rem;
+ max-width: $login-sections-max-width;
+
+ .event-selection {
+ display: block;
+ }
+
+ mat-form-field {
+ width: 100%;
+ }
+
+ .nickname {
+ margin-bottom: 2rem;
+
+ .nickname-input {
+ width: 100%;
+ @include byor-input;
+ }
+
+ label {
+ display: block;
+ margin-bottom: .3rem;
+ }
+ }
+
+ .button-disabled {
+ border: none;
+ }
+
+ .button-text {
+ color: #ffffff;
+ }
+}
+
+.banner-section {
+ margin: 0 auto;
+ padding: 6rem 1.3rem;
+ max-width: $login-sections-max-width;
+ text-align: center;
+
+ a {
+ border-bottom: none;
+ img {
+ max-width: 100%;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/modules/login/nickname/nickname.component.spec.ts b/src/app/modules/login/nickname/nickname.component.spec.ts
new file mode 100644
index 0000000..c978ae4
--- /dev/null
+++ b/src/app/modules/login/nickname/nickname.component.spec.ts
@@ -0,0 +1,44 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NicknameComponent } from './nickname.component';
+import { RouterTestingModule } from '@angular/router/testing';
+import { AppMaterialModule } from 'src/app/app-material.module';
+import { HttpClientModule } from '@angular/common/http';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { VotingEvent } from 'src/app/models/voting-event';
+import { AppSessionService } from 'src/app/app-session.service';
+
+class MockAppSessionService {
+ private votingEvent: VotingEvent;
+
+ constructor() {
+ this.votingEvent = { _id: '123', name: 'an event', status: 'open', creationTS: 'abc' };
+ }
+
+ getSelectedVotingEvent() {
+ return this.votingEvent;
+ }
+}
+
+describe('NicknameComponent', () => {
+ let component: NicknameComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [NicknameComponent],
+ imports: [RouterTestingModule, AppMaterialModule, HttpClientModule, BrowserAnimationsModule],
+ providers: [{ provide: AppSessionService, useClass: MockAppSessionService }]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NicknameComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/modules/login/nickname/nickname.component.ts b/src/app/modules/login/nickname/nickname.component.ts
new file mode 100644
index 0000000..25ad127
--- /dev/null
+++ b/src/app/modules/login/nickname/nickname.component.ts
@@ -0,0 +1,108 @@
+import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { Observable, Subject, fromEvent, Subscription, NEVER } from 'rxjs';
+import { shareReplay, map, share, switchMap, tap, filter } from 'rxjs/operators';
+
+import { AppSessionService } from 'src/app/app-session.service';
+import { ConfigurationService } from 'src/app/services/configuration.service';
+import { BackendService } from 'src/app/services/backend.service';
+import { VoteCredentials } from 'src/app/models/vote-credentials';
+import { ErrorService } from 'src/app/services/error.service';
+import { logError } from 'src/app/utils/utils';
+
+@Component({
+ selector: 'byor-nickname',
+ templateUrl: './nickname.component.html',
+ styleUrls: ['./nickname.component.scss']
+})
+export class NicknameComponent implements AfterViewInit, OnDestroy, OnInit {
+ constructor(
+ private router: Router,
+ private errorService: ErrorService,
+ public appSession: AppSessionService,
+ private configurationService: ConfigurationService,
+ private backend: BackendService
+ ) {}
+
+ isValidInputData$: Observable;
+ message$ = new Subject();
+ configuration$: Observable;
+
+ goToVoteSubscription: Subscription;
+
+ @ViewChild('nickname') voterFirstName: ElementRef;
+ @ViewChild('startButton', { read: ElementRef }) startButtonRef: ElementRef;
+
+ ngOnInit() {
+ this.configuration$ = this.configurationService.defaultConfiguration().pipe(shareReplay(1));
+ }
+
+ ngAfterViewInit() {
+ // notify when nickname is changed
+ const nickname$ = fromEvent(this.voterFirstName.nativeElement, 'keyup').pipe(map(() => this.voterFirstName.nativeElement.value));
+
+ const startButtonClick$ = fromEvent(this.startButtonRef.nativeElement, 'click');
+
+ // the main subscription
+ this.goToVoteSubscription = this.goToVote$(nickname$, startButtonClick$).subscribe(
+ (nickname) => {
+ this.appSession.setCredentials({ nickname });
+ this.router.navigate(['vote/start']);
+ },
+ (error) => {
+ logError(error);
+ let _errMsg = error;
+ if (error.message) {
+ _errMsg = error.message;
+ }
+ this.errorService.setError(_errMsg);
+ this.errorService.setErrorMessage(error);
+ this.router.navigate(['error']);
+ }
+ );
+ }
+
+ ngOnDestroy() {
+ if (this.goToVoteSubscription) {
+ this.goToVoteSubscription.unsubscribe();
+ }
+ }
+
+ // this method takes in input all Observable created out of DOM events which are relevant for this Component
+ // in this way we can easily test this logic with marble tests
+ goToVote$(nickname$: Observable, startButtonClick$: Observable) {
+ // notifies when the input data provided changes - the value notified is true of false
+ // depending on the fact that the input data is valid or not
+ this.isValidInputData$ = nickname$.pipe(
+ map((nickname) => this.isNicknameValid(nickname)),
+ share() // share() is used since this Observable is used also on the Html template
+ );
+
+ const clickOnVote$ = nickname$.pipe(
+ switchMap((nickname) => (this.isNicknameValid(nickname) ? startButtonClick$.pipe(map(() => nickname)) : NEVER))
+ );
+
+ // notifies when the user has clicked to go to voting session and he has not voted yet
+ return clickOnVote$.pipe(
+ switchMap((nickname) => {
+ const votingEvent = this.appSession.getSelectedVotingEvent();
+ // to remove when we refactor hasAlreadyVoted
+ const oldCredentials: VoteCredentials = { voterId: { firstName: nickname, lastName: '' }, votingEvent };
+ return this.backend.hasAlreadyVoted(oldCredentials).pipe(
+ tap((hasAlreadyVoted) => {
+ if (hasAlreadyVoted) {
+ this.message$.next(`You have already voted for ${votingEvent.name}`);
+ }
+ }),
+ filter((hasAlreadyVoted) => !hasAlreadyVoted),
+ map(() => nickname)
+ );
+ })
+ );
+ }
+
+ isNicknameValid(nickname: string) {
+ return nickname && nickname.trim().length > 0;
+ }
+}
diff --git a/src/app/modules/vote/start-voting-session/start-voting-session.component.ts b/src/app/modules/vote/start-voting-session/start-voting-session.component.ts
index c884af8..9f59e4f 100644
--- a/src/app/modules/vote/start-voting-session/start-voting-session.component.ts
+++ b/src/app/modules/vote/start-voting-session/start-voting-session.component.ts
@@ -8,6 +8,7 @@ import { VoteService } from '../services/vote.service';
import { ErrorService } from 'src/app/services/error.service';
import { ConfigurationService } from 'src/app/services/configuration.service';
import { logError } from 'src/app/utils/utils';
+import { AppSessionService } from 'src/app/app-session.service';
@Component({
selector: 'byor-start-voting-session',
@@ -34,7 +35,8 @@ export class StartVotingSessionComponent implements AfterViewInit, OnDestroy, On
private router: Router,
private voteService: VoteService,
private errorService: ErrorService,
- private configurationService: ConfigurationService
+ private configurationService: ConfigurationService,
+ private appSession: AppSessionService
) {}
openVotingEvents$: Observable>;
@@ -83,6 +85,7 @@ export class StartVotingSessionComponent implements AfterViewInit, OnDestroy, On
.subscribe(
(credentials) => {
this.voteService.credentials = credentials;
+ this.appSession.setSelectedVotingEvent(credentials.votingEvent);
this.router.navigate(['vote/start']);
},
(error) => {
diff --git a/src/app/modules/vote/vote/vote.component.ts b/src/app/modules/vote/vote/vote.component.ts
index bd8fab2..fe2fcc5 100644
--- a/src/app/modules/vote/vote/vote.component.ts
+++ b/src/app/modules/vote/vote/vote.component.ts
@@ -23,6 +23,8 @@ import { Comment } from 'src/app/models/comment';
import { logError } from 'src/app/utils/utils';
import { AppSessionService } from 'src/app/app-session.service';
import { getActionName } from 'src/app/utils/voting-event-flow.util';
+import { ninvoke } from 'q';
+import { ConfigurationService } from 'src/app/services/configuration.service';
@Component({
selector: 'byor-vote',
@@ -68,7 +70,8 @@ export class VoteComponent implements AfterViewInit, OnDestroy {
private errorService: ErrorService,
public dialog: MatDialog,
private voteService: VoteService,
- private appSession: AppSessionService
+ private appSession: AppSessionService,
+ private configurationService: ConfigurationService
) {}
ngAfterViewInit() {
@@ -112,7 +115,8 @@ export class VoteComponent implements AfterViewInit, OnDestroy {
}
getTechnologies() {
- const votingEvent = this.appSession.getSelectedVotingEvent();
+ // @todo remove "|| this.voteService.credentials.votingEvent" once the enableVotingEventFlow toggle is removed
+ const votingEvent = this.appSession.getSelectedVotingEvent() || this.voteService.credentials.votingEvent;
return this.backEnd.getVotingEvent(votingEvent._id).pipe(
map((event) => {
let technologies = event.technologies;
@@ -146,7 +150,7 @@ export class VoteComponent implements AfterViewInit, OnDestroy {
}
createNewTechnology(name: string, quadrant: string) {
- const votingEvent = this.voteService.credentials.votingEvent;
+ const votingEvent = this.appSession.getSelectedVotingEvent();
const technology: Technology = {
name: name,
isnew: true,
@@ -189,7 +193,7 @@ export class VoteComponent implements AfterViewInit, OnDestroy {
}
goToConversation(technology: Technology) {
- this.voteService.technology = technology;
+ this.appSession.setSelectedTechnology(technology);
this.router.navigate(['vote/conversation']);
}
@@ -205,12 +209,22 @@ export class VoteComponent implements AfterViewInit, OnDestroy {
}
saveVotes() {
- this.backEnd.saveVote(this.votes, this.voteService.credentials).subscribe(
- (resp) => {
+ const credentials = this.appSession.getCredentials();
+ const votingEvent = this.appSession.getSelectedVotingEvent();
+ let voterIdentification;
+ let oldCredentials: VoteCredentials;
+ if (credentials) {
+ voterIdentification = credentials.nickname || credentials.userId;
+ oldCredentials = { voterId: { firstName: voterIdentification, lastName: '' }, votingEvent };
+ } else {
+ oldCredentials = this.voteService.credentials;
+ voterIdentification = oldCredentials.voterId.firstName + ' ' + oldCredentials.voterId.lastName;
+ }
+ combineLatest(this.backEnd.saveVote(this.votes, oldCredentials), this.configurationService.defaultConfiguration()).subscribe(
+ ([resp, config]) => {
if (resp.error) {
if (resp.error.errorCode === 'V-01') {
- const voterName = this.getVoterFirstLastName(this.voteService.credentials);
- this.messageVote = ` ${voterName} has already voted`;
+ this.messageVote = ` ${voterIdentification} has already voted`;
} else {
this.messageVote = `Vote could not be saved - look at the browser console
- maybe there is something there`;
@@ -221,7 +235,8 @@ export class VoteComponent implements AfterViewInit, OnDestroy {
});
dialogRef.afterClosed().subscribe((result) => {
- this.router.navigate(['/vote']);
+ const route = config.enableVotingEventFlow ? 'nickname' : '/vote';
+ this.router.navigate([route]);
});
}
},
diff --git a/src/app/utils/voting-event-flow.util.ts b/src/app/utils/voting-event-flow.util.ts
index 3801319..afce2f9 100644
--- a/src/app/utils/voting-event-flow.util.ts
+++ b/src/app/utils/voting-event-flow.util.ts
@@ -10,7 +10,7 @@ export function getIdentificationRoute(votingEvent: VotingEvent) {
if (identificationType === 'login') {
route = 'login-voting-event';
} else if (identificationType === 'nickname') {
- route = 'vote';
+ route = 'nickname';
} else {
throw new Error(`No route defined for identification type ${identificationType}`);
}