From 7369e807e5fd32117c2f5f1403ebef759be02eb9 Mon Sep 17 00:00:00 2001 From: Jason Etheridge Date: Mon, 1 May 2023 03:20:13 -0400 Subject: [PATCH] ui --- .../app/staff/admin/server/admin-server.module.ts | 2 + .../server/custom-org-unit-trees.component.css | 14 + .../server/custom-org-unit-trees.component.html | 47 ++++ .../server/custom-org-unit-trees.component.ts | 285 +++++++++++++++++++++ .../src/app/staff/admin/server/routing.module.ts | 4 + 5 files changed, 352 insertions(+) create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.css create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.ts 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 9f49a7d98f..cd267fd19e 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 @@ -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 index 0000000000..7b27960df7 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.css @@ -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 index 0000000000..9dc9ff657b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.html @@ -0,0 +1,47 @@ + + + +Update Succeeded + + +Update Failed + + + + + + + {{org.name()}} -- {{org.shortname()}} + + + +
+
+

Full Org Unit Tree

+
+ + +
+
+
+

Custom Org Unit Tree

+
+ + +
+
+
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 index 0000000000..86d2433169 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/custom-org-unit-trees.component.ts @@ -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 { + 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 { + + 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} + }); + } + +} + 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 caadbcb897..6374668fce 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 @@ -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) -- 2.11.0