1 import {Component, OnInit, AfterViewInit, Input, ViewChild, OnDestroy} from '@angular/core';
2 import {tap} from 'rxjs/operators/tap';
3 import {IdlObject} from '@eg/core/idl.service';
4 import {NetService} from '@eg/core/net.service';
5 import {EventService} from '@eg/core/event.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {AuthService} from '@eg/core/auth.service';
8 import {ToastService} from '@eg/share/toast/toast.service';
9 import {ComboboxComponent,
10 ComboboxEntry} from '@eg/share/combobox/combobox.component';
11 import {VandelayService, VandelayImportSelection,
12 VANDELAY_UPLOAD_PATH} from './vandelay.service';
13 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
14 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
15 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
16 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
17 import {Subject} from 'rxjs/Subject';
18 import {ServerStoreService} from '@eg/core/server-store.service';
20 const TEMPLATE_SETTING_NAME = 'eg.cat.vandelay.import.templates';
22 const TEMPLATE_ATTRS = [
30 'autoOverlayAcqCopies',
31 'selectedHoldingsProfile',
32 'selectedMergeProfile',
33 'selectedFallThruMergeProfile',
34 'selectedTrashGroups',
38 interface ImportOptions {
40 overlay_map?: {[qrId: number]: /* breId */ number};
41 import_no_match?: boolean;
42 auto_overlay_exact?: boolean;
43 auto_overlay_best_match?: boolean;
44 auto_overlay_1match?: boolean;
45 opp_acq_copy_overlay?: boolean;
47 fall_through_merge_profile?: any;
48 strip_field_groups?: number[];
49 match_quality_ratio: number,
54 templateUrl: 'import.component.html'
56 export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
59 selectedQueue: ComboboxEntry; // freetext enabled
61 // used for applying a default queue ID value when we have
62 // a load-time queue before the queue combobox entries exist.
65 bibTrashGroups: IdlObject[];
66 selectedTrashGroups: number[];
68 activeQueueId: number;
69 selectedBucket: number;
70 selectedBibSource: number;
71 selectedMatchSet: number;
72 selectedHoldingsProfile: number;
73 selectedMergeProfile: number;
74 selectedFallThruMergeProfile: number;
77 defaultMatchSet: string;
79 importNonMatching: boolean;
80 mergeOnExact: boolean;
81 mergeOnSingleMatch: boolean;
82 mergeOnBestMatch: boolean;
83 minQualityRatio: number;
84 autoOverlayAcqCopies: boolean;
86 // True after the first upload, then remains true.
87 showProgress: boolean;
89 // Upload in progress.
92 // True only after successful upload
93 uploadComplete: boolean;
95 // Upload / processsing session key
96 // Generated by the server
99 // Optional enqueue/import tracker session name.
102 selectedTemplate: string;
103 formTemplates: {[name: string]: any};
104 newTemplateName: string;
106 @ViewChild('fileSelector') private fileSelector;
107 @ViewChild('uploadProgress')
108 private uploadProgress: ProgressInlineComponent;
109 @ViewChild('enqueueProgress')
110 private enqueueProgress: ProgressInlineComponent;
111 @ViewChild('importProgress')
112 private importProgress: ProgressInlineComponent;
114 // Need these refs so values can be applied via external stimuli
115 @ViewChild('formTemplateSelector')
116 private formTemplateSelector: ComboboxComponent;
117 @ViewChild('recordTypeSelector')
118 private recordTypeSelector: ComboboxComponent;
119 @ViewChild('bibSourceSelector')
120 private bibSourceSelector: ComboboxComponent;
121 @ViewChild('matchSetSelector')
122 private matchSetSelector: ComboboxComponent;
123 @ViewChild('holdingsProfileSelector')
124 private holdingsProfileSelector: ComboboxComponent;
125 @ViewChild('mergeProfileSelector')
126 private mergeProfileSelector: ComboboxComponent;
127 @ViewChild('fallThruMergeProfileSelector')
128 private fallThruMergeProfileSelector: ComboboxComponent;
130 @ViewChild('dupeQueueAlert')
131 private dupeQueueAlert: AlertDialogComponent;
134 private http: HttpClient,
135 private toast: ToastService,
136 private evt: EventService,
137 private net: NetService,
138 private auth: AuthService,
139 private org: OrgService,
140 private store: ServerStoreService,
141 private vandelay: VandelayService
143 this.applyDefaults();
147 this.minQualityRatio = 0;
148 this.selectedBibSource = 1; // default to system local
149 this.recordType = 'bib';
150 this.bibTrashGroups = [];
151 this.formTemplates = {};
153 if (this.vandelay.importSelection) {
155 if (!this.vandelay.importSelection.queue) {
156 // Incomplete import selection, clear it.
157 this.vandelay.importSelection = null;
161 const queue = this.vandelay.importSelection.queue;
162 this.recordType = queue.queue_type();
163 this.selectedMatchSet = queue.match_set();
165 // This will be propagated to selectedQueue as a combobox
166 // entry via the combobox
167 this.startQueueId = queue.id();
169 if (this.recordType === 'bib') {
170 this.selectedBucket = queue.match_bucket();
171 this.selectedHoldingsProfile = queue.item_attr_def();
179 this.loadStartupData();
183 // If we successfully completed the most recent
184 // upload/import assume the importSelection can be cleared.
185 if (this.uploadComplete) {
186 this.clearSelection();
190 importSelection(): VandelayImportSelection {
191 return this.vandelay.importSelection;
194 loadStartupData(): Promise<any> {
195 // Note displaying and manipulating a progress dialog inside
196 // the AfterViewInit cycle leads to errors because the child
197 // component is modifed after dirty checking.
200 this.vandelay.getMergeProfiles(),
201 this.vandelay.getAllQueues('bib'),
202 this.vandelay.getAllQueues('authority'),
203 this.vandelay.getMatchSets('bib'),
204 this.vandelay.getMatchSets('authority'),
205 this.vandelay.getBibBuckets(),
206 this.vandelay.getBibSources(),
207 this.vandelay.getItemImportDefs(),
208 this.vandelay.getBibTrashGroups().then(
209 groups => this.bibTrashGroups = groups),
210 this.org.settings(['vandelay.default_match_set']).then(
211 s => this.defaultMatchSet = s['vandelay.default_match_set']),
215 return Promise.all(promises);
219 this.store.getItem(TEMPLATE_SETTING_NAME).then(
221 this.formTemplates = templates || {};
223 Object.keys(this.formTemplates).forEach(name => {
224 if (this.formTemplates[name].default) {
225 this.selectedTemplate = name;
232 formatTemplateEntries(): ComboboxEntry[] {
235 Object.keys(this.formTemplates || {}).forEach(
236 name => entries.push({id: name, label: name}));
241 // Format typeahead data sets
242 formatEntries(etype: string): ComboboxEntry[] {
243 const rtype = this.recordType;
248 return (this.vandelay.bibSources || []).map(
249 s => { return {id: s.id(), label: s.source()}; });
252 list = this.vandelay.bibBuckets;
256 list = (this.vandelay.allQueues[rtype] || [])
257 .filter(q => q.complete() === 'f');
261 list = this.vandelay.matchSets[rtype];
264 case 'importItemDefs':
265 list = this.vandelay.importItemAttrDefs;
268 case 'mergeProfiles':
269 list = this.vandelay.mergeProfiles;
273 return (list || []).map(item => {
274 return {id: item.id(), label: item.name()};
278 selectEntry($event: ComboboxEntry, etype: string) {
279 const id = $event ? $event.id : null;
283 this.recordType = id;
286 this.selectedBibSource = id;
290 this.selectedBucket = id;
294 this.selectedMatchSet = id;
297 case 'importItemDefs':
298 this.selectedHoldingsProfile = id;
301 case 'mergeProfiles':
302 this.selectedMergeProfile = id;
305 case 'FallThruMergeProfile':
306 this.selectedFallThruMergeProfile = id;
311 fileSelected($event) {
312 this.selectedFile = $event.target.files[0];
315 // Required form data varies depending on context.
316 hasNeededData(): boolean {
317 if (this.vandelay.importSelection) {
318 return this.importActionSelected();
320 return this.selectedQueue
321 && Boolean(this.recordType) && Boolean(this.selectedFile)
325 importActionSelected(): boolean {
326 return this.importNonMatching
328 || this.mergeOnSingleMatch
329 || this.mergeOnBestMatch;
332 // 1. create queue if necessary
333 // 2. upload MARC file
334 // 3. Enqueue MARC records
337 this.sessionKey = null;
338 this.showProgress = true;
339 this.isUploading = true;
340 this.uploadComplete = false;
341 this.resetProgressBars();
346 this.activeQueueId = queueId;
347 return this.uploadFile();
349 err => Promise.reject('queue create failed')
351 ok => this.processSpool(),
352 err => Promise.reject('process spool failed')
354 ok => this.importRecords(),
355 err => Promise.reject('import records failed')
358 this.isUploading = false;
359 this.uploadComplete = true;
362 console.log('file upload failed: ', err);
363 this.isUploading = false;
364 this.resetProgressBars();
370 resetProgressBars() {
371 this.uploadProgress.update({value: 0, max: 1});
372 this.enqueueProgress.update({value: 0, max: 1});
373 this.importProgress.update({value: 0, max: 1});
376 // Extract selected queue ID or create a new queue when requested.
377 resolveQueue(): Promise<number> {
379 if (this.selectedQueue.freetext) {
380 // Free text queue selector means create a new entry.
381 // TODO: first check for name dupes
383 return this.vandelay.createQueue(
384 this.selectedQueue.label,
386 this.selectedHoldingsProfile,
387 this.selectedMatchSet,
392 const evt = this.evt.parse(err);
394 if (evt.textcode.match(/QUEUE_EXISTS/)) {
395 this.dupeQueueAlert.open();
397 alert(evt); // server error
401 return Promise.reject('Queue Create Failed');
405 return Promise.resolve(this.selectedQueue.id);
409 uploadFile(): Promise<any> {
411 if (this.vandelay.importSelection) {
412 // Nothing to upload when processing pre-queued records.
413 return Promise.resolve();
416 const formData: FormData = new FormData();
418 formData.append('ses', this.auth.token());
419 formData.append('marc_upload',
420 this.selectedFile, this.selectedFile.name);
422 if (this.selectedBibSource) {
423 formData.append('bib_source', ''+this.selectedBibSource);
426 const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
427 {reportProgress: true, responseType: 'text'});
429 return this.http.request(req).pipe(tap(
431 if (evt.type === HttpEventType.UploadProgress) {
432 this.uploadProgress.update(
433 {value: evt.loaded, max: evt.total});
435 } else if (evt instanceof HttpResponse) {
436 this.sessionKey = evt.body as string;
438 'Vandelay file uploaded OK with key '+this.sessionKey);
442 (err: HttpErrorResponse) => {
444 this.toast.danger(err.error);
449 processSpool(): Promise<any> {
451 if (this.vandelay.importSelection) {
452 // Nothing to enqueue when processing pre-queued records
453 return Promise.resolve();
455 var spoolType = this.recordType;
456 if (this.recordType == 'authority') spoolType = 'auth'
458 const method = `open-ils.vandelay.${spoolType}.process_spool`;
460 return new Promise((resolve, reject) => {
462 'open-ils.vandelay', method,
463 this.auth.token(), this.sessionKey, this.activeQueueId,
464 null, null, this.selectedBibSource,
465 (this.sessionName || null), true
468 const e = this.evt.parse(tracker);
469 if (e) { console.error(e); return reject(); }
471 // Spooling is in progress, track the results.
472 this.vandelay.pollSessionTracker(tracker.id())
475 this.enqueueProgress.update({
476 // enqueue API only tracks actions performed
478 value: trkr.actions_performed()
481 err => { console.log(err); reject(); },
483 this.enqueueProgress.update({max: 1, value: 1});
492 importRecords(): Promise<any> {
494 if (!this.importActionSelected()) {
495 return Promise.resolve();
498 const selection = this.vandelay.importSelection;
500 if (selection && !selection.importQueue) {
501 return this.importRecordQueue(selection.recordIds);
503 return this.importRecordQueue();
507 importRecordQueue(recIds?: number[]): Promise<any> {
508 const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
510 let method = `open-ils.vandelay.${rtype}_queue.import`;
511 const options: ImportOptions = this.compileImportOptions();
513 let target: number | number[] = this.activeQueueId;
514 if (recIds && recIds.length) {
515 method = `open-ils.vandelay.${rtype}_record.list.import`;
519 return new Promise((resolve, reject) => {
520 this.net.request('open-ils.vandelay',
521 method, this.auth.token(), target, options)
524 const e = this.evt.parse(tracker);
525 if (e) { console.error(e); return reject(); }
527 // Spooling is in progress, track the results.
528 this.vandelay.pollSessionTracker(tracker.id())
531 this.importProgress.update({
532 max: trkr.total_actions(),
533 value: trkr.actions_performed()
536 err => { console.log(err); reject(); },
538 this.importProgress.update({max: 1, value: 1});
547 compileImportOptions(): ImportOptions {
549 const options: ImportOptions = {
550 session_key: this.sessionKey,
551 import_no_match: this.importNonMatching,
552 auto_overlay_exact: this.mergeOnExact,
553 auto_overlay_best_match: this.mergeOnBestMatch,
554 auto_overlay_1match: this.mergeOnSingleMatch,
555 opp_acq_copy_overlay: this.autoOverlayAcqCopies,
556 merge_profile: this.selectedMergeProfile,
557 fall_through_merge_profile: this.selectedFallThruMergeProfile,
558 strip_field_groups: this.selectedTrashGroups,
559 match_quality_ratio: this.minQualityRatio,
563 if (this.vandelay.importSelection) {
564 options.overlay_map = this.vandelay.importSelection.overlayMap;
571 this.vandelay.importSelection = null;
572 this.startQueueId = null;
576 console.log('opening queue ' + this.activeQueueId);
582 TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
584 console.debug("Saving import profile", template);
586 this.formTemplates[this.selectedTemplate] = template;
587 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
590 markTemplateDefault() {
592 Object.keys(this.formTemplates).forEach(
593 name => delete this.formTemplates.default
596 this.formTemplates[this.selectedTemplate].default = true;
598 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
601 templateSelectorChange(entry: ComboboxEntry) {
604 this.selectedTemplate = '';
608 this.selectedTemplate = entry.label; // label == name
610 if (entry.freetext) {
611 // User is entering a new template name.
616 // User selected an existing template, apply it to the form.
618 const template = this.formTemplates[entry.id];
620 // Copy the template values into "this"
621 TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
623 // Some values must be manually passed to the combobox'es
625 this.recordTypeSelector.applyEntryId(this.recordType);
626 this.bibSourceSelector.applyEntryId(this.selectedBibSource);
627 this.matchSetSelector.applyEntryId(this.selectedMatchSet);
628 this.holdingsProfileSelector
629 .applyEntryId(this.selectedHoldingsProfile);
630 this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
631 this.fallThruMergeProfileSelector
632 .applyEntryId(this.selectedFallThruMergeProfile);
636 delete this.formTemplates[this.selectedTemplate];
637 this.formTemplateSelector.selected = null;
638 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);