From 03508c903f6b5f8a7e44d0e12d2ce29b24edc459 Mon Sep 17 00:00:00 2001 From: Sean Engelhardt Date: Thu, 6 Dec 2018 12:28:05 +0100 Subject: [PATCH] Add motion blocks --- client/src/app/shared/models/agenda/item.ts | 10 + .../app/shared/models/motions/motion-block.ts | 7 +- .../topic-detail/topic-detail.component.scss | 3 +- .../category-list.component.html | 2 +- .../category-list.component.scss | 7 - .../motion-block-detail.component.html | 124 +++++++++++ .../motion-block-detail.component.scss | 52 +++++ .../motion-block-detail.component.spec.ts | 26 +++ .../motion-block-detail.component.ts | 209 ++++++++++++++++++ .../motion-block-list.component.html | 77 +++++++ .../motion-block-list.component.scss | 11 + .../motion-block-list.component.spec.ts | 26 +++ .../motion-block-list.component.ts | 169 ++++++++++++++ .../motion-detail.component.html | 29 ++- .../motion-detail/motion-detail.component.ts | 18 ++ .../motion-list/motion-list.component.html | 22 +- .../motion-list/motion-list.component.scss | 1 - .../site/motions/models/view-motion-block.ts | 39 ++++ .../app/site/motions/models/view-motion.ts | 7 + .../site/motions/motions-routing.module.ts | 4 + client/src/app/site/motions/motions.module.ts | 6 +- .../motion-block-repository.service.spec.ts | 15 ++ .../motion-block-repository.service.ts | 127 +++++++++++ .../services/motion-multiselect.service.ts | 2 +- .../services/motion-repository.service.ts | 19 +- client/src/styles.scss | 56 +++-- 26 files changed, 1030 insertions(+), 38 deletions(-) create mode 100644 client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.html create mode 100644 client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.scss create mode 100644 client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.spec.ts create mode 100644 client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.ts create mode 100644 client/src/app/site/motions/components/motion-block-list/motion-block-list.component.html create mode 100644 client/src/app/site/motions/components/motion-block-list/motion-block-list.component.scss create mode 100644 client/src/app/site/motions/components/motion-block-list/motion-block-list.component.spec.ts create mode 100644 client/src/app/site/motions/components/motion-block-list/motion-block-list.component.ts create mode 100644 client/src/app/site/motions/models/view-motion-block.ts create mode 100644 client/src/app/site/motions/services/motion-block-repository.service.spec.ts create mode 100644 client/src/app/site/motions/services/motion-block-repository.service.ts diff --git a/client/src/app/shared/models/agenda/item.ts b/client/src/app/shared/models/agenda/item.ts index caff22036..0bf57ee3f 100644 --- a/client/src/app/shared/models/agenda/item.ts +++ b/client/src/app/shared/models/agenda/item.ts @@ -10,6 +10,16 @@ interface ContentObject { collection: string; } +/** + * Determine visibility states for agenda items + * Coming from "OpenSlidesConfigVariables" property "agenda_hide_internal_items_on_projector" + */ +export const itemVisibilityChoices = [ + { key: 1, name: 'Public item' }, + { key: 2, name: 'Internal item' }, + { key: 3, name: 'Hidden item' } +]; + /** * Representations of agenda Item * @ignore diff --git a/client/src/app/shared/models/motions/motion-block.ts b/client/src/app/shared/models/motions/motion-block.ts index d63010638..d2e6885bc 100644 --- a/client/src/app/shared/models/motions/motion-block.ts +++ b/client/src/app/shared/models/motions/motion-block.ts @@ -17,7 +17,12 @@ export class MotionBlock extends AgendaBaseModel { return this.title; } + /** + * Get the URL to the motion block + * + * @returns the URL as string + */ public getDetailStateURL(): string { - return 'TODO'; + return `/motions/blocks/${this.id}`; } } diff --git a/client/src/app/site/agenda/components/topic-detail/topic-detail.component.scss b/client/src/app/site/agenda/components/topic-detail/topic-detail.component.scss index 60a81d9f5..3f34e3e73 100644 --- a/client/src/app/site/agenda/components/topic-detail/topic-detail.component.scss +++ b/client/src/app/site/agenda/components/topic-detail/topic-detail.component.scss @@ -3,8 +3,7 @@ } .topic-title { - padding: 40px; - padding-left: 25px; + padding: 40px 0 40px 25px; line-height: 180%; font-size: 120%; color: #317796; // TODO: put in theme as $primary diff --git a/client/src/app/site/motions/components/category-list/category-list.component.html b/client/src/app/site/motions/components/category-list/category-list.component.html index c1cc0b933..f5ae99f48 100644 --- a/client/src/app/site/motions/components/category-list/category-list.component.html +++ b/client/src/app/site/motions/components/category-list/category-list.component.html @@ -66,7 +66,7 @@ {{ updateForm.get('name').value }} -
+
{{ motionsInCategory(category).length }}
diff --git a/client/src/app/site/motions/components/category-list/category-list.component.scss b/client/src/app/site/motions/components/category-list/category-list.component.scss index 1da7013bf..557b0fe47 100644 --- a/client/src/app/site/motions/components/category-list/category-list.component.scss +++ b/client/src/app/site/motions/components/category-list/category-list.component.scss @@ -31,13 +31,6 @@ .header-size { grid-column-start: 3; - border-radius: 50%; - width: 20px; - height: 20px; - padding: 3px; - background: lightgray; - color: #000; - text-align: center; } } diff --git a/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.html b/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.html new file mode 100644 index 000000000..24c78fe82 --- /dev/null +++ b/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.html @@ -0,0 +1,124 @@ + + +
+

{{ 'Motion block' | translate }} {{ block.id }}

+ +
+ + + +
+
+ + + +
+ + +
+

{{ block.title }}

+

{{ blockEditForm.get('title').value }}

+
+ + + + + + + + Motion + {{ motion.title }} + + + + + State + + + {{ motion.state.name | translate }} + + + + + + + Recommendation + + + {{ + motion.recommendation + ? (motion.recommendation.recommendation_label | translate) + : ('not set' | translate) + }} + + + + + + + + + + + + + + +
+
+ + + + + + + + + + diff --git a/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.scss b/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.scss new file mode 100644 index 000000000..2d4194ec0 --- /dev/null +++ b/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.scss @@ -0,0 +1,52 @@ +.block-title { + padding: 40px; + padding-left: 25px; + line-height: 180%; + font-size: 120%; + color: #317796; // TODO: put in theme as $primary + + h2 { + margin: 0; + font-weight: normal; + } +} + +.block-card { + margin: 0 20px 0 20px; + padding: 25px; + + button { + .mat-icon { + margin-right: 5px; + } + } +} + +.chip-container { + display: block; + height: 5em; + line-height: 5em; +} + +.os-headed-listview-table { + // Title + .mat-column-title { + flex: 4 0 0; + } + + // State + .mat-column-state { + flex: 2 0 0; + } + + // Recommendation + .mat-column-recommendation { + flex: 2 0 0; + } + + // Remove + .mat-column-remove { + flex: 1 0 0; + justify-content: flex-end !important; + } +} diff --git a/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.spec.ts b/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.spec.ts new file mode 100644 index 000000000..7e1a14553 --- /dev/null +++ b/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MotionBlockDetailComponent } from './motion-block-detail.component'; +import { E2EImportsModule } from 'e2e-imports.module'; + +describe('MotionBlockDetailComponent', () => { + let component: MotionBlockDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [MotionBlockDetailComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MotionBlockDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.ts b/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.ts new file mode 100644 index 000000000..39bd9f0a9 --- /dev/null +++ b/client/src/app/site/motions/components/motion-block-detail/motion-block-detail.component.ts @@ -0,0 +1,209 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { MatSnackBar } from '@angular/material'; + +import { TranslateService } from '@ngx-translate/core'; + +import { ListViewBaseComponent } from 'app/site/base/list-view-base'; +import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service'; +import { MotionRepositoryService } from '../../services/motion-repository.service'; +import { MotionBlock } from 'app/shared/models/motions/motion-block'; +import { ViewMotionBlock } from '../../models/view-motion-block'; +import { ViewMotion } from '../../models/view-motion'; +import { PromptService } from 'app/core/services/prompt.service'; + +/** + * Detail component to display one motion block + */ +@Component({ + selector: 'os-motion-block-detail', + templateUrl: './motion-block-detail.component.html', + styleUrls: ['./motion-block-detail.component.scss'] +}) +export class MotionBlockDetailComponent extends ListViewBaseComponent implements OnInit { + /** + * Determines the block id from the given URL + */ + public block: ViewMotionBlock; + + /** + * All motions in this block + */ + public motions: ViewMotion[]; + + /** + * Determine the edit mode + */ + public editBlock = false; + + /** + * The form to edit blocks + */ + @ViewChild('blockEditForm') + public blockEditForm: FormGroup; + + /** + * Constructor for motion block details + * + * @param titleService Setting the title + * @param translate translations + * @param matSnackBar showing errors + * @param router navigating + * @param route determine the blocks ID by the route + * @param repo the motion blocks repository + * @param motionRepo the motion repository + * @param promptService the displaying prompts before deleting + */ + public constructor( + titleService: Title, + translate: TranslateService, + matSnackBar: MatSnackBar, + private router: Router, + private route: ActivatedRoute, + private repo: MotionBlockRepositoryService, + private motionRepo: MotionRepositoryService, + private promptService: PromptService + ) { + super(titleService, translate, matSnackBar); + } + + /** + * Init function. + * Sets the title, observes the block and the motions belonging in this block + */ + public ngOnInit(): void { + super.setTitle('Motion Block'); + this.initTable(); + + this.blockEditForm = new FormGroup({ + title: new FormControl('', Validators.required) + }); + + const blockId = +this.route.snapshot.params.id; + this.block = this.repo.getViewModel(blockId); + + this.repo.getViewModelObservable(blockId).subscribe(newBlock => { + // necessary since the subscription can return undefined + if (newBlock) { + this.block = newBlock; + + // set the blocks title in the form + this.blockEditForm.get('title').setValue(this.block.title); + + this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(newMotions => { + this.motions = newMotions; + this.dataSource.data = this.motions; + }); + } + }); + } + + /** + * Get link to the list of speakers of the corresponding agenda item + * + * @returns the link to the list of speakers as string + */ + public getSpeakerLink(): string { + if (this.block) { + return `/agenda/${this.block.agenda_item_id}/speakers`; + } + } + + /** + * Returns the columns that should be shown in the table + * + * @returns an array of strings building the column definition + */ + public getColumnDefinition(): string[] { + return ['title', 'state', 'recommendation', 'remove']; + } + + /** + * Click handler for recommendation button + */ + public async onFollowRecButton(): Promise { + const content = this.translate.instant( + `Are you sure you want to override the state of all motions of this motion block?` + ); + if (await this.promptService.open(this.block.title, content)) { + for (const motion of this.motions) { + if (!motion.isInFinalState()) { + this.motionRepo.setState(motion, motion.recommendation_id); + } + } + } + } + + /** + * Click handler for the motion title cell in the table + * Navigate to the motion that was clicked on + * + * @param motion the selected ViewMotion + */ + public onClickMotionTitle(motion: ViewMotion): void { + this.router.navigate([`/motions/${motion.id}`]); + } + + /** + * Click handler to delete motion blocks + */ + public async onDeleteBlockButton(): Promise { + const content = this.translate.instant('Are you sure you want to delete this motion block?'); + if (await this.promptService.open(this.block.title, content)) { + await this.repo.delete(this.block); + this.router.navigate(['../'], { relativeTo: this.route }); + } + } + + /** + * Click handler for the delete button on the table + * + * @param motion the corresponding motion + */ + public async onRemoveMotionButton(motion: ViewMotion): Promise { + const content = this.translate.instant('Are you sure you want to remove this motion from motion block?'); + if (await this.promptService.open(motion.title, content)) { + this.repo.removeMotionFromBlock(motion); + } + } + + /** + * Clicking escape while in editForm should deactivate edit mode. + * + * @param event The key that was pressed + */ + public onKeyDown(event: KeyboardEvent): void { + if (event.key === 'Escape') { + this.editBlock = false; + } + } + + /** + * Determine if following the recommendations should be possible. + * Following a recommendation implies, that a valid recommendation exists. + */ + public isFollowingProhibited(): boolean { + if (this.motions) { + return this.motions.every(motion => motion.isInFinalState() || !motion.recommendation_id); + } else { + return false; + } + } + + /** + * Save event handler + */ + public saveBlock(): void { + this.editBlock = false; + this.repo.update(this.blockEditForm.value as MotionBlock, this.block); + } + + /** + * Click handler for the edit button + */ + public toggleEditMode(): void { + this.editBlock = !this.editBlock; + } +} diff --git a/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.html b/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.html new file mode 100644 index 000000000..85c5a78b0 --- /dev/null +++ b/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.html @@ -0,0 +1,77 @@ + + +

Motion blocks

+
+ + + + Create new motion block + +
+ +

+ + + + A name is required + + +

+ + +

+ +

+ + + + + + {{ type.name | translate }} + + + +
+
+ + + + + + +
+ + + + + + + Name + {{ block.title }} + + + + + Motions + + {{ getMotionAmount(block.motionBlock) }} + + + + + +
+
diff --git a/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.scss b/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.scss new file mode 100644 index 000000000..628d20945 --- /dev/null +++ b/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.scss @@ -0,0 +1,11 @@ +.os-headed-listview-table { + // Title + .mat-column-title { + flex: 3 0 0; + } + + // Amount + .mat-column-amount { + flex: 1 0 0; + } +} diff --git a/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.spec.ts b/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.spec.ts new file mode 100644 index 000000000..fcdd2943d --- /dev/null +++ b/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MotionBlockListComponent } from './motion-block-list.component'; +import { E2EImportsModule } from 'e2e-imports.module'; + +describe('MotionBlockListComponent', () => { + let component: MotionBlockListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [MotionBlockListComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MotionBlockListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.ts b/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.ts new file mode 100644 index 000000000..7af140972 --- /dev/null +++ b/client/src/app/site/motions/components/motion-block-list/motion-block-list.component.ts @@ -0,0 +1,169 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { MatSnackBar } from '@angular/material'; +import { BehaviorSubject } from 'rxjs'; + +import { TranslateService } from '@ngx-translate/core'; + +import { ListViewBaseComponent } from 'app/site/base/list-view-base'; +import { MotionBlock } from 'app/shared/models/motions/motion-block'; +import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item'; +import { DataStoreService } from 'app/core/services/data-store.service'; +import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service'; +import { ViewMotionBlock } from '../../models/view-motion-block'; + +/** + * Table for the motion blocks + */ +@Component({ + selector: 'os-motion-block-list', + templateUrl: './motion-block-list.component.html', + styleUrls: ['./motion-block-list.component.scss'] +}) +export class MotionBlockListComponent extends ListViewBaseComponent implements OnInit { + /** + * Holds the create form + */ + public createBlockForm: FormGroup; + + /** + * The new motion block to create + */ + public blockToCreate: MotionBlock | null; + + /** + * Holds the agenda items to select the parent item + */ + public items: BehaviorSubject; + + /** + * Determine the default agenda visibility + */ + public defaultVisibility: number; + + /** + * Determine visibility states for the agenda that will be created implicitly + */ + public itemVisibility = itemVisibilityChoices; + + /** + * Constructor for the motion block list view + * + * @param titleService sets the title + * @param translate translations + * @param matSnackBar display errors in the snack bar + * @param router routing to children + * @param route determine the local route + * @param repo the motion block repository + * @param DS the dataStore + * @param formBuilder creates forms + */ + public constructor( + titleService: Title, + translate: TranslateService, + matSnackBar: MatSnackBar, + private router: Router, + private route: ActivatedRoute, + private repo: MotionBlockRepositoryService, + private DS: DataStoreService, + private formBuilder: FormBuilder + ) { + super(titleService, translate, matSnackBar); + + this.createBlockForm = this.formBuilder.group({ + title: ['', Validators.required], + agenda_type: ['', Validators.required], + agenda_parent_id: [''] + }); + } + + /** + * Observe the agendaItems for changes. + */ + public ngOnInit(): void { + super.setTitle('Motion Blocks'); + this.initTable(); + + this.items = new BehaviorSubject(this.DS.getAll(Item)); + + this.DS.changeObservable.subscribe(model => { + if (model instanceof Item) { + this.items.next(this.DS.getAll(Item)); + } + }); + + this.repo.getViewModelListObservable().subscribe(newMotionblocks => { + this.dataSource.data = newMotionblocks; + }); + + this.repo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility)); + } + + /** + * Returns the columns that should be shown in the table + * + * @returns an array of strings building the column definition + */ + public getColumnDefinition(): string[] { + return ['title', 'amount']; + } + + /** + * Action while clicking on a row. Navigate to the detail page of given block + * + * @param block the given motion block + */ + public onSelectRow(block: ViewMotionBlock): void { + this.router.navigate([`${block.id}`], { relativeTo: this.route }); + } + + /** + * return the amount of motions in a motion block + * + * @param motionBlock the block to determine the amount of motions for + * @returns a number that indicates how many motions are in the given block + */ + public getMotionAmount(motionBlock: MotionBlock): number { + return this.repo.getMotionAmountByBlock(motionBlock); + } + + /** + * Helper function reset the form and set the default values + */ + public resetForm(): void { + this.createBlockForm.reset(); + this.createBlockForm.get('agenda_type').setValue(this.defaultVisibility); + } + + /** + * Click handler for the plus button + */ + public onPlusButton(): void { + if (!this.blockToCreate) { + this.resetForm(); + this.blockToCreate = new MotionBlock(); + } + } + + /** + * Click handler for the save button. + * Sends the block to create to the repository and resets the form. + */ + public onSaveNewButton(): void { + if (this.createBlockForm.valid) { + const blockPatch = this.createBlockForm.value; + if (!blockPatch.agenda_parent_id) { + delete blockPatch.agenda_parent_id; + } + + this.blockToCreate.patchValues(blockPatch); + this.repo.create(this.blockToCreate); + this.resetForm(); + this.blockToCreate = null; + } + // set a form control as "touched" to trigger potential error messages + this.createBlockForm.get('title').markAsTouched(); + } +} diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html index 3ab765f07..0c9604ca8 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html @@ -274,8 +274,12 @@

Category

-
+ +
+

Motion block

+ + + + + + + {{ motion.motion_block ? motion.motion_block : ('not set' | translate) }} + +
+
diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts index 7f77a963e..db016084c 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts @@ -31,6 +31,7 @@ import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators'; import { LocalPermissionsService } from '../../services/local-permissions.service'; import { ViewCreateMotion } from '../../models/view-create-motion'; import { CreateMotion } from '../../models/create-motion'; +import { MotionBlock } from 'app/shared/models/motions/motion-block'; /** * Component for the motion detail view @@ -178,6 +179,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { */ public supporterObserver: BehaviorSubject; + /** + * Subject for the motion blocks + */ + public blockObserver: BehaviorSubject; + /** * Determine if the name of supporters are visible */ @@ -249,6 +255,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.supporterObserver = new BehaviorSubject(DS.getAll(User)); this.categoryObserver = new BehaviorSubject(DS.getAll(Category)); this.workflowObserver = new BehaviorSubject(DS.getAll(Workflow)); + this.blockObserver = new BehaviorSubject(DS.getAll(MotionBlock)); // Make sure the subjects are updated, when a new Model for the type arrives this.DS.changeObservable.subscribe(newModel => { @@ -259,6 +266,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.categoryObserver.next(DS.getAll(Category)); } else if (newModel instanceof Workflow) { this.workflowObserver.next(DS.getAll(Workflow)); + } else if (newModel instanceof MotionBlock) { + this.blockObserver.next(DS.getAll(MotionBlock)); } }); // load config variables @@ -762,6 +771,15 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.repo.setCatetory(this.motion, id); } + /** + * Add the current motion to a motion block + * + * @param id Motion block id + */ + public setBlock(id: number): void { + this.repo.setBlock(this.motion, id); + } + /** * Observes the repository for changes in the motion recommender */ diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.html b/client/src/app/site/motions/components/motion-list/motion-list.component.html index 6572f18e9..6266c7af2 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.html @@ -72,7 +72,16 @@ State -
device_hub{{ motion.category }}
+
+
+ device_hub + {{ motion.category }} +
+
+ widgets + {{ motion.motion_block.title }} +
+
@@ -117,6 +126,15 @@ device_hub Categories + - diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.scss b/client/src/app/site/motions/components/motion-list/motion-list.component.scss index 85c3e8fcf..a85bf13af 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.scss +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.scss @@ -35,7 +35,6 @@ /** State */ .mat-column-state { flex: 0 0 160px; - justify-content:flex-end !important; mat-icon { font-size: 150%; diff --git a/client/src/app/site/motions/models/view-motion-block.ts b/client/src/app/site/motions/models/view-motion-block.ts new file mode 100644 index 000000000..8eb0c1aff --- /dev/null +++ b/client/src/app/site/motions/models/view-motion-block.ts @@ -0,0 +1,39 @@ +import { BaseViewModel } from 'app/site/base/base-view-model'; +import { MotionBlock } from 'app/shared/models/motions/motion-block'; + +/** + * ViewModel for motion blocks. + * @ignore + */ +export class ViewMotionBlock extends BaseViewModel { + private _motionBlock: MotionBlock; + + public get motionBlock(): MotionBlock { + return this._motionBlock; + } + + public get id(): number { + return this.motionBlock ? this.motionBlock.id : null; + } + + public get title(): string { + return this.motionBlock ? this.motionBlock.title : null; + } + + public get agenda_item_id(): number { + return this.motionBlock ? this.motionBlock.agenda_item_id : null; + } + + public constructor(motionBlock: MotionBlock) { + super(); + this._motionBlock = motionBlock; + } + + public updateValues(update: MotionBlock): void { + this._motionBlock = update; + } + + public getTitle(): string { + return this.title + } +} diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 006ad77b8..ea85c1f9d 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -347,6 +347,13 @@ export class ViewMotion extends BaseViewModel { return !!this.statute_paragraph_id; } + /** + * Determine if the motion is in its final workflow state + */ + public isInFinalState(): boolean { + return this.nextStates.length === 0; + } + /** * It's a paragraph-based amendments if only one paragraph is to be changed, * specified by amendment_paragraphs-array diff --git a/client/src/app/site/motions/motions-routing.module.ts b/client/src/app/site/motions/motions-routing.module.ts index 8273351e1..8e1dcfd24 100644 --- a/client/src/app/site/motions/motions-routing.module.ts +++ b/client/src/app/site/motions/motions-routing.module.ts @@ -8,6 +8,8 @@ import { StatuteParagraphListComponent } from './components/statute-paragraph-li import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-list.component'; import { CallListComponent } from './components/call-list/call-list.component'; import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.component'; +import { MotionBlockListComponent } from './components/motion-block-list/motion-block-list.component'; +import { MotionBlockDetailComponent } from './components/motion-block-detail/motion-block-detail.component'; const routes: Routes = [ { path: '', component: MotionListComponent }, @@ -15,6 +17,8 @@ const routes: Routes = [ { path: 'comment-section', component: MotionCommentSectionListComponent }, { path: 'statute-paragraphs', component: StatuteParagraphListComponent }, { path: 'call-list', component: CallListComponent }, + { path: 'blocks', component: MotionBlockListComponent }, + { path: 'blocks/:id', component: MotionBlockDetailComponent }, { path: 'new', component: MotionDetailComponent }, { path: ':id', component: MotionDetailComponent }, { path: ':id/speakers', component: SpeakerListComponent }, diff --git a/client/src/app/site/motions/motions.module.ts b/client/src/app/site/motions/motions.module.ts index 15d0e77d9..522551adf 100644 --- a/client/src/app/site/motions/motions.module.ts +++ b/client/src/app/site/motions/motions.module.ts @@ -16,6 +16,8 @@ import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-b import { PersonalNoteComponent } from './components/personal-note/personal-note.component'; import { CallListComponent } from './components/call-list/call-list.component'; import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.component'; +import { MotionBlockListComponent } from './components/motion-block-list/motion-block-list.component'; +import { MotionBlockDetailComponent } from './components/motion-block-detail/motion-block-detail.component'; @NgModule({ imports: [CommonModule, MotionsRoutingModule, SharedModule], @@ -32,7 +34,9 @@ import { AmendmentCreateWizardComponent } from './components/amendment-create-wi MetaTextBlockComponent, PersonalNoteComponent, CallListComponent, - AmendmentCreateWizardComponent + AmendmentCreateWizardComponent, + MotionBlockListComponent, + MotionBlockDetailComponent ], entryComponents: [ MotionChangeRecommendationComponent, diff --git a/client/src/app/site/motions/services/motion-block-repository.service.spec.ts b/client/src/app/site/motions/services/motion-block-repository.service.spec.ts new file mode 100644 index 000000000..880756dc5 --- /dev/null +++ b/client/src/app/site/motions/services/motion-block-repository.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed } from '@angular/core/testing'; + +import { MotionBlockRepositoryService } from './motion-block-repository.service'; +import { E2EImportsModule } from 'e2e-imports.module'; + +describe('MotionBlockRepositoryService', () => { + beforeEach(() => TestBed.configureTestingModule({ + imports: [E2EImportsModule] + })); + + it('should be created', () => { + const service: MotionBlockRepositoryService = TestBed.get(MotionBlockRepositoryService); + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/motions/services/motion-block-repository.service.ts b/client/src/app/site/motions/services/motion-block-repository.service.ts new file mode 100644 index 000000000..cd82315b6 --- /dev/null +++ b/client/src/app/site/motions/services/motion-block-repository.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@angular/core'; + +import { MotionBlock } from 'app/shared/models/motions/motion-block'; +import { ViewMotionBlock } from '../models/view-motion-block'; +import { BaseRepository } from 'app/site/base/base-repository'; +import { DataStoreService } from 'app/core/services/data-store.service'; +import { CollectionStringModelMapperService } from 'app/core/services/collectionStringModelMapper.service'; +import { DataSendService } from 'app/core/services/data-send.service'; +import { Identifiable } from 'app/shared/models/base/identifiable'; +import { Motion } from 'app/shared/models/motions/motion'; +import { ViewMotion } from '../models/view-motion'; +import { Observable } from 'rxjs'; +import { MotionRepositoryService } from './motion-repository.service'; +import { map } from 'rxjs/operators'; +import { ConfigService } from 'app/core/services/config.service'; + +/** + * Repository service for motion blocks + */ +@Injectable({ + providedIn: 'root' +}) +export class MotionBlockRepositoryService extends BaseRepository { + /** + * Constructor for the motion block repository + * + * @param DS Data Store + * @param mapperService Mapping collection strings to classes + * @param dataSend Send models to the server + * @param motionRepo Accessing the motion repository + * @param config To access config variables + */ + public constructor( + DS: DataStoreService, + mapperService: CollectionStringModelMapperService, + private dataSend: DataSendService, + private motionRepo: MotionRepositoryService, + private config: ConfigService + ) { + super(DS, mapperService, MotionBlock); + } + + /** + * Updates a given motion block + * + * @param update a partial motion block containing the update data + * @param viewBlock the motion block to update + */ + public async update(update: Partial, viewBlock: ViewMotionBlock): Promise { + const updateMotionBlock = new MotionBlock(); + updateMotionBlock.patchValues(viewBlock.motionBlock); + updateMotionBlock.patchValues(update); + return await this.dataSend.updateModel(updateMotionBlock); + } + + /** + * Deletes a motion block from the server + * + * @param newBlock the motion block to delete + */ + public async delete(newBlock: ViewMotionBlock): Promise { + return await this.dataSend.deleteModel(newBlock.motionBlock); + } + + /** + * Creates a new motion block to the server + * + * @param newBlock The new block to create + * @returns the ID of the created model as promise + */ + public async create(newBlock: MotionBlock): Promise { + return await this.dataSend.createModel(newBlock); + } + + /** + * Converts a given motion block into a ViewModel + * + * @param block a motion block + * @returns a new ViewMotionBlock + */ + protected createViewModel(block: MotionBlock): ViewMotionBlock { + return new ViewMotionBlock(block); + } + + /** + * Removes the motion block id from the given motion + * + * @param viewMotion The motion to alter + */ + public removeMotionFromBlock(viewMotion: ViewMotion): void { + const updateMotion = viewMotion.motion; + updateMotion.motion_block_id = null; + this.motionRepo.update(updateMotion, viewMotion); + } + + /** + * Filter the DataStore by Motions and returns the + * + * @param block the motion block + * @returns the number of motions inside a motion block + */ + public getMotionAmountByBlock(block: MotionBlock): number { + return this.DS.filter(Motion, motion => motion.motion_block_id === block.id).length; + } + + /** + * Get agenda visibility from the config + * + * @return An observable to the default agenda visibility + */ + public getDefaultAgendaVisibility(): Observable { + return this.config.get('agenda_new_items_default_visibility').pipe(map(key => +key)); + } + + /** + * Observe the motion repository and return the motions belonging to the given + * block as observable + * + * @param block a motion block + * @returns an observable to view motions + */ + public getViewMotionsByBlock(block: MotionBlock): Observable { + return this.motionRepo + .getViewModelListObservable() + .pipe(map(viewMotions => viewMotions.filter(viewMotion => viewMotion.motion_block_id === block.id))); + } +} diff --git a/client/src/app/site/motions/services/motion-multiselect.service.ts b/client/src/app/site/motions/services/motion-multiselect.service.ts index 180c889a1..2b4d9e0f4 100644 --- a/client/src/app/site/motions/services/motion-multiselect.service.ts +++ b/client/src/app/site/motions/services/motion-multiselect.service.ts @@ -82,7 +82,7 @@ export class MotionMultiselectService { * * @param motions The motions to change */ - public async setStatus(motions: ViewMotion[]): Promise { + public async setStateOfMultiple(motions: ViewMotion[]): Promise { const title = this.translate.instant('This will set the state of all selected motions to:'); const choices = this.workflowRepo.getAllWorkflowStates().map(workflowState => ({ id: workflowState.id, diff --git a/client/src/app/site/motions/services/motion-repository.service.ts b/client/src/app/site/motions/services/motion-repository.service.ts index 19511c777..7d47eb1aa 100644 --- a/client/src/app/site/motions/services/motion-repository.service.ts +++ b/client/src/app/site/motions/services/motion-repository.service.ts @@ -26,6 +26,7 @@ import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree import { TreeService } from 'app/core/services/tree.service'; import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph'; import { CreateMotion } from '../models/create-motion'; +import { MotionBlock } from 'app/shared/models/motions/motion-block'; /** * Repository Services for motions (and potentially categories) @@ -41,7 +42,6 @@ import { CreateMotion } from '../models/create-motion'; providedIn: 'root' }) export class MotionRepositoryService extends BaseRepository { - /** * Creates a MotionRepository * @@ -64,7 +64,7 @@ export class MotionRepositoryService extends BaseRepository private readonly diff: DiffService, private treeService: TreeService ) { - super(DS, mapperService, Motion, [Category, User, Workflow, Item]); + super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock]); } /** @@ -81,11 +81,12 @@ export class MotionRepositoryService extends BaseRepository const supporters = this.DS.getMany(User, motion.supporters_id); const workflow = this.DS.get(Workflow, motion.workflow_id); const item = this.DS.get(Item, motion.agenda_item_id); + const block = this.DS.get(MotionBlock, motion.motion_block_id); let state: WorkflowState = null; if (workflow) { state = workflow.getStateById(motion.state_id); } - return new ViewMotion(motion, category, submitters, supporters, workflow, state, item); + return new ViewMotion(motion, category, submitters, supporters, workflow, state, item, block); } /** @@ -179,6 +180,18 @@ export class MotionRepositoryService extends BaseRepository await this.update(motion, viewMotion); } + /** + * Add the motion to a motion block + * + * @param viewMotion the motion to add + * @param blockId the ID of the motion block + */ + public async setBlock(viewMotion: ViewMotion, blockId: number): Promise { + const motion = viewMotion.motion; + motion.motion_block_id = blockId; + await this.update(motion, viewMotion); + } + /** * Sends the changed nodes to the server. * diff --git a/client/src/styles.scss b/client/src/styles.scss index feb51201a..64229d64b 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -16,6 +16,29 @@ @import '~angular-tree-component/dist/angular-tree-component.css'; +// Shared scss definitions +%os-table { + width: 100%; + + /** size of the mat row */ + mat-row { + height: 60px; + } + + mat-row:hover { + cursor: pointer; + background-color: rgba(0, 0, 0, 0.025); + } + + mat-row.selected { + cursor: pointer; + background-color: rgba(0, 0, 0, 0.055); + } + mat-row.lg { + height: 90px; + } +} + * { font-family: Roboto, Arial, Helvetica, sans-serif; } @@ -98,29 +121,16 @@ body { } .os-listview-table { - width: 100%; + @extend %os-table; /** hide mat header row */ .mat-header-row { display: none; } +} - /** size of the mat row */ - mat-row { - height: 60px; - } - - mat-row:hover { - cursor: pointer; - background-color: rgba(0, 0, 0, 0.025); - } - mat-row.selected { - cursor: pointer; - background-color: rgba(0, 0, 0, 0.055); - } - mat-row.lg { - height: 90px; - } +.os-headed-listview-table { + @extend %os-table; } .card-plus-distance { @@ -204,6 +214,18 @@ mat-panel-title mat-icon { margin: 8px 8px 8px 0; } +// to display quantities. Use in span or div +.os-amount-chip { + border-radius: 50%; + width: 20px; + height: 20px; + line-height: 20px; + padding: 3px; + background: lightgray; + color: #000; + text-align: center; +} + .mat-chip:focus, .mat-basic-chip:focus { outline: none;