Merge pull request #4038 from emanuelschuetze/motion-ui

Improved UI of motion list and detail view
This commit is contained in:
Sean 2018-11-29 12:40:35 +01:00 committed by GitHub
commit e694d9e0dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 382 additions and 112 deletions

View File

@ -131,19 +131,19 @@ export class HttpService {
/** /**
* Exectures a post on a url with a certain object * Exectures a post on a url with a certain object
* @param url string of the url to send semothing to * @param url The url to send the request to.
* @param data The data to send * @param data An optional payload for the request.
* @param header optional HTTP header if required * @param header optional HTTP header if required
* @returns A promise holding a generic * @returns A promise holding a generic
*/ */
public async post<T>(url: string, data: any, header?: HttpHeaders): Promise<T> { public async post<T>(url: string, data?: any, header?: HttpHeaders): Promise<T> {
return await this.send<T>(url, HTTPMethod.POST, data, header); return await this.send<T>(url, HTTPMethod.POST, data, header);
} }
/** /**
* Exectures a put on a url with a certain object * Exectures a put on a url with a certain object
* @param url string of the url to send semothing to * @param url The url to send the request to.
* @param data the object that should be send * @param data The payload for the request.
* @param header optional HTTP header if required * @param header optional HTTP header if required
* @returns A promise holding a generic * @returns A promise holding a generic
*/ */
@ -153,8 +153,8 @@ export class HttpService {
/** /**
* Exectures a put on a url with a certain object * Exectures a put on a url with a certain object
* @param url the url that should be called * @param url The url to send the request to.
* @param data: The data to send * @param data: The payload for the request.
* @param header optional HTTP header if required * @param header optional HTTP header if required
* @returns A promise holding a generic * @returns A promise holding a generic
*/ */
@ -164,7 +164,7 @@ export class HttpService {
/** /**
* Makes a delete request. * Makes a delete request.
* @param url the url that should be called * @param url The url to send the request to.
* @param data An optional data to send in the requestbody. * @param data An optional data to send in the requestbody.
* @param header optional HTTP header if required * @param header optional HTTP header if required
* @returns A promise holding a generic * @returns A promise holding a generic

View File

@ -79,7 +79,7 @@
<mat-accordion multi='true' class='on-transition-fade'> <mat-accordion multi='true' class='on-transition-fade'>
<!-- MetaInfo Panel--> <!-- MetaInfo Panel-->
<mat-expansion-panel #metaInfoPanel [expanded]="editMotion" class='meta-info-block meta-info-panel'> <mat-expansion-panel #metaInfoPanel [expanded]="true" class='meta-info-block meta-info-panel'>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon>info</mat-icon> <mat-icon>info</mat-icon>
@ -93,9 +93,6 @@
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<os-motion-comments *ngIf="!newMotion" [motion]="motion"></os-motion-comments>
<os-personal-note *ngIf="!newMotion" [motion]="motion"></os-personal-note>
<!-- Content --> <!-- Content -->
<mat-expansion-panel #contentPanel [expanded]='true'> <mat-expansion-panel #contentPanel [expanded]='true'>
<mat-expansion-panel-header> <mat-expansion-panel-header>
@ -109,6 +106,9 @@
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container> <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<os-motion-comments *ngIf="!newMotion" [motion]="motion"></os-motion-comments>
<os-personal-note *ngIf="!newMotion" [motion]="motion"></os-personal-note>
</mat-accordion> </mat-accordion>
</ng-template> </ng-template>
@ -150,7 +150,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<!-- Submitter --> <!-- Submitters -->
<div *ngIf="motion && motion.submitters || newMotion"> <div *ngIf="motion && motion.submitters || newMotion">
<div *ngIf="newMotion"> <div *ngIf="newMotion">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']"> <div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
@ -166,75 +166,97 @@
</div> </div>
</div> </div>
<!-- Supporter --> <!-- Supporters -->
<div *ngIf='motion && motion.hasSupporters() || editMotion'> <div *ngIf='motion && minSupporters'>
<!-- print all motion supporters -->
<div *ngIf="editMotion"> <div *ngIf="editMotion">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']"> <div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('supporters_id')" <os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('supporters_id')"
[multiple]="true" listname="{{ 'Supporters' | translate }}" [InputListValues]="supporterObserver"></os-search-value-selector> [multiple]="true" listname="{{ 'Supporters' | translate }}" [InputListValues]="supporterObserver"></os-search-value-selector>
</div> </div>
</div> </div>
<div *ngIf="!editMotion && motion.hasSupporters()"> <div *ngIf="!editMotion">
<h4 translate>Supporters</h4> <h4 *ngIf="perms.isAllowed('support', motion) || motion.hasSupporters()" translate>Supporters</h4>
<ul *ngFor="let supporter of motion.supporters"> <!-- support button -->
<li>{{ supporter.full_name }}</li> <button type="button" *ngIf="perms.isAllowed('support', motion)" (click)=support() mat-raised-button color="primary">
</ul> <mat-icon>thumb_up</mat-icon>
{{ 'Support' | translate }}
</button>
<!-- unsupport button -->
<button type="button" *ngIf="perms.isAllowed('unsupport', motion)" (click)=unsupport() mat-raised-button color="primary">
<mat-icon>thumb_down</mat-icon>
{{ 'Unsupport' | translate }}
</button>
<!-- show supporters (TODO: open in dialog) -->
<button type="button" *ngIf="motion.hasSupporters()" (click)=openSupportersDialog() mat-button>
{{ motion.supporters.length }} {{ 'supporters' | translate }}
</button>
<p *ngIf="showSupporters">
<mat-chip-list *ngFor="let supporter of motion.supporters">
<mat-chip>{{ supporter.full_name }}</mat-chip>
</mat-chip-list>
</p>
</div> </div>
</div> </div>
<!-- State --> <!-- State -->
<div *ngIf='motion && motion.state'> <div *ngIf='motion && !editMotion'>
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata'];complement:true"> <h4 translate>State</h4>
<ng-container *ngIf="!newMotion"> <mat-menu #stateMenu='matMenu'>
<h4 translate>State</h4> <button *ngFor='let state of motion.nextStates' mat-menu-item
{{ motion.state }} (click)=setState(state.id)>{{ state.name | translate }}
</ng-container> </button>
</div> <mat-divider></mat-divider>
<div *ngIf="!editMotion"> <button mat-menu-item (click)=setState(null)>
<mat-form-field *osPerms="['motions.can_manage', 'motions.can_manage_metadata']"> <mat-icon>replay</mat-icon> {{ 'Reset state' | translate }}
<mat-select placeholder="{{ 'State' | translate }}" formControlName='state_id' (selectionChange)="onChangeState($event)"> </button>
<mat-option [value]="motion.state_id">{{ motion.state }}</mat-option> </mat-menu>
<mat-divider></mat-divider> <mat-basic-chip [matMenuTriggerFor]='stateMenu' [ngClass]="{
<mat-option *ngFor="let state of motion.nextStates" [value]="state.id">{{ state }}</mat-option> 'green': motion.state.css_class === 'success',
<mat-divider></mat-divider> 'red': motion.state.css_class === 'danger',
<mat-option [value]="null"> 'grey': motion.state.css_class === 'default',
<mat-icon>replay</mat-icon> 'lightblue': motion.state.css_class === 'primary' }">
<span translate>Reset State</span> {{ motion.state.name | translate }}
</mat-option> </mat-basic-chip>
</mat-select>
</mat-form-field> <!--*osPerms="['motions.can_manage', 'motions.can_manage_metadata']; -->
</div>
</div> </div>
<!-- Recommendation --> <!-- Recommendation -->
<div *ngIf='motion && motion.state && recommender'> <div *ngIf='motion && recommender && !editMotion'>
<mat-form-field *ngIf="!editMotion && !newMotion"> <h4 translate>{{ recommender }}</h4>
<mat-select [placeholder]=recommender formControlName='recommendation_id' (selectionChange)="onChangerRecommenderState($event)"> <mat-menu #recommendationMenu='matMenu'>
<mat-option *ngFor="let recommendation of motion.possibleRecommendations" [value]="recommendation.id"> <button *ngFor='let recommendation of motion.possibleRecommendations' mat-menu-item
{{ recommendation.recommendation_label | translate }} (click)=setRecommendation(recommendation.id)>{{ recommendation.recommendation_label | translate }}
</mat-option> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<mat-option [value]="null"> <button mat-menu-item (click)=setRecommendation(null)>
<mat-icon>replay</mat-icon><span translate>Reset recommendation</span> <mat-icon>replay</mat-icon> {{ 'Reset recommendation' | translate }}
</mat-option> </button>
</mat-select> </mat-menu>
</mat-form-field> <mat-basic-chip [matMenuTriggerFor]='recommendationMenu' class="bluegrey">
{{ motion.recommendation ? (motion.recommendation.recommendation_label | translate) : ('not set' | translate) }}
</mat-basic-chip>
</div> </div>
<!-- Category --> <!-- Category -->
<div *ngIf="motion && motion.category_id || editMotion"> <!-- Disabled during "new motion" since changing has no effect -->
<div *ngIf='!editMotion'> <div *ngIf="motion && !editMotion">
<h4 translate>Category</h4> <h4 translate>Category</h4>
{{ motion.category }} <mat-menu #categoryMenu='matMenu'>
</div> <button *ngFor='let category of categoryObserver.value' mat-menu-item
<div *ngIf="editMotion || newMotion"> (click)=setCategory(category.id)>{{ category }}
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('category_id')" </button>
[multiple]="false" listname="{{ 'Category' | translate }}" [InputListValues]="categoryObserver" includeNone="true"></os-search-value-selector> <button mat-menu-item (click)=setCategory(null)>
</div> ---
</button>
</mat-menu>
<mat-basic-chip [matMenuTriggerFor]='categoryMenu' class="grey">
{{ motion.category ? motion.category : ('not set' | translate) }}
</mat-basic-chip>
</div> </div>
<!-- Workflow (just during creation) --> <!-- 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']">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('workflow_id')" <os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('workflow_id')"
@ -270,7 +292,7 @@
<button type="button" mat-icon-button [matMenuTriggerFor]="lineNumberingMenu" matTooltip="{{ 'Line numbering' | translate }}"> <button type="button" mat-icon-button [matMenuTriggerFor]="lineNumberingMenu" matTooltip="{{ 'Line numbering' | translate }}">
<mat-icon>format_list_numbered</mat-icon> <mat-icon>format_list_numbered</mat-icon>
</button> </button>
<button type="button" mat-icon-button [matMenuTriggerFor]="changeRecoMenu" matTooltip="{{ 'Change recommendations' | translate }}"> <button *ngIf="allChangingObjects.length > 0" type="button" mat-icon-button [matMenuTriggerFor]="changeRecoMenu" matTooltip="{{ 'Change recommendations' | translate }}">
<mat-icon>rate_review</mat-icon> <mat-icon>rate_review</mat-icon>
</button> </button>
</div> </div>
@ -293,21 +315,15 @@
</div> </div>
<!-- Title --> <!-- Title -->
<div *ngIf="motion && motion.title || editMotion"> <div *ngIf="motion && editMotion">
<div *ngIf='!editMotion'> <mat-form-field class="wide-form">
<h4>{{motion.title}}</h4> <input matInput osAutofocus placeholder="{{ 'Title' | translate }}"
</div> formControlName='title' [value]='motionCopy.title' required>
<mat-form-field *ngIf="editMotion" class="wide-form">
<input matInput osAutofocus placeholder="{{ 'Title' | translate }}" formControlName='title' [value]='motionCopy.title'
required>
</mat-form-field> </mat-form-field>
</div> </div>
<!-- Text --> <!-- Text -->
<!-- TODO: this is a config variable. Read it out --> <span class="text-prefix-label">{{ preamble | translate }}</span>
<span class="text-prefix-label" translate>The assembly may decide:</span>
<ng-container *ngIf='motion && !editMotion && !motion.isStatuteAmendment()'> <ng-container *ngIf='motion && !editMotion && !motion.isStatuteAmendment()'>
<div *ngIf="!isRecoModeDiff()" class="motion-text" [class.line-numbers-none]="isLineNumberingNone()" <div *ngIf="!isRecoModeDiff()" class="motion-text" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-outside]="isLineNumberingOutside()"> [class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-outside]="isLineNumberingOutside()">
@ -343,17 +359,18 @@
[init]="tinyMceSettings" [init]="tinyMceSettings"
*ngIf="editMotion" *ngIf="editMotion"
></editor> ></editor>
</div> </div>
</form> </form>
</ng-template> </ng-template>
<!-- Line number Menu --> <!-- Line number Menu -->
<mat-menu #lineNumberingMenu="matMenu" > <mat-menu #lineNumberingMenu="matMenu">
<button mat-menu-item translate (click)=setLineNumberingMode(0)>none</button> <div *ngIf="motion">
<button mat-menu-item translate (click)=setLineNumberingMode(1)>inline</button> <button mat-menu-item translate (click)=setLineNumberingMode(0) [ngClass]="{ 'selected': motion.lnMode === 0 }">none</button>
<button mat-menu-item translate (click)=setLineNumberingMode(2)>outside</button> <button mat-menu-item translate (click)=setLineNumberingMode(1) [ngClass]="{ 'selected': motion.lnMode === 1 }">inline</button>
<button mat-menu-item translate (click)=setLineNumberingMode(2) [ngClass]="{ 'selected': motion.lnMode === 2 }">outside</button>
</div>
</mat-menu> </mat-menu>
<!-- Diff View Menu --> <!-- Diff View Menu -->

View File

@ -1,7 +1,7 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatExpansionPanel, MatSnackBar, MatSelectChange, MatCheckboxChange } from '@angular/material'; import { MatDialog, MatExpansionPanel, MatSnackBar, MatCheckboxChange } from '@angular/material';
import { Category } from '../../../../shared/models/motions/category'; import { Category } from '../../../../shared/models/motions/category';
import { ViewportService } from '../../../../core/services/viewport.service'; import { ViewportService } from '../../../../core/services/viewport.service';
@ -28,6 +28,7 @@ import { StatuteParagraphRepositoryService } from '../../services/statute-paragr
import { ConfigService } from '../../../../core/services/config.service'; import { ConfigService } from '../../../../core/services/config.service';
import { Workflow } from 'app/shared/models/motions/workflow'; import { Workflow } from 'app/shared/models/motions/workflow';
import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators'; import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators';
import { LocalPermissionsService } from '../../services/local-permissions.service';
/** /**
* Component for the motion detail view * Component for the motion detail view
@ -94,11 +95,22 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
private _motion: ViewMotion; private _motion: ViewMotion;
/** /**
* Value of the configuration variable `motions_statutes_enabled` - are statutes enabled? * Value of the config variable `motions_statutes_enabled` - are statutes enabled?
* @TODO replace by direct access to config variable, once it's available from the templates * @TODO replace by direct access to config variable, once it's available from the templates
*/ */
public statutesEnabled: boolean; public statutesEnabled: boolean;
/**
* Value of the config variable `motions_min_supporters`
*/
public minSupporters: number;
/**
* Value of the config variable `motions_preamble`
*/
public preamble: string;
/** /**
* Copy of the motion that the user might edit * Copy of the motion that the user might edit
*/ */
@ -154,6 +166,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
*/ */
public supporterObserver: BehaviorSubject<User[]>; public supporterObserver: BehaviorSubject<User[]>;
/**
* Determine if the name of supporters are visible
*/
public showSupporters = false;
/** /**
* Value for os-motion-detail-diff: when this is set, that component scrolls to the given change * Value for os-motion-detail-diff: when this is set, that component scrolls to the given change
*/ */
@ -193,6 +210,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
public vp: ViewportService, public vp: ViewportService,
public perms: LocalPermissionsService,
private op: OperatorService, private op: OperatorService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -226,11 +244,22 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.workflowObserver.next(DS.getAll(Workflow)); this.workflowObserver.next(DS.getAll(Workflow));
} }
}); });
// load config variables
this.configService.get('motions_statutes_enabled').subscribe( this.configService.get('motions_statutes_enabled').subscribe(
(enabled: boolean): void => { (enabled: boolean): void => {
this.statutesEnabled = enabled; this.statutesEnabled = enabled;
} }
); );
this.configService.get('motions_min_supporters').subscribe(
(supporters: number): void => {
this.minSupporters = supporters;
}
);
this.configService.get('motions_preamble').subscribe(
(preamble: string): void => {
this.preamble = preamble;
}
);
} }
/** /**
@ -343,7 +372,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* The AutoUpdate-Service should see a change once it arrives and show it * The AutoUpdate-Service should see a change once it arrives and show it
* in the list view automatically * in the list view automatically
* *
* TODO: state is not yet saved. Need a special "put" command. Repo should handle this.
*/ */
public async saveMotion(): Promise<void> { public async saveMotion(): Promise<void> {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value }; const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
@ -410,7 +438,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Sets the motions line numbering mode * Sets the motions line numbering mode
* @param mode Needs to fot to the enum defined in ViewMotion * @param mode Needs to got the enum defined in ViewMotion
*/ */
public setLineNumberingMode(mode: LineNumberingMode): void { public setLineNumberingMode(mode: LineNumberingMode): void {
this.motion.lnMode = mode; this.motion.lnMode = mode;
@ -578,19 +606,49 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
} }
/** /**
* Executed after selecting a state * Supports the motion (as requested user)
* @param selection MatSelectChange that contains the workflow id
*/ */
public onChangeState(selection: MatSelectChange): void { public support(): void {
this.repo.setState(this.motion, selection.value); this.repo.support(this.motion).then(null, this.raiseError);
} }
/** /**
* Executed after selecting the recommenders state * Unsupports the motion
* @param selection MatSelectChange that contains the workflow id
*/ */
public onChangerRecommenderState(selection: MatSelectChange): void { public unsupport(): void {
this.repo.setRecommenderState(this.motion, selection.value); this.repo.unsupport(this.motion).then(null, this.raiseError);
}
/**
* Opens the dialog with all supporters.
* TODO: open dialog here!
*/
public openSupportersDialog(): void {
this.showSupporters = !this.showSupporters;
}
/**
* Sets the state
* @param id Motion state id
*/
public setState(id: number): void {
this.repo.setState(this.motion, id);
}
/**
* Sets the recommendation
* @param id Motion recommendation id
*/
public setRecommendation(id: number): void {
this.repo.setRecommendation(this.motion, id);
}
/**
* Sets the category for current motion
* @param id Motion category id
*/
public setCategory(id: number): void {
this.repo.setCatetory(this.motion, id);
} }
/** /**

View File

@ -65,6 +65,20 @@
<span translate>by</span> <span translate>by</span>
{{ motion.submitters }} {{ motion.submitters }}
</span> </span>
<br>
<!-- state -->
<mat-basic-chip [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>
<!-- recommendation -->
<span *ngIf="motion.recommendation" >
<mat-basic-chip class="bluegrey">{{ motion.recommendation.recommendation_label | translate }}</mat-basic-chip>
</span>
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
@ -72,13 +86,10 @@
<!-- state column --> <!-- state column -->
<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" (click)="selectMotion(motion)">
<!--div *ngIf='isDisplayIcon(motion.state) && motion.state' class='innerTable'> <div *ngIf='motion.category' class='small'>
<mat-icon>{{ getStateIcon(motion.state) }}</mat-icon> <mat-icon>device_hub</mat-icon> {{ motion.category }}
</div>--> </div>
<mat-chip-list>
<mat-chip color="accent">{{ motion.state }}</mat-chip>
</mat-chip-list>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
@ -98,7 +109,7 @@
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)' <mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
*matRowDef="let row; columns: getColumnDefinition()"> *matRowDef="let row; columns: getColumnDefinition()" class="lg">
</mat-row> </mat-row>
</mat-table> </mat-table>

View File

@ -1,8 +1,8 @@
/** css hacks https://codepen.io/edge0703/pen/iHJuA */ /** css hacks https://codepen.io/edge0703/pen/iHJuA */
.innerTable { .innerTable {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: top;
line-height: normal; line-height: 150%;
} }
.os-listview-table { .os-listview-table {
@ -23,10 +23,10 @@
.motion-list-title { .motion-list-title {
font-weight: bold; font-weight: bold;
font-size: 16px;
} }
.motion-list-from { .motion-list-from {
margin-top: 5px;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
font-size: 90%; font-size: 90%;
} }

View File

@ -227,6 +227,11 @@ export class ViewMotion extends BaseViewModel {
this._block = block; this._block = block;
// TODO: Should be set using a a config variable // TODO: Should be set using a a config variable
/*this._configService.get('motions_default_line_numbering').subscribe(
(mode: string): void => {
this.lnMode = LineNumberingMode.Outside;
}
);*/
this.lnMode = LineNumberingMode.Outside; this.lnMode = LineNumberingMode.Outside;
this.crMode = ChangeRecoMode.Original; this.crMode = ChangeRecoMode.Original;
this.lineLength = 80; this.lineLength = 80;
@ -265,9 +270,9 @@ export class ViewMotion extends BaseViewModel {
this.updateItem(update as Item); this.updateItem(update as Item);
} else if (update instanceof MotionBlock) { } else if (update instanceof MotionBlock) {
this.updateMotionBlock(update); this.updateMotionBlock(update);
} else if (update instanceof User) {
this.updateUser(update as User);
} }
// TODO: There is no way (yet) to add Submitters to a motion
// Thus, this feature could not be tested
} }
/** /**
@ -310,6 +315,23 @@ export class ViewMotion extends BaseViewModel {
} }
} }
/**
* Update routine for the agenda Item
* @param update potentially the changed agenda Item. Needs manual verification
*/
public updateUser(update: User): void {
if (this.motion) {
if (this.motion.submitters && this.motion.submitters.findIndex(user => user.user_id === update.id)) {
const userIndex = this.submitters.findIndex(user => user.id === update.id);
this.submitters[userIndex] = update as User;
}
if (this.motion.supporters_id && this.motion.supporters_id.includes(update.id)) {
const userIndex = this.supporters.findIndex(user => user.id === update.id);
this.supporters[userIndex] = update as User;
}
}
}
public hasSupporters(): boolean { public hasSupporters(): boolean {
return !!(this.supporters && this.supporters.length > 0); return !!(this.supporters && this.supporters.length > 0);
} }

View File

@ -0,0 +1,13 @@
import { TestBed } from '@angular/core/testing';
import { LocalPermissionsService } from './local-permissions.service';
import { E2EImportsModule } from '../../../../e2e-imports.module';
describe('LocalPermissionsService', () => {
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
it('should be created', () => {
const service: LocalPermissionsService = TestBed.get(LocalPermissionsService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import { OperatorService } from '../../../core/services/operator.service';
import { ViewMotion } from '../models/view-motion';
import { ConfigService } from '../../../core/services/config.service';
@Injectable({
providedIn: 'root'
})
export class LocalPermissionsService {
public configMinSupporters: number;
public constructor(
private operator: OperatorService,
private configService: ConfigService,
) {
// load config variables
this.configService.get('motions_min_supporters').subscribe(
(supporters: number): void => {
this.configMinSupporters = supporters;
}
);
}
/**
* Should determine if the user (Operator) has the
* correct permission to perform the given action.
*
* actions might be:
* - support
*
* @param action the action the user tries to perform
*/
public isAllowed(action: string, motion?: ViewMotion): boolean {
if (motion) {
switch (action) {
case 'support':
return (
this.operator.hasPerms('motions.can_support') &&
this.configMinSupporters > 0 &&
motion.state.allow_support &&
(motion.submitters.indexOf(this.operator.user) === -1) &&
(motion.supporters.indexOf(this.operator.user) === -1));
case 'unsupport':
return (
motion.state.allow_support &&
(motion.supporters.indexOf(this.operator.user) !== -1)
);
default:
return false;
}
}
}
}

View File

@ -106,7 +106,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
* Creates a (real) motion with patched data and delegate it * Creates a (real) motion with patched data and delegate it
* to the {@link DataSendService} * to the {@link DataSendService}
* *
* @param update the form data containing the update values * @param update the form data containing the updated values
* @param viewMotion The View Motion. If not present, a new motion will be created * @param viewMotion The View Motion. If not present, a new motion will be created
* TODO: Remove the viewMotion and make it actually distignuishable from save() * TODO: Remove the viewMotion and make it actually distignuishable from save()
*/ */
@ -123,7 +123,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
* Creates a (real) motion with patched data and delegate it * Creates a (real) motion with patched data and delegate it
* to the {@link DataSendService} * to the {@link DataSendService}
* *
* @param update the form data containing the update values * @param update the form data containing the updated values
* @param viewMotion The View Motion. If not present, a new motion will be created * @param viewMotion The View Motion. If not present, a new motion will be created
*/ */
public async update(update: Partial<Motion>, viewMotion: ViewMotion): Promise<void> { public async update(update: Partial<Motion>, viewMotion: ViewMotion): Promise<void> {
@ -158,11 +158,23 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
* Set the recommenders state of a motion * Set the recommenders state of a motion
* *
* @param viewMotion target motion * @param viewMotion target motion
* @param stateId the number that indicates the state * @param recommendationId the number that indicates the recommendation
*/ */
public async setRecommenderState(viewMotion: ViewMotion, stateId: number): Promise<void> { public async setRecommendation(viewMotion: ViewMotion, recommendationId: number): Promise<void> {
const restPath = `/rest/motions/motion/${viewMotion.id}/set_recommendation/`; const restPath = `/rest/motions/motion/${viewMotion.id}/set_recommendation/`;
await this.httpService.put(restPath, { recommendation: stateId }); await this.httpService.put(restPath, { recommendation: recommendationId });
}
/**
* Set the category of a motion
*
* @param viewMotion target motion
* @param categoryId the number that indicates the category
*/
public async setCatetory(viewMotion: ViewMotion, categoryId: number): Promise<void> {
const motion = viewMotion.motion;
motion.category_id = categoryId;
await this.update(motion, viewMotion);
} }
/** /**
@ -175,6 +187,26 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
await this.httpService.post(url, data); await this.httpService.post(url, data);
} }
/**
* Supports the motion
*
* @param viewMotion target motion
*/
public async support(viewMotion: ViewMotion): Promise<void> {
const url = `/rest/motions/motion/${viewMotion.id}/support/`;
await this.httpService.post(url);
}
/**
* Unsupports the motion
*
* @param viewMotion target motion
*/
public async unsupport(viewMotion: ViewMotion): Promise<void> {
const url = `/rest/motions/motion/${viewMotion.id}/support/`;
await this.httpService.delete(url);
}
/** /**
* Format the motion text using the line numbering and change * Format the motion text using the line numbering and change
* reco algorithm. * reco algorithm.

View File

@ -41,6 +41,10 @@ body {
padding: 0; padding: 0;
} }
.small {
font-size: 90%;
}
.generic-mini-button { .generic-mini-button {
bottom: -28px; bottom: -28px;
z-index: 100; z-index: 100;
@ -114,6 +118,9 @@ body {
cursor: pointer; cursor: pointer;
background-color: rgba(0, 0, 0, 0.055); background-color: rgba(0, 0, 0, 0.055);
} }
mat-row.lg {
height: 90px;
}
} }
.card-plus-distance { .card-plus-distance {
@ -157,7 +164,6 @@ mat-panel-title mat-icon {
padding-right: 30px; padding-right: 30px;
} }
.hidden-cell { .hidden-cell {
flex: 0; flex: 0;
width: 0; width: 0;
@ -188,3 +194,59 @@ mat-panel-title mat-icon {
display: none; display: none;
} }
} }
.mat-chip,
.mat-basic-chip {
font-size: 12px;
min-height: 22px !important;
border-radius: 5px !important;
padding: 4px 8px !important;
margin: 8px 8px 8px 0;
}
.mat-chip:focus,
.mat-basic-chip:focus {
outline: none;
}
button.mat-menu-item.selected {
font-weight: bold !important;
}
/** Colors **/
.lightblue {
background-color: rgb(33, 150, 243) !important;
color: white !important;
}
.darkblue {
background-color: rgb(63, 81, 181) !important;
color: white !important;
}
.green,
.success {
background-color: rgb(76, 175, 80) !important;
color: white !important;
}
.red,
.error {
background-color: rgb(255, 82, 82) !important;
color: white !important;
}
.yellow,
.warning {
background-color: rgb(255, 193, 7) !important;
color: white !important;
}
.bluegrey {
background-color: rgb(96, 125, 139) !important;
color: white !important;
}
.grey {
background-color: #e0e0e0 !important;
color: rgba(0, 0, 0, 0.87) !important;
}