LP1840050 Modularize various standalone components + more.
authorBill Erickson <berickxx@gmail.com>
Fri, 16 Aug 2019 21:00:11 +0000 (17:00 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 16 Aug 2019 21:20:22 +0000 (17:20 -0400)
Create container modules for the String, Translate, FM record editor, and
Admin Page components & services.  This simplifies imports and allows us
to avoid requiring these modules on pages that don't need them.  In
particular, the staff splash page now loads fewer imports, which should
improve initial load/login time.

Additionally some components were enhanced.

FM record editor now has a eg-fm-record-editor-action component so users
can pass in an action, rendered as a button at the bottom of the editor.

FM record editor gets a delete record option and hideBanner option.

FM record editor now better handles real-time updates of its underlying
recordId and record values, including updates to some editor callers to
migrate to the modified API (replace recId with recordId).

Signed-off-by: Bill Erickson <berickxx@gmail.com>
22 files changed:
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor-action.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/org-family-select/org-family-select.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/string/string.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/translate/translate.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/translate/translate.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/translate/translate.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/common.module.ts
Open-ILS/src/eg2/src/app/staff/cat/vandelay/match-set-list.component.ts
Open-ILS/src/eg2/src/app/staff/cat/vandelay/vandelay.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/record/parts.component.ts
Open-ILS/src/eg2/src/app/staff/common.module.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts
Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts
Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html [deleted file]
Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts [deleted file]

diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor-action.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor-action.component.ts
new file mode 100644 (file)
index 0000000..298856d
--- /dev/null
@@ -0,0 +1,31 @@
+import {Component, Input, Output, EventEmitter, Host, OnInit} from '@angular/core';
+import {FmRecordEditorComponent} from './fm-editor.component';
+
+@Component({
+  selector: 'eg-fm-record-editor-action',
+  template: '<ng-template></ng-template>' // no-op
+})
+
+export class FmRecordEditorActionComponent implements OnInit {
+
+    // unique identifier
+    @Input() key: string;
+
+    @Input() label: string;
+
+    @Input() buttonCss = 'btn-outline-dark';
+
+    // Emits the 'key' of the clicked action.
+    @Output() actionClick: EventEmitter<string>;
+
+    @Input() disabled: boolean;
+
+    constructor(@Host() private editor: FmRecordEditorComponent) {
+        this.actionClick = new EventEmitter<string>();
+    }
+
+    ngOnInit() {
+        this.editor.actions.push(this);
+    }
+}
+
index fc11eee..afae0ef 100644 (file)
@@ -4,8 +4,13 @@
 <eg-string #successStr text="Update Succeeded" i18n-text></eg-string>
 <eg-string #failStr text="Update Failed" i18n-text></eg-string>
 
+<eg-confirm-dialog #confirmDel
+  dialogTitle="Delete?" i18n-dialogTitle
+  dialogBody="Delete {{recordLabel}}?" i18n-dialogBody>
+</eg-confirm-dialog>
+
 <ng-template #dialogContent>
-  <div class="modal-header bg-info">
+  <div class="modal-header bg-info" *ngIf="!hideBanner">
     <h4 class="modal-title" i18n>Record Editor: {{recordLabel}}</h4>
     <ng-container *ngIf="isDialog()">
       <button type="button" class="close" 
@@ -67,7 +72,7 @@
                 [limitPerms]="modePerms[mode]"
                 [readOnly]="field.readOnly"
                 [applyDefault]="field.orgDefaultAllowed"
-                [initialOrgId]="record[field.name]()"
+                [applyOrgId]="record[field.name]()"
                 (onChange)="record[field.name]($event)">
               </eg-org-select>
             </ng-container>
                 [required]="field.isRequired()"
                 [entries]="field.linkedValues"
                 [asyncDataSource]="field.linkedValuesSource"
-                [startId]="record[field.name]()"
+                [selectedId]="record[field.name]()"
                 (onChange)="record[field.name]($event ? $event.id : null)">
               </eg-combobox>
             </ng-container>
     </form>
   </div>
   <div class="modal-footer">
+    <button type="button" class="btn {{action.buttonCss}}"
+      *ngFor="let action of actions" [disabled]="action.disabled"
+      (click)="action.actionClick.emit({action: action.key, record: record})">
+      {{action.label}}
+    </button>
     <ng-container *ngIf="isDialog()">
       <button type="button" class="btn btn-success" *ngIf="mode == 'view'"
         (click)="close()" i18n>Close</button>
       <button type="button" class="btn btn-warning ml-2" *ngIf="mode != 'view'"
         (click)="cancel()" i18n>Cancel</button>
     </ng-container>
+
+    <ng-container *ngIf="showDelete && mode != 'view'">
+      <button type="button" class="btn btn-warning" (click)="remove()"
+        [disabled]="record && record.isnew()" i18n>Delete</button>
+    </ng-container>
+
     <button type="button" class="btn btn-info" 
       [disabled]="fmEditForm.invalid" *ngIf="mode != 'view'"
       (click)="save()" i18n>Save</button>
index a574e54..aa65a93 100644 (file)
@@ -10,7 +10,9 @@ import {ToastService} from '@eg/share/toast/toast.service';
 import {StringComponent} from '@eg/share/string/string.component';
 import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
-import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
+import {TranslateComponent} from '@eg/share/translate/translate.component';
+import {FmRecordEditorActionComponent} from './fm-editor-action.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 
 
 interface CustomFieldTemplate {
@@ -82,11 +84,6 @@ export class FmRecordEditorComponent
     // IDL class hint (e.g. "aou")
     @Input() idlClass: string;
 
-    recId: any;
-
-    // IDL record we are editing
-    record: IdlObject;
-
     // Permissions extracted from the permacrud defs in the IDL
     // for the current IDL class
     modePerms: {[mode: string]: string};
@@ -123,18 +120,26 @@ export class FmRecordEditorComponent
     // Display within a modal dialog window or inline in the page.
     @Input() displayMode: 'dialog' | 'inline' = 'dialog';
 
+    // Hide the top 'Record Editor: ...' banner.  Primarily useful
+    // for displayMode === 'inline'
+    @Input() hideBanner: boolean;
+
+    // Emit the modified object when the save action completes.
+    @Output() recordSaved = new EventEmitter<IdlObject>();
+
     // Emit the modified object when the save action completes.
-    @Output() onSave$ = new EventEmitter<IdlObject>();
+    @Output() recordDeleted = new EventEmitter<IdlObject>();
 
     // Emit the original object when the save action is canceled.
-    @Output() onCancel$ = new EventEmitter<IdlObject>();
+    @Output() recordCanceled = new EventEmitter<IdlObject>();
 
     // Emit an error message when the save action fails.
-    @Output() onError$ = new EventEmitter<string>();
+    @Output() recordError = new EventEmitter<string>();
 
     @ViewChild('translator') private translator: TranslateComponent;
     @ViewChild('successStr') successStr: StringComponent;
     @ViewChild('failStr') failStr: StringComponent;
+    @ViewChild('confirmDel') confirmDel: ConfirmDialogComponent;
 
     // IDL info for the the selected IDL class
     idlDef: any;
@@ -154,12 +159,60 @@ export class FmRecordEditorComponent
     //       'view' for viewing an existing record without editing
     @Input() mode: 'create' | 'update' | 'view' = 'create';
 
-    // Record ID to view/update.  Value is dynamic.  Records are not
-    // fetched until .open() is called.
+    // recordId and record getters and setters.
+    // Note that setting the this.recordId to NULL does not clear the
+    // current value of this.record and vice versa.  Only viable data
+    // is actionable.  This allows the caller to use both @Input()'s
+    // without each clobbering the other.
+
+    // Record ID to view/update.
+    _recordId: any = null;
     @Input() set recordId(id: any) {
-        if (id) { this.recId = id; }
+        if (id) {
+            if (id !== this._recordId) {
+                this._recordId = id;
+                this._record = null; // force re-fetch
+                this.handleRecordChange();
+            }
+        } else {
+            this._recordId = null;
+        }
     }
 
+    get recordId(): any {
+        return this._recordId;
+    }
+
+    // IDL record we are editing
+    _record: IdlObject = null;
+    @Input() set record(r: IdlObject) {
+        if (r) {
+            if (!this.idl.pkeyMatches(this.record, r)) {
+                this._record = r;
+                this._recordId = null; // avoid mismatch
+                this.handleRecordChange();
+            }
+        } else {
+            this._record = null;
+        }
+    }
+
+    get record(): IdlObject {
+        return this._record;
+    }
+
+    actions: FmRecordEditorActionComponent[] = [];
+
+    initDone: boolean;
+
+    // Comma-separated list of field names defining the order in which
+    // fields should be rendered in the form.  Any fields not represented
+    // will be rendered alphabetically by label after the named fields.
+    @Input() fieldOrder: string;
+
+    // When true, show a delete button and support delete operations.
+    @Input() showDelete: boolean;
+
     constructor(
       private modal: NgbModal, // required for passing to parent
       private idl: IdlService,
@@ -184,17 +237,27 @@ export class FmRecordEditorComponent
         } else {
             this.initRecord();
         }
+        this.initDone = true;
+    }
+
+    // If the record ID changes after ngOnInit has been called
+    // and we're using displayMode=inline, force the data to
+    // resync in real time
+    handleRecordChange() {
+        if (this.initDone && !this.isDialog()) {
+            this.initRecord();
+        }
     }
 
     isDialog(): boolean {
         return this.displayMode === 'dialog';
     }
 
-    // Set the record value and clear the recId value to
-    // indicate the record is our current source of data.
+    // DEPRECATED: This is a duplicate of this.record = abc;
     setRecord(record: IdlObject) {
-        this.record = record;
-        this.recId = null;
+        console.warn('fm-editor:setRecord() is deprecated. ' +
+            'Use editor.record = abc or [record]="abc" instead');
+        this.record = record; // this calls the setter
     }
 
     // Translate comma-separated string versions of various inputs
@@ -228,21 +291,25 @@ export class FmRecordEditorComponent
         if (this.mode === 'update' || this.mode === 'view') {
 
             let promise;
-            if (this.record && this.recId === null) {
+            if (this.record && this.recordId === null) {
                 promise = Promise.resolve(this.record);
-            } else {
+            } else if (this.recordId) {
                 promise =
-                    this.pcrud.retrieve(this.idlClass, this.recId).toPromise();
+                    this.pcrud.retrieve(this.idlClass, this.recordId).toPromise();
+            } else {
+                // Not enough data yet to fetch anything
+                return Promise.resolve();
             }
 
             return promise.then(rec => {
 
                 if (!rec) {
                     return Promise.reject(`No '${this.idlClass}'
-                        record found with id ${this.recId}`);
+                        record found with id ${this.recordId}`);
                 }
 
-                this.record = rec;
+                // Set this._record (not this.record) to avoid loop in initRecord()
+                this._record = rec;
                 this.convertDatatypesToJs();
                 return this.getFieldList();
             });
@@ -252,7 +319,9 @@ export class FmRecordEditorComponent
         //
         // Create a new record from the stub record provided by the
         // caller or a new from-scratch record
-        this.setRecord(this.record || this.idl.create(this.idlClass));
+        // Set this._record (not this.record) to avoid loop in initRecord()
+        this._record = this.record || this.idl.create(this.idlClass);
+        this._recordId = null; // avoid future confusion
 
         return this.getFieldList();
     }
@@ -274,7 +343,8 @@ export class FmRecordEditorComponent
     // Modifies the provided FM record in place, replacing JS values
     // with IDL-compatible values.
     convertDatatypesToIdl(rec: IdlObject) {
-        const fields = this.idlDef.fields;
+        const fields = this.idlDef.fields.filter(f => !f.virtual);
+
         fields.forEach(field => {
             if (field.datatype === 'bool') {
                 if (rec[field.name]() === true) {
@@ -318,13 +388,35 @@ export class FmRecordEditorComponent
 
     private getFieldList(): Promise<any> {
 
-        this.fields = this.idlDef.fields.filter(f =>
-            !f.virtual && !this.hiddenFieldsList.includes(f.name)
-        );
+        const fields = this.idlDef.fields.filter(f =>
+            !f.virtual && !this.hiddenFieldsList.includes(f.name));
 
         // Wait for all network calls to complete
         return Promise.all(
-            this.fields.map(field => this.constructOneField(field)));
+            fields.map(field => this.constructOneField(field))
+
+        ).then(() => {
+
+            if (!this.fieldOrder) {
+                this.fields = fields.sort((a, b) => a.label < b.label ? -1 : 1);
+                return;
+            }
+
+            let newList = [];
+            const ordered = this.fieldOrder.split(/,/);
+
+            ordered.forEach(name => {
+                const f1 = fields.filter(f2 => f2.name === name)[0];
+                if (f1) { newList.push(f1); }
+            });
+
+            // Sort remaining fields by label
+            const remainder = fields.filter(f => !ordered.includes(f.name));
+            remainder.sort((a, b) => a.label < b.label ? -1 : 1);
+            newList = newList.concat(remainder);
+
+            this.fields = newList;
+        });
     }
 
     private constructOneField(field: any): Promise<any> {
@@ -474,20 +566,39 @@ export class FmRecordEditorComponent
         this.convertDatatypesToIdl(recToSave);
         this.pcrud[this.mode]([recToSave]).toPromise().then(
             result => {
-                this.onSave$.emit(result);
+                this.recordSaved.emit(result);
                 this.successStr.current().then(msg => this.toast.success(msg));
                 if (this.isDialog()) { this.close(result); }
             },
             error => {
-                this.onError$.emit(error);
+                this.recordError.emit(error);
                 this.failStr.current().then(msg => this.toast.warning(msg));
                 if (this.isDialog()) { this.error(error); }
             }
         );
     }
 
+    remove() {
+        this.confirmDel.open().subscribe(confirmed => {
+            if (!confirmed) { return; }
+            const recToRemove = this.idl.clone(this.record);
+            this.pcrud.remove(recToRemove).toPromise().then(
+                result => {
+                    this.recordDeleted.emit(result);
+                    this.successStr.current().then(msg => this.toast.success(msg));
+                    if (this.isDialog()) { this.close(result); }
+                },
+                error => {
+                    this.recordError.emit(error);
+                    this.failStr.current().then(msg => this.toast.warning(msg));
+                    if (this.isDialog()) { this.error(error); }
+                }
+            );
+        });
+    }
+
     cancel() {
-        this.onCancel$.emit(this.record);
+        this.recordCanceled.emit(this.record);
         this.close();
     }
 
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.module.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.module.ts
new file mode 100644 (file)
index 0000000..6e9c5c3
--- /dev/null
@@ -0,0 +1,30 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
+import {StringModule} from '@eg/share/string/string.module';
+import {TranslateModule} from '@eg/share/translate/translate.module';
+import {FmRecordEditorComponent} from './fm-editor.component';
+import {FmRecordEditorActionComponent} from './fm-editor-action.component';
+
+
+@NgModule({
+    declarations: [
+        FmRecordEditorComponent,
+        FmRecordEditorActionComponent
+    ],
+    imports: [
+        EgCommonModule,
+        StringModule,
+        TranslateModule,
+        CommonWidgetsModule
+    ],
+    exports: [
+        FmRecordEditorComponent,
+        FmRecordEditorActionComponent
+    ],
+    providers: [
+    ]
+})
+
+export class FmRecordEditorModule { }
+
diff --git a/Open-ILS/src/eg2/src/app/share/org-family-select/org-family-select.module.ts b/Open-ILS/src/eg2/src/app/share/org-family-select/org-family-select.module.ts
new file mode 100644 (file)
index 0000000..ea5fc2f
--- /dev/null
@@ -0,0 +1,26 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {EgCoreModule} from '@eg/core/core.module';
+import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
+import {OrgFamilySelectComponent} from './org-family-select.component';
+import {ReactiveFormsModule} from '@angular/forms';
+
+@NgModule({
+    declarations: [
+        OrgFamilySelectComponent
+    ],
+    imports: [
+        EgCommonModule,
+        EgCoreModule,
+        CommonWidgetsModule,
+        ReactiveFormsModule
+    ],
+    exports: [
+        OrgFamilySelectComponent
+    ],
+    providers: [
+    ]
+})
+
+export class OrgFamilySelectModule { }
+
diff --git a/Open-ILS/src/eg2/src/app/share/string/string.module.ts b/Open-ILS/src/eg2/src/app/share/string/string.module.ts
new file mode 100644 (file)
index 0000000..185ede1
--- /dev/null
@@ -0,0 +1,25 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {EgCoreModule} from '@eg/core/core.module';
+import {StringComponent} from '@eg/share/string/string.component';
+import {StringService} from '@eg/share/string/string.service';
+
+
+@NgModule({
+    declarations: [
+        StringComponent
+    ],
+    imports: [
+        EgCommonModule,
+        EgCoreModule
+    ],
+    exports: [
+        StringComponent
+    ],
+    providers: [
+        StringService
+    ]
+})
+
+export class StringModule { }
+
diff --git a/Open-ILS/src/eg2/src/app/share/translate/translate.component.html b/Open-ILS/src/eg2/src/app/share/translate/translate.component.html
new file mode 100644 (file)
index 0000000..61b9cb4
--- /dev/null
@@ -0,0 +1,62 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>
+      {{idlClassDef.label}}
+    </h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body form-common form-validated" *ngIf="idlObj">
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Field Name</label>
+      <input 
+        type="text" 
+        [disabled]="true"
+        class="form-control col-lg-7"
+        value="{{idlClassDef.field_map[field].label}}">
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Current Value</label>
+      <input 
+        type="text" 
+        [disabled]="true"
+        class="form-control col-lg-7"
+        value="{{idlObj[field]()}}">
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Select Locale</label>
+      <select class="form-control col-lg-7" 
+        (change)="localeChanged($event)"
+        [(ngModel)]="selectedLocale">
+        <option value="{{locale.code()}}" *ngFor="let locale of locales">
+          {{locale.name()}}
+        </option>
+      </select>
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" i18n>Translation</label>
+      <input 
+        id='translation-input'
+        type="text" 
+        class="form-control col-lg-7"
+        required
+        i18n-placeholder
+        (keyup.enter)="translate()"
+        placeholder="Translation..." 
+        [(ngModel)]="translatedValue"/>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button *ngIf="prevString" (click)="prevString()" 
+      class="btn btn-info" i18n>Prev String</button>
+    <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)="close()" class="btn btn-warning ml-2" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/translate/translate.component.ts b/Open-ILS/src/eg2/src/app/share/translate/translate.component.ts
new file mode 100644 (file)
index 0000000..4880973
--- /dev/null
@@ -0,0 +1,145 @@
+import {Component, OnInit, Input, Renderer2} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {LocaleService} from '@eg/core/locale.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+  selector: 'eg-translate',
+  templateUrl: 'translate.component.html'
+})
+
+export class TranslateComponent
+    extends DialogComponent implements OnInit {
+
+    idlClassDef: any;
+    locales: IdlObject[];
+    selectedLocale: string;
+    translatedValue: string;
+    existingTranslation: IdlObject;
+
+    // These actions should update the idlObject and/or fieldName values,
+    // forcing the dialog to load a new string to translate.  When set,
+    // applying a translation in the dialog will leave the dialog window open
+    // so the next/prev buttons can be used to fetch the next string.
+    nextString: () => void;
+    prevString: () => void;
+
+    idlObj: IdlObject;
+    @Input() set idlObject(o: IdlObject) {
+        if (o) {
+            this.idlObj = o;
+            this.idlClassDef = this.idl.classes[o.classname];
+            this.fetchTranslation();
+        }
+    }
+
+    field: string;
+    @Input() set fieldName(n: string) {
+        this.field = n;
+    }
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private renderer: Renderer2,
+        private idl: IdlService,
+        private toast: ToastService,
+        private locale: LocaleService,
+        private pcrud: PcrudService,
+        private auth: AuthService) {
+        super(modal);
+    }
+
+    ngOnInit() {
+        // Default to the login locale
+        this.selectedLocale = this.locale.currentLocaleCode();
+        this.locales = [];
+        this.locale.supportedLocales().subscribe(l => this.locales.push(l));
+
+        this.onOpen$.subscribe(() => {
+            const elm = this.renderer.selectRootElement('#translation-input');
+            if (elm) {
+                elm.focus();
+                elm.select();
+            }
+        });
+    }
+
+    localeChanged(code: string) {
+        this.fetchTranslation();
+    }
+
+    fetchTranslation() {
+        const exist = this.existingTranslation;
+
+        if (exist
+            && exist.fq_field() === this.fqField()
+            && exist.identity_value() === this.identValue()) {
+            // Already have the current translation object.
+            return;
+        }
+
+        this.translatedValue = '';
+        this.existingTranslation = null;
+
+        this.pcrud.search('i18n', {
+            translation: this.selectedLocale,
+            fq_field : this.fqField(),
+            identity_value: this.identValue()
+        }).subscribe(tr => {
+            this.existingTranslation = tr;
+            this.translatedValue = tr.string();
+            console.debug('found existing translation ', tr);
+        });
+    }
+
+    fqField(): string {
+        return this.idlClassDef.classname + '.' + this.field;
+    }
+
+    identValue(): string {
+        return this.idlObj[this.idlClassDef.pkey || 'id']();
+    }
+
+    translate() {
+        if (!this.translatedValue) { return; }
+
+        let entry;
+
+        if (this.existingTranslation) {
+            entry = this.existingTranslation;
+            entry.string(this.translatedValue);
+
+            this.pcrud.update(entry).toPromise().then(
+                ok => {
+                    if (!this.nextString) {
+                        this.close(this.translatedValue);
+                    }
+                },
+                err => console.error(err)
+            );
+
+            return;
+        }
+
+        entry = this.idl.create('i18n');
+        entry.fq_field(this.fqField());
+        entry.identity_value(this.identValue());
+        entry.translation(this.selectedLocale);
+        entry.string(this.translatedValue);
+
+        this.pcrud.create(entry).toPromise().then(
+            ok => {
+                if (!this.nextString) {
+                    this.close(this.translatedValue);
+                }
+            },
+            err => console.error('Translation creation failed')
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/translate/translate.module.ts b/Open-ILS/src/eg2/src/app/share/translate/translate.module.ts
new file mode 100644 (file)
index 0000000..41dbd5d
--- /dev/null
@@ -0,0 +1,23 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {EgCoreModule} from '@eg/core/core.module';
+import {TranslateComponent} from './translate.component';
+
+
+@NgModule({
+    declarations: [
+        TranslateComponent
+    ],
+    imports: [
+        EgCommonModule,
+        EgCoreModule
+    ],
+    exports: [
+        TranslateComponent
+    ],
+    providers: [
+    ]
+})
+
+export class TranslateModule { }
+
index 5bd71d3..4bb5b30 100644 (file)
@@ -1,6 +1,9 @@
 import {NgModule} from '@angular/core';
 import {StaffCommonModule} from '@eg/staff/common.module';
 import {LinkTableComponent, LinkTableLinkComponent} from '@eg/staff/share/link-table/link-table.component';
+import {TranslateModule} from '@eg/share/translate/translate.module';
+import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module';
+import {AdminPageModule} from '@eg/staff/share/admin-page/admin-page.module';
 import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
 
 @NgModule({
@@ -10,10 +13,16 @@ import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.componen
     BasicAdminPageComponent
   ],
   imports: [
-    StaffCommonModule
+    StaffCommonModule,
+    TranslateModule,
+    FmRecordEditorModule,
+    AdminPageModule
   ],
   exports: [
     StaffCommonModule,
+    TranslateModule,
+    FmRecordEditorModule,
+    AdminPageModule,
     LinkTableComponent,
     LinkTableLinkComponent,
     BasicAdminPageComponent
index c33999a..8efe6b5 100644 (file)
@@ -59,7 +59,7 @@ export class MatchSetListComponent implements AfterViewInit {
         this.grid.onRowActivate.subscribe(
             (matchSet: IdlObject) => {
                 this.editDialog.mode = 'update';
-                this.editDialog.recId = matchSet.id();
+                this.editDialog.recordId = matchSet.id();
                 this.editDialog.open({size: 'lg'})
                     .subscribe(() => this.grid.reload());
             }
index 9bbfd46..0f39428 100644 (file)
@@ -1,8 +1,10 @@
 import {NgModule} from '@angular/core';
+import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module';
 import {StaffCommonModule} from '@eg/staff/common.module';
 import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
 import {HttpClientModule} from '@angular/common/http';
 import {TreeModule} from '@eg/share/tree/tree.module';
+import {AdminPageModule} from '@eg/staff/share/admin-page/admin-page.module';
 import {VandelayRoutingModule} from './routing.module';
 import {VandelayService} from './vandelay.service';
 import {VandelayComponent} from './vandelay.component';
@@ -48,6 +50,8 @@ import {RecentImportsComponent} from './recent-imports.component';
   imports: [
     TreeModule,
     StaffCommonModule,
+    FmRecordEditorModule,
+    AdminPageModule,
     CatalogCommonModule,
     VandelayRoutingModule,
     HttpClientModule,
index e0fbff8..4fc82a2 100644 (file)
@@ -1,4 +1,5 @@
 import {NgModule} from '@angular/core';
+import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module';
 import {StaffCommonModule} from '@eg/staff/common.module';
 import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
 import {CatalogRoutingModule} from './routing.module';
@@ -54,6 +55,7 @@ import {SearchTemplatesComponent} from './search-templates.component';
   ],
   imports: [
     StaffCommonModule,
+    FmRecordEditorModule,
     CatalogCommonModule,
     CatalogRoutingModule,
     HoldsModule,
index 2f59374..9766c7f 100644 (file)
@@ -80,7 +80,7 @@ export class PartsComponent implements OnInit {
         this.partsGrid.onRowActivate.subscribe(
             (part: IdlObject) => {
                 this.editDialog.mode = 'update';
-                this.editDialog.recId = part.id();
+                this.editDialog.recordId = part.id();
                 this.editDialog.open()
                     .subscribe(ok => this.partsGrid.reload());
             }
@@ -89,7 +89,7 @@ export class PartsComponent implements OnInit {
         this.createNew = () => {
 
             const part = this.idl.create('bmp');
-            part.record(this.recId);
+            part.record(this.recordId);
             this.editDialog.record = part;
 
             this.editDialog.mode = 'create';
index 66c62c3..a22df80 100644 (file)
@@ -4,21 +4,16 @@ import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
 import {AudioService} from '@eg/share/util/audio.service';
 import {GridModule} from '@eg/share/grid/grid.module';
 import {StaffBannerComponent} from './share/staff-banner.component';
-import {OrgFamilySelectComponent} from '@eg/share/org-family-select/org-family-select.component';
 import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive';
 import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
 import {AccessKeyInfoComponent} from '@eg/share/accesskey/accesskey-info.component';
 import {OpChangeComponent} from '@eg/staff/share/op-change/op-change.component';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {ToastComponent} from '@eg/share/toast/toast.component';
-import {StringComponent} from '@eg/share/string/string.component';
-import {StringService} from '@eg/share/string/string.service';
+import {StringModule} from '@eg/share/string/string.module';
 import {TitleComponent} from '@eg/share/title/title.component';
-import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 import {BucketDialogComponent} from '@eg/staff/share/buckets/bucket-dialog.component';
 import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
-import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
-import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
 import {EgHelpPopoverComponent} from '@eg/share/eg-help-popover/eg-help-popover.component';
 import {ReactiveFormsModule} from '@angular/forms';
 
@@ -29,22 +24,18 @@ import {ReactiveFormsModule} from '@angular/forms';
 @NgModule({
   declarations: [
     StaffBannerComponent,
-    OrgFamilySelectComponent,
     AccessKeyDirective,
     AccessKeyInfoComponent,
     ToastComponent,
-    StringComponent,
     TitleComponent,
     OpChangeComponent,
-    FmRecordEditorComponent,
     BucketDialogComponent,
     BibSummaryComponent,
-    TranslateComponent,
-    AdminPageComponent,
     EgHelpPopoverComponent
   ],
   imports: [
     EgCommonModule,
+    StringModule,
     ReactiveFormsModule,
     CommonWidgetsModule,
     GridModule
@@ -54,18 +45,14 @@ import {ReactiveFormsModule} from '@angular/forms';
     CommonWidgetsModule,
     GridModule,
     StaffBannerComponent,
-    OrgFamilySelectComponent,
     AccessKeyDirective,
     AccessKeyInfoComponent,
     ToastComponent,
-    StringComponent,
+    StringModule,
     TitleComponent,
     OpChangeComponent,
-    FmRecordEditorComponent,
     BucketDialogComponent,
     BibSummaryComponent,
-    TranslateComponent,
-    AdminPageComponent,
     EgHelpPopoverComponent
   ]
 })
@@ -77,7 +64,6 @@ export class StaffCommonModule {
             providers: [ // Export staff-wide services
                 AccessKeyService,
                 AudioService,
-                StringService,
                 ToastService
             ]
         };
index 2febd8e..f9a8bd3 100644 (file)
   <!-- note: fieldOptions would be best defined in the .ts file, but
       want to demostrate it can be set in the template as well -->
   <eg-fm-record-editor #fmRecordEditor 
-      idlClass="cmrcfld" mode="create" 
-      [fieldOptions]="{marc_record_type:{customValues:[{id:'biblio'},{id:'serial'},{id:'authority'}]},description:{customTemplate:{template:descriptionTemplate,context:{'hello':'goodbye'}}}}"
-      recordId="1" orgDefaultAllowed="owner">
+    idlClass="cmrcfld" mode="create" hiddenFields="id"
+    fieldOrder="owner,name,description,marc_format,marc_record_type,tag"
+    [fieldOptions]="{marc_record_type:{customValues:[{id:'biblio'},{id:'serial'},{id:'authority'}]},description:{customTemplate:{template:descriptionTemplate,context:{'hello':'goodbye'}}}}"
+    recordId="1" orgDefaultAllowed="owner">
   </eg-fm-record-editor>
   <button class="btn btn-dark" (click)="openEditor()">
       Fm Record Editor
index 740d4d1..efbca92 100644 (file)
@@ -365,7 +365,7 @@ export class SandboxComponent implements OnInit {
 
     showEditDialog(idlThing: IdlObject): Promise<any> {
         this.editDialog.mode = 'update';
-        this.editDialog.recId = idlThing['id']();
+        this.editDialog.recordId = idlThing['id']();
         return new Promise((resolve, reject) => {
             this.editDialog.open({size: 'lg'}).subscribe(
                 ok => {
index 0937ab0..a8db49a 100644 (file)
@@ -1,8 +1,11 @@
 import {NgModule} from '@angular/core';
+import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module';
 import {StaffCommonModule} from '@eg/staff/common.module';
+import {TranslateModule} from '@eg/share/translate/translate.module';
 import {SandboxRoutingModule} from './routing.module';
 import {SandboxComponent} from './sandbox.component';
 import {ReactiveFormsModule} from '@angular/forms';
+import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-select.module';
 
 @NgModule({
   declarations: [
@@ -10,6 +13,9 @@ import {ReactiveFormsModule} from '@angular/forms';
   ],
   imports: [
     StaffCommonModule,
+    TranslateModule,
+    FmRecordEditorModule,
+    OrgFamilySelectModule,
     SandboxRoutingModule,
     ReactiveFormsModule
   ],
index f920d7b..23d6520 100644 (file)
@@ -3,7 +3,7 @@ import {ActivatedRoute} from '@angular/router';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {GridDataSource} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
-import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
+import {TranslateComponent} from '@eg/share/translate/translate.component';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {Pager} from '@eg/share/util/pager';
 import {PcrudService} from '@eg/core/pcrud.service';
@@ -240,7 +240,7 @@ export class AdminPageComponent implements OnInit {
 
     showEditDialog(idlThing: IdlObject): Promise<any> {
         this.editDialog.mode = 'update';
-        this.editDialog.recId = idlThing[this.pkeyField]();
+        this.editDialog.recordId = idlThing[this.pkeyField]();
         return new Promise((resolve, reject) => {
             this.editDialog.open({size: this.dialogSize}).subscribe(
                 result => {
@@ -284,7 +284,7 @@ export class AdminPageComponent implements OnInit {
         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.recordId = null;
         this.editDialog.record = null;
         this.editDialog.open({size: this.dialogSize}).subscribe(
             ok => {
diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.module.ts b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.module.ts
new file mode 100644 (file)
index 0000000..ae94c9f
--- /dev/null
@@ -0,0 +1,33 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {EgCoreModule} from '@eg/core/core.module';
+import {GridModule} from '@eg/share/grid/grid.module';
+import {StringModule} from '@eg/share/string/string.module';
+import {TranslateModule} from '@eg/share/translate/translate.module';
+import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module';
+import {AdminPageComponent} from './admin-page.component';
+import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-select.module';
+
+
+@NgModule({
+    declarations: [
+        AdminPageComponent
+    ],
+    imports: [
+        EgCommonModule,
+        EgCoreModule,
+        StringModule,
+        OrgFamilySelectModule,
+        TranslateModule,
+        FmRecordEditorModule,
+        GridModule
+    ],
+    exports: [
+        AdminPageComponent
+    ],
+    providers: [
+    ]
+})
+
+export class AdminPageModule { }
+
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
deleted file mode 100644 (file)
index 61b9cb4..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<ng-template #dialogContent>
-  <div class="modal-header bg-info">
-    <h4 class="modal-title" i18n>
-      {{idlClassDef.label}}
-    </h4>
-    <button type="button" class="close" 
-      i18n-aria-label aria-label="Close" (click)="close()">
-      <span aria-hidden="true">&times;</span>
-    </button>
-  </div>
-  <div class="modal-body form-common form-validated" *ngIf="idlObj">
-    <div class="form-group row">
-      <label class="col-lg-4 text-right font-weight-bold" 
-        i18n>Field Name</label>
-      <input 
-        type="text" 
-        [disabled]="true"
-        class="form-control col-lg-7"
-        value="{{idlClassDef.field_map[field].label}}">
-    </div>
-    <div class="form-group row">
-      <label class="col-lg-4 text-right font-weight-bold" 
-        i18n>Current Value</label>
-      <input 
-        type="text" 
-        [disabled]="true"
-        class="form-control col-lg-7"
-        value="{{idlObj[field]()}}">
-    </div>
-    <div class="form-group row">
-      <label class="col-lg-4 text-right font-weight-bold" 
-        i18n>Select Locale</label>
-      <select class="form-control col-lg-7" 
-        (change)="localeChanged($event)"
-        [(ngModel)]="selectedLocale">
-        <option value="{{locale.code()}}" *ngFor="let locale of locales">
-          {{locale.name()}}
-        </option>
-      </select>
-    </div>
-    <div class="form-group row">
-      <label class="col-lg-4 text-right font-weight-bold" i18n>Translation</label>
-      <input 
-        id='translation-input'
-        type="text" 
-        class="form-control col-lg-7"
-        required
-        i18n-placeholder
-        (keyup.enter)="translate()"
-        placeholder="Translation..." 
-        [(ngModel)]="translatedValue"/>
-    </div>
-  </div>
-  <div class="modal-footer">
-    <button *ngIf="prevString" (click)="prevString()" 
-      class="btn btn-info" i18n>Prev String</button>
-    <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)="close()" class="btn btn-warning ml-2" i18n>Cancel</button>
-  </div>
-</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts
deleted file mode 100644 (file)
index 4880973..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-import {Component, OnInit, Input, Renderer2} from '@angular/core';
-import {IdlService, IdlObject} from '@eg/core/idl.service';
-import {ToastService} from '@eg/share/toast/toast.service';
-import {LocaleService} from '@eg/core/locale.service';
-import {AuthService} from '@eg/core/auth.service';
-import {PcrudService} from '@eg/core/pcrud.service';
-import {DialogComponent} from '@eg/share/dialog/dialog.component';
-import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
-
-@Component({
-  selector: 'eg-translate',
-  templateUrl: 'translate.component.html'
-})
-
-export class TranslateComponent
-    extends DialogComponent implements OnInit {
-
-    idlClassDef: any;
-    locales: IdlObject[];
-    selectedLocale: string;
-    translatedValue: string;
-    existingTranslation: IdlObject;
-
-    // These actions should update the idlObject and/or fieldName values,
-    // forcing the dialog to load a new string to translate.  When set,
-    // applying a translation in the dialog will leave the dialog window open
-    // so the next/prev buttons can be used to fetch the next string.
-    nextString: () => void;
-    prevString: () => void;
-
-    idlObj: IdlObject;
-    @Input() set idlObject(o: IdlObject) {
-        if (o) {
-            this.idlObj = o;
-            this.idlClassDef = this.idl.classes[o.classname];
-            this.fetchTranslation();
-        }
-    }
-
-    field: string;
-    @Input() set fieldName(n: string) {
-        this.field = n;
-    }
-
-    constructor(
-        private modal: NgbModal, // required for passing to parent
-        private renderer: Renderer2,
-        private idl: IdlService,
-        private toast: ToastService,
-        private locale: LocaleService,
-        private pcrud: PcrudService,
-        private auth: AuthService) {
-        super(modal);
-    }
-
-    ngOnInit() {
-        // Default to the login locale
-        this.selectedLocale = this.locale.currentLocaleCode();
-        this.locales = [];
-        this.locale.supportedLocales().subscribe(l => this.locales.push(l));
-
-        this.onOpen$.subscribe(() => {
-            const elm = this.renderer.selectRootElement('#translation-input');
-            if (elm) {
-                elm.focus();
-                elm.select();
-            }
-        });
-    }
-
-    localeChanged(code: string) {
-        this.fetchTranslation();
-    }
-
-    fetchTranslation() {
-        const exist = this.existingTranslation;
-
-        if (exist
-            && exist.fq_field() === this.fqField()
-            && exist.identity_value() === this.identValue()) {
-            // Already have the current translation object.
-            return;
-        }
-
-        this.translatedValue = '';
-        this.existingTranslation = null;
-
-        this.pcrud.search('i18n', {
-            translation: this.selectedLocale,
-            fq_field : this.fqField(),
-            identity_value: this.identValue()
-        }).subscribe(tr => {
-            this.existingTranslation = tr;
-            this.translatedValue = tr.string();
-            console.debug('found existing translation ', tr);
-        });
-    }
-
-    fqField(): string {
-        return this.idlClassDef.classname + '.' + this.field;
-    }
-
-    identValue(): string {
-        return this.idlObj[this.idlClassDef.pkey || 'id']();
-    }
-
-    translate() {
-        if (!this.translatedValue) { return; }
-
-        let entry;
-
-        if (this.existingTranslation) {
-            entry = this.existingTranslation;
-            entry.string(this.translatedValue);
-
-            this.pcrud.update(entry).toPromise().then(
-                ok => {
-                    if (!this.nextString) {
-                        this.close(this.translatedValue);
-                    }
-                },
-                err => console.error(err)
-            );
-
-            return;
-        }
-
-        entry = this.idl.create('i18n');
-        entry.fq_field(this.fqField());
-        entry.identity_value(this.identValue());
-        entry.translation(this.selectedLocale);
-        entry.string(this.translatedValue);
-
-        this.pcrud.create(entry).toPromise().then(
-            ok => {
-                if (!this.nextString) {
-                    this.close(this.translatedValue);
-                }
-            },
-            err => console.error('Translation creation failed')
-        );
-    }
-}
-
-