Merge pull request #3919 from FinnStutzenstein/statute

statute paragraphs list view
This commit is contained in:
Finn Stutzenstein 2018-10-19 07:50:04 +02:00 committed by GitHub
commit 214a310069
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 497 additions and 21 deletions

View File

@ -1,4 +1,29 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Permission } from '../../../core/services/operator.service';
/**
* One entry for the ellipsis menu.
*/
export interface EllipsisMenuItem {
/**
* The text for the menu entry
*/
text: string;
/**
* An optional icon to display before the text.
*/
icon?: string;
/**
* The action to be performed on click.
*/
action: string;
/**
* An optional permission to see this entry.
*/
perm?: Permission;
}
/**
* Reusable head bar component for Apps.
@ -38,9 +63,9 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
* This will execute a function with the name provided in the
* `action` field.
* ```ts
* onEllipsisItem(event: any) {
* if (event.action) {
* this[event.action]();
* onEllipsisItem(item: EllipsisMenuItem) {
* if (typeof this[item.action] === 'function') {
* this[item.action]();
* }
* }
* ```
@ -69,7 +94,7 @@ export class HeadBarComponent implements OnInit {
* The parent needs to provide a menu, i.e `[menuList]=myMenu`.
*/
@Input()
public menuList: any[];
public menuList: EllipsisMenuItem[];
/**
* Emit a signal to the parent component if the plus button was clicked
@ -81,7 +106,7 @@ export class HeadBarComponent implements OnInit {
* Emit a signal to the parent of an item in the menuList was selected.
*/
@Output()
public ellipsisMenuItem = new EventEmitter<any>();
public ellipsisMenuItem = new EventEmitter<EllipsisMenuItem>();
/**
* Empty constructor
@ -97,7 +122,7 @@ export class HeadBarComponent implements OnInit {
* Emits a signal to the parent if an item in the menu was clicked.
* @param item
*/
public clickMenu(item: any): void {
public clickMenu(item: EllipsisMenuItem): void {
this.ellipsisMenuItem.emit(item);
}

View File

@ -4,6 +4,7 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { MatTableDataSource, MatTable, MatSort, MatPaginator } from '@angular/material';
import { BaseViewModel } from './base-view-model';
import { EllipsisMenuItem } from '../../shared/components/head-bar/head-bar.component';
export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseComponent {
/**
@ -55,9 +56,9 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
*
* @param event clicked entry from ellipsis menu
*/
public onEllipsisItem(event: any): void {
if (event.action) {
this[event.action]();
public onEllipsisItem(item: EllipsisMenuItem): void {
if (typeof this[item.action] === 'function') {
this[item.action]();
}
}
}

View File

@ -58,7 +58,7 @@
<span translate>Edit section details:</span>
<p>
<mat-form-field>
<input formControlName="name" matInput placeholder="{{'Name' | translate}}">
<input formControlName="name" matInput placeholder="{{'Name' | translate}}" required>
<mat-hint *ngIf="!updateForm.controls.name.valid">
<span translate>Required</span>
</mat-hint>

View File

@ -46,6 +46,11 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
{
text: 'Motion comment sections',
action: 'toMotionCommentSections'
},
{
text: 'Statute paragrpahs',
action: 'toStatuteParagraphs',
perm: 'motions.can_manage'
}
];
@ -141,6 +146,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
this.router.navigate(['./comment-section'], { relativeTo: this.route });
}
/**
* navigate to 'motion/statute-paragraphs'
*/
public toStatuteParagraphs(): void {
this.router.navigate(['./statute-paragraphs'], { relativeTo: this.route });
}
/**
* Download all motions As PDF and DocX
*

View File

@ -0,0 +1,87 @@
<os-head-bar appName="Statute paragraphs" [plusButton]=true (plusButtonClicked)=onPlusButton()
[menuList]="menuList" (ellipsisMenuItem)=onEllipsisItem($event)></os-head-bar>
<div class="head-spacer"></div>
<mat-card *ngIf="statuteParagraphToCreate">
<mat-card-title translate>Create new statute paragraph</mat-card-title>
<mat-card-content>
<form [formGroup]="createForm">
<p>
<mat-form-field>
<input formControlName="title" matInput placeholder="{{'Title' | translate}}" required>
<mat-hint *ngIf="!createForm.controls.title.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
<p>
<mat-form-field>
<textarea formControlName="text" matInput placeholder="{{'Statute paragraph' | translate}}" required></textarea>
<mat-hint *ngIf="!createForm.controls.text.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="create()" translate>Create</button>
<button mat-button (click)="statuteParagraphToCreate = null" translate>Abort</button>
</mat-card-actions>
</mat-card>
<mat-accordion class="os-card">
<mat-expansion-panel *ngFor="let statuteParagraph of this.statuteParagraphs" (opened)="openId = statuteParagraph.id"
(closed)="panelClosed(statuteParagraph)" [expanded]="openId === statuteParagraph.id" multiple="false">
<mat-expansion-panel-header>
<mat-panel-title>
{{ statuteParagraph.title }}
</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="updateForm" *ngIf="editId === statuteParagraph.id">
<span translate>Edit statute paragraph details:</span>
<p>
<mat-form-field>
<input formControlName="title" matInput placeholder="{{'Title' | translate}}" required>
<mat-hint *ngIf="!updateForm.controls.title.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
<p>
<mat-form-field>
<textarea formControlName="text" matInput placeholder="{{'Statute paragraph' | translate}}" required></textarea>
<mat-hint *ngIf="!createForm.controls.text.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
</form>
<ng-container *ngIf="editId !== statuteParagraph.id">
<mat-card>
<mat-card-title>{{ statuteParagraph.title }}</mat-card-title>
<mat-card-content>
<div [innerHTML]="statuteParagraph.text"></div>
</mat-card-content>
</mat-card>
</ng-container>
<mat-action-row>
<button *ngIf="editId !== statuteParagraph.id" mat-button class="on-transition-fade" (click)="onEditButton(statuteParagraph)"
mat-icon-button>
<mat-icon>edit</mat-icon>
</button>
<button *ngIf="editId === statuteParagraph.id" mat-button class="on-transition-fade" (click)="editId = null"
mat-icon-button>
<mat-icon>cancel</mat-icon>
</button>
<button *ngIf="editId === statuteParagraph.id" mat-button class="on-transition-fade" (click)="onSaveButton(statuteParagraph)"
mat-icon-button>
<mat-icon>save</mat-icon>
</button>
<button mat-button class='on-transition-fade' (click)=onDeleteButton(statuteParagraph) mat-icon-button>
<mat-icon>delete</mat-icon>
</button>
</mat-action-row>
</mat-expansion-panel>
</mat-accordion>
<mat-card *ngIf="statuteParagraphs.length === 0">
<mat-card-content><div class="noContent" translate>No statute paragraphs yet...</div></mat-card-content>
</mat-card>

View File

@ -0,0 +1,17 @@
.head-spacer {
width: 100%;
height: 60px;
line-height: 60px;
text-align: right;
background: white; /* TODO: remove this and replace with theme */
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
mat-card {
margin-bottom: 20px;
}
.noContent {
text-align: center;
color: gray; /* TODO: remove this and replace with theme */
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StatuteParagraphListComponent } from './statute-paragraph-list.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('StatuteParagraphListComponent', () => {
let component: StatuteParagraphListComponent;
let fixture: ComponentFixture<StatuteParagraphListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [StatuteParagraphListComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StatuteParagraphListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,169 @@
import { Component, OnInit } from '@angular/core';
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 { EllipsisMenuItem } from '../../../../shared/components/head-bar/head-bar.component';
/**
* List view for the statute paragraphs.
*/
@Component({
selector: 'os-statute-paragraph-list',
templateUrl: './statute-paragraph-list.component.html',
styleUrls: ['./statute-paragraph-list.component.scss']
})
export class StatuteParagraphListComponent extends BaseComponent implements OnInit {
/**
* content of the ellipsis menu
*/
public menuList: EllipsisMenuItem[] = [
{
text: 'Sort statute paragraphs',
action: 'sortStatuteParagraphs'
}
];
public statuteParagraphToCreate: StatuteParagraph | null;
/**
* Source of the Data
*/
public statuteParagraphs: ViewStatuteParagraph[] = [];
/**
* The current focussed formgroup
*/
public updateForm: FormGroup;
public createForm: FormGroup;
public openId: number | null;
public editId: number | null;
/**
* The usual component constructor
* @param titleService
* @param translate
* @param repo
* @param formBuilder
*/
public constructor(
protected titleService: Title,
protected translate: TranslateService,
private repo: StatuteParagraphRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService
) {
super(titleService, translate);
const form = {
title: ['', Validators.required],
text: ['', Validators.required]
};
this.createForm = this.formBuilder.group(form);
this.updateForm = this.formBuilder.group(form);
}
/**
* Init function.
*
* Sets the title and gets/observes statute paragrpahs from DataStore
*/
public ngOnInit(): void {
super.setTitle('Statute paragraphs');
this.repo.getViewModelListObservable().subscribe(newViewStatuteParagraphs => {
this.statuteParagraphs = newViewStatuteParagraphs;
});
}
/**
* Add a new Section.
*/
public onPlusButton(): void {
if (!this.statuteParagraphToCreate) {
this.createForm.reset();
this.createForm.setValue({
title: '',
text: ''
});
this.statuteParagraphToCreate = new StatuteParagraph();
}
}
public create(): void {
if (this.createForm.valid) {
this.statuteParagraphToCreate.patchValues(this.createForm.value as StatuteParagraph);
this.repo.create(this.statuteParagraphToCreate).subscribe(resp => {
this.statuteParagraphToCreate = null;
});
}
}
/**
* Executed on edit button
* @param viewStatuteParagraph
*/
public onEditButton(viewStatuteParagraph: ViewStatuteParagraph): void {
this.editId = viewStatuteParagraph.id;
this.updateForm.setValue({
title: viewStatuteParagraph.title,
text: viewStatuteParagraph.text
});
}
/**
* Saves the statute paragrpah
*/
public onSaveButton(viewStatuteParagraph: ViewStatuteParagraph): void {
if (this.updateForm.valid) {
this.repo
.update(this.updateForm.value as Partial<StatuteParagraph>, viewStatuteParagraph)
.subscribe(resp => {
this.openId = this.editId = null;
});
}
}
/**
* is executed, when the delete button is pressed
*/
public async onDeleteButton(viewStatuteParagraph: ViewStatuteParagraph): Promise<any> {
const content = this.translate.instant('Delete') + ` ${viewStatuteParagraph.title}?`;
if (await this.promptService.open('Are you sure?', content)) {
this.repo.delete(viewStatuteParagraph).subscribe(resp => {
this.openId = this.editId = null;
});
}
}
/**
* Is executed when a mat-extension-panel is closed
* @param viewStatuteParagraph the statute paragraph in the panel
*/
public panelClosed(viewStatuteParagraph: ViewStatuteParagraph): void {
this.openId = null;
if (this.editId) {
this.onSaveButton(viewStatuteParagraph);
}
}
public onEllipsisItem(item: EllipsisMenuItem): void {
if (item.action === 'sortStatuteParagrpahs') {
this.sortStatuteParagrpahs();
}
}
/**
* TODO: navigate to a sorting view
*/
public sortStatuteParagrpahs(): void {
console.log('sort statute paragraphs');
}
}

View File

@ -1,5 +1,4 @@
import { Category } from '../../../shared/models/motions/category';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from '../../base/base-view-model';
/**
@ -77,7 +76,7 @@ export class ViewCategory extends BaseViewModel {
this._opened = false;
}
public getTitle(translate?: TranslateService): string {
public getTitle(): string {
return this.name;
}

View File

@ -1,4 +1,3 @@
import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from '../../base/base-view-model';
import { MotionCommentSection } from '../../../shared/models/motions/motion-comment-section';
import { Group } from '../../../shared/models/users/group';
@ -17,9 +16,6 @@ export class ViewMotionCommentSection extends BaseViewModel {
private _read_groups: Group[];
private _write_groups: Group[];
public edit = false;
public open = false;
public get section(): MotionCommentSection {
return this._section;
}
@ -59,7 +55,7 @@ export class ViewMotionCommentSection extends BaseViewModel {
this._write_groups = write_groups;
}
public getTitle(translate?: TranslateService): string {
public getTitle(): string {
return this.name;
}

View File

@ -5,7 +5,6 @@ import { Workflow } from '../../../shared/models/motions/workflow';
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
import { BaseModel } from '../../../shared/models/base/base-model';
import { BaseViewModel } from '../../base/base-view-model';
import { TranslateService } from '@ngx-translate/core';
enum LineNumbering {
None,
@ -182,7 +181,7 @@ export class ViewMotion extends BaseViewModel {
this.crMode = ChangeReco.Original;
}
public getTitle(translate?: TranslateService): string {
public getTitle(): string {
return this.title;
}

View File

@ -0,0 +1,66 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Group } from '../../../shared/models/users/group';
import { BaseModel } from '../../../shared/models/base/base-model';
import { StatuteParagraph } from '../../../shared/models/motions/statute-paragraph';
/**
* State paragrpah class for the View
*
* Stores a statute paragraph including all (implicit) references
* Provides "safe" access to variables and functions in {@link StatuteParagraph}
* @ignore
*/
export class ViewStatuteParagraph extends BaseViewModel {
private _paragraph: StatuteParagraph;
public get statuteParagraph(): StatuteParagraph {
return this._paragraph;
}
public get id(): number {
return this.statuteParagraph ? this.statuteParagraph.id : null;
}
public get title(): string {
return this.statuteParagraph ? this.statuteParagraph.title : null;
}
public get text(): string {
return this.statuteParagraph ? this.statuteParagraph.text : null;
}
public get weight(): number {
return this.statuteParagraph ? this.statuteParagraph.weight : null;
}
public constructor(paragraph: StatuteParagraph) {
super();
this._paragraph = paragraph;
}
public getTitle(): string {
return this.title;
}
/**
* Updates the local objects if required
* @param section
*/
public updateValues(paragraph: BaseModel): void {
if (paragraph instanceof StatuteParagraph) {
this._paragraph = paragraph as StatuteParagraph;
}
}
// TODO: Implement updating of groups
public updateGroup(group: Group): void {
console.log(this._paragraph, group);
}
/**
* Duplicate this motion into a copy of itself
*/
public copy(): ViewStatuteParagraph {
return new ViewStatuteParagraph(this._paragraph);
}
}

View File

@ -4,11 +4,13 @@ import { MotionListComponent } from './components/motion-list/motion-list.compon
import { MotionDetailComponent } from './components/motion-detail/motion-detail.component';
import { CategoryListComponent } from './components/category-list/category-list.component';
import { MotionCommentSectionListComponent } from './components/motion-comment-section-list/motion-comment-section-list.component';
import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component';
const routes: Routes = [
{ path: '', component: MotionListComponent },
{ path: 'category', component: CategoryListComponent },
{ path: 'comment-section', component: MotionCommentSectionListComponent },
{ path: 'statute-paragraphs', component: StatuteParagraphListComponent },
{ path: 'new', component: MotionDetailComponent },
{ path: ':id', component: MotionDetailComponent }
];

View File

@ -7,9 +7,16 @@ import { MotionListComponent } from './components/motion-list/motion-list.compon
import { MotionDetailComponent } from './components/motion-detail/motion-detail.component';
import { CategoryListComponent } from './components/category-list/category-list.component';
import { MotionCommentSectionListComponent } from './components/motion-comment-section-list/motion-comment-section-list.component';
import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component';
@NgModule({
imports: [CommonModule, MotionsRoutingModule, SharedModule],
declarations: [MotionListComponent, MotionDetailComponent, CategoryListComponent, MotionCommentSectionListComponent]
declarations: [
MotionListComponent,
MotionDetailComponent,
CategoryListComponent,
MotionCommentSectionListComponent,
StatuteParagraphListComponent
]
})
export class MotionsModule {}

View File

@ -0,0 +1,20 @@
import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { StatuteParagraphRepositoryService } from './statute-paragraph-repository.service';
describe('StatuteParagraphRepositoryService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [StatuteParagraphRepositoryService]
});
});
it('should be created', inject(
[StatuteParagraphRepositoryService],
(service: StatuteParagraphRepositoryService) => {
expect(service).toBeTruthy();
}
));
});

