LP1823393 Org unit type Angular admin UI
authorBill Erickson <berickxx@gmail.com>
Thu, 24 Jan 2019 17:22:42 +0000 (12:22 -0500)
committerJane Sandberg <sandbej@linnbenton.edu>
Fri, 24 May 2019 17:56:02 +0000 (10:56 -0700)
Ports the "Organization Types" admin UI from Dojo to Angular.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
Open-ILS/src/eg2/src/app/share/tree/tree.component.ts
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-type.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-type.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts

index 3e41fa2..14aa386 100644 (file)
@@ -85,7 +85,6 @@ export class FmRecordEditorComponent
     recId: any;
 
     // IDL record we are editing
-    // TODO: allow this to be update in real time by the caller?
     record: IdlObject;
 
     // Permissions extracted from the permacrud defs in the IDL
@@ -180,6 +179,13 @@ export class FmRecordEditorComponent
         );
     }
 
+    // Set the record value and clear the recId value to
+    // indicate the record is our current source of data.
+    setRecord(record: IdlObject) {
+        this.record = record;
+        this.recId = null;
+    }
+
     // Translate comma-separated string versions of various inputs
     // to arrays.
     private listifyInputs() {
@@ -207,8 +213,16 @@ export class FmRecordEditorComponent
         };
 
         if (this.mode === 'update' || this.mode === 'view') {
-            return this.pcrud.retrieve(this.idlClass, this.recId)
-            .toPromise().then(rec => {
+
+            let promise;
+            if (this.record && this.recId === null) {
+                promise = Promise.resolve(this.record);
+            } else {
+                promise = 
+                    this.pcrud.retrieve(this.idlClass, this.recId).toPromise();
+            }
+
+            return promise.then(rec => {
 
                 if (!rec) {
                     return Promise.reject(`No '${this.idlClass}'
@@ -224,7 +238,9 @@ export class FmRecordEditorComponent
         // create a new record from scratch or from a stub record
         // provided by the caller.
         this.pkeyIsEditable = !('pkey_sequence' in this.idlDef);
-        if (!this.record) {
+        if (!this.record || this.recId) {
+            // user provided no seed record, create one.
+            this.recId = null;
             this.record = this.idl.create(this.idlClass);
         }
         return this.getFieldList();
index d3fccda..f519268 100644 (file)
@@ -47,6 +47,7 @@ export class TreeComponent implements OnInit {
     ngOnInit() {}
 
     displayNodes(): TreeNode[] {
+        if (!this.tree) { return []; }
         return this.tree.nodeList(true);
     }
 
index 5e6058d..211283e 100644 (file)
@@ -72,7 +72,7 @@
     <eg-link-table-link i18n-label label="Org Unit Proximity Adjustments"  
       routerLink="/staff/admin/server/actor/org_unit_proximity_adjustment"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Organization Types"  
-      url="/eg/staff/admin/server/legacy/actor/org_unit_type"></eg-link-table-link>
+      routerLink="/staff/admin/server/actor/org_unit_type"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Org Unit Setting Types"  
       routerLink="/staff/admin/server/config/org_unit_setting_type"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Organizational Units"  
index 1f00a8a..8e76239 100644 (file)
@@ -1,16 +1,20 @@
 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';
 
 @NgModule({
   declarations: [
-      AdminServerSplashComponent
+      AdminServerSplashComponent,
+      OrgUnitTypeComponent
   ],
   imports: [
     AdminCommonModule,
-    AdminServerRoutingModule
+    AdminServerRoutingModule,
+    TreeModule
   ],
   exports: [
   ],
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-type.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-type.component.html
new file mode 100644 (file)
index 0000000..d812d12
--- /dev/null
@@ -0,0 +1,90 @@
+<eg-staff-banner bannerText="Org Unit Type Configuration" i18n-bannerText>
+</eg-staff-banner>
+
+<ng-template #editStrTmpl i18n>Org Unit Type Update Succeeded</ng-template>
+<eg-string #editString [template]="editStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>Org Unit Type Succeessfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-template #errorStrTmpl i18n>Org Unit Type Update Failed</ng-template>
+<eg-string #errorString [template]="errorStrTmpl"></eg-string>
+
+<eg-confirm-dialog #delConfirm 
+  i18n-dialogTitle i18n-dialogBody
+  dialogTitle="Confirm Delete"
+  dialogBody="Delete Org Unit Type {{selected ? selected.label : ''}}?">
+</eg-confirm-dialog>
+
+<eg-fm-record-editor #editDialog idlClass="aout" readonlyFields="depth,parent">
+</eg-fm-record-editor>
+
+<div class="row">
+  <div class="col-lg-4">
+    <h3 i18n>Org Unit Types</h3>
+    <eg-tree [tree]="tree" (nodeClicked)="nodeClicked($event)"></eg-tree>
+  </div>
+  <div class="col-lg-8">
+    <h3 i18n class="mb-3">Selected Org Unit Type</h3>
+    <ng-container *ngIf="!selected">
+      <div class="alert alert-info font-italic" i18n>
+        Select an org unit type from the tree on the left.
+      </div>
+    </ng-container>
+    <div *ngIf="selected" class="common-form striped-even">
+      <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">
+        <!-- TODO: use FmRecordEditPaneComponent once it exists -->
+        <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>Label: </label>
+        </div>
+        <div class="col-lg-8 font-weight-bold">
+          {{selected.callerData.opac_label()}}
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-lg-4">
+          <label i18n>Can Have Users: </label>
+        </div>
+        <div class="col-lg-8 font-weight-bold">
+          <!-- TODO: use <eg-bool/> once merged-->
+          {{selected.callerData.can_have_users() == 't'}}
+        </div>
+      </div>
+      <div class="row">
+          <div class="col-lg-4">
+            <label i18n>Can Have Volumes: </label>
+          </div>
+          <div class="col-lg-8 font-weight-bold">
+            <!-- TODO: use <eg-bool/> once merged-->
+            {{selected.callerData.can_have_vols() == 't'}}
+          </div>
+        </div>
+      <div class="row">
+        <div class="col-lg-4">
+          <label i18n>Depth: </label>
+        </div>
+        <div class="col-lg-8 font-weight-bold">
+          {{selected.callerData.depth()}}
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-type.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-type.component.ts
new file mode 100644 (file)
index 0000000..1820cc8
--- /dev/null
@@ -0,0 +1,159 @@
+import {Component, Input, ViewChild, OnInit} from '@angular/core';
+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} from '@eg/share/fm-editor/fm-editor.component';
+
+@Component({
+    templateUrl: './org-unit-type.component.html'
+})
+
+export class OrgUnitTypeComponent implements OnInit {
+
+    tree: Tree;
+    selected: TreeNode;
+    @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+    @ViewChild('editString') editString: StringComponent;
+    @ViewChild('createString') createString: StringComponent;
+    @ViewChild('errorString') errorString: StringComponent;
+    @ViewChild('delConfirm') delConfirm: ConfirmDialogComponent;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private toast: ToastService
+    ) {}
+
+
+    ngOnInit() {
+        this.loadAoutTree();
+    }
+
+    loadAoutTree() {
+        this.pcrud.search('aout', {depth: 0},
+            {flesh: -1, flesh_fields: {aout: ['children']}},
+            {anonymous: true}
+        ).subscribe(aoutTree => this.ingestAoutTree(aoutTree));
+    }
+
+    // Translate the org unt type tree into a structure EgTree can use.
+    ingestAoutTree(aoutTree) {
+
+        const handleNode = (aoutNode: IdlObject): TreeNode => {
+            if (!aoutNode) { return; }
+
+            const treeNode = new TreeNode({
+                id: aoutNode.id(),
+                label: aoutNode.name(),
+                callerData: aoutNode
+            });
+
+            aoutNode.children().forEach(childNode =>
+                treeNode.children.push(handleNode(childNode))
+            );
+
+            return treeNode;
+        };
+
+        const rootNode = handleNode(aoutTree);
+        this.tree = new Tree(rootNode);
+    }
+
+    nodeClicked($event: any) {
+        this.selected = $event;
+    }
+
+    postUpdate(message: StringComponent) {
+        // Modifying org unit types means refetching the org unit
+        // data normally fetched on page load, since it includes
+        // org unit type data.
+        this.org.fetchOrgs().then(
+            ok => {
+                message.current().then(str => this.toast.success(str));
+            }
+        );
+    }
+
+    edit() {
+        this.editDialog.mode = 'update';
+        this.editDialog.setRecord(this.selected.callerData);
+
+        this.editDialog.open().then(
+            success => {
+                this.postUpdate(this.editString);
+            },
+            rejected => {
+                if (rejected && rejected.dismissed) {
+                    return;
+                }
+                this.errorString.current()
+                    .then(str => this.toast.danger(str));
+            }
+        );
+    }
+
+    remove() {
+        this.delConfirm.open().then(
+            ok => {
+                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.postUpdate(this.editString);
+                    }
+                );
+            },
+            notConfirmed => {}
+        );
+    }
+
+    addChild() {
+        const parentTreeNode = this.selected;
+        const parentType = parentTreeNode.callerData;
+
+        const newType = this.idl.create('aout');
+        newType.parent(parentType.id());
+        newType.depth(Number(parentType.depth()) + 1);
+
+        this.editDialog.setRecord(newType);
+        this.editDialog.mode = 'create';
+
+        this.editDialog.open().then(
+            result => { // aout 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.postUpdate(this.createString);
+            },
+
+            rejected => {
+                if (rejected && rejected.dismissed) {
+                    return;
+                }
+                this.errorString.current()
+                    .then(str => this.toast.danger(str));
+            }
+        );
+    }
+}
+
index ceb60f2..c971ed7 100644 (file)
@@ -2,11 +2,15 @@ import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
 import {AdminServerSplashComponent} from './admin-server-splash.component';
 import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
+import {OrgUnitTypeComponent} from './org-unit-type.component';
 
 const routes: Routes = [{
     path: 'splash',
     component: AdminServerSplashComponent
 }, {
+    path: 'actor/org_unit_type',
+    component: OrgUnitTypeComponent
+}, {
     path: ':schema/:table',
     component: BasicAdminPageComponent
 }];