Initial polling

This commit is contained in:
GabrielMeyer 2019-10-29 14:00:52 +01:00 committed by FinnStutzenstein
parent 1b761d31c0
commit 8d77c0495b
41 changed files with 1164 additions and 368 deletions

View File

@ -21,15 +21,16 @@
</div>
<!-- Parent item -->
<div *ngIf="itemObserver.value.length > 0">
<os-search-value-selector
ngDefaultControl
[formControl]="form.get('agenda_parent_id')"
[multiple]="false"
[includeNone]="true"
listname="{{ 'Parent agenda item' | translate }}"
[inputListValues]="itemObserver"
></os-search-value-selector>
<div *ngIf="itemObserver.value.length > 0" [formGroup]="form">
<mat-form-field>
<os-search-value-selector
formControlName="agenda_parent_id"
[multiple]="false"
[includeNone]="true"
placeholder="{{ 'Parent agenda item' | translate }}"
[inputListValues]="itemObserver"
></os-search-value-selector>
</mat-form-field>
</div>
</ng-container>
</ng-container>

View File

@ -1,12 +1,13 @@
<div class="attachment-container" *ngIf="controlName">
<os-search-value-selector
class="selector"
ngDefaultControl
[multiple]="true"
listname="{{ 'Attachments' | translate }}"
[formControl]="controlName"
[inputListValues]="mediaFileList"
></os-search-value-selector>
<div class="attachment-container" *ngIf="contentForm">
<mat-form-field>
<os-search-value-selector
class="selector"
[multiple]="true"
placeholder="{{ 'Attachments' | translate }}"
[formControl]="contentForm"
[inputListValues]="mediaFileList"
></os-search-value-selector>
</mat-form-field>
<button type="button" mat-icon-button (click)="openUploadDialog(uploadDialog)" *osPerms="'mediafiles.can_manage'">
<mat-icon>cloud_upload</mat-icon>
</button>

View File

@ -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<ViewMediafile[]> implements OnInit {
/**
* Output for an error handler
*/
@Output()
public errorHandler: EventEmitter<string> = 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<ViewMediafile[]>;
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<HTMLElement>,
@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 {}
}

View File

@ -38,14 +38,14 @@
(keydown)="keyDownFunction($event)"
/>
</mat-form-field>
<os-search-value-selector
*ngIf="searchList"
ngDefaultControl
[formControl]="extensionFieldForm.get('list')"
[fullWidth]="true"
[inputListValues]="searchList"
[listname]="searchListLabel"
></os-search-value-selector>
<mat-form-field *ngIf="searchList">
<os-search-value-selector
formControlName="list"
[fullWidth]="true"
[inputListValues]="searchList"
[placeholder]="searchListLabel"
></os-search-value-selector>
</mat-form-field>
<button mat-button (click)="changeEditMode(true)">{{ 'Save' | translate }}</button>
<button mat-button (click)="changeEditMode()">{{ 'Cancel' | translate }}</button>

View File

@ -13,16 +13,17 @@
</div>
<!-- Directory selector, if no external directory is provided -->
<div *ngIf="showDirectorySelector">
<os-search-value-selector
ngDefaultControl
[formControl]="directorySelectionForm.get('parent_id')"
[multiple]="false"
[includeNone]="true"
[noneTitle]="'Base folder'"
listname="{{ 'Parent directory' | translate }}"
[inputListValues]="directoryBehaviorSubject"
></os-search-value-selector>
<div *ngIf="showDirectorySelector" [formGroup]="directorySelectionForm">
<mat-form-field>
<os-search-value-selector
formControlName="parent_id"
[multiple]="false"
[includeNone]="true"
[noneTitle]="'Base folder'"
placeholder="{{ 'Parent directory' | translate }}"
[inputListValues]="directoryBehaviorSubject"
></os-search-value-selector>
</mat-form-field>
</div>
<div>
@ -69,14 +70,15 @@
<!-- Access groups -->
<ng-container matColumnDef="access_groups">
<th mat-header-cell *matHeaderCellDef><span translate>Access groups</span></th>
<td mat-cell *matCellDef="let file">
<os-search-value-selector
ngDefaultControl
[formControl]="file.form.get('access_groups_id')"
[multiple]="true"
listname="{{ 'Access groups' | translate }}"
[inputListValues]="groupsBehaviorSubject"
></os-search-value-selector>
<td mat-cell *matCellDef="let file" [formGroup]="file.form">
<mat-form-field>
<os-search-value-selector
formControlName="access_groups_id"
[multiple]="true"
placeholder="{{ 'Access groups' | translate }}"
[inputListValues]="groupsBehaviorSubject"
></os-search-value-selector>
</mat-form-field>
</td>
</ng-container>

