ui
authorJason Etheridge <jason@EquinoxOLI.org>
Mon, 1 May 2023 07:20:13 +0000 (03:20 -0400)
committerJason Etheridge <phasefx@gmail.com>
Thu, 25 May 2023 16:45:32 +0000 (12:45 -0400)
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts

index 9f49a7d..cd267fd 100644 (file)
@@ -3,6 +3,7 @@ import {TreeModule} from '@eg/share/tree/tree.module';
 import {AdminCommonModule} from '@eg/staff/admin/common.module';
 import {AdminServerRoutingModule} from './routing.module';
 import {AdminServerSplashComponent} from './admin-server-splash.component';
+import {CustomOrgUnitTreesComponent} from './custom-org-unit-trees.component';
 import {OrgUnitTypeComponent} from './org-unit-type.component';
 import {PrintTemplateComponent} from './print-template.component';
 import {SampleDataService} from '@eg/share/util/sample-data.service';
@@ -18,6 +19,7 @@ generated UI's into lazy-loadable sub-mobules. */
   declarations: [
       AdminServerSplashComponent,
       OrgUnitTypeComponent,
+      CustomOrgUnitTreesComponent,
       PrintTemplateComponent,
       PermGroupTreeComponent,
       PermGroupMapDialogComponent
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.css b/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.css
new file mode 100644 (file)
index 0000000..7b27960
--- /dev/null
@@ -0,0 +1,14 @@
+.org-unit-types-row {
+    align-items: stretch;
+}
+
+.org-unit-types-col-aside {
+    height: 100%;
+}
+
+eg-tree { 
+    display: block;
+    height: 100%; 
+    overflow-y: auto;
+    width: 100%;
+}
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.html
new file mode 100644 (file)
index 0000000..9dc9ff6
--- /dev/null
@@ -0,0 +1,47 @@
+<eg-staff-banner bannerText="Custom Org Unit Trees" i18n-bannerText>
+</eg-staff-banner>
+
+<ng-template #editStrTmpl i18n>Update Succeeded</ng-template>
+<eg-string #editString [template]="editStrTmpl"></eg-string>
+
+<ng-template #errorStrTmpl i18n>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 {{selected ? selected.label : ''}}?">
+</eg-confirm-dialog>
+
+<ng-template #treeNodeLabelTmpl let-org="org">
+  <span *ngIf="org" i18n>{{org.name()}} -- {{org.shortname()}}</span>
+</ng-template>
+<eg-string #treeNodeLabel key='admin.server.org_unit.treenode'
+  [template]="treeNodeLabelTmpl"></eg-string>
+
+<div class="row org-unit-types-row">
+  <div class="col-lg-4 org-unit-types-col-aside">
+    <h3 i18n>Full Org Unit Tree</h3>
+    <div class="border rounded p-1" >
+      <eg-tree
+        showTogglers="true"
+        showSelectors="true"
+        disableRootSelector="true"
+        [tree]="tree"
+        (nodeClicked)="nodeClicked($event)">
+      </eg-tree>
+    </div>
+  </div>
+  <div class="col-lg-4 org-unit-types-col-aside">
+    <h3 i18n>Custom Org Unit Tree</h3>
+    <div class="border rounded p-1" >
+      <eg-tree
+        showTogglers="true"
+        showSelectors="true"
+        disableRootSelector="true"
+        [tree]="custom_tree"
+        (nodeClicked)="custom_nodeClicked($event)">
+      </eg-tree>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.ts
new file mode 100644 (file)
index 0000000..86d2433
--- /dev/null
@@ -0,0 +1,285 @@
+import {Component, ViewChild, OnInit} from '@angular/core';
+import { firstValueFrom } from 'rxjs';
+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 {StringService} from '@eg/share/string/string.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+    templateUrl: './custom-org-unit-trees.component.html',
+    styleUrls: [ './custom-org-unit-trees.component.css' ],
+})
+
+export class CustomOrgUnitTreesComponent implements OnInit {
+
+    tree: Tree;
+    custom_tree: Tree;
+    selected: TreeNode;
+    custom_selected: TreeNode;
+    orgUnitTab: string;
+
+    @ViewChild('editString', { static: true }) editString: StringComponent;
+    @ViewChild('errorString', { static: true }) errorString: StringComponent;
+    @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private strings: StringService,
+        private toast: ToastService
+    ) {}
+
+
+    ngOnInit() {
+        this.loadAouTree(this.org.root().id());
+        this.loadCustomTree();
+    }
+
+    orgSaved(orgId: number | IdlObject) {
+        let id: number;
+
+        if (orgId) { // new org created, focus it.
+            id = typeof orgId === 'object' ? orgId.id() : orgId;
+        } else if (this.currentOrg()) {
+            id = this.currentOrg().id();
+        }
+
+        this.loadAouTree(id).then(_ => this.postUpdate(this.editString));
+    }
+
+    orgDeleted() {
+        this.loadAouTree();
+    }
+
+
+    async loadAouTree(selectNodeId?: number): Promise<any> {
+        const flesh = ['children', 'ou_type', 'hours_of_operation'];
+
+        try {
+            const tree = await firstValueFrom(this.pcrud.search('aou', {parent_ou : null},
+                {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
+            ));
+
+            this.ingestAouTree(tree);
+            if (!selectNodeId) { selectNodeId = this.org.root().id(); }
+
+            const node = this.tree.findNode(selectNodeId);
+            this.selected = node;
+            this.tree.selectNode(node);
+
+            return this.tree;
+        } catch (E) {
+            console.warn('caught from pcrud (aou)', E);
+        }
+    }
+
+    async loadCustomTree(): Promise<any> {
+
+        const flesh = ['children', 'org_unit'];
+
+        let tree_type: IdlObject;
+        try {
+            tree_type = await firstValueFrom(
+                this.pcrud.search('aouct', { purpose: 'opac', })
+            );
+        } catch(E) {
+            console.warn('caught from pcrud (aouct)', E);
+            tree_type = null;
+        }
+        let tree_id: number;
+        if (tree_type) {
+            tree_id = tree_type.id();
+        } else {
+            tree_id = null;
+        }
+
+        let tree: IdlObject;
+        try {
+            tree = await firstValueFrom(
+                this.pcrud.search('aouctn', {tree: tree_id, parent_node : null, },
+                {flesh : -1, flesh_fields : {aouctn : flesh}}, {authoritative: true}
+            ));
+        } catch(E) {
+            console.warn('phasefx: caught from pcrud (aouctn)', E);
+            tree = null;
+        }
+        
+        console.warn('custom tree', tree);
+        this.ingestCustomTree(tree);
+        return this.custom_tree;
+    }
+
+    // Translate the org unt type tree into a structure EgTree can use.
+    ingestAouTree(aouTree: IdlObject) {
+
+        const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
+            if (!orgNode) { return; }
+
+            const treeNode = new TreeNode({
+                id: orgNode.id(),
+                label: orgNode.name(),
+                callerData: {orgUnit: orgNode},
+                expanded: expand
+            });
+
+            // Apply the compiled label asynchronously
+            this.strings.interpolate(
+                'admin.server.org_unit.treenode', {org: orgNode}
+            ).then(label => treeNode.label = label);
+
+            // Tree node labels are "name -- shortname".  Sorting
+            // by name suffices and bypasses the need the wait
+            // for all of the labels to interpolate.
+            orgNode.children()
+            .sort((a: IdlObject, b: IdlObject) => a.name() < b.name() ? -1 : 1)
+            .forEach((childNode: IdlObject) =>
+                treeNode.children.push(handleNode(childNode))
+            );
+
+            return treeNode;
+        };
+
+        const rootNode = handleNode(aouTree, true);
+        this.tree = new Tree(rootNode);
+    }
+    
+    ingestCustomTree(aouctnTree: IdlObject) {
+
+        const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
+            if (!orgNode) { return; }
+
+            const treeNode = new TreeNode({
+                id: orgNode.id(),
+                label: orgNode.org_unit().name(),
+                callerData: {orgUnit: orgNode},
+                expanded: expand
+            });
+
+            // Apply the compiled label asynchronously
+            this.strings.interpolate(
+                'admin.server.org_unit.treenode', {org: orgNode.org_unit()}
+            ).then(label => treeNode.label = label);
+
+            // Tree node labels are "name -- shortname".  Sorting
+            // by name suffices and bypasses the need the wait
+            // for all of the labels to interpolate.
+            orgNode.children()
+            .sort((a: IdlObject, b: IdlObject) => a.org_unit().name() < b.org_unit().name() ? -1 : 1)
+            .forEach((childNode: IdlObject) =>
+                treeNode.children.push(handleNode(childNode))
+            );
+
+            return treeNode;
+        };
+
+        const rootNode = handleNode(aouctnTree, true);
+        this.custom_tree = new Tree(rootNode);
+    }
+
+    nodeClicked($event: any) {
+        this.selected = $event;
+    }
+
+    custom_nodeClicked($event: any) {
+        this.custom_selected = $event;
+    }
+    
+    currentOrg(): IdlObject {
+        return this.selected ? this.selected.callerData.orgUnit : null;
+    }
+
+    orgHasChildren(): boolean {
+        const org = this.currentOrg();
+        return (org && org.children().length > 0);
+    }
+
+    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(() =>
+            message.current().then(str => this.toast.success(str)));
+    }
+
+    remove() {
+        this.delConfirm.open().subscribe(confirmed => {
+            if (!confirmed) { return; }
+
+            const org = this.selected.callerData.orgUnit;
+
+            this.pcrud.remove(org).subscribe(
+                ok2 => {},
+                err => {
+                    this.errorString.current()
+                      .then(str => this.toast.danger(str));
+                },
+                ()  => {
+                    // Avoid updating until we know the entire
+                    // pcrud action/transaction completed.
+                    // After removal, select the parent org if available
+                    // otherwise the root org.
+                    const orgId = org.parent_ou() ?
+                        org.parent_ou() : this.org.root().id();
+                    this.loadAouTree(orgId).then(_ =>
+                        this.postUpdate(this.editString));
+                }
+            );
+        });
+    }
+
+    orgTypeOptions(): ComboboxEntry[] {
+        let ouType = this.currentOrg().ou_type();
+
+        if (typeof ouType === 'number') {
+            // May not be fleshed for new org units
+            ouType = this.org.typeMap()[ouType];
+        }
+        const curDepth = ouType.depth();
+
+        return this.org.typeList()
+            .filter(type_ => type_.depth() === curDepth)
+            .map(type_ => ({id: type_.id(), label: type_.name()}));
+    }
+
+    orgChildTypes(): IdlObject[] {
+        let ouType = this.currentOrg().ou_type();
+
+        if (typeof ouType === 'number') {
+            // May not be fleshed for new org units
+            ouType = this.org.typeMap()[ouType];
+        }
+
+        const depth = ouType.depth();
+        return this.org.typeList()
+            .filter(type_ => type_.depth() === depth + 1);
+    }
+
+    addChild() {
+        const parentTreeNode = this.selected;
+        const parentOrg = this.currentOrg();
+        const newType = this.orgChildTypes()[0];
+
+        const org = this.idl.create('aou');
+        org.isnew(true);
+        org.parent_ou(parentOrg.id());
+        org.ou_type(newType.id());
+        org.children([]);
+
+        // Create a dummy, detached org node to keep the UI happy.
+        this.selected = new TreeNode({
+            id: org.id(),
+            label: org.name(),
+            callerData: {orgUnit: org}
+        });
+    }
+
+}
+
index caadbcb..6374668 100644 (file)
@@ -3,6 +3,7 @@ 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';
+import {CustomOrgUnitTreesComponent} from './custom-org-unit-trees.component';
 import {PrintTemplateComponent} from './print-template.component';
 import {PermGroupTreeComponent} from './perm-group-tree.component';
 
@@ -13,6 +14,9 @@ const routes: Routes = [{
     path: 'actor/org_unit_type',
     component: OrgUnitTypeComponent
 }, {
+    path: 'actor/custom_org_unit_trees',
+    component: CustomOrgUnitTreesComponent
+}, {
     path: 'config/coded_value_map',
     loadChildren: () =>
       import('./coded-value-maps/coded-value-maps.module').then(m => m.CodedValueMapsModule)