Merge pull request #4049 from tsiegleauq/motion-block
Add motion blocks
This commit is contained in:
commit
527382367a
@ -10,6 +10,16 @@ interface ContentObject {
|
|||||||
collection: string;
|
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
|
* Representations of agenda Item
|
||||||
* @ignore
|
* @ignore
|
||||||
|
@ -17,7 +17,12 @@ export class MotionBlock extends AgendaBaseModel {
|
|||||||
return this.title;
|
return this.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL to the motion block
|
||||||
|
*
|
||||||
|
* @returns the URL as string
|
||||||
|
*/
|
||||||
public getDetailStateURL(): string {
|
public getDetailStateURL(): string {
|
||||||
return 'TODO';
|
return `/motions/blocks/${this.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.topic-title {
|
.topic-title {
|
||||||
padding: 40px;
|
padding: 40px 0 40px 25px;
|
||||||
padding-left: 25px;
|
|
||||||
line-height: 180%;
|
line-height: 180%;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
color: #317796; // TODO: put in theme as $primary
|
color: #317796; // TODO: put in theme as $primary
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
{{ updateForm.get('name').value }}
|
{{ updateForm.get('name').value }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-size">
|
<div class="header-size os-amount-chip">
|
||||||
{{ motionsInCategory(category).length }}
|
{{ motionsInCategory(category).length }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,13 +31,6 @@
|
|||||||
|
|
||||||
.header-size {
|
.header-size {
|
||||||
grid-column-start: 3;
|
grid-column-start: 3;
|
||||||
border-radius: 50%;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
padding: 3px;
|
|
||||||
background: lightgray;
|
|
||||||
color: #000;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
<os-head-bar
|
||||||
|
mainButtonIcon="edit"
|
||||||
|
[nav]="false"
|
||||||
|
[mainButton]="true"
|
||||||
|
[editMode]="editBlock"
|
||||||
|
(mainEvent)="toggleEditMode()"
|
||||||
|
(saveEvent)="saveBlock()"
|
||||||
|
>
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot">
|
||||||
|
<h2 *ngIf="block && !editBlock">{{ 'Motion block' | translate }} {{ block.id }}</h2>
|
||||||
|
|
||||||
|
<form [formGroup]="blockEditForm" (ngSubmit)="saveBlock()" (keydown)="onKeyDown($event)" *ngIf="editBlock">
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
matInput
|
||||||
|
osAutofocus
|
||||||
|
required
|
||||||
|
formControlName="title"
|
||||||
|
placeholder="{{ 'Title' | translate }}"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<div class="menu-slot">
|
||||||
|
<button type="button" mat-icon-button [matMenuTriggerFor]="motionBlockMenu">
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div *ngIf="block" class="block-title on-transition-fade">
|
||||||
|
<h2 *ngIf="!editBlock">{{ block.title }}</h2>
|
||||||
|
<h2 *ngIf="editBlock">{{ blockEditForm.get('title').value }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-card class="block-card">
|
||||||
|
<button mat-raised-button color="primary" (click)="onFollowRecButton()" [disabled]="isFollowingProhibited()">
|
||||||
|
<mat-icon>done_all</mat-icon>
|
||||||
|
<span translate>Follow recommendations for all motions</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource" matSort>
|
||||||
|
<!-- title column -->
|
||||||
|
<ng-container matColumnDef="title">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <span translate>Motion</span> </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let motion" (click)="onClickMotionTitle(motion)"> {{ motion.title }} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- state column -->
|
||||||
|
<ng-container matColumnDef="state">
|
||||||
|
<mat-header-cell *matHeaderCellDef> <span translate>State</span> </mat-header-cell>
|
||||||
|
<mat-cell class="chip-container" *matCellDef="let motion">
|
||||||
|
<mat-basic-chip
|
||||||
|
disableRipple
|
||||||
|
[ngClass]="{
|
||||||
|
green: motion.state.css_class === 'success',
|
||||||
|
red: motion.state.css_class === 'danger',
|
||||||
|
grey: motion.state.css_class === 'default',
|
||||||
|
lightblue: motion.state.css_class === 'primary'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ motion.state.name | translate }}
|
||||||
|
</mat-basic-chip>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Recommendation column -->
|
||||||
|
<ng-container matColumnDef="recommendation">
|
||||||
|
<mat-header-cell *matHeaderCellDef> <span translate>Recommendation</span> </mat-header-cell>
|
||||||
|
<mat-cell class="chip-container" *matCellDef="let motion">
|
||||||
|
<mat-basic-chip disableRipple class="bluegrey">
|
||||||
|
{{
|
||||||
|
motion.recommendation
|
||||||
|
? (motion.recommendation.recommendation_label | translate)
|
||||||
|
: ('not set' | translate)
|
||||||
|
}}
|
||||||
|
</mat-basic-chip>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Remove motion column -->
|
||||||
|
<ng-container matColumnDef="remove">
|
||||||
|
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let motion">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
color="warn"
|
||||||
|
matTooltip="{{ 'Remove from motion block' | translate }}"
|
||||||
|
(click)="onRemoveMotionButton(motion)"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: getColumnDefinition()"> </mat-row>
|
||||||
|
</table>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- The menu content -->
|
||||||
|
<mat-menu #motionBlockMenu="matMenu">
|
||||||
|
<button mat-menu-item [routerLink]="getSpeakerLink()">
|
||||||
|
<mat-icon>mic</mat-icon>
|
||||||
|
<span translate>List of speakers</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button mat-menu-item>
|
||||||
|
<mat-icon>videocam</mat-icon>
|
||||||
|
<span translate>Project</span>
|
||||||
|
</button>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<button mat-menu-item class="red-warning-text" (click)="onDeleteBlockButton()">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
<span translate>Delete</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<MotionBlockDetailComponent>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
@ -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<ViewMotion> 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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
<os-head-bar [nav]="false" [mainButton]="true" (mainEvent)="onPlusButton()">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot"><h2 translate>Motion blocks</h2></div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<!-- Creating a new motion block -->
|
||||||
|
<mat-card class="os-card" *ngIf="blockToCreate">
|
||||||
|
<mat-card-title translate>Create new motion block</mat-card-title>
|
||||||
|
<mat-card-content>
|
||||||
|
<form [formGroup]="createBlockForm">
|
||||||
|
<!-- Title -->
|
||||||
|
<p>
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
formControlName="title"
|
||||||
|
matInput
|
||||||
|
placeholder="{{ 'Title' | translate }}"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="createBlockForm.get('title').hasError('required')" translate>
|
||||||
|
A name is required
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Parent item -->
|
||||||
|
<p>
|
||||||
|
<os-search-value-selector
|
||||||
|
ngDefaultControl
|
||||||
|
listname="{{ 'Parent Item' | translate }}"
|
||||||
|
[form]="createBlockForm"
|
||||||
|
[formControl]="createBlockForm.get('agenda_parent_id')"
|
||||||
|
[multiple]="false"
|
||||||
|
[includeNone]="true"
|
||||||
|
[InputListValues]="items"
|
||||||
|
></os-search-value-selector>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Visibility -->
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-select formControlName="agenda_type" placeholder="{{ 'Agenda visibility' | translate }}">
|
||||||
|
<mat-option *ngFor="let type of itemVisibility" [value]="type.key">
|
||||||
|
<span>{{ type.name | translate }}</span>
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
|
||||||
|
<!-- Save and Cancel buttons -->
|
||||||
|
<mat-card-actions>
|
||||||
|
<button mat-button (click)="onSaveNewButton()"><span translate>Save</span></button>
|
||||||
|
<button mat-button (click)="blockToCreate = null"><span translate>Cancel</span></button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<mat-card class="os-card">
|
||||||
|
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource" matSort>
|
||||||
|
<!-- title column -->
|
||||||
|
<ng-container matColumnDef="title">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <span translate>Name</span> </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let block"> {{ block.title }} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- amount column -->
|
||||||
|
<ng-container matColumnDef="amount">
|
||||||
|
<mat-header-cell *matHeaderCellDef> <span translate>Motions</span> </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let block">
|
||||||
|
<span class="os-amount-chip">{{ getMotionAmount(block.motionBlock) }}</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||||
|
<mat-row (click)="onSelectRow(row)" *matRowDef="let row; columns: getColumnDefinition()"> </mat-row>
|
||||||
|
</table>
|
||||||
|
</mat-card>
|
@ -0,0 +1,11 @@
|
|||||||
|
.os-headed-listview-table {
|
||||||
|
// Title
|
||||||
|
.mat-column-title {
|
||||||
|
flex: 3 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
.mat-column-amount {
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
}
|
@ -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<MotionBlockListComponent>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
@ -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<ViewMotionBlock> 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<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
@ -274,8 +274,12 @@
|
|||||||
<div *ngIf="motion && !editMotion">
|
<div *ngIf="motion && !editMotion">
|
||||||
<h4 translate>Category</h4>
|
<h4 translate>Category</h4>
|
||||||
<mat-menu #categoryMenu='matMenu'>
|
<mat-menu #categoryMenu='matMenu'>
|
||||||
<button *ngFor='let category of categoryObserver.value' mat-menu-item
|
<button
|
||||||
(click)=setCategory(category.id)>{{ category }}
|
mat-menu-item
|
||||||
|
*ngFor="let category of categoryObserver.value"
|
||||||
|
(click)="setCategory(category.id)"
|
||||||
|
>
|
||||||
|
{{ category }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)=setCategory(null)>
|
<button mat-menu-item (click)=setCategory(null)>
|
||||||
---
|
---
|
||||||
@ -286,6 +290,27 @@
|
|||||||
</mat-basic-chip>
|
</mat-basic-chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Block -->
|
||||||
|
<div *ngIf="motion && !editMotion">
|
||||||
|
<h4 translate>Motion block</h4>
|
||||||
|
|
||||||
|
<mat-menu #blockMenu='matMenu'>
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
*ngFor="let block of blockObserver.value"
|
||||||
|
(click)="setBlock(block.id)"
|
||||||
|
>
|
||||||
|
{{ block }}
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="setBlock(null)">
|
||||||
|
---
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
<mat-basic-chip [matMenuTriggerFor]='blockMenu' class="grey">
|
||||||
|
{{ motion.motion_block ? motion.motion_block : ('not set' | translate) }}
|
||||||
|
</mat-basic-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Workflow -->
|
<!-- Workflow -->
|
||||||
<div *ngIf="editMotion">
|
<div *ngIf="editMotion">
|
||||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||||
|
@ -31,6 +31,7 @@ import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators';
|
|||||||
import { LocalPermissionsService } from '../../services/local-permissions.service';
|
import { LocalPermissionsService } from '../../services/local-permissions.service';
|
||||||
import { ViewCreateMotion } from '../../models/view-create-motion';
|
import { ViewCreateMotion } from '../../models/view-create-motion';
|
||||||
import { CreateMotion } from '../../models/create-motion';
|
import { CreateMotion } from '../../models/create-motion';
|
||||||
|
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for the motion detail view
|
* Component for the motion detail view
|
||||||
@ -178,6 +179,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public supporterObserver: BehaviorSubject<User[]>;
|
public supporterObserver: BehaviorSubject<User[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject for the motion blocks
|
||||||
|
*/
|
||||||
|
public blockObserver: BehaviorSubject<MotionBlock[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the name of supporters are visible
|
* 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.supporterObserver = new BehaviorSubject(DS.getAll(User));
|
||||||
this.categoryObserver = new BehaviorSubject(DS.getAll(Category));
|
this.categoryObserver = new BehaviorSubject(DS.getAll(Category));
|
||||||
this.workflowObserver = new BehaviorSubject(DS.getAll(Workflow));
|
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
|
// Make sure the subjects are updated, when a new Model for the type arrives
|
||||||
this.DS.changeObservable.subscribe(newModel => {
|
this.DS.changeObservable.subscribe(newModel => {
|
||||||
@ -259,6 +266,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
this.categoryObserver.next(DS.getAll(Category));
|
this.categoryObserver.next(DS.getAll(Category));
|
||||||
} else if (newModel instanceof Workflow) {
|
} else if (newModel instanceof Workflow) {
|
||||||
this.workflowObserver.next(DS.getAll(Workflow));
|
this.workflowObserver.next(DS.getAll(Workflow));
|
||||||
|
} else if (newModel instanceof MotionBlock) {
|
||||||
|
this.blockObserver.next(DS.getAll(MotionBlock));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// load config variables
|
// load config variables
|
||||||
@ -762,6 +771,15 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
this.repo.setCatetory(this.motion, id);
|
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
|
* Observes the repository for changes in the motion recommender
|
||||||
*/
|
*/
|
||||||
|
@ -72,7 +72,16 @@
|
|||||||
<ng-container matColumnDef="state">
|
<ng-container matColumnDef="state">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let motion">
|
<mat-cell *matCellDef="let motion">
|
||||||
<div *ngIf="motion.category" class="small"><mat-icon>device_hub</mat-icon>{{ motion.category }}</div>
|
<div class="innerTable">
|
||||||
|
<div class="small" *ngIf="motion.category">
|
||||||
|
<mat-icon>device_hub</mat-icon>
|
||||||
|
{{ motion.category }}
|
||||||
|
</div>
|
||||||
|
<div class="small" *ngIf="motion.motion_block">
|
||||||
|
<mat-icon>widgets</mat-icon>
|
||||||
|
{{ motion.motion_block.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@ -117,6 +126,15 @@
|
|||||||
<mat-icon>device_hub</mat-icon>
|
<mat-icon>device_hub</mat-icon>
|
||||||
<span translate>Categories</span>
|
<span translate>Categories</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item routerLink="blocks">
|
||||||
|
<!-- possible icons:
|
||||||
|
dashboard
|
||||||
|
widgets
|
||||||
|
view_module
|
||||||
|
-->
|
||||||
|
<mat-icon>widgets</mat-icon>
|
||||||
|
<span translate>Motion blocks</span>
|
||||||
|
</button>
|
||||||
<button mat-menu-item routerLink="statute-paragraphs" *ngIf="statutesEnabled">
|
<button mat-menu-item routerLink="statute-paragraphs" *ngIf="statutesEnabled">
|
||||||
<mat-icon>account_balance</mat-icon>
|
<mat-icon>account_balance</mat-icon>
|
||||||
<span translate>Statute</span>
|
<span translate>Statute</span>
|
||||||
@ -138,7 +156,7 @@
|
|||||||
<mat-icon>sort</mat-icon>
|
<mat-icon>sort</mat-icon>
|
||||||
<span translate>Move to agenda item</span>
|
<span translate>Move to agenda item</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setStatus(selectedRows))">
|
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setStateOfMultiple(selectedRows))">
|
||||||
<mat-icon>label</mat-icon>
|
<mat-icon>label</mat-icon>
|
||||||
<span translate>Set status</span>
|
<span translate>Set status</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -35,7 +35,6 @@
|
|||||||
/** State */
|
/** State */
|
||||||
.mat-column-state {
|
.mat-column-state {
|
||||||
flex: 0 0 160px;
|
flex: 0 0 160px;
|
||||||
justify-content:flex-end !important;
|
|
||||||
|
|
||||||
mat-icon {
|
mat-icon {
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
|
39
client/src/app/site/motions/models/view-motion-block.ts
Normal file
39
client/src/app/site/motions/models/view-motion-block.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -347,6 +347,13 @@ export class ViewMotion extends BaseViewModel {
|
|||||||
return !!this.statute_paragraph_id;
|
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,
|
* It's a paragraph-based amendments if only one paragraph is to be changed,
|
||||||
* specified by amendment_paragraphs-array
|
* specified by amendment_paragraphs-array
|
||||||
|
@ -8,6 +8,8 @@ import { StatuteParagraphListComponent } from './components/statute-paragraph-li
|
|||||||
import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-list.component';
|
import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-list.component';
|
||||||
import { CallListComponent } from './components/call-list/call-list.component';
|
import { CallListComponent } from './components/call-list/call-list.component';
|
||||||
import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.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 = [
|
const routes: Routes = [
|
||||||
{ path: '', component: MotionListComponent },
|
{ path: '', component: MotionListComponent },
|
||||||
@ -15,6 +17,8 @@ const routes: Routes = [
|
|||||||
{ path: 'comment-section', component: MotionCommentSectionListComponent },
|
{ path: 'comment-section', component: MotionCommentSectionListComponent },
|
||||||
{ path: 'statute-paragraphs', component: StatuteParagraphListComponent },
|
{ path: 'statute-paragraphs', component: StatuteParagraphListComponent },
|
||||||
{ path: 'call-list', component: CallListComponent },
|
{ path: 'call-list', component: CallListComponent },
|
||||||
|
{ path: 'blocks', component: MotionBlockListComponent },
|
||||||
|
{ path: 'blocks/:id', component: MotionBlockDetailComponent },
|
||||||
{ path: 'new', component: MotionDetailComponent },
|
{ path: 'new', component: MotionDetailComponent },
|
||||||
{ path: ':id', component: MotionDetailComponent },
|
{ path: ':id', component: MotionDetailComponent },
|
||||||
{ path: ':id/speakers', component: SpeakerListComponent },
|
{ path: ':id/speakers', component: SpeakerListComponent },
|
||||||
|
@ -16,6 +16,8 @@ import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-b
|
|||||||
import { PersonalNoteComponent } from './components/personal-note/personal-note.component';
|
import { PersonalNoteComponent } from './components/personal-note/personal-note.component';
|
||||||
import { CallListComponent } from './components/call-list/call-list.component';
|
import { CallListComponent } from './components/call-list/call-list.component';
|
||||||
import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.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({
|
@NgModule({
|
||||||
imports: [CommonModule, MotionsRoutingModule, SharedModule],
|
imports: [CommonModule, MotionsRoutingModule, SharedModule],
|
||||||
@ -32,7 +34,9 @@ import { AmendmentCreateWizardComponent } from './components/amendment-create-wi
|
|||||||
MetaTextBlockComponent,
|
MetaTextBlockComponent,
|
||||||
PersonalNoteComponent,
|
PersonalNoteComponent,
|
||||||
CallListComponent,
|
CallListComponent,
|
||||||
AmendmentCreateWizardComponent
|
AmendmentCreateWizardComponent,
|
||||||
|
MotionBlockListComponent,
|
||||||
|
MotionBlockDetailComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
MotionChangeRecommendationComponent,
|
MotionChangeRecommendationComponent,
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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<ViewMotionBlock, MotionBlock> {
|
||||||
|
/**
|
||||||
|
* 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<MotionBlock>, viewBlock: ViewMotionBlock): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<Identifiable> {
|
||||||
|
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<number> {
|
||||||
|
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<ViewMotion[]> {
|
||||||
|
return this.motionRepo
|
||||||
|
.getViewModelListObservable()
|
||||||
|
.pipe(map(viewMotions => viewMotions.filter(viewMotion => viewMotion.motion_block_id === block.id)));
|
||||||
|
}
|
||||||
|
}
|
@ -82,7 +82,7 @@ export class MotionMultiselectService {
|
|||||||
*
|
*
|
||||||
* @param motions The motions to change
|
* @param motions The motions to change
|
||||||
*/
|
*/
|
||||||
public async setStatus(motions: ViewMotion[]): Promise<void> {
|
public async setStateOfMultiple(motions: ViewMotion[]): Promise<void> {
|
||||||
const title = this.translate.instant('This will set the state of all selected motions to:');
|
const title = this.translate.instant('This will set the state of all selected motions to:');
|
||||||
const choices = this.workflowRepo.getAllWorkflowStates().map(workflowState => ({
|
const choices = this.workflowRepo.getAllWorkflowStates().map(workflowState => ({
|
||||||
id: workflowState.id,
|
id: workflowState.id,
|
||||||
|
@ -26,6 +26,7 @@ import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree
|
|||||||
import { TreeService } from 'app/core/services/tree.service';
|
import { TreeService } from 'app/core/services/tree.service';
|
||||||
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
||||||
import { CreateMotion } from '../models/create-motion';
|
import { CreateMotion } from '../models/create-motion';
|
||||||
|
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository Services for motions (and potentially categories)
|
* Repository Services for motions (and potentially categories)
|
||||||
@ -41,7 +42,6 @@ import { CreateMotion } from '../models/create-motion';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion> {
|
export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a MotionRepository
|
* Creates a MotionRepository
|
||||||
*
|
*
|
||||||
@ -64,7 +64,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
|||||||
private readonly diff: DiffService,
|
private readonly diff: DiffService,
|
||||||
private treeService: TreeService
|
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<ViewMotion, Motion>
|
|||||||
const supporters = this.DS.getMany(User, motion.supporters_id);
|
const supporters = this.DS.getMany(User, motion.supporters_id);
|
||||||
const workflow = this.DS.get(Workflow, motion.workflow_id);
|
const workflow = this.DS.get(Workflow, motion.workflow_id);
|
||||||
const item = this.DS.get(Item, motion.agenda_item_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;
|
let state: WorkflowState = null;
|
||||||
if (workflow) {
|
if (workflow) {
|
||||||
state = workflow.getStateById(motion.state_id);
|
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<ViewMotion, Motion>
|
|||||||
await this.update(motion, viewMotion);
|
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<void> {
|
||||||
|
const motion = viewMotion.motion;
|
||||||
|
motion.motion_block_id = blockId;
|
||||||
|
await this.update(motion, viewMotion);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the changed nodes to the server.
|
* Sends the changed nodes to the server.
|
||||||
*
|
*
|
||||||
|
@ -16,6 +16,29 @@
|
|||||||
|
|
||||||
@import '~angular-tree-component/dist/angular-tree-component.css';
|
@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;
|
font-family: Roboto, Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
@ -98,29 +121,16 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.os-listview-table {
|
.os-listview-table {
|
||||||
width: 100%;
|
@extend %os-table;
|
||||||
|
|
||||||
/** hide mat header row */
|
/** hide mat header row */
|
||||||
.mat-header-row {
|
.mat-header-row {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** size of the mat row */
|
|
||||||
mat-row {
|
|
||||||
height: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-row:hover {
|
.os-headed-listview-table {
|
||||||
cursor: pointer;
|
@extend %os-table;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-plus-distance {
|
.card-plus-distance {
|
||||||
@ -204,6 +214,18 @@ mat-panel-title mat-icon {
|
|||||||
margin: 8px 8px 8px 0;
|
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-chip:focus,
|
||||||
.mat-basic-chip:focus {
|
.mat-basic-chip:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
Loading…
Reference in New Issue
Block a user