View File

@ -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
});
}

View File

@ -1,19 +1,12 @@
<mat-form-field [style.display]="fullWidth ? 'block' : 'inline-block'">
<mat-select
[formControl]="formControl"
placeholder="{{ listname | translate }}"
[multiple]="multiple"
#thisSelector
>
<ngx-mat-select-search ngModel (ngModelChange)="onSearch($event)"></ngx-mat-select-search>
<div *ngIf="!multiple && includeNone">
<mat-option [value]="null">
{{ noneTitle | translate }}
</mat-option>
<mat-divider></mat-divider>
</div>
<mat-option *ngFor="let selectedItem of getFilteredItems()" [value]="selectedItem.id">
{{ selectedItem.getTitle() | translate }}
<mat-select [formControl]="contentForm" [multiple]="multiple">
<ngx-mat-select-search [formControl]="searchValue"></ngx-mat-select-search>
<ng-container *ngIf="!multiple && includeNone">
<mat-option [value]="null">
{{ noneTitle | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-divider></mat-divider>
</ng-container>
<mat-option *ngFor="let selectedItem of getFilteredItems()" [value]="selectedItem.id">
{{ selectedItem.getTitle() | translate }}
</mat-option>
</mat-select>

View File

@ -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 = <FormControl>formGroup.get('testArray');
const formControl = formBuilder.control([]);
hostComponent.searchValueSelectorComponent.contentForm = formControl;
hostFixture.detectChanges();
expect(hostComponent.searchValueSelectorComponent).toBeTruthy();

View File

@ -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
* <os-search-value-selector
* ngDefaultControl
* [multiple]="true"
* placeholder="Placeholder"
* [InputListValues]="myListValues"
* [formControl]="myformcontrol">
* [inputListValues]="myListValues"
* formControlName="myformcontrol">
* </os-search-value-selector>
* ```
*
@ -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<Selectable[]> {
/**
* 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<HTMLElement>
) {
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);
}
}

View File

@ -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: <TheComponent>}]
* })
* ```
*/
export abstract class BaseFormControlComponent<T> extends MatFormFieldControl<T>
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<void>();
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<HTMLElement>,
@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);
}
}

View File

@ -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<MotionPoll, MotionOption> {
public constructor(input?: any) {
super(MotionPoll.COLLECTIONSTRING, input);
}
public get pollmethodVerbose(): string {
return MotionPollMethodsVerbose[this.pollmethod];
}
}
export interface MotionPoll extends MotionPollWithoutNestedModels {}

View File

