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 { 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 * Enum for different HTTPMethods
@ -18,14 +19,14 @@ export enum HTTPMethod {
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class HttpService { export class HttpService {
/** /**
* Construct a HttpService * Construct a HttpService
* *
* @param http The HTTP Client * @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> { private async send<T>(url: string, method: HTTPMethod, data?: any): Promise<T> {
if (!url.endsWith('/')) { if (!url.endsWith('/')) {
@ -33,15 +34,66 @@ export class HttpService {
} }
const options = { const options = {
body: data, body: data
}; };
try { try {
const response = await this.http.request<T>(method, url, options).toPromise(); const response = await this.http.request<T>(method, url, options).toPromise();
return response; return response;
} catch (e) { } catch (e) {
console.log("error", e); throw this.handleError(e);
throw 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 { ListViewBaseComponent } from '../../base/list-view-base';
import { AgendaRepositoryService } from '../services/agenda-repository.service'; import { AgendaRepositoryService } from '../services/agenda-repository.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material';
/** /**
* List view for the agenda. * List view for the agenda.
@ -21,14 +22,18 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
* The usual constructor for components * The usual constructor for components
* @param titleService * @param titleService
* @param translate * @param translate
* @param matSnackBar
* @param router
* @param repo
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar,
private router: Router, private router: Router,
private repo: AgendaRepositoryService 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 { ViewAssignment } from '../models/view-assignment';
import { ListViewBaseComponent } from '../../base/list-view-base'; import { ListViewBaseComponent } from '../../base/list-view-base';
import { AssignmentRepositoryService } from '../services/assignment-repository.service'; import { AssignmentRepositoryService } from '../services/assignment-repository.service';
import { MatSnackBar } from '@angular/material';
/** /**
* Listview for the assignments * Listview for the assignments
@ -18,12 +19,18 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
/** /**
* Constructor. * Constructor.
* *
* @param repo the repository
* @param titleService * @param titleService
* @param translate * @param translate
* @param matSnackBar
* @param repo the repository
*/ */
public constructor(private repo: AssignmentRepositoryService, titleService: Title, translate: TranslateService) { public constructor(
super(titleService, translate); 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 { ViewChild } from '@angular/core';
import { BaseComponent } from '../../base.component';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; 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 { 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 * 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 * Constructor for list view bases
* @param titleService the title serivce * @param titleService the title serivce
* @param translate the translate service * @param translate the translate service
* @param matSnackBar
*/ */
public constructor(titleService: Title, translate: TranslateService) { public constructor(titleService: Title, translate: TranslateService, matSnackBar: MatSnackBar) {
super(titleService, translate); super(titleService, translate, matSnackBar);
} }
/** /**

View File

@ -115,20 +115,18 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit {
/** /**
* Updates the this config field. * 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. // TODO: Fix the Datetimepicker parser and formatter.
if (this.configItem.inputType === 'datetimepicker') { if (this.configItem.inputType === 'datetimepicker') {
value = Date.parse(value); value = Date.parse(value);
} }
this.debounceTimeout = null; this.debounceTimeout = null;
try { this.repo.update({ value: value }, this.configItem).then(() => {
await this.repo.update({ value: value }, this.configItem);
this.error = null; this.error = null;
this.showSuccessIcon(); this.showSuccessIcon();
} catch (e) { }, this.setError.bind(this));
this.setError(e.error.detail);
}
} }
/** /**

View File

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

View File

@ -6,6 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ViewMediafile } from '../models/view-mediafile'; import { ViewMediafile } from '../models/view-mediafile';
import { MediafileRepositoryService } from '../services/mediafile-repository.service'; import { MediafileRepositoryService } from '../services/mediafile-repository.service';
import { ListViewBaseComponent } from '../../base/list-view-base'; import { ListViewBaseComponent } from '../../base/list-view-base';
import { MatSnackBar } from '@angular/material';
/** /**
* Lists all the uploaded files. * Lists all the uploaded files.
@ -25,11 +26,12 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
* @param translate * @param translate
*/ */
public constructor( public constructor(
private repo: MediafileRepositoryService, titleService: Title,
protected titleService: Title, translate: TranslateService,
protected 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 { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { Category } from '../../../../shared/models/motions/category'; import { Category } from '../../../../shared/models/motions/category';
import { CategoryRepositoryService } from '../../services/category-repository.service'; import { CategoryRepositoryService } from '../../services/category-repository.service';
import { ViewCategory } from '../../models/view-category'; 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 { Motion } from '../../../../shared/models/motions/motion';
import { SortingListComponent } from '../../../../shared/components/sorting-list/sorting-list.component'; import { SortingListComponent } from '../../../../shared/components/sorting-list/sorting-list.component';
import { PromptService } from 'app/core/services/prompt.service'; import { PromptService } from 'app/core/services/prompt.service';
import { BaseViewComponent } from '../../../base/base-view';
import { MatSnackBar } from '@angular/material';
/** /**
* List view for the categories. * List view for the categories.
@ -20,7 +21,7 @@ import { PromptService } from 'app/core/services/prompt.service';
templateUrl: './category-list.component.html', templateUrl: './category-list.component.html',
styleUrls: ['./category-list.component.scss'] styleUrls: ['./category-list.component.scss']
}) })
export class CategoryListComponent extends BaseComponent implements OnInit { export class CategoryListComponent extends BaseViewComponent implements OnInit {
/** /**
* Hold the category to create * Hold the category to create
*/ */
@ -56,17 +57,20 @@ export class CategoryListComponent extends BaseComponent implements OnInit {
* The usual component constructor * The usual component constructor
* @param titleService * @param titleService
* @param translate * @param translate
* @param matSnackBar
* @param repo * @param repo
* @param formBuilder * @param formBuilder
* @param promptService
*/ */
public constructor( public constructor(
protected titleService: Title, titleService: Title,
protected translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: CategoryRepositoryService, private repo: CategoryRepositoryService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private promptService: PromptService private promptService: PromptService
) { ) {
super(titleService, translate); super(titleService, translate, matSnackBar);
this.createForm = this.formBuilder.group({ this.createForm = this.formBuilder.group({
prefix: ['', Validators.required], 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 { public keyDownFunction(event: KeyboardEvent, viewCategory?: ViewCategory): void {
if (event.keyCode === 13) { if (event.keyCode === 13) {
console.log('hit enter');
if (viewCategory) { if (viewCategory) {
this.onSaveButton(viewCategory); this.onSaveButton(viewCategory);
} else { } else {
@ -119,11 +124,10 @@ export class CategoryListComponent extends BaseComponent implements OnInit {
/** /**
* Creates a new category. Executed after hitting save. * Creates a new category. Executed after hitting save.
*/ */
public async onCreateButton(): Promise<void> { public onCreateButton(): void {
if (this.createForm.valid) { if (this.createForm.valid) {
this.categoryToCreate.patchValues(this.createForm.value as Category); this.categoryToCreate.patchValues(this.createForm.value as Category);
await this.repo.create(this.categoryToCreate) this.repo.create(this.categoryToCreate).then(() => (this.categoryToCreate = null), this.raiseError);
this.categoryToCreate = null;
} }
} }
@ -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> { public async onSaveButton(viewCategory: ViewCategory): Promise<void> {
if (this.updateForm.valid) { if (this.updateForm.valid) {
// 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); await this.repo.update(this.updateForm.value as Partial<Category>, viewCategory);
} catch (e) {
this.raiseError(e);
}
this.onCancelButton(); this.onCancelButton();
this.sortDataSource(); this.sortDataSource();
} }
@ -174,12 +184,12 @@ export class CategoryListComponent extends BaseComponent implements OnInit {
/** /**
* is executed, when the delete button is pressed * is executed, when the delete button is pressed
* @param viewCategory The category to delete
*/ */
public async onDeleteButton(viewCategory: ViewCategory): Promise<void> { public async onDeleteButton(viewCategory: ViewCategory): Promise<void> {
const content = this.translate.instant('Delete') + ` ${viewCategory.name}?`; const content = this.translate.instant('Delete') + ` ${viewCategory.name}?`;
if (await this.promptService.open('Are you sure?', content)) { if (await this.promptService.open('Are you sure?', content)) {
await this.repo.delete(viewCategory); this.repo.delete(viewCategory).then(() => this.onCancelButton(), this.raiseError);
this.onCancelButton();
} }
} }

View File

@ -15,7 +15,7 @@
</form> </form>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <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. --> <!-- 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> </mat-dialog-actions>

View File

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

View File

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

View File

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

View File

@ -1,16 +1,18 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output } from '@angular/core'; import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import { ViewMotion } from '../../models/view-motion'; import { ViewMotion } from '../../models/view-motion';
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../models/view-unified-change'; 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 { MotionRepositoryService } from '../../services/motion-repository.service';
import { LineRange, ModificationType } from '../../services/diff.service'; import { LineRange, ModificationType } from '../../services/diff.service';
import { ViewChangeReco } from '../../models/view-change-reco'; 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 { ChangeRecommendationRepositoryService } from '../../services/change-recommendation-repository.service';
import { import {
MotionChangeRecommendationComponent, MotionChangeRecommendationComponent,
MotionChangeRecommendationComponentData MotionChangeRecommendationComponentData
} from '../motion-change-recommendation/motion-change-recommendation.component'; } 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. * This component displays the original motion text with the change blocks inside.
@ -36,7 +38,7 @@ import {
templateUrl: './motion-detail-diff.component.html', templateUrl: './motion-detail-diff.component.html',
styleUrls: ['./motion-detail-diff.component.scss'] styleUrls: ['./motion-detail-diff.component.scss']
}) })
export class MotionDetailDiffComponent implements AfterViewInit { export class MotionDetailDiffComponent extends BaseViewComponent implements AfterViewInit {
@Input() @Input()
public motion: ViewMotion; public motion: ViewMotion;
@Input() @Input()
@ -47,13 +49,28 @@ export class MotionDetailDiffComponent implements AfterViewInit {
@Output() @Output()
public createChangeRecommendation: EventEmitter<LineRange> = new EventEmitter<LineRange>(); 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( public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private motionRepo: MotionRepositoryService, private motionRepo: MotionRepositoryService,
private recoRepo: ChangeRecommendationRepositoryService, private recoRepo: ChangeRecommendationRepositoryService,
private dialogService: MatDialog, private dialogService: MatDialog,
private el: ElementRef private el: ElementRef
) {} ) {
super(title, translate, matSnackBar);
}
/** /**
* Returns the part of this motion between two change objects * Returns the part of this motion between two change objects
@ -171,12 +188,16 @@ export class MotionDetailDiffComponent implements AfterViewInit {
* @param {string} value * @param {string} value
*/ */
public async setAcceptanceValue(change: ViewChangeReco, value: string): Promise<void> { public async setAcceptanceValue(change: ViewChangeReco, value: string): Promise<void> {
try {
if (value === 'accepted') { if (value === 'accepted') {
await this.recoRepo.setAccepted(change); await this.recoRepo.setAccepted(change);
} }
if (value === 'rejected') { if (value === 'rejected') {
await this.recoRepo.setRejected(change); await this.recoRepo.setRejected(change);
} }
} catch (e) {
this.raiseError(e);
}
} }
/** /**
@ -185,8 +206,8 @@ export class MotionDetailDiffComponent implements AfterViewInit {
* @param {ViewChangeReco} change * @param {ViewChangeReco} change
* @param {boolean} internal * @param {boolean} internal
*/ */
public async setInternal(change: ViewChangeReco, internal: boolean): Promise<void> { public setInternal(change: ViewChangeReco, internal: boolean): void {
await this.recoRepo.setInternal(change, internal); this.recoRepo.setInternal(change, internal).then(null, this.raiseError);
} }
/** /**
@ -196,10 +217,10 @@ export class MotionDetailDiffComponent implements AfterViewInit {
* @param {ViewChangeReco} reco * @param {ViewChangeReco} reco
* @param {MouseEvent} $event * @param {MouseEvent} $event
*/ */
public async deleteChangeRecommendation(reco: ViewChangeReco, $event: MouseEvent): Promise<void> { public deleteChangeRecommendation(reco: ViewChangeReco, $event: MouseEvent): void {
$event.stopPropagation(); $event.stopPropagation();
$event.preventDefault(); $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 { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 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 { Category } from '../../../../shared/models/motions/category';
import { ViewportService } from '../../../../core/services/viewport.service'; import { ViewportService } from '../../../../core/services/viewport.service';
import { MotionRepositoryService } from '../../services/motion-repository.service'; import { MotionRepositoryService } from '../../services/motion-repository.service';
@ -20,10 +19,10 @@ import {
} from '../motion-change-recommendation/motion-change-recommendation.component'; } from '../motion-change-recommendation/motion-change-recommendation.component';
import { ChangeRecommendationRepositoryService } from '../../services/change-recommendation-repository.service'; import { ChangeRecommendationRepositoryService } from '../../services/change-recommendation-repository.service';
import { ViewChangeReco } from '../../models/view-change-reco'; 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 { ViewUnifiedChange } from '../../models/view-unified-change';
import { OperatorService } from '../../../../core/services/operator.service'; 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 * Component for the motion detail view
@ -33,7 +32,7 @@ import { CategoryRepositoryService } from '../../services/category-repository.se
templateUrl: './motion-detail.component.html', templateUrl: './motion-detail.component.html',
styleUrls: ['./motion-detail.component.scss'] styleUrls: ['./motion-detail.component.scss']
}) })
export class MotionDetailComponent extends BaseComponent implements OnInit { export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* MatExpansionPanel for the meta info * MatExpansionPanel for the meta info
* Only relevant in mobile view * Only relevant in mobile view
@ -131,18 +130,24 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
/** /**
* Constuct the detail view. * Constuct the detail view.
* *
* @param title
* @param translate
* @param matSnackBar
* @param vp the viewport service * @param vp the viewport service
* @param op
* @param router to navigate back to the motion list and to an existing motion * @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 route determine if this is a new or an existing motion
* @param formBuilder For reactive forms. Form Group and Form Control * @param formBuilder For reactive forms. Form Group and Form Control
* @param dialogService For opening dialogs * @param dialogService For opening dialogs
* @param repo: Motion Repository * @param repo Motion Repository
* @param changeRecoRepo: Change Recommendation Repository * @param changeRecoRepo Change Recommendation Repository
* @param DS: The DataStoreService * @param DS The DataStoreService
* @param sanitizer: For making HTML SafeHTML * @param sanitizer For making HTML SafeHTML
* @param translate: Translation Service
*/ */
public constructor( public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
public vp: ViewportService, public vp: ViewportService,
private op: OperatorService, private op: OperatorService,
private router: Router, private router: Router,
@ -151,12 +156,10 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
private dialogService: MatDialog, private dialogService: MatDialog,
private repo: MotionRepositoryService, private repo: MotionRepositoryService,
private changeRecoRepo: ChangeRecommendationRepositoryService, private changeRecoRepo: ChangeRecommendationRepositoryService,
private categoryRepo: CategoryRepositoryService,
private DS: DataStoreService, private DS: DataStoreService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer
protected translate: TranslateService
) { ) {
super(); super(title, translate, matSnackBar);
this.createForm(); this.createForm();
this.getMotionByUrl(); this.getMotionByUrl();
@ -278,6 +281,7 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
const fromForm = new Motion(); const fromForm = new Motion();
fromForm.deserialize(newMotionValues); fromForm.deserialize(newMotionValues);
try {
if (this.newMotion) { if (this.newMotion) {
const response = await this.repo.create(fromForm); const response = await this.repo.create(fromForm);
this.router.navigate(['./motions/' + response.id]); this.router.navigate(['./motions/' + response.id]);
@ -285,7 +289,9 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
await this.repo.update(fromForm, this.motionCopy); await this.repo.update(fromForm, this.motionCopy);
// if the motion was successfully updated, change the edit mode. // if the motion was successfully updated, change the edit mode.
this.editMotion = false; this.editMotion = false;
// TODO: Show errors if there appear here }
} catch (e) {
this.raiseError(e);
} }
} }
@ -319,13 +325,14 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
public deleteMotionButton(): void { public deleteMotionButton(): void {
this.repo.delete(this.motion).then(() => { this.repo.delete(this.motion).then(() => {
this.router.navigate(['./motions/']); this.router.navigate(['./motions/']);
}); }, this.raiseError);
const motList = this.categoryRepo.getMotionsOfCategory(this.motion.category); // 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); const index = motList.indexOf(this.motion.motion, 0);
if (index > -1) { if (index > -1) {
motList.splice(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> <mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
<!-- identifier column --> <!-- identifier column -->
<ng-container matColumnDef="identifier"> <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"> <mat-cell *matCellDef="let motion">
<div class='innerTable'> <div class='innerTable'>
{{motion.identifier}} {{motion.identifier}}
@ -34,7 +34,7 @@
<!-- title column --> <!-- title column -->
<ng-container matColumnDef="title"> <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"> <mat-cell *matCellDef="let motion">
<div class='innerTable'> <div class='innerTable'>
<span class='motion-list-title'>{{motion.title}}</span> <span class='motion-list-title'>{{motion.title}}</span>
@ -49,7 +49,7 @@
<!-- state column --> <!-- state column -->
<ng-container matColumnDef="state"> <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"> <mat-cell *matCellDef="let motion">
<div *ngIf='isDisplayIcon(motion.state) && motion.state' class='innerTable'> <div *ngIf='isDisplayIcon(motion.state) && motion.state' class='innerTable'>
<mat-icon>{{getStateIcon(motion.state)}}></mat-icon> <mat-icon>{{getStateIcon(motion.state)}}></mat-icon>
@ -77,7 +77,7 @@
<button mat-menu-item routerLink="comment-section"> <button mat-menu-item routerLink="comment-section">
<mat-icon>speaker_notes</mat-icon> <mat-icon>speaker_notes</mat-icon>
<span translate>Comments</span> <span translate>Comment sections</span>
</button> </button>
<button mat-menu-item routerLink="statute-paragraphs"> <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 { ViewMotion } from '../../models/view-motion';
import { WorkflowState } from '../../../../shared/models/motions/workflow-state'; import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
import { ListViewBaseComponent } from '../../../base/list-view-base'; import { ListViewBaseComponent } from '../../../base/list-view-base';
import { MatSnackBar } from '@angular/material';
/** /**
* Component that displays all the motions in a Table using DataSource. * Component that displays all the motions in a Table using DataSource.
@ -42,11 +43,12 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
public constructor( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private repo: MotionRepositoryService 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 { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { PromptService } from '../../../../core/services/prompt.service'; import { PromptService } from '../../../../core/services/prompt.service';
import { StatuteParagraph } from '../../../../shared/models/motions/statute-paragraph'; import { StatuteParagraph } from '../../../../shared/models/motions/statute-paragraph';
import { ViewStatuteParagraph } from '../../models/view-statute-paragraph'; import { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
import { StatuteParagraphRepositoryService } from '../../services/statute-paragraph-repository.service'; 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. * List view for the statute paragraphs.
@ -18,7 +19,7 @@ import { StatuteParagraphRepositoryService } from '../../services/statute-paragr
templateUrl: './statute-paragraph-list.component.html', templateUrl: './statute-paragraph-list.component.html',
styleUrls: ['./statute-paragraph-list.component.scss'] styleUrls: ['./statute-paragraph-list.component.scss']
}) })
export class StatuteParagraphListComponent extends BaseComponent implements OnInit { export class StatuteParagraphListComponent extends BaseViewComponent implements OnInit {
public statuteParagraphToCreate: StatuteParagraph | null; public statuteParagraphToCreate: StatuteParagraph | null;
/** /**
@ -40,17 +41,21 @@ export class StatuteParagraphListComponent extends BaseComponent implements OnIn
* The usual component constructor * The usual component constructor
* @param titleService * @param titleService
* @param translate * @param translate
* @param matSnackBar
* @param repo * @param repo
* @param formBuilder * @param formBuilder
* @param promptService
*/ */
public constructor( public constructor(
protected titleService: Title, titleService: Title,
protected translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: StatuteParagraphRepositoryService, private repo: StatuteParagraphRepositoryService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private promptService: PromptService private promptService: PromptService
) { ) {
super(titleService, translate); super(titleService, translate, matSnackBar);
const form = { const form = {
title: ['', Validators.required], title: ['', Validators.required],
text: ['', 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) { if (this.createForm.valid) {
this.statuteParagraphToCreate.patchValues(this.createForm.value as StatuteParagraph); this.statuteParagraphToCreate.patchValues(this.createForm.value as StatuteParagraph);
await this.repo.create(this.statuteParagraphToCreate); this.repo.create(this.statuteParagraphToCreate).then(() => {
this.statuteParagraphToCreate = null; 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) { if (this.updateForm.valid) {
await this.repo.update(this.updateForm.value as Partial<StatuteParagraph>, viewStatuteParagraph); this.repo.update(this.updateForm.value as Partial<StatuteParagraph>, viewStatuteParagraph).then(() => {
this.openId = this.editId = null; 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> { public async onDeleteButton(viewStatuteParagraph: ViewStatuteParagraph): Promise<void> {
const content = this.translate.instant('Delete') + ` ${viewStatuteParagraph.title}?`; const content = this.translate.instant('Delete') + ` ${viewStatuteParagraph.title}?`;
if (await this.promptService.open('Are you sure?', content)) { if (await this.promptService.open('Are you sure?', content)) {
await this.repo.delete(viewStatuteParagraph); this.repo.delete(viewStatuteParagraph).then(() => (this.openId = this.editId = null), this.raiseError);
this.openId = this.editId = null;
} }
} }

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

View File

@ -1,13 +1,14 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; 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 { FormGroup, FormControl, Validators } from '@angular/forms';
import { GroupRepositoryService } from '../../services/group-repository.service'; import { GroupRepositoryService } from '../../services/group-repository.service';
import { ViewGroup } from '../../models/view-group'; import { ViewGroup } from '../../models/view-group';
import { Group } from '../../../../shared/models/users/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 * Component for the Group-List and permission matrix
@ -17,7 +18,7 @@ import { BaseComponent } from '../../../../base.component';
templateUrl: './group-list.component.html', templateUrl: './group-list.component.html',
styleUrls: ['./group-list.component.scss'] styleUrls: ['./group-list.component.scss']
}) })
export class GroupListComponent extends BaseComponent implements OnInit { export class GroupListComponent extends BaseViewComponent implements OnInit {
/** /**
* Holds all Groups * Holds all Groups
*/ */
@ -51,22 +52,37 @@ export class GroupListComponent extends BaseComponent implements OnInit {
* *
* @param titleService Title Service * @param titleService Title Service
* @param translate Translations * @param translate Translations
* @param DS The Data Store * @param matSnackBar
* @param constants Constants * @param repo
* @param promptService
*/ */
public constructor(titleService: Title, translate: TranslateService, public repo: GroupRepositoryService) { public constructor(
super(titleService, translate); 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; this.newGroup = newGroup;
if (!mode) { if (!editMode) {
this.cancelEditing(); this.cancelEditing();
} }
} }
/**
* Creates or updates a group.
*/
public saveGroup(): void { public saveGroup(): void {
if (this.editGroup && this.newGroup) { if (this.editGroup && this.newGroup) {
this.submitNewGroup(); this.submitNewGroup();
@ -86,37 +102,39 @@ export class GroupListComponent extends BaseComponent implements OnInit {
/** /**
* Saves a newly created group. * 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) { if (!this.groupForm.value || !this.groupForm.valid) {
return; return;
} }
await this.repo.create(this.groupForm.value); this.repo.create(this.groupForm.value).then(() => {
this.groupForm.reset(); this.groupForm.reset();
this.cancelEditing(); this.cancelEditing();
}, this.raiseError);
} }
/** /**
* Saves an edited group. * 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) { if (!this.groupForm.value || !this.groupForm.valid) {
return; return;
} }
const updateData = new Group({ name: this.groupForm.value.name }); const updateData = new Group({ name: this.groupForm.value.name });
await this.repo.update(updateData, this.selectedGroup); this.repo.update(updateData, this.selectedGroup).then(() => {
this.cancelEditing(); this.cancelEditing();
}, this.raiseError);
} }
/** /**
* Deletes the selected Group * Deletes the selected Group
*/ */
public async deleteSelectedGroup(): Promise<void> { public async deleteSelectedGroup(): Promise<void> {
await this.repo.delete(this.selectedGroup) const content = this.translate.instant('Delete') + ` ${this.selectedGroup.name}?`;
this.cancelEditing(); 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 * Triggers when a permission was toggled
* @param group * @param viewGroup
* @param perm * @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) }); 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 { Group } from '../../../../shared/models/users/group';
import { DataStoreService } from '../../../../core/services/data-store.service'; import { DataStoreService } from '../../../../core/services/data-store.service';
import { OperatorService } from '../../../../core/services/operator.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 * 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', templateUrl: './user-detail.component.html',
styleUrls: ['./user-detail.component.scss'] styleUrls: ['./user-detail.component.scss']
}) })
export class UserDetailComponent implements OnInit { export class UserDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Info form object * Info form object
*/ */
@ -54,15 +59,32 @@ export class UserDetailComponent implements OnInit {
/** /**
* Constructor for user * 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( public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private repo: UserRepositoryService, private repo: UserRepositoryService,
private DS: DataStoreService, private DS: DataStoreService,
private op: OperatorService private operator: OperatorService,
private promptService: PromptService
) { ) {
super(title, translate, matSnackBar);
this.user = new ViewUser(); this.user = new ViewUser();
if (route.snapshot.url[0] && route.snapshot.url[0].path === 'new') { if (route.snapshot.url[0] && route.snapshot.url[0].path === 'new') {
this.newUser = true; this.newUser = true;
@ -75,7 +97,7 @@ export class UserDetailComponent implements OnInit {
this.ownPage = this.opOwnsPage(Number(params.id)); this.ownPage = this.opOwnsPage(Number(params.id));
// observe operator to find out if we see our own page or not // 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) { if (newOp) {
this.ownPage = this.opOwnsPage(Number(params.id)); 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 { 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 { public isAllowed(action: string): boolean {
switch (action) { switch (action) {
case 'manage': case 'manage':
return this.op.hasPerms('users.can_manage'); return this.operator.hasPerms('users.can_manage');
case 'seeName': 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': 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': 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': case 'changePersonal':
return this.op.hasPerms('user.cans_manage') || this.ownPage; return this.operator.hasPerms('user.cans_manage') || this.ownPage;
default: default:
return false; return false;
} }
@ -254,6 +278,7 @@ export class UserDetailComponent implements OnInit {
* Save / Submit a user * Save / Submit a user
*/ */
public async saveUser(): Promise<void> { public async saveUser(): Promise<void> {
try {
if (this.newUser) { if (this.newUser) {
const response = await this.repo.create(this.personalInfoForm.value); const response = await this.repo.create(this.personalInfoForm.value);
this.newUser = false; this.newUser = false;
@ -265,6 +290,9 @@ export class UserDetailComponent implements OnInit {
this.setEditMode(false); this.setEditMode(false);
this.loadViewUser(this.user.id); 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 * click on the delete user button
*/ */
public async deleteUserButton(): Promise<void> { public async deleteUserButton(): Promise<void> {
await this.repo.delete(this.user); const content = this.translate.instant('Delete') + ` ${this.user.full_name}?`;
this.router.navigate(['./users/']); 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 { UserRepositoryService } from '../../services/user-repository.service';
import { ListViewBaseComponent } from '../../../base/list-view-base'; import { ListViewBaseComponent } from '../../../base/list-view-base';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { MatSnackBar } from '@angular/material';
/** /**
* Component for the user list view. * Component for the user list view.
@ -26,14 +27,15 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* @param translate * @param translate
*/ */
public constructor( public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: UserRepositoryService, private repo: UserRepositoryService,
protected titleService: Title,
protected translate: TranslateService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
protected csvExport: CsvExportService protected csvExport: CsvExportService
) { ) {
super(titleService, translate); super(titleService, translate, matSnackBar);
} }
/** /**