LP1843969 Composite Attribute Entry Defs
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / admin / server / coded-value-maps / composite-def.component.ts
1 import {Component, ViewChild, OnInit} from '@angular/core';
2 import {Router, ActivatedRoute} from '@angular/router';
3 import {Tree, TreeNode} from '@eg/share/tree/tree';
4 import {IdlService} from '@eg/core/idl.service';
5 import {ToastService} from '@eg/share/toast/toast.service';
6 import {PcrudService} from '@eg/core/pcrud.service';
7 import {CompositeNewPointComponent} from './composite-new.component';
8 import {StringComponent} from '@eg/share/string/string.component';
9
10 @Component({
11     templateUrl: './composite-def.component.html'
12 })
13
14 export class CompositeDefComponent implements OnInit {
15     currentId: number; // ccvm id
16
17     // these values displayed at top of page
18     code: string;
19     attribute: string;
20     value: string;
21
22     // data used to build tree
23     tree: Tree;
24     treeIndex = 2; // 1 is always root, so start at 2
25     idmap: any = {};
26     recordAttrDefs: any = {};
27     fetchAttrs: any[] = [];
28     codedValueMaps: any = {};
29
30     newPointType: string;
31     @ViewChild('newPoint', { static: true }) newPoint: CompositeNewPointComponent;
32
33     changesMade = false;
34     noSavedTreeData = false;
35
36     @ViewChild('saveSuccess', { static: true }) saveSuccess: StringComponent;
37     @ViewChild('saveFail', { static: true }) saveFail: StringComponent;
38
39     constructor(
40         private pcrud: PcrudService,
41         private router: Router,
42         private route: ActivatedRoute,
43         private idl: IdlService,
44         private toast: ToastService,
45     ) {
46     }
47
48     ngOnInit() {
49         this.currentId = parseInt(this.route.snapshot.paramMap.get('id'), 10);
50         this.getRecordAttrDefs();
51     }
52
53     getRecordAttrDefs = () => {
54         this.pcrud.retrieveAll('crad', {order_by: {crad: 'name'}}, {atomic: true}).subscribe(defs => {
55             defs.forEach((def) => {
56                 this.recordAttrDefs[def.name()] = def;
57             });
58             this.getCodedMapValues();
59         });
60     }
61
62     getCodedMapValues = () => {
63         this.pcrud.search('ccvm', {'id': this.currentId},
64             {flesh: 1, flesh_fields: {ccvm: ['composite_def', 'ctype']} }).toPromise().then(
65             res => {
66                 this.code = res.code();
67                 this.value = res.value();
68                 this.attribute = res.ctype().label();
69                 if (res.composite_def()) {
70                     this.buildTreeStart(res.composite_def().definition());
71                 } else {
72                     this.noSavedTreeData = true;
73                 }
74             });
75     }
76
77     createNodeLabels = () => {
78         for (const key of Object.keys(this.idmap)) {
79             const nodeCallerData = this.idmap[key].callerData.point;
80             if (nodeCallerData.typeId) {
81                 for (const id of Object.keys(this.codedValueMaps)) {
82                     const m = this.codedValueMaps[id];
83                     if ((m.code() === nodeCallerData.valueId) &&
84                         (m.ctype() === nodeCallerData.typeId)) {
85                         nodeCallerData.valueLabel = m.value();
86                     }
87                 }
88                 this.idmap[key].label = this.buildLabel(nodeCallerData.typeLabel, nodeCallerData.typeId,
89                     nodeCallerData.valueLabel, nodeCallerData.valueId);
90             }
91         }
92     }
93
94     expressionAsString = () => {
95         if (!this.tree) { return ''; }
96
97         const renderNode = (node: TreeNode): string => {
98             const lbl = node.label;
99             if (!node) { return ''; }
100             if (node.children.length) {
101                 let negative = '';
102                 let startParen = '( ';
103                 let endParen = ' )';
104                 if (lbl === 'NOT') {
105                     negative = 'NOT ';
106                     startParen = ''; // parentheses for NOT are redundant
107                     endParen = '';
108                 }
109                 if (this.tree.findParentNode(node) === null) { // no parentheses for root node
110                     startParen = '';
111                     endParen = '';
112                 }
113                 return negative + startParen + node.children.map(renderNode).join(
114                     ' ' + node.label +  ' ') + endParen;
115             } else if ((lbl !== 'NOT') && (lbl !== 'AND') && (lbl !== 'OR')) {
116                 return node.callerData.point.valueLabel;
117             } else {
118                 return '()';
119             }
120         };
121         return renderNode(this.tree.rootNode);
122     }
123
124     buildTreeStart = (def) => {
125         if (def) {
126             const nodeData = JSON.parse(def);
127             let rootNode;
128             if (Array.isArray(nodeData)) {
129                 rootNode = this.addBooleanRootNode('OR');
130                 nodeData.forEach(n => {
131                     this.buildTree(rootNode, n);
132                 });
133             } else {
134                 if (nodeData['_not']) {
135                     rootNode = this.addBooleanRootNode('NOT');
136                     this.buildTree(rootNode, nodeData['_not']);
137                 } else if (nodeData['0']) {
138                     rootNode = this.addBooleanRootNode('AND');
139                     for (const key of Object.keys(nodeData)) {
140                         this.buildTree(rootNode, nodeData[key]);
141                     }
142                 } else { // root node is record
143                     const newRootValues = {
144                         typeLabel: this.recordAttrDefs[nodeData._attr].label(),
145                         typeId: nodeData['_attr'],
146                         valueLabel: null,
147                         valueId: nodeData['_val'],
148                     };
149                     rootNode = {
150                         values: newRootValues
151                     };
152                     rootNode = this.addRecordRootNode(rootNode);
153                     this.fetchAttrs.push({'-and' : {ctype: nodeData['_attr'], code: nodeData['_val']}});
154                 }
155             }
156             if (this.fetchAttrs.length > 0) {
157                 this.pcrud.search('ccvm', {'-or' : this.fetchAttrs}).subscribe(
158                     data => {
159                         this.codedValueMaps[data.id()] = data;
160                     },
161                     err => {
162                         console.debug(err);
163                     },
164                     () => {
165                         this.createNodeLabels();
166                     }
167                 );
168             }
169         }
170     }
171
172     buildTree = (parentNode, nodeData) => {
173         let dataIsArray = false;
174         if (Array.isArray(nodeData)) { dataIsArray = true; }
175         const point = {
176             id: null,
177             expanded: true,
178             children: [],
179             parent: parentNode.id,
180             label: null,
181             typeLabel: null,
182             typeId: null,
183             valueLabel: null,
184             valueId: null,
185         };
186         if (nodeData[0] || (nodeData['_not']) || dataIsArray) {
187             this.buildTreeBoolean(nodeData, dataIsArray, point, parentNode);
188         } else { // not boolean. it's a record
189             this.buildTreeRecord(nodeData, point, parentNode);
190         }
191     }
192
193     buildTreeBoolean = (nodeData: any, dataIsArray: any, point: any, parentNode) => {
194         if (dataIsArray) {
195             point.label = 'OR';
196         } else if (nodeData['_not']) {
197             point.label = 'NOT';
198         } else if (nodeData[0]) {
199             point.label = 'AND';
200         } else {
201             console.debug('Error.  No boolean value found');
202         }
203         point.id = this.treeIndex++;
204         const newNode: TreeNode = new TreeNode({
205             id: point.id,
206             expanded: true,
207             label:  point.label,
208             callerData: {point: point}
209         });
210         parentNode.children.push(newNode);
211         this.idmap[point.id + ''] = newNode;
212         if (dataIsArray) {
213             nodeData.forEach(n => {
214                 this.buildTree(newNode, n);
215             });
216         } else if (nodeData['_not']) {
217             this.buildTree(newNode, nodeData['_not']);
218         } else if (nodeData[0]) {
219             for (const key of Object.keys(nodeData)) {
220                 this.buildTree(newNode, nodeData[key]);
221             }
222         } else {
223             console.debug('Error building tree');
224         }
225     }
226
227     buildTreeRecord = (nodeData: any, point: any, parentNode) => {
228         point.typeLabel = this.recordAttrDefs[nodeData._attr].label();
229         point.typeId = nodeData._attr;
230         point.valueId = nodeData._val;
231         this.fetchAttrs.push({'-and' : {ctype : nodeData._attr, code : nodeData._val}});
232         point.id = this.treeIndex++;
233         const newNode: TreeNode = new TreeNode({
234             id: point.id,
235             expanded: true,
236             label:  null,
237             callerData: {point: point}
238         });
239         parentNode.children.push(newNode);
240         this.idmap[point.id + ''] = newNode;
241     }
242
243     createNewTree = () => {
244         this.changesMade = true;
245         this.treeIndex = 2;
246         if (this.newPointType === 'bool') {
247             this.addBooleanRootNode(this.newPoint.values.boolOp);
248         } else {
249             this.addRecordRootNode(this.newPoint);
250         }
251     }
252
253     addBooleanRootNode = (boolOp: any) => {
254         const point = { id: 1, label: boolOp, children: []};
255         const node: TreeNode = new TreeNode({id: 1, label: boolOp, children: [],
256             callerData: {point: point}});
257         this.idmap['1'] = node;
258         this.tree = new Tree(node);
259         return node;
260     }
261
262     addRecordRootNode = (record: any) => {
263         const point = { id: 1, expanded: true, children: [], label: null, typeLabel: null,
264             typeId: null, valueLabel: null, valueId: null};
265         point.typeLabel = record.values.typeLabel;
266         point.typeId = record.values.typeId;
267         point.valueLabel = record.values.valueLabel;
268         point.valueId = record.values.valueId;
269         const fullLabel = this.buildLabel(point.typeLabel, point.typeId, point.valueLabel, point.valueId);
270         const node: TreeNode = new TreeNode({ id: 1, label: fullLabel, children: [],
271             callerData: {point: point}});
272         this.idmap['1'] = node;
273         this.tree = new Tree(node);
274         return node;
275     }
276
277     buildLabel = (tlbl, tid, vlbl, vid) => {
278         return tlbl + ' (' + tid + ') => ' + vlbl + ' (' + vid + ')';
279     }
280
281     nodeClicked(node: TreeNode) {
282         console.debug('Node clicked on: ' + node.label);
283     }
284
285     deleteTree = () => {
286         this.tree = null;
287         this.idmap = {};
288         this.treeIndex = 2;
289         this.changesMade = true;
290     }
291
292     deleteNode = () => {
293         this.changesMade = true;
294         if (this.isRootNode()) {
295             this.deleteTree();
296         } else {
297             this.tree.removeNode(this.tree.selectedNode());
298         }
299     }
300
301     hasSelectedNode(): boolean {
302         if (this.tree) {
303             return Boolean(this.tree.selectedNode());
304         }
305     }
306
307     isRootNode(): boolean {
308         const node = this.tree.selectedNode();
309         if (node && this.tree.findParentNode(node) === null) {
310             return true;
311         }
312         return false;
313     }
314
315     selectedIsBool(): boolean {
316         if (!this.tree) { return false; }
317         if (this.tree.selectedNode()) {
318             const label = this.tree.selectedNode().label;
319             if (label === 'AND' || label === 'NOT' || label === 'OR') { return true; }
320         }
321         return false;
322     }
323
324     // Disable this:
325     // 1. if no node selected
326     // 2. if trying to add to a non-boolean record
327     // 3. if trying to add more than 1 child to a NOT
328     // 4. if trying to add NOT to an existing NOT
329     // 5. if trying to add before user has made selection of new value or operator
330     addButtonDisabled(): boolean {
331         if (!this.hasSelectedNode()) { return true; }
332         if (!this.selectedIsBool()) { return true; }
333         if ((this.tree.selectedNode().label === 'NOT') &&
334             (this.tree.selectedNode().children.length > 0)) { return true; }
335         if ((this.tree.selectedNode().label === 'NOT') &&
336             (this.newPoint.values.boolOp === 'NOT')) { return true; }
337         if (this.newPointType === 'attr' &&
338             (this.newPoint.values.typeId.length > 0) &&
339             (this.newPoint.values.valueId.length > 0)) { return false; }
340         if (this.newPointType === 'bool' &&
341             (this.newPoint.values.boolOp.length > 0)) { return false; }
342         return true;
343     }
344
345     // Disable this:
346     // 1. if no node selected
347     // 2. if trying to replace a boolean with a non-boolean or vice versa
348     // 3. if trying to replace before user has made selection of new value or operator
349     replaceButtonDisabled(): boolean {
350         if (!this.hasSelectedNode()) { return true; }
351         if (this.newPointType === 'attr' && !this.selectedIsBool() &&
352             (this.newPoint.values.typeId.length > 0) &&
353             (this.newPoint.values.valueId.length > 0)) { return false; }
354         if (this.newPointType === 'bool' && this.selectedIsBool() &&
355             (this.newPoint.values.boolOp.length > 0)) { return false; }
356         return true;
357     }
358
359     // disabled until you select a type and select values for that type
360     newTreeButtonDisabled(): boolean {
361         if ((this.newPointType === 'bool') && (this.newPoint.values.boolOp.length > 0)) {
362             return false;
363         }
364         if ((this.newPointType === 'attr') && (this.newPoint.values.typeId.length > 0) &&
365             (this.newPoint.values.valueId.length > 0)) { return false; }
366         return true;
367     }
368
369     back() {
370         this.router.navigate(['/staff/admin/server/config/coded_value_map']);
371     }
372
373     saveTree = () => {
374         const recordToSave = this.idl.create('ccraed');
375         recordToSave.coded_value(this.currentId);
376         const expression = this.exportTree(this.idmap['1']);
377         const jsonStr = JSON.stringify(expression);
378         recordToSave.definition(jsonStr);
379         if (this.noSavedTreeData) {
380             this.pcrud.create(recordToSave).subscribe(
381                 ok => {
382                     this.saveSuccess.current().then(str => this.toast.success(str));
383                     this.noSavedTreeData = false;
384                 },
385                 err => {
386                     this.saveFail.current().then(str => this.toast.danger(str));
387                 }
388             );
389         } else {
390             this.pcrud.update(recordToSave).subscribe(
391                 async (ok) => {
392                     this.saveSuccess.current().then(str => this.toast.success(str));
393                 },
394                 async (err) => {
395                     this.saveFail.current().then(str => this.toast.danger(str));
396                 }
397             );
398         }
399     }
400
401     exportTree(node: TreeNode): any {
402         const lbl = node.label;
403         if ((lbl !== 'NOT') && (lbl !== 'AND') && (lbl !== 'OR')) {
404             const retval = {_attr: node.callerData.point.typeId, _val: node.callerData.point.valueId};
405             return retval;
406         }
407         if (lbl === 'NOT') {
408             return {_not : this.exportTree(node.children[0])}; // _not nodes may only have one child
409         }
410         let compiled;
411         for (let i = 0; i < node.children.length; i++) {
412             const child = node.children[i];
413             if (!compiled) {
414                 if (node.label === 'OR') {
415                     compiled = [];
416                 } else {
417                     compiled = {};
418                 }
419             }
420             compiled[i] = this.exportTree(child);
421         }
422         return compiled;
423     }
424
425     addChildNode(replace?: boolean) {
426         const targetNode: TreeNode = this.tree.selectedNode();
427         this.changesMade = true;
428         const point = {
429             id: null,
430             expanded: true,
431             children: [],
432             parent: targetNode.id,
433             label: null,
434             typeLabel: null,
435             typeId: null,
436             valueLabel: null,
437             valueId: null,
438         };
439
440         const node: TreeNode = new TreeNode({
441             callerData: {point: point},
442             id: point.id,
443             label: null
444         });
445
446         if (this.newPoint.values.pointType === 'bool') {
447             point.label = this.newPoint.values.boolOp;
448             node.label = point.label;
449         } else {
450             point.typeLabel = this.newPoint.values.typeLabel;
451             point.valueLabel = this.newPoint.values.valueLabel;
452             point.typeId = this.newPoint.values.typeId;
453             point.valueId = this.newPoint.values.valueId;
454         }
455         if (replace) {
456             if (this.newPoint.values.pointType === 'bool') {
457                 targetNode.label = point.label;
458             } else {
459                 targetNode.label = this.buildLabel(point.typeLabel, point.typeId, point.valueLabel,
460                     point.valueId);
461             }
462             targetNode.callerData.point = point;
463         } else {
464             point.id = this.treeIndex;
465             node.id = this.treeIndex++;
466             if (this.newPoint.values.pointType === 'bool') {
467                 node.label = point.label;
468             } else {
469                 node.label = this.buildLabel(point.typeLabel, point.typeId, point.valueLabel,
470                     point.valueId);
471             }
472             point.parent = targetNode.id;
473             targetNode.children.push(node);
474             this.idmap[point.id + ''] = node;
475         }
476     }
477
478  }