From 8d77c0495bd1f094a0c6b09300070db344a36cc5 Mon Sep 17 00:00:00 2001 From: GabrielMeyer Date: Tue, 29 Oct 2019 14:00:52 +0100 Subject: [PATCH] Initial polling --- .../agenda-content-object-form.component.html | 19 ++- .../attachment-control.component.html | 19 ++- .../attachment-control.component.ts | 88 +++++----- .../extension-field.component.html | 16 +- .../media-upload-content.component.html | 38 +++-- .../media-upload-content.component.ts | 5 +- .../search-value-selector.component.html | 29 ++-- .../search-value-selector.component.spec.ts | 8 +- .../search-value-selector.component.ts | 142 ++++++++------- .../shared/models/base/base-form-control.ts | 161 ++++++++++++++++++ .../app/shared/models/motions/motion-poll.ts | 16 +- .../src/app/shared/models/poll/base-poll.ts | 61 +++++++ .../list-of-speakers.component.html | 40 +++-- .../assignment-detail.component.html | 52 +++--- .../history-list/history-list.component.html | 19 ++- .../mediafile-list.component.html | 51 +++--- .../site/motions/models/view-motion-poll.ts | 9 +- ...motion-comment-section-list.component.html | 30 ++-- .../manage-submitters.component.html | 15 +- .../motion-detail.component.html | 69 ++++---- .../motion-detail.component.spec.ts | 4 +- .../motion-detail/motion-detail.component.ts | 4 +- .../motion-poll-preview.component.html | 56 ++++++ .../motion-poll-preview.component.scss | 7 + .../motion-poll-preview.component.spec.ts | 27 +++ .../motion-poll-preview.component.ts | 56 ++++++ .../motion-detail/motion-detail.module.ts | 2 + .../motion-poll-detail.component.html | 92 ++++++++++ .../motion-poll-detail.component.scss | 3 + .../motion-poll-detail.component.spec.ts | 27 +++ .../motion-poll-detail.component.ts | 152 +++++++++++++++++ .../motion-poll-list.component.html | 36 ++++ .../motion-poll-list.component.scss | 0 .../motion-poll-list.component.spec.ts | 27 +++ .../motion-poll-list.component.ts | 45 +++++ .../motion-poll/motion-poll-routing.module.ts | 17 ++ .../modules/motion-poll/motion-poll.module.ts | 13 ++ .../site/motions/motions-routing.module.ts | 5 + .../topic-detail/topic-detail.component.html | 17 +- .../user-detail/user-detail.component.html | 46 ++--- .../styles/global-components-style.scss | 9 + 41 files changed, 1164 insertions(+), 368 deletions(-) create mode 100644 client/src/app/shared/models/base/base-form-control.ts create mode 100644 client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.html create mode 100644 client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.scss create mode 100644 client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.spec.ts create mode 100644 client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.ts create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.html create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.spec.ts create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.ts create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.html create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.scss create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.spec.ts create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.ts create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll-routing.module.ts create mode 100644 client/src/app/site/motions/modules/motion-poll/motion-poll.module.ts diff --git a/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.html b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.html index d0a41560d..e6f7ae75d 100644 --- a/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.html +++ b/client/src/app/shared/components/agenda-content-object-form/agenda-content-object-form.component.html @@ -21,15 +21,16 @@ -
- +
+ + +
diff --git a/client/src/app/shared/components/attachment-control/attachment-control.component.html b/client/src/app/shared/components/attachment-control/attachment-control.component.html index 65b22b0eb..609beaabf 100644 --- a/client/src/app/shared/components/attachment-control/attachment-control.component.html +++ b/client/src/app/shared/components/attachment-control/attachment-control.component.html @@ -1,12 +1,13 @@ -
- +
+ + + diff --git a/client/src/app/shared/components/attachment-control/attachment-control.component.ts b/client/src/app/shared/components/attachment-control/attachment-control.component.ts index 66a6e4be2..2ea306771 100644 --- a/client/src/app/shared/components/attachment-control/attachment-control.component.ts +++ b/client/src/app/shared/components/attachment-control/attachment-control.component.ts @@ -1,44 +1,68 @@ -import { Component, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core'; -import { ControlValueAccessor, FormControl } from '@angular/forms'; -import { MatDialog } from '@angular/material'; +import { FocusMonitor } from '@angular/cdk/a11y'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + OnInit, + Optional, + Output, + Self, + TemplateRef +} from '@angular/core'; +import { FormBuilder, NgControl } from '@angular/forms'; +import { MatDialog, MatFormFieldControl } from '@angular/material'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service'; +import { BaseFormControlComponent } from 'app/shared/models/base/base-form-control'; import { mediumDialogSettings } from 'app/shared/utils/dialog-settings'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; @Component({ selector: 'os-attachment-control', templateUrl: './attachment-control.component.html', - styleUrls: ['./attachment-control.component.scss'] + styleUrls: ['./attachment-control.component.scss'], + providers: [{ provide: MatFormFieldControl, useExisting: AttachmentControlComponent }], + changeDetection: ChangeDetectionStrategy.OnPush }) -export class AttachmentControlComponent implements OnInit, ControlValueAccessor { +export class AttachmentControlComponent extends BaseFormControlComponent implements OnInit { /** * Output for an error handler */ @Output() public errorHandler: EventEmitter = new EventEmitter(); - /** - * The form-control name to access the value for the form-control - */ - @Input() - public controlName: FormControl; - /** * The file list that is necessary for the `SearchValueSelector` */ public mediaFileList: Observable; + public get empty(): boolean { + return !this.contentForm.value.length; + } + public get controlType(): string { + return 'attachment-control'; + } + /** * Default constructor * * @param dialogService Reference to the `MatDialog` * @param mediaService Reference for the `MediaFileRepositoryService` */ - public constructor(private dialogService: MatDialog, private mediaService: MediafileRepositoryService) {} + public constructor( + fb: FormBuilder, + fm: FocusMonitor, + element: ElementRef, + @Optional() @Self() public ngControl: NgControl, + private dialogService: MatDialog, + private mediaService: MediafileRepositoryService + ) { + super(fb, fm, element, ngControl); + } /** * On init method @@ -64,11 +88,9 @@ export class AttachmentControlComponent implements OnInit, ControlValueAccessor * @param fileIDs a list with the ids of the uploaded files */ public uploadSuccess(fileIDs: number[]): void { - if (this.controlName) { - const newValues = [...this.controlName.value, ...fileIDs]; - this.controlName.setValue(newValues); - this.dialogService.closeAll(); - } + const newValues = [...this.contentForm.value, ...fileIDs]; + this.updateForm(newValues); + this.dialogService.closeAll(); } /** @@ -80,29 +102,13 @@ export class AttachmentControlComponent implements OnInit, ControlValueAccessor this.errorHandler.emit(error); } - /** - * Function to write a new value to the form. - * Satisfy the interface. - * - * @param value The new value for this form. - */ - public writeValue(value: any): void { - if (value && this.controlName) { - this.controlName.setValue(value); - } + public onContainerClick(event: MouseEvent): void { + // TODO: implement + } + protected initializeForm(): void { + this.contentForm = this.fb.control([]); + } + protected updateForm(value: ViewMediafile[]): void { + this.contentForm.setValue(value); } - - /** - * Function executed when the control's value changed. - * - * @param fn the function that is executed. - */ - public registerOnChange(fn: any): void {} - - /** - * To satisfy the interface - * - * @param fn the registered callback function for onBlur-events. - */ - public registerOnTouched(fn: any): void {} } diff --git a/client/src/app/shared/components/extension-field/extension-field.component.html b/client/src/app/shared/components/extension-field/extension-field.component.html index 1c8850c5b..3e98933f8 100644 --- a/client/src/app/shared/components/extension-field/extension-field.component.html +++ b/client/src/app/shared/components/extension-field/extension-field.component.html @@ -38,14 +38,14 @@ (keydown)="keyDownFunction($event)" /> - + + + diff --git a/client/src/app/shared/components/media-upload-content/media-upload-content.component.html b/client/src/app/shared/components/media-upload-content/media-upload-content.component.html index 4cc4a808e..3b05c2a24 100644 --- a/client/src/app/shared/components/media-upload-content/media-upload-content.component.html +++ b/client/src/app/shared/components/media-upload-content/media-upload-content.component.html @@ -13,16 +13,17 @@
-
- +
+ + +
@@ -69,14 +70,15 @@ Access groups - - + + + + diff --git a/client/src/app/shared/components/media-upload-content/media-upload-content.component.ts b/client/src/app/shared/components/media-upload-content/media-upload-content.component.ts index 71beedb6b..3638de4e9 100644 --- a/client/src/app/shared/components/media-upload-content/media-upload-content.component.ts +++ b/client/src/app/shared/components/media-upload-content/media-upload-content.component.ts @@ -90,7 +90,8 @@ export class MediaUploadContentComponent implements OnInit { public get selectedDirectoryId(): number | null { if (this.showDirectorySelector) { - return this.directorySelectionForm.controls.parent_id.value; + const parent = this.directorySelectionForm.controls.parent_id; + return !parent.value || typeof parent.value !== 'number' ? null : parent.value; } else { return this.directoryId; } @@ -110,7 +111,7 @@ export class MediaUploadContentComponent implements OnInit { this.directoryBehaviorSubject = this.repo.getDirectoryBehaviorSubject(); this.groupsBehaviorSubject = this.groupRepo.getViewModelListBehaviorSubject(); this.directorySelectionForm = this.formBuilder.group({ - parent_id: [] + parent_id: null }); } diff --git a/client/src/app/shared/components/search-value-selector/search-value-selector.component.html b/client/src/app/shared/components/search-value-selector/search-value-selector.component.html index e3f019d3c..e3bb6f2cb 100644 --- a/client/src/app/shared/components/search-value-selector/search-value-selector.component.html +++ b/client/src/app/shared/components/search-value-selector/search-value-selector.component.html @@ -1,19 +1,12 @@ - - - -
- - {{ noneTitle | translate }} - - -
- - {{ selectedItem.getTitle() | translate }} + + + + + {{ noneTitle | translate }} - -
+ + + + {{ selectedItem.getTitle() | translate }} + + diff --git a/client/src/app/shared/components/search-value-selector/search-value-selector.component.spec.ts b/client/src/app/shared/components/search-value-selector/search-value-selector.component.spec.ts index ac15cebd3..c242cf735 100644 --- a/client/src/app/shared/components/search-value-selector/search-value-selector.component.spec.ts +++ b/client/src/app/shared/components/search-value-selector/search-value-selector.component.spec.ts @@ -1,6 +1,6 @@ import { Component, ViewChild } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormBuilder, FormControl } from '@angular/forms'; +import { FormBuilder } from '@angular/forms'; import { BehaviorSubject } from 'rxjs'; @@ -43,10 +43,8 @@ describe('SearchValueSelectorComponent', () => { hostComponent.searchValueSelectorComponent.inputListValues = subject; const formBuilder: FormBuilder = TestBed.get(FormBuilder); - const formGroup = formBuilder.group({ - testArray: [] - }); - hostComponent.searchValueSelectorComponent.formControl = formGroup.get('testArray'); + const formControl = formBuilder.control([]); + hostComponent.searchValueSelectorComponent.contentForm = formControl; hostFixture.detectChanges(); expect(hostComponent.searchValueSelectorComponent).toBeTruthy(); diff --git a/client/src/app/shared/components/search-value-selector/search-value-selector.component.ts b/client/src/app/shared/components/search-value-selector/search-value-selector.component.ts index d01428439..7b5a046a9 100644 --- a/client/src/app/shared/components/search-value-selector/search-value-selector.component.ts +++ b/client/src/app/shared/components/search-value-selector/search-value-selector.component.ts @@ -1,31 +1,38 @@ -import { ChangeDetectionStrategy, Component, Input, OnDestroy, ViewChild } from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { MatSelect } from '@angular/material'; +import { FocusMonitor } from '@angular/cdk/a11y'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + Optional, + Self +} from '@angular/core'; +import { FormBuilder, FormControl, NgControl } from '@angular/forms'; +import { MatFormFieldControl } from '@angular/material'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, Subscription } from 'rxjs'; +import { Observable } from 'rxjs'; import { auditTime } from 'rxjs/operators'; +import { BaseFormControlComponent } from 'app/shared/models/base/base-form-control'; import { Selectable } from '../selectable'; /** - * Reusable Searchable Value Selector + * Searchable Value Selector * - * Use `multiple="true"`, `[InputListValues]=myValues`,`[formControl]="myformcontrol"` and `placeholder={{listname}}` to pass the Values and Listname + * Use `multiple="true"`, `[inputListValues]=myValues`,`formControlName="myformcontrol"` and `placeholder={{listname}}` to pass the Values and Listname * * ## Examples: * * ### Usage of the selector: * - * ngDefaultControl: https://stackoverflow.com/a/39053470 - * * ```html * + * [inputListValues]="myListValues" + * formControlName="myformcontrol"> * * ``` * @@ -35,24 +42,10 @@ import { Selectable } from '../selectable'; selector: 'os-search-value-selector', templateUrl: './search-value-selector.component.html', styleUrls: ['./search-value-selector.component.scss'], + providers: [{ provide: MatFormFieldControl, useExisting: SearchValueSelectorComponent }], changeDetection: ChangeDetectionStrategy.OnPush }) -export class SearchValueSelectorComponent implements OnDestroy { - /** - * Saves the current subscription to _inputListSubject. - */ - private _inputListSubscription: Subscription = null; - - /** - * Value of the search input - */ - private searchValue = ''; - - /** - * All items - */ - private selectableItems: Selectable[]; - +export class SearchValueSelectorComponent extends BaseFormControlComponent { /** * Decide if this should be a single or multi-select-field */ @@ -83,55 +76,41 @@ export class SearchValueSelectorComponent implements OnDestroy { if (!value) { return; } - - if (Array.isArray(value)) { - this.selectableItems = value; - } else { - // unsubscribe to old subscription. - if (this._inputListSubscription) { - this._inputListSubscription.unsubscribe(); - } - this._inputListSubscription = value.pipe(auditTime(10)).subscribe(items => { + this.subscriptions.push( + value.pipe(auditTime(10)).subscribe(items => { this.selectableItems = items; - if (this.formControl) { - !!items && items.length > 0 - ? this.formControl.enable({ emitEvent: false }) - : this.formControl.disable({ emitEvent: false }); + if (this.contentForm) { + this.disabled = !items || (!!items && !items.length); } - }); - } + }) + ); } - /** - * Placeholder of the List - */ - @Input() - public listname: string; + public searchValue: FormControl; + + public get empty(): boolean { + return Array.isArray(this.contentForm.value) ? !this.contentForm.value.length : !this.contentForm.value; + } + + public controlType = 'search-value-selector'; /** - * Name of the Form + * All items */ - @Input() - public formControl: FormControl; - - /** - * The MultiSelect Component - */ - @ViewChild('thisSelector', { static: true }) - public thisSelector: MatSelect; + private selectableItems: Selectable[]; /** * Empty constructor */ - public constructor(protected translate: TranslateService) {} - - /** - * Unsubscribe on destroing. - */ - public ngOnDestroy(): void { - if (this._inputListSubscription) { - this._inputListSubscription.unsubscribe(); - } + public constructor( + protected translate: TranslateService, + cd: ChangeDetectorRef, + fb: FormBuilder, + @Optional() @Self() public ngControl: NgControl, + fm: FocusMonitor, + element: ElementRef + ) { + super(fb, fm, element, ngControl); } /** @@ -141,29 +120,42 @@ export class SearchValueSelectorComponent implements OnDestroy { */ public getFilteredItems(): Selectable[] { if (this.selectableItems) { + const searchValue: string = this.searchValue.value.toLowerCase(); return this.selectableItems.filter(item => { const idString = '' + item.id; const foundId = idString .trim() .toLowerCase() - .indexOf(this.searchValue) !== -1; + .indexOf(searchValue) !== -1; if (foundId) { return true; } - const searchableString = this.translate.instant(item.getTitle()).toLowerCase(); - return searchableString.indexOf(this.searchValue) > -1; + + return ( + item + .toString() + .toLowerCase() + .indexOf(searchValue) > -1 + ); }); } } - /** - * Function to set the search value. - * - * @param searchValue the new value the user is searching for. - */ - public onSearch(searchValue: string): void { - this.searchValue = searchValue.toLowerCase(); + public onContainerClick(event: MouseEvent): void { + if ((event.target as Element).tagName.toLowerCase() !== 'select') { + // this.element.nativeElement.querySelector('select').focus(); + } + } + + protected initializeForm(): void { + this.contentForm = this.fb.control([]); + this.searchValue = this.fb.control(''); + } + + protected updateForm(value: Selectable[] | null): void { + const nextValue = value; + this.contentForm.setValue(nextValue); } } diff --git a/client/src/app/shared/models/base/base-form-control.ts b/client/src/app/shared/models/base/base-form-control.ts new file mode 100644 index 000000000..18624e980 --- /dev/null +++ b/client/src/app/shared/models/base/base-form-control.ts @@ -0,0 +1,161 @@ +import { FocusMonitor } from '@angular/cdk/a11y'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ElementRef, HostBinding, Input, OnDestroy, Optional, Self } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NgControl } from '@angular/forms'; +import { MatFormFieldControl } from '@angular/material'; + +import { Subject, Subscription } from 'rxjs'; + +/** + * Abstract class to implement some simple logic and provide the subclass as a controllable form-control in `MatFormField`. + * + * Please remember to prepare the `providers` in the `@Component`-decorator. Something like: + * + * ```ts + * @Component({ + * selector: ..., + * templateUrl: ..., + * styleUrls: [...], + * providers: [{ provide: MatFormFieldControl, useExisting: }] + * }) + * ``` + */ +export abstract class BaseFormControlComponent extends MatFormFieldControl + implements OnDestroy, ControlValueAccessor { + public static nextId = 0; + + @HostBinding() public id = `base-form-control-${BaseFormControlComponent.nextId++}`; + + @HostBinding('class.floating') public get shouldLabelFloat(): boolean { + return this.focused || !this.empty; + } + + @HostBinding('attr.aria-describedby') public describedBy = ''; + + @Input() + public set value(value: T | null) { + this.updateForm(value); + this.stateChanges.next(); + } + + public get value(): T | null { + return this.contentForm.value || null; + } + + @Input() + public set placeholder(placeholder: string) { + this._placeholder = placeholder; + this.stateChanges.next(); + } + + public get placeholder(): string { + return this._placeholder; + } + + @Input() + public set required(required: boolean) { + this._required = coerceBooleanProperty(required); + this.stateChanges.next(); + } + + public get required(): boolean { + return this._required; + } + + @Input() + public set disabled(disable: boolean) { + this._disabled = coerceBooleanProperty(disable); + this._disabled ? this.contentForm.disable() : this.contentForm.enable(); + this.stateChanges.next(); + } + + public get disabled(): boolean { + return this._disabled; + } + + public abstract get empty(): boolean; + + public abstract get controlType(): string; + + public contentForm: FormControl | FormGroup; + + public stateChanges = new Subject(); + + public errorState = false; + + public focused = false; + + private _placeholder: string; + + private _required = false; + + private _disabled = false; + + protected subscriptions: Subscription[] = []; + + public constructor( + protected fb: FormBuilder, + protected fm: FocusMonitor, + protected element: ElementRef, + @Optional() @Self() public ngControl: NgControl + ) { + super(); + + this.initializeForm(); + + if (this.ngControl !== null) { + this.ngControl.valueAccessor = this; + } + + this.subscriptions.push( + fm.monitor(element.nativeElement, true).subscribe(origin => { + this.focused = !!origin; + this.stateChanges.next(); + }), + this.contentForm.valueChanges.subscribe(nextValue => this.push(nextValue)) + ); + } + + public ngOnDestroy(): void { + for (const subscription of this.subscriptions) { + subscription.unsubscribe(); + } + this.subscriptions = []; + + this.fm.stopMonitoring(this.element.nativeElement); + + this.stateChanges.complete(); + } + + public writeValue(value: T): void { + this.value = value; + } + public registerOnChange(fn: any): void { + this._onChange = fn; + } + public registerOnTouched(fn: any): void { + this._onTouched = fn; + } + public setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + public setDescribedByIds(ids: string[]): void { + this.describedBy = ids.join(' '); + } + + public abstract onContainerClick(event: MouseEvent): void; + + protected _onChange = (value: T) => {}; + + protected _onTouched = (value: T) => {}; + + protected abstract initializeForm(): void; + + protected abstract updateForm(value: T | null): void; + + protected push(value: T): void { + this._onChange(value); + this._onTouched(value); + } +} diff --git a/client/src/app/shared/models/motions/motion-poll.ts b/client/src/app/shared/models/motions/motion-poll.ts index 01dd6287e..f9991450c 100644 --- a/client/src/app/shared/models/motions/motion-poll.ts +++ b/client/src/app/shared/models/motions/motion-poll.ts @@ -1,14 +1,18 @@ import { BasePoll, BasePollWithoutNestedModels } from '../poll/base-poll'; import { MotionOption } from './motion-option'; -export enum MotionPollmethods { - 'YN' = 'YN', - 'YNA' = 'YNA' +export enum MotionPollMethods { + YN = 'YN', + YNA = 'YNA' } +export const MotionPollMethodsVerbose = { + YN: 'Yes/No', + YNA: 'Yes/No/Abstain' +}; export interface MotionPollWithoutNestedModels extends BasePollWithoutNestedModels { motion_id: number; - pollmethod: MotionPollmethods; + pollmethod: MotionPollMethods; } /** @@ -22,5 +26,9 @@ export class MotionPoll extends BasePoll { public constructor(input?: any) { super(MotionPoll.COLLECTIONSTRING, input); } + + public get pollmethodVerbose(): string { + return MotionPollMethodsVerbose[this.pollmethod]; + } } export interface MotionPoll extends MotionPollWithoutNestedModels {} diff --git a/client/src/app/shared/models/poll/base-poll.ts b/client/src/app/shared/models/poll/base-poll.ts index 1fc1fe6fe..f5cb70763 100644 --- a/client/src/app/shared/models/poll/base-poll.ts +++ b/client/src/app/shared/models/poll/base-poll.ts @@ -8,12 +8,55 @@ export enum PollState { Published } +export const PollStateVerbose = { + 1: 'Created', + 2: 'Started', + 3: 'Finished', + 4: 'Published' +}; + export enum PollType { Analog = 'analog', Named = 'named', Pseudoanonymous = 'pseudoanonymous' } +export const PollTypeVerbose = { + analog: 'Analog', + named: 'Named', + pseudoanonymous: 'Pseudoanonymous' +}; + +export enum PercentBase { + YN = 'YN', + YNA = 'YNA', + Valid = 'valid', + Cast = 'cast', + Disabled = 'disabled' +} + +export const PercentBaseVerbose = { + YN: 'Yes/No', + YNA: 'Yes/No/Abstain', + valid: 'Valid votes', + cast: 'Casted votes', + disabled: 'Disabled' +}; + +export enum MajorityMethod { + Simple = 'simple', + TwoThirds = 'two_thirds', + ThreeQuarters = 'three_quarters', + Disabled = 'disabled' +} + +export const MajorityMethodVerbose = { + simple: 'Simple', + two_thirds: 'Two Thirds', + three_quarters: 'Three Quarters', + disabled: 'Disabled' +}; + export interface BasePollWithoutNestedModels { state: PollState; type: PollType; @@ -23,6 +66,8 @@ export interface BasePollWithoutNestedModels { votescast: number; groups_id: number[]; voted_id: number[]; + majority_method: MajorityMethod; + onehundred_percent_base: PercentBase; } export abstract class BasePoll> extends BaseDecimalModel { @@ -31,5 +76,21 @@ export abstract class BasePoll> extends BaseDecimal protected getDecimalFields(): (keyof BasePoll)[] { return ['votesvalid', 'votesinvalid', 'votescast']; } + + public get stateVerbose(): string { + return PollStateVerbose[this.state]; + } + + public get typeVerbose(): string { + return PollTypeVerbose[this.type]; + } + + public get majorityMethodVerbose(): string { + return MajorityMethodVerbose[this.majority_method]; + } + + public get percentBaseVerbose(): string { + return PercentBaseVerbose[this.onehundred_percent_base]; + } } export interface BasePoll> extends BasePollWithoutNestedModels {} diff --git a/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.html b/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.html index 81ac75268..aac27ad5c 100644 --- a/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.html +++ b/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.html @@ -1,4 +1,10 @@ - +

@@ -8,7 +14,15 @@

@@ -77,12 +91,7 @@
- + @@ -124,13 +133,14 @@
- + + +
diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html index 8ab276b7a..563566f66 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html @@ -145,10 +145,7 @@ *ngIf="assignment && assignment.polls && assignment.polls.length" > - + @@ -194,14 +191,15 @@ *ngIf="hasPerms('addOthers') && filteredCandidates && filteredCandidates.value.length > 0" [formGroup]="candidatesForm" > - + + +
@@ -238,11 +236,7 @@
- + {{ 'The title is required' | translate }}
@@ -256,22 +250,20 @@ > -
- -
+
+ + + - -
diff --git a/client/src/app/site/history/components/history-list/history-list.component.html b/client/src/app/site/history/components/history-list/history-list.component.html index f92137f09..897133b57 100644 --- a/client/src/app/site/history/components/history-list/history-list.component.html +++ b/client/src/app/site/history/components/history-list/history-list.component.html @@ -13,15 +13,16 @@
- - + + + +
@@ -301,16 +302,17 @@

Please enter a name for the new directory:

- + - + + +
@@ -328,16 +330,17 @@

Move into directory

-
+

Please select the directory:

- + + +
diff --git a/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.html b/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.html index 067edb1ac..516afe296 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.html +++ b/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.html @@ -34,13 +34,14 @@
- + + +

diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html index fb9411c67..14670936f 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html @@ -459,14 +459,15 @@

- -
+ + +
@@ -601,15 +602,16 @@
-
- -
+ + + + +
@@ -783,13 +785,14 @@
- + + +
@@ -805,8 +808,8 @@
@@ -818,25 +821,27 @@
- + + +
- + + +
diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.spec.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.spec.ts index 6575de8bd..f68d81bf8 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.spec.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.spec.ts @@ -7,6 +7,7 @@ import { MotionCommentsComponent } from '../motion-comments/motion-comments.comp import { MotionDetailDiffComponent } from '../motion-detail-diff/motion-detail-diff.component'; import { MotionDetailOriginalChangeRecommendationsComponent } from '../motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component'; import { MotionDetailComponent } from './motion-detail.component'; +import { MotionPollPreviewComponent } from '../motion-poll/motion-poll-preview/motion-poll-preview.component'; import { MotionPollComponent } from '../motion-poll/motion-poll.component'; import { PersonalNoteComponent } from '../personal-note/personal-note.component'; @@ -24,7 +25,8 @@ describe('MotionDetailComponent', () => { ManageSubmittersComponent, MotionPollComponent, MotionDetailOriginalChangeRecommendationsComponent, - MotionDetailDiffComponent + MotionDetailDiffComponent, + MotionPollPreviewComponent ] }).compileComponents(); })); diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts index 7c7962aef..9cb6c1f46 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts @@ -1382,9 +1382,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, * Handler for creating a poll */ public createPoll(): void { - // TODO - // this.repo.createPoll({}).catch(this.raiseError); - throw new Error('TODO'); + this.router.navigate(['motions', 'polls', 'new'], { queryParams: { parent: this.motion.id || null } }); } /** diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.html b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.html new file mode 100644 index 000000000..78f801c34 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.html @@ -0,0 +1,56 @@ + + {{ poll.title }} + +
+
{{ 'Current state' | translate }}: {{ poll.stateVerbose }}
+
{{ 'Groups' | translate }}: {{ poll.groups }}
+
{{ 'Method' | translate }}: {{ poll.pollmethodVerbose }}
+
{{ 'Type' | translate }}: {{ poll.typeVerbose }}
+
+ + +
+
+ + + +

Enter votes

+
+
+

+ + + + Required + + +

+ + +

Statute paragraph

+ +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.scss b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.scss new file mode 100644 index 000000000..7eb491efc --- /dev/null +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.scss @@ -0,0 +1,7 @@ +.poll-content { + padding-bottom: 8px; +} + +.poll-footer { + text-align: end; +} diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.spec.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.spec.ts new file mode 100644 index 000000000..818e19927 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { E2EImportsModule } from 'e2e-imports.module'; + +import { MotionPollPreviewComponent } from './motion-poll-preview.component'; + +describe('MotionPollPreviewComponent', () => { + let component: MotionPollPreviewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [MotionPollPreviewComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MotionPollPreviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.ts new file mode 100644 index 000000000..01f9bc977 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll-preview/motion-poll-preview.component.ts @@ -0,0 +1,56 @@ +import { Component, Input } from '@angular/core'; +import { MatSnackBar } from '@angular/material'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; + +import { TranslateService } from '@ngx-translate/core'; + +import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; +import { PromptService } from 'app/core/ui-services/prompt.service'; +import { PollType } from 'app/shared/models/poll/base-poll'; +import { BaseViewComponent } from 'app/site/base/base-view'; +import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; + +@Component({ + selector: 'os-motion-poll-preview', + templateUrl: './motion-poll-preview.component.html', + styleUrls: ['./motion-poll-preview.component.scss'] +}) +export class MotionPollPreviewComponent extends BaseViewComponent { + @Input() + public poll: ViewMotionPoll; + + public pollTypes = PollType; + + public constructor( + title: Title, + protected translate: TranslateService, + matSnackbar: MatSnackBar, + private repo: MotionPollRepositoryService, + private promptDialog: PromptService, + private router: Router + ) { + super(title, translate, matSnackbar); + } + + public openPoll(): void { + this.router.navigate(['motions', 'polls', this.poll.id]); + } + + public editPoll(): void { + this.router.navigate(['motions', 'polls', this.poll.id], { queryParams: { edit: true } }); + } + + public async deletePoll(): Promise { + const title = 'Delete poll'; + const text = 'Do you really want to delete the selected poll?'; + + if (await this.promptDialog.open(title, text)) { + await this.repo.delete(this.poll); + } + } + + public enterAnalogVotes(): void { + throw new Error('TODO'); + } +} diff --git a/client/src/app/site/motions/modules/motion-detail/motion-detail.module.ts b/client/src/app/site/motions/modules/motion-detail/motion-detail.module.ts index 518f37b1e..05cb39f66 100644 --- a/client/src/app/site/motions/modules/motion-detail/motion-detail.module.ts +++ b/client/src/app/site/motions/modules/motion-detail/motion-detail.module.ts @@ -11,6 +11,7 @@ import { MotionDetailOriginalChangeRecommendationsComponent } from './components import { MotionDetailRoutingModule } from './motion-detail-routing.module'; import { MotionDetailComponent } from './components/motion-detail/motion-detail.component'; import { MotionPollDialogComponent } from './components/motion-poll/motion-poll-dialog.component'; +import { MotionPollPreviewComponent } from './components/motion-poll/motion-poll-preview/motion-poll-preview.component'; import { MotionPollComponent } from './components/motion-poll/motion-poll.component'; import { MotionTitleChangeRecommendationDialogComponent } from './components/motion-title-change-recommendation-dialog/motion-title-change-recommendation-dialog.component'; import { PersonalNoteComponent } from './components/personal-note/personal-note.component'; @@ -24,6 +25,7 @@ import { PersonalNoteComponent } from './components/personal-note/personal-note. PersonalNoteComponent, ManageSubmittersComponent, MotionPollComponent, + MotionPollPreviewComponent, MotionPollDialogComponent, MotionDetailDiffComponent, MotionDetailOriginalChangeRecommendationsComponent, diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.html b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.html new file mode 100644 index 000000000..c54d59d88 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.html @@ -0,0 +1,92 @@ + +
+

New vote

+

{{ poll.title }}

+
+
+ + + + + + + + +

{{ poll.title }}

+ +
+
{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}
+
+ {{ 'Groups' | translate }}: + {{ group.getTitle() | translate }} +
+
{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }}
+
{{ 'Poll method' | translate }}: {{ poll.pollmethodVerbose | translate }}
+
{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}
+
{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}
+
+
+
+ + +
+ + + A title is required + + + + {{ + option.value | translate + }} + + This field is required + + + + + + + {{ + option.value + }} + + This field is required + + + + {{ + option.value | translate + }} + + + + + {{ + option.value | translate + }} + + +
+
diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss new file mode 100644 index 000000000..621e890fd --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss @@ -0,0 +1,3 @@ +.poll-content { + padding-top: 10px; +} diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.spec.ts b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.spec.ts new file mode 100644 index 000000000..29e8cf621 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { E2EImportsModule } from 'e2e-imports.module'; + +import { MotionPollDetailComponent } from './motion-poll-detail.component'; + +describe('MotionPollDetailComponent', () => { + let component: MotionPollDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [MotionPollDetailComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MotionPollDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.ts b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.ts new file mode 100644 index 000000000..e44c359c3 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.ts @@ -0,0 +1,152 @@ +import { Location } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material'; +import { Title } from '@angular/platform-browser'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; + +import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; +import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; +import { MotionPoll, MotionPollMethodsVerbose } from 'app/shared/models/motions/motion-poll'; +import { + MajorityMethodVerbose, + PercentBaseVerbose, + PollStateVerbose, + PollTypeVerbose +} from 'app/shared/models/poll/base-poll'; +import { BaseViewComponent } from 'app/site/base/base-view'; +import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; +import { ViewGroup } from 'app/site/users/models/view-group'; + +@Component({ + selector: 'os-motion-poll-detail', + templateUrl: './motion-poll-detail.component.html', + styleUrls: ['./motion-poll-detail.component.scss'] +}) +export class MotionPollDetailComponent extends BaseViewComponent implements OnInit { + private pollId: number; + + public pollStates = PollStateVerbose; + public pollMethods = MotionPollMethodsVerbose; + public pollTypes = PollTypeVerbose; + public percentBases = PercentBaseVerbose; + public majorityMethods = MajorityMethodVerbose; + + public userGroups: ViewGroup[] = []; + + public groupObservable: Observable = null; + + public isNewPoll = false; + + public poll: ViewMotionPoll = null; + + public motionId: number; + + public isEditingPoll = false; + + public contentForm: FormGroup; + + public constructor( + title: Title, + protected translate: TranslateService, + matSnackbar: MatSnackBar, + private repo: MotionPollRepositoryService, + private route: ActivatedRoute, + private router: Router, + private fb: FormBuilder, + private groupRepo: GroupRepositoryService, + private location: Location + ) { + super(title, translate, matSnackbar); + } + + public ngOnInit(): void { + this.findComponentById(); + this.createPoll(); + + this.groupObservable = this.groupRepo.getViewModelListObservable(); + this.subscriptions.push( + this.groupRepo.getViewModelListObservable().subscribe(groups => (this.userGroups = groups)) + ); + } + + public savePoll(): void { + const pollValues = this.contentForm.value; + const poll: MotionPoll = this.isNewPoll ? new MotionPoll() : this.poll.poll; + Object.keys(pollValues).forEach(key => (poll[key] = pollValues[key])); + if (this.isNewPoll) { + poll.motion_id = this.motionId; + this.repo.create(poll).then(success => { + if (success && success.id) { + this.pollId = success.id; + this.router.navigate(['motions', 'polls', this.pollId]); + } + }, this.raiseError); + } else { + this.repo.update(pollValues, this.poll).then(() => (this.isEditingPoll = false), this.raiseError); + } + } + + public editPoll(): void { + this.isEditingPoll = true; + } + + public backToView(): void { + if (this.pollId) { + this.isEditingPoll = false; + } else { + // TODO + this.location.back(); + } + } + + private findComponentById(): void { + const params = this.route.snapshot.params; + const queryParams = this.route.snapshot.queryParams; + if (params && params.id) { + this.pollId = +params.id; + this.subscriptions.push( + this.repo.getViewModelObservable(this.pollId).subscribe(poll => { + if (poll) { + this.poll = poll; + this.updateForm(); + } + }) + ); + } else { + this.isNewPoll = true; + this.isEditingPoll = true; + if (queryParams && queryParams.parent) { + this.motionId = +queryParams.parent; + } + } + if (queryParams && queryParams.edit) { + this.isEditingPoll = true; + } + } + + private createPoll(): void { + this.contentForm = this.fb.group({ + title: ['', Validators.required], + type: ['', Validators.required], + pollmethod: ['', Validators.required], + onehundred_percent_base: ['', Validators.required], + majority_method: ['', Validators.required], + groups_id: [[]] + }); + if (this.poll) { + this.updateForm(); + } + } + + private updateForm(): void { + if (this.contentForm) { + Object.keys(this.contentForm.controls).forEach(key => { + this.contentForm.get(key).setValue(this.poll[key]); + }); + } + } +} diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.html b/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.html new file mode 100644 index 000000000..d4f67dbb1 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.html @@ -0,0 +1,36 @@ + +
Motions poll list
+ + + +
+ + +
+ + {{ poll.title }} +
+
+ {{ poll.stateVerbose }} +
+
+ + + + + diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.scss b/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.spec.ts b/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.spec.ts new file mode 100644 index 000000000..819250015 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { E2EImportsModule } from 'e2e-imports.module'; + +import { MotionPollListComponent } from './motion-poll-list.component'; + +describe('MotionPollListComponent', () => { + let component: MotionPollListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [MotionPollListComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MotionPollListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.ts b/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.ts new file mode 100644 index 000000000..0f6cbefd8 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-list/motion-poll-list.component.ts @@ -0,0 +1,45 @@ +import { Component, OnInit } from '@angular/core'; +import { MatSnackBar } from '@angular/material'; +import { Title } from '@angular/platform-browser'; + +import { TranslateService } from '@ngx-translate/core'; +import { PblColumnDefinition } from '@pebula/ngrid'; + +import { StorageService } from 'app/core/core-services/storage.service'; +import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; +import { BaseListViewComponent } from 'app/site/base/base-list-view'; +import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; + +@Component({ + selector: 'os-motion-poll-list', + templateUrl: './motion-poll-list.component.html', + styleUrls: ['./motion-poll-list.component.scss'] +}) +export class MotionPollListComponent extends BaseListViewComponent implements OnInit { + public tableColumnDefinition: PblColumnDefinition[] = [ + { + prop: 'title', + width: 'auto' + }, + { + prop: 'state', + width: 'auto' + } + ]; + + public polls: ViewMotionPoll[] = []; + + public constructor( + title: Title, + protected translate: TranslateService, + matSnackbar: MatSnackBar, + storage: StorageService, + public repo: MotionPollRepositoryService + ) { + super(title, translate, matSnackbar, storage); + } + + public ngOnInit(): void { + this.subscriptions.push(this.repo.getViewModelListObservable().subscribe(polls => (this.polls = polls))); + } +} diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-routing.module.ts b/client/src/app/site/motions/modules/motion-poll/motion-poll-routing.module.ts new file mode 100644 index 000000000..120eac726 --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { MotionPollDetailComponent } from './motion-poll-detail/motion-poll-detail.component'; +import { MotionPollListComponent } from './motion-poll-list/motion-poll-list.component'; + +const routes: Routes = [ + { path: '', component: MotionPollListComponent, pathMatch: 'full' }, + { path: 'new', component: MotionPollDetailComponent }, + { path: ':id', component: MotionPollDetailComponent } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MotionPollRoutingModule {} diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll.module.ts b/client/src/app/site/motions/modules/motion-poll/motion-poll.module.ts new file mode 100644 index 000000000..48c88631f --- /dev/null +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { SharedModule } from 'app/shared/shared.module'; +import { MotionPollDetailComponent } from './motion-poll-detail/motion-poll-detail.component'; +import { MotionPollListComponent } from './motion-poll-list/motion-poll-list.component'; +import { MotionPollRoutingModule } from './motion-poll-routing.module'; + +@NgModule({ + declarations: [MotionPollDetailComponent, MotionPollListComponent], + imports: [CommonModule, SharedModule, MotionPollRoutingModule] +}) +export class MotionPollModule {} diff --git a/client/src/app/site/motions/motions-routing.module.ts b/client/src/app/site/motions/motions-routing.module.ts index 749328d72..a58a3f0f5 100644 --- a/client/src/app/site/motions/motions-routing.module.ts +++ b/client/src/app/site/motions/motions-routing.module.ts @@ -62,6 +62,11 @@ const routes: Routes = [ loadChildren: () => import('./modules/amendment-list/amendment-list.module').then(m => m.AmendmentListModule), data: { basePerm: 'motions.can_see' } }, + { + path: 'polls', + loadChildren: () => import('./modules/motion-poll/motion-poll.module').then(m => m.MotionPollModule), + data: { basePerm: 'motions.can_manage_polls' } + }, { path: ':id', loadChildren: () => import('./modules/motion-detail/motion-detail.module').then(m => m.MotionDetailModule), diff --git a/client/src/app/site/topics/components/topic-detail/topic-detail.component.html b/client/src/app/site/topics/components/topic-detail/topic-detail.component.html index 6722d1b2c..c1d9660ee 100644 --- a/client/src/app/site/topics/components/topic-detail/topic-detail.component.html +++ b/client/src/app/site/topics/components/topic-detail/topic-detail.component.html @@ -70,7 +70,7 @@ @@ -88,13 +88,14 @@
- + + +
diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.html b/client/src/app/site/users/components/user-detail/user-detail.component.html index 2d4642f5b..e1591b231 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.html +++ b/client/src/app/site/users/components/user-detail/user-detail.component.html @@ -75,22 +75,12 @@ - + - +
@@ -142,13 +132,14 @@
- + + +
@@ -184,23 +175,14 @@
- +
- + Only for internal notes.
@@ -245,7 +227,9 @@ {{ user.short_name }} check_box - account_balance + account_balance block diff --git a/client/src/assets/styles/global-components-style.scss b/client/src/assets/styles/global-components-style.scss index df1853054..cdeb70a7a 100644 --- a/client/src/assets/styles/global-components-style.scss +++ b/client/src/assets/styles/global-components-style.scss @@ -136,6 +136,15 @@ right: 0; } + .icon { + color: mat-color($foreground, icon); + } + + .small-icon { + @extend .icon; + font-size: 18px; + } + /** Custom themes for NGrid. Could be an own file if it gets more */ .pbl-ngrid-container { background: mat-color($background, card);