LP1812670 Angular grid shows selector labels
authorBill Erickson <berickxx@gmail.com>
Fri, 25 Jan 2019 20:17:56 +0000 (15:17 -0500)
committerDan Wells <dbw2@calvin.edu>
Fri, 22 Mar 2019 20:23:32 +0000 (16:23 -0400)
* Teach PcrudService how to flesh link fields when a selector is defined
  on the linked class. This uses a new search/retrieve API flag
  {fleshSelectors:true}.

* Teach the grid how to render selector values when configured to do so
  via a new grid component attribute [showLinkSelectors]="true".

* Teach the Angular staff admin page to request linked selectors from
  pcrud and tell its grid to expect them.

* Adds utility function to IdlServer for finding the selector for a
  given class + field.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Dan Wells <dbw2@calvin.edu>

Open-ILS/src/eg2/src/app/core/idl.service.ts
Open-ILS/src/eg2/src/app/core/pcrud.service.ts
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts
Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html
Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts

index 89f8411..b6f8173 100644 (file)
@@ -133,5 +133,19 @@ export class IdlService {
 
         return result;
     }
+
+    // Given a field on an IDL class, returns the name of the field
+    // on the linked class that acts as the selector for the linked class.
+    // Returns null if no selector is found or the field is not a link.
+    getLinkSelector(fmClass: string, field: string): string {
+        const fieldDef = this.classes[fmClass].field_map[field];
+        if (fieldDef.class) {
+            const classDef = this.classes[fieldDef.class];
+            if (classDef.pkey) {
+                return classDef.field_map[classDef.pkey].selector || null;
+            }
+        }
+        return null;
+    }
 }
 
index fc79f30..f023b6d 100644 (file)
@@ -13,6 +13,10 @@ interface PcrudReqOps {
     anonymous?: boolean;
     idlist?: boolean;
     atomic?: boolean;
+    // If true, link-type fields which link to a class that defines a
+    // selector will be fleshed with the linked value.  This affects
+    // retrieve(), retrieveAll(), and search() calls.
+    fleshSelectors?: boolean;
 }
 
 // For for documentation purposes.
@@ -86,10 +90,42 @@ export class PcrudContext {
         this.session.disconnect();
     }
 
