LP1847800 Admin grids support config_field links
authorBill Erickson <berickxx@gmail.com>
Wed, 29 Apr 2020 21:27:23 +0000 (17:27 -0400)
committerChris Sharp <csharp@georgialibraries.org>
Tue, 12 May 2020 15:26:15 +0000 (11:26 -0400)
For IDL fields which have config_field=true, the value in the admin grid
is rendered as a link to the grid for the linked field.  Additionally,
the link contains a gridFilter so the destination grid only displays
rows related to the selected field.

Adds config_field=true values for z39.50 source and two links for hard
due date and hard due date values.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Chris Sharp <csharp@georgialibraries.org>
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts
Open-ILS/src/eg2/src/app/staff/admin/local/admin-carousel.component.ts
Open-ILS/src/eg2/src/app/staff/admin/server/floating-group/floating-group.component.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 c414118..9e91709 100644 (file)
@@ -1252,7 +1252,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
     <class id="cza" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::z3950_attr" oils_persist:tablename="config.z3950_attr" reporter:label="Z39.50 Attribute">
         <fields oils_persist:primary="id" oils_persist:sequence="config.z3950_attr_id_seq">
             <field reporter:label="Z39.50 Attribute ID" name="id" reporter:datatype="id" reporter:selector="label"/>
-            <field reporter:label="Z39.50 Source" name="source" reporter:datatype="link"/>
+            <field reporter:label="Z39.50 Source" name="source" reporter:datatype="link" config_field="true"/>
             <field reporter:label="Name" name="name" reporter:datatype="text"/>
             <field reporter:label="Label" name="label" reporter:datatype="text" oils_persist:i18n="true"/>
             <field reporter:label="Code" name="code"  reporter:datatype="int"/>
@@ -3550,9 +3550,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <field reporter:label="Always Use?" name="forceto" reporter:datatype="bool"/>
                        <field reporter:label="Current Ceiling Date" name="ceiling_date" reporter:datatype="timestamp"/>
             <field reporter:label="Owner" name="owner" reporter:datatype="org_unit"/>
+                       <field reporter:label="Values" name="values" oils_persist:virtual="true" 
+                               reporter:datatype="link" config_field="true"/>
                </fields>
                <links>
             <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="values" reltype="has_many" key="hard_due_date" map="" class="chddv"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -3567,7 +3570,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
        <class id="chddv" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::hard_due_date_values" oils_persist:tablename="config.hard_due_date_values" reporter:label="Hard Due Date Values">
                <fields oils_persist:primary="id" oils_persist:sequence="config.hard_due_date_values_id_seq">
                        <field reporter:label="ID" name="id" reporter:datatype="id"/>
-                       <field reporter:label="Hard Due Date" name="hard_due_date" reporter:datatype="link"/>
+                       <field reporter:label="Hard Due Date" name="hard_due_date" reporter:datatype="link" config_field="true"/>
                        <field reporter:label="Ceiling Date" name="ceiling_date" reporter:datatype="timestamp"/>
             <field reporter:label="Active Date" name="active_date" reporter:datatype="timestamp"/>
                </fields>
index ceed287..1c9b4c8 100644 (file)
@@ -13,6 +13,7 @@ import {IdlService} from '@eg/core/idl.service';
       <eg-staff-banner bannerText="{{classLabel}} Configuration" i18n-bannerText>
       </eg-staff-banner>
       <eg-admin-page persistKeyPfx="{{persistKeyPfx}}" idlClass="{{idlClass}}"
