Motion Attachments + Restructure

This commit is contained in:
Sean Engelhardt 2018-12-10 17:54:48 +01:00
parent 574fde5f6d
commit 00db199947
15 changed files with 574 additions and 333 deletions

View File

@ -24,6 +24,15 @@ export class Mediafile extends ProjectableBaseModel {
this.mediafile = new File(input.mediafile); this.mediafile = new File(input.mediafile);
} }
/**
* Determine the downloadURL
*
* @returns the download URL for the specific file as string
*/
public getDownloadUrl(): string {
return `${this.media_url_prefix}${this.mediafile.name}`;
}
public getTitle(): string { public getTitle(): string {
return this.title; return this.title;
} }

View File

@ -1,4 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { BaseRepository } from '../../base/base-repository'; import { BaseRepository } from '../../base/base-repository';
import { DataStoreService } from '../../../core/services/data-store.service'; import { DataStoreService } from '../../../core/services/data-store.service';
@ -12,6 +14,7 @@ import { ViewSpeaker } from '../models/view-speaker';
import { Speaker } from 'app/shared/models/agenda/speaker'; import { Speaker } from 'app/shared/models/agenda/speaker';
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
import { HttpService } from 'app/core/services/http.service'; import { HttpService } from 'app/core/services/http.service';
import { ConfigService } from 'app/core/services/config.service';
/** /**
* Repository service for users * Repository service for users
@ -27,11 +30,13 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
* @param DS The DataStore * @param DS The DataStore
* @param httpService OpenSlides own HttpService * @param httpService OpenSlides own HttpService
* @param mapperService OpenSlides mapping service for collection strings * @param mapperService OpenSlides mapping service for collection strings
* @param config Read config variables
*/ */
public constructor( public constructor(
protected DS: DataStoreService, protected DS: DataStoreService,
private httpService: HttpService, private httpService: HttpService,
mapperService: CollectionStringModelMapperService mapperService: CollectionStringModelMapperService,
private config: ConfigService
) { ) {
super(DS, mapperService, Item); super(DS, mapperService, Item);
} }
@ -179,4 +184,13 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
const contentObject = this.getContentObject(item); const contentObject = this.getContentObject(item);
return new ViewItem(item, contentObject); return new ViewItem(item, contentObject);
} }
/**
* Get agenda visibility from the config
*
* @return An observable to the default agenda visibility
*/
public getDefaultAgendaVisibility(): Observable<number> {
return this.config.get('agenda_new_items_default_visibility').pipe(map(key => +key));
}
} }

View File

@ -92,8 +92,7 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
* Should be overridden by implementations. Currently there is no default action. * Should be overridden by implementations. Currently there is no default action.
* @param row a ViewModel * @param row a ViewModel
*/ */
public singleSelectAction(row: V) : void { public singleSelectAction(row: V): void {}
}
/** /**
* enables/disables the multiSelect Mode * enables/disables the multiSelect Mode
@ -107,6 +106,13 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
} }
} }
/**
* Select all files in the current data source
*/
public selectAll(): void {
this.selectedRows = this.dataSource.data;
}
/** /**
* Returns the current state of the multiSelect modus * Returns the current state of the multiSelect modus
*/ */

View File

@ -168,6 +168,10 @@
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
<span translate>Exit multiselect</span> <span translate>Exit multiselect</span>
</button> </button>
<button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon>
<span translate>Select all</span>
</button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="deleteSelected()"> <button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="deleteSelected()">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>

View File

@ -43,7 +43,7 @@ export class ViewMediafile extends BaseViewModel {
} }
public get downloadUrl(): string { public get downloadUrl(): string {
return this.mediafile && this.mediafile.mediafile ? `${this.prefix}${this.fileName}` : null; return this.mediafile ? this.mediafile.getDownloadUrl() : null;
} }
public constructor(mediafile?: Mediafile, uploader?: User) { public constructor(mediafile?: Mediafile, uploader?: User) {
@ -100,7 +100,7 @@ export class ViewMediafile extends BaseViewModel {
'video/3gpp', 'video/3gpp',
'video/x-msvideo', 'video/x-msvideo',
'video/x-ms-wmv', 'video/x-ms-wmv',
'video/x-matroska', 'video/x-matroska'
].includes(this.type); ].includes(this.type);
} }

View File

@ -3,14 +3,6 @@
<button class="small-button" type="button" mat-icon-button disableRipple *ngIf="!isEditMode" (click)="onEdit()"> <button class="small-button" type="button" mat-icon-button disableRipple *ngIf="!isEditMode" (click)="onEdit()">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<span *ngIf="isEditMode">
<button class="small-button" type="button" mat-icon-button disableRipple (click)="onSave()">
<mat-icon>save</mat-icon>
</button>
<button class="small-button" type="button" mat-icon-button disableRipple (click)="onCancel()">
<mat-icon>close</mat-icon>
</button>
</span>
</h4> </h4>
<div *ngIf="!isEditMode"> <div *ngIf="!isEditMode">
@ -32,7 +24,13 @@
></os-search-value-selector> ></os-search-value-selector>
</form> </form>
<os-sorting-list class="testclass" [input]="editSubmitterObservable" [live]="true" [count]="true" (sortEvent)="onSortingChange($event)"> <os-sorting-list
class="testclass"
[input]="editSubmitterObservable"
[live]="true"
[count]="true"
(sortEvent)="onSortingChange($event)"
>
<!-- implicit user references into the component using ng-template slot --> <!-- implicit user references into the component using ng-template slot -->
<ng-template let-user> <ng-template let-user>
<button type="button" mat-icon-button matTooltip="{{ 'Remove' | translate }}" (click)="onRemove(user)"> <button type="button" mat-icon-button matTooltip="{{ 'Remove' | translate }}" (click)="onRemove(user)">
@ -40,6 +38,9 @@
</button> </button>
</ng-template> </ng-template>
</os-sorting-list> </os-sorting-list>
<mat-card-actions>
<button type="button" mat-button (click)="onSave()"><span translate>Save</span></button>
<button type="button" mat-button (click)="onCancel()"><span translate>Cancel</span></button>
</mat-card-actions>
</mat-card> </mat-card>
</div> </div>

View File

