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

Evergreen Git git at git.evergreen-ils.org
Fri Sep 6 11:38:36 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  31c4a3d6553ad09a6d04426939b7ffff4b494385 (commit)
       via  a386b82e7cb954ca452f08225b8ff79b84a70ada (commit)
       via  21d1ad73ef3e24a20245d355bde9ccf834117cd5 (commit)
       via  7afc043013af6431b30c71359c5ce105ae42173e (commit)
      from  89a425c4818030d9c2138e95354156a3d748c5f4 (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 31c4a3d6553ad09a6d04426939b7ffff4b494385
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Aug 6 11:34:51 2019 -0400

    LP1823981 Angular perm group admin UX improvements
    
    * Loading progress now increments as data arrives.
    * Application perm list is now sorted
    * Deleted map now use warning background instead of danger as it appears
      to have better contrast.
    * Inherited perms display the label 'Inherited' in the delete colum.
    * Remove border boxes around non-actionable inherited values.
    * Use Bootstrap button links instead of <a> for links to perm groups to
      improve contrast and consistency.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html
index 8a68159a9c..6e1770abe3 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html
@@ -11,7 +11,7 @@
 <eg-string #createMapString i18n-text text="Permission Group Mapping Added">
 </eg-string>
 <eg-string #successMapString i18n-text text="Permission Group Mapping Update Succeeded">
-  </eg-string>
+</eg-string>
 <eg-string #errorMapString i18n-text text="Permission Group Mapping Update Failed">
 </eg-string>
 
@@ -46,7 +46,7 @@
       <ng-container *ngIf="loading">
           <div class="row">
             <div class="col-lg-6">
-              <eg-progress-inline></eg-progress-inline>
+              <eg-progress-inline #loadProgress></eg-progress-inline>
             </div>
           </div>
         </ng-container>
@@ -146,24 +146,24 @@
             </div>
 
             <div class="row" *ngFor="let map of groupPermMaps()"
-                [ngClass]="{'bg-danger': map.isdeleted()}">
+                [ngClass]="{'bg-warning': map.isdeleted()}">
               <div class="col-lg-5">
                 <span title="{{map.perm().description()}}">{{map.perm().code()}}</span>
               </div>
               <ng-container *ngIf="permIsInherited(map); else nativeMap">
                 <div class="col-lg-4">
-                  <a href="javascript:;" (click)="selectGroup(map.grp().id())">
+                  <button class="btn btn-link m-0 p-0"
+                    (click)="selectGroup(map.grp().id())">
                     {{map.grp().name()}}
-                  </a>
+                  </button>
                 </div>
                 <div class="col-lg-1 text-center">{{map.depth()}}</div>
-                <!-- TODO migrate to <eg-bool/> once merged-->
                 <div class="col-lg-1 d-flex flex-column justify-content-center">
-                  <div class="d-flex justify-content-center p-3 rounded border border-secondary">
+                  <div class="d-flex justify-content-center p-1 rounded">
                     <eg-bool [value]="map.grantable() == 't'"></eg-bool>
                   </div>
                 </div>
-                <div class="col-lg-1"> </div>
+                <div class="col-lg-1 font-italic" i18n>Inherited</div>
               </ng-container>
               <ng-template #nativeMap>
                 <div class="col-lg-4">{{map.grp().name()}}</div>
@@ -174,7 +174,7 @@
                   </select>
                 </div>
                 <div class="col-lg-1 d-flex flex-column justify-content-center">
-                  <div class="d-flex justify-content-center p-3 rounded border border-info">                
+                  <div class="d-flex justify-content-center p-1 rounded border border-info">                
                     <input type="checkbox" class="align-middle"
                       i18n-title title="Grantable?"
                       [ngModel]="map.grantable() == 't'"
@@ -182,7 +182,7 @@
                   </div>
                 </div>
                 <div class="col-lg-1 d-flex flex-column justify-content-center">
-                  <div class="d-flex justify-content-center p-3 rounded border border-danger">
+                  <div class="d-flex justify-content-center p-1 rounded border border-danger">
                     <input type="checkbox" class="align-middle"
                       i18n-title title="Delete Mapping"
                       [ngModel]="map.isdeleted()"
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
index bd3aab29b3..242f93bebc 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
@@ -11,6 +11,7 @@ import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 import {FmRecordEditorComponent, FmFieldOptions} from '@eg/share/fm-editor/fm-editor.component';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {PermGroupMapDialogComponent} from './perm-group-map-dialog.component';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
 
 /** Manage permission groups and group permissions */
 
@@ -41,6 +42,7 @@ export class PermGroupTreeComponent implements OnInit {
     @ViewChild('createMapString') createMapString: StringComponent;
     @ViewChild('errorMapString') errorMapString: StringComponent;
     @ViewChild('addMappingDialog') addMappingDialog: PermGroupMapDialogComponent;
+    @ViewChild('loadProgress') loadProgress: ProgressInlineComponent;
 
     constructor(
         private idl: IdlService,
@@ -59,9 +61,13 @@ export class PermGroupTreeComponent implements OnInit {
     async ngOnInit() {
         this.loading = true;
         await this.loadPgtTree();
+        this.loadProgress.increment();
         await this.loadPermissions();
+        this.loadProgress.increment();
         await this.loadPermMaps();
+        this.loadProgress.increment();
         this.setOrgDepths();
+        this.loadProgress.increment();
         this.loading = false;
         return Promise.resolve();
     }
@@ -126,8 +132,9 @@ export class PermGroupTreeComponent implements OnInit {
         // the database ID, because the application_perm field on
         // "pgt" is text instead of a link.  So the value it expects
         // is the code, not the ID.
-        return this.pcrud.retrieveAll('ppl', {order_by: {ppl: ['name']}})
+        return this.pcrud.retrieveAll('ppl', {order_by: {ppl: 'code'}})
         .pipe(map(perm => {
+            this.loadProgress.increment();
             this.permissions.push(perm);
             this.permEntries.push({id: perm.code(), label: perm.code()});
             this.permissions.forEach(p => this.permIdMap[+p.id()] = p);
@@ -138,7 +145,10 @@ export class PermGroupTreeComponent implements OnInit {
         this.permMaps = [];
         return this.pcrud.retrieveAll('pgpm', {},
             {fleshSelectors: true, authoritative: true})
-        .pipe(map((m => this.permMaps.push(m)))).toPromise();
+        .pipe(map(m => {
+            this.loadProgress.increment();
+            this.permMaps.push(m);
+        })).toPromise();
     }
 
     fmEditorOptions(): {[fieldName: string]: FmFieldOptions} {

commit a386b82e7cb954ca452f08225b8ff79b84a70ada
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Aug 6 11:32:18 2019 -0400

    LP1823981 eg-bool component padding
    
    Add a whiff of padding to the <eg-bool> component so the so there is
    some separation between the text and the badge border.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/share/util/bool.component.ts b/Open-ILS/src/eg2/src/app/share/util/bool.component.ts
index a7363b330d..bfbf054553 100644
--- a/Open-ILS/src/eg2/src/app/share/util/bool.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/util/bool.component.ts
@@ -6,10 +6,10 @@ import {Component, Input} from '@angular/core';
     selector: 'eg-bool',
     template: `
       <ng-container>
-        <span *ngIf="value" class="badge badge-success" i18n>Yes</span>
-        <span *ngIf="value == false" class="badge badge-secondary" i18n>No</span>
+        <span *ngIf="value" class="badge badge-success p-1" i18n>Yes</span>
+        <span *ngIf="value == false" class="badge badge-secondary p-1" i18n>No</span>
         <ng-container *ngIf="value === null">
-          <span *ngIf="ternary" class="badge badge-light" i18n>Unset</span>
+          <span *ngIf="ternary" class="badge badge-light p-1" i18n>Unset</span>
           <span *ngIf="!ternary"> </span>
       </ng-container>`
 })

commit 21d1ad73ef3e24a20245d355bde9ccf834117cd5
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Apr 9 11:05:28 2019 -0400

    LP1823981 Release notes for perm group admin
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Client/ang-perm-grp-admin.adoc b/docs/RELEASE_NOTES_NEXT/Client/ang-perm-grp-admin.adoc
new file mode 100644
index 0000000000..71f1b2870d
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Client/ang-perm-grp-admin.adoc
@@ -0,0 +1,14 @@
+Port Permission Group Admin to Angular
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Migrate the Admin => Server Admin => Permission Groups admin page to 
+Angular.
+
+As an added feature, the interface now displays inherited permissions
+alongside linked permissions for each group.  Inherited permissions
+are read-only and act to indicate to the user when a group already has
+a certain permission and therefore may not need a new one added.
+
+Additionally, a new filter option is available in the linked permissions
+interface for filtering the displayed linked permissions by code or 
+description.

commit 7afc043013af6431b30c71359c5ce105ae42173e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Apr 5 18:00:32 2019 -0400

    LP1823981 Angular Permission Group Tree Admin UI
    
    Migrate the Admin => Server Admin => Permission Groups admin page to
    Angular.
    
    As an added feature, the interface now displays inherited permissions
    alongside linked permissions for each group.  Inherited permissions
    are read-only and act to indicate to the user when a group already has
    a certain permission and therefore may not need a new one added.
    
    Additionally, a new filter option is available in the linked permissions
    interface for filtering the displayed linked permissions by code or
    description.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/core/org.service.ts b/Open-ILS/src/eg2/src/app/core/org.service.ts
index c666957d2c..ba2b4e39f8 100644
--- a/Open-ILS/src/eg2/src/app/core/org.service.ts
+++ b/Open-ILS/src/eg2/src/app/core/org.service.ts
@@ -44,6 +44,17 @@ export class OrgService {
         return this.orgList;
     }
 
+    // Returns a list of org unit type objects
+    typeList(): IdlObject[] {
+        const types = [];
+        this.list().forEach(org => {
+            if ((types.filter(t => t.id() === org.ou_type().id())).length === 0) {
+                types.push(org.ou_type());
+            }
+        });
+        return types;
+    }
+
     /**
      * Returns a list of org units that match the selected criteria.
      * All filters must match for an org to be included in the result set.
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 ce0dc3e2ae..85225faa41 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
@@ -67,7 +67,7 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit {
     // Entry ID of the default entry to select (optional)
     // onChange() is NOT fired when applying the default value,
     // unless startIdFiresOnChange is set to true.
-    @Input() startId: any;
+    @Input() startId: any = null;
     @Input() startIdFiresOnChange: boolean;
 
     @Input() idlClass: string;
@@ -187,7 +187,7 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit {
     // Apply a default selection where needed
     applySelection() {
 
-        if (this.startId &&
+        if (this.startId !== null &&
             this.entrylist && !this.defaultSelectionApplied) {
 
             const entry =
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 dcb085bcd7..730084adc9 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
@@ -39,7 +39,7 @@ export interface FmFieldOptions {
 
     // Render the field as a combobox using these values, regardless
     // of the field's datatype.
-    customValues?: {[field: string]: ComboboxEntry[]};
+    customValues?: ComboboxEntry[];
 
     // Provide / override the "selector" value for the linked class.
     // This is the field the combobox will search for typeahead.  If no
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
index d3ed6ebf41..a6e3d2b2a9 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
@@ -80,7 +80,7 @@
     <eg-link-table-link i18n-label label="Organizational Units"  
       url="/eg/staff/admin/server/legacy/actor/org_unit"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Permission Groups"  
-      url="/eg/staff/admin/server/legacy/permission/grp_tree"></eg-link-table-link>
+      routerLink="/staff/admin/server/permission/grp_tree"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Permissions"  
       routerLink="/staff/admin/server/permission/perm_list"></eg-link-table-link>
     <!-- Probably should move this to local admin once it's migrated -->
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
index cbbadd38d6..cd628a39c2 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
@@ -1,18 +1,26 @@
 import {NgModule} from '@angular/core';
 import {TreeModule} from '@eg/share/tree/tree.module';
-import {StaffCommonModule} from '@eg/staff/common.module';
 import {AdminServerRoutingModule} from './routing.module';
 import {AdminCommonModule} from '@eg/staff/admin/common.module';
 import {AdminServerSplashComponent} from './admin-server-splash.component';
 import {OrgUnitTypeComponent} from './org-unit-type.component';
 import {PrintTemplateComponent} from './print-template.component';
 import {SampleDataService} from '@eg/share/util/sample-data.service';
+import {PermGroupTreeComponent} from './perm-group-tree.component';
+import {PermGroupMapDialogComponent} from './perm-group-map-dialog.component';
+
+/* As it stands, all components defined under admin/server are
+imported / declared in the admin/server base module.  This could
+cause the module to baloon in size.  Consider moving non-auto-
+generated UI's into lazy-loadable sub-mobules. */
 
 @NgModule({
   declarations: [
       AdminServerSplashComponent,
       OrgUnitTypeComponent,
-      PrintTemplateComponent
+      PrintTemplateComponent,
+      PermGroupTreeComponent,
+      PermGroupMapDialogComponent
   ],
   imports: [
     AdminCommonModule,
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.html
new file mode 100644
index 0000000000..1c2422f3e5
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.html
@@ -0,0 +1,44 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Add New Permission Group Mapping</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="close()">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row">
+      <div class="col-lg-5" i18n>Permission Group</div>
+      <div class="col-lg-7">{{permGroup.name()}}</div>
+    </div>
+    <div class="row mt-1 pt-1">
+      <div class="col-lg-5" i18n>New Permission</div>
+      <div class="col-lg-7">
+        <eg-combobox [asyncDataSource]="permEntries"
+          (onChange)="perm = $event ? $event.id : null">
+        </eg-combobox>
+      </div>
+    </div>
+    <div class="row mt-1 pt-1">
+      <div class="col-lg-5" i18n>Depth</div>
+      <div class="col-lg-7">
+        <select [(ngModel)]="depth" class="p-1">
+          <option *ngFor="let d of orgDepths" value="{{d}}">{{d}}</option>
+        </select>
+      </div>
+    </div>
+    <div class="row mt-1 pt-1">
+      <div class="col-lg-5" i18n>Grantable</div>
+      <div class="col-lg-7">
+        <input type="checkbox" [(ngModel)]="grantable"/>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="create()" i18n>Create</button>
+    <button type="button" class="btn btn-warning" 
+      (click)="close()" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.ts
new file mode 100644
index 0000000000..2d472d70f7
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.ts
@@ -0,0 +1,109 @@
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {Observable, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+ at Component({
+  selector: 'eg-perm-group-map-dialog',
+  templateUrl: './perm-group-map-dialog.component.html'
+})
+
+/**
+ * Ask the user which part is the lead part then merge others parts in.
+ */
+export class PermGroupMapDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() permGroup: IdlObject;
+
+    @Input() permissions: IdlObject[];
+
+    // List of grp-perm-map objects that relate to the selected permission
+    // group or are linked to a parent group.
+    @Input() permMaps: IdlObject[];
+
+    @Input() orgDepths: number[];
+
+    // Note we have all of the permissions on hand, but rendering the
+    // full list of permissions can caus sluggishness.  Render async instead.
+    permEntries: (term: string) => Observable<ComboboxEntry>;
+
+    // Permissions the user may apply to the current group.
+    trimmedPerms: IdlObject[];
+
+    depth: number;
+    grantable: boolean;
+    perm: number;
+
+    constructor(
+        private idl: IdlService,
+        private pcrud: PcrudService,
+        private modal: NgbModal) {
+        super(modal);
+    }
+
+    ngOnInit() {
+        this.depth = 0;
+        this.grantable = false;
+
+        this.permissions = this.permissions
+            .sort((a, b) => a.code() < b.code() ? -1 : 1);
+
+        this.onOpen$.subscribe(() => this.trimPermissions());
+
+
+        this.permEntries = (term: string) => {
+            if (term === null || term === undefined) { return empty(); }
+            term = ('' + term).toLowerCase();
+
+            // Find entries whose code or description match the search term
+
+            const entries: ComboboxEntry[] =  [];
+            this.trimmedPerms.forEach(p => {
+                if (p.code().toLowerCase().includes(term) ||
+                    p.description().toLowerCase().includes(term)) {
+                    entries.push({id: p.id(), label: p.code()});
+                }
+            });
+
+            return from(entries);
+        };
+    }
+
+    trimPermissions() {
+        this.trimmedPerms = [];
+
+        this.permissions.forEach(p => {
+
+            // Prevent duplicate permissions, for-loop for early exit.
+            for (let idx = 0; idx < this.permMaps.length; idx++) {
+                const map = this.permMaps[idx];
+                if (map.perm().id() === p.id() &&
+                    map.grp().id() === this.permGroup.id()) {
+                    return;
+                }
+            }
+
+            this.trimmedPerms.push(p);
+        });
+    }
+
+    create() {
+        const map = this.idl.create('pgpm');
+
+        map.grp(this.permGroup.id());
+        map.perm(this.perm);
+        map.grantable(this.grantable ? 't' : 'f');
+        map.depth(this.depth);
+
+        this.pcrud.create(map).subscribe(
+            newMap => this.close(newMap),
+            err => throwError(err)
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html
new file mode 100644
index 0000000000..8a68159a9c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html
@@ -0,0 +1,205 @@
+<eg-staff-banner bannerText="Permission Group Configuration" i18n-bannerText>
+</eg-staff-banner>
+
+<eg-string #createString i18n-text text="Permission Group Mapping Added">
+</eg-string>
+<eg-string #successString i18n-text text="Permission Group Update Succeeded">
+</eg-string>
+<eg-string #errorString i18n-text text="Permission Group Update Failed">
+</eg-string>
+
+<eg-string #createMapString i18n-text text="Permission Group Mapping Added">
+</eg-string>
+<eg-string #successMapString i18n-text text="Permission Group Mapping Update Succeeded">
+  </eg-string>
+<eg-string #errorMapString i18n-text text="Permission Group Mapping Update Failed">
+</eg-string>
+
+<eg-fm-record-editor #editDialog idlClass="pgt" readonlyFields="parent"
+   [fieldOptions]="fmEditorOptions()">
+</eg-fm-record-editor>
+
+<eg-confirm-dialog #delConfirm                                                 
+  i18n-dialogTitle i18n-dialogBody                                             
+  dialogTitle="Confirm Delete"                                                 
+  dialogBody="Delete Permission Group {{selected ? selected.label : ''}}?">    
+</eg-confirm-dialog>
+
+
+<eg-perm-group-map-dialog #addMappingDialog 
+  [permGroup]="selected ? selected.callerData : null"
+  [orgDepths]="orgDepths"
+  [permMaps]="groupPermMaps()" [permissions]="permissions">
+</eg-perm-group-map-dialog>
+
+<div class="row">
+  <div class="col-lg-3">
+    <h3 i18n>Permission Groups</h3>
+    <eg-tree [tree]="tree" (nodeClicked)="nodeClicked($event)"></eg-tree>
+  </div>
+  <div class="col-lg-9">
+    <h3 i18n class="mb-3">Selected Permission Group</h3>
+    <ng-container *ngIf="!selected">
+      <div class="alert alert-info font-italic" i18n>
+        Select a permission group from the tree on the left.
+      </div>
+      <ng-container *ngIf="loading">
+          <div class="row">
+            <div class="col-lg-6">
+              <eg-progress-inline></eg-progress-inline>
+            </div>
+          </div>
+        </ng-container>
+    </ng-container>
+    <div *ngIf="selected" class="common-form striped-even">
+      <ngb-tabset>
+        <ngb-tab title="Group Details" i18n-title id="detail">
+          <ng-template ngbTabContent>
+            <div class="row">
+              <div class="col-lg-3">
+                <label i18n>Actions for Selected: </label>
+              </div>
+              <div class="col-lg-9">
+                <button class="btn btn-info mr-2" (click)="edit()" i18n>Edit</button>
+                <button class="btn btn-info mr-2" (click)="addChild()" i18n>Add Child</button>
+                <button class="btn btn-warning mr-2" (click)="remove()" i18n>Delete</button>
+              </div>
+            </div>
+            <div class="row">
+              <div class="col-lg-4">
+                <label i18n>Name: </label>
+              </div>
+              <div class="col-lg-8 font-weight-bold">
+                {{selected.callerData.name()}}
+              </div>
+            </div>
+            <div class="row">
+              <div class="col-lg-4">
+                <label i18n>Description: </label>
+              </div>
+              <div class="col-lg-8 font-weight-bold">
+                {{selected.callerData.description()}}
+              </div>
+            </div>
+            <div class="row">
+              <div class="col-lg-4">
+                <label i18n>User Expiration Interval: </label>
+              </div>
+              <div class="col-lg-8 font-weight-bold">
+                {{selected.callerData.perm_interval()}}
+              </div>
+            </div>
+            <div class="row">
+              <div class="col-lg-4">
+                <label i18n>Application Permission: </label>
+              </div>
+              <div class="col-lg-8 font-weight-bold">
+                {{selected.callerData.application_perm()}}
+              </div>
+            </div>
+            <div class="row">
+              <div class="col-lg-4">
+                <label i18n>Hold Priority: </label>
+              </div>
+              <div class="col-lg-8 font-weight-bold">
+                {{selected.callerData.hold_priority()}}
+              </div>
+            </div>
+            <div class="row">
+              <div class="col-lg-4">
+                <label i18n>User Group?: </label>
+              </div>
+              <div class="col-lg-8 font-weight-bold">
+                <!-- TODO: replace with <eg-bool/> when merged -->
+                {{selected.callerData.usergroup() == 't'}}
+              </div>
+            </div>
+          </ng-template>
+        </ngb-tab>
+        <ngb-tab title="Group Permissions" i18n-title id="perm">
+          <ng-template ngbTabContent>
+
+            <div class="row d-flex m-2 mb-3">
+              <button class="btn btn-success" (click)="applyChanges()" i18n
+                [disabled]='!changesPending()'>
+                Apply Changes
+              </button>
+              <button class="btn btn-info ml-3" (click)="openAddDialog()" i18n>
+                Add New Mapping
+              </button>
+              <div class="col-lg-3">
+                <input class="form-control ml-2" [(ngModel)]="filterText"
+                  i18n-placeholder placeholder="Filter..."/>
+              </div>
+              <button class="btn btn-outline-secondary ml-1" 
+                (click)="filterText=''" i18n>
+                Clear
+              </button>
+            </div>
+            
+            <div class="row font-weight-bold">
+              <div class="col-lg-5" i18n>Permissions</div>
+              <div class="col-lg-4" i18n>Group</div>
+              <div class="col-lg-1" i18n>Depth</div>
+              <div class="col-lg-1" i18n>Grantable?</div>
+              <div class="col-lg-1" i18n>Delete?</div>
+            </div>
+
+            <div class="row" *ngFor="let map of groupPermMaps()"
+                [ngClass]="{'bg-danger': map.isdeleted()}">
+              <div class="col-lg-5">
+                <span title="{{map.perm().description()}}">{{map.perm().code()}}</span>
+              </div>
+              <ng-container *ngIf="permIsInherited(map); else nativeMap">
+                <div class="col-lg-4">
+                  <a href="javascript:;" (click)="selectGroup(map.grp().id())">
+                    {{map.grp().name()}}
+                  </a>
+                </div>
+                <div class="col-lg-1 text-center">{{map.depth()}}</div>
+                <!-- TODO migrate to <eg-bool/> once merged-->
+                <div class="col-lg-1 d-flex flex-column justify-content-center">
+                  <div class="d-flex justify-content-center p-3 rounded border border-secondary">
+                    <eg-bool [value]="map.grantable() == 't'"></eg-bool>
+                  </div>
+                </div>
+                <div class="col-lg-1"> </div>
+              </ng-container>
+              <ng-template #nativeMap>
+                <div class="col-lg-4">{{map.grp().name()}}</div>
+                <div class="col-lg-1">
+                  <select [ngModel]="map.depth()" class="p-1"
+                    (ngModelChange)="map.depth($event); map.ischanged(true)">
+                    <option *ngFor="let d of orgDepths" value="{{d}}">{{d}}</option>
+                  </select>
+                </div>
+                <div class="col-lg-1 d-flex flex-column justify-content-center">
+                  <div class="d-flex justify-content-center p-3 rounded border border-info">                
+                    <input type="checkbox" class="align-middle"
+                      i18n-title title="Grantable?"
+                      [ngModel]="map.grantable() == 't'"
+                      (ngModelChange)="map.grantable($event ? 't' : 'f'); map.ischanged(true)"/>
+                  </div>
+                </div>
+                <div class="col-lg-1 d-flex flex-column justify-content-center">
+                  <div class="d-flex justify-content-center p-3 rounded border border-danger">
+                    <input type="checkbox" class="align-middle"
+                      i18n-title title="Delete Mapping"
+                      [ngModel]="map.isdeleted()"
+                      (ngModelChange)="map.isdeleted($event)"/>
+                  </div>
+                </div>
+              </ng-template>
+            </div>
+            <div class="row d-flex m-2 mb-3">
+              <button class="btn btn-success" (click)="applyChanges()" i18n
+                [disabled]='!changesPending()'>
+                Apply Changes
+              </button>
+            </div>
+          </ng-template>
+        </ngb-tab>
+      </ngb-tabset>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
new file mode 100644
index 0000000000..bd3aab29b3
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
@@ -0,0 +1,338 @@
+import {Component, ViewChild, OnInit} from '@angular/core';
+import {map} from 'rxjs/operators';
+import {Tree, TreeNode} from '@eg/share/tree/tree';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {FmRecordEditorComponent, FmFieldOptions} from '@eg/share/fm-editor/fm-editor.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {PermGroupMapDialogComponent} from './perm-group-map-dialog.component';
+
+/** Manage permission groups and group permissions */
+
+ at Component({
+    templateUrl: './perm-group-tree.component.html'
+})
+
+export class PermGroupTreeComponent implements OnInit {
+
+    tree: Tree;
+    selected: TreeNode;
+    permissions: IdlObject[];
+    permIdMap: {[id: number]: IdlObject};
+    permEntries: ComboboxEntry[];
+    permMaps: IdlObject[];
+    orgDepths: number[];
+    filterText: string;
+
+    // Have to fetch quite a bit of data for this UI.
+    loading: boolean;
+
+    @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+    @ViewChild('delConfirm') delConfirm: ConfirmDialogComponent;
+    @ViewChild('successString') successString: StringComponent;
+    @ViewChild('createString') createString: StringComponent;
+    @ViewChild('errorString') errorString: StringComponent;
+    @ViewChild('successMapString') successMapString: StringComponent;
+    @ViewChild('createMapString') createMapString: StringComponent;
+    @ViewChild('errorMapString') errorMapString: StringComponent;
+    @ViewChild('addMappingDialog') addMappingDialog: PermGroupMapDialogComponent;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private toast: ToastService
+    ) {
+        this.permissions = [];
+        this.permEntries = [];
+        this.permMaps = [];
+        this.permIdMap = {};
+    }
+
+
+    async ngOnInit() {
+        this.loading = true;
+        await this.loadPgtTree();
+        await this.loadPermissions();
+        await this.loadPermMaps();
+        this.setOrgDepths();
+        this.loading = false;
+        return Promise.resolve();
+    }
+
+    setOrgDepths() {
+        const depths = this.org.typeList().map(t => Number(t.depth()));
+        const depths2 = [];
+        depths.forEach(d => {
+            if (!depths2.includes(d)) {
+                depths2.push(d);
+            }
+        });
+        this.orgDepths = depths2.sort();
+    }
+
+    groupPermMaps(): IdlObject[] {
+        if (!this.selected) { return []; }
+
+        let maps = this.inheritedPermissions();
+        maps = maps.concat(
+            this.permMaps.filter(m => +m.grp().id() === +this.selected.id));
+
+        maps = this.applyFilter(maps);
+
+        return maps.sort((m1, m2) =>
+            m1.perm().code() < m2.perm().code() ? -1 : 1);
+    }
+
+    // Chop the filter text into separate words and return true if all
+    // of the words appear somewhere in the combined permission code
+    // plus description text.
+    applyFilter(maps: IdlObject[]) {
+        if (!this.filterText) { return maps; }
+        const parts = this.filterText.toLowerCase().split(' ');
+
+        maps = maps.filter(m => {
+            const target = m.perm().code().toLowerCase()
+                + ' ' + m.perm().description().toLowerCase();
+
+            for (let i = 0; i < parts.length; i++) {
+                const part = parts[i];
+                if (part && target.indexOf(part) === -1) {
+                    return false;
+                }
+            }
+
+            return true;
+        });
+
+        return maps;
+    }
+
+    async loadPgtTree(): Promise<any> {
+
+        return this.pcrud.search('pgt', {parent: null},
+            {flesh: -1, flesh_fields: {pgt: ['children']}}
+        ).pipe(map(pgtTree => this.ingestPgtTree(pgtTree))).toPromise();
+    }
+
+    async loadPermissions(): Promise<any> {
+        // ComboboxEntry's for perms uses code() for id instead of
+        // the database ID, because the application_perm field on
+        // "pgt" is text instead of a link.  So the value it expects
+        // is the code, not the ID.
+        return this.pcrud.retrieveAll('ppl', {order_by: {ppl: ['name']}})
+        .pipe(map(perm => {
+            this.permissions.push(perm);
+            this.permEntries.push({id: perm.code(), label: perm.code()});
+            this.permissions.forEach(p => this.permIdMap[+p.id()] = p);
+        })).toPromise();
+    }
+
+    async loadPermMaps(): Promise<any> {
+        this.permMaps = [];
+        return this.pcrud.retrieveAll('pgpm', {},
+            {fleshSelectors: true, authoritative: true})
+        .pipe(map((m => this.permMaps.push(m)))).toPromise();
+    }
+
+    fmEditorOptions(): {[fieldName: string]: FmFieldOptions} {
+        return {
+            application_perm: {
+                customValues: this.permEntries
+            }
+        };
+    }
+
+    // Translate the org unt type tree into a structure EgTree can use.
+    ingestPgtTree(pgtTree: IdlObject) {
+
+        const handleNode = (pgtNode: IdlObject): TreeNode => {
+            if (!pgtNode) { return; }
+
+            const treeNode = new TreeNode({
+                id: pgtNode.id(),
+                label: pgtNode.name(),
+                callerData: pgtNode
+            });
+
+            pgtNode.children()
+                .sort((c1, c2) => c1.name() < c2.name() ? -1 : 1)
+                .forEach(childNode =>
+                treeNode.children.push(handleNode(childNode))
+            );
+
+            return treeNode;
+        };
+
+        const rootNode = handleNode(pgtTree);
+        this.tree = new Tree(rootNode);
+    }
+
+    groupById(id: number): IdlObject {
+        return this.tree.findNode(id).callerData;
+    }
+
+    permById(id: number): IdlObject {
+        return this.permIdMap[id];
+    }
+
+    // Returns true if the perm map belongs to an ancestore of the
+    // currently selected group.
+    permIsInherited(m: IdlObject): boolean {
+        // We know the provided map came from this.groupPermMaps() which
+        // only returns maps for the selected group plus parent groups.
+        return m.grp().id() !== this.selected.callerData.id();
+    }
+
+    // List of perm maps that owned by perm groups which are ancestors
+    // of the selected group
+    inheritedPermissions(): IdlObject[] {
+        let maps: IdlObject[] = [];
+
+        let treeNode = this.tree.findNode(this.selected.callerData.parent());
+        while (treeNode) {
+            maps = maps.concat(
+                this.permMaps.filter(m => +m.grp().id() === +treeNode.id));
+            treeNode = this.tree.findNode(treeNode.callerData.parent());
+        }
+
+        return maps;
+    }
+
+
+    nodeClicked($event: any) {
+        this.selected = $event;
+
+        // When the user selects a different perm tree node,
+        // reset the edit state for our perm maps.
+
+        this.permMaps.forEach(m => {
+            m.isnew(false);
+            m.ischanged(false);
+            m.isdeleted(false);
+        });
+    }
+
+    edit() {
+        this.editDialog.mode = 'update';
+        this.editDialog.setRecord(this.selected.callerData);
+
+        this.editDialog.open({size: 'lg'}).subscribe(
+            success => {
+                this.successString.current().then(str => this.toast.success(str));
+            },
+            failed => {
+                this.errorString.current()
+                    .then(str => this.toast.danger(str));
+            }
+        );
+    }
+
+    remove() {
+        this.delConfirm.open().subscribe(
+            confirmed => {
+                if (!confirmed) { return; }
+
+                this.pcrud.remove(this.selected.callerData)
+                .subscribe(
+                    ok2 => {},
+                    err => {
+                        this.errorString.current()
+                          .then(str => this.toast.danger(str));
+                    },
+                    ()  => {
+                        // Avoid updating until we know the entire
+                        // pcrud action/transaction completed.
+                        this.tree.removeNode(this.selected);
+                        this.selected = null;
+                        this.successString.current().then(str => this.toast.success(str));
+                    }
+                );
+            }
+        );
+    }
+
+    addChild() {
+        const parentTreeNode = this.selected;
+        const parentType = parentTreeNode.callerData;
+
+        const newType = this.idl.create('pgt');
+        newType.parent(parentType.id());
+
+        this.editDialog.setRecord(newType);
+        this.editDialog.mode = 'create';
+
+        this.editDialog.open({size: 'lg'}).subscribe(
+            result => { // pgt object
+
+                // Add our new node to the tree
+                const newNode = new TreeNode({
+                    id: result.id(),
+                    label: result.name(),
+                    callerData: result
+                });
+                parentTreeNode.children.push(newNode);
+                this.createString.current().then(str => this.toast.success(str));
+            },
+            failed => {
+                this.errorString.current()
+                    .then(str => this.toast.danger(str));
+            }
+        );
+    }
+
+    changesPending(): boolean {
+        return this.modifiedMaps().length > 0;
+    }
+
+    modifiedMaps(): IdlObject[] {
+        return this.permMaps.filter(
+            m => m.isnew() || m.ischanged() || m.isdeleted()
+        );
+    }
+
+    applyChanges() {
+
+        const maps: IdlObject[] = this.modifiedMaps()
+            .map(m => this.idl.clone(m)); // Clone for de-fleshing
+
+        maps.forEach(m => {
+            m.grp(m.grp().id());
+            m.perm(m.perm().id());
+        });
+
+        this.pcrud.autoApply(maps).subscribe(
+            one => console.debug('Modified one mapping: ', one),
+            err => {
+                console.error(err);
+                this.errorMapString.current().then(msg => this.toast.danger(msg));
+            },
+            ()  => {
+                this.successMapString.current().then(msg => this.toast.success(msg));
+                this.loadPermMaps();
+            }
+        );
+    }
+
+    openAddDialog() {
+        this.addMappingDialog.open().subscribe(
+            modified => {
+                this.createMapString.current().then(msg => this.toast.success(msg));
+                this.loadPermMaps();
+            }
+        );
+    }
+
+    selectGroup(id: number) {
+        const node: TreeNode = this.tree.findNode(id);
+        this.tree.selectNode(node);
+        this.nodeClicked(node);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
index 4f9b9ff366..20228783ef 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
@@ -4,6 +4,7 @@ import {AdminServerSplashComponent} from './admin-server-splash.component';
 import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
 import {OrgUnitTypeComponent} from './org-unit-type.component';
 import {PrintTemplateComponent} from './print-template.component';
+import {PermGroupTreeComponent} from './perm-group-tree.component';
 
 const routes: Routes = [{
     path: 'splash',
@@ -15,6 +16,9 @@ const routes: Routes = [{
     path: 'config/print_template',
     component: PrintTemplateComponent
 }, {
+    path: 'permission/grp_tree',
+    component: PermGroupTreeComponent
+}, {
     path: ':schema/:table',
     component: BasicAdminPageComponent
 }];

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

Summary of changes:
 Open-ILS/src/eg2/src/app/core/org.service.ts       |  11 +
 .../src/app/share/combobox/combobox.component.ts   |   4 +-
 .../src/app/share/fm-editor/fm-editor.component.ts |   2 +-
 .../src/eg2/src/app/share/util/bool.component.ts   |   6 +-
 .../server/admin-server-splash.component.html      |   2 +-
 .../app/staff/admin/server/admin-server.module.ts  |  12 +-
 .../server/perm-group-map-dialog.component.html    |  44 +++
 .../server/perm-group-map-dialog.component.ts      | 109 +++++++
 .../admin/server/perm-group-tree.component.html    | 205 ++++++++++++
 .../admin/server/perm-group-tree.component.ts      | 348 +++++++++++++++++++++
 .../src/app/staff/admin/server/routing.module.ts   |   4 +
 .../Client/ang-perm-grp-admin.adoc                 |  14 +
 12 files changed, 752 insertions(+), 9 deletions(-)
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-map-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
 create mode 100644 docs/RELEASE_NOTES_NEXT/Client/ang-perm-grp-admin.adoc


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list