Merge pull request #4164 from MaximilianKrambach/stateRecExtensions

set/read state+recommendation extensions
This commit is contained in:
Emanuel Schütze 2019-01-22 13:14:22 +01:00 committed by GitHub
commit 7b8839f4fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 270 additions and 65 deletions

View File

@ -26,7 +26,7 @@ export class WorkflowState extends Deserializer {
public allow_create_poll: boolean;
public allow_submitter_edit: boolean;
public dont_set_identifier: boolean;
public show_state_extension_field: number;
public show_state_extension_field: boolean;
public merge_amendment_into_final: MergeAmendment;
public show_recommendation_extension_field: boolean;
public next_states_id: number[];

View File

@ -62,7 +62,7 @@
lightblue: motion.state.css_class === 'primary'
}"
>
{{ motion.state.name | translate }}
{{ getStateLabel(motion) }}
</mat-basic-chip>
</mat-cell>
</ng-container>
@ -72,11 +72,7 @@
<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)
}}
{{ getRecommendationLabel(motion) }}
</mat-basic-chip>
</mat-cell>
</ng-container>

View File

@ -1,17 +1,18 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service';
import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service';
import { MotionRepositoryService } from '../../services/motion-repository.service';
import { ViewMotionBlock } from '../../models/view-motion-block';
import { ViewMotion } from '../../models/view-motion';
import { PromptService } from 'app/core/services/prompt.service';
import { ViewMotion } from '../../models/view-motion';
/**
* Detail component to display one motion block
@ -52,6 +53,7 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
* @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(
@ -61,6 +63,7 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
private router: Router,
private route: ActivatedRoute,
private repo: MotionBlockRepositoryService,
private motionRepo: MotionRepositoryService,
private promptService: PromptService
) {
super(titleService, translate, matSnackBar);
@ -199,4 +202,24 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
public toggleEditMode(): void {
this.editBlock = !this.editBlock;
}
/**
* Fetch a motion's current recommendation label
*
* @param motion
* @returns the current recommendation label (with extension)
*/
public getRecommendationLabel(motion: ViewMotion): string {
return this.motionRepo.getExtendedRecommendationLabel(motion);
}
/**
* Fetch a motion's current state label
*
* @param motion
* @returns the current state label (with extension)
*/
public getStateLabel(motion: ViewMotion): string {
return this.motionRepo.getExtendedStateLabel(motion);
}
}

View File

