Separates the field of state and recommendation

- New component for similar situations
- Prevents overwriting and discarding of changes, if an 'autoupdate' is triggered
This commit is contained in:
GabrielMeyer 2019-07-04 14:37:36 +02:00 committed by Sean Engelhardt
parent 3cd9c5497c
commit 0d52aaaa45
11 changed files with 374 additions and 118 deletions

View File

@ -0,0 +1,54 @@
<!-- Chip -->
<div>
<h4 *ngIf="title">{{ title }}</h4>
<mat-menu #triggerMenu="matMenu">
<ng-container
[ngTemplateOutlet]="triggerTemplate">
</ng-container>
</mat-menu>
<os-icon-container
iconTooltip="{{ 'Edit' | translate }}"
icon="create"
[swap]="true"
[showIcon]="!editMode && canBeEdited && hasExtension"
(iconAction)="changeEditMode()">
<mat-basic-chip *ngIf="canBeEdited" [matMenuTriggerFor]="triggerMenu" [ngClass]="classes" class="pointer" disableRipple>
{{ chipValue || '' }}
</mat-basic-chip>
<mat-basic-chip *ngIf="!canBeEdited" [ngClass]="classes" disableRipple>
{{ chipValue }}
</mat-basic-chip>
</os-icon-container>
</div>
<!-- Extension field -->
<div
*ngIf="hasExtension && editMode"
class="spacer-top-10 extension-container"
>
<mat-form-field>
<input
matInput
[(ngModel)]="inputControl"
placeholder="{{ extensionLabel }}"
/>
</mat-form-field>
<os-search-value-selector
*ngIf="searchList"
ngDefaultControl
[form]="extensionFieldForm"
[formControl]="extensionFieldForm.get('list')"
[fullWidth]="true"
[multiple]="false"
[InputListValues]="searchList"
[listname]="searchListLabel"
></os-search-value-selector>
<button mat-button (click)="changeEditMode(true)">{{ 'Save' | translate }}</button>
<button mat-button (click)="changeEditMode()">{{ 'Cancel' | translate }}</button>
</div>
<!-- Optional template for the menu -->
<ng-template #triggerTemplate>
<ng-content select=".trigger-menu"></ng-content>
</ng-template>

View File

