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 {ComboboxEntry} from '@eg/share/combobox/combobox.component';
10 import {VandelayService, VandelayImportSelection,
11 VANDELAY_UPLOAD_PATH} from './vandelay.service';
12 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
13 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
14 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
15 import {Subject} from 'rxjs/Subject';
17 interface ImportOptions {
19 overlay_map?: {[qrId: number]: /* breId */ number};
20 import_no_match?: boolean;
21 auto_overlay_exact?: boolean;
22 auto_overlay_best_match?: boolean;
23 auto_overlay_1match?: boolean;
24 opp_acq_copy_overlay?: boolean;
26 fall_through_merge_profile?: any;
27 strip_field_groups?: number[];
32 templateUrl: 'import.component.html'
34 export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
37 selectedQueue: ComboboxEntry; // freetext enabled
39 // used for applying a default queue ID value when we have
40 // a load-time queue before the queue combobox entries exist.
43 bibTrashGroups: IdlObject[];
44 selectedTrashGroups: number[];
46 activeQueueId: number;
47 selectedBucket: number;
48 selectedBibSource: number;
49 selectedMatchSet: number;
50 selectedHoldingsProfile: number;
51 selectedMergeProfile: number;
52 selectedFallThruMergeProfile: number;
55 defaultMatchSet: string;
57 importNonMatching: boolean;
58 mergeOnExact: boolean;
59 mergeOnSingleMatch: boolean;
60 mergeOnBestMatch: boolean;
61 minQualityRatio: number;
62 autoOverlayAcqCopies: boolean;
64 // True after the first upload, then remains true.
65 showProgress: boolean;
67 // Upload in progress.
70 // True only after successful upload
71 uploadComplete: boolean;
73 // Upload / processsing session key
74 // Generated by the server
77 // Optional enqueue/import tracker session name.
80 @ViewChild('fileSelector') private fileSelector;
81 @ViewChild('uploadProgress')
82 private uploadProgress: ProgressInlineComponent;
83 @ViewChild('enqueueProgress')
84 private enqueueProgress: ProgressInlineComponent;
85 @ViewChild('importProgress')
86 private importProgress: ProgressInlineComponent;
89 private http: HttpClient,
90 private toast: ToastService,
91 private evt: EventService,
92 private net: NetService,
93 private auth: AuthService,
94 private org: OrgService,
95 private vandelay: VandelayService
101 this.minQualityRatio = 0;
102 this.selectedBibSource = 1; // default to system local
103 this.recordType = 'bib';
104 this.bibTrashGroups = [];
106 if (this.vandelay.importSelection) {
108 if (!this.vandelay.importSelection.queue) {
109 // Incomplete import selection, clear it.
110 this.vandelay.importSelection = null;
114 const queue = this.vandelay.importSelection.queue;
115 this.recordType = queue.queue_type();
116 this.selectedMatchSet = queue.match_set();
118 // This will be propagated to selectedQueue as a combobox
119 // entry via the combobox
120 this.startQueueId = queue.id();
122 if (this.recordType === 'bib') {
123 this.selectedBucket = queue.match_bucket();
124 this.selectedHoldingsProfile = queue.item_attr_def();
132 this.loadStartupData();
136 // If we successfully completed the most recent
137 // upload/import assume the importSelection can be cleared.
138 if (this.uploadComplete) {
139 this.clearSelection();
143 importSelection(): VandelayImportSelection {
144 return this.vandelay.importSelection;
147 loadStartupData(): Promise<any> {
148 // Note displaying and manipulating a progress dialog inside
149 // the AfterViewInit cycle leads to errors because the child
150 // component is modifed after dirty checking.
153 this.vandelay.getMergeProfiles(),
154 this.vandelay.getAllQueues('bib'),
155 this.vandelay.getAllQueues('authority'),
156 this.vandelay.getMatchSets('bib'),
157 this.vandelay.getMatchSets('authority'),
158 this.vandelay.getBibBuckets(),
159 this.vandelay.getBibSources(),
160 this.vandelay.getItemImportDefs(),
161 this.vandelay.getBibTrashGroups().then(
162 groups => this.bibTrashGroups = groups),
163 this.org.settings(['vandelay.default_match_set']).then(
164 s => this.defaultMatchSet = s['vandelay.default_match_set'])
167 return Promise.all(promises);
170 // Format typeahead data sets
171 formatEntries(etype: string): ComboboxEntry[] {
172 const rtype = this.recordType;
177 return (this.vandelay.bibSources || []).map(
178 s => { return {id: s.id(), label: s.source()}; });
181 list = this.vandelay.bibBuckets;
185 list = this.vandelay.allQueues[rtype];
189 list = this.vandelay.matchSets[rtype];
192 case 'importItemDefs':
193 list = this.vandelay.importItemAttrDefs;
196 case 'mergeProfiles':
197 list = this.vandelay.mergeProfiles;
201 return (list || []).map(item => {
202 return {id: item.id(), label: item.name()};
206 selectEntry($event: ComboboxEntry, etype: string) {
207 const id = $event ? $event.id : null;
211 this.recordType = id;
214 this.selectedBibSource = id;
218 this.selectedBucket = id;
222 this.selectedMatchSet = id;
225 case 'importItemDefs':
226 this.selectedHoldingsProfile = id;
229 case 'mergeProfiles':
230 this.selectedMergeProfile = id;
233 case 'FallThruMergeProfile':
234 this.selectedFallThruMergeProfile = id;
239 fileSelected($event) {
240 this.selectedFile = $event.target.files[0];
243 // Required form data varies depending on context.
244 hasNeededData(): boolean {
245 if (this.vandelay.importSelection) {
246 return this.importActionSelected();
248 return this.selectedQueue
249 && Boolean(this.recordType) && Boolean(this.selectedFile)
253 importActionSelected(): boolean {
254 return this.importNonMatching
256 || this.mergeOnSingleMatch
257 || this.mergeOnBestMatch;
260 // 1. create queue if necessary
261 // 2. upload MARC file
262 // 3. Enqueue MARC records
265 this.sessionKey = null;
266 this.showProgress = true;
267 this.isUploading = true;
268 this.uploadComplete = false;
269 this.resetProgressBars();
274 this.activeQueueId = queueId;
275 return this.uploadFile();
277 err => Promise.reject('queue create failed')
279 ok => this.processSpool(),
280 err => Promise.reject('process spool failed')
282 ok => this.importRecords(),
283 err => Promise.reject('import records failed')
286 this.isUploading = false;
287 this.uploadComplete = true;
290 console.log('file upload failed: ', err);
291 this.isUploading = false;
292 this.resetProgressBars();
298 resetProgressBars() {
299 this.uploadProgress.update({value: 0, max: 1});
300 this.enqueueProgress.update({value: 0, max: 1});
301 this.importProgress.update({value: 0, max: 1});
304 // Extract selected queue ID or create a new queue when requested.
305 resolveQueue(): Promise<number> {
307 if (this.selectedQueue.freetext) {
308 // Free text queue selector means create a new entry.
309 // TODO: first check for name dupes
311 return this.vandelay.createQueue(
312 this.selectedQueue.label,
314 this.selectedHoldingsProfile,
315 this.selectedMatchSet,
320 return Promise.resolve(this.selectedQueue.id);
324 uploadFile(): Promise<any> {
326 if (this.vandelay.importSelection) {
327 // Nothing to upload when processing pre-queued records.
328 return Promise.resolve();
331 const formData: FormData = new FormData();
333 formData.append('ses', this.auth.token());
334 formData.append('marc_upload',
335 this.selectedFile, this.selectedFile.name);
337 if (this.selectedBibSource) {
338 formData.append('bib_source', ''+this.selectedBibSource);
341 const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
342 {reportProgress: true, responseType: 'text'});
344 return this.http.request(req).pipe(tap(
346 if (evt.type === HttpEventType.UploadProgress) {
347 this.uploadProgress.update(
348 {value: evt.loaded, max: evt.total});
350 } else if (evt instanceof HttpResponse) {
351 this.sessionKey = evt.body as string;
353 'Vandelay file uploaded OK with key '+this.sessionKey);
357 (err: HttpErrorResponse) => {
359 this.toast.danger(err.error);
364 processSpool(): Promise<any> {
366 if (this.vandelay.importSelection) {
367 // Nothing to enqueue when processing pre-queued records
368 return Promise.resolve();
371 const method = `open-ils.vandelay.${this.recordType}.process_spool`;
373 return new Promise((resolve, reject) => {
375 'open-ils.vandelay', method,
376 this.auth.token(), this.sessionKey, this.activeQueueId,
377 null, null, this.selectedBibSource,
378 (this.sessionName || null), true
381 const e = this.evt.parse(tracker);
382 if (e) { console.error(e); return reject(); }
384 // Spooling is in progress, track the results.
385 this.vandelay.pollSessionTracker(tracker.id())
388 this.enqueueProgress.update({
389 // enqueue API only tracks actions performed
391 value: trkr.actions_performed()
394 err => { console.log(err); reject(); },
396 this.enqueueProgress.update({max: 1, value: 1});
405 importRecords(): Promise<any> {
407 if (!this.importActionSelected()) {
408 return Promise.resolve();
411 const selection = this.vandelay.importSelection;
413 if (selection && !selection.importQueue) {
414 return this.importRecordQueue(selection.recordIds);
416 return this.importRecordQueue();
420 importRecordQueue(recIds?: number[]): Promise<any> {
421 const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
423 let method = `open-ils.vandelay.${rtype}_queue.import`;
424 const options: ImportOptions = this.compileImportOptions();
426 let target: number | number[] = this.activeQueueId;
427 if (recIds && recIds.length) {
428 method = `open-ils.vandelay.${rtype}_record.list.import`;
432 return new Promise((resolve, reject) => {
433 this.net.request('open-ils.vandelay',
434 method, this.auth.token(), target, options)
437 const e = this.evt.parse(tracker);
438 if (e) { console.error(e); return reject(); }
440 // Spooling is in progress, track the results.
441 this.vandelay.pollSessionTracker(tracker.id())
444 this.importProgress.update({
445 max: trkr.total_actions(),
446 value: trkr.actions_performed()
449 err => { console.log(err); reject(); },
451 this.importProgress.update({max: 1, value: 1});
460 compileImportOptions(): ImportOptions {
462 const options: ImportOptions = {
463 session_key: this.sessionKey,
464 import_no_match: this.importNonMatching,
465 auto_overlay_exact: this.mergeOnExact,
466 auto_overlay_best_match: this.mergeOnBestMatch,
467 auto_overlay_1match: this.mergeOnSingleMatch,
468 opp_acq_copy_overlay: this.autoOverlayAcqCopies,
469 merge_profile: this.selectedMergeProfile,
470 fall_through_merge_profile: this.selectedFallThruMergeProfile,
471 strip_field_groups: this.selectedTrashGroups,
475 if (this.vandelay.importSelection) {
476 options.overlay_map = this.vandelay.importSelection.overlayMap;
483 this.vandelay.importSelection = null;
484 this.startQueueId = null;
488 console.log('opening queue ' + this.activeQueueId);