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 {Subject} from 'rxjs/Subject';
17 import {ServerStoreService} from '@eg/core/server-store.service';
19 const TEMPLATE_SETTING_NAME = 'eg.cat.vandelay.import.templates';
21 const TEMPLATE_ATTRS = [
29 'autoOverlayAcqCopies',
30 'selectedHoldingsProfile',
31 'selectedMergeProfile',
32 'selectedFallThruMergeProfile',
33 'selectedTrashGroups',
37 interface ImportOptions {
39 overlay_map?: {[qrId: number]: /* breId */ number};
40 import_no_match?: boolean;
41 auto_overlay_exact?: boolean;
42 auto_overlay_best_match?: boolean;
43 auto_overlay_1match?: boolean;
44 opp_acq_copy_overlay?: boolean;
46 fall_through_merge_profile?: any;
47 strip_field_groups?: number[];
48 match_quality_ratio: number,
53 templateUrl: 'import.component.html'
55 export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
58 selectedQueue: ComboboxEntry; // freetext enabled
60 // used for applying a default queue ID value when we have
61 // a load-time queue before the queue combobox entries exist.
64 bibTrashGroups: IdlObject[];
65 selectedTrashGroups: number[];
67 activeQueueId: number;
68 selectedBucket: number;
69 selectedBibSource: number;
70 selectedMatchSet: number;
71 selectedHoldingsProfile: number;
72 selectedMergeProfile: number;
73 selectedFallThruMergeProfile: number;
76 defaultMatchSet: string;
78 importNonMatching: boolean;
79 mergeOnExact: boolean;
80 mergeOnSingleMatch: boolean;
81 mergeOnBestMatch: boolean;
82 minQualityRatio: number;
83 autoOverlayAcqCopies: boolean;
85 // True after the first upload, then remains true.
86 showProgress: boolean;
88 // Upload in progress.
91 // True only after successful upload
92 uploadComplete: boolean;
94 // Upload / processsing session key
95 // Generated by the server
98 // Optional enqueue/import tracker session name.
101 selectedTemplate: string;
102 formTemplates: {[name: string]: any};
103 newTemplateName: string;
105 @ViewChild('fileSelector') private fileSelector;
106 @ViewChild('uploadProgress')
107 private uploadProgress: ProgressInlineComponent;
108 @ViewChild('enqueueProgress')
109 private enqueueProgress: ProgressInlineComponent;
110 @ViewChild('importProgress')
111 private importProgress: ProgressInlineComponent;
113 // Need these refs so values can be applied via external stimuli
114 @ViewChild('formTemplateSelector')
115 private formTemplateSelector: ComboboxComponent;
116 @ViewChild('recordTypeSelector')
117 private recordTypeSelector: ComboboxComponent;
118 @ViewChild('bibSourceSelector')
119 private bibSourceSelector: ComboboxComponent;
120 @ViewChild('matchSetSelector')
121 private matchSetSelector: ComboboxComponent;
122 @ViewChild('holdingsProfileSelector')
123 private holdingsProfileSelector: ComboboxComponent;
124 @ViewChild('mergeProfileSelector')
125 private mergeProfileSelector: ComboboxComponent;
126 @ViewChild('fallThruMergeProfileSelector')
127 private fallThruMergeProfileSelector: ComboboxComponent;
130 private http: HttpClient,
131 private toast: ToastService,
132 private evt: EventService,
133 private net: NetService,
134 private auth: AuthService,
135 private org: OrgService,
136 private store: ServerStoreService,
137 private vandelay: VandelayService
139 this.applyDefaults();
143 this.minQualityRatio = 0;
144 this.selectedBibSource = 1; // default to system local
145 this.recordType = 'bib';
146 this.bibTrashGroups = [];
147 this.formTemplates = {};
149 if (this.vandelay.importSelection) {
151 if (!this.vandelay.importSelection.queue) {
152 // Incomplete import selection, clear it.
153 this.vandelay.importSelection = null;
157 const queue = this.vandelay.importSelection.queue;
158 this.recordType = queue.queue_type();
159 this.selectedMatchSet = queue.match_set();
161 // This will be propagated to selectedQueue as a combobox
162 // entry via the combobox
163 this.startQueueId = queue.id();
165 if (this.recordType === 'bib') {
166 this.selectedBucket = queue.match_bucket();
167 this.selectedHoldingsProfile = queue.item_attr_def();
175 this.loadStartupData();
179 // If we successfully completed the most recent
180 // upload/import assume the importSelection can be cleared.
181 if (this.uploadComplete) {
182 this.clearSelection();
186 importSelection(): VandelayImportSelection {
187 return this.vandelay.importSelection;
190 loadStartupData(): Promise<any> {
191 // Note displaying and manipulating a progress dialog inside
192 // the AfterViewInit cycle leads to errors because the child
193 // component is modifed after dirty checking.
196 this.vandelay.getMergeProfiles(),
197 this.vandelay.getAllQueues('bib'),
198 this.vandelay.getAllQueues('authority'),
199 this.vandelay.getMatchSets('bib'),
200 this.vandelay.getMatchSets('authority'),
201 this.vandelay.getBibBuckets(),
202 this.vandelay.getBibSources(),
203 this.vandelay.getItemImportDefs(),
204 this.vandelay.getBibTrashGroups().then(
205 groups => this.bibTrashGroups = groups),
206 this.org.settings(['vandelay.default_match_set']).then(
207 s => this.defaultMatchSet = s['vandelay.default_match_set']),
211 return Promise.all(promises);
215 this.store.getItem(TEMPLATE_SETTING_NAME).then(
217 this.formTemplates = templates || {};
219 Object.keys(this.formTemplates).forEach(name => {
220 if (this.formTemplates[name].default) {
221 this.selectedTemplate = name;
228 formatTemplateEntries(): ComboboxEntry[] {
231 Object.keys(this.formTemplates || {}).forEach(
232 name => entries.push({id: name, label: name}));
237 // Format typeahead data sets
238 formatEntries(etype: string): ComboboxEntry[] {
239 const rtype = this.recordType;
244 return (this.vandelay.bibSources || []).map(
245 s => { return {id: s.id(), label: s.source()}; });
248 list = this.vandelay.bibBuckets;
252 list = this.vandelay.allQueues[rtype];
256 list = this.vandelay.matchSets[rtype];
259 case 'importItemDefs':
260 list = this.vandelay.importItemAttrDefs;
263 case 'mergeProfiles':
264 list = this.vandelay.mergeProfiles;
268 return (list || []).map(item => {
269 return {id: item.id(), label: item.name()};
273 selectEntry($event: ComboboxEntry, etype: string) {
274 const id = $event ? $event.id : null;
278 this.recordType = id;
281 this.selectedBibSource = id;
285 this.selectedBucket = id;
289 this.selectedMatchSet = id;
292 case 'importItemDefs':
293 this.selectedHoldingsProfile = id;
296 case 'mergeProfiles':
297 this.selectedMergeProfile = id;
300 case 'FallThruMergeProfile':
301 this.selectedFallThruMergeProfile = id;
306 fileSelected($event) {
307 this.selectedFile = $event.target.files[0];
310 // Required form data varies depending on context.
311 hasNeededData(): boolean {
312 if (this.vandelay.importSelection) {
313 return this.importActionSelected();
315 return this.selectedQueue
316 && Boolean(this.recordType) && Boolean(this.selectedFile)
320 importActionSelected(): boolean {
321 return this.importNonMatching
323 || this.mergeOnSingleMatch
324 || this.mergeOnBestMatch;
327 // 1. create queue if necessary
328 // 2. upload MARC file
329 // 3. Enqueue MARC records
332 this.sessionKey = null;
333 this.showProgress = true;
334 this.isUploading = true;
335 this.uploadComplete = false;
336 this.resetProgressBars();
341 this.activeQueueId = queueId;
342 return this.uploadFile();
344 err => Promise.reject('queue create failed')
346 ok => this.processSpool(),
347 err => Promise.reject('process spool failed')
349 ok => this.importRecords(),
350 err => Promise.reject('import records failed')
353 this.isUploading = false;
354 this.uploadComplete = true;
357 console.log('file upload failed: ', err);
358 this.isUploading = false;
359 this.resetProgressBars();
365 resetProgressBars() {
366 this.uploadProgress.update({value: 0, max: 1});
367 this.enqueueProgress.update({value: 0, max: 1});
368 this.importProgress.update({value: 0, max: 1});
371 // Extract selected queue ID or create a new queue when requested.
372 resolveQueue(): Promise<number> {
374 if (this.selectedQueue.freetext) {
376 if (this.selectedQueue && 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 return Promise.resolve(this.selectedQueue.id);
392 var queue_id = this.startQueueId;
393 if (this.selectedQueue) queue_id = this.selectedQueue.id;
394 return Promise.resolve(queue_id);
399 uploadFile(): Promise<any> {
401 if (this.vandelay.importSelection) {
402 // Nothing to upload when processing pre-queued records.
403 return Promise.resolve();
406 const formData: FormData = new FormData();
408 formData.append('ses', this.auth.token());
409 formData.append('marc_upload',
410 this.selectedFile, this.selectedFile.name);
412 if (this.selectedBibSource) {
413 formData.append('bib_source', ''+this.selectedBibSource);
416 const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
417 {reportProgress: true, responseType: 'text'});
419 return this.http.request(req).pipe(tap(
421 if (evt.type === HttpEventType.UploadProgress) {
422 this.uploadProgress.update(
423 {value: evt.loaded, max: evt.total});
425 } else if (evt instanceof HttpResponse) {
426 this.sessionKey = evt.body as string;
428 'Vandelay file uploaded OK with key '+this.sessionKey);
432 (err: HttpErrorResponse) => {
434 this.toast.danger(err.error);
439 processSpool(): Promise<any> {
441 if (this.vandelay.importSelection) {
442 // Nothing to enqueue when processing pre-queued records
443 return Promise.resolve();
445 var spoolType = this.recordType;
446 if (this.recordType == 'authority') spoolType = 'auth'
448 const method = `open-ils.vandelay.${spoolType}.process_spool`;
450 return new Promise((resolve, reject) => {
452 'open-ils.vandelay', method,
453 this.auth.token(), this.sessionKey, this.activeQueueId,
454 null, null, this.selectedBibSource,
455 (this.sessionName || null), true
458 const e = this.evt.parse(tracker);
459 if (e) { console.error(e); return reject(); }
461 // Spooling is in progress, track the results.
462 this.vandelay.pollSessionTracker(tracker.id())
465 this.enqueueProgress.update({
466 // enqueue API only tracks actions performed
468 value: trkr.actions_performed()
471 err => { console.log(err); reject(); },
473 this.enqueueProgress.update({max: 1, value: 1});
482 importRecords(): Promise<any> {
484 if (!this.importActionSelected()) {
485 return Promise.resolve();
488 const selection = this.vandelay.importSelection;
490 if (selection && !selection.importQueue) {
491 return this.importRecordQueue(selection.recordIds);
493 return this.importRecordQueue();
497 importRecordQueue(recIds?: number[]): Promise<any> {
498 const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
500 let method = `open-ils.vandelay.${rtype}_queue.import`;
501 const options: ImportOptions = this.compileImportOptions();
503 let target: number | number[] = this.activeQueueId;
504 if (recIds && recIds.length) {
505 method = `open-ils.vandelay.${rtype}_record.list.import`;
509 return new Promise((resolve, reject) => {
510 this.net.request('open-ils.vandelay',
511 method, this.auth.token(), target, options)
514 const e = this.evt.parse(tracker);
515 if (e) { console.error(e); return reject(); }
517 // Spooling is in progress, track the results.
518 this.vandelay.pollSessionTracker(tracker.id())
521 this.importProgress.update({
522 max: trkr.total_actions(),
523 value: trkr.actions_performed()
526 err => { console.log(err); reject(); },
528 this.importProgress.update({max: 1, value: 1});
537 compileImportOptions(): ImportOptions {
539 const options: ImportOptions = {
540 session_key: this.sessionKey,
541 import_no_match: this.importNonMatching,
542 auto_overlay_exact: this.mergeOnExact,
543 auto_overlay_best_match: this.mergeOnBestMatch,
544 auto_overlay_1match: this.mergeOnSingleMatch,
545 opp_acq_copy_overlay: this.autoOverlayAcqCopies,
546 merge_profile: this.selectedMergeProfile,
547 fall_through_merge_profile: this.selectedFallThruMergeProfile,
548 strip_field_groups: this.selectedTrashGroups,
549 match_quality_ratio: this.minQualityRatio,
553 if (this.vandelay.importSelection) {
554 options.overlay_map = this.vandelay.importSelection.overlayMap;
561 this.vandelay.importSelection = null;
562 this.startQueueId = null;
566 console.log('opening queue ' + this.activeQueueId);
572 TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
574 console.debug("Saving import profile", template);
576 this.formTemplates[this.selectedTemplate] = template;
577 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
580 markTemplateDefault() {
582 Object.keys(this.formTemplates).forEach(
583 name => delete this.formTemplates.default
586 this.formTemplates[this.selectedTemplate].default = true;
588 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
591 templateSelectorChange(entry: ComboboxEntry) {
594 this.selectedTemplate = '';
598 this.selectedTemplate = entry.label; // label == name
600 if (entry.freetext) {
601 // User is entering a new template name.
606 // User selected an existing template, apply it to the form.
608 const template = this.formTemplates[entry.id];
610 // Copy the template values into "this"
611 TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
613 // Some values must be manually passed to the combobox'es
615 this.recordTypeSelector.applyEntryId(this.recordType);
616 this.bibSourceSelector.applyEntryId(this.selectedBibSource);
617 this.matchSetSelector.applyEntryId(this.selectedMatchSet);
618 this.holdingsProfileSelector
619 .applyEntryId(this.selectedHoldingsProfile);
620 this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
621 this.fallThruMergeProfileSelector
622 .applyEntryId(this.selectedFallThruMergeProfile);
626 delete this.formTemplates[this.selectedTemplate];
627 this.formTemplateSelector.selected = null;
628 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);