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:
parent
7665634d42
commit
dced8fbcc7
@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngIf="poll">
|
<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 -->
|
<!-- Poll hint -->
|
||||||
<p *ngIf="pollHint">
|
<p *ngIf="pollHint">
|
||||||
<i>{{ pollHint }}</i>
|
<i>{{ pollHint }}</i>
|
||||||
@ -9,9 +9,7 @@
|
|||||||
<h4 *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1">
|
<h4 *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1">
|
||||||
{{ 'Available votes' | translate }}:
|
{{ 'Available votes' | translate }}:
|
||||||
|
|
||||||
<b>
|
<b> {{ getVotesAvailable() }}/{{ poll.votes_amount }} </b>
|
||||||
{{ getVotesAvailable() }}/{{ poll.votes_amount }}
|
|
||||||
</b>
|
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<!-- Options and Actions -->
|
<!-- Options and Actions -->
|
||||||
@ -39,6 +37,7 @@
|
|||||||
class="vote-button"
|
class="vote-button"
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
(click)="saveSingleVote(option.id, action.vote)"
|
(click)="saveSingleVote(option.id, action.vote)"
|
||||||
|
[disabled]="deliveringVote"
|
||||||
[ngClass]="
|
[ngClass]="
|
||||||
voteRequestData.votes[option.id] === action.vote ||
|
voteRequestData.votes[option.id] === action.vote ||
|
||||||
voteRequestData.votes[option.id] === 1
|
voteRequestData.votes[option.id] === 1
|
||||||
@ -67,6 +66,7 @@
|
|||||||
mat-raised-button
|
mat-raised-button
|
||||||
(click)="saveGlobalVote('N')"
|
(click)="saveGlobalVote('N')"
|
||||||
[ngClass]="voteRequestData.global === 'N' ? 'voted-no' : ''"
|
[ngClass]="voteRequestData.global === 'N' ? 'voted-no' : ''"
|
||||||
|
[disabled]="deliveringVote"
|
||||||
>
|
>
|
||||||
<mat-icon> thumb_down </mat-icon>
|
<mat-icon> thumb_down </mat-icon>
|
||||||
</button>
|
</button>
|
||||||
@ -103,13 +103,19 @@
|
|||||||
|
|
||||||
<ng-template #cannotVote>
|
<ng-template #cannotVote>
|
||||||
<div class="centered-button-wrapper">
|
<div class="centered-button-wrapper">
|
||||||
<div>
|
<div *ngIf="!deliveringVote">
|
||||||
<mat-icon class="vote-submitted">
|
<mat-icon class="vote-submitted">
|
||||||
check_circle
|
check_circle
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
<br />
|
<br />
|
||||||
<span>{{ 'Voting successful.' | translate }}</span>
|
<span>{{ 'Voting successful.' | 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>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@ -67,3 +67,10 @@
|
|||||||
.mat-divider-horizontal {
|
.mat-divider-horizontal {
|
||||||
position: initial;
|
position: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submit-vote-indicator {
|
||||||
|
text-align: center;
|
||||||
|
.mat-spinner {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@ -29,7 +29,8 @@ interface VoteActions {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'os-assignment-poll-vote',
|
selector: 'os-assignment-poll-vote',
|
||||||
templateUrl: './assignment-poll-vote.component.html',
|
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 {
|
export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
|
||||||
public AssignmentPollMethod = AssignmentPollMethod;
|
public AssignmentPollMethod = AssignmentPollMethod;
|
||||||
@ -47,7 +48,8 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
operator: OperatorService,
|
operator: OperatorService,
|
||||||
public vmanager: VotingService,
|
public vmanager: VotingService,
|
||||||
private pollRepo: AssignmentPollRepositoryService,
|
private pollRepo: AssignmentPollRepositoryService,
|
||||||
private promptService: PromptService
|
private promptService: PromptService,
|
||||||
|
private cd: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackbar, operator);
|
super(title, translate, matSnackbar, operator);
|
||||||
}
|
}
|
||||||
@ -58,6 +60,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
this.defineVoteOptions();
|
this.defineVoteOptions();
|
||||||
} else {
|
} else {
|
||||||
this.alreadyVoted = true;
|
this.alreadyVoted = true;
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,20 +107,24 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
return !!this.voteRequestData.global;
|
return !!this.voteRequestData.global;
|
||||||
}
|
}
|
||||||
|
|
||||||
public submitVote(): void {
|
public async submitVote(): Promise<void> {
|
||||||
const title = this.translate.instant('Submit selection now?');
|
const title = this.translate.instant('Submit selection now?');
|
||||||
const content = this.translate.instant('Your decision cannot be changed afterwards.');
|
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) {
|
if (confirmed) {
|
||||||
|
this.deliveringVote = true;
|
||||||
|
this.cd.markForCheck();
|
||||||
this.pollRepo
|
this.pollRepo
|
||||||
.vote(this.voteRequestData, this.poll.id)
|
.vote(this.voteRequestData, this.poll.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.alreadyVoted = true;
|
this.alreadyVoted = true;
|
||||||
})
|
})
|
||||||
.catch(this.raiseError);
|
.catch(this.raiseError)
|
||||||
}
|
.finally(() => {
|
||||||
|
this.deliveringVote = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public saveSingleVote(optionId: number, vote: VoteValue): void {
|
public saveSingleVote(optionId: number, vote: VoteValue): void {
|
||||||
if (this.isGlobalOptionSelected()) {
|
if (this.isGlobalOptionSelected()) {
|
||||||
|
@ -42,10 +42,16 @@
|
|||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
[ngClass]="pollStateActions[poll.state].css"
|
[ngClass]="pollStateActions[poll.state].css"
|
||||||
(click)="changeState(poll.nextState)"
|
(click)="changeState(poll.nextState)"
|
||||||
|
[disabled]="stateChangePending"
|
||||||
>
|
>
|
||||||
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
|
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
|
||||||
<span class="next-state-label">
|
<span class="next-state-label">
|
||||||
|
<ng-container *ngIf="!stateChangePending">
|
||||||
{{ poll.nextStateActionVerbose | translate }}
|
{{ poll.nextStateActionVerbose | translate }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="stateChangePending">
|
||||||
|
{{ 'In progress, please wait...' | translate }}
|
||||||
|
</ng-container>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,19 +2,26 @@
|
|||||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
|
<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 -->
|
<!-- Voting -->
|
||||||
<div class="vote-button" *ngFor="let option of voteOptions">
|
<div class="vote-button" *ngFor="let option of voteOptions">
|
||||||
<button
|
<button
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
(click)="saveVote(option.vote)"
|
(click)="saveVote(option.vote)"
|
||||||
[ngClass]="currentVote && currentVote.vote === option.vote ? option.css : ''"
|
[ngClass]="currentVote && currentVote.vote === option.vote ? option.css : ''"
|
||||||
|
[disabled]="deliveringVote"
|
||||||
>
|
>
|
||||||
<mat-icon> {{ option.icon }}</mat-icon>
|
<mat-icon> {{ option.icon }}</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<span class="vote-label"> {{ option.label | translate }} </span>
|
<span class="vote-label"> {{ option.label | translate }} </span>
|
||||||
</div>
|
</div>
|
||||||
</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-container>
|
||||||
|
|
||||||
<ng-template #userHasVotes>
|
<ng-template #userHasVotes>
|
||||||
|
@ -8,6 +8,14 @@
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submit-vote-indicator {
|
||||||
|
margin-top: 1em;
|
||||||
|
text-align: center;
|
||||||
|
.mat-spinner {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.vote-button {
|
.vote-button {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-gap: 1em;
|
grid-gap: 1em;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@ -22,7 +22,8 @@ interface VoteOption {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'os-motion-poll-vote',
|
selector: 'os-motion-poll-vote',
|
||||||
templateUrl: './motion-poll-vote.component.html',
|
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> {
|
export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPoll> {
|
||||||
public currentVote: VoteOption = {};
|
public currentVote: VoteOption = {};
|
||||||
@ -54,19 +55,28 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
|
|||||||
operator: OperatorService,
|
operator: OperatorService,
|
||||||
public vmanager: VotingService,
|
public vmanager: VotingService,
|
||||||
private pollRepo: MotionPollRepositoryService,
|
private pollRepo: MotionPollRepositoryService,
|
||||||
private promptService: PromptService
|
private promptService: PromptService,
|
||||||
|
private cd: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackbar, operator);
|
super(title, translate, matSnackbar, operator);
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveVote(vote: VoteValue): void {
|
public async saveVote(vote: VoteValue): Promise<void> {
|
||||||
this.currentVote.vote = vote;
|
this.currentVote.vote = vote;
|
||||||
const title = this.translate.instant('Submit selection now?');
|
const title = this.translate.instant('Submit selection now?');
|
||||||
const content = this.translate.instant('Your decision cannot be changed afterwards.');
|
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) {
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -42,10 +42,15 @@
|
|||||||
|
|
||||||
<!-- Change state button -->
|
<!-- Change state button -->
|
||||||
<div *osPerms="'motions.can_manage_polls'; and: !hideChangeState">
|
<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>
|
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
|
||||||
<span class="next-state-label">
|
<span class="next-state-label">
|
||||||
|
<ng-container *ngIf="!stateChangePending">
|
||||||
{{ poll.nextStateActionVerbose | translate }}
|
{{ poll.nextStateActionVerbose | translate }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="stateChangePending">
|
||||||
|
{{ 'In progress, please wait...' | translate }}
|
||||||
|
</ng-container>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,6 +16,8 @@ export abstract class BasePollVoteComponent<V extends ViewBasePoll> extends Base
|
|||||||
|
|
||||||
public votingErrors = VotingError;
|
public votingErrors = VotingError;
|
||||||
|
|
||||||
|
public deliveringVote = false;
|
||||||
|
|
||||||
protected user: ViewUser;
|
protected user: ViewUser;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
@ -15,6 +15,8 @@ import { PollService } from '../services/poll.service';
|
|||||||
import { ViewBasePoll } from '../models/view-base-poll';
|
import { ViewBasePoll } from '../models/view-base-poll';
|
||||||
|
|
||||||
export abstract class BasePollComponent<V extends ViewBasePoll, S extends PollService> extends BaseViewComponent {
|
export abstract class BasePollComponent<V extends ViewBasePoll, S extends PollService> extends BaseViewComponent {
|
||||||
|
public stateChangePending = false;
|
||||||
|
|
||||||
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject([]);
|
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject([]);
|
||||||
|
|
||||||
protected _poll: V;
|
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 title = this.translate.instant('Are you sure you want to reset this vote?');
|
||||||
const content = this.translate.instant('All votes will be lost.');
|
const content = this.translate.instant('All votes will be lost.');
|
||||||
if (await this.promptService.open(title, content)) {
|
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 {
|
} else {
|
||||||
this.repo.changePollState(this._poll).catch(this.raiseError);
|
this.stateChangePending = true;
|
||||||
|
this.repo
|
||||||
|
.changePollState(this._poll)
|
||||||
|
.catch(this.raiseError)
|
||||||
|
.finally(() => {
|
||||||
|
this.stateChangePending = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -778,6 +778,17 @@ button.mat-menu-item.selected {
|
|||||||
display: none !important; /* hide scrollbars in webkit browsers */
|
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 {
|
.import-table {
|
||||||
.table-container {
|
.table-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
Loading…
Reference in New Issue
Block a user