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 && this.selectedQueue.freetext) {
375 // Free text queue selector means create a new entry.
376 // TODO: first check for name dupes
378 return this.vandelay.createQueue(
379 this.selectedQueue.label,
381 this.selectedHoldingsProfile,
382 this.selectedMatchSet,
387 var queue_id = this.startQueueId;
388 if (this.selectedQueue) queue_id = this.selectedQueue.id;
389 return Promise.resolve(queue_id);
393 uploadFile(): Promise<any> {
395 if (this.vandelay.importSelection) {
396 // Nothing to upload when processing pre-queued records.
397 return Promise.resolve();
400 const formData: FormData = new FormData();
402 formData.append('ses', this.auth.token());
403 formData.append('marc_upload',
404 this.selectedFile, this.selectedFile.name);
406 if (this.selectedBibSource) {
407 formData.append('bib_source', ''+this.selectedBibSource);
410 const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
411 {reportProgress: true, responseType: 'text'});
413 return this.http.request(req).pipe(tap(
415 if (evt.type === HttpEventType.UploadProgress) {
416 this.uploadProgress.update(
417 {value: evt.loaded, max: evt.total});
419 } else if (evt instanceof HttpResponse) {
420 this.sessionKey = evt.body as string;
422 'Vandelay file uploaded OK with key '+this.sessionKey);
426 (err: HttpErrorResponse) => {
428 this.toast.danger(err.error);
433 processSpool(): Promise<any> {
435 if (this.vandelay.importSelection) {
436 // Nothing to enqueue when processing pre-queued records
437 return Promise.resolve();
439 var spoolType = this.recordType;
440 if (this.recordType == 'authority') spoolType = 'auth'
442 const method = `open-ils.vandelay.${spoolType}.process_spool`;
444 return new Promise((resolve, reject) => {
446 'open-ils.vandelay', method,
447 this.auth.token(), this.sessionKey, this.activeQueueId,
448 null, null, this.selectedBibSource,
449 (this.sessionName || null), true
452 const e = this.evt.parse(tracker);
453 if (e) { console.error(e); return reject(); }
455 // Spooling is in progress, track the results.
456 this.vandelay.pollSessionTracker(tracker.id())
459 this.enqueueProgress.update({
460 // enqueue API only tracks actions performed
462 value: trkr.actions_performed()
465 err => { console.log(err); reject(); },
467 this.enqueueProgress.update({max: 1, value: 1});
476 importRecords(): Promise<any> {
478 if (!this.importActionSelected()) {
479 return Promise.resolve();
482 const selection = this.vandelay.importSelection;
484 if (selection && !selection.importQueue) {
485 return this.importRecordQueue(selection.recordIds);
487 return this.importRecordQueue();
491 importRecordQueue(recIds?: number[]): Promise<any> {
492 const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
494 let method = `open-ils.vandelay.${rtype}_queue.import`;
495 const options: ImportOptions = this.compileImportOptions();
497 let target: number | number[] = this.activeQueueId;
498 if (recIds && recIds.length) {
499 method = `open-ils.vandelay.${rtype}_record.list.import`;
503 return new Promise((resolve, reject) => {
504 this.net.request('open-ils.vandelay',
505 method, this.auth.token(), target, options)
508 const e = this.evt.parse(tracker);
509 if (e) { console.error(e); return reject(); }
511 // Spooling is in progress, track the results.
512 this.vandelay.pollSessionTracker(tracker.id())
515 this.importProgress.update({
516 max: trkr.total_actions(),
517 value: trkr.actions_performed()
520 err => { console.log(err); reject(); },
522 this.importProgress.update({max: 1, value: 1});
531 compileImportOptions(): ImportOptions {
533 const options: ImportOptions = {
534 session_key: this.sessionKey,
535 import_no_match: this.importNonMatching,
536 auto_overlay_exact: this.mergeOnExact,
537 auto_overlay_best_match: this.mergeOnBestMatch,
538 auto_overlay_1match: this.mergeOnSingleMatch,
539 opp_acq_copy_overlay: this.autoOverlayAcqCopies,
540 merge_profile: this.selectedMergeProfile,
541 fall_through_merge_profile: this.selectedFallThruMergeProfile,
542 strip_field_groups: this.selectedTrashGroups,
543 match_quality_ratio: this.minQualityRatio,
547 if (this.vandelay.importSelection) {
548 options.overlay_map = this.vandelay.importSelection.overlayMap;
555 this.vandelay.importSelection = null;
556 this.startQueueId = null;
560 console.log('opening queue ' + this.activeQueueId);
566 TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
568 console.debug("Saving import profile", template);
570 this.formTemplates[this.selectedTemplate] = template;
571 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
574 markTemplateDefault() {
576 Object.keys(this.formTemplates).forEach(
577 name => delete this.formTemplates.default
580 this.formTemplates[this.selectedTemplate].default = true;
582 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
585 templateSelectorChange(entry: ComboboxEntry) {
588 this.selectedTemplate = '';
592 this.selectedTemplate = entry.label; // label == name
594 if (entry.freetext) {
595 // User is entering a new template name.
600 // User selected an existing template, apply it to the form.
602 const template = this.formTemplates[entry.id];
604 // Copy the template values into "this"
605 TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
607 // Some values must be manually passed to the combobox'es
609 this.recordTypeSelector.applyEntryId(this.recordType);
610 this.bibSourceSelector.applyEntryId(this.selectedBibSource);
611 this.matchSetSelector.applyEntryId(this.selectedMatchSet);
612 this.holdingsProfileSelector
613 .applyEntryId(this.selectedHoldingsProfile);
614 this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
615 this.fallThruMergeProfileSelector
616 .applyEntryId(this.selectedFallThruMergeProfile);
620 delete this.formTemplates[this.selectedTemplate];
621 this.formTemplateSelector.selected = null;
622 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);