+        configLinkBasePath="{{configLinkBasePath}}"
         readonlyFields="{{readonlyFields}}"
         [disableOrgFilter]="disableOrgFilter"></eg-admin-page>
     `
@@ -24,6 +25,7 @@ export class BasicAdminPageComponent implements OnInit {
     classLabel: string;
     persistKeyPfx: string;
     readonlyFields = '';
+    configLinkBasePath = '/staff/admin';
 
     // Tell the admin page to disable and hide the automagic org unit filter
     disableOrgFilter: boolean;
@@ -59,6 +61,8 @@ export class BasicAdminPageComponent implements OnInit {
             // ACQ is a special case, because unlike 'server', 'local',
             // 'workstation', the schema ('acq') is the root of the path.
             this.persistKeyPfx = '';
+        } else {
+            this.configLinkBasePath += '/' + this.persistKeyPfx;
         }
 
         // Pass the readonlyFields param if available
index ab496ec..3d048d6 100644 (file)
@@ -1,4 +1,6 @@
 import {Component, Input, ViewChild, OnInit} from '@angular/core';
+import {Location} from '@angular/common';
+import {FormatService} from '@eg/core/format.service';
 import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
 import {ActivatedRoute} from '@angular/router';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
@@ -28,6 +30,8 @@ export class AdminCarouselComponent extends AdminPageComponent implements OnInit
 
     constructor(
         route: ActivatedRoute,
+        ngLocation: Location,
+        format: FormatService,
         idl: IdlService,
         org: OrgService,
         auth: AuthService,
@@ -36,7 +40,7 @@ export class AdminCarouselComponent extends AdminPageComponent implements OnInit
         toast: ToastService,
         private net: NetService
     ) {
-        super(route, idl, org, auth, pcrud, perm, toast);
+        super(route, ngLocation, format, idl, org, auth, pcrud, perm, toast);
     }
 
     ngOnInit() {
index da84179..c9f4529 100644 (file)
@@ -1,6 +1,8 @@
 import {Pager} from '@eg/share/util/pager';
 import {Component, Input, ViewChild, OnInit} from '@angular/core';
+import {Location} from '@angular/common';
 import {Router, ActivatedRoute} from '@angular/router';
+import {FormatService} from '@eg/core/format.service';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {GridDataSource} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
@@ -25,6 +27,8 @@ export class FloatingGroupComponent extends AdminPageComponent implements OnInit
 
     constructor(
         route: ActivatedRoute,
+        ngLocation: Location,
+        format: FormatService,
         idl: IdlService,
         org: OrgService,
         auth: AuthService,
@@ -33,7 +37,7 @@ export class FloatingGroupComponent extends AdminPageComponent implements OnInit
         toast: ToastService,
         private router: Router
     ) {
-        super(route, idl, org, auth, pcrud, perm, toast);
+        super(route, ngLocation, format, idl, org, auth, pcrud, perm, toast);
     }
 
     ngOnInit() {
index e0ae6c3..c05a98b 100644 (file)
   <ng-container *ngTemplateOutlet="helpTemplate"></ng-container>
 </ng-container>
 
+<ng-template #configFieldLink let-row="row" let-col="col">
+  <a i18n-title title="Link To {{col.label}}"
+    [attr.href]="configFieldLinkUrl(row, col)">{{configLinkLabel(row, col)}}</a>
+</ng-template>
+
 <eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" hideFields="{{hideGridFields}}"
     [sortable]="true" persistKey="{{persistKey}}">
   <eg-grid-toolbar-button [disabled]="!canCreate" 
   </eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
   </eg-grid-toolbar-action>
+  <ng-container *ngFor="let cf of configFields">
+    <eg-grid-column name="{{cf.name}}" [cellTemplate]="configFieldLink">
+    </eg-grid-column>
+  </ng-container>
 </eg-grid>
 
 <eg-fm-record-editor #editDialog idlClass="{{idlClass}}" 
index 6dd7632..f268498 100644 (file)
@@ -1,7 +1,9 @@
 import {Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
 import {ActivatedRoute} from '@angular/router';
+import {Location} from '@angular/common';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
-import {GridDataSource} from '@eg/share/grid/grid';
+import {FormatService} from '@eg/core/format.service';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {TranslateComponent} from '@eg/share/translate/translate.component';
 import {ToastService} from '@eg/share/toast/toast.service';
@@ -83,6 +85,9 @@ export class AdminPageComponent implements OnInit {
     // Override default values for fm-editor
     @Input() defaultNewRecord: IdlObject;
 
+    // Used as the first part of the routerLink path when creating
+    // links to related tables via configField's.
+    @Input() configLinkBasePath: string;
 
     @ViewChild('grid', { static: true }) grid: GridComponent;
     @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
@@ -96,7 +101,7 @@ export class AdminPageComponent implements OnInit {
 
     idlClassDef: any;
     pkeyField: string;
-    configFields: string[];
+    configFields: any[]; // IDL field definitions
 
     // True if any columns on the object support translations
     translateRowIdx: number;
@@ -115,6 +120,8 @@ export class AdminPageComponent implements OnInit {
 
     constructor(
         private route: ActivatedRoute,
+        private ngLocation: Location,
+        private format: FormatService,
         public idl: IdlService,
         private org: OrgService,
         public auth: AuthService,
@@ -152,6 +159,7 @@ export class AdminPageComponent implements OnInit {
     }
 
     ngOnInit() {
+
         this.idlClassDef = this.idl.classes[this.idlClass];
         this.pkeyField = this.idlClassDef.pkey || 'id';
 
@@ -165,6 +173,13 @@ export class AdminPageComponent implements OnInit {
                 this.idlClassDef.table;
         }
 
+
+        // Note the field filter could be based purely on fields
+        // which are links, but that leads to cases where links
+        // are created to tables which are too big and/or admin
+        // interfaces which are not otherwise used because they
+        // have custom UI's instead.
+        // this.idlClassDef.fields.filter(f => f.datatype === 'link');
         this.configFields =
             this.idlClassDef.fields.filter(f => f.config_field);
 
@@ -244,7 +259,10 @@ export class AdminPageComponent implements OnInit {
 
             const search: any = {};
 
-            search[this.orgField] = this.searchOrgs.orgIds || [this.contextOrg.id()];
+            if (this.orgField) {
+                search[this.orgField] =
+                    this.searchOrgs.orgIds || [this.contextOrg.id()];
+            }
 
             if (this.gridFilters) {
                 // Lay the URL grid filters over our search object.
@@ -370,6 +388,82 @@ export class AdminPageComponent implements OnInit {
 
         this.translator.open({size: 'lg'});
     }
+
+    // Construct a routerLink path for a configField.
+    configFieldRouteLink(row: any, col: GridColumn): string {
+        const cf = this.configFields.filter(field => field.name === col.name)[0];
+        const linkClass = this.idl.classes[cf['class']];
+        const pathParts = linkClass.table.split(/\./); // schema.tablename
+        return `${this.configLinkBasePath}/${pathParts[0]}/${pathParts[1]}`;
+    }
+
+    // Compiles a gridFilter value used when navigating to a linked
+    // class via configField.  The filter ensures the linked page
+    // only shows rows which refer back to the object from which the
+    // link was clicked.
+    configFieldRouteParams(row: any, col: GridColumn): any {
+        const cf = this.configFields.filter(field => field.name === col.name)[0];
+        let value = this.configFieldLinkedValue(row, col);
+
+        // For certain has-a relationships, the linked object will be
+        // fleshed so its display (selector) value can be used.
+        // Extract the scalar value found at the remote target field.
+        if (value && typeof value === 'object') { value = value[cf.key](); }
+
+        const filter: any = {};
+        filter[cf.key] = value;
+
+        return {gridFilters : JSON.stringify(filter)};
+    }
+
+    // Returns the value on the local object for the field which
+    // refers to the remote object.  This may be a scalar or a
+    // fleshed IDL object.
+    configFieldLinkedValue(row: any, col: GridColumn): any {
+        const cf = this.configFields.filter(field => field.name === col.name)[0];
+        const linkClass = this.idl.classes[cf['class']];
+
+        // cf.key is the name of the field on the linked object that matches
+        // the value on our local object.
+        // In as has_many relationship, the remote field has its own
+        // 'key' value which determines which field on the local object
+        // represents the other end of the relationship.  This is
+        // typically, but not always the local pkey field.
+
+        const localField =
+            cf.reltype === 'has_many' ?
+            (linkClass.field_map[cf.key].key || this.pkeyField) : cf.name;
+
+        return row[localField]();
+    }
+
+    // Returns a URL suitable for using as an href.
+    // We use an href to jump to the secondary admin page because
+    // routerLink within the same base component results in component
+    // reuse of a series of components which were not designed with
+    // reuse in mind.
+    configFieldLinkUrl(row: any, col: GridColumn): string {
+        const path = this.configFieldRouteLink(row, col);
+        const filters = this.configFieldRouteParams(row, col);
+        const url = path + '?gridFilters=' +
+            encodeURIComponent(filters.gridFilters);
+
+        return this.ngLocation.prepareExternalUrl(url);
+    }
+
+    configLinkLabel(row: any, col: GridColumn): string {
+        const cf = this.configFields.filter(field => field.name === col.name)[0];
+
+        // Has-many links have no specific value to use for display
+        // so just use the column label.
+        if (cf.reltype === 'has_many') { return col.label; }
+
+        return this.format.transform({
+            value: row[col.name](),
+            idlClass: this.idlClass,
+            idlField: col.name
+        });
+    }
 }