diff --git a/client/src/app/shared/components/head-bar/head-bar.component.ts b/client/src/app/shared/components/head-bar/head-bar.component.ts index 3cbe54cac..8d8cef6d6 100644 --- a/client/src/app/shared/components/head-bar/head-bar.component.ts +++ b/client/src/app/shared/components/head-bar/head-bar.component.ts @@ -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,10 +63,10 @@ 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(); + public ellipsisMenuItem = new EventEmitter(); /** * 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); } diff --git a/client/src/app/site/base/list-view-base.ts b/client/src/app/site/base/list-view-base.ts index b02b545bf..d684adc27 100644 --- a/client/src/app/site/base/list-view-base.ts +++ b/client/src/app/site/base/list-view-base.ts @@ -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 extends BaseComponent { /** @@ -55,9 +56,9 @@ export abstract class ListViewBaseComponent 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](); } } } diff --git a/client/src/app/site/motions/components/motion-comment-section-list/motion-comment-section-list.component.html b/client/src/app/site/motions/components/motion-comment-section-list/motion-comment-section-list.component.html index 5d08698a5..49071fbd4 100644 --- a/client/src/app/site/motions/components/motion-comment-section-list/motion-comment-section-list.component.html +++ b/client/src/app/site/motions/components/motion-comment-section-list/motion-comment-section-list.component.html @@ -58,7 +58,7 @@ Edit section details:

- + Required diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/components/motion-list/motion-list.component.ts index cbd15e520..a8dea75fd 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.ts @@ -46,6 +46,11 @@ export class MotionListComponent extends ListViewBaseComponent 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 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 * diff --git a/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.html b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.html new file mode 100644 index 000000000..f8813ebb1 --- /dev/null +++ b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.html @@ -0,0 +1,87 @@ + +

+ + Create new statute paragraph + +
+

+ + + + Required + + +

+

+ + + + Required + + +

+
+
+ + + + +
+ + + + + {{ statuteParagraph.title }} + + +
+ Edit statute paragraph details: +

+ + + + Required + + +

+

+ + + + Required + + +

+
+ + + {{ statuteParagraph.title }} + +
+
+
+
+ + + + + + +
+
+ + + diff --git a/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.scss b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.scss new file mode 100644 index 000000000..9ad75da1d --- /dev/null +++ b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.scss @@ -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 */ +} diff --git a/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.spec.ts b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.spec.ts new file mode 100644 index 000000000..c95db59c0 --- /dev/null +++ b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.ts b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.ts new file mode 100644 index 000000000..e47c7861c --- /dev/null +++ b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.ts @@ -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, viewStatuteParagraph) + .subscribe(resp => { + this.openId = this.editId = null; + }); + } + } + + /** + * is executed, when the delete button is pressed + */ + public async onDeleteButton(viewStatuteParagraph: ViewStatuteParagraph): Promise { + 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'); + } +} diff --git a/client/src/app/site/motions/models/view-category.ts b/client/src/app/site/motions/models/view-category.ts index b9a1a7f73..80d3b5a16 100644 --- a/client/src/app/site/motions/models/view-category.ts +++ b/client/src/app/site/motions/models/view-category.ts @@ -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; } diff --git a/client/src/app/site/motions/models/view-motion-comment-section.ts b/client/src/app/site/motions/models/view-motion-comment-section.ts index cf7d98bd4..4e3be0a12 100644 --- a/client/src/app/site/motions/models/view-motion-comment-section.ts +++ b/client/src/app/site/motions/models/view-motion-comment-section.ts @@ -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; } diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index b6e714110..79bdfff97 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -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; } diff --git a/client/src/app/site/motions/models/view-statute-paragraph.ts b/client/src/app/site/motions/models/view-statute-paragraph.ts new file mode 100644 index 000000000..3c855eff6 --- /dev/null +++ b/client/src/app/site/motions/models/view-statute-paragraph.ts @@ -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); + } +} diff --git a/client/src/app/site/motions/motions-routing.module.ts b/client/src/app/site/motions/motions-routing.module.ts index 0d90247bc..90228594d 100644 --- a/client/src/app/site/motions/motions-routing.module.ts +++ b/client/src/app/site/motions/motions-routing.module.ts @@ -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 } ]; diff --git a/client/src/app/site/motions/motions.module.ts b/client/src/app/site/motions/motions.module.ts index e615a0bcb..67319fcf4 100644 --- a/client/src/app/site/motions/motions.module.ts +++ b/client/src/app/site/motions/motions.module.ts @@ -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 {} diff --git a/client/src/app/site/motions/services/statute-paragraph-repository.service.spec.ts b/client/src/app/site/motions/services/statute-paragraph-repository.service.spec.ts new file mode 100644 index 000000000..3411d9637 --- /dev/null +++ b/client/src/app/site/motions/services/statute-paragraph-repository.service.spec.ts @@ -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(); + } + )); +}); diff --git a/client/src/app/site/motions/services/statute-paragraph-repository.service.ts b/client/src/app/site/motions/services/statute-paragraph-repository.service.ts new file mode 100644 index 000000000..d16cf7afd --- /dev/null +++ b/client/src/app/site/motions/services/statute-paragraph-repository.service.ts @@ -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 { + /** + * 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 { + return this.dataSend.createModel(statuteParagraph); + } + + public update( + statuteParagraph: Partial, + viewStatuteParagraph: ViewStatuteParagraph + ): Observable { + const updateParagraph = viewStatuteParagraph.statuteParagraph; + updateParagraph.patchValues(statuteParagraph); + return this.dataSend.updateModel(updateParagraph, 'put'); + } + + public delete(viewStatuteParagraph: ViewStatuteParagraph): Observable { + return this.dataSend.delete(viewStatuteParagraph.statuteParagraph); + } +}