]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/conify/global/vandelay/match_set.js
LP#1171984 Vandelay authority record matching
[Evergreen.git] / Open-ILS / web / js / ui / default / conify / global / vandelay / match_set.js
1 dojo.require("dijit.Tree");
2 dojo.require("dijit.form.Button");
3 dojo.require("dojo.data.ItemFileWriteStore");
4 dojo.require("dojo.dnd.Source");
5 dojo.require("openils.vandelay.TreeDndSource");
6 dojo.require("openils.vandelay.TreeStoreModel");
7 dojo.require("openils.CGI");
8 dojo.require("openils.User");
9 dojo.require("openils.Util");
10 dojo.require("openils.PermaCrud");
11 dojo.require("openils.widget.ProgressDialog");
12 dojo.require("openils.widget.AutoGrid");
13
14 var localeStrings, node_editor, qnode_editor, _crads, CGI, tree, match_set;
15
16 var NodeEditorAbstract = {
17     "_svf_select_template": null,
18     "_simple_value_getter": function(control) {
19         if (typeof control.selectedIndex != "undefined")
20             return control.options[control.selectedIndex].value;
21         else if (dojo.attr(control, "type") == "checkbox")
22             return control.checked;
23         else
24             return control.value;
25     },
26     "is_sensible": function(thing) {
27         var need_one = 0;
28         this.foi.forEach(function(field) { if (thing[field]()) need_one++; });
29
30         if (need_one != 1) {
31             alert(localeStrings.POINT_NEEDS_ONE);
32             return false;
33         }
34
35         if (thing.tag()) {
36             if (
37                 !thing.tag().match(/^\d{3}$/) ||
38                 thing.subfield().length != 1 ||
39                 !thing.subfield().match(/\S/) ||
40                 thing.subfield().charCodeAt(0) < 32
41             ) {
42                 alert(localeStrings.FAULTY_MARC);
43                 return false;
44             }
45         }
46
47         return true;
48     },
49     "_add_consistent_controls": function(tgt) {
50         if (!this._consistent_controls) {
51             var trs = dojo.query(this._consistent_controls_query);
52             this._consistent_controls = [];
53             for (var i = 0; i < trs.length; i++)
54                 this._consistent_controls[i] = dojo.clone(trs[i]);
55         }
56
57         this._consistent_controls.forEach(
58             function(node) { dojo.place(dojo.clone(node), tgt); }
59         );
60     },
61     "_factories_by_type": {
62         "svf": function() {
63             if (!self._svf_select_template) {
64                 self._svf_select_template = dojo.create(
65                     "select", {"fmfield": "svf"}
66                 );
67                 for (var i=0; i<_crads.length; i++) {
68                     dojo.create(
69                         "option", {
70                             "value": _crads[i].name(),
71                             "innerHTML": _crads[i].label()
72                         }, self._svf_select_template
73                     );
74                 }
75             }
76
77             var select = dojo.clone(self._svf_select_template);
78             dojo.attr(select, "id", "svf-select");
79             var label = dojo.create(
80                 "label", {
81                     "for": "svf-select", "innerHTML": localeStrings.SVF + ":"
82                 }
83             );
84
85             var tr = dojo.create("tr");
86             dojo.place(label, dojo.create("td", null, tr));
87             dojo.place(select, dojo.create("td", null, tr));
88
89             return [tr];
90         },
91         "tag": function() {
92             var rows = [dojo.create("tr"), dojo.create("tr")];
93             dojo.create(
94                 "label", {
95                     "for": "tag-input", "innerHTML": "Tag:"
96                 }, dojo.create("td", null, rows[0])
97             );
98             dojo.create(
99                 "input", {
100                     "id": "tag-input",
101                     "type": "text",
102                     "size": 4,
103                     "maxlength": 3,
104                     "fmfield": "tag"
105                 }, dojo.create("td", null, rows[0])
106             );
107             dojo.create(
108                 "label", {
109                     "for": "subfield-input", "innerHTML": "Subfield: \u2021"
110                 }, dojo.create("td", null, rows[1])
111             );
112             dojo.create(
113                 "input", {
114                     "id": "subfield-input",
115                     "type": "text",
116                     "size": 2,
117                     "maxlength": 1,
118                     "fmfield": "subfield"
119                 }, dojo.create("td", null, rows[1])
120             );
121             return rows;
122         },
123         "heading" : function() {
124             var tr = dojo.create("tr");
125             dojo.create(
126                 "label", {
127                     "for": "heading-input", 
128                     "innerHTML": localeStrings.HEADING_MATCH
129                 }, dojo.create("td", null, tr)
130             );
131
132             dojo.create(
133                 "input", {
134                     "id": "heading-input",
135                     "type": "checkbox",
136                     "checked": true,
137                     "disabled": true, // if you don't want it, don't use it.
138                     "fmfield": "heading"
139                 }, dojo.create("td", null, tr)
140             );
141
142             return [tr];
143         },
144         "bool_op": function() {
145             var tr = dojo.create("tr");
146             dojo.create(
147                 "label",
148                 {"for": "operator-select", "innerHTML": "Operator:"},
149                 dojo.create("td", null, tr)
150             );
151             var select = dojo.create(
152                 "select", {"fmfield": "bool_op", "id": "operator-select"},
153                 dojo.create("td", null, tr)
154             );
155             dojo.create("option", {"value": "AND", "innerHTML": "AND"}, select);
156             dojo.create("option", {"value": "OR", "innerHTML": "OR"}, select);
157
158             return [tr];
159         }
160     }
161 };
162
163 function apply_base_class(cls, basecls) {
164     openils.Util.objectProperties(basecls).forEach(
165         function(m) { cls[m] = basecls[m]; }
166     );
167 }
168
169 function QualityNodeEditor() {
170     var self = this;
171     this.foi = ["tag", "svf"]; /* Fields of Interest - starting points for UI */
172
173     this._init = function(qnode_editor_container) {
174         this._consistent_controls_query =
175             "[consistent-controls], [quality-controls]";
176         this.qnode_editor_container = dojo.byId(qnode_editor_container);
177         this.clear();
178     };
179
180     this.clear = function() {
181         dojo.create(
182             "em", {"innerHTML": localeStrings.WORKING_QM_HERE},
183             this.qnode_editor_container, "only"
184         );
185     };
186
187     this.build_vmsq = function() {
188         var metric = new vmsq();
189         metric.match_set(match_set.id());   /* using global */
190         var controls = dojo.query("[fmfield]", this.qnode_editor_container);
191         for (var i = 0; i < controls.length; i++) {
192             var field = dojo.attr(controls[i], "fmfield");
193             var value = this._simple_value_getter(controls[i]);
194             metric[field](value);
195         }
196
197         if (!this.is_sensible(metric)) return null;    /* will alert() */
198         else return metric;
199     };
200
201     this.add = function(type) {
202         this.clear();
203
204         /* these are the editing widgets */
205         var table = dojo.create("table", {"className": "node-editor"});
206
207         var nodes = this._factories_by_type[type]();
208         for (var i = 0; i < nodes.length; i++) dojo.place(nodes[i], table);
209
210         this._add_consistent_controls(table);
211
212         var ok_cxl_td = dojo.create(
213             "td", {"colspan": 2, "align": "center", "className": "space-me"},
214             dojo.create("tr", null, table)
215         );
216
217         dojo.create(
218             "input", {
219                 "type": "submit", "value": localeStrings.OK,
220                 "onclick": function() {
221                     var metric = self.build_vmsq();
222                     if (metric) {
223                         self.clear();
224                         pcrud.create(
225                             metric, {
226                                 /* borrowed from openils.widget.AutoGrid */
227                                 "oncomplete": function(req, cudResults) {
228                                     var fmObject = cudResults[0];
229                                     if (vmsq_grid.onPostCreate)
230                                         vmsq_grid.onPostCreate(fmObject);
231                                     if (fmObject) {
232                                         vmsq_grid.store.newItem(
233                                             fmObject.toStoreItem()
234                                         );
235                                     }
236                                     setTimeout(function() {
237                                         try {
238                                             vmsq_grid.selection.select(vmsq_grid.rowCount-1);
239                                             vmsq_grid.views.views[0].getCellNode(vmsq_grid.rowCount-1, 1).focus();
240                                         } catch (E) {}
241                                     },200);
242                                 }
243                             }
244                         );
245                     }
246                 }
247             }, ok_cxl_td
248         );
249         dojo.create(
250             "input", {
251                 "type": "reset", "value": localeStrings.CANCEL,
252                 "onclick": function() { self.clear(); }
253             }, ok_cxl_td
254         );
255
256         dojo.place(table, this.qnode_editor_container, "only");
257
258         /* nice */
259         try { dojo.query("select, input", table)[0].focus(); }
260         catch(E) { console.log(String(E)); }
261
262     };
263
264     apply_base_class(self, NodeEditorAbstract);
265     this._init.apply(this, arguments);
266 }
267
268 function NodeEditor() {
269     var self = this;
270     this.foi = ["tag", "svf", "heading", "bool_op"]; /* Fields of Interest - starting points for UI */
271
272     this._init = function(dnd_source, node_editor_container) {
273         this._consistent_controls_query =
274             "[consistent-controls], [point-controls]";
275         this.dnd_source = dnd_source;
276         this.node_editor_container = dojo.byId(node_editor_container);
277
278         // hide match point types which are not relevent to
279         // the current record type
280         if (match_set.mtype() == 'authority') {
281             openils.Util.hide('record-attr-btn');
282         } else {
283             openils.Util.hide('heading-match-btn');
284         }
285     };
286
287     this.clear = function() {
288         this.dnd_source.selectAll().deleteSelectedNodes();
289         dojo.create(
290             "em", {"innerHTML": localeStrings.WORKING_MP_HERE},
291             this.node_editor_container, "only"
292         );
293         this.dnd_source._ready = false;
294     };
295
296     this.build_vmsp = function() {
297         var match_point = new vmsp();
298         var controls = dojo.query("[fmfield]", this.node_editor_container);
299         for (var i = 0; i < controls.length; i++) {
300             var field = dojo.attr(controls[i], "fmfield");
301             var value = this._simple_value_getter(controls[i]);
302             match_point[field](value);
303         }
304
305         if (!this.is_sensible(match_point)) return null;    /* will alert() */
306         else return match_point;
307     };
308
309     this.update_draggable = function(draggable) {
310         var mp;
311
312         if (!(mp = this.build_vmsp())) return;  /* will alert() */
313
314         draggable.match_point = mp;
315         dojo.attr(draggable, "innerHTML", render_vmsp_label(mp));
316         this.dnd_source._ready = true;
317     };
318
319     this.add = function(type) {
320         this.clear();
321
322         /* a representation, not the editing widgets, but will also carry
323          * the fieldmapper object when dragged to the tree */
324         var draggable = dojo.create(
325             "li", {"innerHTML": localeStrings.DEFINE_MP}
326         );
327
328         /* these are the editing widgets */
329         var table = dojo.create("table", {"className": "node-editor"});
330
331         var nodes = this._factories_by_type[type]();
332         for (var i = 0; i < nodes.length; i++) dojo.place(nodes[i], table);
333
334         if (type != "bool_op")
335             this._add_consistent_controls(table);
336
337         dojo.create(
338             "input", {
339                 "type": "submit", "value": localeStrings.OK,
340                 "onclick": function() { self.update_draggable(draggable); }
341             }, dojo.create(
342                 "td", {"colspan": 2, "align": "center"},
343                 dojo.create("tr", null, table)
344             )
345         );
346
347         dojo.place(table, this.node_editor_container, "only");
348
349         this.dnd_source.insertNodes(false, [draggable]);
350
351         /* nice */
352         try { dojo.query("select, input", table)[0].focus(); }
353         catch(E) { console.log(String(E)); }
354
355     };
356
357     apply_base_class(self, NodeEditorAbstract);
358
359     this._init.apply(this, arguments);
360 }
361
362 function find_crad_by_name(name) {
363     for (var i = 0; i < _crads.length; i++) {
364         if (_crads[i].name() == name)
365             return _crads[i];
366     }
367     return null;
368 }
369
370 function render_vmsp_label(point, minimal) {
371     /* "minimal" has these implications:
372      * for svf, only show the code, not the longer label.
373      * no quality display
374      */
375     if (point.bool_op()) {
376         return point.bool_op();
377     } else if (point.svf()) {
378         return (openils.Util.isTrue(point.negate()) ? "NOT " : "") + (
379             minimal ?  point.svf() :
380                 (point.svf() + " / " + find_crad_by_name(point.svf()).label()) +
381                 " | " + dojo.string.substitute(
382                     localeStrings.MATCH_SCORE, [point.quality()]
383                 )
384         );
385     } else if (point.heading() === true || point.heading() == 't') {
386         return localeStrings.HEADING_MATCH +
387             " | " + dojo.string.substitute(
388                 localeStrings.MATCH_SCORE, [point.quality()]);
389     } else {
390         return (openils.Util.isTrue(point.negate()) ? "NOT " : "") +
391             point.tag() + " \u2021" + point.subfield() + (minimal ? "" : " | " +
392                 dojo.string.substitute(
393                     localeStrings.MATCH_SCORE, [point.quality()]
394                 )
395             );
396     }
397 }
398
399 function replace_mode(explicit) {
400     if (typeof explicit == "undefined")
401         tree.model.replace_mode ^= 1;
402     else
403         tree.model.replace_mode = explicit;
404
405     dojo.attr(
406         "replacer", "innerHTML",
407         localeStrings[
408             (tree.model.replace_mode ? "EXIT" : "ENTER") + "_REPLACE_MODE"
409         ]
410     );
411     dojo[tree.model.replace_mode ? "addClass" : "removeClass"](
412         "replacer", "replace-mode"
413     );
414 }
415
416 function delete_selected_in_tree() {
417     /* relies on the fact that we only have one tree that would have
418      * registered a dnd controller. */
419     _tree_dnd_controllers[0].getSelectedItems().forEach(
420         function(item) {
421             if (item === tree.model.root)
422                 alert(localeStrings.LEAVE_ROOT_ALONE);
423             else
424                 tree.model.store.deleteItem(item);
425         }
426     );
427 }
428
429 function new_match_set_tree() {
430     var point = new vmsp();
431     point.bool_op("AND");
432     return [
433         {
434             "id": "root",
435             "children": [],
436             "name": render_vmsp_label(point),
437             "match_point": point
438         }
439     ];
440 }
441
442 /* dojoize_match_set_tree() takes an argument, "point", that is actually a
443  * vmsp fieldmapper object with descendants fleshed hierarchically. It turns
444  * that into a syntactically flat array but preserving the hierarchy
445  * semantically in the language used by dojo data stores, i.e.,
446  *
447  * [
448  *  {'id': 'root', children:[{'_reference': '0'}, {'_reference': '1'}]},
449  *  {'id': '0', children:[]},
450  *  {'id': '1', children:[]}
451  * ],
452  *
453  */
454 function dojoize_match_set_tree(point, depth) {
455     var root = false;
456     if (!depth) {
457         if (!point) {
458             return new_match_set_tree();
459         }
460         depth = 0;
461         root = true;
462     }
463
464     var bathwater = point.children();
465     point.children([]);
466     var item = {
467         "id": (root ? "root" : point.id()),
468         "name": render_vmsp_label(point),
469         "match_point": point.clone(),
470         "children": []
471     };
472     point.children(bathwater);
473
474     var results = [item];
475
476     if (point.children()) {
477         for (var i = 0; i < point.children().length; i++) {
478             var child = point.children()[i];
479             item.children.push({"_reference": child.id()});
480             results = results.concat(
481                 dojoize_match_set_tree(child, ++depth)
482             );
483         }
484     }
485
486     return results;
487 }
488
489 function render_vms_metadata(match_set) {
490     dojo.byId("vms-name").innerHTML = match_set.name();
491     dojo.byId("vms-owner").innerHTML =
492         aou.findOrgUnit(match_set.owner()).name();
493     dojo.byId("vms-mtype").innerHTML = match_set.mtype();
494 }
495
496 function redraw_expression_preview() {
497     tree.model.getRoot(
498         function(root) {
499             tree.model.get_simple_tree(
500                 root, function(r) {
501                     dojo.attr(
502                         "expr-preview",
503                         "innerHTML",
504                         render_expression_preview(r)
505                     );
506                 }
507             );
508         }
509     );
510 }
511
512 function render_expression_preview(r) {
513     if (r.children().length) {
514         return "(" + r.children().map(render_expression_preview).join(
515             " " + render_vmsp_label(r) + " "
516         ) + ")";
517     } else if (!r.bool_op()) {
518         return render_vmsp_label(r, true /* minimal */);
519     } else {
520         return "()";
521     }
522 }
523
524 function save_tree() {
525     progress_dialog.show(true);
526
527     tree.model.getRoot(
528         function(root) {
529             tree.model.get_simple_tree(
530                 root, function(r) {
531                     fieldmapper.standardRequest(
532                         ["open-ils.vandelay",
533                             "open-ils.vandelay.match_set.update"], {
534                             "params": [
535                                 openils.User.authtoken, match_set.id(), r
536                             ],
537                             "async": true,
538                             "oncomplete": function(r) {
539                                 progress_dialog.hide();
540                                 /* catch exceptions */
541                                 r = openils.Util.readResponse(r);
542
543                                 location.href = location.href;
544                             }
545                         }
546                     );
547                 }
548             );
549         }
550     );
551 }
552
553 function init_vmsq_grid() {
554     vmsq_grid.loadAll(
555         {"order_by": {"vmsq": "quality"}},
556         {"match_set": match_set.id()}
557     );
558 }
559
560 function my_init() {
561     progress_dialog.show(true);
562
563     dojo.requireLocalization("openils.vandelay", "match_set");
564     localeStrings = dojo.i18n.getLocalization("openils.vandelay", "match_set");
565
566     pcrud = new openils.PermaCrud();
567     CGI = new openils.CGI();
568
569     if (!CGI.param("match_set")) {
570         alert(localeStrings.NO_CAN_DO);
571         progress_dialog.hide();
572         return;
573     }
574
575     render_vms_metadata(
576         match_set = pcrud.retrieve("vms", CGI.param("match_set"))
577     );
578
579     /* No-one should have hundreds of these or anything, but theoretically
580      * this could be problematic with a big enough list of crad objects. */
581     _crads = match_set.mtype() == 'authority' ? [] :
582         pcrud.retrieveAll("crad", {"order_by": {"crad": "label"}});
583
584     var match_set_tree = fieldmapper.standardRequest(
585         ["open-ils.vandelay", "open-ils.vandelay.match_set.get_tree"],
586         [openils.User.authtoken, CGI.param("match_set")]
587     );
588
589     var store = new dojo.data.ItemFileWriteStore({
590         "data": {
591             "identifier": "id",
592             "label": "name",
593             "items": dojoize_match_set_tree(match_set_tree)
594         }
595     });
596
597     var tree_model = new openils.vandelay.TreeStoreModel({
598         "store": store, "query": {"id": "root"}
599     });
600
601     var src = new dojo.dnd.Source("src-here");
602     tree = new dijit.Tree(
603         {
604             "model": tree_model,
605             "dndController": openils.vandelay.TreeDndSource,
606             "dragThreshold": 8,
607             "betweenThreshold": 5,
608             "persist": false
609         }, "tree-here"
610     );
611
612     node_editor = new NodeEditor(src, "node-editor-container");
613     qnode_editor = new QualityNodeEditor("qnode-editor-container");
614
615     replace_mode(0);
616
617     dojo.connect(
618         src, "onDndDrop", null,
619         function(source, nodes, copy, target) {
620             /* Because of the... interesting... characteristics of DnD
621              * design in dojo/dijit (at least as of 1.3), this callback will
622              * fire both for our working node dndSource and for the tree!
623              */
624             if (source == this)
625                 node_editor.clear();  /* ... because otherwise this acts like a
626                                          copy operation no matter what the user
627                                          does, even though we really want a
628                                          "move." */
629         }
630     );
631
632     redraw_expression_preview();
633     node_editor.clear();
634
635     init_vmsq_grid();
636
637     progress_dialog.hide();
638 }
639
640 openils.Util.addOnLoad(my_init);