@ -13,6 +13,7 @@ import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { DataStoreService } from 'app/core/services/data-store.service'; import { DataStoreService } from 'app/core/services/data-store.service';
import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service'; import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service';
import { ViewMotionBlock } from '../../models/view-motion-block'; import { ViewMotionBlock } from '../../models/view-motion-block';
import { AgendaRepositoryService } from 'app/site/agenda/services/agenda-repository.service';
/** /**
* Table for the motion blocks * Table for the motion blocks
@ -57,6 +58,7 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
* @param router routing to children * @param router routing to children
* @param route determine the local route * @param route determine the local route
* @param repo the motion block repository * @param repo the motion block repository
* @param agendaRepo the agenda repository service
* @param DS the dataStore * @param DS the dataStore
* @param formBuilder creates forms * @param formBuilder creates forms
*/ */
@ -67,6 +69,7 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private repo: MotionBlockRepositoryService, private repo: MotionBlockRepositoryService,
private agendaRepo: AgendaRepositoryService,
private DS: DataStoreService, private DS: DataStoreService,
private formBuilder: FormBuilder private formBuilder: FormBuilder
) { ) {
@ -98,7 +101,7 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
this.dataSource.data = newMotionblocks; this.dataSource.data = newMotionblocks;
}); });
this.repo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility)); this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility));
} }
/** /**

View File

@ -12,7 +12,7 @@
<span translate>Motion</span> <span translate>Motion</span>
<!-- Whitespace between "Motion" and identifier --> <!-- Whitespace between "Motion" and identifier -->
<span>&nbsp;</span> <span *ngIf="!editMotion">{{ motion.identifier }}</span> <span>&nbsp;</span> <span *ngIf="!editMotion">{{ motion.identifier }}</span>
<span *ngIf="editMotion">{{ metaInfoForm.get('identifier').value }}</span> <span *ngIf="editMotion">{{ contentForm.get('identifier').value }}</span>
</h2> </h2>
<h2 *ngIf="newMotion" translate>New motion</h2> <h2 *ngIf="newMotion" translate>New motion</h2>
</div> </div>
@ -21,14 +21,32 @@
<div *ngIf="!editMotion" class="extra-controls-slot on-transition-fade"> <div *ngIf="!editMotion" class="extra-controls-slot on-transition-fade">
<div *ngIf="previousMotion"> <div *ngIf="previousMotion">
<button mat-button (click)="navigateToMotion(previousMotion)"> <button mat-button (click)="navigateToMotion(previousMotion)">
<mat-icon>navigate_before</mat-icon> <!-- possible icons:
arrow_left
chevron_left
first_page
arrow_back
arrow_back_ios
navigate_before
fast_rewind
-->
<mat-icon>chevron_left</mat-icon>
<span>{{ previousMotion.identifier }}</span> <span>{{ previousMotion.identifier }}</span>
</button> </button>
</div> </div>
<div *ngIf="nextMotion"> <div *ngIf="nextMotion">
<button mat-button (click)="navigateToMotion(nextMotion)"> <button mat-button (click)="navigateToMotion(nextMotion)">
<span>{{ nextMotion.identifier }}</span> <span>{{ nextMotion.identifier }}</span>
<mat-icon>navigate_next</mat-icon> <!-- possible icons:
arrow_right
chevron_right
last_page
arrow_forward
arrow_forward_ios
navigate_next
fast_forward
-->
<mat-icon>chevron_right</mat-icon>
</button> </button>
</div> </div>
</div> </div>
@ -87,18 +105,16 @@
</os-head-bar> </os-head-bar>
<!-- Title --> <!-- Title -->
<div *ngIf="motion" class="motion-title on-transition-fade"> <div class="motion-title on-transition-fade" *ngIf="motion && !editMotion">
<h2 *ngIf="!editMotion">{{ motion.title }}</h2> <h2>{{ motion.title }}</h2>
<h2 *ngIf="editMotion">{{ contentForm.get('title').value }}</h2>
</div> </div>
<ng-container *ngIf="vp.isMobile; then: mobileView; else: desktopView"></ng-container> <ng-container *ngIf="vp.isMobile; then mobileView; else desktopView"></ng-container>
<ng-template #mobileView> <ng-template #mobileView>
<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]="true" 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>
@ -126,8 +142,8 @@
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<os-motion-comments *ngIf="!newMotion" [motion]="motion"></os-motion-comments> <os-motion-comments *ngIf="!editMotion" [motion]="motion"></os-motion-comments>
<os-personal-note *ngIf="!newMotion" [motion]="motion"></os-personal-note> <os-personal-note *ngIf="!editMotion" [motion]="motion"></os-personal-note>
</mat-accordion> </mat-accordion>
</ng-template> </ng-template>
@ -139,8 +155,8 @@
<ng-container *ngTemplateOutlet="metaInfoTemplate"></ng-container> <ng-container *ngTemplateOutlet="metaInfoTemplate"></ng-container>
</div> </div>
<os-motion-comments *ngIf="!newMotion" [motion]="motion"></os-motion-comments> <os-motion-comments *ngIf="!editMotion" [motion]="motion"></os-motion-comments>
<os-personal-note *ngIf="!newMotion" [motion]="motion"></os-personal-note> <os-personal-note *ngIf="!editMotion" [motion]="motion"></os-personal-note>
</div> </div>
<div class="desktop-right "> <div class="desktop-right ">
<!-- Content --> <!-- Content -->
@ -150,71 +166,41 @@
</ng-template> </ng-template>
<ng-template #metaInfoTemplate> <ng-template #metaInfoTemplate>
<form [formGroup]="metaInfoForm" (keydown)="onKeyDown($event)" (ngSubmit)="saveMotion()"> <div *ngIf="motion">
<!-- Identifier -->
<div *ngIf="editMotion && !newMotion">
<!-- <div *ngIf="editMotion"> -->
<div *ngIf="!editMotion">
<h4 translate>Identifier</h4>
{{ motion.identifier }}
</div>
<mat-form-field *ngIf="editMotion">
<input
matInput
placeholder="{{ &quot;Identifier&quot; | translate }}"
formControlName="identifier"
[value]="motionCopy.identifier"
/>
</mat-form-field>
</div>
<!-- Submitters --> <!-- Submitters -->
<div *ngIf="motion && motion.submitters || newMotion"> <div *ngIf="motion.submitters || newMotion">
<div *ngIf="newMotion"> <div *ngIf="!editMotion"><os-manage-submitters [motion]="motion"></os-manage-submitters></div>
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector
ngDefaultControl
[form]="metaInfoForm"
[formControl]="metaInfoForm.get('submitters_id')"
[multiple]="true"
listname="{{ 'Submitters' | translate }}"
[InputListValues]="submitterObserver"
></os-search-value-selector>
</div>
</div>
<div *ngIf="!editMotion && !newMotion">
<os-manage-submitters [motion]="motion"></os-manage-submitters>
</div>
</div> </div>
<!-- Supporters --> <!-- do Support -->
<div *ngIf='motion && minSupporters'> <div *ngIf="minSupporters && !editMotion">
<div *ngIf="editMotion">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector
ngDefaultControl
[form]="metaInfoForm"
[formControl]="metaInfoForm.get('supporters_id')"
[multiple]="true"
listname="{{ 'Supporters' | translate }}"
[InputListValues]="supporterObserver"
></os-search-value-selector>
</div>
</div>
<div *ngIf="!editMotion">
<h4 *ngIf="perms.isAllowed('support', motion) || motion.hasSupporters()" translate>Supporters</h4> <h4 *ngIf="perms.isAllowed('support', motion) || motion.hasSupporters()" translate>Supporters</h4>
<!-- support button --> <!-- support button -->
<button type="button" *ngIf="perms.isAllowed('support', motion)" (click)=support() mat-raised-button color="primary"> <button
type="button"
mat-raised-button
color="primary"
(click)="support()"
*ngIf="perms.isAllowed('support', motion)"
>
<mat-icon>thumb_up</mat-icon> <mat-icon>thumb_up</mat-icon>
{{ 'Support' | translate }} {{ 'Support' | translate }}
</button> </button>
<!-- unsupport button --> <!-- unsupport button -->
<button type="button" *ngIf="perms.isAllowed('unsupport', motion)" (click)=unsupport() mat-raised-button color="primary"> <button
type="button"
*ngIf="perms.isAllowed('unsupport', motion)"
(click)="unsupport()"
mat-raised-button
color="primary"
>
<mat-icon>thumb_down</mat-icon> <mat-icon>thumb_down</mat-icon>
{{ 'Unsupport' | translate }} {{ 'Unsupport' | translate }}
</button> </button>
<!-- show supporters (TODO: open in dialog) --> <!-- show supporters (TODO: open in dialog) -->
<button type="button" *ngIf="motion.hasSupporters()" (click)=openSupportersDialog() mat-button> <button type="button" *ngIf="motion.hasSupporters()" (click)="openSupportersDialog()" mat-button>
{{ motion.supporters.length }} {{ 'supporters' | translate }} {{ motion.supporters.length }} {{ 'supporters' | translate }}
</button> </button>
<p *ngIf="showSupporters"> <p *ngIf="showSupporters">
@ -224,25 +210,28 @@
</p> </p>
</div> </div>
</div> <!-- Set State -->
<div *ngIf="!editMotion">
<!-- State -->
<div *ngIf='motion && !editMotion'>
<h4 translate>State</h4> <h4 translate>State</h4>
<mat-menu #stateMenu='matMenu'> <mat-menu #stateMenu="matMenu">
<button *ngFor='let state of motion.nextStates' mat-menu-item <button *ngFor="let state of motion.nextStates" mat-menu-item (click)="setState(state.id)">
(click)=setState(state.id)>{{ state.name | translate }} {{ state.name | translate }}
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)=setState(null)> <button mat-menu-item (click)="setState(null)">
<mat-icon>replay</mat-icon> {{ 'Reset state' | translate }} <mat-icon>replay</mat-icon> {{ 'Reset state' | translate }}
</button> </button>
</mat-menu> </mat-menu>
<mat-basic-chip [matMenuTriggerFor]='stateMenu' [ngClass]="{ <mat-basic-chip
'green': motion.state.css_class === 'success', *ngIf="motion.state"
'red': motion.state.css_class === 'danger', [matMenuTriggerFor]="stateMenu"
'grey': motion.state.css_class === 'default', [ngClass]="{
'lightblue': motion.state.css_class === 'primary' }"> 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 }} {{ motion.state.name | translate }}
</mat-basic-chip> </mat-basic-chip>
@ -250,27 +239,35 @@
</div> </div>
<!-- Recommendation --> <!-- Recommendation -->
<div *ngIf='motion && recommender && !editMotion'> <div *ngIf="recommender && !editMotion">
<h4>{{ recommender }}</h4> <h4>{{ recommender }}</h4>
<mat-menu #recommendationMenu='matMenu'> <mat-menu #recommendationMenu="matMenu">
<button *ngFor='let recommendation of motion.possibleRecommendations' mat-menu-item <button
(click)=setRecommendation(recommendation.id)>{{ recommendation.recommendation_label | translate }} *ngFor="let recommendation of motion.possibleRecommendations"
mat-menu-item
(click)="setRecommendation(recommendation.id)"
>
{{ recommendation.recommendation_label | translate }}
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)=setRecommendation(null)> <button mat-menu-item (click)="setRecommendation(null)">
<mat-icon>replay</mat-icon> {{ 'Reset recommendation' | translate }} <mat-icon>replay</mat-icon> {{ 'Reset recommendation' | translate }}
</button> </button>
</mat-menu> </mat-menu>
<mat-basic-chip [matMenuTriggerFor]='recommendationMenu' class="bluegrey"> <mat-basic-chip [matMenuTriggerFor]="recommendationMenu" class="bluegrey">
{{ motion.recommendation ? (motion.recommendation.recommendation_label | translate) : ('not set' | translate) }} {{
motion.recommendation
? (motion.recommendation.recommendation_label | translate)
: ('not set' | translate)
}}
</mat-basic-chip> </mat-basic-chip>
</div> </div>
<!-- Category --> <!-- Category -->
<!-- Disabled during "new motion" since changing has no effect --> <!-- Disabled during "new motion" since changing has no effect -->
<div *ngIf="motion && !editMotion"> <div *ngIf="!editMotion">
<h4 translate>Category</h4> <h4 translate>Category</h4>
<mat-menu #categoryMenu='matMenu'> <mat-menu #categoryMenu="matMenu">
<button <button
mat-menu-item mat-menu-item
*ngFor="let category of categoryObserver.value" *ngFor="let category of categoryObserver.value"
@ -278,75 +275,34 @@
> >
{{ category }} {{ category }}
</button> </button>
<button mat-menu-item (click)=setCategory(null)> <button mat-menu-item (click)="setCategory(null)">---</button>
---
</button>
</mat-menu> </mat-menu>
<mat-basic-chip [matMenuTriggerFor]='categoryMenu' class="grey"> <mat-basic-chip [matMenuTriggerFor]="categoryMenu" class="grey">
{{ motion.category ? motion.category : ('not set' | translate) }} {{ motion.category ? motion.category : ('not set' | translate) }}
</mat-basic-chip> </mat-basic-chip>
</div> </div>
<!-- Block --> <!-- Block -->
<div *ngIf="motion && !editMotion"> <div *ngIf="!editMotion">
<h4 translate>Motion block</h4> <h4 translate>Motion block</h4>
<mat-menu #blockMenu='matMenu'> <mat-menu #blockMenu="matMenu">
<button <button mat-menu-item *ngFor="let block of blockObserver.value" (click)="setBlock(block.id)">
mat-menu-item
*ngFor="let block of blockObserver.value"
(click)="setBlock(block.id)"
>
{{ block }} {{ block }}
</button> </button>
<button mat-menu-item (click)="setBlock(null)"> <button mat-menu-item (click)="setBlock(null)">---</button>
---
</button>
</mat-menu> </mat-menu>
<mat-basic-chip [matMenuTriggerFor]='blockMenu' class="grey"> <mat-basic-chip [matMenuTriggerFor]="blockMenu" class="grey">
{{ motion.motion_block ? motion.motion_block : ('not set' | translate) }} {{ motion.motion_block ? motion.motion_block : ('not set' | translate) }}
</mat-basic-chip> </mat-basic-chip>
</div> </div>
<!-- Workflow --> <!-- Origin - display only -->
<div *ngIf="editMotion"> <div *ngIf="!editMotion && motion.origin">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector
ngDefaultControl
[form]="metaInfoForm"
[formControl]="metaInfoForm.get('workflow_id')"
[multiple]="false"
listname="{{ 'Workflow' | translate }}"
[InputListValues]="workflowObserver"
></os-search-value-selector>
</div>
</div>
<!-- Origin -->
<div *ngIf="(motion && motion.origin) || editMotion">
<div *ngIf="!editMotion">
<h4 translate>Origin</h4> <h4 translate>Origin</h4>
{{ motion.origin }} {{ motion.origin }}
</div> </div>
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<mat-form-field *ngIf="editMotion">
<input
matInput
placeholder="{{ 'Origin' | translate}}"
formControlName="origin"
[value]="motionCopy.origin"
/>
</mat-form-field>
</div> </div>
</div>
<!-- Voting -->
<!--
<div *ngIf='motion.polls && motion.polls.length > 0 || editMotion'>
<h4 translate>Voting</h4>
</div>
-->
</form>
</ng-template> </ng-template>
<ng-template #contentTemplate> <ng-template #contentTemplate>
@ -356,9 +312,10 @@
(clickdown)="onKeyDown($event)" (clickdown)="onKeyDown($event)"
(keydown)="onKeyDown($event)" (keydown)="onKeyDown($event)"
(ngSubmit)="saveMotion()" (ngSubmit)="saveMotion()"
*ngIf="motion"
> >
<!-- Line Number and Diff buttons --> <!-- Line Number and Diff buttons -->
<div *ngIf="motion && !editMotion && !motion.isStatuteAmendment()" class="motion-text-controls"> <div *ngIf="!editMotion && !motion.isStatuteAmendment()" class="motion-text-controls">
<button <button
type="button" type="button"
mat-icon-button mat-icon-button
@ -397,13 +354,36 @@
</mat-form-field> </mat-form-field>
</div> </div>
<!-- Title --> <!-- Submitter -->
<div *ngIf="(motion && motion.title) || editMotion"> <div *ngIf="newMotion" class="content-field form100">
<div *ngIf="!editMotion"> <div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<h4>{{ motion.title }}</h4> <os-search-value-selector
ngDefaultControl
[form]="contentForm"
[formControl]="contentForm.get('submitters_id')"
[multiple]="true"
listname="{{ 'Submitters' | translate }}"
[InputListValues]="submitterObserver"
></os-search-value-selector>
</div>
</div> </div>
<mat-form-field *ngIf="editMotion" class="wide-form"> <div class="form-id-title">
<!-- Identifier -->
<div *ngIf="editMotion && !newMotion" class="content-field form-identifier">
<mat-form-field *ngIf="editMotion">
<input
matInput
placeholder="{{ 'Identifier' | translate }}"
formControlName="identifier"
[value]="motionCopy.identifier"
/>
</mat-form-field>
</div>
<!-- Title -->
<div *ngIf="editMotion" class="content-field form-title">
<mat-form-field *ngIf="editMotion">
<input <input
matInput matInput
osAutofocus osAutofocus
@ -414,14 +394,13 @@
/> />
</mat-form-field> </mat-form-field>
</div> </div>
</div>
<!-- Text --> <!-- Text -->
<span class="text-prefix-label">{{ preamble | translate }}</span> <span class="text-prefix-label">{{ preamble | translate }}</span>
<!-- Regular motions or traditional amendments --> <!-- Regular motions or traditional amendments -->
<ng-container <ng-container *ngIf="!editMotion && !motion.isStatuteAmendment() && !motion.isParagraphBasedAmendment()">
*ngIf="motion && !editMotion && !motion.isStatuteAmendment() && !motion.isParagraphBasedAmendment()"
>
<div <div
*ngIf="!isRecoModeDiff()" *ngIf="!isRecoModeDiff()"
class="motion-text" class="motion-text"
@ -451,7 +430,7 @@
</ng-container> </ng-container>
<div <div
class="motion-text line-numbers-none" class="motion-text line-numbers-none"
*ngIf="motion && !editMotion && motion.isStatuteAmendment()" *ngIf="!editMotion && motion.isStatuteAmendment()"
[innerHTML]="getFormattedStatuteAmendment()" [innerHTML]="getFormattedStatuteAmendment()"
></div> ></div>
@ -459,21 +438,120 @@
<editor formControlName="text" [init]="tinyMceSettings" *ngIf="motion && editMotion"></editor> <editor formControlName="text" [init]="tinyMceSettings" *ngIf="motion && editMotion"></editor>
<!-- Paragraph-based amendments --> <!-- Paragraph-based amendments -->
<ng-container *ngIf="motion && !editMotion && motion.isParagraphBasedAmendment()"> <ng-container *ngIf="!editMotion && motion.isParagraphBasedAmendment()">
<ng-container *ngTemplateOutlet="paragraphBasedAmendment"></ng-container> <ng-container *ngTemplateOutlet="paragraphBasedAmendment"></ng-container>
</ng-container> </ng-container>
<!-- Reason --> <!-- Reason -->
<div *ngIf="motion || editMotion"> <div *ngIf="motion.reason || editMotion">
<h5 *ngIf="motion.reason || editMotion" translate>Reason</h5> <h3 translate>Reason</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 <editor formControlName="reason" [init]="tinyMceSettings" *ngIf="editMotion"></editor>
formControlName='reason' </div>
[init]="tinyMceSettings"
*ngIf="editMotion" <div class="extra-data">
></editor> <!-- Attachments -->
<div *ngIf="motion.hasAttachments() || editMotion" class="content-field form100">
<div *ngIf="!editMotion">
<h4 translate>Attachments</h4>
<mat-list dense>
<mat-list-item *ngFor="let file of motion.attachments">
<a [routerLink]="" (click)="onClickAttacment(file)">{{ file.title }}</a>
</mat-list-item>
</mat-list>
</div>
<div *ngIf="editMotion">
<os-search-value-selector
ngDefaultControl
[form]="contentForm"
[formControl]="contentForm.get('attachments_id')"
[multiple]="true"
listname="{{ 'Attachments' | translate }}"
[InputListValues]="mediafilesObserver"
></os-search-value-selector>
</div>
</div>
<!-- Category form -->
<div class="content-field form100" *ngIf="newMotion && categoryObserver.value.length > 0">
<os-search-value-selector
ngDefaultControl
[form]="contentForm"
[formControl]="contentForm.get('category_id')"
[multiple]="false"
[includeNone]="true"
listname="{{ 'Category' | translate }}"
[InputListValues]="categoryObserver"
></os-search-value-selector>
</div>
<!-- Parent item -->
<div class="content-field form100" *ngIf="newMotion && agendaItemObserver.value.length > 0">
<os-search-value-selector
ngDefaultControl
[form]="contentForm"
[formControl]="contentForm.get('parent_id')"
[multiple]="false"
[includeNone]="true"
listname="{{ 'Parent Item' | translate }}"
[InputListValues]="agendaItemObserver"
></os-search-value-selector>
</div>
<!-- Visibility -->
<div class="content-field form100" *ngIf="newMotion">
<mat-form-field>
<mat-select formControlName="agenda_type" placeholder="{{ 'Agenda visibility' | translate }}">
<mat-option *ngFor="let type of itemVisibility" [value]="type.key">
<span>{{ type.name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Supporter form -->
<div class="content-field form100" *ngIf="editMotion && minSupporters">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector
ngDefaultControl
[form]="contentForm"
[formControl]="contentForm.get('supporters_id')"
[multiple]="true"
listname="{{ 'Supporters' | translate }}"
[InputListValues]="supporterObserver"
></os-search-value-selector>
</div>
</div>
<!-- Workflow -->
<div class="content-field form100" *ngIf="editMotion && workflowObserver.value.length > 1">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector
ngDefaultControl
[form]="contentForm"
[formControl]="contentForm.get('workflow_id')"
[multiple]="false"
listname="{{ 'Workflow' | translate }}"
[InputListValues]="workflowObserver"
></os-search-value-selector>
</div>
</div>
<!-- Origin form -->
<div class="content-field form100" *ngIf="editMotion">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<mat-form-field>
<input
matInput
placeholder="{{ 'Origin' | translate }}"
formControlName="origin"
[value]="motionCopy.origin"
/>
</mat-form-field>
</div>
</div>
</div> </div>
</form> </form>
</ng-template> </ng-template>
@ -522,9 +600,15 @@
<!-- Line number Menu --> <!-- Line number Menu -->
<mat-menu #lineNumberingMenu="matMenu"> <mat-menu #lineNumberingMenu="matMenu">
<div *ngIf="motion"> <div *ngIf="motion">
<button mat-menu-item translate (click)=setLineNumberingMode(0) [ngClass]="{ 'selected': motion.lnMode === 0 }">none</button> <button mat-menu-item translate (click)="setLineNumberingMode(0)" [ngClass]="{ selected: motion.lnMode === 0 }">
<button mat-menu-item translate (click)=setLineNumberingMode(1) [ngClass]="{ 'selected': motion.lnMode === 1 }">inline</button> none
<button mat-menu-item translate (click)=setLineNumberingMode(2) [ngClass]="{ 'selected': motion.lnMode === 2 }">outside</button> </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> </div>
</mat-menu> </mat-menu>

View File

@ -23,7 +23,6 @@ span {
line-height: 180%; line-height: 180%;
font-size: 120%; font-size: 120%;
color: #317796; // TODO: put in theme as $primary color: #317796; // TODO: put in theme as $primary
h2 { h2 {
margin: 0; margin: 0;
font-weight: normal; font-weight: normal;
@ -41,58 +40,6 @@ span {
} }
} }
.motion-submitter {
display: inline;
font-weight: bold;
font-size: 70%;
}
.meta-info-block {
form {
div + div {
margin-top: 15px;
}
ul {
margin: 5px;
}
}
h3 {
display: block;
// padding-top: 0;
margin-top: 0px; //distance between heading and text
margin-bottom: 3px; //distance between heading and text
font-size: 80%;
color: gray;
mat-icon {
margin-left: 5px;
}
}
.mat-form-field-label {
font-size: 12pt;
color: gray;
}
.mat-form-field-label-wrapper {
mat-icon {
margin-left: 5px;
}
}
}
.wide-form {
textarea {
height: 25vh;
}
::ng-deep {
width: 100%;
}
}
.meta-info-panel { .meta-info-panel {
padding-top: 25px; padding-top: 25px;
@ -143,6 +90,45 @@ span {
display: block; display: block;
margin: 0 10px 7px 0px; margin: 0 10px 7px 0px;
} }
.extra-data {
margin-top: 1em;
}
.content-field {
display: inline-block;
::ng-deep {
.mat-form-field {
width: 100%;
}
}
}
.form100 {
width: 100%;
}
.form70 {
width: 70%;
}
.form30 {
width: 30%;
}
.form-id-title {
display: flex;
.form-identifier {
flex: 0 0 95px;
max-width: 95px;
margin-right: 1em;
}
.form-title {
flex: 1 1 auto;
}
}
} }
.desktop-view { .desktop-view {

View File

@ -1,7 +1,9 @@
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 { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatExpansionPanel, MatSnackBar, MatCheckboxChange } from '@angular/material'; import { MatDialog, MatExpansionPanel, MatSnackBar, MatCheckboxChange } from '@angular/material';
import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators';
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';
@ -19,7 +21,7 @@ import {
} from '../motion-change-recommendation/motion-change-recommendation.component'; } from '../motion-change-recommendation/motion-change-recommendation.component';
import { ChangeRecommendationRepositoryService } from '../../services/change-recommendation-repository.service'; import { ChangeRecommendationRepositoryService } from '../../services/change-recommendation-repository.service';
import { ViewChangeReco } from '../../models/view-change-reco'; import { ViewChangeReco } from '../../models/view-change-reco';
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
import { ViewUnifiedChange } from '../../models/view-unified-change'; import { ViewUnifiedChange } from '../../models/view-unified-change';
import { OperatorService } from '../../../../core/services/operator.service'; import { OperatorService } from '../../../../core/services/operator.service';
import { BaseViewComponent } from '../../../base/base-view'; import { BaseViewComponent } from '../../../base/base-view';
@ -27,11 +29,14 @@ import { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
import { StatuteParagraphRepositoryService } from '../../services/statute-paragraph-repository.service'; import { StatuteParagraphRepositoryService } from '../../services/statute-paragraph-repository.service';
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 { LocalPermissionsService } from '../../services/local-permissions.service'; import { LocalPermissionsService } from '../../services/local-permissions.service';
import { ViewCreateMotion } from '../../models/view-create-motion'; import { ViewCreateMotion } from '../../models/view-create-motion';
import { CreateMotion } from '../../models/create-motion'; import { CreateMotion } from '../../models/create-motion';
import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { itemVisibilityChoices, Item } from 'app/shared/models/agenda/item';
import { PromptService } from 'app/core/services/prompt.service';
import { AgendaRepositoryService } from 'app/site/agenda/services/agenda-repository.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
/** /**
* Component for the motion detail view * Component for the motion detail view
@ -56,11 +61,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
@ViewChild('contentPanel') @ViewChild('contentPanel')
public contentPanel: MatExpansionPanel; public contentPanel: MatExpansionPanel;
/**
* Motions meta-info
*/
public metaInfoForm: FormGroup;
/** /**
* Motion content. Can be a new version * Motion content. Can be a new version
*/ */
@ -184,6 +184,16 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
*/ */
public blockObserver: BehaviorSubject<MotionBlock[]>; public blockObserver: BehaviorSubject<MotionBlock[]>;
/**
* Subject for mediafiles
*/
public mediafilesObserver: BehaviorSubject<Mediafile[]>;
/**
* Subject for agenda items
*/
public agendaItemObserver: BehaviorSubject<Item[]>;
/** /**
* Determine if the name of supporters are visible * Determine if the name of supporters are visible
*/ */
@ -209,6 +219,16 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
*/ */
public showAmendmentContext = false; public showAmendmentContext = false;
/**
* Determines the default agenda item visibility
*/
public defaultVisibility: number;
/**
* Determine visibility states for the agenda that will be created implicitly
*/
public itemVisibility = itemVisibilityChoices;
/** /**
* Constuct the detail view. * Constuct the detail view.
* *
@ -222,11 +242,13 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* @param formBuilder For reactive forms. Form Group and Form Control * @param formBuilder For reactive forms. Form Group and Form Control
* @param dialogService For opening dialogs * @param dialogService For opening dialogs
* @param repo Motion Repository * @param repo Motion Repository
* @param agendaRepo Read out agenda variables
* @param changeRecoRepo Change Recommendation Repository * @param changeRecoRepo Change Recommendation Repository
* @param statuteRepo: Statute Paragraph Repository * @param statuteRepo: Statute Paragraph Repository
* @param DS The DataStoreService * @param DS The DataStoreService
* @param configService The configuration provider * @param configService The configuration provider
* @param sanitizer For making HTML SafeHTML * @param sanitizer For making HTML SafeHTML
* @param promptService ensure safe deletion
*/ */
public constructor( public constructor(
title: Title, title: Title,
@ -240,15 +262,15 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private dialogService: MatDialog, private dialogService: MatDialog,
private repo: MotionRepositoryService, private repo: MotionRepositoryService,
private agendaRepo: AgendaRepositoryService,
private changeRecoRepo: ChangeRecommendationRepositoryService, private changeRecoRepo: ChangeRecommendationRepositoryService,
private statuteRepo: StatuteParagraphRepositoryService, private statuteRepo: StatuteParagraphRepositoryService,
private DS: DataStoreService, private DS: DataStoreService,
private configService: ConfigService, private configService: ConfigService,
private sanitizer: DomSanitizer private sanitizer: DomSanitizer,
private promptService: PromptService
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.createForm();
this.getMotionByUrl();
// Initial Filling of the Subjects // Initial Filling of the Subjects
this.submitterObserver = new BehaviorSubject(DS.getAll(User)); this.submitterObserver = new BehaviorSubject(DS.getAll(User));
@ -256,6 +278,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.categoryObserver = new BehaviorSubject(DS.getAll(Category)); this.categoryObserver = new BehaviorSubject(DS.getAll(Category));
this.workflowObserver = new BehaviorSubject(DS.getAll(Workflow)); this.workflowObserver = new BehaviorSubject(DS.getAll(Workflow));
this.blockObserver = new BehaviorSubject(DS.getAll(MotionBlock)); this.blockObserver = new BehaviorSubject(DS.getAll(MotionBlock));
this.mediafilesObserver = new BehaviorSubject(DS.getAll(Mediafile));
this.agendaItemObserver = new BehaviorSubject(DS.getAll(Item));
// 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 => {
@ -268,8 +292,13 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.workflowObserver.next(DS.getAll(Workflow)); this.workflowObserver.next(DS.getAll(Workflow));
} else if (newModel instanceof MotionBlock) { } else if (newModel instanceof MotionBlock) {
this.blockObserver.next(DS.getAll(MotionBlock)); this.blockObserver.next(DS.getAll(MotionBlock));
} else if (newModel instanceof Mediafile) {
this.mediafilesObserver.next(DS.getAll(Mediafile));
} else if (newModel instanceof Item) {
this.agendaItemObserver.next(DS.getAll(Item));
} }
}); });
// load config variables // load config variables
this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled)); this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled));
this.configService.get('motions_min_supporters').subscribe(supporters => (this.minSupporters = supporters)); this.configService.get('motions_min_supporters').subscribe(supporters => (this.minSupporters = supporters));
@ -277,6 +306,45 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.configService.get('motions_amendments_enabled').subscribe(enabled => (this.amendmentsEnabled = enabled)); this.configService.get('motions_amendments_enabled').subscribe(enabled => (this.amendmentsEnabled = enabled));
} }
/**
* Init.
* Sets the surrounding motions to navigate back and forth
*/
public ngOnInit(): void {
this.createForm();
this.getMotionByUrl();
this.repo.getViewModelListObservable().subscribe(newMotionList => {
if (newMotionList) {
this.allMotions = newMotionList;
this.setSurroundingMotions();
}
});
this.statuteRepo.getViewModelListObservable().subscribe(newViewStatuteParagraphs => {
this.statuteParagraphs = newViewStatuteParagraphs;
});
// Set the default visibility using observers
this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => {
if (visibility && this.newMotion) {
this.contentForm.get('agenda_type').setValue(visibility);
}
});
// disable the selector for attachments if there are none
this.mediafilesObserver.subscribe(files => {
if (this.createForm) {
const attachmentsCtrl = this.contentForm.get('attachments_id');
if (this.mediafilesObserver.value.length === 0) {
attachmentsCtrl.disable();
} else {
attachmentsCtrl.enable();
}
}
});
}
/** /**
* Merges amendments and change recommendations and sorts them by the line numbers. * Merges amendments and change recommendations and sorts them by the line numbers.
* Called each time one of these arrays changes. * Called each time one of these arrays changes.
@ -350,12 +418,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* Async load the values of the motion in the Form. * Async load the values of the motion in the Form.
*/ */
public patchForm(formMotion: ViewMotion): void { public patchForm(formMotion: ViewMotion): void {
const metaInfoPatch = {};
Object.keys(this.metaInfoForm.controls).forEach(ctrl => {
metaInfoPatch[ctrl] = formMotion[ctrl];
});
this.metaInfoForm.patchValue(metaInfoPatch);
const contentPatch: { [key: string]: any } = {}; const contentPatch: { [key: string]: any } = {};
Object.keys(this.contentForm.controls).forEach(ctrl => { Object.keys(this.contentForm.controls).forEach(ctrl => {
contentPatch[ctrl] = formMotion[ctrl]; contentPatch[ctrl] = formMotion[ctrl];
@ -380,20 +442,19 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* TODO: Build a custom form validator * TODO: Build a custom form validator
*/ */
public createForm(): void { public createForm(): void {
this.metaInfoForm = this.formBuilder.group({
identifier: [''],
category_id: [''],
state_id: [''],
recommendation_id: [''],
submitters_id: [],
supporters_id: [[]],
workflow_id: [],
origin: ['']
});
this.contentForm = this.formBuilder.group({ this.contentForm = this.formBuilder.group({
identifier: [''],
title: ['', Validators.required], title: ['', Validators.required],
text: ['', Validators.required], text: ['', Validators.required],
reason: [''], reason: [''],
category_id: [''],
attachments_id: [[]],
parent_id: [],
agenda_type: [''],
submitters_id: [],
supporters_id: [[]],
workflow_id: [],
origin: [''],
statute_amendment: [''], // Internal value for the checkbox, not saved to the model statute_amendment: [''], // Internal value for the checkbox, not saved to the model
statute_paragraph_id: [''] statute_paragraph_id: ['']
}); });
@ -417,6 +478,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* *
* @param motionValues valus for the new motion * @param motionValues valus for the new motion
* @param ctor The motion constructor, so different motion types can be created. * @param ctor The motion constructor, so different motion types can be created.
*
* @returns the motion to save
*/ */
private prepareMotionForSave<T extends Motion>(motionValues: any, ctor: new (...args: any[]) => T): T { private prepareMotionForSave<T extends Motion>(motionValues: any, ctor: new (...args: any[]) => T): T {
const motion = new ctor(); const motion = new ctor();
@ -440,7 +503,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* Creates a motion. Calls the "patchValues" function in the MotionObject * Creates a motion. Calls the "patchValues" function in the MotionObject
*/ */
public async createMotion(): Promise<void> { public async createMotion(): Promise<void> {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value }; const newMotionValues = { ...this.contentForm.value };
const motion = this.prepareMotionForSave(newMotionValues, CreateMotion); const motion = this.prepareMotionForSave(newMotionValues, CreateMotion);
try { try {
@ -455,7 +518,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* Save a motion. Calls the "patchValues" function in the MotionObject * Save a motion. Calls the "patchValues" function in the MotionObject
*/ */
public async updateMotion(): Promise<void> { public async updateMotion(): Promise<void> {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value }; const newMotionValues = { ...this.contentForm.value };
const motion = this.prepareMotionForSave(newMotionValues, Motion); const motion = this.prepareMotionForSave(newMotionValues, Motion);
this.repo.update(motion, this.motionCopy).then(() => (this.editMotion = false), this.raiseError); this.repo.update(motion, this.motionCopy).then(() => (this.editMotion = false), this.raiseError);
} }
@ -473,6 +536,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* get the formated motion text from the repository. * get the formated motion text from the repository.
*
* @returns formated motion texts
*/ */
public getFormattedTextPlain(): string { public getFormattedTextPlain(): string {
// Prevent this.allChangingObjects to be reordered from within formatMotion // Prevent this.allChangingObjects to be reordered from within formatMotion
@ -510,9 +575,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* If `this.motion` is an amendment, this returns a specified line range from the parent motion * If `this.motion` is an amendment, this returns a specified line range from the parent motion
* (e.g. to show the contect in which this amendment is happening) * (e.g. to show the contect in which this amendment is happening)
* *
* @param {number} from * @param from the line number to start
* @param {number} to * @param to the line number to stop
* @returns {SafeHtml} * @returns safe html strings
*/ */
public getParentMotionRange(from: number, to: number): SafeHtml { public getParentMotionRange(from: number, to: number): SafeHtml {
const str = this.repo.extractMotionLineRange(this.motion.parent_id, { from, to }, true); const str = this.repo.extractMotionLineRange(this.motion.parent_id, { from, to }, true);
@ -521,7 +586,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* get the diff html from the statute amendment, as SafeHTML for [innerHTML] * get the diff html from the statute amendment, as SafeHTML for [innerHTML]
* @returns {SafeHtml} *
* @returns safe html strings
*/ */
public getFormattedStatuteAmendment(): SafeHtml { public getFormattedStatuteAmendment(): SafeHtml {
const diffHtml = this.repo.formatStatuteAmendment(this.statuteParagraphs, this.motion, this.motion.lineLength); const diffHtml = this.repo.formatStatuteAmendment(this.statuteParagraphs, this.motion, this.motion.lineLength);
@ -530,17 +596,18 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Trigger to delete the motion. * Trigger to delete the motion.
* Sends a delete request over the repository and
* shows a "are you sure" dialog
*/ */
public async deleteMotionButton(): Promise<void> { public async deleteMotionButton(): Promise<void> {
this.repo.delete(this.motion).then(() => { const content = this.translate.instant('Are you sure you want to delete this motion block?');
if (await this.promptService.open(this.motion.title, content)) {
await this.repo.delete(this.motion);
this.router.navigate(['./motions/']); this.router.navigate(['./motions/']);
}, this.raiseError); }
} }
/** /**
* Sets the motions line numbering mode * Sets the motions line numbering mode
*
* @param mode Needs to got 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 {
@ -549,6 +616,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Returns true if no line numbers are to be shown. * Returns true if no line numbers are to be shown.
*
* @returns whether there are line numbers at all
*/ */
public isLineNumberingNone(): boolean { public isLineNumberingNone(): boolean {
return this.motion.lnMode === LineNumberingMode.None; return this.motion.lnMode === LineNumberingMode.None;
@ -556,6 +625,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Returns true if the line numbers are to be shown within the text with no line breaks. * Returns true if the line numbers are to be shown within the text with no line breaks.
*
* @returns whether the line numberings are inside
*/ */
public isLineNumberingInline(): boolean { public isLineNumberingInline(): boolean {
return this.motion.lnMode === LineNumberingMode.Inside; return this.motion.lnMode === LineNumberingMode.Inside;
@ -563,6 +634,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Returns true if the line numbers are to be shown to the left of the text. * Returns true if the line numbers are to be shown to the left of the text.
*
* @returns whether the line numberings are outside
*/ */
public isLineNumberingOutside(): boolean { public isLineNumberingOutside(): boolean {
return this.motion.lnMode === LineNumberingMode.Outside; return this.motion.lnMode === LineNumberingMode.Outside;
@ -627,6 +700,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Comes from the head bar * Comes from the head bar
*
* @param mode * @param mode
*/ */
public setEditMode(mode: boolean): void { public setEditMode(mode: boolean): void {
@ -644,6 +718,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
} }
} }
/**
* Sets the default workflow ID during form creation
*/
public updateWorkflowIdForCreateForm(): void { public updateWorkflowIdForCreateForm(): void {
const isStatuteAmendment = !!this.contentForm.get('statute_amendment').value; const isStatuteAmendment = !!this.contentForm.get('statute_amendment').value;
const configKey = isStatuteAmendment ? 'motions_statute_amendments_workflow' : 'motions_workflow'; const configKey = isStatuteAmendment ? 'motions_statute_amendments_workflow' : 'motions_workflow';
@ -663,7 +740,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
skipWhile(id => !id) skipWhile(id => !id)
) )
.subscribe(id => { .subscribe(id => {
this.metaInfoForm.patchValue({ this.contentForm.patchValue({
workflow_id: parseInt(id, 10) workflow_id: parseInt(id, 10)
}); });
}); });
@ -671,6 +748,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* If the checkbox is deactivated, the statute_paragraph_id-field needs to be reset, as only that field is saved * If the checkbox is deactivated, the statute_paragraph_id-field needs to be reset, as only that field is saved
*
* @param {MatCheckboxChange} $event * @param {MatCheckboxChange} $event
*/ */
public onStatuteAmendmentChange($event: MatCheckboxChange): void { public onStatuteAmendmentChange($event: MatCheckboxChange): void {
@ -682,6 +760,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* The paragraph of the statute to amend was changed -> change the input fields below * The paragraph of the statute to amend was changed -> change the input fields below
*
* @param {number} newValue * @param {number} newValue
*/ */
public onStatuteParagraphChange(newValue: number): void { public onStatuteParagraphChange(newValue: number): void {
@ -694,6 +773,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Navigates the user to the given ViewMotion * Navigates the user to the given ViewMotion
*
* @param motion target * @param motion target
*/ */
public navigateToMotion(motion: ViewMotion): void { public navigateToMotion(motion: ViewMotion): void {
@ -749,6 +829,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Sets the state * Sets the state
*
* @param id Motion state id * @param id Motion state id
*/ */
public setState(id: number): void { public setState(id: number): void {
@ -757,6 +838,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Sets the recommendation * Sets the recommendation
*
* @param id Motion recommendation id * @param id Motion recommendation id
*/ */
public setRecommendation(id: number): void { public setRecommendation(id: number): void {
@ -765,6 +847,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Sets the category for current motion * Sets the category for current motion
*
* @param id Motion category id * @param id Motion category id
*/ */
public setCategory(id: number): void { public setCategory(id: number): void {
@ -797,32 +880,29 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
/** /**
* Create the absolute path to the corresponding list of speakers * Create the absolute path to the corresponding list of speakers
*
* @returns the link to the corresponding list of speakers as string * @returns the link to the corresponding list of speakers as string
*/ */
public getSpeakerLink(): string { public getSpeakerLink(): string {
return `/agenda/${this.motion.agenda_item_id}/speakers`; return `/agenda/${this.motion.agenda_item_id}/speakers`;
} }
/**
* Click handler for attachments
*
* @param attachment the selected file
*/
public onClickAttacment(attachment: Mediafile): void {
window.open(attachment.getDownloadUrl());
}
/** /**
* Determine if the user has the correct requirements to alter the motion * Determine if the user has the correct requirements to alter the motion
* TODO: All views should probably have a "isAllowedTo" routine to simplify this process
*
* @returns whether or not the OP is allowed to edit the motion
*/ */
public opCanEdit(): boolean { public opCanEdit(): boolean {
return this.op.hasPerms('motions.can_manage', 'motions.can_manage_metadata'); return this.op.hasPerms('motions.can_manage', 'motions.can_manage_metadata');
} }
/**
* Init.
* Sets the surrounding motions to navigate back and forth
*/
public ngOnInit(): void {
this.repo.getViewModelListObservable().subscribe(newMotionList => {
if (newMotionList) {
this.allMotions = newMotionList;
this.setSurroundingMotions();
}
});
this.statuteRepo.getViewModelListObservable().subscribe(newViewStatuteParagraphs => {
this.statuteParagraphs = newViewStatuteParagraphs;
});
}
} }

View File

@ -41,13 +41,21 @@
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let motion"> <mat-cell *matCellDef="let motion">
<div class="innerTable"> <div class="innerTable">
<span class="motion-list-title">{{ motion.title }}</span> <br /> <span class="motion-list-title">{{ motion.title }}</span>
<!-- attachments -->
<span class="attached-files" *ngIf="motion.hasAttachments()">
<!-- <mat-basic-chip class="bluegrey"> <mat-icon>attach_file</mat-icon> </mat-basic-chip> -->
<mat-icon>attach_file</mat-icon>
</span>
<br />
<span class="motion-list-from" *ngIf="motion.submitters.length"> <span class="motion-list-from" *ngIf="motion.submitters.length">
<span translate>by</span> {{ motion.submitters }} <span translate>by</span> {{ motion.submitters }}
</span> </span>
<br *ngIf="motion.submitters.length" /> <br *ngIf="motion.submitters.length" />
<!-- state --> <!-- state -->
<mat-basic-chip <mat-basic-chip
*ngIf="motion.state"
[ngClass]="{ [ngClass]="{
green: motion.state.css_class === 'success', green: motion.state.css_class === 'success',
red: motion.state.css_class === 'danger', red: motion.state.css_class === 'danger',

View File

@ -30,6 +30,17 @@
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
font-size: 90%; font-size: 90%;
} }
.attached-files {
.mat-icon {
display: inline-flex;
vertical-align: middle;
$icon-size: 16px;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
}
}
} }
/** State */ /** State */

View File

@ -9,6 +9,7 @@ import { ViewMotionCommentSection } from './view-motion-comment-section';
import { MotionComment } from '../../../shared/models/motions/motion-comment'; import { MotionComment } from '../../../shared/models/motions/motion-comment';
import { Item } from 'app/shared/models/agenda/item'; import { Item } from 'app/shared/models/agenda/item';
import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
export enum LineNumberingMode { export enum LineNumberingMode {
None, None,
@ -39,6 +40,7 @@ export class ViewMotion extends BaseViewModel {
protected _state: WorkflowState; protected _state: WorkflowState;
protected _item: Item; protected _item: Item;
protected _block: MotionBlock; protected _block: MotionBlock;
protected _attachments: Mediafile[];
/** /**
* Indicates the LineNumberingMode Mode. * Indicates the LineNumberingMode Mode.
@ -79,7 +81,7 @@ export class ViewMotion extends BaseViewModel {
} }
public get identifier(): string { public get identifier(): string {
return this.motion ? this.motion.identifier : null; return this.motion && this.motion.identifier ? this.motion.identifier : null;
} }
public get title(): string { public get title(): string {
@ -188,6 +190,10 @@ export class ViewMotion extends BaseViewModel {
return this._item; return this._item;
} }
public get agenda_type(): number {
return this.item ? this.item.type : null;
}
public get motion_block_id(): number { public get motion_block_id(): number {
return this.motion ? this.motion.motion_block_id : null; return this.motion ? this.motion.motion_block_id : null;
} }
@ -209,7 +215,15 @@ export class ViewMotion extends BaseViewModel {
} }
public get tags_id(): number[] { public get tags_id(): number[] {
return this._motion ? this._motion.tags_id : null; return this.motion ? this.motion.tags_id : null;
}
public get attachments_id(): number[] {
return this.motion ? this.motion.attachments_id : null
}
public get attachments(): Mediafile[] {
return this._attachments ? this._attachments : null;
} }
public constructor( public constructor(
@ -220,10 +234,10 @@ export class ViewMotion extends BaseViewModel {
workflow?: Workflow, workflow?: Workflow,
state?: WorkflowState, state?: WorkflowState,
item?: Item, item?: Item,
block?: MotionBlock block?: MotionBlock,
attachments?: Mediafile[],
) { ) {
super(); super();
this._motion = motion; this._motion = motion;
this._category = category; this._category = category;
this._submitters = submitters; this._submitters = submitters;
@ -232,6 +246,7 @@ export class ViewMotion extends BaseViewModel {
this._state = state; this._state = state;
this._item = item; this._item = item;
this._block = block; this._block = block;
this._attachments = attachments;
// 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( /*this._configService.get('motions_default_line_numbering').subscribe(
@ -255,6 +270,7 @@ export class ViewMotion extends BaseViewModel {
/** /**
* Returns the motion comment for the given section. Null, if no comment exist. * Returns the motion comment for the given section. Null, if no comment exist.
*
* @param section The section to search the comment for. * @param section The section to search the comment for.
*/ */
public getCommentForSection(section: ViewMotionCommentSection): MotionComment { public getCommentForSection(section: ViewMotionCommentSection): MotionComment {
@ -266,6 +282,7 @@ export class ViewMotion extends BaseViewModel {
/** /**
* Updates the local objects if required * Updates the local objects if required
*
* @param update * @param update
*/ */
public updateValues(update: BaseModel): void { public updateValues(update: BaseModel): void {
@ -279,11 +296,14 @@ export class ViewMotion extends BaseViewModel {
this.updateMotionBlock(update); this.updateMotionBlock(update);
} else if (update instanceof User) { } else if (update instanceof User) {
this.updateUser(update as User); this.updateUser(update as User);
} else if (update instanceof Mediafile) {
this.updateAttachments(update as Mediafile);
} }
} }
/** /**
* Update routine for the category * Update routine for the category
*
* @param category potentially the changed category. Needs manual verification * @param category potentially the changed category. Needs manual verification
*/ */
public updateCategory(category: Category): void { public updateCategory(category: Category): void {
@ -294,6 +314,7 @@ export class ViewMotion extends BaseViewModel {
/** /**
* 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 {
@ -304,6 +325,7 @@ export class ViewMotion extends BaseViewModel {
/** /**
* Update routine for the agenda Item * Update routine for the agenda Item
*
* @param item potentially the changed agenda Item. Needs manual verification * @param item potentially the changed agenda Item. Needs manual verification
*/ */
public updateItem(item: Item): void { public updateItem(item: Item): void {
@ -314,6 +336,7 @@ export class ViewMotion extends BaseViewModel {
/** /**
* Update routine for the motion block * Update routine for the motion block
*
* @param block potentially the changed motion block. Needs manual verification * @param block potentially the changed motion block. Needs manual verification
*/ */
public updateMotionBlock(block: MotionBlock): void { public updateMotionBlock(block: MotionBlock): void {
@ -323,7 +346,8 @@ export class ViewMotion extends BaseViewModel {
} }
/** /**
* Update routine for the agenda Item * Update routine for supporters and submitters
*
* @param update potentially the changed agenda Item. Needs manual verification * @param update potentially the changed agenda Item. Needs manual verification
*/ */
public updateUser(update: User): void { public updateUser(update: User): void {
@ -339,10 +363,28 @@ export class ViewMotion extends BaseViewModel {
} }
} }
/**
* Update routine for attachments
*
* @param update
*/
public updateAttachments(update: Mediafile): void {
if (this.motion) {
if (this.attachments_id && this.attachments_id.includes(update.id)) {
const attachmentIndex = this.attachments.findIndex(mediafile => mediafile.id === update.id);
this.attachments[attachmentIndex] = update as Mediafile;
}
}
}
public hasSupporters(): boolean { public hasSupporters(): boolean {
return !!(this.supporters && this.supporters.length > 0); return !!(this.supporters && this.supporters.length > 0);
} }
public hasAttachments(): boolean {
return !!(this.attachments && this.attachments.length > 0);
}
public isStatuteAmendment(): boolean { public isStatuteAmendment(): boolean {
return !!this.statute_paragraph_id; return !!this.statute_paragraph_id;
} }
@ -372,7 +414,10 @@ export class ViewMotion extends BaseViewModel {
this._submitters, this._submitters,
this._supporters, this._supporters,
this._workflow, this._workflow,
this._state this._state,
this._item,
this._block,
this._attachments
); );
} }
} }

View File

@ -12,7 +12,6 @@ import { ViewMotion } from '../models/view-motion';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { MotionRepositoryService } from './motion-repository.service'; import { MotionRepositoryService } from './motion-repository.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ConfigService } from 'app/core/services/config.service';
/** /**
* Repository service for motion blocks * Repository service for motion blocks
@ -28,14 +27,12 @@ export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock
* @param mapperService Mapping collection strings to classes * @param mapperService Mapping collection strings to classes
* @param dataSend Send models to the server * @param dataSend Send models to the server
* @param motionRepo Accessing the motion repository * @param motionRepo Accessing the motion repository
* @param config To access config variables
*/ */
public constructor( public constructor(
DS: DataStoreService, DS: DataStoreService,
mapperService: CollectionStringModelMapperService, mapperService: CollectionStringModelMapperService,
private dataSend: DataSendService, private dataSend: DataSendService,
private motionRepo: MotionRepositoryService, private motionRepo: MotionRepositoryService,
private config: ConfigService
) { ) {
super(DS, mapperService, MotionBlock); super(DS, mapperService, MotionBlock);
} }
@ -103,15 +100,6 @@ export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock
return this.DS.filter(Motion, motion => motion.motion_block_id === block.id).length; return this.DS.filter(Motion, motion => motion.motion_block_id === block.id).length;
} }
/**
* Get agenda visibility from the config
*
* @return An observable to the default agenda visibility
*/
public getDefaultAgendaVisibility(): Observable<number> {
return this.config.get('agenda_new_items_default_visibility').pipe(map(key => +key));
}
/** /**
* Observe the motion repository and return the motions belonging to the given * Observe the motion repository and return the motions belonging to the given
* block as observable * block as observable

View File

@ -27,6 +27,7 @@ import { TreeService } from 'app/core/services/tree.service';
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph'; import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
import { CreateMotion } from '../models/create-motion'; import { CreateMotion } from '../models/create-motion';
import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
/** /**
* Repository Services for motions (and potentially categories) * Repository Services for motions (and potentially categories)
@ -64,7 +65,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
private readonly diff: DiffService, private readonly diff: DiffService,
private treeService: TreeService private treeService: TreeService
) { ) {
super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock]); super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock, Mediafile]);
} }
/** /**
@ -82,11 +83,12 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
const workflow = this.DS.get(Workflow, motion.workflow_id); const workflow = this.DS.get(Workflow, motion.workflow_id);
const item = this.DS.get(Item, motion.agenda_item_id); const item = this.DS.get(Item, motion.agenda_item_id);
const block = this.DS.get(MotionBlock, motion.motion_block_id); const block = this.DS.get(MotionBlock, motion.motion_block_id);
const attachments = this.DS.getMany(Mediafile, motion.attachments_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); return new ViewMotion(motion, category, submitters, supporters, workflow, state, item, block, attachments);
} }
/** /**