Client: Add form validation and request handling

This commit is contained in:
Sean 2021-01-12 17:38:21 +01:00
parent ea180246c7
commit bc382df68f
8 changed files with 99 additions and 40 deletions

View File

@ -1,4 +1,4 @@
import { FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
/** /**
* Constant to validate a `duration` field. * Constant to validate a `duration` field.
@ -17,3 +17,13 @@ export const durationValidator: ValidatorFn = (control: FormGroup): ValidationEr
const regExp = /^\s*([0-9]+)(:)?([0-5][0-9]?)?\s*[h|m]?$/g; const regExp = /^\s*([0-9]+)(:)?([0-5][0-9]?)?\s*[h|m]?$/g;
return regExp.test(control.value) || control.value === '' ? null : { valid: false }; return regExp.test(control.value) || control.value === '' ? null : { valid: false };
}; };
export function isNumberRange(minCtrlName: string, maxCtrlName: string): ValidatorFn {
return (formControl: AbstractControl): { [key: string]: any } => {
const min = formControl.get(minCtrlName).value;
const max = formControl.get(maxCtrlName).value;
if (min > max) {
return { rangeError: true };
}
};
}

View File

@ -40,10 +40,10 @@
<!-- Amount of Votes --> <!-- Amount of Votes -->
<small *ngIf="poll.max_votes_amount > 1"> <small *ngIf="poll.max_votes_amount > 1">
<ng-container *ngIf="poll.max_votes_amount !== poll.min_votes_amount"> <ng-container *ngIf="poll.max_votes_amount !== poll.min_votes_amount">
<span> {{ pollPropertyVerbose.max_votes_amount | translate }}: {{ poll.max_votes_amount }}</span>
<br />
<span> {{ pollPropertyVerbose.min_votes_amount | translate }}: {{ poll.min_votes_amount }}</span> <span> {{ pollPropertyVerbose.min_votes_amount | translate }}: {{ poll.min_votes_amount }}</span>
<br /> <br />
<span> {{ pollPropertyVerbose.max_votes_amount | translate }}: {{ poll.max_votes_amount }}</span>
<br />
</ng-container> </ng-container>
<ng-container *ngIf="poll.max_votes_amount === poll.min_votes_amount"> <ng-container *ngIf="poll.max_votes_amount === poll.min_votes_amount">

View File

@ -27,8 +27,7 @@ export class AssignmentPollMetaInfoComponent {
public constructor() {} public constructor() {}
public userCanVoe(): boolean { public userCanVoe(): boolean {
// this.poll.canBeVotedFor return this.poll.canBeVotedFor();
return true;
} }
public getOptionTitle(option: ViewAssignmentOption): string { public getOptionTitle(option: ViewAssignmentOption): string {

View File

@ -31,6 +31,11 @@
<h4 *ngIf="(poll.isMethodY || poll.isMethodN) && poll.max_votes_amount > 1"> <h4 *ngIf="(poll.isMethodY || poll.isMethodN) && poll.max_votes_amount > 1">
{{ 'Available votes' | translate }}: {{ 'Available votes' | translate }}:
<b> {{ getVotesAvailable(delegation) }}/{{ poll.max_votes_amount }} </b> <b> {{ getVotesAvailable(delegation) }}/{{ poll.max_votes_amount }} </b>
<span *ngIf="poll.min_votes_amount > 1">
({{ 'At least' | translate }} <b>{{ poll.min_votes_amount }}</b
>)
</span>
</h4> </h4>
<!-- Options and Actions --> <!-- Options and Actions -->
@ -169,7 +174,7 @@
mat-flat-button mat-flat-button
color="accent" color="accent"
(click)="submitVote(delegation)" (click)="submitVote(delegation)"
[disabled]="getVotesCount(delegation) == 0" [disabled]="getVotesCount(delegation) < minVotes"
> >
<mat-icon> how_to_vote </mat-icon> <mat-icon> how_to_vote </mat-icon>
<span> <span>

View File

@ -57,6 +57,10 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
return this.poll.assignment.default_poll_description; return this.poll.assignment.default_poll_description;
} }
public get minVotes(): number {
return this.poll.min_votes_amount;
}
public constructor( public constructor(
title: Title, title: Title,
protected translate: TranslateService, protected translate: TranslateService,

View File

@ -58,6 +58,8 @@ export abstract class BasePollDialogComponent<
votes: this.getVoteData(), votes: this.getVoteData(),
publish_immediately: this.publishImmediately publish_immediately: this.publishImmediately
}; };
console.log('answer: ', answer);
this.dialogRef.close(answer); this.dialogRef.close(answer);
} }

View File

@ -30,7 +30,7 @@
</mat-form-field> </mat-form-field>
<!-- Groups entitled to Vote --> <!-- Groups entitled to Vote -->
<mat-form-field *ngIf="contentForm.get('type').value && contentForm.get('type').value !== 'analog'"> <mat-form-field *ngIf="isEVotingSelected">
<os-search-value-selector <os-search-value-selector
formControlName="groups_id" formControlName="groups_id"
[multiple]="true" [multiple]="true"
@ -57,6 +57,23 @@
<mat-error>{{ 'This field is required.' | translate }}</mat-error> <mat-error>{{ 'This field is required.' | translate }}</mat-error>
</mat-form-field> </mat-form-field>
<ng-container formGroupName="votes_amount" *ngIf="isEVotingSelected">
<!-- Min Amount of Votes -->
<mat-form-field *ngIf="showAmountAndGlobal(data)">
<input
type="number"
matInput
placeholder="{{ PollPropertyVerbose.min_votes_amount | translate }}"
formControlName="min_votes_amount"
min="1"
required
[errorStateMatcher]="parentErrorStateMatcher"
/>
<mat-error *ngIf="contentForm.controls['votes_amount'].hasError('rangeError')">
{{ 'Min votes must be smaller or equal to max votes' | translate }}
</mat-error>
</mat-form-field>
<!-- Max Amount of Votes --> <!-- Max Amount of Votes -->
<mat-form-field *ngIf="showAmountAndGlobal(data)"> <mat-form-field *ngIf="showAmountAndGlobal(data)">
<input <input
@ -68,22 +85,11 @@
required required
/> />
</mat-form-field> </mat-form-field>
</ng-container>
<!-- Min Amount of Votes -->
<mat-form-field>
<input
type="number"
matInput
placeholder="{{ PollPropertyVerbose.min_votes_amount | translate }}"
formControlName="min_votes_amount"
min="1"
required
/>
</mat-form-field>
</div> </div>
<!-- Amount of Votes and global options --> <!-- Amount of Votes and global options -->
<div class="global-options" *ngIf="showAmountAndGlobal(data)"> <div class="global-options" *ngIf="isEVotingSelected && showAmountAndGlobal(data)">
<mat-checkbox formControlName="global_yes"> <mat-checkbox formControlName="global_yes">
{{ PollPropertyVerbose.global_yes | translate }} {{ PollPropertyVerbose.global_yes | translate }}
</mat-checkbox> </mat-checkbox>

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
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';
@ -13,7 +13,9 @@ import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-priv
import { AssignmentPollMethod, AssignmentPollPercentBase } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod, AssignmentPollPercentBase } from 'app/shared/models/assignments/assignment-poll';
import { PercentBase } from 'app/shared/models/poll/base-poll'; import { PercentBase } from 'app/shared/models/poll/base-poll';
import { PollType } from 'app/shared/models/poll/base-poll'; import { PollType } from 'app/shared/models/poll/base-poll';
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
import { infoDialogSettings } from 'app/shared/utils/dialog-settings'; import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
import { isNumberRange } from 'app/shared/validators/custom-validators';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { BaseViewComponentDirective } from 'app/site/base/base-view'; import { BaseViewComponentDirective } from 'app/site/base/base-view';
import { import {
@ -39,6 +41,7 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
* The form-group for the meta-info. * The form-group for the meta-info.
*/ */
public contentForm: FormGroup; public contentForm: FormGroup;
public parentErrorStateMatcher = new ParentErrorStateMatcher();
public PollType = PollType; public PollType = PollType;
public PollPropertyVerbose = PollPropertyVerbose; public PollPropertyVerbose = PollPropertyVerbose;
@ -102,6 +105,10 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
} }
} }
public get isEVotingSelected(): boolean {
return this.contentForm.get('type').value && this.contentForm.get('type').value !== 'analog';
}
/** /**
* Constructor. Retrieves necessary metadata from the pollService, * Constructor. Retrieves necessary metadata from the pollService,
* injects the poll itself * injects the poll itself
@ -141,11 +148,7 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
} }
} }
Object.keys(this.contentForm.controls).forEach(key => { this.patchForm(this.contentForm);
if (this.data[key]) {
this.contentForm.get(key).patchValue(this.data[key]);
}
});
} }
this.updatePollValues(this.contentForm.value); this.updatePollValues(this.contentForm.value);
this.updatePercentBases(this.contentForm.get('pollmethod').value); this.updatePercentBases(this.contentForm.get('pollmethod').value);
@ -173,6 +176,25 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
this.setWarning(); this.setWarning();
} }
/**
* Generic recursive helper function to patch the form
* will transitive move poll.min_votes_amount and poll.max_votes_amount into
* form.votes_amount.min_votes_amount/max_votes_amount
* @param formGroup
*/
private patchForm(formGroup: FormGroup): void {
for (const key of Object.keys(formGroup.controls)) {
const currentControl = formGroup.controls[key];
if (currentControl instanceof FormControl) {
if (this.data[key]) {
currentControl.patchValue(this.data[key]);
}
} else if (currentControl instanceof FormGroup) {
this.patchForm(currentControl);
}
}
}
private disablePollType(): void { private disablePollType(): void {
this.contentForm.get('type').disable(); this.contentForm.get('type').disable();
} }
@ -243,8 +265,14 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
} }
} }
public getValues<V extends ViewBasePoll>(): Partial<V> { public getValues(): Partial<T> {
return { ...this.data, ...this.contentForm.value }; return { ...this.data, ...this.serializeForm(this.contentForm) };
}
private serializeForm(formGroup: FormGroup): Partial<T> {
const formData = { ...formGroup.value, ...formGroup.value.votes_amount };
delete formData.votes_amount;
return formData;
} }
/** /**
@ -300,8 +328,13 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
pollmethod: ['', Validators.required], pollmethod: ['', Validators.required],
onehundred_percent_base: ['', Validators.required], onehundred_percent_base: ['', Validators.required],
majority_method: ['', Validators.required], majority_method: ['', Validators.required],
votes_amount: this.fb.group(
{
max_votes_amount: [1, [Validators.required, Validators.min(1)]], max_votes_amount: [1, [Validators.required, Validators.min(1)]],
min_votes_amount: [1, [Validators.required, Validators.min(1)]], min_votes_amount: [1, [Validators.required, Validators.min(1)]]
},
{ validator: isNumberRange('min_votes_amount', 'max_votes_amount') }
),
groups_id: [], groups_id: [],
global_yes: [false], global_yes: [false],
global_no: [false], global_no: [false],