@ -220,26 +220,33 @@
<mat-menu #stateMenu="matMenu">
<button *ngFor="let state of motion.nextStates" mat-menu-item (click)="setState(state.id)">
{{ state.name | translate }}
<span *ngIf="state.show_state_extension_field">&nbsp;...</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setState(null)" *ngIf="perms.isAllowed('change_metadata', motion)">
<mat-icon>replay</mat-icon> {{ 'Reset state' | translate }}
</button>
</mat-menu>
<mat-basic-chip
*ngIf="perms.isAllowed('change_metadata', motion)"
[matMenuTriggerFor]="stateMenu"
[ngClass]="getStateCssColor()"
>
{{ motion.state.name | translate }}
<div *ngIf="perms.isAllowed('change_metadata', motion)">
<mat-basic-chip [matMenuTriggerFor]="stateMenu" [ngClass]="getStateCssColor()">
{{ stateLabel }}
</mat-basic-chip>
<mat-basic-chip
*ngIf="!perms.isAllowed('change_metadata', motion)"
[ngClass]="getStateCssColor()"
>
{{ motion.state.name | translate }}
<div *ngIf="motion.state.show_state_extension_field" class="spacer-top-10">
<mat-form-field>
<input matInput placeholder="{{ 'Extension' | translate }}"
[(ngModel)]="newStateExtension"/>
</mat-form-field>
<button mat-icon-button (click)="setStateExtension()">
<mat-icon>check</mat-icon>
</button>
</div>
</div>
<div *ngIf="!perms.isAllowed('change_metadata', motion)">
<mat-basic-chip [ngClass]="getStateCssColor()" >
{{ stateLabel }}
</mat-basic-chip>
</div>
</div>
<!-- Recommendation -->
<div *ngIf="recommender && !editMotion">
@ -251,6 +258,7 @@
(click)="setRecommendation(recommendation.id)"
>
{{ recommendation.recommendation_label | translate }}
<span *ngIf="recommendation.show_recommendation_extension_field">&nbsp;...</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item *ngIf="perms.isAllowed('change_metadata', motion)"
@ -258,28 +266,26 @@
<mat-icon>replay</mat-icon> {{ 'Reset recommendation' | translate }}
</button>
</mat-menu>
<mat-basic-chip
*ngIf="perms.isAllowed('change_metadata', motion)"
[matMenuTriggerFor]="recommendationMenu"
class="bluegrey"
>
{{
motion.recommendation
? (motion.recommendation.recommendation_label | translate)
: ('not set' | translate)
}}
<div *ngIf="perms.isAllowed('change_metadata', motion)">
<mat-basic-chip [matMenuTriggerFor]="recommendationMenu" class="bluegrey">
{{ recommendationLabel }}
</mat-basic-chip>
<mat-basic-chip
*ngIf="!perms.isAllowed('change_metadata', motion)"
class="bluegrey"
>
{{
motion.recommendation
? (motion.recommendation.recommendation_label | translate)
: ('not set' | translate)
}}
<div *ngIf="motion.recommendation && motion.recommendation.show_recommendation_extension_field"
class="spacer-top-10">
<mat-form-field>
<input matInput placeholder="{{ 'Extension' | translate }}"
[(ngModel)]="newRecommendationExtension"/>
</mat-form-field>
<button mat-icon-button (click)="setRecommendationExtension()">
<mat-icon>check</mat-icon>
</button>
</div>
</div>
<div *ngIf="!perms.isAllowed('change_metadata', motion)">
<mat-basic-chip class="bluegrey">
{{ recommendationLabel }}
</mat-basic-chip>
</div>
<button mat-button *ngIf="canFollowRecommendation()" (click)="onFollowRecButton()">
<span translate>Follow recommendation</span>
</button>

View File

