FF needs universalxpconnect to launch the marc editor
[Evergreen.git] / Open-ILS / web / vandelay / vandelay.js
1 /* ---------------------------------------------------------------------------
2 # Copyright (C) 2008  Georgia Public Library Service
3 # Bill Erickson <erickson@esilibrary.com>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # --------------------------------------------------------------------------- */
15 dojo.require("dojo.parser");
16 dojo.require("dojo.io.iframe"); 
17 dojo.require("dijit.ProgressBar"); 
18 dojo.require("dijit.form.Button"); 
19 dojo.require("dijit.form.FilteringSelect"); 
20 dojo.require("dijit.layout.ContentPane");
21 dojo.require("dijit.layout.TabContainer");
22 dojo.require("dijit.layout.LayoutContainer");
23 dojo.require('dijit.form.Button');
24 dojo.require('dijit.Toolbar');
25 dojo.require('dijit.Tooltip');
26 dojo.require('dijit.Menu');
27 dojo.require("dijit.Dialog");
28 dojo.require("dojo.cookie");
29 dojo.require("dojox.grid.Grid");
30 dojo.require("dojo.data.ItemFileReadStore");
31 dojo.require('dojo.date.locale');
32 dojo.require('dojo.date.stamp');
33 dojo.require("fieldmapper.Fieldmapper");
34 dojo.require("fieldmapper.dojoData");
35 dojo.require('openils.CGI');
36 dojo.require('openils.User');
37 dojo.require('openils.Event');
38 dojo.require('openils.MarcXPathParser');
39 dojo.require('openils.GridColumnPicker');
40
41
42 var globalDivs = [
43     'vl-generic-progress',
44     'vl-generic-progress-with-total',
45     'vl-marc-upload-div',
46     'vl-queue-div',
47     'vl-match-div',
48     'vl-marc-html-div',
49     'vl-queue-select-div',
50     'vl-marc-upload-status-div',
51     'vl-attr-editor-div',
52 ];
53
54 var authtoken;
55 var VANDELAY_URL = '/vandelay-upload';
56 var bibAttrDefs = [];
57 var authAttrDefs = [];
58 var queuedRecords = [];
59 var queuedRecordsMap = {};
60 var bibAttrsFetched = false;
61 var authAttrsFetched = false;
62 var attrDefMap = {}; // maps attr def code names to attr def ids
63 var currentType;
64 var currentQueueId = null;
65 var userCache = {};
66 var currentMatchedRecords; // set of loaded matched bib records
67 var currentOverlayRecordsMap; // map of import record to overlay record
68 var currentImportRecId; // when analyzing matches, this is the current import record
69 var userBibQueues = []; // only non-complete queues
70 var userAuthQueues = []; // only non-complete queues
71 var allUserBibQueues;
72 var allUserAuthQueues;
73 var selectableGridRecords;
74 var cgi = new openils.CGI();
75 var vlQueueGridColumePicker;
76
77 /**
78   * Grab initial data
79   */
80 function vlInit() {
81     authtoken = dojo.cookie('ses') || cgi.param('ses');
82     var initNeeded = 4; // how many async responses do we need before we're init'd 
83     var initCount = 0; // how many async reponses we've received
84
85     function checkInitDone() {
86         initCount++;
87         if(initCount == initNeeded)
88             runStartupCommands();
89     }
90
91     // Fetch the bib and authority attribute definitions 
92     vlFetchBibAttrDefs(function () { checkInitDone(); });
93     vlFetchAuthAttrDefs(function () { checkInitDone(); });
94
95     vlRetrieveQueueList('bib', null, 
96         function(list) {
97             allUserBibQueues = list;
98             for(var i = 0; i < allUserBibQueues.length; i++) {
99                 if(allUserBibQueues[i].complete() == 'f')
100                     userBibQueues.push(allUserBibQueues[i]);
101             }
102             checkInitDone();
103         }
104     );
105
106     vlRetrieveQueueList('auth', null, 
107         function(list) {
108             allUserAuthQueues = list;
109             for(var i = 0; i < allUserAuthQueues.length; i++) {
110                 if(allUserAuthQueues[i].complete() == 'f')
111                     userAuthQueues.push(allUserAuthQueues[i]);
112             }
113             checkInitDone();
114         }
115     );
116
117     vlAttrEditorInit();
118 }
119
120
121 dojo.addOnLoad(vlInit);
122
123
124 // fetch the bib and authority attribute definitions
125
126 function vlFetchBibAttrDefs(postcomplete) {
127     bibAttrDefs = [];
128     fieldmapper.standardRequest(
129         ['open-ils.permacrud', 'open-ils.permacrud.search.vqbrad'],
130         {   async: true,
131             params: [authtoken, {id:{'!=':null}}],
132             onresponse: function(r) {
133                 var def = r.recv().content(); 
134                 if(e = openils.Event.parse(def[0])) 
135                     return alert(e);
136                 bibAttrDefs.push(def);
137             },
138             oncomplete: function() {
139                 bibAttrDefs = bibAttrDefs.sort(
140                     function(a, b) {
141                         if(a.id() > b.id()) return 1;
142                         if(a.id() < b.id()) return -1;
143                         return 0;
144                     }
145                 );
146                 postcomplete();
147             }
148         }
149     );
150 }
151
152 function vlFetchAuthAttrDefs(postcomplete) {
153     authAttrDefs = [];
154     fieldmapper.standardRequest(
155         ['open-ils.permacrud', 'open-ils.permacrud.search.vqarad'],
156         {   async: true,
157             params: [authtoken, {id:{'!=':null}}],
158             onresponse: function(r) {
159                 var def = r.recv().content(); 
160                 if(e = openils.Event.parse(def[0])) 
161                     return alert(e);
162                 authAttrDefs.push(def);
163             },
164             oncomplete: function() {
165                 authAttrDefs = authAttrDefs.sort(
166                     function(a, b) {
167                         if(a.id() > b.id()) return 1;
168                         if(a.id() < b.id()) return -1;
169                         return 0;
170                     }
171                 );
172                 postcomplete();
173             }
174         }
175     );
176 }
177
178 function vlRetrieveQueueList(type, filter, onload) {
179     type = (type == 'bib') ? type : 'authority';
180     fieldmapper.standardRequest(
181         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.owner.retrieve.atomic'],
182         {   async: true,
183             params: [authtoken, null, filter],
184             oncomplete: function(r) {
185                 var list = r.recv().content();
186                 if(e = openils.Event.parse(list[0]))
187                     return alert(e);
188                 onload(list);
189             }
190         }
191     );
192
193 }
194
195 function displayGlobalDiv(id) {
196     for(var i = 0; i < globalDivs.length; i++) {
197         try {
198             dojo.style(dojo.byId(globalDivs[i]), 'display', 'none');
199         } catch(e) {
200             alert('please define div ' + globalDivs[i]);
201         }
202     }
203     dojo.style(dojo.byId(id),'display','block');
204 }
205
206 function runStartupCommands() {
207     currentQueueId = cgi.param('qid');
208     currentType = cgi.param('qtype');
209     dojo.style('vl-nav-bar', 'visibility', 'visible');
210     if(currentQueueId)
211         return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
212     vlShowUploadForm();
213 }
214
215 /**
216   * asynchronously upload a file of MARC records
217   */
218 function uploadMARC(onload){
219     dojo.byId('vl-upload-status-count').innerHTML = '0';
220     dojo.byId('vl-ses-input').value = authtoken;
221     displayGlobalDiv('vl-marc-upload-status-div');
222     dojo.io.iframe.send({
223         url: VANDELAY_URL,
224         method: "post",
225         handleAs: "html",
226         form: dojo.byId('vl-marc-upload-form'),
227         handle: function(data,ioArgs){
228             var content = data.documentElement.textContent;
229             onload(content);
230         }
231     });
232 }       
233
234 /**
235   * Creates a new vandelay queue
236   */
237 function createQueue(queueName, type, onload) {
238     fieldmapper.standardRequest(
239         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.create'],
240         {   async: true,
241             params: [authtoken, queueName, null, type],
242             oncomplete : function(r) {
243                 var queue = r.recv().content();
244                 if(e = openils.Event.parse(queue)) 
245                     return alert(e);
246                 onload(queue);
247             }
248         }
249     );
250 }
251
252 /**
253   * Tells vandelay to pull a batch of records from the cache and explode them
254   * out into the vandelay tables
255   */
256 function processSpool(key, queueId, type, onload) {
257     fieldmapper.standardRequest(
258         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'.process_spool'],
259         {   async: true,
260             params: [authtoken, key, queueId],
261             onresponse : function(r) {
262                 var resp = r.recv().content();
263                 if(e = openils.Event.parse(resp)) 
264                     return alert(e);
265                 dojo.byId('vl-upload-status-count').innerHTML = resp;
266             },
267             oncomplete : function(r) {onload();}
268         }
269     );
270 }
271
272 function retrieveQueuedRecords(type, queueId, onload) {
273     displayGlobalDiv('vl-generic-progress');
274     queuedRecords = [];
275     queuedRecordsMap = {};
276     currentOverlayRecordsMap = {};
277     selectableGridRecords = {};
278     resetVlQueueGridLayout();
279
280     var method = 'open-ils.vandelay.'+type+'_queue.records.retrieve.atomic';
281     if(vlQueueGridShowMatches.checked)
282         method = method.replace('records', 'records.matches');
283
284     var limit = parseInt(vlQueueDisplayLimit.getValue());
285     var offset = limit * parseInt(vlQueueDisplayPage.getValue()-1);
286
287     fieldmapper.standardRequest(
288         ['open-ils.vandelay', method],
289         {   async: true,
290             params: [authtoken, queueId, 
291                 {   clear_marc: 1, 
292                     offset: offset,
293                     limit: limit
294                 }
295             ],
296             /* intermittent bug in streaming, multipart requests prevents use of onreponse for now...
297             onresponse: function(r) {
298                 var rec = r.recv().content();
299                 if(e = openils.Event.parse(rec))
300                     return alert(e);
301                 queuedRecords.push(rec);
302                 queuedRecordsMap[rec.id()] = rec;
303             },
304             */
305             oncomplete: function(r){
306                 var recs = r.recv().content();
307                 if(e = openils.Event.parse(recs[0]))
308                     return alert(e);
309                 for(var i = 0; i < recs.length; i++) {
310                     var rec = recs[i];
311                     queuedRecords.push(rec);
312                     queuedRecordsMap[rec.id()] = rec;
313                 }
314                 onload();
315             }
316         }
317     );
318 }
319
320 //function vlLoadMatchUI(recId, attrCode) {
321 function vlLoadMatchUI(recId) {
322     displayGlobalDiv('vl-generic-progress');
323     //var matches = getRecMatchesFromAttrCode(queuedRecordsMap[recId], attrCode);
324     var matches = queuedRecordsMap[recId].matches();
325     var records = [];
326     currentImportRecId = recId;
327     for(var i = 0; i < matches.length; i++)
328         records.push(matches[i].eg_record());
329
330     var retrieve = ['open-ils.search', 'open-ils.search.biblio.record_entry.slim.retrieve'];
331     var params = [records];
332     if(currentType == 'auth') {
333         retrieve = ['open-ils.cat', 'open-ils.cat.authority.record.retrieve'];
334         parmas = [authtoken, records, {clear_marc:1}];
335     }
336
337     fieldmapper.standardRequest(
338         retrieve,
339         {   async: true,
340             params:params,
341             oncomplete: function(r) {
342                 var recs = r.recv().content();
343                 if(e = openils.Event.parse(recs))
344                     return alert(e);
345
346                 /* ui mangling */
347                 displayGlobalDiv('vl-match-div');
348                 resetVlMatchGridLayout();
349                 currentMatchedRecords = recs;
350                 vlMatchGrid.setStructure(vlMatchGridLayout);
351
352                 // build the data store of records with match information
353                 var dataStore = bre.toStoreData(recs, null, 
354                     {virtualFields:['dest_matchpoint', 'src_matchpoint', '_id']});
355                 dataStore.identifier = '_id';
356
357
358                 for(var i = 0; i < dataStore.items.length; i++) {
359                     var item = dataStore.items[i];
360                     item._id = i; // just need something unique
361                     for(var j = 0; j < matches.length; j++) {
362                         var match = matches[j];
363                         if(match.eg_record() == item.id) {
364                             item.dest_matchpoint = match.field_type();
365                             var attr = getRecAttrFromMatch(queuedRecordsMap[recId], match);
366                             //item.src_matchpoint = getRecAttrDefFromAttr(attr, currentType).description();
367                             item.src_matchpoint = getRecAttrDefFromAttr(attr, currentType).code();
368                         }
369                     }
370                 }
371
372                 // now populate the grid
373                 vlPopulateMatchGrid(vlMatchGrid, dataStore);
374             }
375         }
376     );
377 }
378
379 function vlPopulateMatchGrid(grid, data) {
380     var store = new dojo.data.ItemFileReadStore({data:data});
381     var model = new dojox.grid.data.DojoData(
382         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
383     grid.setModel(model);
384     grid.update();
385 }
386
387
388 function vlLoadMARCHtml(recId, inCat, oncomplete) {
389     dijit.byId('vl-marc-html-done-button').onClick = oncomplete;
390     dijit.byId('vl-marc-html-edit-button').onClick = function() {vlLoadMarcEditor(currentType, recId);};
391     displayGlobalDiv('vl-generic-progress');
392     var api;
393     var params = [recId, 1];
394     if(inCat) {
395         api = ['open-ils.search', 'open-ils.search.biblio.record.html'];
396         if(currentType == 'auth')
397             api = ['open-ils.search', 'open-ils.search.authority.to_html'];
398     } else {
399         params = [authtoken, recId];
400         api = ['open-ils.vandelay', 'open-ils.vandelay.queued_bib_record.html'];
401         if(currentType == 'auth')
402             api = ['open-ils.vandelay', 'open-ils.vandelay.queued_authority_record.html'];
403     }
404     fieldmapper.standardRequest(
405         api, 
406         {   async: true,
407             params: params,
408             oncomplete: function(r) {
409             displayGlobalDiv('vl-marc-html-div');
410                 var html = r.recv().content();
411                 dojo.byId('vl-marc-record-html').innerHTML = html;
412             }
413         }
414     );
415 }
416
417
418 /*
419 function getRecMatchesFromAttrCode(rec, attrCode) {
420     var matches = [];
421     var attr = getRecAttrFromCode(rec, attrCode);
422     for(var j = 0; j < rec.matches().length; j++) {
423         var match = rec.matches()[j];
424         if(match.matched_attr() == attr.id()) 
425             matches.push(match);
426     }
427     return matches;
428 }
429 */
430
431 function getRecAttrFromMatch(rec, match) {
432     for(var i = 0; i < rec.attributes().length; i++) {
433         var attr = rec.attributes()[i];
434         if(attr.id() == match.matched_attr())
435             return attr;
436     }
437 }
438
439 function getRecAttrDefFromAttr(attr, type) {
440     var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
441     for(var i = 0; i < defs.length; i++) {
442         var def = defs[i];
443         if(def.id() == attr.field())
444             return def;
445     }
446 }
447
448 function getRecAttrFromCode(rec, attrCode) {
449     var defId = attrDefMap[attrCode];
450     var attrs = rec.attributes();
451     for(var i = 0; i < attrs.length; i++) {
452         var attr = attrs[i];
453         if(attr.field() == defId) 
454             return attr;
455     }
456     return null;
457 }
458
459 function vlGetViewMatches(rowIdx) {
460     var data = this.grid.model.getRow(rowIdx);
461     if(!data) return '';
462     var rec = queuedRecordsMap[data.id];
463     if(rec.matches().length > 0)
464         return this.value.replace('RECID', data.id);
465     return '';
466 }
467
468 function getAttrValue(rowIdx) {
469     var data = this.grid.model.getRow(rowIdx);
470     if(!data) return '';
471     var attrCode = this.field.split('.')[1];
472     var rec = queuedRecordsMap[data.id];
473     var attr = getRecAttrFromCode(rec, attrCode);
474     return (attr) ? attr.attr_value() : '';
475 }
476
477 function vlGetDateTimeField(rowIdx) {
478     data = this.grid.model.getRow(rowIdx);
479     if(!data) return '';
480     if(!data[this.field]) return '';
481     var date = dojo.date.stamp.fromISOString(data[this.field]);
482     return dojo.date.locale.format(date, {selector:'date'});
483 }
484
485 function vlGetCreator(rowIdx) {
486     data = this.grid.model.getRow(rowIdx);
487     if(!data) return '';
488     var id = data.creator;
489     if(userCache[id])
490         return userCache[id].usrname();
491     var user = fieldmapper.standardRequest(
492         ['open-ils.actor', 'open-ils.actor.user.retrieve'], [authtoken, id]);
493     if(e = openils.Event.parse(user))
494         return alert(e);
495     userCache[id] = user;
496     return user.usrname();
497 }
498
499 function vlGetViewMARC(rowIdx) {
500     data = this.grid.model.getRow(rowIdx);
501     if(data) 
502         return this.value.replace('RECID', data.id);
503 }
504
505 function vlGetOverlayTargetSelector(rowIdx) {
506     data = this.grid.model.getRow(rowIdx);
507     if(data) {
508         var value = this.value.replace('ID', data.id);
509         var overlay = currentOverlayRecordsMap[currentImportRecId];
510         if(overlay && overlay == data.id) 
511             value = value.replace('/>', 'checked="checked"/>');
512         return value;
513     }
514 }
515
516 /**
517   * see if the user has enabled overlays for the current match set and, 
518   * if so, map the current import record to the overlay target.
519   */
520 function vlHandleOverlayTargetSelected() {
521     if(vlOverlayTargetEnable.checked) {
522         for(var i = 0; i < currentMatchedRecords.length; i++) {
523             var matchRecId = currentMatchedRecords[i].id();
524             if(dojo.byId('vl-overlay-target-'+matchRecId).checked) {
525                 console.log("found overlay target " + matchRecId);
526                 currentOverlayRecordsMap[currentImportRecId] = matchRecId;
527                 dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = true;
528                 dojo.byId('vl-record-list-selected-' + currentImportRecId).parentNode.className = 'overlay_selected';
529                 return;
530             }
531         }
532     } else {
533         delete currentOverlayRecordsMap[currentImportRecId];
534         dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = false;
535     }
536 }
537
538 var vlQueueGridBuilt = false;
539 function buildRecordGrid(type) {
540     displayGlobalDiv('vl-queue-div');
541
542     currentOverlayRecordsMap = {};
543
544     if(!vlQueueGridBuilt) {
545         var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
546         for(var i = 0; i < defs.length; i++) {
547             var def = defs[i]
548             attrDefMap[def.code()] = def.id();
549             var col = {
550                 name:def.description(), 
551                 field:'attr.' + def.code(),
552                 get: getAttrValue,
553                 selectableColumn:true
554             };
555             vlQueueGridLayout[0].cells[0].push(col);
556         }
557         vlQueueGridBuilt = true;
558     }
559
560     var storeData;
561     if(type == 'bib')
562         storeData = vqbr.toStoreData(queuedRecords);
563     else
564         storeData = vqar.toStoreData(queuedRecords);
565
566     var store = new dojo.data.ItemFileReadStore({data:storeData});
567     var model = new dojox.grid.data.DojoData(
568         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
569     vlQueueGrid.setModel(model);
570
571     if(vlQueueGridColumePicker) {
572         vlQueueGrid.update();
573     } else {
574         vlQueueGridColumePicker = 
575             new openils.GridColumnPicker(vlQueueGridColumePickerDialog, 
576                 vlQueueGrid, vlQueueGridLayout, authtoken, 'vandelay.queue');
577         vlQueueGridColumePicker.load();
578     }
579 }
580
581 function vlQueueGridPrevPage() {
582     var page = parseInt(vlQueueDisplayPage.getValue());
583     if(page < 2) return;
584     vlQueueDisplayPage.setValue(page - 1);
585     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
586 }
587
588 function vlQueueGridNextPage() {
589     vlQueueDisplayPage.setValue(parseInt(vlQueueDisplayPage.getValue())+1);
590     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
591 }
592
593 function vlDeleteQueue(type, queueId, onload) {
594     fieldmapper.standardRequest(
595         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.delete'],
596         {   async: true,
597             params: [authtoken, queueId],
598             oncomplete: function(r) {
599                 var resp = r.recv().content();
600                 if(e = openils.Event.parse(resp))
601                     return alert(e);
602                 onload();
603             }
604         }
605     );
606 }
607
608
609 function vlQueueGridDrawSelectBox(rowIdx) {
610     var data = this.grid.model.getRow(rowIdx);
611     if(!data) return '';
612     var domId = 'vl-record-list-selected-' +data.id;
613     selectableGridRecords[domId] = data.id;
614     return "<div><input type='checkbox' id='"+domId+"'/></div>";
615 }
616
617 function vlSelectAllQueueGridRecords() {
618     for(var id in selectableGridRecords) 
619         dojo.byId(id).checked = true;
620 }
621 function vlSelectNoQueueGridRecords() {
622     for(var id in selectableGridRecords) 
623         dojo.byId(id).checked = false;
624 }
625 function vlToggleQueueGridSelect() {
626     if(dojo.byId('vl-queue-grid-row-selector').checked)
627         vlSelectAllQueueGridRecords();
628     else
629         vlSelectNoQueueGridRecords();
630 }
631
632 var handleRetrieveRecords = function() {
633     buildRecordGrid(currentType);
634 }
635
636 function vlImportSelectedRecords() {
637     displayGlobalDiv('vl-generic-progress-with-total');
638     var records = [];
639
640     for(var id in selectableGridRecords) {
641         if(dojo.byId(id).checked) {
642             var recId = selectableGridRecords[id];
643             var rec = queuedRecordsMap[recId];
644             if(!rec.import_time()) 
645                 records.push(recId);
646         }
647     }
648
649     fieldmapper.standardRequest(
650         ['open-ils.vandelay', 'open-ils.vandelay.'+currentType+'_record.list.import'],
651         {   async: true,
652             params: [authtoken, records, {overlay_map:currentOverlayRecordsMap}],
653             onresponse: function(r) {
654                 var resp = r.recv().content();
655                 if(e = openils.Event.parse(resp))
656                     return alert(e);
657                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
658             },
659             oncomplete: function() {
660                 return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
661             }
662         }
663     );
664 }
665
666 function vlImportRecordQueue(type, queueId, noMatchOnly, onload) {
667     displayGlobalDiv('vl-generic-progress-with-total');
668     var method = 'open-ils.vandelay.bib_queue.import';
669     if(noMatchOnly)
670         method = method.replace('import', 'nomatch.import');
671     if(type == 'auth')
672         method = method.replace('bib', 'auth');
673
674     fieldmapper.standardRequest(
675         ['open-ils.vandelay', method],
676         {   async: true,
677             params: [authtoken, queueId],
678             onresponse: function(r) {
679                 var resp = r.recv().content();
680                 if(e = openils.Event.parse(resp))
681                     return alert(e);
682                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
683             },
684             oncomplete: function() {onload();}
685         }
686     );
687 }
688
689
690 /**
691   * Create queue, upload MARC, process spool, load the newly created queue 
692   */
693 function batchUpload() {
694     var queueName = dijit.byId('vl-queue-name').getValue();
695     currentType = dijit.byId('vl-record-type').getValue();
696
697     var handleProcessSpool = function() {
698         console.log('records uploaded and spooled');
699         if(vlUploadQueueAutoImport.checked) {
700             vlImportRecordQueue(currentType, currentQueueId, true,  
701                 function() {
702                     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
703                 }
704             );
705         } else {
706             retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
707         }
708     }
709
710     var handleUploadMARC = function(key) {
711         console.log('marc uploaded');
712         dojo.style(dojo.byId('vl-upload-status-processing'), 'display', 'block');
713         processSpool(key, currentQueueId, currentType, handleProcessSpool);
714     };
715
716     var handleCreateQueue = function(queue) {
717         console.log('queue created ' + queue.name());
718         currentQueueId = queue.id();
719         uploadMARC(handleUploadMARC);
720     };
721     
722     if(vlUploadQueueSelector.getValue() && !queueName) {
723         currentQueueId = vlUploadQueueSelector.getValue();
724         console.log('adding records to existing queue ' + currentQueueId);
725         uploadMARC(handleUploadMARC);
726     } else {
727         createQueue(queueName, currentType, handleCreateQueue);
728     }
729 }
730
731
732 function vlFleshQueueSelect(selector, type) {
733     var data = (type == 'bib') ? vbq.toStoreData(allUserBibQueues) : vaq.toStoreData(allUserAuthQueues);
734     selector.store = new dojo.data.ItemFileReadStore({data:data});
735     selector.setValue(null);
736     selector.setDisplayedValue('');
737     if(data[0])
738         selector.setValue(data[0].id());
739 }
740
741 function vlShowUploadForm() {
742     displayGlobalDiv('vl-marc-upload-div');
743     vlFleshQueueSelect(vlUploadQueueSelector, vlUploadRecordType.getValue());
744 }
745
746 function vlShowQueueSelect() {
747     displayGlobalDiv('vl-queue-select-div');
748     vlFleshQueueSelect(vlQueueSelectQueueList, vlQueueSelectType.getValue());
749 }
750
751 function vlFetchQueueFromForm() {
752     currentType = vlQueueSelectType.getValue();
753     currentQueueId = vlQueueSelectQueueList.getValue();
754     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
755 }
756
757 function vlOpenMarcEditWindow(rec) {
758     /*
759         To run in Firefox directly, must set signed.applets.codebase_principal_support
760         to true in about:config
761     */
762     netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
763     win = window.open('/xul/server/cat/marcedit.xul'); // XXX version?
764     win.xulG = {
765         record : {marc : rec.marc()},
766         save : {
767             label: 'Save', // XXX
768             func: function(xmlString) {
769                 var method = 'open-ils.permacrud.update.' + rec.classname;
770                 rec.marc(xmlString);
771                 fieldmapper.standardRequest(
772                     ['open-ils.permacrud', method],
773                     {   async: true,
774                         params: [authtoken, rec],
775                         oncomplete: function(r) {
776                             if(e = openils.Event.parse(rec))
777                                 return alert(e);
778                             alert('Record Updated'); // XXX
779                             win.close();
780                             // XXX reload marc html view with updates
781                         }
782                     }
783                 );
784             },
785         }
786     };
787 }
788
789 function vlLoadMarcEditor(type, recId) {
790     var method = 'open-ils.permacrud.search.vqbr';
791     if(currentType != 'bib')
792         method = method.replace(/vqbr/,'vqar');
793
794     fieldmapper.standardRequest(
795         ['open-ils.permacrud', method],
796         {   async: true, 
797             params: [authtoken, {id : recId}],
798             oncomplete: function(r) {
799                 var rec = r.recv().content();
800                 if(e = openils.Event.parse(rec))
801                     return alert(e);
802                 vlOpenMarcEditWindow(rec);
803             }
804         }
805     );
806 }
807
808
809
810 //------------------------------------------------------------
811 // attribute editors
812
813 // attribute-editor global variables
814
815 var ATTR_EDITOR_IN_UPDATE_MODE = false; // true on 'edit', false on 'create'
816 var ATTR_EDIT_ID = null;                // id of current 'edit' attribute
817 var ATTR_EDIT_GROUP = 'bib';            // bib-attrs or auth-attrs
818
819 function vlAttrEditorInit() {
820     // set up tooltips on the edit form
821     connectTooltip('attr-editor-tags'); 
822     connectTooltip('attr-editor-subfields'); 
823 }
824
825 function vlShowAttrEditor() {
826     displayGlobalDiv('vl-attr-editor-div');
827     loadAttrEditorGrid();
828     idHide('vl-generic-progress');
829 }
830
831 function setAttrEditorGroup(groupName) {
832     // put us into 'bib'-attr or 'auth'-attr mode.
833     if (ATTR_EDIT_GROUP != groupName) {
834         ATTR_EDIT_GROUP = groupName;
835         loadAttrEditorGrid();
836     }
837 }
838
839 function onAttrEditorOpen() {
840     // the "bars" have the create/update/cancel/etc. buttons.
841     var create_bar = document.getElementById('attr-editor-create-bar');
842     var update_bar = document.getElementById('attr-editor-update-bar');
843     if (ATTR_EDITOR_IN_UPDATE_MODE) {
844         update_bar.style.display='table-row';
845         create_bar.style.display='none';
846         // hide the dropdown-button
847         idStyle('vl-create-attr-editor-button', 'visibility', 'hidden');
848     } else {
849         dijit.byId('attr-editor-dialog').reset();
850         create_bar.style.display='table-row';
851         update_bar.style.display='none';
852     }
853 }
854
855 function onAttrEditorClose() {
856     // reset the form to a "create" form. (We may have borrowed it for editing.)
857     ATTR_EDITOR_IN_UPDATE_MODE = false;
858     // show the dropdown-button
859     idStyle('vl-create-attr-editor-button', 'visibility', 'visible');
860 }
861
862 function loadAttrEditorGrid() {
863     var _data = (ATTR_EDIT_GROUP == 'auth') ? 
864         vqarad.toStoreData(authAttrDefs) : vqbrad.toStoreData(bibAttrDefs) ;
865                  
866     var store = new dojo.data.ItemFileReadStore({data:_data});
867     var model = new dojox.grid.data.DojoData(
868         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
869     attrEditorGrid.setModel(model);
870     attrEditorGrid.setStructure(vlAttrGridLayout);
871     attrEditorGrid.onRowClick = onAttrEditorClick;
872     attrEditorGrid.update();
873 }
874
875 function attrGridGetTag(n) {
876     // grid helper: return the tags from the row's xpath column.
877     var xp = this.grid.model.getRow(n);
878     return xp && xpathParser.parse(xp.xpath).tags;
879 }
880
881 function attrGridGetSubfield(n) {
882     // grid helper: return the subfields from the row's xpath column.
883     var xp = this.grid.model.getRow(n);
884     return xp && xpathParser.parse(xp.xpath).subfields;
885 }
886
887 function onAttrEditorClick(evt) {
888     var row = attrEditorGrid.model.getRow(evt.rowIndex);
889     ATTR_EDIT_ID = row.id;
890     ATTR_EDITOR_IN_UPDATE_MODE = true;
891
892     // populate the popup editor.
893     dojo.byId('attr-editor-code').value = row.code;
894     dojo.byId('attr-editor-description').value = row.description;
895     var parsed_xpath = xpathParser.parse(row.xpath);
896     dojo.byId('attr-editor-tags').value = parsed_xpath.tags;
897     dojo.byId('attr-editor-subfields').value = parsed_xpath.subfields;
898     dojo.byId('attr-editor-identifier').value = (row.ident ? 'True':'False');
899     dojo.byId('attr-editor-xpath').value = row.xpath;
900     dojo.byId('attr-editor-remove').value = row.remove;
901
902     // set up UI for editing
903     dojo.byId('vl-create-attr-editor-button').click();
904 }
905
906 function vlSaveAttrDefinition(data) {
907     idHide('vl-attr-editor-div');
908     idShow('vl-generic-progress');
909
910     data.id = ATTR_EDIT_ID;
911
912     // this ought to honour custom xpaths, but overwrite xpaths
913     // derived from tags/subfields.
914     if (data.xpath == '' || looksLikeDerivedXpath(data.xpath)) {
915         var _xpath = tagAndSubFieldsToXpath(data.tag, data.subfield);
916         data.xpath = _xpath;
917     }
918
919     // build up our permacrud params. Key variables here are
920     // "create or update" and "bib or auth".
921
922     var isAuth   = (ATTR_EDIT_GROUP == 'auth');
923     var isCreate = (ATTR_EDIT_ID == null);
924     var rad      = isAuth ? new vqarad() : new vqbrad() ;
925     var method   = 'open-ils.permacrud' + (isCreate ? '.create.' : '.update.') 
926         + (isAuth ? 'vqarad' : 'vqbrad');
927     var _data    = rad.fromStoreItem(data);
928
929     _data.ischanged(1);
930
931     fieldmapper.standardRequest(
932         ['open-ils.permacrud', method],
933         {   async: true,
934             params: [authtoken, _data ],
935             onresponse: function(r) { },
936             oncomplete: function(r) {
937                 attrEditorFetchAttrDefs(vlShowAttrEditor);
938                 ATTR_EDIT_ID = null;
939             },
940             onerror: function(r) {
941                 alert('vlSaveAttrDefinition comms error: ' + r);
942             }
943         }
944     );
945 }
946
947 function attrEditorFetchAttrDefs(callback) {
948     var fn = (ATTR_EDIT_GROUP == 'auth') ? vlFetchAuthAttrDefs : vlFetchBibAttrDefs;
949     return fn(callback);
950 }
951
952 function vlAttrDelete() {
953     idHide('vl-attr-editor-div');
954     idShow('vl-generic-progress');
955
956     var isAuth = (ATTR_EDIT_GROUP == 'auth');
957     var method = 'open-ils.permacrud.delete.' + (isAuth ? 'vqarad' : 'vqbrad');
958     var rad    = isAuth ? new vqarad() : new vqbrad() ;
959     fieldmapper.standardRequest(
960         ['open-ils.permacrud', method],
961         {   async: true,
962             params: [authtoken, rad.fromHash({ id : ATTR_EDIT_ID }), ],
963             oncomplete: function() {
964                 dijit.byId('attr-editor-dialog').onCancel(); // close the dialog
965                 attrEditorFetchAttrDefs(vlShowAttrEditor);
966                 ATTR_EDIT_ID = null;
967             },
968             onerror: function(r) {
969                 alert('vlAttrDelete comms error: ' + r);
970             }
971         }
972     );
973 }
974
975 // ------------------------------------------------------------
976 // utilities for attribute editors
977
978 // dom utilities (maybe dojo does these, and these should be replaced)
979
980 function idStyle(obId, k, v)    { document.getElementById(obId).style[k] = v;   }
981 function idShow(obId)           { idStyle(obId, 'display', 'block');            }
982 function idHide(obId)           { idStyle(obId, 'display' , 'none');            }
983
984 function connectTooltip(fieldId) {
985     // Given an element id, look up a tooltip element in the doc (same
986     // id with a '-tip' suffix) and associate the two. Maybe dojo has
987     // a better way to do this?
988     var fld = dojo.byId(fieldId);
989     var tip = dojo.byId(fieldId + '-tip');
990     dojo.connect(fld, 'onfocus', function(evt) {
991                      dijit.showTooltip(tip.innerHTML, fld, ['below', 'after']); });
992     dojo.connect(fld, 'onblur', function(evt) { dijit.hideTooltip(fld); });
993 }
994
995 // xpath utilities
996
997 var xpathParser = new openils.MarcXPathParser();
998
999 function tagAndSubFieldsToXpath(tags, subfields) {
1000     // given tags, and subfields, build up an XPath.
1001     try {
1002         var parts = {
1003             'tags':tags.match(/[\d]+/g), 
1004             'subfields':subfields.match(/[a-zA-z]/g) };
1005         return xpathParser.compile(parts);
1006     } catch (err) {
1007         return {'parts':null, 'tags':null, 'error':err};
1008     }
1009 }
1010
1011 function looksLikeDerivedXpath(path) {
1012     // Does this path look like it was derived from tags and subfields?
1013     var parsed = xpathParser.parse(path);
1014     if (parsed.tags == null) 
1015         return false;
1016     var compiled = xpathParser.compile(parsed);
1017     return (path == compiled);
1018 }
1019
1020 // amazing xpath-util unit-tests
1021 if (!looksLikeDerivedXpath('//*[@tag="901"]/*[@code="c"]'))     alert('vandelay xpath-utility error');
1022 if ( looksLikeDerivedXpath('ba-boo-ba-boo!'))                   alert('vandelay xpath-utility error');