Merge pull request #3968 from FinnStutzenstein/ErrorHandling

error handling
This commit is contained in:
Finn Stutzenstein 2018-11-08 11:37:48 +01:00 committed by GitHub
commit 080b6f52ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 445 additions and 206 deletions

View File

@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core';
/**
* Enum for different HTTPMethods
@ -18,14 +19,14 @@ export enum HTTPMethod {
@Injectable({
providedIn: 'root'
})
export class HttpService {
/**
* Construct a HttpService
*
* @param http The HTTP Client
* @param translate
*/
public constructor(private http: HttpClient) {}
public constructor(private http: HttpClient, private translate: TranslateService) {}
private async send<T>(url: string, method: HTTPMethod, data?: any): Promise<T> {
if (!url.endsWith('/')) {
@ -33,15 +34,66 @@ export class HttpService {
}
const options = {
body: data,
body: data
};
try {
const response = await this.http.request<T>(method, url, options).toPromise();
return response;
} catch (e) {
console.log("error", e);
throw e;
throw this.handleError(e);
}
}
/**
* Takes an error thrown by the HttpClient. Processes it to return a string that can
* be presented to the user.
* @param e The error thrown.
* @returns The prepared and translated message for the user
*/
private handleError(e: any): string {
let error = this.translate.instant('Error') + ': ';
// If the error is no HttpErrorResponse, it's not clear what is wrong.
if (!(e instanceof HttpErrorResponse)) {
console.error('Unknown error thrown by the http client: ', e);
error += this.translate.instant('An unknown error occurred.');
return error;
}
if (!e.error) {
error += this.translate.instant("The server didn't respond.");
} else if (typeof e.error === 'object') {
if (e.error.detail) {
error += this.processErrorTexts(e.error.detail);
} else {
error = Object.keys(e.error)
.map(key => {
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
return this.translate.instant(capitalizedKey) + ': ' + this.processErrorTexts(e.error[key]);
})
.join(', ');
}
} else if (e.status === 500) {
error += this.translate.instant('A server error occured. Please contact your system administrator.');
} else if (e.status > 500) {
error += this.translate.instant('The server cound not be reached') + ` (${e.status})`
} else {
error += e.message;
}
return error;
}
/**
* Errors from the servers may be string or array of strings. This function joins the strings together,
* if an array is send.
* @param str a string or a string array to join together.
*/
private processErrorTexts(str: string | string[]): string {
if (str instanceof Array) {
return str.join(' ');
} else {
return str;
}
}

View File

@ -5,6 +5,7 @@ import { ViewItem } from '../models/view-item';
import { ListViewBaseComponent } from '../../base/list-view-base';
import { AgendaRepositoryService } from '../services/agenda-repository.service';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material';
/**
* List view for the agenda.
@ -21,14 +22,18 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
* The usual constructor for components
* @param titleService
* @param translate
* @param matSnackBar
* @param router
* @param repo
*/
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private router: Router,
private repo: AgendaRepositoryService
) {
super(titleService, translate);
super(titleService, translate, matSnackBar);
}
/**

View File

@ -4,6 +4,7 @@ import { Title } from '@angular/platform-browser';
import { ViewAssignment } from '../models/view-assignment';
import { ListViewBaseComponent } from '../../base/list-view-base';
import { AssignmentRepositoryService } from '../services/assignment-repository.service';
import { MatSnackBar } from '@angular/material';
/**
* Listview for the assignments
@ -18,12 +19,18 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
/**
* Constructor.
*
* @param repo the repository
* @param titleService
* @param translate
* @param matSnackBar
* @param repo the repository
*/
public constructor(private repo: AssignmentRepositoryService, titleService: Title, translate: TranslateService) {
super(titleService, translate);
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: AssignmentRepositoryService
) {
super(titleService, translate, matSnackBar);
}
/**

View File

@ -0,0 +1,48 @@
import { BaseComponent } from '../../base.component';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material';
import { OnDestroy } from '@angular/core';
/**
* A base class for all views. Implements a generic error handling by raising a snack bar
* with the error. The error is dismissed, if the component is destroyed, so if the
* view is leaved.
*/
export abstract class BaseViewComponent extends BaseComponent implements OnDestroy {
/**
* A reference to the current error snack bar.
*/
private errorSnackBar: MatSnackBarRef<SimpleSnackBar>;
/**
* Constructor for bas elist views
* @param titleService the title serivce, passed to the base component
* @param translate the translate service, passed to the base component
* @param matSnackBar the snack bar service. Needed for showing errors.
*/
public constructor(titleService: Title, translate: TranslateService, private matSnackBar: MatSnackBar) {
super(titleService, translate);
}
/**
* Opens an error snack bar with the given error message.
* This is implemented as an arrow function to capture the called `this`. You can use this function
* as callback (`.then(..., this.raiseError)`) instead of doing `this.raiseError.bind(this)`.
* @param message The message to show.
*/
protected raiseError = (message: string): void => {
this.errorSnackBar = this.matSnackBar.open(message, this.translate.instant('OK'), {
duration: 0
});
};
/**
* automatically dismisses the error snack bar, if the component is destroyed.
*/
public ngOnDestroy(): void {
if (this.errorSnackBar) {
this.errorSnackBar.dismiss();
}
}
}

View File

