Wait for server while voting

Blocks voting state changes and prevents the user from sending multiple
vote values while the server is not responding during voting
This commit is contained in:
Sean 2020-06-03 14:53:31 +02:00
parent 7665634d42
commit dced8fbcc7
11 changed files with 117 additions and 34 deletions

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="poll">
<ng-container *ngIf="vmanager.canVote(poll) && !alreadyVoted; else cannotVote">
<ng-container *ngIf="vmanager.canVote(poll) && !alreadyVoted && !deliveringVote; else cannotVote">
<!-- Poll hint -->
<p *ngIf="pollHint">
<i>{{ pollHint }}</i>
@ -9,9 +9,7 @@
<h4 *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1">
{{ 'Available votes' | translate }}:
<b>
{{ getVotesAvailable() }}/{{ poll.votes_amount }}
</b>
<b> {{ getVotesAvailable() }}/{{ poll.votes_amount }} </b>
</h4>
<!-- Options and Actions -->
@ -39,6 +37,7 @@
class="vote-button"
mat-raised-button
(click)="saveSingleVote(option.id, action.vote)"
[disabled]="deliveringVote"
[ngClass]="
voteRequestData.votes[option.id] === action.vote ||
voteRequestData.votes[option.id] === 1
@ -67,6 +66,7 @@
mat-raised-button
(click)="saveGlobalVote('N')"
[ngClass]="voteRequestData.global === 'N' ? 'voted-no' : ''"
[disabled]="deliveringVote"
>
<mat-icon> thumb_down </mat-icon>
</button>
@ -103,13 +103,19 @@
<ng-template #cannotVote>
<div class="centered-button-wrapper">
<div>
<div *ngIf="!deliveringVote">
<mat-icon class="vote-submitted">
check_circle
</mat-icon>
<br />
<span>{{ 'Voting successful.' | translate }}</span>
</div>
<div *ngIf="deliveringVote" class="submit-vote-indicator">
<mat-spinner class="small-spinner"></mat-spinner>
<br />
<span>{{ 'Delivering vote... Please wait!' | translate }}</span>
</div>
</div>
</ng-template>

View File

