removed extra require
[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     if(currentQueueId)
210         return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
211     dojo.style('vl-nav-bar', 'visibility', 'visible');
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     displayGlobalDiv('vl-generic-progress');
391     var api;
392     var params = [recId, 1];
393     if(inCat) {
394         api = ['open-ils.search', 'open-ils.search.biblio.record.html'];
395         if(currentType == 'auth')
396             api = ['open-ils.search', 'open-ils.search.authority.to_html'];
397     } else {
398         params = [authtoken, recId];
399         api = ['open-ils.vandelay', 'open-ils.vandelay.queued_bib_record.html'];
400         if(currentType == 'auth')
401             api = ['open-ils.vandelay', 'open-ils.vandelay.queued_authority_record.html'];
402     }
403     fieldmapper.standardRequest(
404         api, 
405         {   async: true,
406             params: params,
407             oncomplete: function(r) {
408             displayGlobalDiv('vl-marc-html-div');
409                 var html = r.recv().content();
410                 dojo.byId('vl-marc-record-html').innerHTML = html;
411             }
412         }
413     );
414 }
415
416
417 /*
418 function getRecMatchesFromAttrCode(rec, attrCode) {
419     var matches = [];
420     var attr = getRecAttrFromCode(rec, attrCode);
421     for(var j = 0; j < rec.matches().length; j++) {
422         var match = rec.matches()[j];
423         if(match.matched_attr() == attr.id()) 
424             matches.push(match);
425     }
426     return matches;
427 }
428 */
429
430 function getRecAttrFromMatch(rec, match) {
431     for(var i = 0; i < rec.attributes().length; i++) {
432         var attr = rec.attributes()[i];
433         if(attr.id() == match.matched_attr())
434             return attr;
435     }
436 }
437
438 function getRecAttrDefFromAttr(attr, type) {
439     var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
440     for(var i = 0; i < defs.length; i++) {
441         var def = defs[i];
442         if(def.id() == attr.field())
443             return def;
444     }
445 }
446
447 function getRecAttrFromCode(rec, attrCode) {
448     var defId = attrDefMap[attrCode];
449     var attrs = rec.attributes();
450     for(var i = 0; i < attrs.length; i++) {
451         var attr = attrs[i];
452         if(attr.field() == defId) 
453             return attr;
454     }
455     return null;
456 }
457
458 function vlGetViewMatches(rowIdx) {
459     var data = this.grid.model.getRow(rowIdx);
460     if(!data) return '';
461     var rec = queuedRecordsMap[data.id];
462     if(rec.matches().length > 0)
463         return this.value.replace('RECID', data.id);
464     return '';
465 }
466
467 function getAttrValue(rowIdx) {
468     var data = this.grid.model.getRow(rowIdx);
469     if(!data) return '';
470     var attrCode = this.field.split('.')[1];
471     var rec = queuedRecordsMap[data.id];
472     var attr = getRecAttrFromCode(rec, attrCode);
473     return (attr) ? attr.attr_value() : '';
474 }
475
476 function vlGetDateTimeField(rowIdx) {
477     data = this.grid.model.getRow(rowIdx);
478     if(!data) return '';
479     if(!data[this.field]) return '';
480     var date = dojo.date.stamp.fromISOString(data[this.field]);
481     return dojo.date.locale.format(date, {selector:'date'});
482 }
483
484 function vlGetCreator(rowIdx) {
485     data = this.grid.model.getRow(rowIdx);
486     if(!data) return '';
487     var id = data.creator;
488     if(userCache[id])
489         return userCache[id].usrname();
490     var user = fieldmapper.standardRequest(
491         ['open-ils.actor', 'open-ils.actor.user.retrieve'], [authtoken, id]);
492     if(e = openils.Event.parse(user))
493         return alert(e);
494     userCache[id] = user;
495     return user.usrname();
496 }
497
498 function vlGetViewMARC(rowIdx) {
499     data = this.grid.model.getRow(rowIdx);
500     if(data) 
501         return this.value.replace('RECID', data.id);
502 }
503
504 function vlGetOverlayTargetSelector(rowIdx) {
505     data = this.grid.model.getRow(rowIdx);
506     if(data) {
507         var value = this.value.replace('ID', data.id);
508         var overlay = currentOverlayRecordsMap[currentImportRecId];
509         if(overlay && overlay == data.id) 
510             value = value.replace('/>', 'checked="checked"/>');
511         return value;
512     }
513 }
514
515 /**
516   * see if the user has enabled overlays for the current match set and, 
517   * if so, map the current import record to the overlay target.
518   */
519 function vlHandleOverlayTargetSelected() {
520     if(vlOverlayTargetEnable.checked) {
521         for(var i = 0; i < currentMatchedRecords.length; i++) {
522             var matchRecId = currentMatchedRecords[i].id();
523             if(dojo.byId('vl-overlay-target-'+matchRecId).checked) {
524                 console.log("found overlay target " + matchRecId);
525                 currentOverlayRecordsMap[currentImportRecId] = matchRecId;
526                 dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = true;
527                 dojo.byId('vl-record-list-selected-' + currentImportRecId).parentNode.className = 'overlay_selected';
528                 return;
529             }
530         }
531     } else {
532         delete currentOverlayRecordsMap[currentImportRecId];
533         dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = false;
534     }
535 }
536
537 var vlQueueGridBuilt = false;
538 function buildRecordGrid(type) {
539     displayGlobalDiv('vl-queue-div');
540
541     currentOverlayRecordsMap = {};
542
543     if(!vlQueueGridBuilt) {
544         var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
545         for(var i = 0; i < defs.length; i++) {
546             var def = defs[i]
547             attrDefMap[def.code()] = def.id();
548             var col = {
549                 name:def.description(), 
550                 field:'attr.' + def.code(),
551                 get: getAttrValue,
552                 selectableColumn:true
553             };
554             vlQueueGridLayout[0].cells[0].push(col);
555         }
556         vlQueueGridBuilt = true;
557     }
558
559     var storeData;
560     if(type == 'bib')
561         storeData = vqbr.toStoreData(queuedRecords);
562     else
563         storeData = vqar.toStoreData(queuedRecords);
564
565     var store = new dojo.data.ItemFileReadStore({data:storeData});
566     var model = new dojox.grid.data.DojoData(
567         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
568
569     vlQueueGrid.setModel(model);
570     if(vlQueueGridColumePicker) 
571         vlQueueGrid.setStructure(vlQueueGridColumePicker.structure);
572     else
573         vlQueueGrid.setStructure(vlQueueGridLayout);
574     vlQueueGrid.update();
575
576     if(!vlQueueGridColumePicker) {
577         vlQueueGridColumePicker = 
578             new openils.GridColumnPicker(vlQueueGridColumePickerDialog, vlQueueGrid);
579     }
580 }
581
582 function vlQueueGridPrevPage() {
583     var page = parseInt(vlQueueDisplayPage.getValue());
584     if(page < 2) return;
585     vlQueueDisplayPage.setValue(page - 1);
586     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
587 }
588
589 function vlQueueGridNextPage() {
590     vlQueueDisplayPage.setValue(parseInt(vlQueueDisplayPage.getValue())+1);
591     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
592 }
593
594 function vlDeleteQueue(type, queueId, onload) {
595     fieldmapper.standardRequest(
596         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.delete'],
597         {   async: true,
598             params: [authtoken, queueId],
599             oncomplete: function(r) {
600                 var resp = r.recv().content();
601                 if(e = openils.Event.parse(resp))
602                     return alert(e);
603                 onload();
604             }
605         }
606     );
607 }
608
609
610 function vlQueueGridDrawSelectBox(rowIdx) {
611     var data = this.grid.model.getRow(rowIdx);
612     if(!data) return '';
613     var domId = 'vl-record-list-selected-' +data.id;
614     selectableGridRecords[domId] = data.id;
615     return "<div><input type='checkbox' id='"+domId+"'/></div>";
616 }
617
618 function vlSelectAllQueueGridRecords() {
619     for(var id in selectableGridRecords) 
620         dojo.byId(id).checked = true;
621 }
622 function vlSelectNoQueueGridRecords() {
623     for(var id in selectableGridRecords) 
624         dojo.byId(id).checked = false;
625 }
626 function vlToggleQueueGridSelect() {
627     if(dojo.byId('vl-queue-grid-row-selector').checked)
628         vlSelectAllQueueGridRecords();
629     else
630         vlSelectNoQueueGridRecords();
631 }
632
633 var handleRetrieveRecords = function() {
634     buildRecordGrid(currentType);
635 }
636
637 function vlImportSelectedRecords() {
638     displayGlobalDiv('vl-generic-progress-with-total');
639     var records = [];
640
641     for(var id in selectableGridRecords) {
642         if(dojo.byId(id).checked) {
643             var recId = selectableGridRecords[id];
644             var rec = queuedRecordsMap[recId];
645             if(!rec.import_time()) 
646                 records.push(recId);
647         }
648     }
649
650     fieldmapper.standardRequest(
651         ['open-ils.vandelay', 'open-ils.vandelay.'+currentType+'_record.list.import'],
652         {   async: true,
653             params: [authtoken, records, {overlay_map:currentOverlayRecordsMap}],
654             onresponse: function(r) {
655                 var resp = r.recv().content();
656                 if(e = openils.Event.parse(resp))
657                     return alert(e);
658                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
659             },
660             oncomplete: function() {
661                 return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
662             }
663         }
664     );
665 }
666
667 function vlImportRecordQueue(type, queueId, noMatchOnly, onload) {
668     displayGlobalDiv('vl-generic-progress-with-total');
669     var method = 'open-ils.vandelay.bib_queue.import';
670     if(noMatchOnly)
671         method = method.replace('import', 'nomatch.import');
672     if(type == 'auth')
673         method = method.replace('bib', 'auth');
674
675     fieldmapper.standardRequest(
676         ['open-ils.vandelay', method],
677         {   async: true,
678             params: [authtoken, queueId],
679             onresponse: function(r) {
680                 var resp = r.recv().content();
681                 if(e = openils.Event.parse(resp))
682                     return alert(e);
683                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
684             },
685             oncomplete: function() {onload();}
686         }
687     );
688 }
689
690
691 /**
692   * Create queue, upload MARC, process spool, load the newly created queue 
693   */
694 function batchUpload() {
695     var queueName = dijit.byId('vl-queue-name').getValue();
696     currentType = dijit.byId('vl-record-type').getValue();
697
698     var handleProcessSpool = function() {
699         console.log('records uploaded and spooled');
700         if(vlUploadQueueAutoImport.checked) {
701             vlImportRecordQueue(currentType, currentQueueId, true,  
702                 function() {
703                     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
704                 }
705             );
706         } else {
707             retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
708         }
709     }
710
711     var handleUploadMARC = function(key) {
712         console.log('marc uploaded');
713         dojo.style(dojo.byId('vl-upload-status-processing'), 'display', 'block');
714         processSpool(key, currentQueueId, currentType, handleProcessSpool);
715     };
716
717     var handleCreateQueue = function(queue) {
718         console.log('queue created ' + queue.name());
719         currentQueueId = queue.id();
720         uploadMARC(handleUploadMARC);
721     };
722     
723     if(vlUploadQueueSelector.getValue() && !queueName) {
724         currentQueueId = vlUploadQueueSelector.getValue();
725         console.log('adding records to existing queue ' + currentQueueId);
726         uploadMARC(handleUploadMARC);
727     } else {
728         createQueue(queueName, currentType, handleCreateQueue);
729     }
730 }
731
732
733 function vlFleshQueueSelect(selector, type) {
734     var data = (type == 'bib') ? vbq.toStoreData(allUserBibQueues) : vaq.toStoreData(allUserAuthQueues);
735     selector.store = new dojo.data.ItemFileReadStore({data:data});
736     selector.setValue(null);
737     selector.setDisplayedValue('');
738     if(data[0])
739         selector.setValue(data[0].id());
740 }
741
742 function vlShowUploadForm() {
743     displayGlobalDiv('vl-marc-upload-div');
744     vlFleshQueueSelect(vlUploadQueueSelector, vlUploadRecordType.getValue());
745 }
746
747 function vlShowQueueSelect() {
748     displayGlobalDiv('vl-queue-select-div');
749     vlFleshQueueSelect(vlQueueSelectQueueList, vlQueueSelectType.getValue());
750 }
751
752 function vlFetchQueueFromForm() {
753     currentType = vlQueueSelectType.getValue();
754     currentQueueId = vlQueueSelectQueueList.getValue();
755     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
756 }
757
758
759
760 //------------------------------------------------------------
761 // attribute editors
762
763 // attribute-editor global variables
764
765 var ATTR_EDITOR_IN_UPDATE_MODE = false; // true on 'edit', false on 'create'
766 var ATTR_EDIT_ID = null;                // id of current 'edit' attribute
767 var ATTR_EDIT_GROUP = 'bib';            // bib-attrs or auth-attrs
768
769 function vlAttrEditorInit() {
770     // set up tooltips on the edit form
771     connectTooltip('attr-editor-tags'); 
772     connectTooltip('attr-editor-subfields'); 
773 }
774
775 function vlShowAttrEditor() {
776     displayGlobalDiv('vl-attr-editor-div');
777     loadAttrEditorGrid();
778     idHide('vl-generic-progress');
779 }
780
781 function setAttrEditorGroup(groupName) {
782     // put us into 'bib'-attr or 'auth'-attr mode.
783     if (ATTR_EDIT_GROUP != groupName) {
784         ATTR_EDIT_GROUP = groupName;
785         loadAttrEditorGrid();
786     }
787 }
788
789 function onAttrEditorOpen() {
790     // the "bars" have the create/update/cancel/etc. buttons.
791     var create_bar = document.getElementById('attr-editor-create-bar');
792     var update_bar = document.getElementById('attr-editor-update-bar');
793     if (ATTR_EDITOR_IN_UPDATE_MODE) {
794         update_bar.style.display='table-row';
795         create_bar.style.display='none';
796         // hide the dropdown-button
797         idStyle('vl-create-attr-editor-button', 'visibility', 'hidden');
798     } else {
799         dijit.byId('attr-editor-dialog').reset();
800         create_bar.style.display='table-row';
801         update_bar.style.display='none';
802     }
803 }
804
805 function onAttrEditorClose() {
806     // reset the form to a "create" form. (We may have borrowed it for editing.)
807     ATTR_EDITOR_IN_UPDATE_MODE = false;
808     // show the dropdown-button
809     idStyle('vl-create-attr-editor-button', 'visibility', 'visible');
810 }
811
812 function loadAttrEditorGrid() {
813     var _data = (ATTR_EDIT_GROUP == 'auth') ? 
814         vqarad.toStoreData(authAttrDefs) : vqbrad.toStoreData(bibAttrDefs) ;
815                  
816     var store = new dojo.data.ItemFileReadStore({data:_data});
817     var model = new dojox.grid.data.DojoData(
818         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
819     attrEditorGrid.setModel(model);
820     attrEditorGrid.setStructure(vlAttrGridLayout);
821     attrEditorGrid.onRowClick = onAttrEditorClick;
822     attrEditorGrid.update();
823 }
824
825 function attrGridGetTag(n) {
826     // grid helper: return the tags from the row's xpath column.
827     var xp = this.grid.model.getRow(n);
828     return xp && xpathParser.parse(xp.xpath).tags;
829 }
830
831 function attrGridGetSubfield(n) {
832     // grid helper: return the subfields from the row's xpath column.
833     var xp = this.grid.model.getRow(n);
834     return xp && xpathParser.parse(xp.xpath).subfields;
835 }
836
837 function onAttrEditorClick(evt) {
838     var row = attrEditorGrid.model.getRow(evt.rowIndex);
839     ATTR_EDIT_ID = row.id;
840     ATTR_EDITOR_IN_UPDATE_MODE = true;
841
842     // populate the popup editor.
843     dojo.byId('attr-editor-code').value = row.code;
844     dojo.byId('attr-editor-description').value = row.description;
845     var parsed_xpath = xpathParser.parse(row.xpath);
846     dojo.byId('attr-editor-tags').value = parsed_xpath.tags;
847     dojo.byId('attr-editor-subfields').value = parsed_xpath.subfields;
848     dojo.byId('attr-editor-identifier').value = (row.ident ? 'True':'False');
849     dojo.byId('attr-editor-xpath').value = row.xpath;
850     dojo.byId('attr-editor-remove').value = row.remove;
851
852     // set up UI for editing
853     dojo.byId('vl-create-attr-editor-button').click();
854 }
855
856 function vlSaveAttrDefinition(data) {
857     idHide('vl-attr-editor-div');
858     idShow('vl-generic-progress');
859
860     data.id = ATTR_EDIT_ID;
861
862     // this ought to honour custom xpaths, but overwrite xpaths
863     // derived from tags/subfields.
864     if (data.xpath == '' || looksLikeDerivedXpath(data.xpath)) {
865         var _xpath = tagAndSubFieldsToXpath(data.tag, data.subfield);
866         data.xpath = _xpath;
867     }
868
869     // build up our permacrud params. Key variables here are
870     // "create or update" and "bib or auth".
871
872     var isAuth   = (ATTR_EDIT_GROUP == 'auth');
873     var isCreate = (ATTR_EDIT_ID == null);
874     var rad      = isAuth ? new vqarad() : new vqbrad() ;
875     var method   = 'open-ils.permacrud' + (isCreate ? '.create.' : '.update.') 
876         + (isAuth ? 'vqarad' : 'vqbrad');
877     var _data    = rad.fromStoreItem(data);
878
879     _data.ischanged(1);
880
881     fieldmapper.standardRequest(
882         ['open-ils.permacrud', method],
883         {   async: true,
884             params: [authtoken, _data ],
885             onresponse: function(r) { },
886             oncomplete: function(r) {
887                 attrEditorFetchAttrDefs(vlShowAttrEditor);
888                 ATTR_EDIT_ID = null;
889             },
890             onerror: function(r) {
891                 alert('vlSaveAttrDefinition comms error: ' + r);
892             }
893         }
894     );
895 }
896
897 function attrEditorFetchAttrDefs(callback) {
898     var fn = (ATTR_EDIT_GROUP == 'auth') ? vlFetchAuthAttrDefs : vlFetchBibAttrDefs;
899     return fn(callback);
900 }
901
902 function vlAttrDelete() {
903     idHide('vl-attr-editor-div');
904     idShow('vl-generic-progress');
905
906     var isAuth = (ATTR_EDIT_GROUP == 'auth');
907     var method = 'open-ils.permacrud.delete.' + (isAuth ? 'vqarad' : 'vqbrad');
908     var rad    = isAuth ? new vqarad() : new vqbrad() ;
909     fieldmapper.standardRequest(
910         ['open-ils.permacrud', method],
911         {   async: true,
912             params: [authtoken, rad.fromHash({ id : ATTR_EDIT_ID }), ],
913             oncomplete: function() {
914                 dijit.byId('attr-editor-dialog').onCancel(); // close the dialog
915                 attrEditorFetchAttrDefs(vlShowAttrEditor);
916                 ATTR_EDIT_ID = null;
917             },
918             onerror: function(r) {
919                 alert('vlAttrDelete comms error: ' + r);
920             }
921         }
922     );
923 }
924
925 // ------------------------------------------------------------
926 // utilities for attribute editors
927
928 // dom utilities (maybe dojo does these, and these should be replaced)
929
930 function idStyle(obId, k, v)    { document.getElementById(obId).style[k] = v;   }
931 function idShow(obId)           { idStyle(obId, 'display', 'block');            }
932 function idHide(obId)           { idStyle(obId, 'display' , 'none');            }
933
934 function connectTooltip(fieldId) {
935     // Given an element id, look up a tooltip element in the doc (same
936     // id with a '-tip' suffix) and associate the two. Maybe dojo has
937     // a better way to do this?
938     var fld = dojo.byId(fieldId);
939     var tip = dojo.byId(fieldId + '-tip');
940     dojo.connect(fld, 'onfocus', function(evt) {
941                      dijit.showTooltip(tip.innerHTML, fld, ['below', 'after']); });
942     dojo.connect(fld, 'onblur', function(evt) { dijit.hideTooltip(fld); });
943 }
944
945 // xpath utilities
946
947 var xpathParser = new openils.MarcXPathParser();
948
949 function tagAndSubFieldsToXpath(tags, subfields) {
950     // given tags, and subfields, build up an XPath.
951     try {
952         var parts = {
953             'tags':tags.match(/[\d]+/g), 
954             'subfields':subfields.match(/[a-zA-z]/g) };
955         return xpathParser.compile(parts);
956     } catch (err) {
957         return {'parts':null, 'tags':null, 'error':err};
958     }
959 }
960
961 function looksLikeDerivedXpath(path) {
962     // Does this path look like it was derived from tags and subfields?
963     var parsed = xpathParser.parse(path);
964     if (parsed.tags == null) 
965         return false;
966     var compiled = xpathParser.compile(parsed);
967     return (path == compiled);
968 }
969
970 // amazing xpath-util unit-tests
971 if (!looksLikeDerivedXpath('//*[@tag="901"]/*[@code="c"]'))     alert('vandelay xpath-utility error');
972 if ( looksLikeDerivedXpath('ba-boo-ba-boo!'))                   alert('vandelay xpath-utility error');