@ -1,11 +1,11 @@
import { ViewChild } from '@angular/core';
import { BaseComponent } from '../../base.component';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { MatTableDataSource, MatTable, MatSort, MatPaginator } from '@angular/material';
import { MatTableDataSource, MatTable, MatSort, MatPaginator, MatSnackBar } from '@angular/material';
import { BaseViewModel } from './base-view-model';
import { BaseViewComponent } from './base-view';
export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseComponent {
export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseViewComponent {
/**
* The data source for a table. Requires to be initialised with a BaseViewModel
*/
@ -33,9 +33,10 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
* Constructor for list view bases
* @param titleService the title serivce
* @param translate the translate service
* @param matSnackBar
*/
public constructor(titleService: Title, translate: TranslateService) {
super(titleService, translate);
public constructor(titleService: Title, translate: TranslateService, matSnackBar: MatSnackBar) {
super(titleService, translate, matSnackBar);
}
/**

View File

@ -115,20 +115,18 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit {
/**
* Updates the this config field.
* @param value The new value to set.
*/
private async update(value: any): Promise<void> {
private update(value: any): void {
// TODO: Fix the Datetimepicker parser and formatter.
if (this.configItem.inputType === 'datetimepicker') {
value = Date.parse(value);
}
this.debounceTimeout = null;
try {
await this.repo.update({ value: value }, this.configItem);
this.repo.update({ value: value }, this.configItem).then(() => {
this.error = null;
this.showSuccessIcon();
} catch (e) {
this.setError(e.error.detail);
}
}, this.setError.bind(this));
}
/**

View File

@ -90,21 +90,18 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
* Observes the operator, if a user was already logged in, recreate to user and skip the login
*/
public ngOnInit(): void {
// Get the login data. Save information to the login data service
this.http.get<any>(environment.urlPrefix + '/users/login/').then(
response => {
if (response.info_text) {
this.installationNotice = this.matSnackBar.open(response.info_text, this.translate.instant('OK'), {
duration: 5000
});
}
this.loginDataService.setPrivacyPolicy(response.privacy_policy);
this.loginDataService.setLegalNotice(response.legal_notice);
},
() => {
// TODO: Error handling
// Get the login data. Save information to the login data service. If there is an
// error, ignore it.
// TODO: This has to be caught by the offline service
this.http.get<any>(environment.urlPrefix + '/users/login/').then(response => {
if (response.info_text) {
this.installationNotice = this.matSnackBar.open(response.info_text, this.translate.instant('OK'), {
duration: 5000
});
}
);
this.loginDataService.setPrivacyPolicy(response.privacy_policy);
this.loginDataService.setLegalNotice(response.legal_notice);
}, () => {});
}
public ngOnDestroy(): void {

View File

@ -6,6 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ViewMediafile } from '../models/view-mediafile';
import { MediafileRepositoryService } from '../services/mediafile-repository.service';
import { ListViewBaseComponent } from '../../base/list-view-base';
import { MatSnackBar } from '@angular/material';
/**
* Lists all the uploaded files.
@ -25,11 +26,12 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
* @param translate
*/
public constructor(
private repo: MediafileRepositoryService,
protected titleService: Title,
protected translate: TranslateService
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: MediafileRepositoryService
) {
super(titleService, translate);
super(titleService, translate, matSnackBar);
}
/**

View File

@ -3,7 +3,6 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { Category } from '../../../../shared/models/motions/category';
import { CategoryRepositoryService } from '../../services/category-repository.service';
import { ViewCategory } from '../../models/view-category';
@ -11,6 +10,8 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Motion } from '../../../../shared/models/motions/motion';
import { SortingListComponent } from '../../../../shared/components/sorting-list/sorting-list.component';
import { PromptService } from 'app/core/services/prompt.service';
import { BaseViewComponent } from '../../../base/base-view';
import { MatSnackBar } from '@angular/material';
/**
* List view for the categories.
@ -20,7 +21,7 @@ import { PromptService } from 'app/core/services/prompt.service';
templateUrl: './category-list.component.html',
styleUrls: ['./category-list.component.scss']
})
export class CategoryListComponent extends BaseComponent implements OnInit {
export class CategoryListComponent extends BaseViewComponent implements OnInit {
/**
* Hold the category to create
*/
@ -56,17 +57,20 @@ export class CategoryListComponent extends BaseComponent implements OnInit {
* The usual component constructor
* @param titleService
* @param translate
* @param matSnackBar
* @param repo
* @param formBuilder
* @param promptService
*/
public constructor(
protected titleService: Title,
protected translate: TranslateService,
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: CategoryRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService
) {
super(titleService, translate);
super(titleService, translate, matSnackBar);
this.createForm = this.formBuilder.group({
prefix: ['', Validators.required],
@ -80,11 +84,12 @@ export class CategoryListComponent extends BaseComponent implements OnInit {
}
/**
* Event on Key Down in form
* Event on key-down in form
* @param event
* @param viewCategory
*/
public keyDownFunction(event: KeyboardEvent, viewCategory?: ViewCategory): void {
if (event.keyCode === 13) {
console.log('hit enter');
if (viewCategory) {
this.onSaveButton(viewCategory);
} else {
@ -119,11 +124,10 @@ export class CategoryListComponent extends BaseComponent implements OnInit {
/**
* Creates a new category. Executed after hitting save.
*/
public async onCreateButton(): Promise<void> {
public onCreateButton(): void {
if (this.createForm.valid) {
this.categoryToCreate.patchValues(this.createForm.value as Category);
await this.repo.create(this.categoryToCreate)
this.categoryToCreate = null;
this.repo.create(this.categoryToCreate).then(() => (this.categoryToCreate = null), this.raiseError);
}
}
@ -141,11 +145,17 @@ export class CategoryListComponent extends BaseComponent implements OnInit {
}
/**
* Saves the categories
* Saves the category
* @param viewCategory
*/
public async onSaveButton(viewCategory: ViewCategory): Promise<void> {
if (this.updateForm.valid) {
await this.repo.update(this.updateForm.value as Partial<Category>, viewCategory);
// TODO: Check the motion sorting code below. If it is removed, change to .then() syntax.
try {
await this.repo.update(this.updateForm.value as Partial<Category>, viewCategory);
} catch (e) {
this.raiseError(e);
}
this.onCancelButton();
this.sortDataSource();
}
@ -174,12 +184,12 @@ export class CategoryListComponent extends BaseComponent implements OnInit {
/**
* is executed, when the delete button is pressed
* @param viewCategory The category to delete
*/
public async onDeleteButton(viewCategory: ViewCategory): Promise<void> {
const content = this.translate.instant('Delete') + ` ${viewCategory.name}?`;
if (await this.promptService.open('Are you sure?', content)) {
await this.repo.delete(viewCategory);
this.onCancelButton();
this.repo.delete(viewCategory).then(() => this.onCancelButton(), this.raiseError);
}
}

View File

@ -15,7 +15,7 @@
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Abort</button>
<button mat-button mat-dialog-close translate>Abort</button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button (click)="saveChangeRecommendation()">save</button>
<button mat-button (click)="saveChangeRecommendation()" translate>Save</button>
</mat-dialog-actions>

View File

@ -1,9 +1,12 @@
import { Component, Inject } from '@angular/core';
import { LineRange, ModificationType } from '../../services/diff.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { MAT_DIALOG_DATA, MatDialogRef, MatSnackBar } from '@angular/material';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ChangeRecommendationRepositoryService } from '../../services/change-recommendation-repository.service';
import { ViewChangeReco } from '../../models/view-change-reco';
import { BaseViewComponent } from '../../../base/base-view';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
/**
* Data that needs to be provided to the MotionChangeRecommendationComponent dialog
@ -39,7 +42,7 @@ export interface MotionChangeRecommendationComponentData {
templateUrl: './motion-change-recommendation.component.html',
styleUrls: ['./motion-change-recommendation.component.scss']
})
export class MotionChangeRecommendationComponent {
export class MotionChangeRecommendationComponent extends BaseViewComponent {
/**
* Determine if the change recommendation is edited
*/
@ -86,10 +89,15 @@ export class MotionChangeRecommendationComponent {
public constructor(
@Inject(MAT_DIALOG_DATA) public data: MotionChangeRecommendationComponentData,
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private formBuilder: FormBuilder,
private repo: ChangeRecommendationRepositoryService,
private dialogRef: MatDialogRef<MotionChangeRecommendationComponent>
) {
super(title, translate, matSnackBar);
this.editReco = data.editChangeRecommendation;
this.newReco = data.newChangeRecommendation;
this.changeReco = data.changeRecommendation;
@ -116,14 +124,16 @@ export class MotionChangeRecommendationComponent {
!this.contentForm.controls.public.value
);
if (this.newReco) {
await this.repo.createByViewModel(this.changeReco);
this.dialogRef.close();
// @TODO Show an error message
} else {
await this.repo.update(this.changeReco.changeRecommendation, this.changeReco);
this.dialogRef.close();
// @TODO Show an error message
try {
if (this.newReco) {
await this.repo.createByViewModel(this.changeReco);
this.dialogRef.close();
} else {
await this.repo.update(this.changeReco.changeRecommendation, this.changeReco);
this.dialogRef.close();
}
} catch (e) {
this.raiseError(e);
}
}
}

View File

@ -50,7 +50,7 @@
</ng-container>
</div>
<div class="write">
<mat-icon>add</mat-icon>
<mat-icon>edit</mat-icon>
{{ section.write_groups }}
<ng-container *ngIf="section.write_groups.length === 0">
&ndash;

View File

@ -3,7 +3,6 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { MotionCommentSection } from '../../../../shared/models/motions/motion-comment-section';
import { ViewMotionCommentSection } from '../../models/view-motion-comment-section';
@ -12,6 +11,8 @@ import { PromptService } from '../../../../core/services/prompt.service';
import { BehaviorSubject } from 'rxjs';
import { Group } from '../../../../shared/models/users/group';
import { DataStoreService } from '../../../../core/services/data-store.service';
import { BaseViewComponent } from '../../../base/base-view';
import { MatSnackBar } from '@angular/material';
/**
* List view for the categories.
@ -21,7 +22,7 @@ import { DataStoreService } from '../../../../core/services/data-store.service';
templateUrl: './motion-comment-section-list.component.html',
styleUrls: ['./motion-comment-section-list.component.scss']
})
export class MotionCommentSectionListComponent extends BaseComponent implements OnInit {
export class MotionCommentSectionListComponent extends BaseViewComponent implements OnInit {
public commentSectionToCreate: MotionCommentSection | null;
/**
@ -45,18 +46,23 @@ export class MotionCommentSectionListComponent extends BaseComponent implements
* The usual component constructor
* @param titleService
* @param translate
* @param matSnackBar
* @param repo
* @param formBuilder
* @param promptService
* @param DS
*/
public constructor(
protected titleService: Title,
protected translate: TranslateService,
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: MotionCommentSectionRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService,
private DS: DataStoreService
) {
super(titleService, translate);
super(titleService, translate, matSnackBar);
const form = {
name: ['', Validators.required],
read_groups_id: [[]],
@ -98,7 +104,7 @@ export class MotionCommentSectionListComponent extends BaseComponent implements
}
/**
* Add a new Section.
* Opens the create form.
*/
public onPlusButton(): void {
if (!this.commentSectionToCreate) {
@ -111,11 +117,15 @@ export class MotionCommentSectionListComponent extends BaseComponent implements
}
}
public async create(): Promise<void> {
/**
* Creates the comment section from the create form.
*/
public create(): void {
if (this.createForm.valid) {
this.commentSectionToCreate.patchValues(this.createForm.value as MotionCommentSection);
await this.repo.create(this.commentSectionToCreate);
this.commentSectionToCreate = null;
this.repo
.create(this.commentSectionToCreate)
.then(() => (this.commentSectionToCreate = null), this.raiseError);
}
}
@ -135,22 +145,24 @@ export class MotionCommentSectionListComponent extends BaseComponent implements
/**
* Saves the categories
* @param viewSection The section to save
*/
public async onSaveButton(viewSection: ViewMotionCommentSection): Promise<void> {
public onSaveButton(viewSection: ViewMotionCommentSection): void {
if (this.updateForm.valid) {
await this.repo.update(this.updateForm.value as Partial<MotionCommentSection>, viewSection);
this.openId = this.editId = null;
this.repo.update(this.updateForm.value as Partial<MotionCommentSection>, viewSection).then(() => {
this.openId = this.editId = null;
}, this.raiseError);
}
}
/**
* is executed, when the delete button is pressed
* @param viewSection The section to delete
*/
public async onDeleteButton(viewSection: ViewMotionCommentSection): Promise<void> {
const content = this.translate.instant('Delete') + ` ${viewSection.name}?`;
if (await this.promptService.open('Are you sure?', content)) {
await this.repo.delete(viewSection);
this.openId = this.editId = null;
this.repo.delete(viewSection).then(() => (this.openId = this.editId = null), this.raiseError);
}
}

View File

@ -1,16 +1,18 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import { ViewMotion } from '../../models/view-motion';
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../models/view-unified-change';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
import { MotionRepositoryService } from '../../services/motion-repository.service';
import { LineRange, ModificationType } from '../../services/diff.service';
import { ViewChangeReco } from '../../models/view-change-reco';
import { MatDialog } from '@angular/material';
import { MatDialog, MatSnackBar } from '@angular/material';
import { ChangeRecommendationRepositoryService } from '../../services/change-recommendation-repository.service';
import {
MotionChangeRecommendationComponent,
MotionChangeRecommendationComponentData
} from '../motion-change-recommendation/motion-change-recommendation.component';
import { BaseViewComponent } from '../../../base/base-view';
import { TranslateService } from '@ngx-translate/core';
/**
* This component displays the original motion text with the change blocks inside.
@ -36,7 +38,7 @@ import {
templateUrl: './motion-detail-diff.component.html',
styleUrls: ['./motion-detail-diff.component.scss']
})
export class MotionDetailDiffComponent implements AfterViewInit {
export class MotionDetailDiffComponent extends BaseViewComponent implements AfterViewInit {
@Input()
public motion: ViewMotion;
@Input()
@ -47,13 +49,28 @@ export class MotionDetailDiffComponent implements AfterViewInit {
@Output()
public createChangeRecommendation: EventEmitter<LineRange> = new EventEmitter<LineRange>();
/**
* @param title
* @param translate
* @param matSnackBar
* @param sanitizer
* @param motionRepo
* @param recoRepo
* @param dialogService
* @param el
*/
public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private sanitizer: DomSanitizer,
private motionRepo: MotionRepositoryService,
private recoRepo: ChangeRecommendationRepositoryService,
private dialogService: MatDialog,
private el: ElementRef
) {}
) {
super(title, translate, matSnackBar);
}
/**
* Returns the part of this motion between two change objects
@ -171,11 +188,15 @@ export class MotionDetailDiffComponent implements AfterViewInit {
* @param {string} value
*/
public async setAcceptanceValue(change: ViewChangeReco, value: string): Promise<void> {
if (value === 'accepted') {
await this.recoRepo.setAccepted(change);
}
if (value === 'rejected') {
await this.recoRepo.setRejected(change);
try {
if (value === 'accepted') {
await this.recoRepo.setAccepted(change);
}
if (value === 'rejected') {
await this.recoRepo.setRejected(change);
}
} catch (e) {
this.raiseError(e);
}
}
@ -185,8 +206,8 @@ export class MotionDetailDiffComponent implements AfterViewInit {
* @param {ViewChangeReco} change
* @param {boolean} internal
*/
public async setInternal(change: ViewChangeReco, internal: boolean): Promise<void> {
await this.recoRepo.setInternal(change, internal);
public setInternal(change: ViewChangeReco, internal: boolean): void {
this.recoRepo.setInternal(change, internal).then(null, this.raiseError);
}
/**
@ -196,10 +217,10 @@ export class MotionDetailDiffComponent implements AfterViewInit {
* @param {ViewChangeReco} reco
* @param {MouseEvent} $event
*/
public async deleteChangeRecommendation(reco: ViewChangeReco, $event: MouseEvent): Promise<void> {
public deleteChangeRecommendation(reco: ViewChangeReco, $event: MouseEvent): void {
$event.stopPropagation();
$event.preventDefault();
await this.recoRepo.delete(reco);
this.recoRepo.delete(reco).then(null, this.raiseError);
}
/**

View File

@ -1,9 +1,8 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatExpansionPanel, MatSelectChange } from '@angular/material';
import { MatDialog, MatExpansionPanel, MatSnackBar, MatSelectChange } from '@angular/material';
import { BaseComponent } from '../../../../base.component';
import { Category } from '../../../../shared/models/motions/category';
import { ViewportService } from '../../../../core/services/viewport.service';
import { MotionRepositoryService } from '../../services/motion-repository.service';
@ -20,10 +19,10 @@ import {
} from '../motion-change-recommendation/motion-change-recommendation.component';
import { ChangeRecommendationRepositoryService } from '../../services/change-recommendation-repository.service';
import { ViewChangeReco } from '../../models/view-change-reco';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
import { ViewUnifiedChange } from '../../models/view-unified-change';
import { OperatorService } from '../../../../core/services/operator.service';
import { CategoryRepositoryService } from '../../services/category-repository.service';
import { BaseViewComponent } from '../../../base/base-view';
/**
* Component for the motion detail view
@ -33,7 +32,7 @@ import { CategoryRepositoryService } from '../../services/category-repository.se
templateUrl: './motion-detail.component.html',
styleUrls: ['./motion-detail.component.scss']
})
export class MotionDetailComponent extends BaseComponent implements OnInit {
export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/**
* MatExpansionPanel for the meta info
* Only relevant in mobile view
@ -131,18 +130,24 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
/**
* Constuct the detail view.
*
* @param title
* @param translate
* @param matSnackBar
* @param vp the viewport service
* @param op
* @param router to navigate back to the motion list and to an existing motion
* @param route determine if this is a new or an existing motion
* @param formBuilder For reactive forms. Form Group and Form Control
* @param dialogService For opening dialogs
* @param repo: Motion Repository
* @param changeRecoRepo: Change Recommendation Repository
* @param DS: The DataStoreService
* @param sanitizer: For making HTML SafeHTML
* @param translate: Translation Service
* @param repo Motion Repository
* @param changeRecoRepo Change Recommendation Repository
* @param DS The DataStoreService
* @param sanitizer For making HTML SafeHTML
*/
public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
public vp: ViewportService,
private op: OperatorService,
private router: Router,
@ -151,12 +156,10 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
private dialogService: MatDialog,
private repo: MotionRepositoryService,
private changeRecoRepo: ChangeRecommendationRepositoryService,
private categoryRepo: CategoryRepositoryService,
private DS: DataStoreService,
private sanitizer: DomSanitizer,
protected translate: TranslateService
private sanitizer: DomSanitizer
) {
super();
super(title, translate, matSnackBar);
this.createForm();
this.getMotionByUrl();
@ -278,14 +281,17 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
const fromForm = new Motion();
fromForm.deserialize(newMotionValues);
if (this.newMotion) {
const response = await this.repo.create(fromForm);
this.router.navigate(['./motions/' + response.id]);
} else {
await this.repo.update(fromForm, this.motionCopy);
// if the motion was successfully updated, change the edit mode.
this.editMotion = false;
// TODO: Show errors if there appear here
try {
if (this.newMotion) {
const response = await this.repo.create(fromForm);
this.router.navigate(['./motions/' + response.id]);
} else {
await this.repo.update(fromForm, this.motionCopy);
// if the motion was successfully updated, change the edit mode.
this.editMotion = false;
}
} catch (e) {
this.raiseError(e);
}
}
@ -319,13 +325,14 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
public deleteMotionButton(): void {
this.repo.delete(this.motion).then(() => {
this.router.navigate(['./motions/']);
});
const motList = this.categoryRepo.getMotionsOfCategory(this.motion.category);
}, this.raiseError);
// TODO: this needs to be in the autoupdate code.
/*const motList = this.categoryRepo.getMotionsOfCategory(this.motion.category);
const index = motList.indexOf(this.motion.motion, 0);
if (index > -1) {
motList.splice(index, 1);
}
this.categoryRepo.updateCategoryNumbering(this.motion.category, motList);
this.categoryRepo.updateCategoryNumbering(this.motion.category, motList);*/
}
/**

View File

@ -24,7 +24,7 @@
<mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
<!-- identifier column -->
<ng-container matColumnDef="identifier">
<mat-header-cell *matHeaderCellDef mat-sort-header> Identifier </mat-header-cell>
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div class='innerTable'>
{{motion.identifier}}
@ -34,7 +34,7 @@
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> Title </mat-header-cell>
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div class='innerTable'>
<span class='motion-list-title'>{{motion.title}}</span>
@ -49,7 +49,7 @@
<!-- state column -->
<ng-container matColumnDef="state">
<mat-header-cell *matHeaderCellDef mat-sort-header> State </mat-header-cell>
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div *ngIf='isDisplayIcon(motion.state) && motion.state' class='innerTable'>
<mat-icon>{{getStateIcon(motion.state)}}></mat-icon>
@ -77,7 +77,7 @@
<button mat-menu-item routerLink="comment-section">
<mat-icon>speaker_notes</mat-icon>
<span translate>Comments</span>
<span translate>Comment sections</span>
</button>
<button mat-menu-item routerLink="statute-paragraphs">

View File

@ -8,6 +8,7 @@ import { MotionRepositoryService } from '../../services/motion-repository.servic
import { ViewMotion } from '../../models/view-motion';
import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
import { ListViewBaseComponent } from '../../../base/list-view-base';
import { MatSnackBar } from '@angular/material';
/**
* Component that displays all the motions in a Table using DataSource.
@ -42,11 +43,12 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private router: Router,
private route: ActivatedRoute,
private repo: MotionRepositoryService
) {
super(titleService, translate);
super(titleService, translate, matSnackBar);
}
/**

View File

@ -3,12 +3,13 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { PromptService } from '../../../../core/services/prompt.service';
import { StatuteParagraph } from '../../../../shared/models/motions/statute-paragraph';
import { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
import { StatuteParagraphRepositoryService } from '../../services/statute-paragraph-repository.service';
import { BaseViewComponent } from '../../../base/base-view';
import { MatSnackBar } from '@angular/material';
/**
* List view for the statute paragraphs.
@ -18,7 +19,7 @@ import { StatuteParagraphRepositoryService } from '../../services/statute-paragr
templateUrl: './statute-paragraph-list.component.html',
styleUrls: ['./statute-paragraph-list.component.scss']
})
export class StatuteParagraphListComponent extends BaseComponent implements OnInit {
export class StatuteParagraphListComponent extends BaseViewComponent implements OnInit {
public statuteParagraphToCreate: StatuteParagraph | null;
/**
@ -40,17 +41,21 @@ export class StatuteParagraphListComponent extends BaseComponent implements OnIn
* The usual component constructor
* @param titleService
* @param translate
* @param matSnackBar
* @param repo
* @param formBuilder
* @param promptService
*/
public constructor(
protected titleService: Title,
protected translate: TranslateService,
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: StatuteParagraphRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService
) {
super(titleService, translate);
super(titleService, translate, matSnackBar);
const form = {
title: ['', Validators.required],
text: ['', Validators.required]
@ -85,11 +90,15 @@ export class StatuteParagraphListComponent extends BaseComponent implements OnIn
}
}
public async create(): Promise<void> {
/**
* Handler when clicking on create to create a new statute paragraph
*/
public create(): void {
if (this.createForm.valid) {
this.statuteParagraphToCreate.patchValues(this.createForm.value as StatuteParagraph);
await this.repo.create(this.statuteParagraphToCreate);
this.statuteParagraphToCreate = null;
this.repo.create(this.statuteParagraphToCreate).then(() => {
this.statuteParagraphToCreate = null;
}, this.raiseError);
}
}
@ -107,23 +116,25 @@ export class StatuteParagraphListComponent extends BaseComponent implements OnIn
}
/**
* Saves the statute paragrpah
* Saves the statute paragraph
* @param viewStatuteParagraph The statute paragraph to save
*/
public async onSaveButton(viewStatuteParagraph: ViewStatuteParagraph): Promise<void> {
public onSaveButton(viewStatuteParagraph: ViewStatuteParagraph): void {
if (this.updateForm.valid) {
await this.repo.update(this.updateForm.value as Partial<StatuteParagraph>, viewStatuteParagraph);
this.openId = this.editId = null;
this.repo.update(this.updateForm.value as Partial<StatuteParagraph>, viewStatuteParagraph).then(() => {
this.openId = this.editId = null;
}, this.raiseError);
}
}
/**
* is executed, when the delete button is pressed
* Is executed, when the delete button is pressed
* @param viewStatuteParagraph The statute paragraph to delete
*/
public async onDeleteButton(viewStatuteParagraph: ViewStatuteParagraph): Promise<void> {
const content = this.translate.instant('Delete') + ` ${viewStatuteParagraph.title}?`;
if (await this.promptService.open('Are you sure?', content)) {
await this.repo.delete(viewStatuteParagraph);
this.openId = this.editId = null;
this.repo.delete(viewStatuteParagraph).then(() => (this.openId = this.editId = null), this.raiseError);
}
}

View File

@ -54,17 +54,12 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<ViewCh
}
/**
* Given a change recommendation view object, a entry in the backend is created and the new
* change recommendation view object is returned (as an observable).
*
* @param {ViewChangeReco} view
* @deprecated Will not work with PR #3928. There will just be the id as response to create requests.
* Two possibilities: Make a server change to still retrieve the created object or you have to wait for the
* correct autoupdate.
* Given a change recommendation view object, a entry in the backend is created.
* @param view
* @returns The id of the created change recommendation
*/
public async createByViewModel(view: ViewChangeReco): Promise<Identifiable> {
return await this.dataSend.createModel(view.changeRecommendation);
// return new ViewChangeReco(cr);
}
/**

View File

@ -7,6 +7,7 @@ import { TagRepositoryService } from '../../services/tag-repository.service';
import { ViewTag } from '../../models/view-tag';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { PromptService } from '../../../../core/services/prompt.service';
import { MatSnackBar } from '@angular/material';
/**
* Listview for the complete lsit of available Tags
@ -32,15 +33,18 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag> implements
* Constructor.
* @param titleService
* @param translate
* @param repo the repository
* @param matSnackBar
* @param repo the tag repository
* @param promptService
*/
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: TagRepositoryService,
private promptService: PromptService
) {
super(titleService, translate);
super(titleService, translate, matSnackBar);
}
/**
@ -70,26 +74,26 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag> implements
/**
* Saves a newly created tag.
*/
public async submitNewTag(): Promise<void> {
public submitNewTag(): void {
if (!this.tagForm.value || !this.tagForm.valid) {
return;
}
await this.repo.create(this.tagForm.value);
this.tagForm.reset();
this.cancelEditing();
this.repo.create(this.tagForm.value).then(() => {
this.tagForm.reset();
this.cancelEditing();
}, this.raiseError);
}
/**
* Saves an edited tag.
*/
public async submitEditedTag(): Promise<void> {
public submitEditedTag(): void {
if (!this.tagForm.value || !this.tagForm.valid) {
return;
}
const updateData = new Tag({ name: this.tagForm.value.name });
await this.repo.update(updateData, this.selectedTag);
this.cancelEditing();
this.repo.update(updateData, this.selectedTag).then(() => this.cancelEditing(), this.raiseError);
}
/**
@ -98,11 +102,13 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag> implements
public async deleteSelectedTag(): Promise<void> {
const content = this.translate.instant('Delete') + ` ${this.selectedTag.name}?`;
if (await this.promptService.open(this.translate.instant('Are you sure?'), content)) {
await this.repo.delete(this.selectedTag);
this.cancelEditing();
this.repo.delete(this.selectedTag).then(() => this.cancelEditing(), this.raiseError);
}
}
/**
* Canceles the editing
*/
public cancelEditing(): void {
this.newTag = false;
this.editTag = false;
@ -126,6 +132,11 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag> implements
this.cancelEditing();
}
}
/**
* Handles keyboard events. On enter, the editing is canceled.
* @param event
*/
public keyDownFunction(event: KeyboardEvent): void {
if (event.keyCode === 27) {
this.cancelEditing();

View File

@ -1,13 +1,14 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { MatTableDataSource } from '@angular/material';
import { MatTableDataSource, MatSnackBar } from '@angular/material';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { GroupRepositoryService } from '../../services/group-repository.service';
import { ViewGroup } from '../../models/view-group';
import { Group } from '../../../../shared/models/users/group';
import { BaseComponent } from '../../../../base.component';
import { BaseViewComponent } from '../../../base/base-view';
import { PromptService } from '../../../../core/services/prompt.service';
/**
* Component for the Group-List and permission matrix
@ -17,7 +18,7 @@ import { BaseComponent } from '../../../../base.component';
templateUrl: './group-list.component.html',
styleUrls: ['./group-list.component.scss']
})
export class GroupListComponent extends BaseComponent implements OnInit {
export class GroupListComponent extends BaseViewComponent implements OnInit {
/**
* Holds all Groups
*/
@ -51,22 +52,37 @@ export class GroupListComponent extends BaseComponent implements OnInit {
*
* @param titleService Title Service
* @param translate Translations
* @param DS The Data Store
* @param constants Constants
* @param matSnackBar
* @param repo
* @param promptService
*/
public constructor(titleService: Title, translate: TranslateService, public repo: GroupRepositoryService) {
super(titleService, translate);
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
public repo: GroupRepositoryService,
private promptService: PromptService
) {
super(titleService, translate, matSnackBar);
}
public setEditMode(mode: boolean, newGroup: boolean = true): void {
this.editGroup = mode;
/**
* Set, if the view is in edit mode. If editMod eis false, the editing is canceled.
* @param editMode
* @param newGroup Set to true, if the edit mode is for creating instead of updating a group.
*/
public setEditMode(editMode: boolean, newGroup: boolean = true): void {
this.editGroup = editMode;
this.newGroup = newGroup;
if (!mode) {
if (!editMode) {
this.cancelEditing();
}
}
/**
* Creates or updates a group.
*/
public saveGroup(): void {
if (this.editGroup && this.newGroup) {
this.submitNewGroup();
@ -86,37 +102,39 @@ export class GroupListComponent extends BaseComponent implements OnInit {
/**
* Saves a newly created group.
* @param form form data given by the group
*/
public async submitNewGroup(): Promise<void> {
public submitNewGroup(): void {
if (!this.groupForm.value || !this.groupForm.valid) {
return;
}
await this.repo.create(this.groupForm.value);
this.groupForm.reset();
this.cancelEditing();
this.repo.create(this.groupForm.value).then(() => {
this.groupForm.reset();
this.cancelEditing();
}, this.raiseError);
}
/**
* Saves an edited group.
* @param form form data given by the group
*/
public async submitEditedGroup(): Promise<void> {
public submitEditedGroup(): void {
if (!this.groupForm.value || !this.groupForm.valid) {
return;
}
const updateData = new Group({ name: this.groupForm.value.name });
await this.repo.update(updateData, this.selectedGroup);
this.cancelEditing();
this.repo.update(updateData, this.selectedGroup).then(() => {
this.cancelEditing();
}, this.raiseError);
}
/**
* Deletes the selected Group
*/
public async deleteSelectedGroup(): Promise<void> {
await this.repo.delete(this.selectedGroup)
this.cancelEditing();
const content = this.translate.instant('Delete') + ` ${this.selectedGroup.name}?`;
if (await this.promptService.open(this.translate.instant('Are you sure?'), content)) {
this.repo.delete(this.selectedGroup).then(() => this.cancelEditing(), this.raiseError);
}
}
/**
@ -130,12 +148,12 @@ export class GroupListComponent extends BaseComponent implements OnInit {
/**
* Triggers when a permission was toggled
* @param group
* @param viewGroup
* @param perm
*/
public async togglePerm(viewGroup: ViewGroup, perm: string): Promise<void> {
public togglePerm(viewGroup: ViewGroup, perm: string): void {
const updateData = new Group({ permissions: viewGroup.getAlteredPermissions(perm) });
await this.repo.update(updateData, viewGroup);
this.repo.update(updateData, viewGroup).then(null, this.raiseError);
}
/**

View File

@ -7,6 +7,11 @@ import { UserRepositoryService } from '../../services/user-repository.service';
import { Group } from '../../../../shared/models/users/group';
import { DataStoreService } from '../../../../core/services/data-store.service';
import { OperatorService } from '../../../../core/services/operator.service';
import { BaseViewComponent } from '../../../base/base-view';
import { TranslateService } from '@ngx-translate/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { PromptService } from '../../../../core/services/prompt.service';
/**
* Users detail component for both new and existing users
@ -16,7 +21,7 @@ import { OperatorService } from '../../../../core/services/operator.service';
templateUrl: './user-detail.component.html',
styleUrls: ['./user-detail.component.scss']
})
export class UserDetailComponent implements OnInit {
export class UserDetailComponent extends BaseViewComponent implements OnInit {
/**
* Info form object
*/
@ -54,15 +59,32 @@ export class UserDetailComponent implements OnInit {
/**
* Constructor for user
*
* @param title Title
* @param translate TranslateService
* @param matSnackBar MatSnackBar
* @param formBuilder FormBuilder
* @param route ActivatedRoute
* @param router Router
* @param repo UserRepositoryService
* @param DS DataStoreService
* @param operator OperatorService
* @param promptService PromptService
*/
public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private repo: UserRepositoryService,
private DS: DataStoreService,
private op: OperatorService
private operator: OperatorService,
private promptService: PromptService
) {
super(title, translate, matSnackBar);
this.user = new ViewUser();
if (route.snapshot.url[0] && route.snapshot.url[0].path === 'new') {
this.newUser = true;
@ -75,7 +97,7 @@ export class UserDetailComponent implements OnInit {
this.ownPage = this.opOwnsPage(Number(params.id));
// observe operator to find out if we see our own page or not
this.op.getObservable().subscribe(newOp => {
this.operator.getObservable().subscribe(newOp => {
if (newOp) {
this.ownPage = this.opOwnsPage(Number(params.id));
}
@ -86,10 +108,12 @@ export class UserDetailComponent implements OnInit {
}
/**
* sets the ownPage variable if the operator owns the page
* Checks, if the given user id matches with the operator ones.
* @param userId The id to check, if it's the operator
* @returns If the user is the operator
*/
public opOwnsPage(userId: number): boolean {
return this.op.user && this.op.user.id === userId;
return this.operator.user && this.operator.user.id === userId;
}
/**
@ -108,15 +132,15 @@ export class UserDetailComponent implements OnInit {
public isAllowed(action: string): boolean {
switch (action) {
case 'manage':
return this.op.hasPerms('users.can_manage');
return this.operator.hasPerms('users.can_manage');
case 'seeName':
return this.op.hasPerms('users.can_see_name', 'users.can_manage') || this.ownPage;
return this.operator.hasPerms('users.can_see_name', 'users.can_manage') || this.ownPage;
case 'seeExtra':
return this.op.hasPerms('users.can_see_extra_data', 'users.can_manage');
return this.operator.hasPerms('users.can_see_extra_data', 'users.can_manage');
case 'seePersonal':
return this.op.hasPerms('users.can_see_extra_data', 'users.can_manage') || this.ownPage;
return this.operator.hasPerms('users.can_see_extra_data', 'users.can_manage') || this.ownPage;
case 'changePersonal':
return this.op.hasPerms('user.cans_manage') || this.ownPage;
return this.operator.hasPerms('user.cans_manage') || this.ownPage;
default:
return false;
}
@ -254,16 +278,20 @@ export class UserDetailComponent implements OnInit {
* Save / Submit a user
*/
public async saveUser(): Promise<void> {
if (this.newUser) {
const response = await this.repo.create(this.personalInfoForm.value);
this.newUser = false;
this.router.navigate([`./users/${response.id}`]);
} else {
// TODO (Issue #3962): We need a waiting-State, so if autoupdates come before the response,
// the user is also updated.
await this.repo.update(this.personalInfoForm.value, this.user);
this.setEditMode(false);
this.loadViewUser(this.user.id);
try {
if (this.newUser) {
const response = await this.repo.create(this.personalInfoForm.value);
this.newUser = false;
this.router.navigate([`./users/${response.id}`]);
} else {
// TODO (Issue #3962): We need a waiting-State, so if autoupdates come before the response,
// the user is also updated.
await this.repo.update(this.personalInfoForm.value, this.user);
this.setEditMode(false);
this.loadViewUser(this.user.id);
}
} catch (e) {
this.raiseError(e);
}
}
@ -285,8 +313,10 @@ export class UserDetailComponent implements OnInit {
* click on the delete user button
*/
public async deleteUserButton(): Promise<void> {
await this.repo.delete(this.user);
this.router.navigate(['./users/']);
const content = this.translate.instant('Delete') + ` ${this.user.full_name}?`;
if (await this.promptService.open(this.translate.instant('Are you sure?'), content)) {
this.repo.delete(this.user).then(() => this.router.navigate(['./users/']), this.raiseError);
}
}
/**

View File

@ -7,6 +7,7 @@ import { ViewUser } from '../../models/view-user';
import { UserRepositoryService } from '../../services/user-repository.service';
import { ListViewBaseComponent } from '../../../base/list-view-base';
import { Router, ActivatedRoute } from '@angular/router';
import { MatSnackBar } from '@angular/material';
/**
* Component for the user list view.
@ -26,14 +27,15 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* @param translate
*/
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: UserRepositoryService,
protected titleService: Title,
protected translate: TranslateService,
private router: Router,
private route: ActivatedRoute,
protected csvExport: CsvExportService
) {
super(titleService, translate);
super(titleService, translate, matSnackBar);
}
/**