Enhance amendment wizard

- close button instead of back-button
- "are you sure" prompt if chances to the wizard were made
- edit and save events, like every other view
- enhanced next, previous, create logic that follows validation

also:
- fixed a bug with custom cancel events in the head-bar
- mobile button has the correct icon again
This commit is contained in:
Sean Engelhardt 2019-08-26 13:51:25 +02:00
parent b1c02133ee
commit af8b49450b
9 changed files with 107 additions and 34 deletions

View File

@ -15,20 +15,28 @@ export class RoutingStateService {
/** /**
* Hold the previous URL * Hold the previous URL
*/ */
private previousUrl: string; private _previousUrl: string;
/** /**
* Unsafe paths that the user should not go "back" to * Unsafe paths that the user should not go "back" to
* TODO: Might also work using Routing parameters * TODO: Might also work using Routing parameters
*/ */
private unsafeUrls: string[] = ['/login', '/privacypolicy', '/legalnotice']; private unsafeUrls: string[] = ['/login', '/privacypolicy', '/legalnotice', '/new', '/create'];
/** /**
* Checks if the previous URL is safe to navigate to. * Checks if the previous URL is safe to navigate to.
* If this fails, the open nav button should be shown * If this fails, the open nav button should be shown
*/ */
public get isSafePrevUrl(): boolean { public get isSafePrevUrl(): boolean {
return !this.previousUrl || !this.unsafeUrls.includes(this.previousUrl); if (this._previousUrl) {
return !this.unsafeUrls.some(unsafeUrl => this._previousUrl.includes(unsafeUrl));
} else {
return false;
}
}
public get previousUrl(): string {
return this._previousUrl;
} }
/** /**
@ -43,7 +51,7 @@ export class RoutingStateService {
pairwise() pairwise()
) )
.subscribe((event: any[]) => { .subscribe((event: any[]) => {
this.previousUrl = event[0].urlAfterRedirects; this._previousUrl = event[0].urlAfterRedirects;
}); });
} }

View File

@ -11,7 +11,7 @@
</button> </button>
<!-- Cancel edit button --> <!-- Cancel edit button -->
<button mat-icon-button *ngIf="editMode" (click)="cancelEditEvent ? sendCancelEditEvent() : sendMainEvent()"> <button mat-icon-button *ngIf="editMode" (click)="isCancelEditUsed ? sendCancelEditEvent() : sendMainEvent()">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
@ -44,7 +44,7 @@
<!-- Save button --> <!-- Save button -->
<button mat-button *ngIf="editMode" [disabled]="!isSaveButtonEnabled" (click)="save()"> <button mat-button *ngIf="editMode" [disabled]="!isSaveButtonEnabled" (click)="save()">
<strong translate class="upper">Save</strong> <strong translate class="upper">{{ saveText }}</strong>
</button> </button>
<!-- Menu button slot --> <!-- Menu button slot -->
@ -61,5 +61,10 @@
(click)="sendMainEvent()" (click)="sendMainEvent()"
matTooltip="{{ mainActionTooltip | translate }}" matTooltip="{{ mainActionTooltip | translate }}"
> >
<mat-icon>{{ mainButtonIcon }}</mat-icon> <mat-icon *ngIf="mainButtonIcon === 'add_circle'; else mainIconBlock">
add
</mat-icon>
</button> </button>
<ng-template #mainIconBlock>
{{ mainButtonIcon }}
</ng-template>

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MainMenuService } from 'app/core/core-services/main-menu.service'; import { MainMenuService } from 'app/core/core-services/main-menu.service';
@ -17,11 +17,14 @@ import { ViewportService } from 'app/core/ui-services/viewport.service';
* ```html * ```html
* <os-head-bar * <os-head-bar
* prevUrl="../.." * prevUrl="../.."
* saveText="Create"
* [nav]="false" * [nav]="false"
* [goBack]="true" * [goBack]="true"
* [mainButton]="opCanEdit()" * [mainButton]="opCanEdit()"
* [mainButtonIcon]="edit" * [mainButtonIcon]="edit"
* [backButtonIcon]="arrow_back"
* [editMode]="editMotion" * [editMode]="editMotion"
* [isSaveButtonEnabled]="myConditionIsTrue()"
* [multiSelectMode]="isMultiSelect" * [multiSelectMode]="isMultiSelect"
* (mainEvent)="setEditMode(!editMotion)" * (mainEvent)="setEditMode(!editMotion)"
* (saveEvent)="saveMotion()"> * (saveEvent)="saveMotion()">
@ -52,7 +55,7 @@ import { ViewportService } from 'app/core/ui-services/viewport.service';
templateUrl: './head-bar.component.html', templateUrl: './head-bar.component.html',
styleUrls: ['./head-bar.component.scss'] styleUrls: ['./head-bar.component.scss']
}) })
export class HeadBarComponent { export class HeadBarComponent implements OnInit {
/** /**
* Determine if the the navigation "hamburger" icon should be displayed in mobile mode * Determine if the the navigation "hamburger" icon should be displayed in mobile mode
*/ */
@ -65,6 +68,12 @@ export class HeadBarComponent {
@Input() @Input()
public mainButtonIcon = 'add_circle'; public mainButtonIcon = 'add_circle';
/**
* Custom text to show as "save"
*/
@Input()
public saveText = 'Save';
/** /**
* Determine edit mode * Determine edit mode
*/ */
@ -123,6 +132,11 @@ export class HeadBarComponent {
@Output() @Output()
public cancelEditEvent = new EventEmitter<void>(); public cancelEditEvent = new EventEmitter<void>();
/**
* To detect if the cancel event was used
*/
public isCancelEditUsed = false;
/** /**
* Sends a signal if a detail view should be saved * Sends a signal if a detail view should be saved
*/ */
@ -144,6 +158,13 @@ export class HeadBarComponent {
private routingState: RoutingStateService private routingState: RoutingStateService
) {} ) {}
/**
* Detect if the cancel edit event was used
*/
public ngOnInit(): void {
this.isCancelEditUsed = this.cancelEditEvent.observers.length > 0;
}
/** /**
* Emits a signal to the parent if * Emits a signal to the parent if
*/ */

View File

@ -1,15 +1,24 @@
<os-head-bar [nav]="false" [goBack]="false"> <os-head-bar
[nav]="false"
[editMode]="true"
saveText="Create"
[isSaveButtonEnabled]="matStepper.selectedIndex === 1"
(saveEvent)="saveAmendment()"
(cancelEditEvent)="cancelCreation()"
>
<!-- Title --> <!-- Title -->
<div class="title-slot"><h2 translate>New amendment</h2></div> <div class="title-slot"><h2 translate>New amendment</h2></div>
<div class="menu-slot">
<!-- Next-button -->
<div class="extra-controls-slot">
<div *ngIf="matStepper.selectedIndex === 0"> <div *ngIf="matStepper.selectedIndex === 0">
<button mat-button [disabled]="contentForm.value.selectedParagraph === null" (click)="matStepper.next()"> <button mat-button [disabled]="contentForm.value.selectedParagraphs.length === 0" (click)="matStepper.next()">
<span class="upper" translate>Next</span> <span class="upper" translate>Next</span>
</button> </button>
</div> </div>
<div *ngIf="matStepper.selectedIndex === 1"> <div *ngIf="matStepper.selectedIndex === 1">
<button type="button" mat-button (click)="saveAmendment()"> <button type="button" mat-button (click)="matStepper.previous()">
<span class="upper" translate>Create</span> <span class="upper" translate>Previous</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -8,6 +8,7 @@ import { TranslateService } from '@ngx-translate/core';
import { MotionRepositoryService, ParagraphToChoose } from 'app/core/repositories/motions/motion-repository.service'; import { MotionRepositoryService, ParagraphToChoose } from 'app/core/repositories/motions/motion-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { CreateMotion } from 'app/site/motions/models/create-motion'; import { CreateMotion } from 'app/site/motions/models/create-motion';
import { ViewMotion } from 'app/site/motions/models/view-motion'; import { ViewMotion } from 'app/site/motions/models/view-motion';
@ -54,14 +55,15 @@ export class AmendmentCreateWizardComponent extends BaseViewComponent {
/** /**
* Constructs this component. * Constructs this component.
* *
* @param {Title} titleService set the browser title * @param titleService set the browser title
* @param {TranslateService} translate the translation service * @param translate the translation service
* @param {ConfigService} configService The configuration provider * @param configService The configuration provider
* @param {FormBuilder} formBuilder Form builder * @param formBuilder Form builder
* @param {MotionRepositoryService} repo Motion Repository * @param repo Motion Repository
* @param {ActivatedRoute} route The activated route * @param route The activated route
* @param {Router} router The router * @param router The router
* @param {MatSnackBar} matSnackBar Material Design SnackBar * @param promptService Show a prompt by leaving the view
* @param matSnackBar Material Design SnackBar
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
@ -71,6 +73,7 @@ export class AmendmentCreateWizardComponent extends BaseViewComponent {
private repo: MotionRepositoryService, private repo: MotionRepositoryService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private promptService: PromptService,
matSnackBar: MatSnackBar matSnackBar: MatSnackBar
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
@ -103,6 +106,21 @@ export class AmendmentCreateWizardComponent extends BaseViewComponent {
}); });
} }
/**
* Cancel the editing.
* Only fires when the form was dirty
*/
public async cancelCreation(): Promise<void> {
if (this.contentForm.dirty || this.contentForm.value.selectedParagraphs.length > 0) {
const title = this.translate.instant('Are you sure you want to discard this amendment?');
if (await this.promptService.open(title)) {
this.router.navigate(['..'], { relativeTo: this.route });
}
} else {
this.router.navigate(['..'], { relativeTo: this.route });
}
}
/** /**
* Creates the forms for the Motion and the MotionVersion * Creates the forms for the Motion and the MotionVersion
*/ */

View File

@ -1,8 +1,8 @@
<os-head-bar <os-head-bar
[mainButton]="perms.isAllowed('can_create_amendments', motion)" [mainButton]="perms.isAllowed('can_create_amendments', motion)"
mainActionTooltip="New amendment" mainActionTooltip="New amendment"
prevUrl="../.." [prevUrl]="getPrevUrl()"
[goBack]="motion && !!motion.parent_id" [goBack]="routingStateService.isSafePrevUrl"
[nav]="false" [nav]="false"
[editMode]="editMotion" [editMode]="editMotion"
[isSaveButtonEnabled]="contentForm.valid" [isSaveButtonEnabled]="contentForm.valid"

View File

@ -26,6 +26,7 @@ import { DiffLinesInParagraph, DiffService, LineRange } from 'app/core/ui-servic
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { PersonalNoteService } from 'app/core/ui-services/personal-note.service'; import { PersonalNoteService } from 'app/core/ui-services/personal-note.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { RoutingStateService } from 'app/core/ui-services/routing-state.service';
import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewportService } from 'app/core/ui-services/viewport.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Motion } from 'app/shared/models/motions/motion'; import { Motion } from 'app/shared/models/motions/motion';
@ -445,7 +446,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
private blockRepo: MotionBlockRepositoryService, private blockRepo: MotionBlockRepositoryService,
private itemRepo: ItemRepositoryService, private itemRepo: ItemRepositoryService,
private motionSortService: MotionSortListService, private motionSortService: MotionSortListService,
private motionFilterService: MotionFilterListService private motionFilterService: MotionFilterListService,
public routingStateService: RoutingStateService
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
} }
@ -1548,15 +1550,17 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
} }
/** /**
* Tries to "logically" navigate back. If the motion has a parent, it will * Tries to determine the previous URL if it's considered unsafe
* try to navigate to the parent
* rather than just into the list view.
* *
* @returns the target to navigate to * @returns the target to navigate to
*/ */
public getPrevUrl(): string { public getPrevUrl(): string {
if (this.motion && this.motion.parent_id) { if (this.motion && this.motion.parent_id) {
return `../../${this.motion.parent_id}`; if (this.routingStateService.previousUrl && this.routingStateService.isSafePrevUrl) {
return this.routingStateService.previousUrl;
} else {
return this.motion.parent.getDetailStateURL();
}
} }
return '../..'; return '../..';
} }

