--- /dev/null
+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}
+ });
+ }
+
+}
+