1 /* eslint-disable no-await-in-loop, no-shadow */
2 import {Component, ViewChild, OnInit} from '@angular/core';
3 import {catchError, firstValueFrom, lastValueFrom, of, take, defaultIfEmpty} from 'rxjs';
4 import {Tree, TreeNode} from '@eg/share/tree/tree';
5 import {IdlService, IdlObject} from '@eg/core/idl.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {ToastService} from '@eg/share/toast/toast.service';
9 import {StringComponent} from '@eg/share/string/string.component';
10 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
11 import {CustomOrgUnitTreesDialogComponent} from './custom-org-unit-trees-dialog.component';
14 templateUrl: './custom-org-unit-trees.component.html',
15 styleUrls: [ './custom-org-unit-trees.component.css' ],
18 export class CustomOrgUnitTreesComponent implements OnInit {
22 aouctn_root: IdlObject;
26 custom_selected: TreeNode;
28 singleNodeSelected = false;
29 multipleNodesSelected = false;
30 noNodesSelected = false;
32 @ViewChild('editString', { static: true }) editString: StringComponent;
33 @ViewChild('errorString', { static: true }) errorString: StringComponent;
34 @ViewChild('successString', { static: true }) successString: StringComponent;
35 @ViewChild('updateFailedString', { static: true }) updateFailedString: StringComponent;
36 @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
37 @ViewChild('moveNodeElsewhereDialog', { static: true })
38 moveNodeElsewhereDialog: CustomOrgUnitTreesDialogComponent;
41 private idl: IdlService,
42 private org: OrgService,
43 private pcrud: PcrudService,
44 // private strings: StringService,
45 private toast: ToastService
51 await this.loadAouTree(this.org.root().id());
52 await this.loadCustomTree('opac');
53 // console.warn('CustomOrgUnitTreesComponent, this', this);
55 console.error('caught during ngOnInit',E);
59 async loadAouTree(selectNodeId?: number): Promise<any> {
60 const flesh = ['children', 'ou_type', 'hours_of_operation'];
63 const tree = await firstValueFrom(this.pcrud.search('aou', {parent_ou : null},
64 {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
67 this.ingestAouTree(tree); // sets this.tree as a side-effect
68 if (!selectNodeId) { selectNodeId = this.org.root().id(); }
70 /* const node = this.tree.findNode(selectNodeId);
72 this.tree.selectNode(node);*/
76 console.warn('caught from pcrud (aou)', E);
80 async loadCustomTree(purpose: string): Promise<any> {
81 const flesh = ['children', 'org_unit'];
83 this.tree_type = await firstValueFrom(
84 this.pcrud.search('aouct', { purpose: purpose })
87 defaultIfEmpty(undefined),
88 catchError((err: unknown) => {
89 console.warn('caught from pcrud (aouct): 1', err);
97 tree_id = this.tree_type.id();
98 this.active = this.tree_type.active() === 't';
103 this.aouctn_root = undefined;
105 this.aouctn_root = await firstValueFrom(
106 this.pcrud.search('aouctn', {tree: tree_id, parent_node: null},
107 {flesh: -1, flesh_fields: {aouctn: flesh}}, {authoritative: true})
110 defaultIfEmpty(undefined),
111 catchError((err: unknown) => {
112 console.warn('phasefx: caught from pcrud (aouctn): 2', err);
113 return of(undefined);
118 this.tree_type = this.idl.create('aouct');
119 this.tree_type.isnew('t');
120 this.tree_type.purpose('opac');
121 this.tree_type.active(this.active ? 't' : 'f');
123 if (this.aouctn_root) {
124 this.ingestCustomTree(this.aouctn_root); // sets this.custom_tree as a side-effect
126 this.custom_tree = this.tree.clone();
128 return this.custom_tree;
131 // Translate the org unt type tree into a structure EgTree can use.
132 ingestAouTree(aouTree: IdlObject) {
134 const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
135 if (!orgNode) { return; }
137 const treeNode = new TreeNode({
139 label: orgNode.name() + '--' + orgNode.shortname(),
140 callerData: {orgId: orgNode.id()},
144 // Tree node labels are "name -- shortname". Sorting
145 // by name suffices and bypasses the need the wait
146 // for all of the labels to interpolate.
148 .sort((a: IdlObject, b: IdlObject) => a.name() < b.name() ? -1 : 1)
149 .forEach((childNode: IdlObject) =>
150 treeNode.children.push(handleNode(childNode))
156 const rootNode = handleNode(aouTree, true);
157 this.tree = new Tree(rootNode);
160 ingestCustomTree(aouctnTree: IdlObject) {
162 const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
163 if (!orgNode) { return; }
165 const treeNode = new TreeNode({
167 label: orgNode.org_unit().name() + '--' + orgNode.org_unit().shortname(),
168 callerData: {orgId: orgNode.org_unit().id()},
173 .sort((a: IdlObject, b: IdlObject) => a.sibling_order() < b.sibling_order() ? -1 : 1)
174 .forEach((childNode: IdlObject) =>
175 treeNode.children.push(handleNode(childNode))
181 const rootNode = handleNode(aouctnTree, true);
182 this.custom_tree = new Tree(rootNode);
185 nodeClicked($event: any) {
186 // this.selected = $event;
187 // console.log('custom: nodeClicked',typeof $event);
190 custom_nodeClicked($event: any) {
191 // this.custom_selected = $event;
192 // console.log('custom: custom_nodeClicked',typeof $event);
195 nodeChecked($event: any) {
196 // this.selected = $event;
197 // console.log('custom: nodeChecked',typeof $event);
200 custom_nodeChecked($event: any) {
201 // this.custom_selected = $event;
202 // console.log('custom: custom_nodeChecked',typeof $event);
205 isCopyNodesAllowed(): boolean {
208 // console.log('isCopyNodesAllowed: tree not ready', false);
211 const sourceNodes = this.tree.selectedNodes();
212 if (sourceNodes.length === 0) {
213 // console.log('isCopyNodesAllowed: no sourceNodes selected', false);
216 const destinationNode = this.custom_tree.selectedNode();
217 if (!destinationNode) {
218 // console.log('isCopyNodesAllowed: no destinationNode selected', false);
221 for (const sourceNode of sourceNodes) {
222 if (this.custom_tree.findNodesByFieldAndValue('label', sourceNode.label).length > 0) {
223 // console.log('isCopyNodesAllowed: selected SourceNode already in custom_tree', false);
226 if (sourceNode === this.tree.rootNode) {
227 // console.log('isCopyNodesAllowed: rootNode is sacrosanct', false);
231 // console.log('isCopyNodesAllowed', true);
234 console.log('isCopyNodesAllowed, error', E);
240 // console.log('copyNodes');
241 const sourceNodes = this.tree.selectedNodes();
242 const targetNode = this.custom_tree.selectedNode();
243 if (!this.isCopyNodesAllowed()) {
246 this._copyNodes(sourceNodes, targetNode, false);
249 _copyNodes(sourceNodes: TreeNode[], targetNode: TreeNode, cloneChildren = true) {
250 // console.log('_copyNodes', { sourceNodes: sourceNodes, targetNode: targetNode });
251 const traverseTreeAndCopySourceNodes = (currentNode: TreeNode, targetNode: TreeNode) => {
252 // console.log('traverseTreeAndCopySourceNodes', currentNode.label);
253 if (sourceNodes.map(n => n.label).includes(currentNode.label)) {
254 // console.log('found a source node, copying',currentNode.label);
255 const newNode = currentNode.clone();
256 if (!cloneChildren) {
257 newNode.children = [];
259 targetNode.children.push(newNode);
260 targetNode = newNode;
263 for (const childNode of currentNode.children) {
264 traverseTreeAndCopySourceNodes(childNode, targetNode);
268 traverseTreeAndCopySourceNodes(this.tree.rootNode, targetNode);
269 this.custom_tree.nodeList(); // re-index
272 isDeleteNodesAllowed(): boolean {
274 if (!this.custom_tree) {
275 // console.log('isDeleteNodesAllowed: custom_tree not ready', false);
278 const targetNodes = this.custom_tree.selectedNodes();
279 if (targetNodes.length === 0) {
280 // console.log('isDeleteNodesAllowed: no targetNodes selected', false);
283 for (const targetNode of targetNodes) {
284 if (targetNode === this.custom_tree.rootNode) {
285 // console.log('isDeleteNodesAllowed: rootNode is sacrosanct', false);
289 // console.log('isDeleteNodesAllowed', true);
292 console.log('isDeleteNodesAllowed, error', E);
297 deleteNodes(targetNodes: TreeNode[]) {
298 if (! this.isDeleteNodesAllowed()) {
301 if (! window.confirm($localize`Are you sure?`)) {
305 // Sort nodes by depth in descending order
306 targetNodes.sort((a, b) => b.depth - a.depth);
308 for (const targetNode of targetNodes) {
309 if (targetNode !== this.custom_tree.rootNode) {
310 // console.log('removing node',targetNode);
311 this.custom_tree.removeNode(targetNode);
314 this.custom_tree.nodeList(); // re-index
317 deleteNode(node: TreeNode) {
318 this.deleteNodes([node]);
321 deleteSelectedNodes() {
322 this.deleteNodes(this.custom_tree.selectedNodes());
325 isMoveNodeUpAllowed(node: TreeNode): boolean {
326 const parentNode = this.custom_tree.findParentNode(node);
328 const index = parentNode.children.indexOf(node);
336 moveNodeUp(node: TreeNode) {
337 const selectedNode = node || this.custom_tree.selectedNode();
338 if (!this.isMoveNodeUpAllowed(node)) {
341 const parentNode = this.custom_tree.findParentNode(selectedNode);
343 const index = parentNode.children.indexOf(selectedNode);
345 // Swap the selected node with its previous sibling.
346 const temp = parentNode.children[index - 1];
347 parentNode.children[index - 1] = selectedNode;
348 parentNode.children[index] = temp;
349 this.custom_tree.nodeList(); // re-index
354 isMoveNodeDownAllowed(node: TreeNode): boolean {
355 const parentNode = this.custom_tree.findParentNode(node);
357 const index = parentNode.children.indexOf(node);
358 if (index < parentNode.children.length - 1) {
367 moveNodeDown(node: TreeNode) {
368 if (!this.isMoveNodeDownAllowed(node)) {
371 const parentNode = this.custom_tree.findParentNode(node);
373 const index = parentNode.children.indexOf(node);
374 if (index < parentNode.children.length - 1) {
375 // Swap the selected node with its next sibling.
376 const temp = parentNode.children[index + 1];
377 parentNode.children[index + 1] = node;
378 parentNode.children[index] = temp;
379 this.custom_tree.nodeList(); // re-index
384 isMoveNodeElsewhereAllowed(node: TreeNode): boolean {
385 return node !== this.custom_tree.rootNode;
388 moveNodeElsewhere() {
389 const nodeToMove = this.custom_tree.selectedNode();
390 const selectionTree = this.custom_tree.clone();
392 // prune nodeToMove and descendants from destination selection tree
393 const equivalentNode = selectionTree.findNodesByFieldAndValue(
394 'label',nodeToMove.label)[0];
395 selectionTree.removeNode(equivalentNode);
397 this.moveNodeElsewhereDialog.customTree = selectionTree;
398 this.moveNodeElsewhereDialog.nodeToMove = nodeToMove;
401 this.moveNodeElsewhereDialog.open({size: 'lg'}).subscribe(
403 // console.log('modal result',result);
406 // Find the equivalent node in custom_tree
407 const targetNodeInCustomTree = this.custom_tree.findNodesByFieldAndValue(
408 'label',result.label)[0];
410 // Prevent a node from becoming its own parent.
411 if (nodeToMove === targetNodeInCustomTree
412 || this.custom_tree.findParentNode(targetNodeInCustomTree) === nodeToMove) {
416 // Remove the selected node from its current parent's children.
417 // this.custom_tree.removeNode(nodeToMove);
419 // Add the selected node as the last child of the target node in custom_tree.
420 if (targetNodeInCustomTree) {
421 this.custom_tree.removeNode(nodeToMove);
422 // this._copyNodes([nodeToMove], targetNodeInCustomTree);
423 targetNodeInCustomTree.children.push( nodeToMove );
427 this.custom_tree.nodeList();
430 console.error('moveNodeHere',E);
437 async applyChanges() {
438 // console.log('applyChanges');
439 if (this.active !== (this.tree_type.active() === 't')) {
440 this.tree_type.active(this.active ? 't' : 'f');
441 this.tree_type.ischanged('t');
444 if (this.tree_type.isnew()) {
445 this.tree_type = await firstValueFrom(this.pcrud.create(this.tree_type));
446 } else if (this.tree_type.ischanged()) {
447 await firstValueFrom(this.pcrud.update(this.tree_type));
449 await this.createNewAouctns(this.custom_tree.rootNode);
450 this.successString.current().then(str => this.toast.success(str));
453 console.error('Error applying changes:', error);
454 this.updateFailedString.current().then(str => this.toast.danger(str));
458 async createNewAouctns(node: TreeNode, parent_id: number = null, order = 0) {
459 // console.log('createNewAouctns for ' + node.label + ' with parent_id = ' + parent_id + ' and order = ' + order, node);
460 // delete the existing custom nodes for the custom tree
461 // TODO: this is what the dojo interface did, but do we really need so much churn?
462 // TODO: we may want to move this to an OpenSRF method so we can wrap the entire
463 // delete and create into a single transaction
464 if (this.aouctn_root) {
465 if (this.org.get(this.aouctn_root.org_unit()).id() === node.callerData.orgId) {
466 // console.warn('removing aouctn for org ' + this.org.get(node.callerData.orgId).shortname(), this.aouctn_root);
467 const result = await lastValueFrom(this.pcrud.remove(this.aouctn_root));
468 // console.log('remove returned', result);
469 // console.log('this should have cascaded and deleted all descendants');
470 // console.log('setting aouctn_root to null');
471 this.aouctn_root = null;
474 let newNode = this.idl.create('aouctn');
476 newNode.parent_node(parent_id);
477 newNode.sibling_order(order);
478 newNode.org_unit(node.callerData.orgId);
479 newNode.tree(this.tree_type.id());
480 // console.warn('creating aouctn for org ' + this.org.get(node.callerData.orgId).shortname(), newNode);
482 // Send the new node to the server and get back the updated node
483 newNode = await firstValueFrom(this.pcrud.create(newNode));
484 // console.log('pcrud.create returned', newNode);
485 if (!this.aouctn_root) {
486 // console.log('setting it to aouctn_root; parent_node =', newNode.parent_node())
487 this.aouctn_root = newNode;
490 // If the original TreeNode has children, create new aouctn's for each child
491 if (node.children && node.children.length > 0) {
492 // console.log('looping through children for ' + this.org.get(newNode.org_unit()).shortname());
493 for (let i = 0; i < node.children.length; i++) {
494 await this.createNewAouctns(node.children[i], newNode.id(), i);
496 // console.log('finished with children for ' + this.org.get(newNode.org_unit()).shortname());
499 // console.warn('final version of node', newNode);