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 // Always clear the import selection when navigating away from
185 this.clearSelection();
188 importSelection(): VandelayImportSelection {
189 return this.vandelay.importSelection;
192 loadStartupData(): Promise<any> {
193 // Note displaying and manipulating a progress dialog inside
194 // the AfterViewInit cycle leads to errors because the child
195 // component is modifed after dirty checking.
198 this.vandelay.getMergeProfiles(),
199 this.vandelay.getAllQueues('bib'),
200 this.vandelay.getAllQueues('authority'),
201 this.vandelay.getMatchSets('bib'),
202 this.vandelay.getMatchSets('authority'),
203 this.vandelay.getBibBuckets(),
204 this.vandelay.getBibSources(),
205 this.vandelay.getItemImportDefs(),
206 this.vandelay.getBibTrashGroups().then(
207 groups => this.bibTrashGroups = groups),
208 this.org.settings(['vandelay.default_match_set']).then(
209 s => this.defaultMatchSet = s['vandelay.default_match_set']),
213 return Promise.all(promises);
217 this.store.getItem(TEMPLATE_SETTING_NAME).then(
219 this.formTemplates = templates || {};
221 Object.keys(this.formTemplates).forEach(name => {
222 if (this.formTemplates[name].default) {
223 this.selectedTemplate = name;
230 formatTemplateEntries(): ComboboxEntry[] {
233 Object.keys(this.formTemplates || {}).forEach(
234 name => entries.push({id: name, label: name}));
239 // Format typeahead data sets
240 formatEntries(etype: string): ComboboxEntry[] {
241 const rtype = this.recordType;
246 return (this.vandelay.bibSources || []).map(
247 s => { return {id: s.id(), label: s.source()}; });
250 list = this.vandelay.bibBuckets;
254 list = (this.vandelay.allQueues[rtype] || [])
255 .filter(q => q.complete() === 'f');
259 list = this.vandelay.matchSets[rtype];
262 case 'importItemDefs':
263 list = this.vandelay.importItemAttrDefs;
266 case 'mergeProfiles':
267 list = this.vandelay.mergeProfiles;
271 return (list || []).map(item => {
272 return {id: item.id(), label: item.name()};
276 selectEntry($event: ComboboxEntry, etype: string) {
277 const id = $event ? $event.id : null;
281 this.recordType = id;
284 this.selectedBibSource = id;
288 this.selectedBucket = id;
292 this.selectedMatchSet = id;
295 case 'importItemDefs':
296 this.selectedHoldingsProfile = id;
299 case 'mergeProfiles':
300 this.selectedMergeProfile = id;
303 case 'FallThruMergeProfile':
304 this.selectedFallThruMergeProfile = id;
309 fileSelected($event) {
310 this.selectedFile = $event.target.files[0];
313 // Required form data varies depending on context.
314 hasNeededData(): boolean {
315 if (this.vandelay.importSelection) {
316 return this.importActionSelected();
318 return this.selectedQueue
319 && Boolean(this.recordType) && Boolean(this.selectedFile)
323 importActionSelected(): boolean {
324 return this.importNonMatching
326 || this.mergeOnSingleMatch
327 || this.mergeOnBestMatch;
330 // 1. create queue if necessary
331 // 2. upload MARC file
332 // 3. Enqueue MARC records
335 this.sessionKey = null;
336 this.showProgress = true;
337 this.isUploading = true;
338 this.uploadComplete = false;
339 this.resetProgressBars();
344 this.activeQueueId = queueId;
345 return this.uploadFile();
347 err => Promise.reject('queue create failed')
349 ok => this.processSpool(),
350 err => Promise.reject('process spool failed')
352 ok => this.importRecords(),
353 err => Promise.reject('import records failed')
356 this.isUploading = false;
357 this.uploadComplete = true;
360 console.log('file upload failed: ', err);
361 this.isUploading = false;
362 this.resetProgressBars();
368 resetProgressBars() {
369 this.uploadProgress.update({value: 0, max: 1});
370 this.enqueueProgress.update({value: 0, max: 1});
371 this.importProgress.update({value: 0, max: 1});
374 // Extract selected queue ID or create a new queue when requested.
375 resolveQueue(): Promise<number> {
377 if (this.selectedQueue.freetext) {
378 // Free text queue selector means create a new entry.
379 // TODO: first check for name dupes
381 return this.vandelay.createQueue(
382 this.selectedQueue.label,
384 this.selectedHoldingsProfile,
385 this.selectedMatchSet,
390 const evt = this.evt.parse(err);
392 if (evt.textcode.match(/QUEUE_EXISTS/)) {
393 this.dupeQueueAlert.open();
395 alert(evt); // server error
399 return Promise.reject('Queue Create Failed');
403 return Promise.resolve(this.selectedQueue.id);
407 uploadFile(): Promise<any> {
409 if (this.vandelay.importSelection) {
410 // Nothing to upload when processing pre-queued records.
411 return Promise.resolve();
414 const formData: FormData = new FormData();
416 formData.append('ses', this.auth.token());
417 formData.append('marc_upload',
418 this.selectedFile, this.selectedFile.name);
420 if (this.selectedBibSource) {
421 formData.append('bib_source', ''+this.selectedBibSource);
424 const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
425 {reportProgress: true, responseType: 'text'});
427 return this.http.request(req).pipe(tap(
429 if (evt.type === HttpEventType.UploadProgress) {
430 this.uploadProgress.update(
431 {value: evt.loaded, max: evt.total});
433 } else if (evt instanceof HttpResponse) {
434 this.sessionKey = evt.body as string;
436 'Vandelay file uploaded OK with key '+this.sessionKey);
440 (err: HttpErrorResponse) => {
442 this.toast.danger(err.error);
447 processSpool(): Promise<any> {
449 if (this.vandelay.importSelection) {
450 // Nothing to enqueue when processing pre-queued records
451 return Promise.resolve();
453 var spoolType = this.recordType;
454 if (this.recordType == 'authority') spoolType = 'auth'
456 const method = `open-ils.vandelay.${spoolType}.process_spool`;
458 return new Promise((resolve, reject) => {
460 'open-ils.vandelay', method,
461 this.auth.token(), this.sessionKey, this.activeQueueId,
462 null, null, this.selectedBibSource,
463 (this.sessionName || null), true
466 const e = this.evt.parse(tracker);
467 if (e) { console.error(e); return reject(); }
469 // Spooling is in progress, track the results.
470 this.vandelay.pollSessionTracker(tracker.id())
473 this.enqueueProgress.update({
474 // enqueue API only tracks actions performed
476 value: trkr.actions_performed()
479 err => { console.log(err); reject(); },
481 this.enqueueProgress.update({max: 1, value: 1});
490 importRecords(): Promise<any> {
492 if (!this.importActionSelected()) {
493 return Promise.resolve();
496 const selection = this.vandelay.importSelection;
498 if (selection && !selection.importQueue) {
499 return this.importRecordQueue(selection.recordIds);
501 return this.importRecordQueue();
505 importRecordQueue(recIds?: number[]): Promise<any> {
506 const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
508 let method = `open-ils.vandelay.${rtype}_queue.import`;
509 const options: ImportOptions = this.compileImportOptions();
511 let target: number | number[] = this.activeQueueId;
512 if (recIds && recIds.length) {
513 method = `open-ils.vandelay.${rtype}_record.list.import`;
517 return new Promise((resolve, reject) => {
518 this.net.request('open-ils.vandelay',
519 method, this.auth.token(), target, options)
522 const e = this.evt.parse(tracker);
523 if (e) { console.error(e); return reject(); }
525 // Spooling is in progress, track the results.
526 this.vandelay.pollSessionTracker(tracker.id())
529 this.importProgress.update({
530 max: trkr.total_actions(),
531 value: trkr.actions_performed()
534 err => { console.log(err); reject(); },
536 this.importProgress.update({max: 1, value: 1});
545 compileImportOptions(): ImportOptions {
547 const options: ImportOptions = {
548 session_key: this.sessionKey,
549 import_no_match: this.importNonMatching,
550 auto_overlay_exact: this.mergeOnExact,
551 auto_overlay_best_match: this.mergeOnBestMatch,
552 auto_overlay_1match: this.mergeOnSingleMatch,
553 opp_acq_copy_overlay: this.autoOverlayAcqCopies,
554 merge_profile: this.selectedMergeProfile,
555 fall_through_merge_profile: this.selectedFallThruMergeProfile,
556 strip_field_groups: this.selectedTrashGroups,
557 match_quality_ratio: this.minQualityRatio,
561 if (this.vandelay.importSelection) {
562 options.overlay_map = this.vandelay.importSelection.overlayMap;
569 this.vandelay.importSelection = null;
570 this.startQueueId = null;
574 console.log('opening queue ' + this.activeQueueId);
580 TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
582 console.debug("Saving import profile", template);
584 this.formTemplates[this.selectedTemplate] = template;
585 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
588 markTemplateDefault() {
590 Object.keys(this.formTemplates).forEach(
591 name => delete this.formTemplates.default
594 this.formTemplates[this.selectedTemplate].default = true;
596 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
599 templateSelectorChange(entry: ComboboxEntry) {
602 this.selectedTemplate = '';
606 this.selectedTemplate = entry.label; // label == name
608 if (entry.freetext) {
609 // User is entering a new template name.
614 // User selected an existing template, apply it to the form.
616 const template = this.formTemplates[entry.id];
618 // Copy the template values into "this"
619 TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
621 // Some values must be manually passed to the combobox'es
623 this.recordTypeSelector.applyEntryId(this.recordType);
624 this.bibSourceSelector.applyEntryId(this.selectedBibSource);
625 this.matchSetSelector.applyEntryId(this.selectedMatchSet);
626 this.holdingsProfileSelector
627 .applyEntryId(this.selectedHoldingsProfile);
628 this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
629 this.fallThruMergeProfileSelector
630 .applyEntryId(this.selectedFallThruMergeProfile);
634 delete this.formTemplates[this.selectedTemplate];
635 this.formTemplateSelector.selected = null;
636 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);