From 2f8ada257adcb2eb54ecb3517eff9381fa902f25 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Wed, 3 Apr 2019 12:14:52 -0400 Subject: [PATCH] LP1823041 Angular dialogs return observables Dialog.open() now returns an observable to the caller. This allows dialogs to pass 0 or more success events, error events, and close events each as descrete actions to the caller. Existing dialogs are updated to expect an Observable response to .open(). Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg --- .../share/accesskey/accesskey-info.component.html | 3 +- .../src/app/share/dialog/confirm.component.html | 7 +- .../eg2/src/app/share/dialog/dialog.component.ts | 115 +++++++++++++-------- .../src/app/share/dialog/progress.component.html | 2 +- .../eg2/src/app/share/dialog/prompt.component.html | 5 +- .../app/share/fm-editor/fm-editor.component.html | 10 +- .../src/app/share/fm-editor/fm-editor.component.ts | 14 +-- .../share/grid/grid-column-config.component.html | 3 +- .../workstations/workstations.component.ts | 10 +- .../staff/cat/vandelay/match-set-list.component.ts | 12 +-- .../src/app/staff/cat/vandelay/queue.component.ts | 45 ++++---- .../record/part-merge-dialog.component.html | 5 +- .../app/staff/catalog/record/parts.component.ts | 16 +-- .../eg2/src/app/staff/sandbox/sandbox.component.ts | 13 +-- .../staff/share/admin-page/admin-page.component.ts | 109 +++++++++++++++++-- .../share/buckets/bucket-dialog.component.html | 3 +- .../staff/share/op-change/op-change.component.html | 5 +- .../staff/share/translate/translate.component.html | 5 +- Open-ILS/src/eg2/tsconfig.json | 3 +- 19 files changed, 233 insertions(+), 152 deletions(-) diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html index 82ed72a4b0..ae584cec40 100644 --- a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html +++ b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html @@ -2,8 +2,7 @@ diff --git a/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html b/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html index 21766cac09..3db73cc8e0 100644 --- a/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html +++ b/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html @@ -2,16 +2,15 @@ diff --git a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts index e17fe8dcc7..79a5c8605d 100644 --- a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts +++ b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts @@ -1,19 +1,32 @@ import {Component, Input, OnInit, ViewChild, TemplateRef, EventEmitter} from '@angular/core'; +import {Observable, Observer} from 'rxjs'; import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap'; /** * Dialog base class. Handles the ngbModal logic. * Sub-classed component templates must have a #dialogContent selector * at the root of the template (see ConfirmDialogComponent). + * + * Dialogs interact with the caller via Observable. + * + * dialog.open().subscribe( + * value => handleValue(value), + * error => handleError(error), + * () => console.debug('dialog closed') + * ); + * + * It is up to the dialog implementer to decide what values to + * pass to the caller via the dialog.respond(data) and/or + * dialog.close(data) methods. + * + * dialog.close(...) closes the modal window and completes the + * observable, unless an error was previously passed, in which + * case the observable is already complete. + * + * dialog.close() with no data closes the dialog without passing + * any values to the caller. */ -export interface DialogRejectionResponse { - // Did the user simply close the dialog without performing an action. - dismissed?: boolean; - // Relays error, etc. messages from the dialog handler to the caller. - message?: string; -} - @Component({ selector: 'eg-dialog', template: '' @@ -32,6 +45,9 @@ export class DialogComponent implements OnInit { // called in the overridding method. onOpen$ = new EventEmitter(); + // How we relay responses to the caller. + observer: Observer; + // The modalRef allows direct control of the modal instance. private modalRef: NgbModalRef = null; @@ -41,11 +57,11 @@ export class DialogComponent implements OnInit { this.onOpen$ = new EventEmitter(); } - async open(options?: NgbModalOptions): Promise { + open(options?: NgbModalOptions): Observable { if (this.modalRef !== null) { - console.warn('Dismissing existing dialog'); - this.dismiss(); + this.error('Dialog was replaced!'); + this.finalize(); } this.modalRef = this.modalService.open(this.dialogContent, options); @@ -55,49 +71,62 @@ export class DialogComponent implements OnInit { setTimeout(() => this.onOpen$.emit(true)); } - return new Promise( (resolve, reject) => { + return new Observable(observer => { + this.observer = observer; this.modalRef.result.then( - (result) => { - resolve(result); - this.modalRef = null; - }, - - (result) => { - // NgbModal creates some result values for us, which - // are outside of our control. Other dismissal - // reasons are agreed upon by implementing subclasses. - console.debug('dialog closed with ' + result); - - const dismissed = ( - result === 0 // body click - || result === 1 // Esc key - || result === 'canceled' // Cancel button - || result === 'cross_click' // modal top-right X - ); - - const rejection: DialogRejectionResponse = { - dismissed: dismissed, - message: result - }; - - reject(rejection); - this.modalRef = null; - } + // Results are relayed to the caller via our observer. + // Our Observer is marked complete via this.close(). + // Nothing to do here. + result => {}, + + // Modal was dismissed via UI control which + // bypasses call to this.close() + dismissed => this.finalize() ); }); } - close(reason?: any): void { - if (this.modalRef) { - this.modalRef.close(reason); + // Send a response to the caller without closing the dialog. + respond(value: any) { + if (this.observer && value !== undefined) { + this.observer.next(value); + } + } + + // Sends error event to the caller and closes the dialog. + // Once an error is sent, our observable is complete and + // cannot be used again to send any messages. + error(value: any, close?: boolean) { + if (this.observer) { + console.error('Dialog produced error', value); + this.observer.error(value); + this.observer = null; } + if (this.modalRef) { this.modalRef.close(); } + this.finalize(); + } + + // Close the dialog, optionally with a value to relay to the caller. + // Calling close() with no value simply dismisses the dialog. + close(value?: any) { + this.respond(value); + if (this.modalRef) { this.modalRef.close(); } + this.finalize(); + } + + dismiss() { + console.warn('Dialog.dismiss() is deprecated. Use close() instead'); + this.close(); } - dismiss(reason?: any): void { - if (this.modalRef) { - this.modalRef.dismiss(reason); + // Clean up after closing the dialog. + finalize() { + if (this.observer) { // null if this.error() called + this.observer.complete(); + this.observer = null; } + this.modalRef = null; } } diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html index 78ca3d0c95..c1fdf20c0f 100644 --- a/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html +++ b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html @@ -3,7 +3,7 @@ diff --git a/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html b/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html index 1d7936b176..17a6b50726 100644 --- a/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html +++ b/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html @@ -2,8 +2,7 @@ @@ -17,6 +16,6 @@ + (click)="close()" i18n>Cancel diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html index 1b0935fb0a..5db749af35 100644 --- a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html +++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html @@ -5,13 +5,18 @@ diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/parts.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/parts.component.ts index 3ab8e8f8e9..2f59374084 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/parts.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/parts.component.ts @@ -81,10 +81,8 @@ export class PartsComponent implements OnInit { (part: IdlObject) => { this.editDialog.mode = 'update'; this.editDialog.recId = part.id(); - this.editDialog.open().then( - ok => this.partsGrid.reload(), - err => {} - ); + this.editDialog.open() + .subscribe(ok => this.partsGrid.reload()); } ); @@ -95,10 +93,7 @@ export class PartsComponent implements OnInit { this.editDialog.record = part; this.editDialog.mode = 'create'; - this.editDialog.open().then( - ok => this.partsGrid.reload(), - err => {} - ); + this.editDialog.open().subscribe(ok => this.partsGrid.reload()); }; this.deleteSelected = (parts: IdlObject[]) => { @@ -113,10 +108,7 @@ export class PartsComponent implements OnInit { this.mergeSelected = (parts: IdlObject[]) => { if (parts.length < 2) { return; } this.mergeDialog.parts = parts; - this.mergeDialog.open().then( - ok => this.partsGrid.reload(), - err => console.debug('Dialog dismissed') - ); + this.mergeDialog.open().subscribe(ok => this.partsGrid.reload()); }; } } diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts index 6d4e2eaafc..de94b5ef74 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts @@ -158,15 +158,10 @@ export class SandboxComponent implements OnInit { } openEditor() { - this.fmRecordEditor.open({size: 'lg'}).then( - ok => { console.debug(ok); }, - err => { - if (err && err.dismissed) { - console.debug('dialog was dismissed'); - } else { - console.error(err); - } - } + this.fmRecordEditor.open({size: 'lg'}).subscribe( + pcrudResult => console.debug('Record editor performed action'), + err => console.error(err), + () => console.debug('Dialog closed') ); } diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts index 509f95cce6..2faf8f9d1d 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts @@ -171,6 +171,91 @@ export class AdminPageComponent implements OnInit { this.grid.onRowActivate.subscribe( (idlThing: IdlObject) => this.showEditDialog(idlThing) ); + + this.editSelected = (idlThings: IdlObject[]) => { + + // Edit each IDL thing one at a time + const editOneThing = (thing: IdlObject) => { + if (!thing) { return; } + + this.showEditDialog(thing).then( + () => editOneThing(idlThings.shift())); + }; + + editOneThing(idlThings.shift()); + }; + + this.createNew = () => { + this.editDialog.mode = 'create'; + // We reuse the same editor for all actions. Be sure + // create action does not try to modify an existing record. + this.editDialog.recId = null; + this.editDialog.record = null; + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.createString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + }, + error => { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + ); + }; + + this.deleteSelected = (idlThings: IdlObject[]) => { + idlThings.forEach(idlThing => idlThing.isdeleted(true)); + this.pcrud.autoApply(idlThings).subscribe( + val => console.debug('deleted: ' + val), + err => {}, + () => this.grid.reload() + ); + }; + + // Open the field translation dialog. + // Link the next/previous actions to cycle through each translatable + // field on each row. + this.translate = () => { + this.translateRowIdx = 0; + this.translateFieldIdx = 0; + this.translator.fieldName = this.translatableFields[this.translateFieldIdx]; + this.translator.idlObject = this.dataSource.data[this.translateRowIdx]; + + this.translator.nextString = () => { + + if (this.translateFieldIdx < this.translatableFields.length - 1) { + this.translateFieldIdx++; + + } else if (this.translateRowIdx < this.dataSource.data.length - 1) { + this.translateRowIdx++; + this.translateFieldIdx = 0; + } + + this.translator.idlObject = + this.dataSource.data[this.translateRowIdx]; + this.translator.fieldName = + this.translatableFields[this.translateFieldIdx]; + }; + + this.translator.prevString = () => { + + if (this.translateFieldIdx > 0) { + this.translateFieldIdx--; + + } else if (this.translateRowIdx > 0) { + this.translateRowIdx--; + this.translateFieldIdx = 0; + } + + this.translator.idlObject = + this.dataSource.data[this.translateRowIdx]; + this.translator.fieldName = + this.translatableFields[this.translateFieldIdx]; + }; + + this.translator.open({size: 'lg'}); + }; } checkCreatePerms() { @@ -262,22 +347,24 @@ export class AdminPageComponent implements OnInit { return this.contextOrg && this.contextOrg.children().length === 0; } - showEditDialog(idlThing: IdlObject) { + showEditDialog(idlThing: IdlObject): Promise { this.editDialog.mode = 'update'; this.editDialog.recId = idlThing[this.pkeyField](); - return this.editDialog.open({size: this.dialogSize}).then( - ok => { - this.successString.current() - .then(str => this.toast.success(str)); - this.grid.reload(); - }, - rejection => { - if (!rejection.dismissed) { + return new Promise((resolve, reject) => { + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + resolve(result); + }, + error => { this.updateFailedString.current() .then(str => this.toast.danger(str)); + reject(error); } - } - ); + ); + }); } editSelected(idlThings: IdlObject[]) { diff --git a/Open-ILS/src/eg2/src/app/staff/share/buckets/bucket-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/buckets/bucket-dialog.component.html index 32b6e2ec74..2c595487f6 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/buckets/bucket-dialog.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/buckets/bucket-dialog.component.html @@ -10,8 +10,7 @@ Add Records from queue #{{fromBibQueue}} to Bucket diff --git a/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html b/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html index e5a6f493b5..d472202999 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html @@ -2,8 +2,7 @@ @@ -60,6 +59,6 @@ diff --git a/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html index 7aa59b46c3..61b9cb4f90 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html @@ -4,8 +4,7 @@ {{idlClassDef.label}} @@ -58,6 +57,6 @@ - + diff --git a/Open-ILS/src/eg2/tsconfig.json b/Open-ILS/src/eg2/tsconfig.json index 14a504dc91..157d6e6722 100644 --- a/Open-ILS/src/eg2/tsconfig.json +++ b/Open-ILS/src/eg2/tsconfig.json @@ -18,7 +18,8 @@ ], "lib": [ "es2017", - "dom" + "dom", + "es2018.promise" ] } } -- 2.11.0