View File

@ -0,0 +1,50 @@
import { Injectable } from '@angular/core';
import { DataSendService } from '../../../core/services/data-send.service';
import { Observable } from 'rxjs';
import { DataStoreService } from '../../../core/services/data-store.service';
import { BaseRepository } from '../../base/base-repository';
import { ViewStatuteParagraph } from '../models/view-statute-paragraph';
import { StatuteParagraph } from '../../../shared/models/motions/statute-paragraph';
/**
* Repository Services for statute paragraphs
*
* Rather than manipulating models directly, the repository is meant to
* inform the {@link DataSendService} about changes which will send
* them to the Server.
*/
@Injectable({
providedIn: 'root'
})
export class StatuteParagraphRepositoryService extends BaseRepository<ViewStatuteParagraph, StatuteParagraph> {
/**
* Creates a StatuteParagraphRepository
* Converts existing and incoming statute paragraphs to ViewStatuteParagraphs
* Handles CRUD using an observer to the DataStore
* @param DataSend
*/
public constructor(protected DS: DataStoreService, private dataSend: DataSendService) {
super(DS, StatuteParagraph);
}
protected createViewModel(statuteParagraph: StatuteParagraph): ViewStatuteParagraph {
return new ViewStatuteParagraph(statuteParagraph);
}
public create(statuteParagraph: StatuteParagraph): Observable<any> {
return this.dataSend.createModel(statuteParagraph);
}
public update(
statuteParagraph: Partial<StatuteParagraph>,
viewStatuteParagraph: ViewStatuteParagraph
): Observable<any> {
const updateParagraph = viewStatuteParagraph.statuteParagraph;
updateParagraph.patchValues(statuteParagraph);
return this.dataSend.updateModel(updateParagraph, 'put');
}
public delete(viewStatuteParagraph: ViewStatuteParagraph): Observable<any> {
return this.dataSend.delete(viewStatuteParagraph.statuteParagraph);
}
}