+    // Adds "flesh" logic to retrieve linked values for all fields
+    // that link to a class which defines a selector field.
+    applySelectorFleshing(fmClass: string, pcrudOps: any) {
+        pcrudOps = pcrudOps || {};
+
+        if (!pcrudOps.flesh) {
+            pcrudOps.flesh = 1;
+        }
+
+        if (!pcrudOps.flesh_fields) {
+            pcrudOps.flesh_fields = {};
+        }
+
+        this.idl.classes[fmClass].fields
+        .filter(f => f.datatype === 'link' && !f.virtual)
+        .forEach(field => {
+            const selector = this.idl.getLinkSelector(fmClass, field.name);
+            if (!selector) { return; }
+
+            if (!pcrudOps.flesh_fields[fmClass]) {
+                pcrudOps.flesh_fields[fmClass] = [];
+            }
+
+            if (pcrudOps.flesh_fields[fmClass].indexOf(field.name) < 0) {
+                pcrudOps.flesh_fields[fmClass].push(field.name);
+            }
+        });
+    }
+
     retrieve(fmClass: string, pkey: Number | string,
             pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
         reqOps = reqOps || {};
         this.authoritative = reqOps.authoritative || false;
+        if (reqOps.fleshSelectors) {
+            this.applySelectorFleshing(fmClass, pcrudOps);
+        }
         return this.dispatch(
             `open-ils.pcrud.retrieve.${fmClass}`,
              [this.token(reqOps), pkey, pcrudOps]);
@@ -112,6 +148,10 @@ export class PcrudContext {
 
         if (reqOps.atomic) { method += '.atomic'; }
 
+        if (reqOps.fleshSelectors) {
+            this.applySelectorFleshing(fmClass, pcrudOps);
+        }
+
         return this.dispatch(method, [this.token(reqOps), search, pcrudOps]);
     }
 
index 17c0e46..45dd167 100644 (file)
@@ -273,9 +273,8 @@ export class FmRecordEditorComponent
                     // 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 idField = this.idl.classes[field.class].pkey;
                     const selector =
-                        this.idl.classes[field.class].field_map[idField].selector;
+                        this.idl.getLinkSelector(this.idlClass, field.name);
 
                     if (selector && selector !== field.name) {
                         promises.push(
index d48028b..66686ef 100644 (file)
@@ -80,6 +80,20 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
     // grid data.
     @Input() pageOffset: number;
 
+    // If true and an idlClass is specificed, the grid assumes
+    // datatype=link fields that link to classes which define a selector
+    // are fleshed with the linked object.  And, instead of displaying
+    // the raw field value, displays the selector value from the linked
+    // object.  The caller is responsible for fleshing the appropriate
+    // fields in the GridDataSource getRows handler.
+    //
+    // This only applies to auto-generated columns.
+    //
+    // For example, idlClass="aou" and field="ou_type", the display
+    // value will be ou_type().name() since "name" is the selector
+    // field on the "aout" class.
+    @Input() showLinkSelectors: boolean;
+
     context: GridContext;
 
     // These events are emitted from our grid-body component.
@@ -112,6 +126,7 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
         this.context.isMultiSortable = this.multiSortable === true;
         this.context.useLocalSort = this.useLocalSort === true;
         this.context.disableSelect = this.disableSelect === true;
+        this.context.showLinkSelectors = this.showLinkSelectors === true;
         this.context.disableMultiSelect = this.disableMultiSelect === true;
         this.context.rowFlairIsEnabled = this.rowFlairIsEnabled  === true;
         this.context.rowFlairCallback = this.rowFlairCallback;
index dcffc95..16c5ea1 100644 (file)
@@ -437,6 +437,7 @@ export class GridContext {
     defaultVisibleFields: string[];
     defaultHiddenFields: string[];
     overflowCells: boolean;
+    showLinkSelectors: boolean;
 
     // Services injected by our grid component
     idl: IdlService;
@@ -624,12 +625,11 @@ export class GridContext {
 
     getRowColumnValue(row: any, col: GridColumn): string {
         let val;
-        if (col.name in row) {
+
+        if (col.path) {
+            val = this.nestedItemFieldValue(row, col);
+        } else if (col.name in row) {
             val = this.getObjectFieldValue(row, col.name);
-        } else {
-            if (col.path) {
-                val = this.nestedItemFieldValue(row, col);
-            }
         }
 
         return this.format.transform({
@@ -657,7 +657,7 @@ export class GridContext {
         for (let i = 0; i < steps.length; i++) {
             const step = steps[i];
 
-            if (typeof obj !== 'object') {
+            if (obj === null || obj === undefined || typeof obj !== 'object') {
                 // We have run out of data to step through before
                 // reaching the end of the path.  Conclude fleshing via
                 // callback if provided then exit.
@@ -861,6 +861,15 @@ export class GridContext {
             col.datatype = field.datatype;
             col.isIndex = (field.name === pkeyField);
             col.isAuto = true;
+
+            if (this.showLinkSelectors) {
+                const selector = this.idl.getLinkSelector(
+                    this.columnSet.idlClass, field.name);
+                if (selector) {
+                    col.path = field.name + '.' + selector;
+                }
+            }
+
             this.columnSet.add(col);
         });
     }
index 44e407b..95819f0 100644 (file)
@@ -49,7 +49,7 @@
 <eg-translate #translator></eg-translate>
 
 <eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" 
-    [sortable]="true" persistKey="{{persistKey}}">
+    [sortable]="true" persistKey="{{persistKey}}" [showLinkSelectors]="true">
   <eg-grid-toolbar-button [disabled]="!canCreate" 
     label="New {{idlClassDef.label}}" i18n-label [action]="createNew">
   </eg-grid-toolbar-button>
index cd6e706..4561a04 100644 (file)
@@ -302,11 +302,13 @@ export class AdminPageComponent implements OnInit {
 
                 const search = {};
                 search[this.orgField] = orgs;
-                return this.pcrud.search(this.idlClass, search, searchOps);
+                return this.pcrud.search(
+                    this.idlClass, search, searchOps, {fleshSelectors: true});
             }
 
             // No org filter -- fetch all rows
-            return this.pcrud.retrieveAll(this.idlClass, searchOps);
+            return this.pcrud.retrieveAll(
+                this.idlClass, searchOps, {fleshSelectors: true});
         };
     }