@ -67,3 +67,10 @@
.mat-divider-horizontal {
position: initial;
}
.submit-vote-indicator {
text-align: center;
.mat-spinner {
margin: auto;
}
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
@ -29,7 +29,8 @@ interface VoteActions {
@Component({
selector: 'os-assignment-poll-vote',
templateUrl: './assignment-poll-vote.component.html',
styleUrls: ['./assignment-poll-vote.component.scss']
styleUrls: ['./assignment-poll-vote.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
public AssignmentPollMethod = AssignmentPollMethod;
@ -47,7 +48,8 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
operator: OperatorService,
public vmanager: VotingService,
private pollRepo: AssignmentPollRepositoryService,
private promptService: PromptService
private promptService: PromptService,
private cd: ChangeDetectorRef
) {
super(title, translate, matSnackbar, operator);
}
@ -58,6 +60,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
this.defineVoteOptions();
} else {
this.alreadyVoted = true;
this.cd.markForCheck();
}
}
@ -104,20 +107,24 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
return !!this.voteRequestData.global;
}
public submitVote(): void {
public async submitVote(): Promise<void> {
const title = this.translate.instant('Submit selection now?');
const content = this.translate.instant('Your decision cannot be changed afterwards.');
this.promptService.open(title, content).then(confirmed => {
const confirmed = await this.promptService.open(title, content);
if (confirmed) {
this.deliveringVote = true;
this.cd.markForCheck();
this.pollRepo
.vote(this.voteRequestData, this.poll.id)
.then(() => {
this.alreadyVoted = true;
})
.catch(this.raiseError);
}
.catch(this.raiseError)
.finally(() => {
this.deliveringVote = false;
});
}
}
public saveSingleVote(optionId: number, vote: VoteValue): void {
if (this.isGlobalOptionSelected()) {

View File

@ -42,10 +42,16 @@
mat-stroked-button
[ngClass]="pollStateActions[poll.state].css"
(click)="changeState(poll.nextState)"
[disabled]="stateChangePending"
>
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
<span class="next-state-label">
<ng-container *ngIf="!stateChangePending">
{{ poll.nextStateActionVerbose | translate }}
</ng-container>
<ng-container *ngIf="stateChangePending">
{{ 'In progress, please wait...' | translate }}
</ng-container>
</span>
</button>
</div>

View File

@ -2,19 +2,26 @@
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
<div *ngIf="vmanager.canVote(poll)" class="vote-button-grid">
<div *ngIf="vmanager.canVote(poll) && !deliveringVote" class="vote-button-grid">
<!-- Voting -->
<div class="vote-button" *ngFor="let option of voteOptions">
<button
mat-raised-button
(click)="saveVote(option.vote)"
[ngClass]="currentVote && currentVote.vote === option.vote ? option.css : ''"
[disabled]="deliveringVote"
>
<mat-icon> {{ option.icon }}</mat-icon>
</button>
<span class="vote-label"> {{ option.label | translate }} </span>
</div>
</div>
<div *ngIf="deliveringVote" class="submit-vote-indicator">
<mat-spinner class="small-spinner"></mat-spinner>
<br />
<span>{{ 'Delivering vote... Please wait!' | translate }}</span>
</div>
</ng-container>
<ng-template #userHasVotes>

View File

@ -8,6 +8,14 @@
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
.submit-vote-indicator {
margin-top: 1em;
text-align: center;
.mat-spinner {
margin: auto;
}
}
.vote-button {
display: inline-grid;
grid-gap: 1em;

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
@ -22,7 +22,8 @@ interface VoteOption {
@Component({
selector: 'os-motion-poll-vote',
templateUrl: './motion-poll-vote.component.html',
styleUrls: ['./motion-poll-vote.component.scss']
styleUrls: ['./motion-poll-vote.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPoll> {
public currentVote: VoteOption = {};
@ -54,19 +55,28 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
operator: OperatorService,
public vmanager: VotingService,
private pollRepo: MotionPollRepositoryService,
private promptService: PromptService
private promptService: PromptService,
private cd: ChangeDetectorRef
) {
super(title, translate, matSnackbar, operator);
}
public saveVote(vote: VoteValue): void {
public async saveVote(vote: VoteValue): Promise<void> {
this.currentVote.vote = vote;
const title = this.translate.instant('Submit selection now?');
const content = this.translate.instant('Your decision cannot be changed afterwards.');
this.promptService.open(title, content).then(confirmed => {
const confirmed = await this.promptService.open(title, content);
if (confirmed) {
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
}
this.deliveringVote = true;
this.cd.markForCheck();
this.pollRepo
.vote(vote, this.poll.id)
.catch(this.raiseError)
.finally(() => {
this.deliveringVote = false;
});
}
}
}

View File

@ -42,10 +42,15 @@
<!-- Change state button -->
<div *osPerms="'motions.can_manage_polls'; and: !hideChangeState">
<button mat-stroked-button [ngClass]="pollStateActions[poll.state].css" (click)="changeState(poll.nextState)">
<button mat-stroked-button [ngClass]="pollStateActions[poll.state].css" (click)="changeState(poll.nextState)" [disabled]="stateChangePending">
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
<span class="next-state-label">
<ng-container *ngIf="!stateChangePending">
{{ poll.nextStateActionVerbose | translate }}
</ng-container>
<ng-container *ngIf="stateChangePending">
{{ 'In progress, please wait...' | translate }}
</ng-container>
</span>
</button>
</div>

View File

@ -16,6 +16,8 @@ export abstract class BasePollVoteComponent<V extends ViewBasePoll> extends Base
public votingErrors = VotingError;
public deliveringVote = false;
protected user: ViewUser;
public constructor(

View File

@ -15,6 +15,8 @@ import { PollService } from '../services/poll.service';
import { ViewBasePoll } from '../models/view-base-poll';
export abstract class BasePollComponent<V extends ViewBasePoll, S extends PollService> extends BaseViewComponent {
public stateChangePending = false;
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject([]);
protected _poll: V;
@ -55,10 +57,22 @@ export abstract class BasePollComponent<V extends ViewBasePoll, S extends PollSe
const title = this.translate.instant('Are you sure you want to reset this vote?');
const content = this.translate.instant('All votes will be lost.');
if (await this.promptService.open(title, content)) {
this.repo.resetPoll(this._poll).catch(this.raiseError);
this.stateChangePending = true;
this.repo
.resetPoll(this._poll)
.catch(this.raiseError)
.finally(() => {
this.stateChangePending = false;
});
}
} else {
this.repo.changePollState(this._poll).catch(this.raiseError);
this.stateChangePending = true;
this.repo
.changePollState(this._poll)
.catch(this.raiseError)
.finally(() => {
this.stateChangePending = false;
});
}
}

View File

@ -778,6 +778,17 @@ button.mat-menu-item.selected {
display: none !important; /* hide scrollbars in webkit browsers */
}
.small-spinner {
// 24px is the size of a normal icon
$spinner-size: 24px;
height: $spinner-size !important;
height: $spinner-size !important;
svg {
height: $spinner-size !important;
height: $spinner-size !important;
}
}
.import-table {
.table-container {
width: 100%;