@ -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<T, O extends BaseOption<any>> extends BaseDecimalModel<T> {
@ -31,5 +76,21 @@ export abstract class BasePoll<T, O extends BaseOption<any>> extends BaseDecimal
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
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<T, O extends BaseOption<any>> extends BasePollWithoutNestedModels {}

View File

@ -1,4 +1,10 @@
<os-head-bar [nav]="false" [goBack]="true" [editMode]="isSortMode" (cancelEditEvent)="onCancelSorting()" (saveEvent)="onSaveSorting()">
<os-head-bar
[nav]="false"
[goBack]="true"
[editMode]="isSortMode"
(cancelEditEvent)="onCancelSorting()"
(saveEvent)="onSaveSorting()"
>
<!-- Title -->
<div class="title-slot">
<h2>
@ -8,7 +14,15 @@
</h2>
</div>
<div class="menu-slot" *osPerms="['agenda.can_manage_list_of_speakers', 'core.can_manage_projector']">
<button type="button" mat-icon-button matTooltip="{{ 'Re-add last speaker' | translate }}" (click)="readdLastSpeaker()" [disabled]="!finishedSpeakers || !finishedSpeakers.length"><mat-icon>undo</mat-icon></button>
<button
type="button"
mat-icon-button
matTooltip="{{ 'Re-add last speaker' | translate }}"
(click)="readdLastSpeaker()"
[disabled]="!finishedSpeakers || !finishedSpeakers.length"
>
<mat-icon>undo</mat-icon>
</button>
<button type="button" mat-icon-button [matMenuTriggerFor]="speakerMenu"><mat-icon>more_vert</mat-icon></button>
</div>
</os-head-bar>
@ -77,12 +91,7 @@
<!-- Waiting speakers -->
<div class="waiting-list" *ngIf="speakers && speakers.length > 0">
<os-sorting-list
[input]="speakers"
[live]="!isSortMode"
[count]="true"
[enable]="opCanManage() && isSortMode"
>
<os-sorting-list [input]="speakers" [live]="!isSortMode" [count]="true" [enable]="opCanManage() && isSortMode">
<!-- implicit speaker references into the component using ng-template slot -->
<ng-template let-speaker>
<span *osPerms="'agenda.can_manage_list_of_speakers'">
@ -124,13 +133,14 @@
<!-- Search for speakers -->
<div *osPerms="'agenda.can_manage_list_of_speakers'">
<form *ngIf="filteredUsers && filteredUsers.value.length > 0" [formGroup]="addSpeakerForm">
<os-search-value-selector
class="search-users"
ngDefaultControl
[formControl]="addSpeakerForm.get('user_id')"
listname="{{ 'Select or search new speaker ...' | translate }}"
[inputListValues]="filteredUsers"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
class="search-users"
formControlName="user_id"
placeholder="{{ 'Select or search new speaker ...' | translate }}"
[inputListValues]="filteredUsers"
></os-search-value-selector>
</mat-form-field>
</form>
</div>

View File

@ -145,10 +145,7 @@
*ngIf="assignment && assignment.polls && assignment.polls.length"
>
<!-- TODO avoid animation/switching on update -->
<mat-tab
*ngFor="let poll of assignment.polls; let i = index; trackBy: trackByIndex"
[label]="poll.title"
>
<mat-tab *ngFor="let poll of assignment.polls; let i = index; trackBy: trackByIndex" [label]="poll.title">
<os-assignment-poll [assignment]="assignment" [poll]="poll"> </os-assignment-poll>
</mat-tab>
</mat-tab-group>
@ -194,14 +191,15 @@
*ngIf="hasPerms('addOthers') && filteredCandidates && filteredCandidates.value.length > 0"
[formGroup]="candidatesForm"
>
<os-search-value-selector
class="search-bar"
ngDefaultControl
[formControl]="candidatesForm.get('userId')"
[multiple]="false"
listname="{{ 'Select a new candidate' | translate }}"
[inputListValues]="filteredCandidates"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
class="search-bar"
formControlName="userId"
[multiple]="false"
placeholder="{{ 'Select a new candidate' | translate }}"
[inputListValues]="filteredCandidates"
></os-search-value-selector>
</mat-form-field>
</form>
</div>
@ -238,11 +236,7 @@
<div>
<!-- title -->
<mat-form-field class="full-width">
<input
matInput
placeholder="{{ 'Title' | translate }}"
formControlName="title"
/>
<input matInput placeholder="{{ 'Title' | translate }}" formControlName="title" />
<mat-error>{{ 'The title is required' | translate }}</mat-error>
</mat-form-field>
</div>
@ -256,22 +250,20 @@
></editor>
<!-- searchValueSelector: tags -->
<div class="content-field" *ngIf="tagsAvailable">
<os-search-value-selector
ngDefaultControl
[formControl]="assignmentForm.get('tags_id')"
[multiple]="true"
[includeNone]="true"
listname="{{ 'Tags' | translate }}"
[inputListValues]="tagsObserver"
></os-search-value-selector>
</div>
<div class="content-field" *ngIf="tagsAvailable" [formGroup]="assignmentForm">
<mat-form-field>
<os-search-value-selector
formControlName="tags_id"
[multiple]="true"
[includeNone]="true"
placeholder="{{ 'Tags' | translate }}"
[inputListValues]="tagsObserver"
></os-search-value-selector>
</mat-form-field>
<!-- Attachments -->
<div class="content-field">
<os-attachment-control
formControlName="attachments_id"
(errorHandler)="raiseError($event)"
[controlName]="assignmentForm.get('attachments_id')"
></os-attachment-control>
</div>

View File

@ -13,15 +13,16 @@
<div class="custom-table-header">
<div>
<span>
<os-search-value-selector
ngDefaultControl
[formControl]="modelSelectForm.get('model')"
[multiple]="false"
[includeNone]="false"
listname="{{ 'Motion' | translate }}"
[inputListValues]="collectionObserver"
></os-search-value-selector>
<span [formGroup]="modelSelectForm">
<mat-form-field>
<os-search-value-selector
formControlName="model"
[multiple]="false"
[includeNone]="false"
placeholder="{{ 'Motion' | translate }}"
[inputListValues]="collectionObserver"
></os-search-value-selector>
</mat-form-field>
</span>
<span class="spacer-left-20">
<button mat-button (click)="refresh()" *ngIf="currentModelId">

View File

@ -269,13 +269,14 @@
<mat-error *ngIf="fileEditForm.invalid" translate>Required</mat-error>
</mat-form-field>
<os-search-value-selector
ngDefaultControl
[formControl]="fileEditForm.get('access_groups_id')"
[multiple]="true"
listname="{{ 'Access groups' | translate }}"
[inputListValues]="groupsBehaviorSubject"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="access_groups_id"
[multiple]="true"
placeholder="{{ 'Access groups' | translate }}"
[inputListValues]="groupsBehaviorSubject"
></os-search-value-selector>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>
@ -301,16 +302,17 @@
<form class="edit-file-form" [formGroup]="newDirectoryForm">
<p translate>Please enter a name for the new directory:</p>
<mat-form-field>
<input matInput osAutofocus formControlName="title" required />
<input matInput osAutofocus formControlName="title" placeholder="{{ 'Title' | translate }}" required />
</mat-form-field>
<os-search-value-selector
ngDefaultControl
[formControl]="newDirectoryForm.get('access_groups_id')"
[multiple]="true"
listname="{{ 'Access groups' | translate }}"
[inputListValues]="groupsBehaviorSubject"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="access_groups_id"
[multiple]="true"
placeholder="{{ 'Access groups' | translate }}"
[inputListValues]="groupsBehaviorSubject"
></os-search-value-selector>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>
@ -328,16 +330,17 @@
<h1 mat-dialog-title>
<span translate>Move into directory</span>
</h1>
<div class="os-form-card-mobile" mat-dialog-content>
<div class="os-form-card-mobile" [formGroup]="moveForm" mat-dialog-content>
<p translate>Please select the directory:</p>
<os-search-value-selector
ngDefaultControl
[formControl]="moveForm.get('directory_id')"
[includeNone]="true"
[noneTitle]="'Base folder'"
listname="{{ 'Parent directory' | translate }}"
[inputListValues]="filteredDirectoryBehaviorSubject"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="directory_id"
[includeNone]="true"
[noneTitle]="'Base folder'"
placeholder="{{ 'Parent directory' | translate }}"
[inputListValues]="filteredDirectoryBehaviorSubject"
></os-search-value-selector>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button type="submit" mat-button color="primary" [mat-dialog-close]="true">

View File

@ -18,17 +18,16 @@ export class ViewMotionPoll extends BaseProjectableViewModel<MotionPoll> impleme
}
public getSlide(): ProjectorElementBuildDeskriptor {
/*return {
return {
getBasicProjectorElement: options => ({
name: Motion.COLLECTIONSTRING,
name: MotionPoll.COLLECTIONSTRING,
id: this.id,
getIdentifiers: () => ['name', 'id']
}),
slideOptions: [],
projectionDefaultName: 'motions',
projectionDefaultName: 'motion-poll',
getDialogTitle: this.getTitle
};*/
throw new Error('TODO');
};
}
}

