[open-ils-commits] [GIT] Evergreen ILS branch master updated. fc8bd352006cded176635cb31c0924a06f90777e

Evergreen Git git at git.evergreen-ils.org
Mon Mar 25 17:01:19 EDT 2019


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  fc8bd352006cded176635cb31c0924a06f90777e (commit)
       via  a3731c9261f3ab89503fc0e9609cbc941b235703 (commit)
       via  9e69671b29ba913ec9d1c414c06034dc8f8de9d4 (commit)
       via  00de31433cc02b1f3233f8ed2aff5ab30f9c24e1 (commit)
       via  d63c7c397cf215fe92d02454bdc8c6f55e1be3c0 (commit)
       via  1f6c9b60e2aad209c50b94dbac760eb2a2accae0 (commit)
      from  29b100c9e761ad841ab2c65659c95dac392efe99 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit fc8bd352006cded176635cb31c0924a06f90777e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Mar 25 20:48:05 2019 +0000

    LP1811288 Sandbox editor handles dismissals
    
    Teach the sandbox FM editor example to log a useful message on dialog
    dismissal instead of throwing an error (as a result of the uncaught
    rejection).
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
index f112aedf40..84e127e3c0 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
@@ -37,7 +37,7 @@
       [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)="fmRecordEditor.open({size:'lg'})">
