1 import {Component, OnInit, AfterViewInit, Input,
2 ViewChild, OnDestroy} from '@angular/core';
3 import {Subject} from 'rxjs';
4 import {tap} from 'rxjs/operators';
5 import {IdlObject} from '@eg/core/idl.service';
6 import {NetService} from '@eg/core/net.service';
7 import {EventService} from '@eg/core/event.service';
8 import {OrgService} from '@eg/core/org.service';
9 import {AuthService} from '@eg/core/auth.service';
10 import {ToastService} from '@eg/share/toast/toast.service';
11 import {ComboboxComponent,
12 ComboboxEntry} from '@eg/share/combobox/combobox.component';
13 import {VandelayService, VandelayImportSelection,
14 VANDELAY_UPLOAD_PATH} from './vandelay.service';
15 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
16 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
17 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
18 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
19 import {ServerStoreService} from '@eg/core/server-store.service';
21 const TEMPLATE_SETTING_NAME = 'eg.cat.vandelay.import.templates';
23 const TEMPLATE_ATTRS = [
31 'autoOverlayAcqCopies',
32 'selectedHoldingsProfile',
33 'selectedMergeProfile',
34 'selectedFallThruMergeProfile',
35 'selectedTrashGroups',
39 interface ImportOptions {
41 overlay_map?: {[qrId: number]: /* breId */ number};
42 import_no_match?: boolean;
43 auto_overlay_exact?: boolean;
44 auto_overlay_best_match?: boolean;
45 auto_overlay_1match?: boolean;
46 opp_acq_copy_overlay?: boolean;
48 fall_through_merge_profile?: any;
49 strip_field_groups?: number[];
50 match_quality_ratio: number;
55 templateUrl: 'import.component.html'
57 export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
60 selectedQueue: ComboboxEntry; // freetext enabled
62 // used for applying a default queue ID value when we have
63 // a load-time queue before the queue combobox entries exist.
66 bibTrashGroups: IdlObject[];
67 selectedTrashGroups: number[];
69 activeQueueId: number;
70 selectedBucket: number;
71 selectedBibSource: number;
72 selectedMatchSet: number;
73 selectedHoldingsProfile: number;
74 selectedMergeProfile: number;
75 selectedFallThruMergeProfile: number;
78 defaultMatchSet: string;
80 importNonMatching: boolean;
81 mergeOnExact: boolean;
82 mergeOnSingleMatch: boolean;
83 mergeOnBestMatch: boolean;
84 minQualityRatio: number;
85 autoOverlayAcqCopies: boolean;
87 // True after the first upload, then remains true.
88 showProgress: boolean;
90 // Upload in progress.
93 // True only after successful upload
94 uploadComplete: boolean;
96 // Upload / processsing session key
97 // Generated by the server
100 // Optional enqueue/import tracker session name.
103 selectedTemplate: string;
104 formTemplates: {[name: string]: any};
105 newTemplateName: string;
107 @ViewChild('fileSelector') private fileSelector;
108 @ViewChild('uploadProgress')
109 private uploadProgress: ProgressInlineComponent;
110 @ViewChild('enqueueProgress')
111 private enqueueProgress: ProgressInlineComponent;
112 @ViewChild('importProgress')
113 private importProgress: ProgressInlineComponent;
115 // Need these refs so values can be applied via external stimuli
116 @ViewChild('formTemplateSelector')
117 private formTemplateSelector: ComboboxComponent;
118 @ViewChild('recordTypeSelector')
119 private recordTypeSelector: ComboboxComponent;
120 @ViewChild('bibSourceSelector')
121 private bibSourceSelector: ComboboxComponent;
122 @ViewChild('matchSetSelector')
123 private matchSetSelector: ComboboxComponent;
124 @ViewChild('holdingsProfileSelector')
125 private holdingsProfileSelector: ComboboxComponent;
126 @ViewChild('mergeProfileSelector')
127 private mergeProfileSelector: ComboboxComponent;
128 @ViewChild('fallThruMergeProfileSelector')
129 private fallThruMergeProfileSelector: ComboboxComponent;
131 @ViewChild('dupeQueueAlert')
132 private dupeQueueAlert: AlertDialogComponent;
135 private http: HttpClient,
136 private toast: ToastService,
137 private evt: EventService,
138 private net: NetService,
139 private auth: AuthService,
140 private org: OrgService,
141 private store: ServerStoreService,
142 private vandelay: VandelayService
144 this.applyDefaults();
148 this.minQualityRatio = 0;
149 this.selectedBibSource = 1; // default to system local
150 this.recordType = 'bib';
151 this.bibTrashGroups = [];
152 this.formTemplates = {};
154 if (this.vandelay.importSelection) {
156 if (!this.vandelay.importSelection.queue) {
157 // Incomplete import selection, clear it.
158 this.vandelay.importSelection = null;
162 const queue = this.vandelay.importSelection.queue;
163 this.recordType = queue.queue_type();
164 this.selectedMatchSet = queue.match_set();
166 // This will be propagated to selectedQueue as a combobox
167 // entry via the combobox
168 this.startQueueId = queue.id();
170 if (this.recordType === 'bib') {
171 this.selectedBucket = queue.match_bucket();
172 this.selectedHoldingsProfile = queue.item_attr_def();
180 this.loadStartupData();
184 // Always clear the import selection when navigating away from
186 this.clearSelection();
189 importSelection(): VandelayImportSelection {
190 return this.vandelay.importSelection;
193 loadStartupData(): Promise<any> {
194 // Note displaying and manipulating a progress dialog inside
195 // the AfterViewInit cycle leads to errors because the child
196 // component is modifed after dirty checking.
199 this.vandelay.getMergeProfiles(),
200 this.vandelay.getAllQueues('bib'),
201 this.vandelay.getAllQueues('authority'),
202 this.vandelay.getMatchSets('bib'),
203 this.vandelay.getMatchSets('authority'),
204 this.vandelay.getBibBuckets(),
205 this.vandelay.getBibSources(),
206 this.vandelay.getItemImportDefs(),
207 this.vandelay.getBibTrashGroups().then(
208 groups => this.bibTrashGroups = groups),
209 this.org.settings(['vandelay.default_match_set']).then(
210 s => this.defaultMatchSet = s['vandelay.default_match_set']),
214 return Promise.all(promises);
218 this.store.getItem(TEMPLATE_SETTING_NAME).then(
220 this.formTemplates = templates || {};
222 Object.keys(this.formTemplates).forEach(name => {
223 if (this.formTemplates[name].default) {
224 this.selectedTemplate = name;
231 formatTemplateEntries(): ComboboxEntry[] {
234 Object.keys(this.formTemplates || {}).forEach(
235 name => entries.push({id: name, label: name}));
240 // Format typeahead data sets
241 formatEntries(etype: string): ComboboxEntry[] {
242 const rtype = this.recordType;
247 return (this.vandelay.bibSources || []).map(
249 return {id: s.id(), label: s.source()};
253 list = this.vandelay.bibBuckets;
257 list = (this.vandelay.allQueues[rtype] || [])
258 .filter(q => q.complete() === 'f');
262 list = this.vandelay.matchSets[rtype];
265 case 'importItemDefs':
266 list = this.vandelay.importItemAttrDefs;
269 case 'mergeProfiles':
270 list = this.vandelay.mergeProfiles;
274 return (list || []).map(item => {
275 return {id: item.id(), label: item.name()};
279 selectEntry($event: ComboboxEntry, etype: string) {
280 const id = $event ? $event.id : null;
284 this.recordType = id;
288 this.selectedBibSource = id;
292 this.selectedBucket = id;
296 this.selectedMatchSet = id;
299 case 'importItemDefs':
300 this.selectedHoldingsProfile = id;
303 case 'mergeProfiles':
304 this.selectedMergeProfile = id;
307 case 'FallThruMergeProfile':
308 this.selectedFallThruMergeProfile = id;
313 fileSelected($event) {
314 this.selectedFile = $event.target.files[0];
317 // Required form data varies depending on context.
318 hasNeededData(): boolean {
319 if (this.vandelay.importSelection) {
320 return this.importActionSelected();
322 return this.selectedQueue &&
323 Boolean(this.recordType) && Boolean(this.selectedFile);
327 importActionSelected(): boolean {
328 return this.importNonMatching
330 || this.mergeOnSingleMatch
331 || this.mergeOnBestMatch;
334 // 1. create queue if necessary
335 // 2. upload MARC file
336 // 3. Enqueue MARC records
339 this.sessionKey = null;
340 this.showProgress = true;
341 this.isUploading = true;
342 this.uploadComplete = false;
343 this.resetProgressBars();
348 this.activeQueueId = queueId;
349 return this.uploadFile();
351 err => Promise.reject('queue create failed')
353 ok => this.processSpool(),
354 err => Promise.reject('process spool failed')
356 ok => this.importRecords(),
357 err => Promise.reject('import records failed')
360 this.isUploading = false;
361 this.uploadComplete = true;
364 console.log('file upload failed: ', err);
365 this.isUploading = false;
366 this.resetProgressBars();
372 resetProgressBars() {
373 this.uploadProgress.update({value: 0, max: 1});
374 this.enqueueProgress.update({value: 0, max: 1});
375 this.importProgress.update({value: 0, max: 1});
378 // Extract selected queue ID or create a new queue when requested.
379 resolveQueue(): Promise<number> {
381 if (this.selectedQueue.freetext) {
382 // Free text queue selector means create a new entry.
383 // TODO: first check for name dupes
385 return this.vandelay.createQueue(
386 this.selectedQueue.label,
388 this.selectedHoldingsProfile,
389 this.selectedMatchSet,
394 const evt = this.evt.parse(err);
396 if (evt.textcode.match(/QUEUE_EXISTS/)) {
397 this.dupeQueueAlert.open();
399 alert(evt); // server error
403 return Promise.reject('Queue Create Failed');
407 return Promise.resolve(this.selectedQueue.id);
411 uploadFile(): Promise<any> {
413 if (this.vandelay.importSelection) {
414 // Nothing to upload when processing pre-queued records.
415 return Promise.resolve();
418 const formData: FormData = new FormData();
420 formData.append('ses', this.auth.token());
421 formData.append('marc_upload',
422 this.selectedFile, this.selectedFile.name);
424 if (this.selectedBibSource) {
425 formData.append('bib_source', '' + this.selectedBibSource);
428 const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
429 {reportProgress: true, responseType: 'text'});
431 return this.http.request(req).pipe(tap(
433 if (evt.type === HttpEventType.UploadProgress) {
434 this.uploadProgress.update(
435 {value: evt.loaded, max: evt.total});
437 } else if (evt instanceof HttpResponse) {
438 this.sessionKey = evt.body as string;
440 'Vandelay file uploaded OK with key ' + this.sessionKey);
444 (err: HttpErrorResponse) => {
446 this.toast.danger(err.error);
451 processSpool(): Promise<any> {
453 if (this.vandelay.importSelection) {
454 // Nothing to enqueue when processing pre-queued records
455 return Promise.resolve();
458 let spoolType = this.recordType;
459 if (this.recordType === 'authority') {
463 const method = `open-ils.vandelay.${spoolType}.process_spool`;
465 return new Promise((resolve, reject) => {
467 'open-ils.vandelay', method,
468 this.auth.token(), this.sessionKey, this.activeQueueId,
469 null, null, this.selectedBibSource,
470 (this.sessionName || null), true
473 const e = this.evt.parse(tracker);
474 if (e) { console.error(e); return reject(); }
476 // Spooling is in progress, track the results.
477 this.vandelay.pollSessionTracker(tracker.id())
480 this.enqueueProgress.update({
481 // enqueue API only tracks actions performed
483 value: trkr.actions_performed()
486 err => { console.log(err); reject(); },
488 this.enqueueProgress.update({max: 1, value: 1});
497 importRecords(): Promise<any> {
499 if (!this.importActionSelected()) {
500 return Promise.resolve();
503 const selection = this.vandelay.importSelection;
505 if (selection && !selection.importQueue) {
506 return this.importRecordQueue(selection.recordIds);
508 return this.importRecordQueue();
512 importRecordQueue(recIds?: number[]): Promise<any> {
513 const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
515 let method = `open-ils.vandelay.${rtype}_queue.import`;
516 const options: ImportOptions = this.compileImportOptions();
518 let target: number | number[] = this.activeQueueId;
519 if (recIds && recIds.length) {
520 method = `open-ils.vandelay.${rtype}_record.list.import`;
524 return new Promise((resolve, reject) => {
525 this.net.request('open-ils.vandelay',
526 method, this.auth.token(), target, options)
529 const e = this.evt.parse(tracker);
530 if (e) { console.error(e); return reject(); }
532 // Spooling is in progress, track the results.
533 this.vandelay.pollSessionTracker(tracker.id())
536 this.importProgress.update({
537 max: trkr.total_actions(),
538 value: trkr.actions_performed()
541 err => { console.log(err); reject(); },
543 this.importProgress.update({max: 1, value: 1});
552 compileImportOptions(): ImportOptions {
554 const options: ImportOptions = {
555 session_key: this.sessionKey,
556 import_no_match: this.importNonMatching,
557 auto_overlay_exact: this.mergeOnExact,
558 auto_overlay_best_match: this.mergeOnBestMatch,
559 auto_overlay_1match: this.mergeOnSingleMatch,
560 opp_acq_copy_overlay: this.autoOverlayAcqCopies,
561 merge_profile: this.selectedMergeProfile,
562 fall_through_merge_profile: this.selectedFallThruMergeProfile,
563 strip_field_groups: this.selectedTrashGroups,
564 match_quality_ratio: this.minQualityRatio,
568 if (this.vandelay.importSelection) {
569 options.overlay_map = this.vandelay.importSelection.overlayMap;
576 this.vandelay.importSelection = null;
577 this.startQueueId = null;
581 console.log('opening queue ' + this.activeQueueId);
587 TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
589 console.debug('Saving import profile', template);
591 this.formTemplates[this.selectedTemplate] = template;
592 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
595 markTemplateDefault() {
597 Object.keys(this.formTemplates).forEach(
598 name => delete this.formTemplates.default
601 this.formTemplates[this.selectedTemplate].default = true;
603 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
606 templateSelectorChange(entry: ComboboxEntry) {
609 this.selectedTemplate = '';
613 this.selectedTemplate = entry.label; // label == name
615 if (entry.freetext) {
616 // User is entering a new template name.
621 // User selected an existing template, apply it to the form.
623 const template = this.formTemplates[entry.id];
625 // Copy the template values into "this"
626 TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
628 // Some values must be manually passed to the combobox'es
630 this.recordTypeSelector.applyEntryId(this.recordType);
631 this.bibSourceSelector.applyEntryId(this.selectedBibSource);
632 this.matchSetSelector.applyEntryId(this.selectedMatchSet);
633 this.holdingsProfileSelector
634 .applyEntryId(this.selectedHoldingsProfile);
635 this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
636 this.fallThruMergeProfileSelector
637 .applyEntryId(this.selectedFallThruMergeProfile);
641 delete this.formTemplates[this.selectedTemplate];
642 this.formTemplateSelector.selected = null;
643 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);