View File

@ -87,22 +87,24 @@
</mat-form-field>
</p>
<p>
<os-search-value-selector
ngDefaultControl
[formControl]="commentFieldForm.get('read_groups_id')"
[multiple]="true"
listname="Groups with read permissions"
[inputListValues]="groups"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="read_groups_id"
[multiple]="true"
placeholder="Groups with read permissions"
[inputListValues]="groups"
></os-search-value-selector>
</mat-form-field>
</p>
<p>
<os-search-value-selector
ngDefaultControl
[formControl]="commentFieldForm.get('write_groups_id')"
[multiple]="true"
listname="Groups with write permissions"
[inputListValues]="groups"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="write_groups_id"
[multiple]="true"
placeholder="Groups with write permissions"
[inputListValues]="groups"
></os-search-value-selector>
</mat-form-field>
</p>
</form>
</div>

View File

@ -34,13 +34,14 @@
</os-sorting-list>
<form *ngIf="users && users.value.length > 0" [formGroup]="addSubmitterForm">
<os-search-value-selector
class="search-users"
ngDefaultControl
[formControl]="addSubmitterForm.get('userId')"
listname="{{ 'Select or search new submitter ...' | translate }}"
[inputListValues]="users"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
class="search-users"
formControlName="userId"
placeholder="{{ 'Select or search new submitter ...' | translate }}"
[inputListValues]="users"
></os-search-value-selector>
</mat-form-field>
</form>
<p>