+  <button class="btn btn-dark" (click)="openEditor()">
       Fm Record Editor
   </button>
 </div>
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 4ee4ebc0f7..9b058cd5b1 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
@@ -14,6 +14,7 @@ import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {PrintService} from '@eg/share/print/print.service';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {FormatService} from '@eg/core/format.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 
 @Component({
   templateUrl: 'sandbox.component.html'
@@ -29,6 +30,9 @@ export class SandboxComponent implements OnInit {
     @ViewChild('printTemplate')
     private printTemplate: TemplateRef<any>;
 
+    @ViewChild('fmRecordEditor')
+    private fmRecordEditor: FmRecordEditorComponent;
+
     // @ViewChild('helloStr') private helloStr: StringComponent;
 
     gridDataSource: GridDataSource = new GridDataSource();
@@ -132,6 +136,19 @@ 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);
+                }
+            }
+        );
+    }
+
     btGridRowClassCallback(row: any): string {
         if (row.id() === 1) {
             return 'text-uppercase font-weight-bold text-danger';

commit a3731c9261f3ab89503fc0e9609cbc941b235703
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Mar 25 20:29:27 2019 +0000

    LP1811288 Allow Combobox to default to field id
    
    If a combobox field is provided without a label, use the id value as the
    display value.  This helps avoid ugly JS errors while trying to format
    a null string.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
index 272a27189d..8b018d5e38 100644
--- a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
@@ -102,7 +102,8 @@ export class ComboboxComponent implements OnInit {
         this.defaultSelectionApplied = false;
 
         this.formatDisplayString = (result: ComboboxEntry) => {
-            return result.label.trim();
+            const display = result.label || result.id;
+            return (display + '').trim();
         };
     }
 

commit 9e69671b29ba913ec9d1c414c06034dc8f8de9d4
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Mar 22 14:43:34 2019 -0700

    LP1811288 Admin grids preload combobox values
    
    Adds a global option to the fieldmapper editor component to preload
    linked selector values by default.  Update the basic admin pages to use
    the new flag to preload comboboxes.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
index ec8d0b1968..3e41fa221d 100644
--- a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
@@ -117,6 +117,10 @@ export class FmRecordEditorComponent
     // IDL record display label.  Defaults to the IDL label.
     @Input() recordLabel: string;
 
+    // When true at the component level, pre-fetch the combobox data
+    // for all combobox fields.  See also FmFieldOptions.
+    @Input() preloadLinkedValues: boolean;
+
     // Emit the modified object when the save action completes.
     @Output() onSave$ = new EventEmitter<IdlObject>();
 
@@ -368,6 +372,13 @@ export class FmRecordEditorComponent
 
         const fieldOptions = this.fieldOptions[field.name] || {};
 
+        // globally preloading unless a field-specific value is set.
+        if (this.preloadLinkedValues) {
+            if (!('preloadLinkedValues' in fieldOptions)) {
+                fieldOptions.preloadLinkedValues = true;
+            }
+        }
+
         const selector = fieldOptions.linkedSearchField ||
             this.getClassSelector(field.class);
 
diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html
index 95819f0a0a..7a47a3d424 100644
--- a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html
@@ -62,7 +62,8 @@
   </eg-grid-toolbar-action>
 </eg-grid>
 
-<eg-fm-record-editor #editDialog idlClass="{{idlClass}}" readonlyFields="{{readonlyFields}}">
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}" 
+    [preloadLinkedValues]="true" readonlyFields="{{readonlyFields}}">
 </eg-fm-record-editor>
 
 

commit 00de31433cc02b1f3233f8ed2aff5ab30f9c24e1
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Feb 20 13:51:36 2019 -0500

    LP1811288 Basic admin page readonlyFields repair
    
    Fix a bug in the basic admin page component that prevented the readOnly
    fields from successfully propagating to the fieldmapper editor.
    
    To Test:
    1. Navigate to /eg2/en-US/staff/admin/booking/resource_type
    2. Click "New Resource Type"
    3. Confirm the "Bibliographic Record" field is disabled.
    
    Includes ng-lint repair as well.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts
index d0b9224cf6..472245c892 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts
@@ -12,7 +12,7 @@ import {IdlService} from '@eg/core/idl.service';
       </eg-title>
       <eg-staff-banner bannerText="{{classLabel}} Configuration" i18n-bannerText>
       </eg-staff-banner>
-      <eg-admin-page persistKeyPfx="{{persistKeyPfx}}" idlClass="{{idlClass}}" 
+      <eg-admin-page persistKeyPfx="{{persistKeyPfx}}" idlClass="{{idlClass}}"
         readonlyFields="{{readonlyFields}}"></eg-admin-page>
     `
 })
@@ -59,12 +59,11 @@ export class BasicAdminPageComponent implements OnInit {
 
         // Pass the readonlyFields param if available
         if (this.route.snapshot.data &&
-            this.route.snapshot.data.length &&
+            this.route.snapshot.data[0] && // snapshot.data is a HASH.
             this.route.snapshot.data[0].readonlyFields) {
             this.readonlyFields = this.route.snapshot.data[0].readonlyFields;
         }
 
-
         Object.keys(this.idl.classes).forEach(class_ => {
             const classDef = this.idl.classes[class_];
             if (classDef.table === fullTable) {

commit d63c7c397cf215fe92d02454bdc8c6f55e1be3c0
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Jan 10 18:04:59 2019 -0500

    LP1811288 Angular fm-editor uses combobox
    
    * Linked field options traditionally rendered via <select> are now
      rendered with an eg-combobox.
    * Caller has option to force a combobox to preload values or rely solely
      on typehead.
    * Caller has option to provide a canned list of combobox values.
    * Caller has option to set / override which field on the linked class
      should searched by the typeahead
    * General improvements to fm-editor form building and field options
      management.
    * Includes Angular7 rxjs import repairs
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

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 0d7fe1e0ff..aad65d15d6 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
@@ -15,68 +15,64 @@
         </div>
         <div class="col-lg-7">
 
-          <span *ngIf="field.template">
-            <ng-container
-              *ngTemplateOutlet="field.template; context:customTemplateFieldContext(field)">
+          <ng-container [ngSwitch]="inputType(field)">
+
+            <ng-container *ngSwitchCase="'template'">
+              <ng-container
+                *ngTemplateOutlet="field.template; context:customTemplateFieldContext(field)">
+              </ng-container> 
             </ng-container> 
-          </span>
 
-          <span *ngIf="!field.template">
+            <ng-container *ngSwitchCase="'readonly'">
+              <span>{{record[field.name]()}}</span>
+            </ng-container>
 
-            <span *ngIf="field.datatype == 'id' && !pkeyIsEditable">
-              {{record[field.name]()}}
-            </span>
-  
-            <ng-container *ngIf="field.datatype == 'id' && pkeyIsEditable">
-              <ng-container *ngIf="field.readOnly">
-                <span>{{record[field.name]()}}</span>
-              </ng-container>
-              <ng-container *ngIf="!field.readOnly">
-                <input
-                  class="form-control"
-                  name="{{field.name}}"
-                  id="{{idPrefix}}-{{field.name}}"
-                  placeholder="{{field.label}}..."
-                  i18n-placeholder
-                  [required]="field.isRequired()"
-                  [ngModel]="record[field.name]()"
-                  (ngModelChange)="record[field.name]($event)"/>
-              </ng-container>
+            <ng-container *ngSwitchCase="'readonly-money'">
+              <span>{{record[field.name]() | currency}}</span>
             </ng-container>
-  
-            <ng-container 
-              *ngIf="field.datatype == 'text' || field.datatype == 'interval'">
-              <ng-container *ngIf="field.readOnly">
-                <span>{{record[field.name]()}}</span>
-              </ng-container>
-              <ng-container *ngIf="!field.readOnly">
-                <input
-                  class="form-control"
-                  name="{{field.name}}"
-                  id="{{idPrefix}}-{{field.name}}"
-                  placeholder="{{field.label}}..."
-                  i18n-placeholder
-                  [required]="field.isRequired()"
-                  [ngModel]="record[field.name]()"
-                  (ngModelChange)="record[field.name]($event)"/>
+
+            <ng-container *ngSwitchCase="'readonly-list'">
+              <ng-container *ngIf="field.linkedValues">
+                <span>{{field.linkedValues[0].label}}</span>
               </ng-container>
             </ng-container>
 
-            <!-- TODO: add support to eg-date-select for read-only view -->
-            <span *ngIf="field.datatype == 'timestamp'">
+            <ng-container *ngSwitchCase="'timestamp'">
               <eg-date-select
                 domId="{{idPrefix}}-{{field.name}}"
                 (onChangeAsIso)="record[field.name]($event)"
                 initialIso="{{record[field.name]()}}">
               </eg-date-select>
-            </span>
+            </ng-container>
 
-            <ng-container *ngIf="field.datatype == 'int'">
-              <ng-container *ngIf="field.readOnly">
-                <span>{{record[field.name]()}}</span>
-              </ng-container>
-              <ng-container *ngIf="!field.readOnly">
+            <ng-container *ngSwitchCase="'org_unit'">
+              <eg-org-select
+                placeholder="{{field.label}}..."
+                i18n-placeholder
+                domId="{{idPrefix}}-{{field.name}}"
+                [limitPerms]="modePerms[mode]"
+                [readOnly]="field.readOnly"
+                [applyDefault]="field.orgDefaultAllowed"
+                [initialOrgId]="record[field.name]()"
+                (onChange)="record[field.name]($event)">
+              </eg-org-select>
+            </ng-container>
+          
+            <ng-container *ngSwitchCase="'money'">
+              <input
+                class="form-control"
+                type="number" step="0.1"
+                name="{{field.name}}"
+                id="{{idPrefix}}-{{field.name}}"
+                placeholder="{{field.label}}..."
+                i18n-placeholder
+                [readonly]="field.readOnly"
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]()"
+                (ngModelChange)="record[field.name]($event)"/>
+            </ng-container>
 
+            <ng-container *ngSwitchCase="'int'">
               <input
                 class="form-control"
                 type="number"
@@ -87,91 +83,55 @@
                 [required]="field.isRequired()"
                 [ngModel]="record[field.name]()"
                 (ngModelChange)="record[field.name]($event)"/>
-              </ng-container>
             </ng-container>
 
-            <ng-container *ngIf="field.datatype == 'float'">
-              <ng-container *ngIf="field.readOnly">
-                <span>{{record[field.name]()}}</span>
-              </ng-container>
-              <ng-container *ngIf="!field.readOnly">
-                <input
-                  class="form-control"
-                  type="number" step="0.1"
-                  name="{{field.name}}"
-                  id="{{idPrefix}}-{{field.name}}"
-                  placeholder="{{field.label}}..."
-                  i18n-placeholder
-                  [required]="field.isRequired()"
-                  [ngModel]="record[field.name]()"
-                  (ngModelChange)="record[field.name]($event)"/>
-              </ng-container>
+            <ng-container *ngSwitchCase="'float'">
+              <input
+                class="form-control"
+                type="number" step="0.1"
+                name="{{field.name}}"
+                id="{{idPrefix}}-{{field.name}}"
+                placeholder="{{field.label}}..."
+                i18n-placeholder
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]()"
+                (ngModelChange)="record[field.name]($event)"/>
             </ng-container>
-      
-            <ng-container *ngIf="field.datatype == 'money'">
-              <ng-container *ngIf="field.readOnly">
-                <span>{{record[field.name]() | currency}}</span>
-              </ng-container>
-              <ng-container *ngIf="!field.readOnly">
-                <input
-                  class="form-control"
-                  type="number" step="0.1"
-                  name="{{field.name}}"
-                  id="{{idPrefix}}-{{field.name}}"
-                  placeholder="{{field.label}}..."
-                  i18n-placeholder
-                  [readonly]="field.readOnly"
-                  [required]="field.isRequired()"
-                  [ngModel]="record[field.name]()"
-                  (ngModelChange)="record[field.name]($event)"/>
-              </ng-container>
+
+            <ng-container *ngSwitchCase="'text'">
+              <input
+                class="form-control"
+                id="{{idPrefix}}-{{field.name}}" name="{{field.name}}"
+                type="text"
+                placeholder="{{field.label}}..." i18n-placeholder
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]()"
+                (ngModelChange)="record[field.name]($event)"/>
             </ng-container>
-  
-            <input *ngIf="field.datatype == 'bool'"
-              class="form-check-input"
-              type="checkbox"
-              name="{{field.name}}"
-              id="{{idPrefix}}-{{field.name}}"
-              [disabled]="field.readOnly"
-              [ngModel]="record[field.name]()"
-              (ngModelChange)="record[field.name]($event)"/>
-  
-            <ng-container *ngIf="field.datatype == 'link'">
-              <ng-container *ngIf="field.readOnly">
-                <!-- in readOnly mode, if a value is present, it will
-                    live as the only item in the linkedValues array -->
-                <ng-container *ngIf="(field.linkedValues != null) && (field.linkedValues.length)">
-                  <span>{{field.linkedValues[0].name}}</span>
-                </ng-container>
-              </ng-container>
-              <ng-container *ngIf="!field.readOnly">
-                <span [ngClass]="{nullable : !field.isRequired()}">
-                  <select
-                    class="form-control"
-                    name="{{field.name}}"
-                    id="{{idPrefix}}-{{field.name}}"
-                    [required]="field.isRequired()"
-                    [ngModel]="record[field.name]()"
-                    (ngModelChange)="record[field.name]($event)">
-                    <option *ngFor="let item of field.linkedValues" 
-                      [value]="item.id">{{item.name}}</option>
-                  </select>
-                </span>
-              </ng-container>
+
+            <ng-container *ngSwitchCase="'bool'">
+              <input
+                class="form-check-input"
+                type="checkbox"
+                name="{{field.name}}"
+                id="{{idPrefix}}-{{field.name}}"
+                [disabled]="field.readOnly"
+                [ngModel]="record[field.name]()"
+                (ngModelChange)="record[field.name]($event)"/>
             </ng-container>
   
-            <eg-org-select *ngIf="field.datatype == 'org_unit'"
-              placeholder="{{field.label}}..."
-              i18n-placeholder
-              domId="{{idPrefix}}-{{field.name}}"
-              [limitPerms]="modePerms[mode]"
-              [readOnly]="field.readOnly"
-              [applyDefault]="field.orgDefaultAllowed"
-              [initialOrgId]="record[field.name]()"
-              (onChange)="record[field.name]($event)">
-            </eg-org-select>
-
-          </span>
+            <ng-container *ngSwitchCase="'list'">
+              <eg-combobox
+                id="{{idPrefix}}-{{field.name}}" name="{{field.name}}"
+                placeholder="{{field.label}}..." i18n-placeholder 
+                [required]="field.isRequired()"
+                [entries]="field.linkedValues"
+                [asyncDataSource]="field.linkedValuesSource"
+                [startId]="record[field.name]()"
+                (onChange)="record[field.name]($event ? $event.id : null)">
+              </eg-combobox>
+            </ng-container>
+          </ng-container> <!-- switch -->
         </div>
       </div>
     </form>
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
index 45dd167bb6..ec8d0b1968 100644
--- a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
@@ -1,10 +1,13 @@
 import {Component, OnInit, Input,
     Output, EventEmitter, TemplateRef} from '@angular/core';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
 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 {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 
 interface CustomFieldTemplate {
     template: TemplateRef<any>;
@@ -26,6 +29,43 @@ interface CustomFieldContext {
     [fields: string]: any;
 }
 
+// Collection of extra options that may be applied to fields
+// for controling non-default behaviour.
+export interface FmFieldOptions {
+
+    // Render the field as a combobox using these values, regardless
+    // of the field's datatype.
+    customValues?: {[field: string]: ComboboxEntry[]};
+
+    // Provide / override the "selector" value for the linked class.
+    // This is the field the combobox will search for typeahead.  If no
+    // field is defined, the "selector" field is used.  If no "selector"
+    // field exists, the combobox will pre-load all linked values so
+    // the user can click to navigate.
+    linkedSearchField?: string;
+
+    // When true for combobox fields, pre-fetch the combobox data
+    // so the user can click or type to find values.
+    preloadLinkedValues?: boolean;
+
+    // Directly override the required state of the field.
+    // This only has an affect if the value is true.
+    isRequired?: boolean;
+
+    // If this function is defined, the function will be called
+    // at render time to see if the field should be marked are required.
+    // This supersedes all other isRequired specifiers.
+    isRequiredOverride?: (field: string, record: IdlObject) => boolean;
+
+    // Directly apply the readonly status of the field.
+    // This only has an affect if the value is true.
+    isReadonly?: boolean;
+
+    // Render the field using this custom template instead of chosing
+    // from the default set of form inputs.
+    customTemplate?: CustomFieldTemplate;
+}
+
 @Component({
   selector: 'eg-fm-record-editor',
   templateUrl: './fm-editor.component.html',
@@ -43,6 +83,7 @@ export class FmRecordEditorComponent
     //       'view' for viewing an existing record without editing
     mode: 'create' | 'update' | 'view' = 'create';
     recId: any;
+
     // IDL record we are editing
     // TODO: allow this to be update in real time by the caller?
     record: IdlObject;
@@ -51,8 +92,9 @@ export class FmRecordEditorComponent
     // for the current IDL class
     modePerms: {[mode: string]: string};
 
-    @Input() customFieldTemplates:
-        {[fieldName: string]: CustomFieldTemplate} = {};
+    // Collection of FmFieldOptions for specifying non-default
+    // behaviour for each field (by field name).
+    @Input() fieldOptions: {[fieldName: string]: FmFieldOptions} = {};
 
     // list of fields that should not be displayed
     @Input() hiddenFieldsList: string[] = [];
@@ -72,14 +114,6 @@ export class FmRecordEditorComponent
     @Input() orgDefaultAllowedList: string[] = [];
     @Input() orgDefaultAllowed: string; // comma-separated string version
 
-    // hash, keyed by field name, of functions to invoke to check
-    // whether a field is required.  Each callback is passed the field
-    // name and the record and should return a boolean value. This
-    // supports cases where whether a field is required or not depends
-    // on the current value of another field.
-    @Input() isRequiredOverride:
-        {[field: string]: (field: string, record: IdlObject) => boolean};
-
     // IDL record display label.  Defaults to the IDL label.
     @Input() recordLabel: string;
 
@@ -227,14 +261,27 @@ export class FmRecordEditorComponent
         });
     }
 
+    // Returns the name of the field on a class (typically via a linked
+    // field) that acts as the selector value for display / search.
+    getClassSelector(class_: string): string {
+        if (class_) {
+            const linkedClass = this.idl.classes[class_];
+            return linkedClass.pkey ?
+                linkedClass.field_map[linkedClass.pkey].selector : null;
+        }
+        return null;
+    }
 
-    private flattenLinkedValues(cls: string, list: IdlObject[]): any[] {
-        const idField = this.idl.classes[cls].pkey;
-        const selector =
-            this.idl.classes[cls].field_map[idField].selector || idField;
+    private flattenLinkedValues(field: any, list: IdlObject[]): ComboboxEntry[] {
+        const class_ = field.class;
+        const fieldOptions = this.fieldOptions[field.name] || {};
+        const idField = this.idl.classes[class_].pkey;
+
+        const selector = fieldOptions.linkedSearchField
+            || this.getClassSelector(class_) || idField;
 
         return list.map(item => {
-            return {id: item[idField](), name: item[selector]()};
+            return {id: item[idField](), label: item[selector]()};
         });
     }
 
@@ -244,73 +291,134 @@ export class FmRecordEditorComponent
             !f.virtual && !this.hiddenFieldsList.includes(f.name)
         );
 
-        const promises = [];
-
-        this.fields.forEach(field => {
-            field.readOnly = this.mode === 'view'
-                || this.readonlyFieldsList.includes(field.name);
-
-            if (this.isRequiredOverride &&
-                field.name in this.isRequiredOverride) {
-                field.isRequired = () => {
-                    return this.isRequiredOverride[field.name](field.name, this.record);
-                };
-            } else {
-                field.isRequired = () => {
-                    return field.required ||
-                        this.requiredFieldsList.includes(field.name);
-                };
-            }
+        // Wait for all network calls to complete
+        return Promise.all(
+            this.fields.map(field => this.constructOneField(field)));
+    }
+
+    private constructOneField(field: any): Promise<any> {
+
+        let promise = null;
+        const fieldOptions = this.fieldOptions[field.name] || {};
+
+        field.readOnly = this.mode === 'view'
+            || fieldOptions.isReadonly === true
+            || this.readonlyFieldsList.includes(field.name);
+
+        if (fieldOptions.isRequiredOverride) {
+            field.isRequired = () => {
+                return fieldOptions.isRequiredOverride(field.name, this.record);
+            };
+        } else {
+            field.isRequired = () => {
+                return field.required
+                    || fieldOptions.isRequired
+                    || this.requiredFieldsList.includes(field.name);
+            };
+        }
+
+        if (fieldOptions.customValues) {
+
+            field.linkedValues = fieldOptions.customValues;
+
+        } else if (field.datatype === 'link' && field.readOnly) {
+
+            // no need to fetch all possible values for read-only fields
+            const idToFetch = this.record[field.name]();
 
-            if (field.datatype === 'link' && field.readOnly) {
-
-                // no need to fetch all possible values for read-only fields
-                const idToFetch = this.record[field.name]();
-
-                if (idToFetch) {
-
-                    // If the linked class defines a selector field, fetch the
-                    // linked data so we can display the data within the selector
-                    // field.  Otherwise, avoid the network lookup and let the
-                    // bare value (usually an ID) be displayed.
-                    const selector =
-                        this.idl.getLinkSelector(this.idlClass, field.name);
-
-                    if (selector && selector !== field.name) {
-                        promises.push(
-                            this.pcrud.retrieve(field.class, this.record[field.name]())
-                            .toPromise().then(list => {
-                                field.linkedValues =
-                                    this.flattenLinkedValues(field.class, Array(list));
-                            })
-                        );
-                    } else {
-                        // No selector, display the raw id/key value.
-                        field.linkedValues = [{id: idToFetch, name: idToFetch}];
-                    }
+            if (idToFetch) {
+
+                // If the linked class defines a selector field, fetch the
+                // linked data so we can display the data within the selector
+                // field.  Otherwise, avoid the network lookup and let the
+                // bare value (usually an ID) be displayed.
+                const selector = fieldOptions.linkedSearchField ||
+                    this.getClassSelector(field.class);
+
+                if (selector && selector !== field.name) {
+                    promise = this.pcrud.retrieve(field.class, idToFetch)
+                        .toPromise().then(list => {
+                            field.linkedValues =
+                                this.flattenLinkedValues(field, Array(list));
+                        });
+                } else {
+                    // No selector, display the raw id/key value.
+                    field.linkedValues = [{id: idToFetch, name: idToFetch}];
                 }
-            } else if (field.datatype === 'link') {
-                promises.push(
-                    this.pcrud.retrieveAll(field.class, {}, {atomic : true})
-                    .toPromise().then(list => {
-                        field.linkedValues =
-                            this.flattenLinkedValues(field.class, list);
-                    })
-                );
-            } else if (field.datatype === 'org_unit') {
-                field.orgDefaultAllowed =
-                    this.orgDefaultAllowedList.includes(field.name);
             }
 
-            if (this.customFieldTemplates[field.name]) {
-                field.template = this.customFieldTemplates[field.name].template;
-                field.context = this.customFieldTemplates[field.name].context;
-            }
+        } else if (field.datatype === 'link') {
 
-        });
+            promise = this.wireUpCombobox(field);
 
-        // Wait for all network calls to complete
-        return Promise.all(promises);
+        } else if (field.datatype === 'org_unit') {
+            field.orgDefaultAllowed =
+                this.orgDefaultAllowedList.includes(field.name);
+        }
+
+        if (fieldOptions.customTemplate) {
+            field.template = fieldOptions.customTemplate.template;
+            field.context = fieldOptions.customTemplate.context;
+        }
+
+        return promise || Promise.resolve();
+    }
+
+    wireUpCombobox(field: any): Promise<any> {
+
+        const fieldOptions = this.fieldOptions[field.name] || {};
+
+        const selector = fieldOptions.linkedSearchField ||
+            this.getClassSelector(field.class);
+
+        if (!selector && !fieldOptions.preloadLinkedValues) {
+            // User probably expects an async data source, but we can't
+            // provide one without a selector.  Warn the user.
+            console.warn(`Class ${field.class} has no selector.
+                Pre-fetching all rows for combobox`);
+        }
+
+        if (fieldOptions.preloadLinkedValues || !selector) {
+            return this.pcrud.retrieveAll(field.class, {}, {atomic : true})
+            .toPromise().then(list => {
+                field.linkedValues =
+                    this.flattenLinkedValues(field, list);
+            });
+        }
+
+        // If we have a selector, wire up for async data retrieval
+        field.linkedValuesSource =
+            (term: string): Observable<ComboboxEntry> => {
+
+            const search = {};
+            const orderBy = {order_by: {}};
+            const idField = this.idl.classes[field.class].pkey || 'id';
+
+            search[selector] = {'ilike': `%${term}%`};
+            orderBy.order_by[field.class] = selector;
+
+            return this.pcrud.search(field.class, search, orderBy)
+            .pipe(map(idlThing =>
+                // Map each object into a ComboboxEntry upon arrival
+                this.flattenLinkedValues(field, [idlThing])[0]
+            ));
+        };
+
+        // Using an async data source, but a value is already set
+        // on the field.  Fetch the linked object and add it to the
+        // combobox entry list so it will be avilable for display
+        // at dialog load time.
+        const linkVal = this.record[field.name]();
+        if (linkVal !== null && linkVal !== undefined) {
+            return this.pcrud.retrieve(field.class, linkVal).toPromise()
+            .then(idlThing => {
+                field.linkedValues =
+                    this.flattenLinkedValues(field, Array(idlThing));
+            });
+        }
+
+        // No linked value applied, nothing to pre-fetch.
+        return Promise.resolve();
     }
 
     // Returns a context object to be inserted into a custom
@@ -335,6 +443,53 @@ export class FmRecordEditorComponent
     cancel() {
         this.dismiss('canceled');
     }
+
+    // Returns a string describing the type of input to display
+    // for a given field.  This helps cut down on the if/else
+    // nesti-ness in the template.  Each field will match
+    // exactly one type.
+    inputType(field: any): string {
+
+        if (field.template) {
+            return 'template';
+        }
+
+        // Some widgets handle readOnly for us.
+        if (   field.datatype === 'timestamp'
+            || field.datatype === 'org_unit'
+            || field.datatype === 'bool') {
+            return field.datatype;
+        }
+
+        if (field.readOnly) {
+            if (field.datatype === 'money') {
+                return 'readonly-money';
+            }
+
+            if (field.datatype === 'link' || field.linkedValues) {
+                return 'readonly-list';
+            }
+
+            return 'readonly';
+        }
+
+        if (field.datatype === 'id' && !this.pkeyIsEditable) {
+            return 'readonly';
+        }
+
+        if (   field.datatype === 'int'
+            || field.datatype === 'float'
+            || field.datatype === 'money') {
+            return field.datatype;
+        }
+
+        if (field.datatype === 'link' || field.linkedValues) {
+            return 'list';
+        }
+
+        // datatype == text / interval / editable-pkey
+        return 'text';
+    }
 }
 
 
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
index 5f1f1ad0bf..f112aedf40 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
@@ -30,9 +30,11 @@
       (ngModelChange)="record[field.name]($event)">
     </textarea>
   </ng-template>
+  <!-- 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" 
-      [customFieldTemplates]="{description:{template:descriptionTemplate,context:{'hello':'goodbye'}}}"
+      [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)="fmRecordEditor.open({size:'lg'})">

commit 1f6c9b60e2aad209c50b94dbac760eb2a2accae0
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jan 11 13:06:23 2019 -0500

    LP1811288 Combobox support entrylist+async / id labels
    
    Allow the caller to pass a seed entrylist value for async comboboxes.
    This is useful when a value should be applied to the box on load instead
    of waiting for user input for typeahead loading.
    
    Allow combobox entries to default to using the 'id' field for the label
    if no label is provided.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
index 47237e9f9b..0a5deeeb8c 100644
--- a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
+++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
@@ -1,7 +1,7 @@
 
 <!-- todo disabled -->
 <ng-template #displayTemplate let-r="result">
-{{r.label}}
+{{r.label || r.id}}
 </ng-template>
 
 <div class="d-flex">
diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
index 323623c3a0..272a27189d 100644
--- a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
@@ -11,7 +11,8 @@ import {StoreService} from '@eg/core/store.service';
 
 export interface ComboboxEntry {
   id: any;
-  label: string;
+  // If no label is provided, the 'id' value is used.
+  label?: string;
   freetext?: boolean;
 }
 
@@ -69,8 +70,17 @@ export class ComboboxComponent implements OnInit {
     defaultSelectionApplied: boolean;
 
     @Input() set entries(el: ComboboxEntry[]) {
-        this.entrylist = el;
-        this.applySelection();
+        if (el) {
+            this.entrylist = el;
+            this.applySelection();
+
+            // It's possible to provide an entrylist at load time, but
+            // fetch all future data via async data source.  Track the
+            // values we already have so async lookup won't add them again.
+            // A new entry list wipes out any existing async values.
+            this.asyncIds = {};
+            el.forEach(entry => this.asyncIds['' + entry.id] = true);
+        }
     }
 
     // Emitted when the value is changed via UI.
@@ -138,6 +148,14 @@ export class ComboboxComponent implements OnInit {
         this.selected = this.entrylist.filter(e => e.id === entryId)[0];
     }
 
+    addAsyncEntry(entry: ComboboxEntry) {
+        // Avoid duplicate async entries
+        if (!this.asyncIds['' + entry.id]) {
+            this.asyncIds['' + entry.id] = true;
+            this.addEntry(entry);
+        }
+    }
+
     onBlur() {
         // When the selected value is a string it means we have either
         // no value (user cleared the input) or a free-text value.
@@ -180,12 +198,7 @@ export class ComboboxComponent implements OnInit {
 
         return new Observable(observer => {
             this.asyncDataSource(term).subscribe(
-                (entry: ComboboxEntry) => {
-                    if (!this.asyncIds['' + entry.id]) {
-                        this.asyncIds['' + entry.id] = true;
-                        this.addEntry(entry);
-                    }
-                },
+                (entry: ComboboxEntry) => this.addAsyncEntry(entry),
                 err => {},
                 ()  => {
                     observer.next(term);
@@ -215,6 +228,9 @@ export class ComboboxComponent implements OnInit {
             map((term: string) => {
 
                 if (term === '' || term === '_CLICK_') {
+                    // Avoid displaying the existing entries on-click
+                    // for async sources, becuase that implies we have
+                    // the full data set. (setting?)
                     if (this.asyncDataSource) {
                         return [];
                     } else {
@@ -226,9 +242,10 @@ export class ComboboxComponent implements OnInit {
 
                 // Filter entrylist whose labels substring-match the
                 // text entered.
-                return this.entrylist.filter(entry =>
-                    entry.label.toLowerCase().indexOf(term.toLowerCase()) > -1
-                );
+                return this.entrylist.filter(entry => {
+                    const label = entry.label || entry.id;
+                    return label.toLowerCase().indexOf(term.toLowerCase()) > -1;
+                });
             })
         );
     }

-----------------------------------------------------------------------

Summary of changes:
 .../src/app/share/combobox/combobox.component.html |   2 +-
 .../src/app/share/combobox/combobox.component.ts   |  44 ++-
 .../app/share/fm-editor/fm-editor.component.html   | 214 ++++++--------
 .../src/app/share/fm-editor/fm-editor.component.ts | 316 ++++++++++++++++-----
 .../app/staff/admin/basic-admin-page.component.ts  |   5 +-
 .../src/app/staff/sandbox/sandbox.component.html   |   6 +-
 .../eg2/src/app/staff/sandbox/sandbox.component.ts |  17 ++
 .../share/admin-page/admin-page.component.html     |   3 +-
 8 files changed, 385 insertions(+), 222 deletions(-)


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list