From b901a7e97d7e7be96aceb8cff3cbec6ac83b6cc6 Mon Sep 17 00:00:00 2001
From: Bill Erickson <berickxx@gmail.com>
Date: Thu, 3 Jan 2019 10:17:42 -0500
Subject: [PATCH] LP1809288 Angular fm-editor read-only additions

* Add read-only view to org-select
* FM editor displays read-only values as plain text
* Mark readOnly checkboxes "disabled"
* Link fields only fetch linked data when an IDL selector exists
* Minor code/style changes
** Define all class vars before class methods (ng-lint)
** Replace some tabs with spaces (ng-lint)
** Avoid unnecessary type defs with default values (ng-lint)
** More const goodness (ng-lint)
** camelCase some vars for consistentcy

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
---
 .../app/share/fm-editor/fm-editor.component.html   | 180 ++++++++++++---------
 .../src/app/share/fm-editor/fm-editor.component.ts |  46 ++++--
 .../app/share/org-select/org-select.component.html |  32 ++--
 .../app/share/org-select/org-select.component.ts   |   9 +-
 .../app/staff/admin/basic-admin-page.component.ts  |   8 +-
 5 files changed, 169 insertions(+), 106 deletions(-)

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 4aab72d7b4..233c3027d9 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
@@ -27,28 +27,42 @@
               {{record[field.name]()}}
             </span>
   
-            <input *ngIf="field.datatype == 'id' && pkeyIsEditable"
-              class="form-control"
-              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 *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>
   
-            <input *ngIf="field.datatype == 'text' || field.datatype == 'interval'"
-              class="form-control"
-              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 
+              *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>
+            </ng-container>
 
+            <!-- TODO: add support to eg-date-select for read-only view -->
             <span *ngIf="field.datatype == 'timestamp'">
               <eg-date-select
                 domId="{{idPrefix}}-{{field.name}}"
@@ -57,83 +71,101 @@
               </eg-date-select>
             </span>
 
-            <input *ngIf="field.datatype == 'int'"
-              class="form-control"
-              type="number"
-              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)"/>
-  
-            <input *ngIf="field.datatype == 'float'"
-              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)"/>
-  
-            <span *ngIf="field.datatype == 'money'">
-              <!-- in read-only mode display the local-aware currency -->
-              <input *ngIf="field.readOnly"
-                class="form-control"
-                type="number" step="0.1"
-                name="{{field.name}}"
-                id="{{idPrefix}}-{{field.name}}"
-                [readonly]="field.readOnly"
-                [required]="field.isRequired()"
-                [ngModel]="record[field.name]() | currency"/>
-  
-              <input *ngIf="!field.readOnly"
+            <ng-container *ngIf="field.datatype == 'int'">
+              <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"
+                type="number"
                 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)"/>
-            </span>
+              </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>
+      
+            <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>
   
             <input *ngIf="field.datatype == 'bool'"
               class="form-check-input"
               type="checkbox"
               name="{{field.name}}"
               id="{{idPrefix}}-{{field.name}}"
-              [readonly]="field.readOnly"
+              [disabled]="field.readOnly"
               [ngModel]="record[field.name]()"
               (ngModelChange)="record[field.name]($event)"/>
   
-            <span *ngIf="field.datatype == 'link'"
-              [ngClass]="{nullable : !field.isRequired()}">
-              <select
-                class="form-control"
-                name="{{field.name}}"
-                id="{{idPrefix}}-{{field.name}}"
-                [disabled]="field.readOnly"
-                [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 *ngIf="field.datatype == 'link'">
+              <ng-container *ngIf="field.readOnly">
+                <!-- in readOnly mode, if a value is presetn, it will
+                    live as the only item in the linkedValues array -->
+                <ng-container *ngIf="field.linkedValues[0]">
+                  <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>
   
             <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)">
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 7b90a02437..25d05525bd 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
@@ -100,6 +100,9 @@ export class FmRecordEditorComponent
     // list of fields on the IDL, since some are hidden, virtual, etc.
     fields: any[];
 
+    // DOM id prefix to prevent id collisions.
+    idPrefix: string;
+
     @Input() editMode(mode: 'create' | 'update' | 'view') {
         this.mode = mode;
     }
@@ -110,8 +113,6 @@ export class FmRecordEditorComponent
         if (id) { this.recId = id; }
     }
 