View File

@ -214,7 +214,7 @@
<mat-menu #motionListMenu="matMenu"> <mat-menu #motionListMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">
<div *ngIf="perms.isAllowed('change_metadata')"> <div *ngIf="perms.isAllowed('change_metadata') && selectedView === 'list'">
<button mat-menu-item (click)="toggleMultiSelect()"> <button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
<span translate>Multiselect</span> <span translate>Multiselect</span>
@ -265,7 +265,7 @@
</button> </button>
</div> </div>
<button mat-menu-item (click)="openExportDialog()"> <button mat-menu-item *ngIf="selectedView === 'list'" (click)="openExportDialog()">
<mat-icon>archive</mat-icon> <mat-icon>archive</mat-icon>
<span translate>Export</span> <span translate>Export</span>
</button> </button>

View File

@ -31,6 +31,14 @@ import { MotionSortListService } from 'app/site/motions/services/motion-sort-lis
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { MotionExportDialogComponent } from '../../../shared-motion/motion-export-dialog/motion-export-dialog.component'; import { MotionExportDialogComponent } from '../../../shared-motion/motion-export-dialog/motion-export-dialog.component';
/**
* Determine the types of the motionList
*/
type MotionListviewType = 'tiles' | 'list';
/**
* Tile information
*/
interface TileCategoryInformation { interface TileCategoryInformation {
filter: string; filter: string;
name: string; name: string;
@ -88,7 +96,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
/** /**
* String to define the current selected view. * String to define the current selected view.
*/ */
public selectedView: string; public selectedView: MotionListviewType;
/** /**
* Columns to display in table when desktop view is available * Columns to display in table when desktop view is available
@ -236,7 +244,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
this.categoryRepo.getViewModelListObservable().subscribe(cats => { this.categoryRepo.getViewModelListObservable().subscribe(cats => {
this.categories = cats; this.categories = cats;
if (cats.length > 0) { if (cats.length > 0) {
this.storage.get<string>('motionListView').then(savedView => { this.storage.get<string>('motionListView').then((savedView: MotionListviewType) => {
this.selectedView = savedView ? savedView : 'tiles'; this.selectedView = savedView ? savedView : 'tiles';
}); });
} else { } else {
@ -381,7 +389,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
* *
* @param value is the new view the user has selected. * @param value is the new view the user has selected.
*/ */
public onChangeView(value: string): void { public onChangeView(value: MotionListviewType): void {
this.selectedView = value; this.selectedView = value;
this.storage.set('motionListView', value); this.storage.set('motionListView', value);
} }