<div class="modal-header bg-info">
<h4 class="modal-title" i18n>Access Key Assignments</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-header bg-info">
<h4 class="modal-title">{{dialogTitle}}</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body"><p>{{dialogBody}}</p></div>
<div class="modal-footer">
<button type="button" class="btn btn-success"
- (click)="close('confirmed')" i18n>Confirm</button>
+ (click)="close(true)" i18n>Confirm</button>
<button type="button" class="btn btn-warning"
- (click)="dismiss('canceled')" i18n>Cancel</button>
+ (click)="close(false)" i18n>Cancel</button>
</div>
</ng-template>
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: '<ng-template></ng-template>'
// called in the overridding method.
onOpen$ = new EventEmitter<any>();
+ // How we relay responses to the caller.
+ observer: Observer<any>;
+
// The modalRef allows direct control of the modal instance.
private modalRef: NgbModalRef = null;
this.onOpen$ = new EventEmitter<any>();
}
- async open(options?: NgbModalOptions): Promise<any> {
+ open(options?: NgbModalOptions): Observable<any> {
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);
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;
}
}
<h4 *ngIf="dialogTitle" class="modal-title">{{dialogTitle}}</h4>
<button type="button" class="close"
i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-header bg-info">
<h4 class="modal-title">{{dialogTitle}}</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<button type="button" class="btn btn-success"
(click)="close(promptValue)" i18n>Confirm</button>
<button type="button" class="btn btn-warning"
- (click)="dismiss('canceled')" i18n>Cancel</button>
+ (click)="close()" i18n>Cancel</button>
</div>
</ng-template>
<div class="modal-header bg-info">
<h4 class="modal-title" i18n>Record Editor: {{recordLabel}}</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form #fmEditForm="ngForm" role="form" class="form-validated common-form striped-odd">
+ <ng-container *ngIf="!record">
+ <!-- display a progress dialog while the editor
+ fetches the needed data -->
+ <eg-progress-inline></eg-progress-inline>
+ </ng-container>
+ <ng-container *ngIf="record">
<div class="form-group row" *ngFor="let field of fields">
<div class="col-lg-3">
<label for="{{idPrefix}}-{{field.name}}">{{field.label}}</label>
</a>
</div>
</div>
+ </ng-container>
</form>
</div>
<div class="modal-footer">
import {AuthService} from '@eg/core/auth.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {DialogComponent} from '@eg/share/dialog/dialog.component';
-import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
// Add some randomness to the generated DOM IDs to ensure against clobbering
this.idPrefix = 'fm-editor-' + Math.floor(Math.random() * 100000);
- }
- // Opening dialog, fetch data.
- open(options?: NgbModalOptions): Promise<any> {
- return this.initRecord().then(
- ok => super.open(options),
- err => console.warn(`Error fetching FM data: ${err}`)
- );
+ this.onOpen$.subscribe(() => this.initRecord());
}
// Set the record value and clear the recId value to
this.convertDatatypesToIdl(recToSave);
this.pcrud[this.mode]([recToSave]).toPromise().then(
result => this.close(result),
- error => this.dismiss(error)
+ error => this.error(error)
);
}
cancel() {
- this.dismiss('canceled');
+ this.close();
}
// Returns a string describing the type of input to display
<div class="modal-header bg-info">
<h4 class="modal-title" i18n>Grid Columns Configuration</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
private handleCollision(): Promise<number> {
return new Promise((resolve, reject) => {
- this.wsExistsDialog.open()
- .then(
- confirmed => {
+ this.wsExistsDialog.open().subscribe(override => {
+ if (override) {
this.registerWorkstationApi(true).then(
wsId => resolve(wsId),
notOk => reject(notOk)
);
- },
- dismissed => reject(dismissed)
- );
+ }
+ });
});
}
this.createNew = () => {
this.editDialog.mode = 'create';
- this.editDialog.open({size: 'lg'}).then(
- ok => this.grid.reload(),
- err => {}
- );
+ this.editDialog.open({size: 'lg'})
+ .subscribe(() => this.grid.reload());
};
this.deleteSelected = (matchSets: IdlObject[]) => {
(matchSet: IdlObject) => {
this.editDialog.mode = 'update';
this.editDialog.recId = matchSet.id();
- this.editDialog.open({size: 'lg'}).then(
- ok => this.grid.reload(),
- err => {}
- );
+ this.editDialog.open({size: 'lg'})
+ .subscribe(() => this.grid.reload());
}
);
}
}
deleteQueue() {
- this.confirmDelDlg.open().then(
- yes => {
- this.progressDlg.open();
- return this.net.request(
- 'open-ils.vandelay',
- `open-ils.vandelay.${this.qtypeShort()}_queue.delete`,
- this.auth.token(), this.queueId
- ).toPromise();
- },
- no => {
- this.progressDlg.close();
- return Promise.reject('delete failed');
- }
- ).then(
- resp => {
- this.progressDlg.close();
- const e = this.evt.parse(resp);
- if (e) {
- console.error(e);
- alert(e);
- } else {
+
+ this.confirmDelDlg.open().subscribe(confirmed => {
+ if (!confirmed) { return; }
+
+ this.progressDlg.open();
+ this.net.request(
+ 'open-ils.vandelay',
+ `open-ils.vandelay.${this.qtypeShort()}_queue.delete`,
+ this.auth.token(), this.queueId
+ ).toPromise().then(
+ resp => {
+ const e = this.evt.parse(resp);
+ if (e) { return new Error(e.toString()); }
+
// Jump back to the main queue page.
this.router.navigate(['/staff/cat/vandelay/queue']);
- }
- },
- err => {
- this.progressDlg.close();
- }
- );
+ },
+ err => console.error('queue deletion failed!', err)
+ ).finally(() => this.progressDlg.close());
+ });
}
exportNonImported() {
<div class="modal-header bg-info">
<h4 class="modal-title" i18n>Merge Monograph Parts</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<button type="button" class="btn btn-success"
(click)="mergeParts()" i18n>Merge</button>
<button type="button" class="btn btn-warning"
- (click)="dismiss('canceled')" i18n>Cancel</button>
+ (click)="close()" i18n>Cancel</button>
</div>
</ng-template>
(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());
}
);
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[]) => {
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());
};
}
}
}
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')
);
}
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() {
return this.contextOrg && this.contextOrg.children().length === 0;
}
- showEditDialog(idlThing: IdlObject) {
+ showEditDialog(idlThing: IdlObject): Promise<any> {
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[]) {
<span *ngIf="fromBibQueue" i18n>Add Records from queue #{{fromBibQueue}} to Bucket</span>
</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-header bg-info">
<h4 class="modal-title" i18n>Change Operator</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
</div>
<div class="modal-footer">
<button (click)="login()" class="btn btn-info" i18n>OK/Continue</button>
- <button (click)="dismiss('canceled')" class="btn btn-warning ml-2" i18n>Cancel</button>
+ <button (click)="close()" class="btn btn-warning ml-2" i18n>Cancel</button>
</div>
</ng-template>
{{idlClassDef.label}}
</h4>
<button type="button" class="close"
- i18n-aria-label aria-label="Close"
- (click)="dismiss('cross_click')">
+ i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<button *ngIf="nextString" (click)="nextString()"
class="btn btn-info mr-3" i18n>Next String</button>
<button (click)="translate()" class="btn btn-info" i18n>Apply</button>
- <button (click)="dismiss('canceled')" class="btn btn-warning ml-2" i18n>Cancel</button>
+ <button (click)="close()" class="btn btn-warning ml-2" i18n>Cancel</button>
</div>
</ng-template>
],
"lib": [
"es2017",
- "dom"
+ "dom",
+ "es2018.promise"
]
}
}