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 }}
-
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+ {{ 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
+
+
+ widgets
+ Motion blocks
+
account_balance
Statute
@@ -138,7 +156,7 @@
sort
Move to agenda item
-
+
label
Set status
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;