@ -114,6 +114,20 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
return false;
}
/**
* @returns the current recommendation label (with extension)
*/
public get recommendationLabel(): string {
return this.repo.getExtendedRecommendationLabel(this.motion);
}
/**
* @returns the current state label (with extension)
*/
public get stateLabel(): string {
return this.repo.getExtendedStateLabel(this.motion);
}
/**
* Saves the target motion. Accessed via the getter and setter.
*/
@ -286,6 +300,16 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
*/
public personalNoteContent: PersonalNoteContent;
/**
* new state extension label to be submitted, if state extensions can be set
*/
public newStateExtension = '';
/**
* new recommendation extension label to be submitted, if recommendation extensions can be set
*/
public newRecommendationExtension = '';
/**
* Constuct the detail view.
*
@ -460,6 +484,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.repo.getViewModelObservable(motionId).subscribe(newViewMotion => {
if (newViewMotion) {
this.motion = newViewMotion;
this.newStateExtension = this.motion.stateExtension;
this.newRecommendationExtension = this.motion.recommendationExtension;
this.personalNoteService.getPersonalNoteObserver(this.motion.motion).subscribe(pn => {
this.personalNoteContent = pn;
});
@ -906,6 +932,14 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.repo.setState(this.motion, id);
}
/**
* triggers the update this motion's state extension according to the current string
* in {@link newStateExtension}
*/
public setStateExtension(): void {
this.repo.setStateExtension(this.motion, this.newStateExtension);
}
/**
* Sets the recommendation
*
@ -915,6 +949,14 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.repo.setRecommendation(this.motion, id);
}
/**
* triggers the update this motion's recommendation extension according to the current string
* in {@link newRecommendationExtension}
*/
public setRecommendationExtension(): void {
this.repo.setRecommendationExtension(this.motion, this.newRecommendationExtension);
}
/**
* Sets the category for current motion
*

View File

@ -17,8 +17,11 @@
</os-head-bar>
<mat-drawer-container class="on-transition-fade">
<os-sort-filter-bar [filterService]="filterService" [sortService]="sortService"
(searchFieldChange)="searchFilter($event)">
<os-sort-filter-bar
[filterService]="filterService"
[sortService]="sortService"
(searchFieldChange)="searchFilter($event)"
>
</os-sort-filter-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
@ -82,14 +85,12 @@
lightblue: motion.state.css_class === 'primary'
}"
>
{{ motion.state.name | translate }}
{{ getStateLabel(motion) }}
</mat-basic-chip>
<!-- recommendation -->
<span *ngIf="motion.recommendation">
<mat-basic-chip class="bluegrey">{{
motion.recommendation.recommendation_label | translate
}}</mat-basic-chip>
<mat-basic-chip class="bluegrey"> {{ getRecommendationLabel(motion) }} </mat-basic-chip>
</span>
</div>
</mat-cell>
@ -198,20 +199,29 @@
<mat-icon>label</mat-icon>
<span translate>Set status</span>
</button>
<button *ngIf="recomendationEnabled" mat-menu-item
(click)="multiselectWrapper(multiselectService.setRecommendation(selectedRows))">
<button
*ngIf="recomendationEnabled"
mat-menu-item
(click)="multiselectWrapper(multiselectService.setRecommendation(selectedRows))"
>
<mat-icon>report</mat-icon>
<!-- TODO: better icon -->
<span translate>Set recommendation</span>
</button>
<button mat-menu-item *ngIf="categories.length"
(click)="multiselectWrapper(multiselectService.setCategory(selectedRows))">
<button
mat-menu-item
*ngIf="categories.length"
(click)="multiselectWrapper(multiselectService.setCategory(selectedRows))"
>
<mat-icon>device_hub</mat-icon>
<!-- TODO: icon -->
<span translate>Set category</span>
</button>
<button mat-menu-item *ngIf="motionBlocks.length"
(click)="multiselectWrapper(multiselectService.setMotionBlock(selectedRows))">
<button
mat-menu-item
*ngIf="motionBlocks.length"
(click)="multiselectWrapper(multiselectService.setMotionBlock(selectedRows))"
>
<mat-icon>widgets</mat-icon>
<!-- TODO: icon -->
<span translate>Set motion block</span>
@ -222,8 +232,11 @@
<!-- TODO: icon -->
<span translate>Add/remove submitters</span>
</button>
<button mat-menu-item *ngIf="tags.length"
(click)="multiselectWrapper(multiselectService.changeTags(selectedRows))">
<button
mat-menu-item
*ngIf="tags.length"
(click)="multiselectWrapper(multiselectService.changeTags(selectedRows))"
>
<mat-icon>bookmarks</mat-icon>
<!-- TODO: icon -->
<span translate>Add/remove tags</span>
@ -247,4 +260,3 @@
</div>
</div>
</mat-menu>

View File

@ -12,6 +12,7 @@ import { MotionBlockRepositoryService } from '../../services/motion-block-reposi
import { MotionCsvExportService } from '../../services/motion-csv-export.service';
import { MotionFilterListService } from '../../services/motion-filter-list.service';
import { MotionMultiselectService } from '../../services/motion-multiselect.service';
import { MotionRepositoryService } from '../../services/motion-repository.service';
import { MotionSortListService } from '../../services/motion-sort-list.service';
import { TagRepositoryService } from 'app/site/tags/services/tag-repository.service';
import { ViewCategory } from '../../models/view-category';
@ -90,6 +91,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
private motionBlockRepo: MotionBlockRepositoryService,
private categoryRepo: CategoryRepositoryService,
private workflowRepo: WorkflowRepositoryService,
private motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService,
public multiselectService: MotionMultiselectService,
public sortService: MotionSortListService,
@ -212,4 +214,24 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
this.raiseError(e);
}
}
/**
* Fetch a motion's current recommendation label
*
* @param motion
* @returns the current recommendation label (with extension)
*/
public getRecommendationLabel(motion: ViewMotion): string {
return this.motionRepo.getExtendedRecommendationLabel(motion);
}
/**
* Fetch a motion's current state label
*
* @param motion
* @returns the current state label (with extension)
*/
public getStateLabel(motion: ViewMotion): string {
return this.motionRepo.getExtendedStateLabel(motion);
}
}

View File