View File

@ -459,14 +459,15 @@
<!-- motion polls -->
<div *ngIf="!editMotion" class="spacer-top-20 spacer-bottom-20">
<os-motion-poll *ngFor="let poll of motion.motion.polls; let i = index" [rawPoll]="poll" [pollIndex]="i">
</os-motion-poll>
<div class="create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)">
<button mat-button (click)="createPoll()">
<mat-icon class="main-nav-color">poll</mat-icon>
<span translate>New vote</span>
</button>
</div>
<mat-accordion>
<os-motion-poll-preview *ngFor="let poll of motion.polls" [poll]="poll"></os-motion-poll-preview>
</mat-accordion>
</div>
</div>
</ng-template>
@ -601,15 +602,16 @@
<!-- Submitter -->
<div *ngIf="newMotion" class="content-field">
<div *ngIf="perms.isAllowed('change_metadata', motion)">
<os-search-value-selector
ngDefaultControl
[formControl]="contentForm.get('submitters_id')"
[multiple]="true"
listname="{{ 'Submitters' | translate }}"
[inputListValues]="submitterObserver"
></os-search-value-selector>
</div>
<ng-container *ngIf="perms.isAllowed('change_metadata', motion)">
<mat-form-field>
<os-search-value-selector
formControlName="submitters_id"
[multiple]="true"
placeholder="{{ 'Submitters' | translate }}"
[inputListValues]="submitterObserver"
></os-search-value-selector>
</mat-form-field>
</ng-container>
</div>
<div class="form-id-title">
@ -783,13 +785,14 @@
<!-- Category form -->
<div class="content-field" *ngIf="newMotion && categoryObserver.value.length > 0">
<os-search-value-selector
ngDefaultControl
[formControl]="contentForm.get('category_id')"
[includeNone]="true"
listname="{{ 'Category' | translate }}"
[inputListValues]="categoryObserver"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="category_id"
[includeNone]="true"
placeholder="{{ 'Category' | translate }}"
[inputListValues]="categoryObserver"
></os-search-value-selector>
</mat-form-field>
</div>
<div class="extra-data">
@ -805,8 +808,8 @@
</div>
<div *osPerms="'motions.can_manage'; and: editMotion">
<os-attachment-control
formControlName="attachments_id"
(errorHandler)="showUploadError($event)"
[controlName]="contentForm.get('attachments_id')"
></os-attachment-control>
</div>
</div>
@ -818,25 +821,27 @@
<!-- Supporter form -->
<div class="content-field" *ngIf="editMotion && minSupporters">
<div *ngIf="perms.isAllowed('change_metadata', motion)">
<os-search-value-selector
ngDefaultControl
[formControl]="contentForm.get('supporters_id')"
[multiple]="true"
listname="{{ 'Supporters' | translate }}"
[inputListValues]="supporterObserver"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="supporters_id"
[multiple]="true"
placeholder="{{ 'Supporters' | translate }}"
[inputListValues]="supporterObserver"
></os-search-value-selector>
</mat-form-field>
</div>
</div>
<!-- Workflow -->
<div class="content-field" *ngIf="editMotion && workflowObserver.value.length > 1">
<div *ngIf="perms.isAllowed('change_metadata', motion)">
<os-search-value-selector
ngDefaultControl
[formControl]="contentForm.get('workflow_id')"
listname="{{ 'Workflow' | translate }}"
[inputListValues]="workflowObserver"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="workflow_id"
placeholder="{{ 'Workflow' | translate }}"
[inputListValues]="workflowObserver"
></os-search-value-selector>
</mat-form-field>
</div>
</div>