-    idPrefix: string;
-
     constructor(
       private modal: NgbModal, // required for passing to parent
       private idl: IdlService,
@@ -127,8 +128,8 @@ export class FmRecordEditorComponent
         this.idlDef = this.idl.classes[this.idlClass];
         this.recordLabel = this.idlDef.label;
 
-	// Add some randomness to the generated DOM IDs to ensure against clobbering
-	this.idPrefix = 'fm-editor-' + Math.floor(Math.random() * 100000);
+        // Add some randomness to the generated DOM IDs to ensure against clobbering
+        this.idPrefix = 'fm-editor-' + Math.floor(Math.random() * 100000);
     }
 
     // Opening dialog, fetch data.
@@ -256,16 +257,33 @@ export class FmRecordEditorComponent
                 };
             }
 
-            if (field.datatype === 'link' && field.readOnly) { // no need to fetch all possible values for read-only fields
-                let id_to_fetch = this.record[field.name]();
-                if (id_to_fetch) {
-                    promises.push(
-                        this.pcrud.retrieve(field.class, this.record[field.name]())
-                        .toPromise().then(list => {
-                            field.linkedValues =
-                                this.flattenLinkedValues(field.class, Array(list));
-                        })
-                    );
+            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 idField = this.idl.classes[field.class].pkey;
+                    const selector =
+                        this.idl.classes[field.class].field_map[idField].selector;
+
+                    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}];
+                    }
                 }
             } else if (field.datatype === 'link') {
                 promises.push(
diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
index b03211420d..d4ffd53cc0 100644
--- a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
+++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
@@ -4,15 +4,23 @@
 {{r.label}}
 </ng-template>
 
-<input type="text" 
-  class="form-control"
-  [attr.id]="domId.length ? domId : null"
-  [placeholder]="placeholder"
-  [(ngModel)]="selected" 
-  [ngbTypeahead]="filter"
-  [resultTemplate]="displayTemplate"
-  [inputFormatter]="formatter"
-  (click)="click$.next($event.target.value)"
-  (selectItem)="orgChanged($event)"
-  #instance="ngbTypeahead"
-/>
+<ng-container *ngIf="readOnly">
+  <span>{{selected.label}}</span>
+</ng-container>
+
+<ng-container *ngIf="!readOnly">
+
+  <input type="text" 
+    class="form-control"
+    [attr.id]="domId.length ? domId : null"
+    [placeholder]="placeholder"
+    [(ngModel)]="selected" 
+    [ngbTypeahead]="filter"
+    [resultTemplate]="displayTemplate"
+    [inputFormatter]="formatter"
+    (click)="click$.next($event.target.value)"
+    (selectItem)="orgChanged($event)"
+    #instance="ngbTypeahead"
+  />
+
+</ng-container>
diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
index 598a9886ae..25ed2b0b3a 100644
--- a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
@@ -54,6 +54,8 @@ export class OrgSelectComponent implements OnInit {
     // An onChange event WILL be generated when a default is applied.
     @Input() applyDefault = false;
 
+    @Input() readOnly = false;
+
     // List of org unit IDs to exclude from the selector
     @Input() set hideOrgs(ids: number[]) {
         if (ids) { this.hidden = ids; }
@@ -153,10 +155,13 @@ export class OrgSelectComponent implements OnInit {
 
     // Format for display in the selector drop-down and input.
     formatForDisplay(org: IdlObject): OrgDisplay {
+        let label = org[this.displayField]();
+        if (!this.readOnly) {
+            label = PAD_SPACE.repeat(org.ou_type().depth()) + label;
+        }
         return {
             id : org.id(),
-            label : PAD_SPACE.repeat(org.ou_type().depth())
-              + org[this.displayField](),
+            label : label,
             disabled : false
         };
     }
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 0427646abc..908dadf1da 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
@@ -19,7 +19,7 @@ export class BasicAdminPageComponent implements OnInit {
     idlClass: string;
     classLabel: string;
     persistKeyPfx: string;
-    readonlyFields: string = '';
+    readonlyFields = '';
 
     constructor(
         private route: ActivatedRoute,
@@ -39,7 +39,7 @@ export class BasicAdminPageComponent implements OnInit {
             const data = this.route.snapshot.data[0];
             if (data) { table = data.table; }
         }
-        const full_table = schema + '.' + table;
+        const fullTable = schema + '.' + table;
 
 
         // Set the prefix to "server", "local", "workstation",
@@ -59,14 +59,14 @@ export class BasicAdminPageComponent implements OnInit {
 
         Object.keys(this.idl.classes).forEach(class_ => {
             const classDef = this.idl.classes[class_];
-            if (classDef.table === full_table) {
+            if (classDef.table === fullTable) {
                 this.idlClass = class_;
                 this.classLabel = classDef.label;
             }
         });
 
         if (!this.idlClass) {
-            throw new Error('Unable to find IDL class for table ' + full_table);
+            throw new Error('Unable to find IDL class for table ' + fullTable);
         }
     }
 }
-- 
2.11.0