@ -241,6 +241,28 @@ export class ViewMotion extends BaseProjectableModel {
return new Date(this.motion.last_modified);
}
/**
* @returns the current state extension if the workwlof allows for extenstion fields
*/
public get stateExtension(): string {
if (this.state && this.state.show_state_extension_field) {
return this.motion.state_extension;
} else {
return null;
}
}
/**
* @returns the current recommendation extension if the workwlof allows for extenstion fields
*/
public get recommendationExtension(): string {
if (this.recommendation && this.recommendation.show_recommendation_extension_field) {
return this.motion.recommendation_extension;
} else {
return null;
}
}
/**
* Gets the comments' section ids of a motion. Used in filter by motionComment
*

View File

@ -146,7 +146,7 @@ export class MotionPdfService {
style: 'boldText'
},
{
text: this.translate.instant(motion.state.name)
text: this.motionRepo.getExtendedStateLabel(motion)
}
]);
@ -158,7 +158,7 @@ export class MotionPdfService {
style: 'boldText'
},
{
text: this.translate.instant(motion.recommendation.recommendation_label)
text: this.motionRepo.getExtendedRecommendationLabel(motion)
}
]);
}

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { tap, map } from 'rxjs/operators';
@ -67,7 +68,8 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
private readonly lineNumbering: LinenumberingService,
private readonly diff: DiffService,
private treeService: TreeService,
private personalNoteService: PersonalNoteService
private personalNoteService: PersonalNoteService,
private translate: TranslateService
) {
super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock, Mediafile]);
}
@ -701,4 +703,84 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
public hasAmendments(motion: ViewMotion): boolean {
return this.getViewModelList().filter(allMotions => allMotions.parent_id === motion.id).length > 0;
}
/**
* updates the state Extension with the string given, if the current workflow allows for it
*
* @param viewMotion
* @param value
*/
public async setStateExtension(viewMotion: ViewMotion, value: string): Promise<void> {
if (viewMotion.state.show_state_extension_field) {
return this.update({ state_extension: value }, viewMotion);
}
}
/**
* updates the recommendation extension with the string given, if the current workflow allows for it
*
* @param viewMotion
* @param value
*/
public async setRecommendationExtension(viewMotion: ViewMotion, value: string): Promise<void> {
if (viewMotion.recommendation.show_recommendation_extension_field) {
return this.update({ recommendation_extension: value }, viewMotion);
}
}
/**
* Get the label for the motion's current state with the extension
* attached (if present). For cross-referencing other motions, `[motion:id]`
* will replaced by the referenced motion's identifier (see {@link solveExtensionPlaceHolder})
*
* @param motion
* @returns the translated state with the extension attached
*/
public getExtendedStateLabel(motion: ViewMotion): string {
let rec = this.translate.instant(motion.state.name);
if (motion.stateExtension && motion.state.show_state_extension_field) {
rec += ' ' + this.solveExtensionPlaceHolder(motion.stateExtension);
}
return rec;
}
/**
* Get the label for the motion's current recommendation with the extension
* attached (if present)
*
* @param motion
* @returns the translated extension with the extension attached, 'not set'
* if no recommendation si set
*/
public getExtendedRecommendationLabel(motion: ViewMotion): string {
if (!motion.recommendation) {
return this.translate.instant('not set');
}
let rec = this.translate.instant(motion.recommendation.recommendation_label);
if (motion.recommendationExtension && motion.recommendation.show_recommendation_extension_field) {
rec += ' ' + this.solveExtensionPlaceHolder(motion.recommendationExtension);
}
return rec;
}
/**
* Replaces any motion placeholder (`[motion:id]`) with the motion's title(s)
*
* @param value
* @returns the string with the motion titles replacing the placeholders, '??' strings for errors
*/
public solveExtensionPlaceHolder(value: string): string {
const beg = value.indexOf('[motion:');
const end = value.indexOf(']');
if (beg > -1 && (end > -1 && end > beg)) {
const id = Number(value.substring(beg + 8, end));
const referedMotion = Number.isNaN(id) ? null : this.getViewModel(id);
const title = referedMotion ? referedMotion.identifier : '??';
value = value.substring(0, beg) + title + value.substring(end + 1);
// recursively check for additional occurrences
return this.solveExtensionPlaceHolder(value);
} else {
return value;
}
}
}