@ -0,0 +1,3 @@
.extension-container .mat-form-field {
display: block;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ExtensionFieldComponent } from './extension-field.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('ExtensionFieldComponent', () => {
let component: ExtensionFieldComponent;
let fixture: ComponentFixture<ExtensionFieldComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ExtensionFieldComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,177 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { FormGroup, FormBuilder } from '@angular/forms';
@Component({
selector: 'os-extension-field',
templateUrl: './extension-field.component.html',
styleUrls: ['./extension-field.component.scss']
})
export class ExtensionFieldComponent implements OnInit {
/**
* Optional additional classes for the `mat-chip`.
*/
@Input()
public classes: string | string[] | object = 'bluegrey';
/**
* Title for this component.
*/
@Input()
public title: string;
/**
* Value of the chip.
*/
@Input()
public chipValue: string;
/**
* Boolean, whether the extension should be shown.
*/
@Input()
public hasExtension = false;
/**
* Optional label for the input.
*/
@Input()
public extensionLabel: string;
/**
* Optional label for the search-list.
*/
@Input()
public searchListLabel: string;
/**
* BehaviourSubject for the search-list.
*/
@Input()
public searchList: BehaviorSubject<object[]>;
/**
* Boolean, whether the input and the search-list can be changed.
*/
@Input()
public canBeEdited = true;
/**
* Boolean, whether the list should fire events, if it changes.
*/
@Input()
public listSubmitOnChange = false;
/**
* Boolean, whether to append the value from list to the input.
*/
@Input()
public appendValueToInput = true;
/**
* Prefix, if the value from list should be appended to the input.
*/
@Input()
public listValuePrefix = '';
/**
* Suffix, if the value from list should be appended to the input.
*/
@Input()
public listValueSuffix = '';
/**
* Initial value of the input-field.
*/
@Input()
public inputValue: string;
/**
* EventEmitter, when clicking on the 'save'-button.
*/
@Output()
public success: EventEmitter<string | object> = new EventEmitter();
/**
* EventEmitter, if the list has changed.
*/
@Output()
public listChange: EventEmitter<number> = new EventEmitter();
/**
* Model for the input-field.
*/
public inputControl = '';
/**
* FormGroup for the search-list.
*/
public extensionFieldForm: FormGroup;
/**
* Boolean to decide, whether to open the extension-input and search-list.
*/
public editMode = false;
/**
* Constructor
*
* @param fb The FormBuilder
*/
public constructor(private fb: FormBuilder) {}
/**
* OnInit-method.
*/
public ngOnInit(): void {
this.initInput();
this.extensionFieldForm = this.fb.group({
list: this.searchList ? [[]] : undefined
});
this.extensionFieldForm.get('list').valueChanges.subscribe((value: number) => {
if (this.listSubmitOnChange) {
this.listChange.emit(value);
}
if (this.appendValueToInput) {
this.inputControl = this.inputControl.concat(
`[${this.listValuePrefix}${value}${this.listValueSuffix}]`
);
}
});
}
/**
* Function to switch to or from editing-mode.
*
* @param save Boolean, whether the changes should be saved or resetted.
*/
public changeEditMode(save: boolean = false): void {
if (save) {
this.sendSuccess();
} else {
this.initInput();
}
this.editMode = !this.editMode;
}
/**
* Initialize the value of the input.
*/
public initInput(): void {
this.inputControl = this.inputValue;
}
/**
* Function to execute, when the values are saved.
*/
public sendSuccess(): void {
if (this.success) {
const submitMessage =
this.listSubmitOnChange || this.appendValueToInput || !this.searchList
? this.inputControl
: { extensionInput: this.inputControl, extensionList: this.extensionFieldForm.get('list').value };
this.success.emit(submitMessage);
}
}
}

View File

@ -1,5 +1,13 @@
<mat-icon *ngIf="!swap">{{ icon }}</mat-icon> <mat-icon
*ngIf="!swap && showIcon"
[matTooltip]="iconTooltip"
[class]="iconAction ? 'pointer' : ''"
(click)="iconClick()">{{ icon }}</mat-icon>
<span class="content-node"> <span class="content-node">
<ng-content></ng-content> <ng-content></ng-content>
</span> </span>
<mat-icon *ngIf="swap">{{ icon }}</mat-icon> <mat-icon
*ngIf="swap && showIcon"
[matTooltip]="iconTooltip"
[class]="iconAction ? 'pointer' : ''"
(click)="iconClick()">{{ icon }}</mat-icon>

View File