View File

@ -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();
}));

View File

@ -1382,9 +1382,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* Handler for creating a poll
*/
public createPoll(): void {
// TODO
// this.repo.createPoll(<any>{}).catch(this.raiseError);
throw new Error('TODO');
this.router.navigate(['motions', 'polls', 'new'], { queryParams: { parent: this.motion.id || null } });
}
/**

View File

@ -0,0 +1,56 @@
<mat-expansion-panel>
<mat-expansion-panel-header *ngIf="poll">{{ poll.title }}</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="poll-content">
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose }}</div>
<div *ngIf="poll.groups">{{ 'Groups' | translate }}: {{ poll.groups }}</div>
<div>{{ 'Method' | translate }}: {{ poll.pollmethodVerbose }}</div>
<div>{{ 'Type' | translate }}: {{ poll.typeVerbose }}</div>
</div>
<mat-divider></mat-divider>
<div class="poll-footer">
<button *ngIf="poll.type === pollTypes.Analog" mat-icon-button (click)="enterAnalogVotes()">
<mat-icon class="small-icon" matTooltip="{{ 'Enter votes' | translate }}">check</mat-icon><!-- TODO: other icon-->
</button>
<button mat-icon-button (click)="openPoll()">
<mat-icon class="small-icon" matTooltip="{{ 'View' | translate }}">pageview</mat-icon>
</button>
<button mat-icon-button (click)="editPoll()">
<mat-icon class="small-icon" matTooltip="{{ 'Edit' | translate }}">edit</mat-icon>
</button>
<button mat-icon-button (click)="deletePoll()">
<mat-icon class="small-icon" matTooltip="{{ 'Delete' | translate }}">delete</mat-icon>
</button>
</div>
</ng-template>
</mat-expansion-panel>
<!-- Template for dialog for entering votes -->
<ng-template #enterVotesDialog>
<h1 mat-dialog-title translate>Enter votes</h1>
<div class="os-form-card-mobile" mat-dialog-content>
<form [formGroup]="statuteParagraphForm" (keydown)="onKeyDown($event)">
<p>
<mat-form-field>
<input formControlName="title" matInput placeholder="{{ 'Title' | translate }}" required />
<mat-hint *ngIf="!statuteParagraphForm.controls.title.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
<span>
<!-- The HTML Editor -->
<h4 translate>Statute paragraph</h4>
<editor formControlName="text" [init]="tinyMceSettings"></editor>
</span>
</form>
</div>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close]="true" [disabled]="!statuteParagraphForm.valid">
<span translate>Save</span>
</button>
<button mat-button [mat-dialog-close]="false">
<span translate>Cancel</span>
</button>
</div>
</ng-template>

View File

@ -0,0 +1,7 @@
.poll-content {
padding-bottom: 8px;
}
.poll-footer {
text-align: end;
}

View File

@ -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<MotionPollPreviewComponent>;
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();
});
});

View File

@ -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<void> {
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');
}
}

View File

@ -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,

View File

@ -0,0 +1,92 @@
<os-head-bar
[goBack]="true"
[nav]="false"
[hasMainButton]="true"
[editMode]="isEditingPoll"
[mainButtonIcon]="'edit'"
[mainActionTooltip]="'Edit' | translate"
[isSaveButtonEnabled]="contentForm.valid"
(mainEvent)="editPoll()"
(saveEvent)="savePoll()"
(cancelEditEvent)="backToView()"
>
<div class="title-slot">
<h2 *ngIf="isNewPoll" translate>New vote</h2>
<h2 *ngIf="!isNewPoll && !!poll">{{ poll.title }}</h2>
</div>
</os-head-bar>
<mat-card [ngClass]="isEditingPoll ? 'os-form-card' : 'os-card'">
<ng-container *ngIf="!isEditingPoll" [ngTemplateOutlet]="viewTemplate"></ng-container>
<ng-container *ngIf="isEditingPoll" [ngTemplateOutlet]="formTemplate"></ng-container>
</mat-card>
<ng-template #viewTemplate>
<ng-container *ngIf="poll">
<h1>{{ poll.title }}</h1>
<mat-divider></mat-divider>
<div class="poll-content">
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}</div>
<div *ngIf="poll.groups">
{{ 'Groups' | translate }}:
<span *ngFor="let group of poll.groups">{{ group.getTitle() | translate }}</span>
</div>
<div>{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }}</div>
<div>{{ 'Poll method' | translate }}: {{ poll.pollmethodVerbose | translate }}</div>
<div>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
</div>
</ng-container>
</ng-template>
<ng-template #formTemplate>
<form [formGroup]="contentForm">
<mat-form-field>
<input matInput formControlName="title" [placeholder]="'Title' | translate" required />
<mat-error translate>A title is required</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select [placeholder]="'Poll type' | translate" formControlName="type" required>
<mat-option *ngFor="let option of pollTypes | keyvalue" [value]="option.key">{{
option.value | translate
}}</mat-option>
</mat-select>
<mat-error translate>This field is required</mat-error>
</mat-form-field>
<mat-form-field *ngIf="contentForm.controls.type.value && contentForm.controls.type.value != 'analog'">
<os-search-value-selector
formControlName="groups_id"
[multiple]="true"
[includeNone]="false"
[placeholder]="'Entitled to vote' | translate"
[inputListValues]="groupObservable"
></os-search-value-selector>
</mat-form-field>
<mat-form-field>
<mat-select [placeholder]="'Poll method' | translate" formControlName="pollmethod" required>
<mat-option *ngFor="let option of pollMethods | keyvalue" [value]="option.key">{{
option.value
}}</mat-option>
</mat-select>
<mat-error translate>This field is required</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select
placeholder="{{ '100% base' | translate }}"
formControlName="onehundred_percent_base"
required
>
<mat-option *ngFor="let option of percentBases | keyvalue" [value]="option.key">{{
option.value | translate
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="{{ 'Majority' | translate }}" formControlName="majority_method" required>
<mat-option *ngFor="let option of majorityMethods | keyvalue" [value]="option.key">{{
option.value | translate
}}</mat-option>
</mat-select>
</mat-form-field>
</form>
</ng-template>

View File

@ -0,0 +1,3 @@
.poll-content {
padding-top: 10px;
}

View File

@ -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<MotionPollDetailComponent>;
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();
});
});

View File

@ -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<ViewGroup[]> = 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]);
});
}
}
}

View File

@ -0,0 +1,36 @@
<os-head-bar>
<div class="title-slot" translate>Motions poll list</div>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="pollMenu"><mat-icon>more_vert</mat-icon></button>
</div>
</os-head-bar>
<os-list-view-table
[repo]="repo"
[vScrollFixed]="64"
[columns]="tableColumnDefinition"
[listStorageKey]="'motion-polls'"
>
<div *pblNgridCellDef="'title'; row as poll; rowContext as context" class="cell-slot fill">
<a
class="detail-link"
(click)="saveScrollIndex('motion-polls', rowContext.identity)"
[routerLink]="poll.id"
*ngIf="!isMultiSelect"
></a>
<span>{{ poll.title }}</span>
</div>
<div *pblNgridCellDef="'state'; row as poll; rowContext as context" class="cell-slot fill">
<span>{{ poll.stateVerbose }}</span>
</div>
</os-list-view-table>
<mat-menu #pollMenu="matMenu">
<!-- Settings -->
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/polls">
<mat-icon>settings</mat-icon>
<span translate>Settings</span>
</button>
</mat-menu>

View File

@ -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<MotionPollListComponent>;
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();
});
});

View File

@ -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<ViewMotionPoll> 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)));
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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),

View File

@ -70,7 +70,7 @@
<!-- Attachments -->
<os-attachment-control
[controlName]="topicForm.get('attachments_id')"
formControlName="attachments_id"
(errorHandler)="raiseError($event)"
></os-attachment-control>
@ -88,13 +88,14 @@
<!-- Parent item -->
<div>
<os-search-value-selector
ngDefaultControl
[formControl]="topicForm.get('agenda_parent_id')"
[includeNone]="true"
listname="{{ 'Parent agenda item' | translate }}"
[inputListValues]="itemObserver"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="agenda_parent_id"
[includeNone]="true"
placeholder="{{ 'Parent agenda item' | translate }}"
[inputListValues]="itemObserver"
></os-search-value-selector>
</mat-form-field>
</div>
</div>
</form>

View File

@ -75,22 +75,12 @@
</mat-form-field>
<!-- First name -->
<mat-form-field class="form37 distance force-min-with">
<input
type="text"
matInput
placeholder="{{ 'Given name' | translate }}"
formControlName="first_name"
/>
<input type="text" matInput placeholder="{{ 'Given name' | translate }}" formControlName="first_name" />
</mat-form-field>
<!-- Last name -->
<mat-form-field class="form37 force-min-with">
<input
type="text"
matInput
placeholder="{{ 'Surname' | translate }}"
formControlName="last_name"
/>
<input type="text" matInput placeholder="{{ 'Surname' | translate }}" formControlName="last_name" />
</mat-form-field>
</div>
@ -142,13 +132,14 @@
<div>
<!-- Groups -->
<os-search-value-selector
ngDefaultControl
[formControl]="personalInfoForm.get('groups_id')"
[multiple]="true"
listname="{{ 'Groups' | translate }}"
[inputListValues]="groups"
></os-search-value-selector>
<mat-form-field>
<os-search-value-selector
formControlName="groups_id"
[multiple]="true"
placeholder="{{ 'Groups' | translate }}"
[inputListValues]="groups"
></os-search-value-selector>
</mat-form-field>
</div>
<div *ngIf="isAllowed('manage')">
@ -184,23 +175,14 @@
<div *ngIf="isAllowed('seePersonal')">
<!-- username -->
<mat-form-field>
<input
type="text"
matInput
placeholder="{{ 'Username' | translate }}"
formControlName="username"
/>
<input type="text" matInput placeholder="{{ 'Username' | translate }}" formControlName="username" />
</mat-form-field>
</div>
<div *ngIf="isAllowed('seeExtra')">
<!-- Comment -->
<mat-form-field>
<input
matInput
placeholder="{{ 'Comment' | translate }}"
formControlName="comment"
/>
<input matInput placeholder="{{ 'Comment' | translate }}" formControlName="comment" />
<mat-hint translate>Only for internal notes.</mat-hint>
</mat-form-field>
</div>
@ -245,7 +227,9 @@
<span class="state-icons">
<span>{{ user.short_name }}</span>
<mat-icon *ngIf="user.is_present" matTooltip="{{ 'Is present' | translate }}">check_box</mat-icon>
<mat-icon *ngIf="user.is_committee" matTooltip="{{ 'Is committee' | translate }}">account_balance</mat-icon>
<mat-icon *ngIf="user.is_committee" matTooltip="{{ 'Is committee' | translate }}"
>account_balance</mat-icon
>
<mat-icon *ngIf="!user.is_active && isAllowed('seeExtra')" matTooltip="{{ 'Inactive' | translate }}"
>block</mat-icon
>

View File

@ -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);