Line 0
Link Here
|
|
|
1 |
(function () { |
2 |
// Bail if there aren't any metadata enrichment plugins installed |
3 |
if (typeof metadata_enrichment_services === 'undefined') { |
4 |
console.log('No metadata enrichment plugins found.') |
5 |
return; |
6 |
} |
7 |
|
8 |
window.addEventListener('load', onload); |
9 |
|
10 |
// Delay between API requests |
11 |
var debounceDelay = 1000; |
12 |
|
13 |
// Elements we work frequently with |
14 |
var textarea = document.getElementById("identifiers_input"); |
15 |
var nameInput = document.getElementById("name"); |
16 |
var cardnumberInput = document.getElementById("batchcardnumber"); |
17 |
var branchcodeSelect = document.getElementById("branchcode"); |
18 |
var processButton = document.getElementById("process_button"); |
19 |
var createButton = document.getElementById("button_create_batch"); |
20 |
var finishButton = document.getElementById("button_finish"); |
21 |
var batchItemsDisplay = document.getElementById("add_batch_items"); |
22 |
var createProgressTotal = document.getElementById("processed_total"); |
23 |
var createProgressCount = document.getElementById("processed_count"); |
24 |
var createProgressFailed = document.getElementById("processed_failed"); |
25 |
var createProgressBar = document.getElementById("processed_progress_bar"); |
26 |
var identifierTable = document.getElementById('identifier-table'); |
27 |
var createRequestsButton = document.getElementById('create-requests-button'); |
28 |
var statusesSelect = document.getElementById('statuscode'); |
29 |
var cancelButton = document.getElementById('lhs').querySelector('button'); |
30 |
var cancelButtonOriginalText = cancelButton.innerHTML; |
31 |
|
32 |
// We need a data structure keyed on identifier type, which tells us how to parse that |
33 |
// identifier type and what services can get its metadata. We receive an array of |
34 |
// available services |
35 |
var supportedIdentifiers = {}; |
36 |
metadata_enrichment_services.forEach(function (service) { |
37 |
// Iterate the identifiers that this service supports |
38 |
Object.keys(service.identifiers_supported).forEach(function (idType) { |
39 |
if (!supportedIdentifiers[idType]) { |
40 |
supportedIdentifiers[idType] = []; |
41 |
} |
42 |
supportedIdentifiers[idType].push(service); |
43 |
}); |
44 |
}); |
45 |
|
46 |
// An object for when we're creating a new batch |
47 |
var emptyBatch = { |
48 |
name: '', |
49 |
backend: null, |
50 |
cardnumber: '', |
51 |
branchcode: '', |
52 |
statuscode: 'NEW' |
53 |
}; |
54 |
|
55 |
// The object that holds the batch we're working with |
56 |
// It's a proxy so we can update portions of the UI |
57 |
// upon changes |
58 |
var batch = new Proxy( |
59 |
{ data: {} }, |
60 |
{ |
61 |
get: function (obj, prop) { |
62 |
return obj[prop]; |
63 |
}, |
64 |
set: function (obj, prop, value) { |
65 |
obj[prop] = value; |
66 |
manageBatchItemsDisplay(); |
67 |
updateBatchInputs(); |
68 |
disableCardnumberInput(); |
69 |
displayPatronName(); |
70 |
updateStatusesSelect(); |
71 |
} |
72 |
} |
73 |
); |
74 |
|
75 |
// The object that holds the contents of the table |
76 |
// It's a proxy so we can make it automatically redraw the |
77 |
// table upon changes |
78 |
var tableContent = new Proxy( |
79 |
{ data: [] }, |
80 |
{ |
81 |
get: function (obj, prop) { |
82 |
return obj[prop]; |
83 |
}, |
84 |
set: function (obj, prop, value) { |
85 |
obj[prop] = value; |
86 |
updateTable(); |
87 |
updateRowCount(); |
88 |
updateProcessTotals(); |
89 |
checkAvailability(); |
90 |
} |
91 |
} |
92 |
); |
93 |
|
94 |
// The object that holds the contents of the table |
95 |
// It's a proxy so we can update portions of the UI |
96 |
// upon changes |
97 |
var statuses = new Proxy( |
98 |
{ data: [] }, |
99 |
{ |
100 |
get: function (obj, prop) { |
101 |
return obj[prop]; |
102 |
}, |
103 |
set: function (obj, prop, value) { |
104 |
obj[prop] = value; |
105 |
updateStatusesSelect(); |
106 |
} |
107 |
} |
108 |
); |
109 |
|
110 |
var progressTotals = new Proxy( |
111 |
{ |
112 |
data: {} |
113 |
}, |
114 |
{ |
115 |
get: function (obj, prop) { |
116 |
return obj[prop]; |
117 |
}, |
118 |
set: function (obj, prop, value) { |
119 |
obj[prop] = value; |
120 |
showCreateRequestsButton(); |
121 |
} |
122 |
} |
123 |
); |
124 |
|
125 |
// Keep track of submission API calls that are in progress |
126 |
// so we don't duplicate them |
127 |
var submissionSent = {}; |
128 |
|
129 |
// Keep track of availability API calls that are in progress |
130 |
// so we don't duplicate them |
131 |
var availabilitySent = {}; |
132 |
|
133 |
// Are we updating an existing batch |
134 |
var isUpdate = false; |
135 |
|
136 |
// The datatable |
137 |
var table; |
138 |
var tableEl = document.getElementById('identifier-table'); |
139 |
|
140 |
// The element that potentially holds the ID of the batch |
141 |
// we're working with |
142 |
var idEl = document.getElementById('ill-batch-details'); |
143 |
var batchId = null; |
144 |
var backend = null; |
145 |
|
146 |
function onload() { |
147 |
$('#ill-batch-modal').on('shown.bs.modal', function () { |
148 |
init(); |
149 |
patronAutocomplete(); |
150 |
batchInputsEventListeners(); |
151 |
createButtonEventListener(); |
152 |
createRequestsButtonEventListener(); |
153 |
moreLessEventListener(); |
154 |
removeRowEventListener(); |
155 |
}); |
156 |
$('#ill-batch-modal').on('hidden.bs.modal', function () { |
157 |
// Reset our state when we close the modal |
158 |
// TODO: need to also reset progress bar and already processed identifiers |
159 |
delete idEl.dataset.batchId; |
160 |
delete idEl.dataset.backend; |
161 |
batchId = null; |
162 |
tableEl.style.display = 'none'; |
163 |
tableContent.data = []; |
164 |
progressTotals.data = { |
165 |
total: 0, |
166 |
count: 0, |
167 |
failed: 0 |
168 |
}; |
169 |
textarea.value = ''; |
170 |
batch.data = {}; |
171 |
cancelButton.innerHTML = cancelButtonOriginalText; |
172 |
// Remove event listeners we created |
173 |
removeEventListeners(); |
174 |
}); |
175 |
}; |
176 |
|
177 |
function init() { |
178 |
batchId = idEl.dataset.batchId; |
179 |
backend = idEl.dataset.backend; |
180 |
emptyBatch.backend = backend; |
181 |
progressTotals.data = { |
182 |
total: 0, |
183 |
count: 0, |
184 |
failed: 0 |
185 |
}; |
186 |
if (batchId) { |
187 |
fetchBatch(); |
188 |
isUpdate = true; |
189 |
setModalHeading(); |
190 |
} else { |
191 |
batch.data = emptyBatch; |
192 |
setModalHeading(); |
193 |
} |
194 |
fetchStatuses(); |
195 |
finishButtonEventListener(); |
196 |
processButtonEventListener(); |
197 |
identifierTextareaEventListener(); |
198 |
displaySupportedIdentifiers(); |
199 |
createButtonEventListener(); |
200 |
updateRowCount(); |
201 |
}; |
202 |
|
203 |
function initPostCreate() { |
204 |
disableCreateButton(); |
205 |
cancelButton.innerHTML = ill_batch_create_cancel_button; |
206 |
}; |
207 |
|
208 |
function setFinishButton() { |
209 |
if (batch.data.patron) { |
210 |
finishButton.removeAttribute('disabled'); |
211 |
} |
212 |
}; |
213 |
|
214 |
function setModalHeading() { |
215 |
var heading = document.getElementById('ill-batch-modal-label'); |
216 |
heading.textContent = isUpdate ? ill_batch_update : ill_batch_add; |
217 |
} |
218 |
|
219 |
// Identify items that have metadata and therefore can have a local request |
220 |
// created, and do so |
221 |
function requestRequestable() { |
222 |
createRequestsButton.setAttribute('disabled', true); |
223 |
createRequestsButton.setAttribute('aria-disabled', true); |
224 |
setFinishButton(); |
225 |
var toCheck = tableContent.data; |
226 |
toCheck.forEach(function (row) { |
227 |
if ( |
228 |
!row.requestId && |
229 |
Object.keys(row.metadata).length > 0 && |
230 |
!submissionSent[row.value] |
231 |
) { |
232 |
submissionSent[row.value] = 1; |
233 |
makeLocalSubmission(row.value, row.metadata); |
234 |
} |
235 |
}); |
236 |
}; |
237 |
|
238 |
// Identify items that can have their availability checked, and do it |
239 |
function checkAvailability() { |
240 |
// Only proceed if we've got services that can check availability |
241 |
if (!batch_availability_services || batch_availability_services.length === 0) return; |
242 |
var toCheck = tableContent.data; |
243 |
toCheck.forEach(function (row) { |
244 |
if ( |
245 |
!row.url && |
246 |
Object.keys(row.metadata).length > 0 && |
247 |
!availabilitySent[row.value] |
248 |
) { |
249 |
availabilitySent[row.value] = 1; |
250 |
getAvailability(row.value, row.metadata); |
251 |
} |
252 |
}); |
253 |
}; |
254 |
|
255 |
// Check availability services for immediate availability, if found, |
256 |
// create a link in the table linking to the item |
257 |
function getAvailability(identifier, metadata) { |
258 |
// Prep the metadata for passing to the availability plugins |
259 |
let availability_object = {}; |
260 |
if (metadata.issn) availability_object['issn'] = metadata.issn; |
261 |
if (metadata.doi) availability_object['doi'] = metadata.doi; |
262 |
if (metadata.pubmedid) availability_object['pubmedid'] = metadata.pubmedid; |
263 |
var prepped = encodeURIComponent(base64EncodeUnicode(JSON.stringify(availability_object))); |
264 |
for (i = 0; i < batch_availability_services.length; i++) { |
265 |
var service = batch_availability_services[i]; |
266 |
window.doApiRequest( |
267 |
service.endpoint + prepped |
268 |
) |
269 |
.then(function (response) { |
270 |
return response.json(); |
271 |
}) |
272 |
.then(function (data) { |
273 |
if (data.results.search_results && data.results.search_results.length > 0) { |
274 |
var result = data.results.search_results[0]; |
275 |
tableContent.data = tableContent.data.map(function (row) { |
276 |
if (row.value === identifier) { |
277 |
row.url = result.url; |
278 |
row.availabilitySupplier = service.name; |
279 |
} |
280 |
return row; |
281 |
}); |
282 |
} |
283 |
}); |
284 |
} |
285 |
}; |
286 |
|
287 |
// Help btoa with > 8 bit strings |
288 |
// Shamelessly grabbed from: https://www.base64encoder.io/javascript/ |
289 |
function base64EncodeUnicode(str) { |
290 |
// First we escape the string using encodeURIComponent to get the UTF-8 encoding of the characters, |
291 |
// then we convert the percent encodings into raw bytes, and finally feed it to btoa() function. |
292 |
utf8Bytes = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { |
293 |
return String.fromCharCode('0x' + p1); |
294 |
}); |
295 |
|
296 |
return btoa(utf8Bytes); |
297 |
}; |
298 |
|
299 |
// Create a local submission and update our local state |
300 |
// upon success |
301 |
function makeLocalSubmission(identifier, metadata) { |
302 |
|
303 |
// Prepare extended_attributes in array format for POST |
304 |
var extended_attributes = []; |
305 |
for (const [key, value] of Object.entries(metadata)) { |
306 |
extended_attributes.push({"type":key, "value":value}); |
307 |
} |
308 |
|
309 |
let date = new Date(); |
310 |
var payload = { |
311 |
batch_id: batchId, |
312 |
ill_backend_id: batch.data.backend, |
313 |
patron_id: batch.data.patron.borrowernumber, |
314 |
library_id: batch.data.branchcode, |
315 |
//FIXME: requested_date and timestamp should not be required by the API, they are handled by the backend create_api method |
316 |
requested_date: $date_to_rfc3339(date.toString()), |
317 |
timestamp: date, |
318 |
extended_attributes: extended_attributes |
319 |
}; |
320 |
window.doCreateSubmission(payload) |
321 |
.then(function (response) { |
322 |
return response.json(); |
323 |
}) |
324 |
.then(function (data) { |
325 |
tableContent.data = tableContent.data.map(function (row) { |
326 |
if (row.value === identifier) { |
327 |
row.requestId = data.ill_request_id; |
328 |
row.requestStatus = data.status; |
329 |
} |
330 |
return row; |
331 |
}); |
332 |
}) |
333 |
.catch(function () { |
334 |
window.handleApiError(ill_batch_api_request_fail); |
335 |
}); |
336 |
}; |
337 |
|
338 |
function updateProcessTotals() { |
339 |
var init = { |
340 |
total: 0, |
341 |
count: 0, |
342 |
failed: 0 |
343 |
}; |
344 |
progressTotals.data = init; |
345 |
var toUpdate = progressTotals.data; |
346 |
tableContent.data.forEach(function (row) { |
347 |
toUpdate.total++; |
348 |
if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) { |
349 |
toUpdate.count++; |
350 |
} |
351 |
if (Object.keys(row.failed).length > 0) { |
352 |
toUpdate.failed++; |
353 |
} |
354 |
}); |
355 |
createProgressTotal.innerHTML = toUpdate.total; |
356 |
createProgressCount.innerHTML = toUpdate.count; |
357 |
createProgressFailed.innerHTML = toUpdate.failed; |
358 |
var percentDone = Math.ceil((toUpdate.count / toUpdate.total) * 100); |
359 |
createProgressBar.setAttribute('aria-valuenow', percentDone); |
360 |
createProgressBar.innerHTML = percentDone + '%'; |
361 |
createProgressBar.style.width = percentDone + '%'; |
362 |
progressTotals.data = toUpdate; |
363 |
}; |
364 |
|
365 |
function displayPatronName() { |
366 |
var span = document.getElementById('patron_link'); |
367 |
if (batch.data.patron) { |
368 |
var link = createPatronLink(); |
369 |
span.appendChild(link); |
370 |
} else { |
371 |
if (span.children.length > 0) { |
372 |
span.removeChild(span.firstChild); |
373 |
} |
374 |
} |
375 |
}; |
376 |
|
377 |
function updateStatusesSelect() { |
378 |
while (statusesSelect.options.length > 0) { |
379 |
statusesSelect.remove(0); |
380 |
} |
381 |
statuses.data.forEach(function (status) { |
382 |
var option = document.createElement('option') |
383 |
option.value = status.code; |
384 |
option.text = status.name; |
385 |
if (batch.data.id && batch.data.statuscode === status.code) { |
386 |
option.selected = true; |
387 |
} |
388 |
statusesSelect.add(option); |
389 |
}); |
390 |
if (isUpdate) { |
391 |
statusesSelect.parentElement.style.display = 'block'; |
392 |
} |
393 |
}; |
394 |
|
395 |
function removeEventListeners() { |
396 |
textarea.removeEventListener('paste', processButtonState); |
397 |
textarea.removeEventListener('keyup', processButtonState); |
398 |
processButton.removeEventListener('click', processIdentifiers); |
399 |
nameInput.removeEventListener('keyup', createButtonState); |
400 |
cardnumberInput.removeEventListener('keyup', createButtonState); |
401 |
branchcodeSelect.removeEventListener('change', createButtonState); |
402 |
createButton.removeEventListener('click', createBatch); |
403 |
identifierTable.removeEventListener('click', toggleMetadata); |
404 |
identifierTable.removeEventListener('click', removeRow); |
405 |
createRequestsButton.remove('click', requestRequestable); |
406 |
}; |
407 |
|
408 |
function finishButtonEventListener() { |
409 |
finishButton.addEventListener('click', doFinish); |
410 |
}; |
411 |
|
412 |
function identifierTextareaEventListener() { |
413 |
textarea.addEventListener('paste', textareaUpdate); |
414 |
textarea.addEventListener('keyup', textareaUpdate); |
415 |
}; |
416 |
|
417 |
function processButtonEventListener() { |
418 |
processButton.addEventListener('click', processIdentifiers); |
419 |
}; |
420 |
|
421 |
function createRequestsButtonEventListener() { |
422 |
createRequestsButton.addEventListener('click', requestRequestable); |
423 |
}; |
424 |
|
425 |
function createButtonEventListener() { |
426 |
createButton.addEventListener('click', createBatch); |
427 |
}; |
428 |
|
429 |
function batchInputsEventListeners() { |
430 |
nameInput.addEventListener('keyup', createButtonState); |
431 |
cardnumberInput.addEventListener('keyup', createButtonState); |
432 |
branchcodeSelect.addEventListener('change', createButtonState); |
433 |
}; |
434 |
|
435 |
function moreLessEventListener() { |
436 |
identifierTable.addEventListener('click', toggleMetadata); |
437 |
}; |
438 |
|
439 |
function removeRowEventListener() { |
440 |
identifierTable.addEventListener('click', removeRow); |
441 |
}; |
442 |
|
443 |
function textareaUpdate() { |
444 |
processButtonState(); |
445 |
updateRowCount(); |
446 |
}; |
447 |
|
448 |
function processButtonState() { |
449 |
if (textarea.value.length > 0) { |
450 |
processButton.removeAttribute('disabled'); |
451 |
processButton.removeAttribute('aria-disabled'); |
452 |
} else { |
453 |
processButton.setAttribute('disabled', true); |
454 |
processButton.setAttribute('aria-disabled', true); |
455 |
} |
456 |
}; |
457 |
|
458 |
function disableCardnumberInput() { |
459 |
if (batch.data.patron) { |
460 |
cardnumberInput.setAttribute('disabled', true); |
461 |
cardnumberInput.setAttribute('aria-disabled', true); |
462 |
} else { |
463 |
cardnumberInput.removeAttribute('disabled'); |
464 |
cardnumberInput.removeAttribute('aria-disabled'); |
465 |
} |
466 |
}; |
467 |
|
468 |
function createButtonState() { |
469 |
if ( |
470 |
nameInput.value.length > 0 && |
471 |
cardnumberInput.value.length > 0 && |
472 |
branchcodeSelect.selectedOptions.length === 1 |
473 |
) { |
474 |
createButton.removeAttribute('disabled'); |
475 |
createButton.setAttribute('display', 'inline-block'); |
476 |
} else { |
477 |
createButton.setAttribute('disabled', 1); |
478 |
createButton.setAttribute('display', 'none'); |
479 |
} |
480 |
}; |
481 |
|
482 |
function doFinish() { |
483 |
updateBatch() |
484 |
.then(function () { |
485 |
$('#ill-batch-modal').modal({ show: false }); |
486 |
location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.id; |
487 |
}); |
488 |
}; |
489 |
|
490 |
// Get all batch statuses |
491 |
function fetchStatuses() { |
492 |
window.doApiRequest('/api/v1/illbatchstatuses') |
493 |
.then(function (response) { |
494 |
return response.json(); |
495 |
}) |
496 |
.then(function (jsoned) { |
497 |
statuses.data = jsoned; |
498 |
}) |
499 |
.catch(function (e) { |
500 |
window.handleApiError(ill_batch_statuses_api_fail); |
501 |
}); |
502 |
}; |
503 |
|
504 |
// Get the batch |
505 |
function fetchBatch() { |
506 |
window.doBatchApiRequest("/" + batchId) |
507 |
.then(function (response) { |
508 |
return response.json(); |
509 |
}) |
510 |
.then(function (jsoned) { |
511 |
batch.data = { |
512 |
id: jsoned.id, |
513 |
name: jsoned.name, |
514 |
backend: jsoned.backend, |
515 |
cardnumber: jsoned.cardnumber, |
516 |
branchcode: jsoned.branchcode, |
517 |
statuscode: jsoned.statuscode |
518 |
} |
519 |
return jsoned; |
520 |
}) |
521 |
.then(function (data) { |
522 |
batch.data = data; |
523 |
}) |
524 |
.catch(function () { |
525 |
window.handleApiError(ill_batch_api_fail); |
526 |
}); |
527 |
}; |
528 |
|
529 |
function createBatch() { |
530 |
var selectedBranchcode = branchcodeSelect.selectedOptions[0].value; |
531 |
var selectedStatuscode = statusesSelect.selectedOptions[0].value; |
532 |
return doBatchApiRequest('', { |
533 |
method: 'POST', |
534 |
headers: { |
535 |
'Content-type': 'application/json' |
536 |
}, |
537 |
body: JSON.stringify({ |
538 |
name: nameInput.value, |
539 |
backend: backend, |
540 |
cardnumber: cardnumberInput.value, |
541 |
branchcode: selectedBranchcode, |
542 |
statuscode: selectedStatuscode |
543 |
}) |
544 |
}) |
545 |
.then(function (response) { |
546 |
if ( response.ok ) { |
547 |
return response.json(); |
548 |
} |
549 |
return Promise.reject(response); |
550 |
}) |
551 |
.then(function (body) { |
552 |
batchId = body.id; |
553 |
batch.data = { |
554 |
id: body.id, |
555 |
name: body.name, |
556 |
backend: body.backend, |
557 |
cardnumber: body.patron.cardnumber, |
558 |
branchcode: body.branchcode, |
559 |
statuscode: body.statuscode, |
560 |
patron: body.patron, |
561 |
status: body.status |
562 |
}; |
563 |
initPostCreate(); |
564 |
}) |
565 |
.catch(function (response) { |
566 |
response.json().then((json) => { |
567 |
if( json.error ) { |
568 |
handleApiError(json.error); |
569 |
} else { |
570 |
handleApiError(ill_batch_create_api_fail); |
571 |
} |
572 |
}) |
573 |
}); |
574 |
}; |
575 |
|
576 |
function updateBatch() { |
577 |
var selectedBranchcode = branchcodeSelect.selectedOptions[0].value; |
578 |
var selectedStatuscode = statusesSelect.selectedOptions[0].value; |
579 |
return doBatchApiRequest('/' + batch.data.id, { |
580 |
method: 'PUT', |
581 |
headers: { |
582 |
'Content-type': 'application/json' |
583 |
}, |
584 |
body: JSON.stringify({ |
585 |
name: nameInput.value, |
586 |
backend: batch.data.backend, |
587 |
cardnumber: batch.data.patron.cardnumber, |
588 |
branchcode: selectedBranchcode, |
589 |
statuscode: selectedStatuscode |
590 |
}) |
591 |
}) |
592 |
.catch(function () { |
593 |
handleApiError(ill_batch_update_api_fail); |
594 |
}); |
595 |
}; |
596 |
|
597 |
function displaySupportedIdentifiers() { |
598 |
var names = Object.keys(supportedIdentifiers).map(function (identifier) { |
599 |
return window['ill_batch_' + identifier]; |
600 |
}); |
601 |
var displayEl = document.getElementById('supported_identifiers'); |
602 |
displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none; |
603 |
} |
604 |
|
605 |
function updateRowCount() { |
606 |
var textEl = document.getElementById('row_count_value'); |
607 |
var val = textarea.value.trim(); |
608 |
var cnt = 0; |
609 |
if (val.length > 0) { |
610 |
cnt = val.split(/\n/).length; |
611 |
} |
612 |
textEl.textContent = cnt; |
613 |
} |
614 |
|
615 |
function showProgress() { |
616 |
var el = document.getElementById('create-progress'); |
617 |
el.style.display = 'block'; |
618 |
} |
619 |
|
620 |
function showCreateRequestsButton() { |
621 |
var data = progressTotals.data; |
622 |
var el = document.getElementById('create-requests'); |
623 |
el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none'; |
624 |
} |
625 |
|
626 |
async function processIdentifiers() { |
627 |
var content = textarea.value; |
628 |
hideErrors(); |
629 |
if (content.length === 0) return; |
630 |
|
631 |
disableProcessButton(); |
632 |
var label = document.getElementById('progress-label').firstChild; |
633 |
label.innerHTML = ill_batch_retrieving_metadata; |
634 |
showProgress(); |
635 |
|
636 |
// Errors encountered when processing |
637 |
var processErrors = {}; |
638 |
|
639 |
// Prepare the content, including trimming each row |
640 |
var contentArr = content.split(/\n/); |
641 |
var trimmed = contentArr.map(function (row) { |
642 |
return row.trim(); |
643 |
}); |
644 |
|
645 |
var parsed = []; |
646 |
|
647 |
trimmed.forEach(function (identifier) { |
648 |
var match = identifyIdentifier(identifier); |
649 |
// If this identifier is not identifiable or |
650 |
// looks like more than one type, we can't be sure |
651 |
// what it is |
652 |
if (match.length != 1) { |
653 |
parsed.push({ |
654 |
type: 'unknown', |
655 |
value: identifier |
656 |
}); |
657 |
} else { |
658 |
parsed.push(match[0]); |
659 |
} |
660 |
}); |
661 |
|
662 |
var unknownIdentifiers = parsed |
663 |
.filter(function (parse) { |
664 |
if (parse.type == 'unknown') { |
665 |
return parse; |
666 |
} |
667 |
}) |
668 |
.map(function (filtered) { |
669 |
return filtered.value; |
670 |
}); |
671 |
|
672 |
if (unknownIdentifiers.length > 0) { |
673 |
processErrors.badidentifiers = { |
674 |
element: 'badids', |
675 |
values: unknownIdentifiers.join(', ') |
676 |
}; |
677 |
}; |
678 |
|
679 |
// Deduping |
680 |
var deduped = []; |
681 |
var dupes = {}; |
682 |
parsed.forEach(function (row) { |
683 |
var value = row.value; |
684 |
var alreadyInDeduped = deduped.filter(function (d) { |
685 |
return d.value === value; |
686 |
}); |
687 |
if (alreadyInDeduped.length > 0 && !dupes[value]) { |
688 |
dupes[value] = 1; |
689 |
} else if (alreadyInDeduped.length === 0) { |
690 |
row.metadata = {}; |
691 |
row.failed = {}; |
692 |
row.requestId = null; |
693 |
deduped.push(row); |
694 |
} |
695 |
}); |
696 |
// Update duplicate error if dupes were found |
697 |
if (Object.keys(dupes).length > 0) { |
698 |
processErrors.duplicates = { |
699 |
element: 'dupelist', |
700 |
values: Object.keys(dupes).join(', ') |
701 |
}; |
702 |
} |
703 |
|
704 |
// Display any errors |
705 |
displayErrors(processErrors); |
706 |
|
707 |
// Now build and display the table |
708 |
if (!table) { |
709 |
buildTable(); |
710 |
} |
711 |
|
712 |
// We may be appending new values to an existing table, |
713 |
// in which case, ensure we don't create duplicates |
714 |
var tabIdentifiers = tableContent.data.map(function (tabId) { |
715 |
return tabId.value; |
716 |
}); |
717 |
var notInTable = deduped.filter(function (ded) { |
718 |
if (!tabIdentifiers.includes(ded.value)) { |
719 |
return ded; |
720 |
} |
721 |
}); |
722 |
if (notInTable.length > 0) { |
723 |
tableContent.data = tableContent.data.concat(notInTable); |
724 |
} |
725 |
|
726 |
// Populate metadata for those records that need it |
727 |
var newData = tableContent.data; |
728 |
for (var i = 0; i < tableContent.data.length; i++) { |
729 |
var row = tableContent.data[i]; |
730 |
// Skip rows that don't need populating |
731 |
if ( |
732 |
Object.keys(tableContent.data[i].metadata).length > 0 || |
733 |
Object.keys(tableContent.data[i].failed).length > 0 |
734 |
) continue; |
735 |
var identifier = { type: row.type, value: row.value }; |
736 |
try { |
737 |
var populated = await populateMetadata(identifier); |
738 |
row.metadata = populated.results.result || {}; |
739 |
} catch (e) { |
740 |
row.failed = ill_populate_failed; |
741 |
} |
742 |
newData[i] = row; |
743 |
tableContent.data = newData; |
744 |
} |
745 |
} |
746 |
|
747 |
function disableProcessButton() { |
748 |
processButton.setAttribute('disabled', true); |
749 |
processButton.setAttribute('aria-disabled', true); |
750 |
} |
751 |
|
752 |
function disableCreateButton() { |
753 |
createButton.setAttribute('disabled', true); |
754 |
createButton.setAttribute('aria-disabled', true); |
755 |
} |
756 |
|
757 |
async function populateMetadata(identifier) { |
758 |
// All services that support this identifier type |
759 |
var services = supportedIdentifiers[identifier.type]; |
760 |
// Check each service and use the first results we get, if any |
761 |
for (var i = 0; i < services.length; i++) { |
762 |
var service = services[i]; |
763 |
var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value; |
764 |
var metadata = await getMetadata(endpoint); |
765 |
if (metadata.errors.length === 0) { |
766 |
var parsed = await parseMetadata(metadata, service); |
767 |
if (parsed.errors.length > 0) { |
768 |
throw Error(metadata.errors.map(function (error) { |
769 |
return error.message; |
770 |
}).join(', ')); |
771 |
} |
772 |
return parsed; |
773 |
} |
774 |
} |
775 |
}; |
776 |
|
777 |
async function getMetadata(endpoint) { |
778 |
var response = await debounce(doApiRequest)(endpoint); |
779 |
return response.json(); |
780 |
}; |
781 |
|
782 |
async function parseMetadata(metadata, service) { |
783 |
var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint; |
784 |
var response = await doApiRequest(endpoint, { |
785 |
method: 'POST', |
786 |
headers: { |
787 |
'Content-type': 'application/json' |
788 |
}, |
789 |
body: JSON.stringify(metadata) |
790 |
}); |
791 |
return response.json(); |
792 |
} |
793 |
|
794 |
// A render function for identifier type |
795 |
function createIdentifierType(data) { |
796 |
return window['ill_batch_' + data]; |
797 |
}; |
798 |
|
799 |
// Get an item's title |
800 |
function getTitle(meta) { |
801 |
if (meta.article_title && meta.article_title.length > 0) { |
802 |
return 'article_title'; |
803 |
return { |
804 |
prop: 'article_title', |
805 |
value: meta.article_title |
806 |
}; |
807 |
} else if (meta.title && meta.title.length > 0) { |
808 |
return 'title'; |
809 |
return { |
810 |
prop: 'title', |
811 |
value: meta.title |
812 |
}; |
813 |
} |
814 |
}; |
815 |
|
816 |
// Create a metadata row |
817 |
function createMetadataRow(data, meta, prop) { |
818 |
if (!meta[prop]) return; |
819 |
|
820 |
var div = document.createElement('div'); |
821 |
div.classList.add('metadata-row'); |
822 |
var label = document.createElement('span'); |
823 |
label.classList.add('metadata-label'); |
824 |
label.innerText = ill_batch_metadata[prop] + ': '; |
825 |
|
826 |
// Add a link to the availability URL if appropriate |
827 |
var value; |
828 |
if (!data.url) { |
829 |
value = document.createElement('span'); |
830 |
} else { |
831 |
value = document.createElement('a'); |
832 |
value.setAttribute('href', data.url); |
833 |
value.setAttribute('target', '_blank'); |
834 |
value.setAttribute('title', ill_batch_available_via + ' ' + data.availabilitySupplier); |
835 |
} |
836 |
value.classList.add('metadata-value'); |
837 |
value.innerText = meta[prop]; |
838 |
div.appendChild(label); |
839 |
div.appendChild(value); |
840 |
|
841 |
return div; |
842 |
} |
843 |
|
844 |
// A render function for displaying metadata |
845 |
function createMetadata(x, y, data) { |
846 |
// If the fetch failed |
847 |
if (data.failed.length > 0) { |
848 |
return data.failed; |
849 |
} |
850 |
|
851 |
// If we've not yet got any metadata back |
852 |
if (Object.keys(data.metadata).length === 0) { |
853 |
return ill_populate_waiting; |
854 |
} |
855 |
|
856 |
var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume']; |
857 |
var meta = data.metadata; |
858 |
|
859 |
var container = document.createElement('div'); |
860 |
container.classList.add('metadata-container'); |
861 |
|
862 |
// Create the title row |
863 |
var title = getTitle(meta); |
864 |
if (title) { |
865 |
// Remove the title element from the props |
866 |
// we're about to iterate |
867 |
core = core.filter(function (i) { |
868 |
return i !== title; |
869 |
}); |
870 |
var titleRow = createMetadataRow(data, meta, title); |
871 |
container.appendChild(titleRow); |
872 |
} |
873 |
|
874 |
var remainder = document.createElement('div'); |
875 |
remainder.classList.add('metadata-remainder'); |
876 |
remainder.style.display = 'none'; |
877 |
// Create the remaining rows |
878 |
core.sort().forEach(function (prop) { |
879 |
var div = createMetadataRow(data, meta, prop); |
880 |
if (div) { |
881 |
remainder.appendChild(div); |
882 |
} |
883 |
}); |
884 |
container.appendChild(remainder); |
885 |
|
886 |
// Add a more/less toggle |
887 |
var firstField = container.firstChild; |
888 |
var moreLess = document.createElement('div'); |
889 |
moreLess.classList.add('more-less'); |
890 |
var moreLessLink = document.createElement('a'); |
891 |
moreLessLink.setAttribute('href', '#'); |
892 |
moreLessLink.classList.add('more-less-link'); |
893 |
moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']'; |
894 |
moreLess.appendChild(moreLessLink); |
895 |
firstField.appendChild(moreLess); |
896 |
|
897 |
return container.outerHTML; |
898 |
}; |
899 |
|
900 |
function removeRow(ev) { |
901 |
if (ev.target.className.includes('remove-row')) { |
902 |
if (!confirm(ill_batch_item_remove)) return; |
903 |
// Find the parent row |
904 |
var ancestor = ev.target.closest('tr'); |
905 |
var identifier = ancestor.querySelector('.identifier').innerText; |
906 |
tableContent.data = tableContent.data.filter(function (row) { |
907 |
return row.value !== identifier; |
908 |
}); |
909 |
} |
910 |
} |
911 |
|
912 |
function toggleMetadata(ev) { |
913 |
if (ev.target.className === 'more-less-link') { |
914 |
// Find the element we need to show |
915 |
var ancestor = ev.target.closest('.metadata-container'); |
916 |
var meta = ancestor.querySelector('.metadata-remainder'); |
917 |
|
918 |
// Display or hide based on its current state |
919 |
var display = window.getComputedStyle(meta).display; |
920 |
|
921 |
meta.style.display = display === 'block' ? 'none' : 'block'; |
922 |
|
923 |
// Update the More / Less text |
924 |
ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]'; |
925 |
} |
926 |
} |
927 |
|
928 |
// A render function for the link to a request ID |
929 |
function createRequestId(x, y, data) { |
930 |
return data.requestId || '-'; |
931 |
} |
932 |
|
933 |
function createRequestStatus(x, y, data) { |
934 |
return data.requestStatus || '-'; |
935 |
} |
936 |
|
937 |
function buildTable(identifiers) { |
938 |
table = KohaTable('identifier-table', { |
939 |
processing: true, |
940 |
deferRender: true, |
941 |
ordering: false, |
942 |
paging: false, |
943 |
searching: false, |
944 |
autoWidth: false, |
945 |
columns: [ |
946 |
{ |
947 |
data: 'type', |
948 |
width: '13%', |
949 |
render: createIdentifierType |
950 |
}, |
951 |
{ |
952 |
data: 'value', |
953 |
width: '25%', |
954 |
className: 'identifier' |
955 |
}, |
956 |
{ |
957 |
data: 'metadata', |
958 |
render: createMetadata |
959 |
}, |
960 |
{ |
961 |
data: 'requestId', |
962 |
width: '6.5%', |
963 |
render: createRequestId |
964 |
}, |
965 |
{ |
966 |
data: 'requestStatus', |
967 |
width: '6.5%', |
968 |
render: createRequestStatus |
969 |
}, |
970 |
{ |
971 |
width: '18%', |
972 |
render: createActions, |
973 |
className: 'action-column' |
974 |
} |
975 |
], |
976 |
createdRow: function (row, data) { |
977 |
if (data.failed.length > 0) { |
978 |
row.classList.add('fetch-failed'); |
979 |
} |
980 |
} |
981 |
}); |
982 |
} |
983 |
|
984 |
function createActions(x, y, data) { |
985 |
return '<button type="button" aria-label='+ ill_button_remove + (data.requestId ? ' disabled aria-disabled="true"' : '') + ' class="btn btn-xs btn-danger remove-row">' + ill_button_remove + '</button>'; |
986 |
} |
987 |
|
988 |
// Redraw the table |
989 |
function updateTable() { |
990 |
if (!table) return; |
991 |
tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none'; |
992 |
tableEl.style.width = '100%'; |
993 |
table.api() |
994 |
.clear() |
995 |
.rows.add(tableContent.data) |
996 |
.draw(); |
997 |
}; |
998 |
|
999 |
function identifyIdentifier(identifier) { |
1000 |
var matches = []; |
1001 |
|
1002 |
// Iterate our available services to see if any can identify this identifier |
1003 |
Object.keys(supportedIdentifiers).forEach(function (identifierType) { |
1004 |
// Since all the services supporting this identifier type should use the same |
1005 |
// regex to identify it, we can just use the first |
1006 |
var service = supportedIdentifiers[identifierType][0]; |
1007 |
var regex = new RegExp(service.identifiers_supported[identifierType].regex); |
1008 |
var match = identifier.match(regex); |
1009 |
if (match && match.groups && match.groups.identifier) { |
1010 |
matches.push({ |
1011 |
type: identifierType, |
1012 |
value: match.groups.identifier |
1013 |
}); |
1014 |
} |
1015 |
}); |
1016 |
return matches; |
1017 |
} |
1018 |
|
1019 |
function displayErrors(errors) { |
1020 |
var keys = Object.keys(errors); |
1021 |
if (keys.length > 0) { |
1022 |
keys.forEach(function (key) { |
1023 |
var el = document.getElementById(errors[key].element); |
1024 |
el.textContent = errors[key].values; |
1025 |
el.style.display = 'inline'; |
1026 |
var container = document.getElementById(key); |
1027 |
container.style.display = 'block'; |
1028 |
}); |
1029 |
var el = document.getElementById('textarea-errors'); |
1030 |
el.style.display = 'flex'; |
1031 |
} |
1032 |
} |
1033 |
|
1034 |
function hideErrors() { |
1035 |
var dupelist = document.getElementById('dupelist'); |
1036 |
var badids = document.getElementById('badids'); |
1037 |
dupelist.textContent = ''; |
1038 |
dupelist.parentElement.style.display = 'none'; |
1039 |
badids.textContent = ''; |
1040 |
badids.parentElement.style.display = 'none'; |
1041 |
var tae = document.getElementById('textarea-errors'); |
1042 |
tae.style.display = 'none'; |
1043 |
} |
1044 |
|
1045 |
function manageBatchItemsDisplay() { |
1046 |
batchItemsDisplay.style.display = batch.data.id ? 'block' : 'none' |
1047 |
}; |
1048 |
|
1049 |
function updateBatchInputs() { |
1050 |
nameInput.value = batch.data.name || ''; |
1051 |
cardnumberInput.value = batch.data.cardnumber || ''; |
1052 |
branchcodeSelect.value = batch.data.branchcode || ''; |
1053 |
} |
1054 |
|
1055 |
function debounce(func) { |
1056 |
var timeout; |
1057 |
return function (...args) { |
1058 |
return new Promise(function (resolve) { |
1059 |
if (timeout) { |
1060 |
clearTimeout(timeout); |
1061 |
} |
1062 |
timeout = setTimeout(function () { |
1063 |
return resolve(func(...args)); |
1064 |
}, debounceDelay); |
1065 |
}); |
1066 |
} |
1067 |
} |
1068 |
|
1069 |
function patronAutocomplete() { |
1070 |
patron_autocomplete( |
1071 |
$('#batch-form #batchcardnumber'), |
1072 |
{ |
1073 |
'on-select-callback': function( event, ui ) { |
1074 |
$("#batch-form #batchcardnumber").val( ui.item.cardnumber ); |
1075 |
return false; |
1076 |
} |
1077 |
} |
1078 |
); |
1079 |
}; |
1080 |
|
1081 |
function createPatronLink() { |
1082 |
if (!batch.data.patron) return; |
1083 |
var patron = batch.data.patron; |
1084 |
var a = document.createElement('a'); |
1085 |
var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber; |
1086 |
var text = patron.surname + ' (' + patron.cardnumber + ')'; |
1087 |
a.setAttribute('title', ill_borrower_details); |
1088 |
a.setAttribute('href', href); |
1089 |
a.textContent = text; |
1090 |
return a; |
1091 |
}; |
1092 |
|
1093 |
})(); |