Motion Repository
Adds a repository to remove the logic in the motion domain object The repository will add a new model layer between the components and the domain objects implicitly add a new buttion into the motion detail view
This commit is contained in:
parent
a5f06d3347
commit
39f266d2de
@ -37,7 +37,7 @@ export class DataSendService {
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return this.http.put<BaseModel>('rest/' + model.collectionString + '/' + model.id, model).pipe(
|
||||
return this.http.patch<BaseModel>('rest/' + model.collectionString + '/' + model.id, model).pipe(
|
||||
tap(
|
||||
response => {
|
||||
console.log('Update model. Response : ', response);
|
||||
|
@ -39,10 +39,23 @@ export abstract class BaseModel extends OpenSlidesComponent implements Deseriali
|
||||
this._collectionString = collectionString;
|
||||
|
||||
if (input) {
|
||||
this.changeNullValuesToUndef(input);
|
||||
this.deserialize(input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent to send literally "null" if should be send
|
||||
* @param input object to deserialize
|
||||
*/
|
||||
public changeNullValuesToUndef(input: any): void {
|
||||
Object.keys(input).forEach(key => {
|
||||
if (input[key] === null) {
|
||||
input[key] = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the collectionString.
|
||||
*
|
||||
|
@ -11,6 +11,7 @@ export abstract class Deserializer implements Deserializable {
|
||||
*/
|
||||
protected constructor(input?: any) {
|
||||
if (input) {
|
||||
this.changeNullValuesToUndef(input);
|
||||
this.deserialize(input);
|
||||
}
|
||||
}
|
||||
@ -22,4 +23,16 @@ export abstract class Deserializer implements Deserializable {
|
||||
public deserialize(input: any): void {
|
||||
Object.assign(this, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent to send literally "null" if should be send
|
||||
* @param input object to deserialize
|
||||
*/
|
||||
public changeNullValuesToUndef(input: any): void {
|
||||
Object.keys(input).forEach(key => {
|
||||
if (input[key] === null) {
|
||||
input[key] = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { BaseModel } from '../base.model';
|
||||
import { MotionSubmitter } from './motion-submitter';
|
||||
import { MotionLog } from './motion-log';
|
||||
import { Config } from '../core/config';
|
||||
import { Workflow } from './workflow';
|
||||
import { User } from '../users/user';
|
||||
import { Category } from './category';
|
||||
import { WorkflowState } from './workflow-state';
|
||||
import { MotionComment } from './motion-comment';
|
||||
import { Workflow } from './workflow';
|
||||
|
||||
/**
|
||||
* Representation of Motion.
|
||||
@ -21,7 +18,7 @@ export class Motion extends BaseModel {
|
||||
public title: string;
|
||||
public text: string;
|
||||
public reason: string;
|
||||
public amendment_paragraphs: string;
|
||||
public amendment_paragraphs: string[];
|
||||
public modified_final_version: string;
|
||||
public parent_id: number;
|
||||
public category_id: number;
|
||||
@ -30,6 +27,7 @@ export class Motion extends BaseModel {
|
||||
public submitters: MotionSubmitter[];
|
||||
public supporters_id: number[];
|
||||
public comments: MotionComment[];
|
||||
public workflow_id: number;
|
||||
public state_id: number;
|
||||
public state_extension: string;
|
||||
public state_required_permission_to_see: string;
|
||||
@ -41,12 +39,8 @@ export class Motion extends BaseModel {
|
||||
public agenda_item_id: number;
|
||||
public log_messages: MotionLog[];
|
||||
|
||||
// dynamic values
|
||||
public workflow: Workflow;
|
||||
|
||||
public constructor(input?: any) {
|
||||
super('motions/motion', input);
|
||||
this.initDataStoreValues();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,121 +51,19 @@ export class Motion extends BaseModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the and the workflow from either dataStore or WebSocket
|
||||
* returns the motion submitters userIDs
|
||||
*/
|
||||
public initDataStoreValues(): void {
|
||||
// check the containing Workflows in DataStore
|
||||
const allWorkflows = this.DS.getAll(Workflow);
|
||||
allWorkflows.forEach(localWorkflow => {
|
||||
if (localWorkflow.isStateContained(this.state_id)) {
|
||||
this.workflow = localWorkflow as Workflow;
|
||||
}
|
||||
});
|
||||
|
||||
// observe for new models
|
||||
this.DS.changeObservable.subscribe(newModel => {
|
||||
if (newModel instanceof Workflow) {
|
||||
if (newModel.isStateContained(this.state_id)) {
|
||||
this.workflow = newModel as Workflow;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* add a new motionSubmitter from user-object
|
||||
* @param user the user
|
||||
*/
|
||||
public addSubmitter(user: User): void {
|
||||
const newSubmitter = new MotionSubmitter();
|
||||
newSubmitter.user_id = user.id;
|
||||
this.submitters.push(newSubmitter);
|
||||
console.log('did addSubmitter. this.submitters: ', this.submitters);
|
||||
}
|
||||
|
||||
/**
|
||||
* return the submitters as uses objects
|
||||
*/
|
||||
public get submitterAsUser(): User[] {
|
||||
const submitterIds: number[] = this.submitters
|
||||
public get submitterIds(): number[] {
|
||||
return this.submitters
|
||||
.sort((a: MotionSubmitter, b: MotionSubmitter) => {
|
||||
return a.weight - b.weight;
|
||||
})
|
||||
.map((submitter: MotionSubmitter) => submitter.user_id);
|
||||
return this.DS.getMany<User>('users/user', submitterIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the category of a motion as object
|
||||
*/
|
||||
public get category(): Category {
|
||||
return this.DS.get<Category>(Category, this.category_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the category in the motion
|
||||
*/
|
||||
public set category(newCategory: Category) {
|
||||
this.category_id = newCategory.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* return the workflow state
|
||||
*/
|
||||
public get state(): WorkflowState {
|
||||
if (this.workflow) {
|
||||
return this.workflow.state_by_id(this.state_id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns possible states for the motion
|
||||
*/
|
||||
public get nextStates(): WorkflowState[] {
|
||||
if (this.workflow && this.state) {
|
||||
return this.state.getNextStates(this.workflow);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the recommendation.
|
||||
*
|
||||
* TODO: Motion workflow needs to be specific on the server
|
||||
*/
|
||||
public get recommendation(): WorkflowState {
|
||||
if (this.recommendation_id && this.workflow && this.workflow.id) {
|
||||
const state = this.workflow.state_by_id(this.recommendation_id);
|
||||
return state;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the value of 'config.motions_recommendations_by'
|
||||
*/
|
||||
public get recomBy(): string {
|
||||
const motionsRecommendationsByConfig = this.DS.filter<Config>(
|
||||
Config,
|
||||
config => config.key === 'motions_recommendations_by'
|
||||
)[0] as Config;
|
||||
|
||||
if (motionsRecommendationsByConfig) {
|
||||
const recomByString: string = motionsRecommendationsByConfig.value as string;
|
||||
return recomByString;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public deserialize(input: any): void {
|
||||
Object.assign(this, input);
|
||||
|
||||
this.submitters = [];
|
||||
if (input.submitters instanceof Array) {
|
||||
input.submitters.forEach(SubmitterData => {
|
||||
this.submitters.push(new MotionSubmitter(SubmitterData));
|
||||
@ -194,4 +86,9 @@ export class Motion extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hack to get them loaded at last
|
||||
*/
|
||||
BaseModel.registerCollectionElement('motions/motion', Motion);
|
||||
BaseModel.registerCollectionElement('motions/category', Category);
|
||||
BaseModel.registerCollectionElement('motions/workflow', Workflow);
|
||||
|
@ -34,7 +34,7 @@ export class Workflow extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
public state_by_id(id: number): WorkflowState {
|
||||
public getStateById(id: number): WorkflowState {
|
||||
let targetState;
|
||||
this.states.forEach(state => {
|
||||
if (id === state.id) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
<main>
|
||||
<mat-toolbar color="primary" translate>
|
||||
Legal Notice
|
||||
</mat-toolbar>
|
||||
|
||||
<mat-card class="os-card">
|
||||
<h2 translate>Legal Notice</h2>
|
||||
</mat-card>
|
||||
|
||||
<os-legal-notice-content></os-legal-notice-content>
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
<main>
|
||||
<mat-toolbar color="primary" translate>
|
||||
Privacy Policy
|
||||
</mat-toolbar>
|
||||
|
||||
<mat-card class="os-card">
|
||||
<h2 translate>Privacy Policy</h2>
|
||||
</mat-card>
|
||||
|
||||
<os-privacy-policy-content></os-privacy-policy-content>
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { MatSort, MatTable, MatTableDataSource } from '@angular/material';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseComponent } from '../../../base.component';
|
||||
import { Category } from '../../../shared/models/motions/category';
|
||||
import { BaseComponent } from '../../../../base.component';
|
||||
import { Category } from '../../../../shared/models/motions/category';
|
||||
|
||||
/**
|
||||
* List view for the categories.
|
@ -16,16 +16,26 @@
|
||||
<span *ngIf="editMotion"> {{contentForm.get('title').value}}</span>
|
||||
<br>
|
||||
<div *ngIf="motion" class='motion-submitter'>
|
||||
<span translate>by</span> {{motion.submitterAsUser}}
|
||||
<span translate>by</span> {{motion.submitters}}
|
||||
</div>
|
||||
</div>
|
||||
<span class='spacer'></span>
|
||||
|
||||
<button class='on-transition-fade' mat-icon-button [matMenuTriggerFor]="motionExtraMenu">
|
||||
<fa-icon icon='ellipsis-v'></fa-icon>
|
||||
</button>
|
||||
<!-- Button on the right-->
|
||||
<div *ngIf="editMotion">
|
||||
<button (click)='cancelEditMotionButton()' class='on-transition-fade' mat-icon-button>
|
||||
<fa-icon icon='times'></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!editMotion">
|
||||
<button class='on-transition-fade' mat-icon-button [matMenuTriggerFor]="motionExtraMenu">
|
||||
<fa-icon icon='ellipsis-v'></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<mat-menu #motionExtraMenu="matMenu">
|
||||
<!-- TODO the functions for the buttons -->
|
||||
<!-- TODO: the functions for the buttons -->
|
||||
<button mat-menu-item translate>Export As...</button>
|
||||
<button mat-menu-item translate>Project</button>
|
||||
<mat-divider></mat-divider>
|
||||
@ -137,24 +147,24 @@
|
||||
<!-- Submitter -->
|
||||
<div *ngIf="motion && motion.submitters || editMotion">
|
||||
<h3 translate>Submitters</h3>
|
||||
{{motion.submitterAsUser}}
|
||||
{{motion.submitters}}
|
||||
</div>
|
||||
|
||||
<!-- Supporter -->
|
||||
<div *ngIf='motion && motion.supporters_id && motion.supporters_id.length > 0 || editMotion'>
|
||||
<div *ngIf='motion && motion.hasSupporters() || editMotion'>
|
||||
<h3 translate>Supporters</h3>
|
||||
<!-- print all motion supporters -->
|
||||
</div>
|
||||
|
||||
<!-- State -->
|
||||
<div *ngIf='!newMotion && motion && motion.workflow && motion.state_id || editMotion'>
|
||||
<div *ngIf='!newMotion && motion && motion.workflow && motion.state || editMotion'>
|
||||
<div *ngIf='!editMotion'>
|
||||
<h3 translate>State</h3>
|
||||
{{motion.state}}
|
||||
</div>
|
||||
<mat-form-field *ngIf="editMotion && !newMotion">
|
||||
<mat-select placeholder='State' formControlName='state_id'>
|
||||
<mat-option [value]="motionCopy.state.id">{{motionCopy.state}}</mat-option>
|
||||
<mat-option [value]="motionCopy.stateId">{{motionCopy.state}}</mat-option>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-option *ngFor="let state of motionCopy.nextStates" [value]="state.id">{{state}}</mat-option>
|
||||
<mat-divider></mat-divider>
|
||||
@ -167,10 +177,10 @@
|
||||
|
||||
<!-- Recommendation -->
|
||||
<!-- The suggestion of the work group weather or not a motion should be accepted -->
|
||||
<div *ngIf='motion && motion.recomBy && (motion.recommendation_id || editMotion)'>
|
||||
<div *ngIf='motion && motion.recommender && (motion.recommendationId || editMotion)'>
|
||||
<div *ngIf='!editMotion'>
|
||||
<h3>{{motion.recomBy}}</h3>
|
||||
{{motion.recommendation.name}}
|
||||
<h3>{{motion.recommender}}</h3>
|
||||
{{motion.recommendation}}
|
||||
</div>
|
||||
<mat-form-field *ngIf="motion && editMotion">
|
||||
<mat-select placeholder='Recommendation' formControlName='recommendation_id'>
|
||||
@ -187,7 +197,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div *ngIf="motion && motion.category_id || editMotion">
|
||||
<div *ngIf="motion && motion.categoryId || editMotion">
|
||||
<div *ngIf='!editMotion'>
|
||||
<h3 translate> Category</h3>
|
||||
{{motion.category}}
|
@ -3,11 +3,11 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material';
|
||||
|
||||
import { BaseComponent } from '../../../base.component';
|
||||
import { Motion } from '../../../shared/models/motions/motion';
|
||||
import { Category } from '../../../shared/models/motions/category';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
import { ViewportService } from '../../../core/services/viewport.service';
|
||||
import { BaseComponent } from '../../../../base.component';
|
||||
import { Category } from '../../../../shared/models/motions/category';
|
||||
import { ViewportService } from '../../../../core/services/viewport.service';
|
||||
import { MotionRepositoryService } from '../../services/motion-repository.service';
|
||||
import { ViewMotion } from '../../models/view-motion';
|
||||
|
||||
/**
|
||||
* Component for the motion detail view
|
||||
@ -20,24 +20,16 @@ import { ViewportService } from '../../../core/services/viewport.service';
|
||||
export class MotionDetailComponent extends BaseComponent implements OnInit {
|
||||
/**
|
||||
* MatExpansionPanel for the meta info
|
||||
* Only relevant in mobile view
|
||||
*/
|
||||
@ViewChild('metaInfoPanel') public metaInfoPanel: MatExpansionPanel;
|
||||
|
||||
/**
|
||||
* MatExpansionPanel for the content panel
|
||||
* Only relevant in mobile view
|
||||
*/
|
||||
@ViewChild('contentPanel') public contentPanel: MatExpansionPanel;
|
||||
|
||||
/**
|
||||
* Target motion. Might be new or old
|
||||
*/
|
||||
public motion: Motion;
|
||||
|
||||
/**
|
||||
* Copy of the motion that the user might edit
|
||||
*/
|
||||
public motionCopy: Motion;
|
||||
|
||||
/**
|
||||
* Motions meta-info
|
||||
*/
|
||||
@ -58,6 +50,16 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
|
||||
*/
|
||||
public newMotion = false;
|
||||
|
||||
/**
|
||||
* Target motion. Might be new or old
|
||||
*/
|
||||
public motion: ViewMotion;
|
||||
|
||||
/**
|
||||
* Copy of the motion that the user might edit
|
||||
*/
|
||||
public motionCopy: ViewMotion;
|
||||
|
||||
/**
|
||||
* Constuct the detail view.
|
||||
*
|
||||
@ -65,14 +67,14 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
|
||||
* @param router to navigate back to the motion list and to an existing motion
|
||||
* @param route determine if this is a new or an existing motion
|
||||
* @param formBuilder For reactive forms. Form Group and Form Control
|
||||
* @param dataSend To send changes of the motion
|
||||
* @param repo: Motion Repository
|
||||
*/
|
||||
public constructor(
|
||||
public vp: ViewportService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: FormBuilder,
|
||||
private dataSend: DataSendService
|
||||
private repo: MotionRepositoryService
|
||||
) {
|
||||
super();
|
||||
this.createForm();
|
||||
@ -82,21 +84,14 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
|
||||
this.editMotion = true;
|
||||
|
||||
// Both are (temporarily) necessary until submitter and supporters are implemented
|
||||
this.motion = new Motion();
|
||||
this.motionCopy = new Motion();
|
||||
// TODO new Motion and ViewMotion
|
||||
this.motion = new ViewMotion();
|
||||
this.motionCopy = new ViewMotion();
|
||||
} else {
|
||||
// load existing motion
|
||||
this.route.params.subscribe(params => {
|
||||
// has the motion of the DataStore was initialized before.
|
||||
this.motion = this.DS.get(Motion, params.id);
|
||||
|
||||
// Observe motion to get the motion in the parameter and also get the changes
|
||||
this.DS.changeObservable.subscribe(newModel => {
|
||||
if (newModel instanceof Motion) {
|
||||
if (newModel.id === +params.id) {
|
||||
this.motion = newModel as Motion;
|
||||
}
|
||||
}
|
||||
this.repo.getViewMotionObservable(params.id).subscribe(newViewMotion => {
|
||||
this.motion = newViewMotion;
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -105,11 +100,11 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
|
||||
/**
|
||||
* Async load the values of the motion in the Form.
|
||||
*/
|
||||
public patchForm(formMotion: Motion): void {
|
||||
public patchForm(formMotion: ViewMotion): void {
|
||||
this.metaInfoForm.patchValue({
|
||||
category_id: formMotion.category_id,
|
||||
state_id: formMotion.state_id,
|
||||
recommendation_id: formMotion.recommendation_id,
|
||||
category_id: formMotion.categoryId,
|
||||
state_id: formMotion.stateId,
|
||||
recommendation_id: formMotion.recommendationId,
|
||||
identifier: formMotion.identifier,
|
||||
origin: formMotion.origin
|
||||
});
|
||||
@ -148,21 +143,22 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
|
||||
* in the list view automatically
|
||||
*
|
||||
* TODO: state is not yet saved. Need a special "put" command
|
||||
*
|
||||
* TODO: Repo should handle
|
||||
*/
|
||||
public saveMotion(): void {
|
||||
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
|
||||
this.motionCopy.patchValues(newMotionValues);
|
||||
|
||||
// TODO: send to normal motion to verify
|
||||
this.dataSend.saveModel(this.motionCopy).subscribe(answer => {
|
||||
if (answer && answer.id && this.newMotion) {
|
||||
this.router.navigate(['./motions/' + answer.id]);
|
||||
}
|
||||
});
|
||||
if (this.newMotion) {
|
||||
this.repo.saveMotion(newMotionValues).subscribe(response => {
|
||||
this.router.navigate(['./motions/' + response.id]);
|
||||
});
|
||||
} else {
|
||||
this.repo.saveMotion(newMotionValues, this.motionCopy).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* return all Categories.
|
||||
* return all Categories
|
||||
*/
|
||||
public getMotionCategories(): Category[] {
|
||||
return this.DS.getAll(Category);
|
||||
@ -175,10 +171,8 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
|
||||
this.editMotion ? (this.editMotion = false) : (this.editMotion = true);
|
||||
if (this.editMotion) {
|
||||
// copy the motion
|
||||
this.motionCopy = new Motion();
|
||||
this.motionCopy.patchValues(this.motion);
|
||||
this.motionCopy = this.motion.copy();
|
||||
this.patchForm(this.motionCopy);
|
||||
|
||||
if (this.vp.isMobile) {
|
||||
this.metaInfoPanel.open();
|
||||
this.contentPanel.open();
|
||||
@ -188,11 +182,26 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the editing process
|
||||
*
|
||||
* If a new motion was created, return to the list.
|
||||
*/
|
||||
public cancelEditMotionButton(): void {
|
||||
if (this.newMotion) {
|
||||
this.router.navigate(['./motions/']);
|
||||
} else {
|
||||
this.editMotion = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger to delete the motion
|
||||
*
|
||||
* TODO: Repo should handle
|
||||
*/
|
||||
public deleteMotionButton(): void {
|
||||
this.dataSend.delete(this.motion).subscribe(answer => {
|
||||
this.repo.deleteMotion(this.motion).subscribe(answer => {
|
||||
this.router.navigate(['./motions/']);
|
||||
});
|
||||
}
|
@ -26,11 +26,11 @@
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> Title </mat-header-cell>
|
||||
<mat-cell *matCellDef="let motion">
|
||||
<div class='innerTable'>
|
||||
<span class='motion-list-title'>{{motion.versions[0].title}}</span>
|
||||
<span class='motion-list-title'>{{motion.title}}</span>
|
||||
<br>
|
||||
<span class='motion-list-from'>
|
||||
<span translate>by</span>
|
||||
{{motion.submitterAsUser}}
|
||||
{{motion.submitters}}
|
||||
</span>
|
||||
</div>
|
||||
</mat-cell>
|
@ -1,12 +1,14 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Motion } from '../../../shared/models/motions/motion';
|
||||
import { MatTable, MatPaginator, MatSort, MatTableDataSource } from '@angular/material';
|
||||
import { Workflow } from '../../../shared/models/motions/workflow';
|
||||
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseComponent } from '../../../../base.component';
|
||||
import { MotionRepositoryService } from '../../services/motion-repository.service';
|
||||
import { ViewMotion } from '../../models/view-motion';
|
||||
import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
|
||||
|
||||
/**
|
||||
* Component that displays all the motions in a Table using DataSource.
|
||||
@ -17,25 +19,17 @@ import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
||||
styleUrls: ['./motion-list.component.scss']
|
||||
})
|
||||
export class MotionListComponent extends BaseComponent implements OnInit {
|
||||
/**
|
||||
* Store motion workflows (to check the status of the motions)
|
||||
*/
|
||||
public workflowArray: Array<Workflow>;
|
||||
|
||||
/**
|
||||
* Store the motions
|
||||
*/
|
||||
public motionArray: Array<Motion>;
|
||||
|
||||
/**
|
||||
* Will be processed by the mat-table
|
||||
*
|
||||
* Will represent the object that comes from the repository
|
||||
*/
|
||||
public dataSource: MatTableDataSource<Motion>;
|
||||
public dataSource: MatTableDataSource<ViewMotion>;
|
||||
|
||||
/**
|
||||
* The table itself.
|
||||
*/
|
||||
@ViewChild(MatTable) public table: MatTable<Motion>;
|
||||
@ViewChild(MatTable) public table: MatTable<ViewMotion>;
|
||||
|
||||
/**
|
||||
* Pagination. Might be turned off to all motions at once.
|
||||
@ -54,6 +48,8 @@ export class MotionListComponent extends BaseComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Use for maximal width
|
||||
*
|
||||
* TODO: Needs vp.desktop check
|
||||
*/
|
||||
public columnsToDisplayFullWidth = ['identifier', 'title', 'meta', 'state'];
|
||||
|
||||
@ -79,12 +75,14 @@ export class MotionListComponent extends BaseComponent implements OnInit {
|
||||
* @param translate Translation
|
||||
* @param router Router
|
||||
* @param route Current route
|
||||
* @param repo Motion Repository
|
||||
*/
|
||||
public constructor(
|
||||
protected titleService: Title,
|
||||
protected translate: TranslateService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private repo: MotionRepositoryService
|
||||
) {
|
||||
super(titleService, translate);
|
||||
}
|
||||
@ -94,19 +92,13 @@ export class MotionListComponent extends BaseComponent implements OnInit {
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Motions');
|
||||
this.workflowArray = this.DS.getAll(Workflow);
|
||||
this.motionArray = this.DS.getAll(Motion);
|
||||
this.dataSource = new MatTableDataSource(this.motionArray);
|
||||
|
||||
this.dataSource = new MatTableDataSource();
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
|
||||
// Observe DataStore for motions. Initially, executes once for every motion.
|
||||
// The alternative approach is to put the observable as DataSource to the table
|
||||
this.DS.changeObservable.subscribe(newModel => {
|
||||
if (newModel instanceof Motion) {
|
||||
this.motionArray = this.DS.getAll(Motion);
|
||||
this.dataSource.data = this.motionArray;
|
||||
}
|
||||
this.repo.getViewMotionListObservable().subscribe(newMotions => {
|
||||
this.dataSource.data = newMotions;
|
||||
});
|
||||
}
|
||||
|
||||
@ -115,12 +107,12 @@ export class MotionListComponent extends BaseComponent implements OnInit {
|
||||
*
|
||||
* @param motion The row the user clicked at
|
||||
*/
|
||||
public selectMotion(motion: Motion): void {
|
||||
public selectMotion(motion: ViewMotion): void {
|
||||
this.router.navigate(['./' + motion.id], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon to the coresponding Motion Status
|
||||
* Get the icon to the corresponding Motion Status
|
||||
* TODO Needs to be more accessible (Motion workflow needs adjustment on the server)
|
||||
* @param state the name of the state
|
||||
*/
|
||||
@ -142,7 +134,11 @@ export class MotionListComponent extends BaseComponent implements OnInit {
|
||||
* @param state
|
||||
*/
|
||||
public isDisplayIcon(state: WorkflowState): boolean {
|
||||
return state.name === 'accepted' || state.name === 'rejected' || state.name === 'not decided';
|
||||
if (state) {
|
||||
return state.name === 'accepted' || state.name === 'rejected' || state.name === 'not decided';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
193
client/src/app/site/motions/models/view-motion.ts
Normal file
193
client/src/app/site/motions/models/view-motion.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import { Motion } from '../../../shared/models/motions/motion';
|
||||
import { Category } from '../../../shared/models/motions/category';
|
||||
import { User } from '../../../shared/models/users/user';
|
||||
import { Workflow } from '../../../shared/models/motions/workflow';
|
||||
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
||||
import { BaseModel } from '../../../shared/models/base.model';
|
||||
|
||||
/**
|
||||
* Motion class for the View
|
||||
*
|
||||
* Stores a motion including all (implicit) references
|
||||
* Provides "safe" access to variables and functions in {@link Motion}
|
||||
* @ignore
|
||||
*/
|
||||
export class ViewMotion {
|
||||
private _motion: Motion;
|
||||
private _category: Category;
|
||||
private _submitters: User[];
|
||||
private _supporters: User[];
|
||||
private _workflow: Workflow;
|
||||
private _state: WorkflowState;
|
||||
|
||||
public get motion(): Motion {
|
||||
return this._motion;
|
||||
}
|
||||
|
||||
public get id(): number {
|
||||
if (this.motion) {
|
||||
return this.motion.id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get identifier(): string {
|
||||
if (this.motion) {
|
||||
return this.motion.identifier;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get title(): string {
|
||||
if (this.motion) {
|
||||
return this.motion.title;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get text(): string {
|
||||
if (this.motion) {
|
||||
return this.motion.text;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get reason(): string {
|
||||
if (this.motion) {
|
||||
return this.motion.reason;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get category(): Category {
|
||||
return this._category;
|
||||
}
|
||||
|
||||
public get categoryId(): number {
|
||||
if (this._motion && this._motion.category_id) {
|
||||
return this.category.id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get submitters(): User[] {
|
||||
return this._submitters;
|
||||
}
|
||||
|
||||
public get supporters(): User[] {
|
||||
return this._supporters;
|
||||
}
|
||||
|
||||
public get workflow(): Workflow {
|
||||
return this._workflow;
|
||||
}
|
||||
|
||||
public get state(): WorkflowState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public get stateId(): number {
|
||||
if (this._motion && this._motion.state_id) {
|
||||
return this._motion.state_id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get recommendationId(): number {
|
||||
return this._motion.recommendation_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME:
|
||||
* name of recommender exist in a config
|
||||
* previously solved using `this.DS.filter<Config>(Config)`
|
||||
* and checking: motionsRecommendationsByConfig.value
|
||||
*
|
||||
*/
|
||||
public get recommender(): string {
|
||||
return null;
|
||||
}
|
||||
|
||||
public get recommendation(): WorkflowState {
|
||||
if (this.recommendationId && this.workflow) {
|
||||
return this.workflow.getStateById(this.recommendationId);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get origin(): string {
|
||||
if (this.motion) {
|
||||
return this.motion.origin;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public get nextStates(): WorkflowState[] {
|
||||
if (this.state && this.workflow) {
|
||||
return this.state.getNextStates(this.workflow);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
motion?: Motion,
|
||||
category?: Category,
|
||||
submitters?: User[],
|
||||
supporters?: User[],
|
||||
workflow?: Workflow,
|
||||
state?: WorkflowState
|
||||
) {
|
||||
this._motion = motion;
|
||||
this._category = category;
|
||||
this._submitters = submitters;
|
||||
this._supporters = supporters;
|
||||
this._workflow = workflow;
|
||||
this._state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the local objects if required
|
||||
* @param update
|
||||
*/
|
||||
public updateValues(update: BaseModel): void {
|
||||
if (update instanceof Workflow) {
|
||||
if (this.motion && update.id === this.motion.workflow_id) {
|
||||
this._workflow = update as Workflow;
|
||||
}
|
||||
} else if (update instanceof Category) {
|
||||
if (this.motion && update.id === this.motion.category_id) {
|
||||
this._category = update as Category;
|
||||
}
|
||||
}
|
||||
// TODO: There is no way (yet) to add Submitters to a motion
|
||||
// Thus, this feature could not be tested
|
||||
}
|
||||
|
||||
public hasSupporters(): boolean {
|
||||
return !!(this.supporters && this.supporters.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate this motion into a copy of itself
|
||||
*/
|
||||
public copy(): ViewMotion {
|
||||
return new ViewMotion(
|
||||
this._motion,
|
||||
this._category,
|
||||
this._submitters,
|
||||
this._supporters,
|
||||
this._workflow,
|
||||
this._state
|
||||
);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { MotionListComponent } from './motion-list/motion-list.component';
|
||||
import { MotionDetailComponent } from './motion-detail/motion-detail.component';
|
||||
import { CategoryListComponent } from './category-list/category-list.component';
|
||||
import { MotionListComponent } from './components/motion-list/motion-list.component';
|
||||
import { MotionDetailComponent } from './components/motion-detail/motion-detail.component';
|
||||
import { CategoryListComponent } from './components/category-list/category-list.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: MotionListComponent },
|
||||
|
@ -3,9 +3,9 @@ import { CommonModule } from '@angular/common';
|
||||
|
||||
import { MotionsRoutingModule } from './motions-routing.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { MotionListComponent } from './motion-list/motion-list.component';
|
||||
import { MotionDetailComponent } from './motion-detail/motion-detail.component';
|
||||
import { CategoryListComponent } from './category-list/category-list.component';
|
||||
import { MotionListComponent } from './components/motion-list/motion-list.component';
|
||||
import { MotionDetailComponent } from './components/motion-detail/motion-detail.component';
|
||||
import { CategoryListComponent } from './components/category-list/category-list.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MotionsRoutingModule, SharedModule],
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { MotionRepositoryService } from './motion-repository.service';
|
||||
|
||||
describe('MotionRepositoryService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [MotionRepositoryService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([MotionRepositoryService], (service: MotionRepositoryService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -0,0 +1,194 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
import { OpenSlidesComponent } from '../../../openslides.component';
|
||||
import { Motion } from '../../../shared/models/motions/motion';
|
||||
import { User } from '../../../shared/models/users/user';
|
||||
import { Category } from '../../../shared/models/motions/category';
|
||||
import { Workflow } from '../../../shared/models/motions/workflow';
|
||||
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
||||
import { ViewMotion } from '../models/view-motion';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Repository Services for motions (and potentially categories)
|
||||
*
|
||||
* The repository is meant to process domain objects (those found under
|
||||
* shared/models), so components can display them and interact with them.
|
||||
*
|
||||
* Rather than manipulating models directly, the repository is meant to
|
||||
* inform the {@link DataSendService} about changes which will send
|
||||
* them to the Server.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MotionRepositoryService extends OpenSlidesComponent {
|
||||
/**
|
||||
* Stores all the viewMotion in an object
|
||||
*/
|
||||
private viewMotionStore: { [motionId: number]: ViewMotion } = {};
|
||||
|
||||
/**
|
||||
* Stores subjects to viewMotions in a list
|
||||
*/
|
||||
private viewMotionSubjects: { [motionId: number]: BehaviorSubject<ViewMotion> } = {};
|
||||
|
||||
/**
|
||||
* Observable subject for the whole list
|
||||
*/
|
||||
private viewMotionListSubject: BehaviorSubject<ViewMotion[]> = new BehaviorSubject<ViewMotion[]>(null);
|
||||
|
||||
/**
|
||||
* Creates a MotionRepository
|
||||
*
|
||||
* Converts existing and incoming motions to ViewMotions
|
||||
* Handles CRUD using an observer to the DataStore
|
||||
* @param DataSend
|
||||
*/
|
||||
public constructor(private dataSend: DataSendService) {
|
||||
super();
|
||||
|
||||
this.populateViewMotions();
|
||||
|
||||
// Could be raise in error if the root injector is not known
|
||||
this.DS.changeObservable.subscribe(model => {
|
||||
if (model instanceof Motion) {
|
||||
// Add new and updated motions to the viewMotionStore
|
||||
this.AddViewMotion(model);
|
||||
this.updateObservables(model.id);
|
||||
} else if (model instanceof Category || model instanceof User || model instanceof Workflow) {
|
||||
// if an domain object we need was added or changed, update ViewMotionStore
|
||||
this.getViewMotionList().forEach(viewMotion => {
|
||||
viewMotion.updateValues(model);
|
||||
});
|
||||
this.updateObservables(model.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Watch the Observables for deleting
|
||||
this.DS.deletedObservable.subscribe(model => {
|
||||
if (model.collection === 'motions/motion') {
|
||||
delete this.viewMotionStore[model.id];
|
||||
this.updateObservables(model.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* called from the constructor.
|
||||
*
|
||||
* Populate the local viewMotionStore with ViewMotion Objects.
|
||||
* Does nothing if the database was not created yet.
|
||||
*/
|
||||
private populateViewMotions(): void {
|
||||
this.DS.getAll<Motion>(Motion).forEach(motion => {
|
||||
this.AddViewMotion(motion);
|
||||
this.updateViewMotionObservable(motion.id);
|
||||
});
|
||||
this.updateViewMotionListObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a motion to a ViewMotion and adds it to the store.
|
||||
*
|
||||
* Foreign references of the motion will be resolved (e.g submitters to users)
|
||||
* Expandable to all (server side) changes that might occur on the motion object.
|
||||
*
|
||||
* @param motion blank motion domain object
|
||||
*/
|
||||
private AddViewMotion(motion: Motion): void {
|
||||
const category = this.DS.get(Category, motion.category_id);
|
||||
const submitters = this.DS.getMany(User, motion.submitterIds);
|
||||
const supporters = this.DS.getMany(User, motion.supporters_id);
|
||||
const workflow = this.DS.get(Workflow, motion.workflow_id);
|
||||
let state: WorkflowState = null;
|
||||
if (workflow) {
|
||||
state = workflow.getStateById(motion.state_id);
|
||||
}
|
||||
this.viewMotionStore[motion.id] = new ViewMotion(motion, category, submitters, supporters, workflow, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and updates a motion
|
||||
*
|
||||
* Creates a (real) motion with patched data and delegate it
|
||||
* to the {@link DataSendService}
|
||||
*
|
||||
* @param update the form data containing the update values
|
||||
* @param viewMotion The View Motion. If not present, a new motion will be created
|
||||
*/
|
||||
public saveMotion(update: any, viewMotion?: ViewMotion): Observable<any> {
|
||||
let updateMotion: Motion;
|
||||
|
||||
if (viewMotion) {
|
||||
// implies that an existing motion was updated
|
||||
updateMotion = viewMotion.motion;
|
||||
} else {
|
||||
// implies that a new motion was created
|
||||
updateMotion = new Motion();
|
||||
}
|
||||
updateMotion.patchValues(update);
|
||||
return this.dataSend.saveModel(updateMotion);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the current observable MotionView
|
||||
*/
|
||||
public getViewMotionObservable(id: number): Observable<ViewMotion> {
|
||||
if (!this.viewMotionSubjects[id]) {
|
||||
this.updateViewMotionObservable(id);
|
||||
}
|
||||
return this.viewMotionSubjects[id].asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* return the Observable of the whole store
|
||||
*/
|
||||
public getViewMotionListObservable(): Observable<ViewMotion[]> {
|
||||
return this.viewMotionListSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleting a motion.
|
||||
*
|
||||
* Extract the motion out of the motionView and delegate
|
||||
* to {@link DataSendService}
|
||||
* @param viewMotion
|
||||
*/
|
||||
public deleteMotion(viewMotion: ViewMotion): Observable<any> {
|
||||
return this.dataSend.delete(viewMotion.motion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ViewMotion observable using a ViewMotion corresponding to the id
|
||||
*/
|
||||
private updateViewMotionObservable(id: number): void {
|
||||
if (!this.viewMotionSubjects[id]) {
|
||||
this.viewMotionSubjects[id] = new BehaviorSubject<ViewMotion>(null);
|
||||
}
|
||||
this.viewMotionSubjects[id].next(this.viewMotionStore[id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* helper function to return the viewMotions as array
|
||||
*/
|
||||
private getViewMotionList(): ViewMotion[] {
|
||||
return Object.values(this.viewMotionStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* update the observable of the list
|
||||
*/
|
||||
private updateViewMotionListObservable(): void {
|
||||
this.viewMotionListSubject.next(this.getViewMotionList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers both the observable update routines
|
||||
*/
|
||||
private updateObservables(id: number): void {
|
||||
this.updateViewMotionListObservable();
|
||||
this.updateViewMotionObservable(id);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user