@ -1,4 +1,4 @@
import { Component, Input, HostBinding } from '@angular/core'; import { Component, Input, HostBinding, Output, EventEmitter } from '@angular/core';
@Component({ @Component({
selector: 'os-icon-container', selector: 'os-icon-container',
@ -37,4 +37,29 @@ export class IconContainerComponent {
*/ */
@Input() @Input()
public swap = false; public swap = false;
/**
* Boolean to decide, when to show the icon.
*/
@Input()
public showIcon = true;
/**
* Optional string as tooltip for icon.
*/
@Input()
public iconTooltip: string;
/**
* Optional action for clicking on the icon.
*/
@Output()
public iconAction: EventEmitter<any> = new EventEmitter();
/**
* Function executed, when the icon is clicked.
*/
public iconClick(): void {
this.iconAction.emit();
}
} }

View File

@ -1,4 +1,4 @@
<mat-form-field [formGroup]="form"> <mat-form-field [formGroup]="form" [style.display]="fullWidth ? 'block' : 'inline-block'">
<mat-select [formControl]="formControl" placeholder="{{ listname | translate }}" [multiple]="multiple" #thisSelector> <mat-select [formControl]="formControl" placeholder="{{ listname | translate }}" [multiple]="multiple" #thisSelector>
<ngx-mat-select-search [formControl]="filterControl"></ngx-mat-select-search> <ngx-mat-select-search [formControl]="filterControl"></ngx-mat-select-search>
<div *ngIf="!multiple && includeNone"> <div *ngIf="!multiple && includeNone">

View File

@ -76,12 +76,21 @@ export class SearchValueSelectorComponent implements OnInit, OnDestroy {
@Input() @Input()
public includeNone = false; public includeNone = false;
/**
* Boolean, whether the component should be rendered with full width.
*/
@Input()
public fullWidth = false;
/** /**
* The inputlist subject. Subscribes to it and updates the selector, if the subject * The inputlist subject. Subscribes to it and updates the selector, if the subject
* changes its values. * changes its values.
*/ */
@Input() @Input()
public set InputListValues(value: BehaviorSubject<Selectable[]>) { public set InputListValues(value: BehaviorSubject<Selectable[]>) {
if (!value) {
return;
}
// unsubscribe to old subscription. // unsubscribe to old subscription.
if (this._inputListSubscription) { if (this._inputListSubscription) {
this._inputListSubscription.unsubscribe(); this._inputListSubscription.unsubscribe();

View File

@ -91,6 +91,7 @@ import { BlockTileComponent } from './components/block-tile/block-tile.component
import { IconContainerComponent } from './components/icon-container/icon-container.component'; import { IconContainerComponent } from './components/icon-container/icon-container.component';
import { ListViewTableComponent } from './components/list-view-table/list-view-table.component'; import { ListViewTableComponent } from './components/list-view-table/list-view-table.component';
import { AgendaContentObjectFormComponent } from './components/agenda-content-object-form/agenda-content-object-form.component'; import { AgendaContentObjectFormComponent } from './components/agenda-content-object-form/agenda-content-object-form.component';
import { ExtensionFieldComponent } from './components/extension-field/extension-field.component';
/** /**
* Share Module for all "dumb" components and pipes. * Share Module for all "dumb" components and pipes.
@ -224,7 +225,8 @@ import { AgendaContentObjectFormComponent } from './components/agenda-content-ob
PblNgridModule, PblNgridModule,
PblNgridMaterialModule, PblNgridMaterialModule,
ListViewTableComponent, ListViewTableComponent,
AgendaContentObjectFormComponent AgendaContentObjectFormComponent,
ExtensionFieldComponent
], ],
declarations: [ declarations: [
PermsDirective, PermsDirective,
@ -259,7 +261,8 @@ import { AgendaContentObjectFormComponent } from './components/agenda-content-ob
BlockTileComponent, BlockTileComponent,
IconContainerComponent, IconContainerComponent,
ListViewTableComponent, ListViewTableComponent,
AgendaContentObjectFormComponent AgendaContentObjectFormComponent,
ExtensionFieldComponent
], ],
providers: [ providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter }, { provide: DateAdapter, useClass: OpenSlidesDateAdapter },

View File

@ -242,12 +242,20 @@
<!-- Set State --> <!-- Set State -->
<div *ngIf="!editMotion"> <div *ngIf="!editMotion">
<h4 translate>State</h4> <os-extension-field
<mat-menu #stateMenu="matMenu"> title="{{ 'State' | translate }}"
[canBeEdited]="perms.isAllowed('change_state', motion)"
[hasExtension]="motion.state && motion.state.show_state_extension_field"
[chipValue]="stateLabel"
[inputValue]="newStateExtension"
[classes]="motion.stateCssColor"
extensionLabel="{{ 'Extension' | translate }}"
(success)="setStateExtension($event)">
<span class="trigger-menu">
<button *ngFor="let state of motion.nextStates" mat-menu-item (click)="setState(state.id)"> <button *ngFor="let state of motion.nextStates" mat-menu-item (click)="setState(state.id)">
{{ state.name | translate }} <span *ngIf="state.show_state_extension_field">&nbsp;...</span> {{ state.name | translate }} <span *ngIf="state.show_state_extension_field">&nbsp;...</span>
</button> </button>
<div *ngIf="perms.isAllowed('change_metadata', motion)"> <div>
<mat-divider *ngIf="motion.nextStates.length > 0"></mat-divider> <mat-divider *ngIf="motion.nextStates.length > 0"></mat-divider>
<button *ngFor="let state of motion.previousStates" mat-menu-item (click)="setState(state.id)"> <button *ngFor="let state of motion.previousStates" mat-menu-item (click)="setState(state.id)">
<mat-icon>arrow_back</mat-icon> {{ state.name | translate }} <mat-icon>arrow_back</mat-icon> {{ state.name | translate }}
@ -257,27 +265,24 @@
<mat-icon>replay</mat-icon> {{ 'Reset state' | translate }} <mat-icon>replay</mat-icon> {{ 'Reset state' | translate }}
</button> </button>
</div> </div>
</mat-menu> </span>
<div *ngIf="perms.isAllowed('change_state', motion)"> </os-extension-field>
<mat-basic-chip [matMenuTriggerFor]="stateMenu" [ngClass]="motion.stateCssColor" disableRipple>
{{ stateLabel }}
</mat-basic-chip>
<div *ngIf="motion.state && motion.state.show_state_extension_field" class="spacer-top-10">
<mat-form-field>
<input matInput placeholder="{{ 'Extension' | translate }}" [(ngModel)]="newStateExtension" />
</mat-form-field>
<button mat-icon-button (click)="setStateExtension()"><mat-icon>check</mat-icon></button>
</div>
</div>
<div *ngIf="!perms.isAllowed('change_state', motion)">
<mat-basic-chip [ngClass]="motion.stateCssColor" disableRipple> {{ stateLabel }} </mat-basic-chip>
</div>
</div> </div>
<!-- Recommendation --> <!-- Recommendation -->
<div *ngIf="recommender && !editMotion"> <div *ngIf="recommender && !editMotion">
<h4 *ngIf="perms.isAllowed('change_metadata', motion) || recommendationLabel">{{ recommender }}</h4> <os-extension-field
<mat-menu #recommendationMenu="matMenu"> title="{{ recommender }}"
[inputValue]="recommendationStateExtension"
[canBeEdited]="perms.isAllowed('change_metadata', motion)"
[chipValue]="recommendationLabel"
[hasExtension]="motion.recommendation && motion.recommendation.show_recommendation_extension_field"
extensionLabel="{{ 'Extension' | translate }}"
[searchList]="motionObserver"
searchListLabel="{{ 'Motions' | translate }}"
listValuePrefix="motion:"
(success)="setRecommendationExtension($event)">
<span class="trigger-menu">
<button <button
*ngFor="let recommendation of motion.possibleRecommendations" *ngFor="let recommendation of motion.possibleRecommendations"
mat-menu-item mat-menu-item
@ -289,45 +294,13 @@
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button <button
mat-menu-item mat-menu-item
*ngIf="perms.isAllowed('change_metadata', motion)"
(click)="setRecommendation(null)" (click)="setRecommendation(null)"
> >
<mat-icon>replay</mat-icon> {{ 'Reset recommendation' | translate }} <mat-icon>replay</mat-icon> {{ 'Reset recommendation' | translate }}
</button> </button>
</mat-menu> </span>
<div *ngIf="perms.isAllowed('change_metadata', motion)"> </os-extension-field>
<mat-basic-chip [matMenuTriggerFor]="recommendationMenu" class="bluegrey" disableRipple>
{{ recommendationLabel || '' }}
</mat-basic-chip>
<div
*ngIf="motion.recommendation && motion.recommendation.show_recommendation_extension_field"
class="spacer-top-10"
>
<form [formGroup]="recommendationExtensionForm">
<mat-form-field>
<input
matInput
[formControl]="recommendationExtensionForm.get('recoExtension')"
placeholder="{{ 'Extension' | translate }}"
/>
</mat-form-field>
<button mat-icon-button (click)="setRecommendationExtension()">
<mat-icon>check</mat-icon>
</button>
<os-search-value-selector
ngDefaultControl
[form]="recommendationExtensionForm"
[formControl]="recommendationExtensionForm.get('motion_id')"
[multiple]="false"
listname="{{ 'Motions' | translate }}"
[InputListValues]="motionObserver"
></os-search-value-selector>
</form>
</div>
</div>
<div *ngIf="!perms.isAllowed('change_metadata', motion) && recommendationLabel">
<mat-basic-chip class="bluegrey" disableRipple> {{ recommendationLabel }} </mat-basic-chip>
</div>
<button <button
mat-stroked-button mat-stroked-button
*ngIf="canFollowRecommendation()" *ngIf="canFollowRecommendation()"

View File

@ -79,11 +79,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
*/ */
public contentForm: FormGroup; public contentForm: FormGroup;
/**
* To search other motions as extension via search value selector
*/
public recommendationExtensionForm: FormGroup;
/** /**
* Determine if the motion is edited * Determine if the motion is edited
*/ */
@ -354,6 +349,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
*/ */
public newStateExtension = ''; public newStateExtension = '';
/**
* State extension label for the recommendation.
*/
public recommendationStateExtension = '';
/** /**
* Constant to identify the notification-message. * Constant to identify the notification-message.
*/ */
@ -616,6 +616,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
super.setTitle(title); super.setTitle(title);
this.motion = motion; this.motion = motion;
this.newStateExtension = this.motion.stateExtension; this.newStateExtension = this.motion.stateExtension;
this.recommendationStateExtension = this.motion.recommendationExtension;
if (!this.editMotion) { if (!this.editMotion) {
this.patchForm(this.motion); this.patchForm(this.motion);
} }
@ -707,7 +708,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
const statuteAmendmentFieldName = 'statute_amendment'; const statuteAmendmentFieldName = 'statute_amendment';
contentPatch[statuteAmendmentFieldName] = formMotion.isStatuteAmendment(); contentPatch[statuteAmendmentFieldName] = formMotion.isStatuteAmendment();
this.contentForm.patchValue(contentPatch); this.contentForm.patchValue(contentPatch);
this.recommendationExtensionForm.get('recoExtension').setValue(this.motion.recommendationExtension);
} }
/** /**
@ -752,17 +752,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
return value.match(/[^\d]/) !== null || parseInt(value, 10) >= maxLineNumber; return value.match(/[^\d]/) !== null || parseInt(value, 10) >= maxLineNumber;
} }
})(); })();
// create the search motion form
this.recommendationExtensionForm = this.formBuilder.group({
motion_id: [],
recoExtension: []
});
// Detect changes in in search motion form
this.recommendationExtensionForm.get('motion_id').valueChanges.subscribe(change => {
this.addMotionExtension(change);
});
} }
/** /**
@ -1301,18 +1290,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* triggers the update this motion's state extension according to the current string * triggers the update this motion's state extension according to the current string
* in {@link newStateExtension} * in {@link newStateExtension}
*/ */
public setStateExtension(): void { public setStateExtension(nextExtension: string): void {
this.repo.setStateExtension(this.motion, this.newStateExtension); this.repo.setStateExtension(this.motion, nextExtension);
}
/**
* Adds an extension in the shape: [Motion:id] to the recoExtension form control
*
* @param id the ID of a selected motion returned by a search value selector
*/
public addMotionExtension(id: number): void {
const recoExtensionValue = this.recommendationExtensionForm.get('recoExtension').value || '';
this.recommendationExtensionForm.get('recoExtension').setValue(`${recoExtensionValue}[motion:${id}]`);
} }
/** /**
@ -1328,8 +1307,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* triggers the update this motion's recommendation extension according to the current string * triggers the update this motion's recommendation extension according to the current string
* in {@link newRecommendationExtension} * in {@link newRecommendationExtension}
*/ */
public setRecommendationExtension(): void { public setRecommendationExtension(nextExtension: string): void {
this.repo.setRecommendationExtension(this.motion, this.recommendationExtensionForm.get('recoExtension').value); this.repo.setRecommendationExtension(this.motion, nextExtension);
} }
/** /**