Add Tags into motion details

Adds tags to motion repository,
view-motion and selectable for the motion detail view
This commit is contained in:
Sean Engelhardt 2019-02-01 12:31:10 +01:00
parent f3c3d8ab8c
commit f992b77d99
5 changed files with 154 additions and 10 deletions

View File

@ -31,6 +31,7 @@ import { ViewUnifiedChange } from '../../../site/motions/models/view-unified-cha
import { ViewStatuteParagraph } from '../../../site/motions/models/view-statute-paragraph'; import { ViewStatuteParagraph } from '../../../site/motions/models/view-statute-paragraph';
import { Workflow } from '../../../shared/models/motions/workflow'; import { Workflow } from '../../../shared/models/motions/workflow';
import { WorkflowState } from '../../../shared/models/motions/workflow-state'; import { WorkflowState } from '../../../shared/models/motions/workflow-state';
import { Tag } from 'app/shared/models/core/tag';
/** /**
* Repository Services for motions (and potentially categories) * Repository Services for motions (and potentially categories)
@ -71,7 +72,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
private personalNoteService: PersonalNoteService, private personalNoteService: PersonalNoteService,
private translate: TranslateService private translate: TranslateService
) { ) {
super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock, Mediafile]); super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock, Mediafile, Tag]);
} }
/** /**
@ -90,11 +91,23 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
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); const block = this.DS.get(MotionBlock, motion.motion_block_id);
const attachments = this.DS.getMany(Mediafile, motion.attachments_id); const attachments = this.DS.getMany(Mediafile, motion.attachments_id);
const tags = this.DS.getMany(Tag, motion.tags_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, block, attachments); return new ViewMotion(
motion,
category,
submitters,
supporters,
workflow,
state,
item,
block,
attachments,
tags
);
} }
/** /**
@ -219,6 +232,26 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
await this.update(motion, viewMotion); await this.update(motion, viewMotion);
} }
/**
* Adds new or removes existing tags from motions
*
* @param viewMotion the motion to tag
* @param tagId the tags id to add or remove
*/
public async setTag(viewMotion: ViewMotion, tagId: number): Promise<void> {
const motion = viewMotion.motion;
const tagIndex = motion.tags_id.findIndex(tag => tag === tagId);
if (tagIndex === -1) {
// add tag to motion
motion.tags_id.push(tagId);
} else {
// remove tag from motion
motion.tags_id.splice(tagIndex, 1);
}
await this.update(motion, viewMotion);
}
/** /**
* Sets the submitters by sending a request to the server, * Sets the submitters by sending a request to the server,
* *

View File

@ -303,6 +303,47 @@
</mat-basic-chip> </mat-basic-chip>
</div> </div>
<!-- Tags -->
<!-- Disabled during "new motion" since changing has no effect -->
<div *ngIf="!editMotion && tagObserver.value.length > 0">
<h4 *ngIf="perms.isAllowed('change_metadata', motion) || motion.hasTags()">Tags</h4>
<!-- For privileged users -->
<div *ngIf="perms.isAllowed('change_metadata', motion)">
<!-- Selection menu -->
<mat-menu #tagMenu="matMenu">
<button mat-menu-item *ngFor="let tag of tagObserver.value" (click)="setTag($event, tag.id)">
<mat-icon *ngIf="motion.tags.includes(tag)">check</mat-icon>
{{ tag }}
</button>
</mat-menu>
<!-- Make the whole container a trigger to prevent unexpected menu behavior -->
<div [matMenuTriggerFor]="tagMenu">
<!-- No selected tags -->
<mat-basic-chip *ngIf="!motion.hasTags()" class="grey" disabled>
{{ '' }}
</mat-basic-chip>
<!-- Display a chip list of tags -->
<mat-chip-list class="mat-chip-list-stacked">
<mat-basic-chip *ngFor="let tag of motion.tags" class="grey" disabled>
{{ tag }}
</mat-basic-chip>
</mat-chip-list>
</div>
</div>
<!-- For non privileged users -->
<div *ngIf="!perms.isAllowed('change_metadata', motion)">
<mat-chip-list class="mat-chip-list-stacked">
<mat-basic-chip *ngFor="let tag of motion.tags" class="grey">
{{ tag }}
</mat-basic-chip>
</mat-chip-list>
</div>
</div>
<!-- Block --> <!-- Block -->
<div *ngIf="!editMotion && blockObserver.value.length > 0"> <div *ngIf="!editMotion && blockObserver.value.length > 0">
<h4 translate>Motion block</h4> <h4 translate>Motion block</h4>
@ -494,8 +535,13 @@
<!-- The HTML Editor --> <!-- The HTML Editor -->
<editor formControlName="text" [init]="tinyMceSettings" *ngIf="motion && editMotion" required></editor> <editor formControlName="text" [init]="tinyMceSettings" *ngIf="motion && editMotion" required></editor>
<div *ngIf="contentForm.get('text').invalid && (contentForm.get('text').dirty || contentForm.get('text').touched)" <div
class="red-warning-text" translate> *ngIf="
contentForm.get('text').invalid && (contentForm.get('text').dirty || contentForm.get('text').touched)
"
class="red-warning-text"
translate
>
This field is required. This field is required.
</div> </div>
@ -506,15 +552,30 @@
<!-- Reason --> <!-- Reason -->
<div *ngIf="motion.reason || editMotion"> <div *ngIf="motion.reason || editMotion">
<h3 [ngClass]="(reasonRequired && contentForm.get('reason').invalid && (contentForm.get('reason').dirty || contentForm.get('reason').touched)) ? 'red-warning-text' : ''"> <h3
[ngClass]="
reasonRequired &&
contentForm.get('reason').invalid &&
(contentForm.get('reason').dirty || contentForm.get('reason').touched)
? 'red-warning-text'
: ''
"
>
<span translate>Reason</span>&nbsp;<span *ngIf="reasonRequired && editMotion">*</span> <span translate>Reason</span>&nbsp;<span *ngIf="reasonRequired && editMotion">*</span>
</h3> </h3>
<div class="motion-text" *ngIf="!editMotion"><div [innerHtml]="motion.reason"></div></div> <div class="motion-text" *ngIf="!editMotion"><div [innerHtml]="motion.reason"></div></div>
<!-- The HTML Editor --> <!-- The HTML Editor -->
<editor formControlName="reason" [init]="tinyMceSettings" *ngIf="editMotion" required></editor> <editor formControlName="reason" [init]="tinyMceSettings" *ngIf="editMotion" required></editor>
<div *ngIf="reasonRequired && contentForm.get('reason').invalid && (contentForm.get('reason').dirty || contentForm.get('reason').touched)" <div
class="red-warning-text" translate> *ngIf="
reasonRequired &&
contentForm.get('reason').invalid &&
(contentForm.get('reason').dirty || contentForm.get('reason').touched)
"
class="red-warning-text"
translate
>
This field is required. This field is required.
</div> </div>
</div> </div>

View File

@ -275,3 +275,9 @@ span {
padding: 0px; padding: 0px;
} }
} }
.mat-chip-list-stacked {
.mat-chip {
margin: 4px 4px 4px 8px;
}
}

View File

@ -40,6 +40,7 @@ import { ViewUnifiedChange } from '../../models/view-unified-change';
import { ViewStatuteParagraph } from '../../models/view-statute-paragraph'; import { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
import { Workflow } from 'app/shared/models/motions/workflow'; import { Workflow } from 'app/shared/models/motions/workflow';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { Tag } from 'app/shared/models/core/tag';
/** /**
* Component for the motion detail view * Component for the motion detail view
@ -221,6 +222,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
*/ */
public agendaItemObserver: BehaviorSubject<Item[]>; public agendaItemObserver: BehaviorSubject<Item[]>;
/**
* Subject for tags
*/
public tagObserver: BehaviorSubject<Tag[]>;
/** /**
* Determine if the name of supporters are visible * Determine if the name of supporters are visible
*/ */
@ -356,6 +362,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.blockObserver = new BehaviorSubject(DS.getAll(MotionBlock)); this.blockObserver = new BehaviorSubject(DS.getAll(MotionBlock));
this.mediafilesObserver = new BehaviorSubject(DS.getAll(Mediafile)); this.mediafilesObserver = new BehaviorSubject(DS.getAll(Mediafile));
this.agendaItemObserver = new BehaviorSubject(DS.getAll(Item)); this.agendaItemObserver = new BehaviorSubject(DS.getAll(Item));
this.tagObserver = new BehaviorSubject(DS.getAll(Tag));
// 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 => {
@ -372,6 +379,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.mediafilesObserver.next(DS.getAll(Mediafile)); this.mediafilesObserver.next(DS.getAll(Mediafile));
} else if (newModel instanceof Item) { } else if (newModel instanceof Item) {
this.agendaItemObserver.next(DS.getAll(Item)); this.agendaItemObserver.next(DS.getAll(Item));
} else if (newModel instanceof Tag) {
this.tagObserver.next(DS.getAll(Tag));
} }
}); });
@ -1013,6 +1022,17 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
} }
} }
/**
* Adds or removes a tag to the current motion
*
* @param id Motion tag id
*/
public setTag(event: MouseEvent, id: number): void {
console.log('event: ', event);
event.stopPropagation();
this.repo.setTag(this.motion, id);
}
/** /**
* Add the current motion to a motion block * Add the current motion to a motion block
* *

View File

@ -12,6 +12,7 @@ import { ViewMotionCommentSection } from './view-motion-comment-section';
import { Workflow } from '../../../shared/models/motions/workflow'; import { Workflow } from '../../../shared/models/motions/workflow';
import { WorkflowState } from '../../../shared/models/motions/workflow-state'; import { WorkflowState } from '../../../shared/models/motions/workflow-state';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { Tag } from 'app/shared/models/core/tag';
/** /**
* The line numbering mode for the motion detail view. * The line numbering mode for the motion detail view.
@ -51,6 +52,7 @@ export class ViewMotion extends BaseProjectableModel {
protected _item: Item; protected _item: Item;
protected _block: MotionBlock; protected _block: MotionBlock;
protected _attachments: Mediafile[]; protected _attachments: Mediafile[];
protected _tags: Tag[];
public personalNote: PersonalNoteContent; public personalNote: PersonalNoteContent;
/** /**
@ -232,6 +234,10 @@ export class ViewMotion extends BaseProjectableModel {
return this._attachments ? this._attachments : null; return this._attachments ? this._attachments : null;
} }
public get tags(): Tag[] {
return this._tags ? this._tags : null;
}
/** /**
* @returns the creation date as Date object * @returns the creation date as Date object
*/ */
@ -313,7 +319,8 @@ export class ViewMotion extends BaseProjectableModel {
state?: WorkflowState, state?: WorkflowState,
item?: Item, item?: Item,
block?: MotionBlock, block?: MotionBlock,
attachments?: Mediafile[] attachments?: Mediafile[],
tags?: Tag[]
) { ) {
super(); super();
this._motion = motion; this._motion = motion;
@ -325,6 +332,7 @@ export class ViewMotion extends BaseProjectableModel {
this._item = item; this._item = item;
this._block = block; this._block = block;
this._attachments = attachments; this._attachments = attachments;
this._tags = tags;
} }
public getTitle(): string { public getTitle(): string {
@ -364,6 +372,8 @@ export class ViewMotion extends BaseProjectableModel {
this.updateUser(update as User); this.updateUser(update as User);
} else if (update instanceof Mediafile) { } else if (update instanceof Mediafile) {
this.updateAttachments(update as Mediafile); this.updateAttachments(update as Mediafile);
} else if (update instanceof Tag) {
this.updateTags(update as Tag);
} }
} }
@ -381,7 +391,7 @@ export class ViewMotion extends BaseProjectableModel {
/** /**
* Update routine for the workflow * Update routine for the workflow
* *
* @param workflow potentially the changed workflow (state). Needs manual verification * @param workflow potentially the (changed workflow (state). Needs manual verification
*/ */
public updateWorkflow(workflow: Workflow): void { public updateWorkflow(workflow: Workflow): void {
if (this.motion && workflow.id === this.motion.workflow_id) { if (this.motion && workflow.id === this.motion.workflow_id) {
@ -443,6 +453,15 @@ export class ViewMotion extends BaseProjectableModel {
} }
} }
public updateTags(update: Tag): void {
if (this.motion) {
if (this.tags_id && this.tags_id.includes(update.id)) {
const tagIndex = this.tags.findIndex(tag => tag.id === update.id);
this.tags[tagIndex] = update as Tag;
}
}
}
public hasSupporters(): boolean { public hasSupporters(): boolean {
return !!(this.supporters && this.supporters.length > 0); return !!(this.supporters && this.supporters.length > 0);
} }
@ -451,6 +470,10 @@ export class ViewMotion extends BaseProjectableModel {
return !!(this.attachments && this.attachments.length > 0); return !!(this.attachments && this.attachments.length > 0);
} }
public hasTags(): boolean {
return !!(this.tags && this.tags.length > 0);
}
public isStatuteAmendment(): boolean { public isStatuteAmendment(): boolean {
return !!this.statute_paragraph_id; return !!this.statute_paragraph_id;
} }
@ -508,7 +531,8 @@ export class ViewMotion extends BaseProjectableModel {
this._state, this._state,
this._item, this._item,
this._block, this._block,
this._attachments this._attachments,
this._tags
); );
} }
} }