]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
LP#1779158 Ang6 Vandelay UI Port
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / cat / vandelay / import.component.ts
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';
16
17 interface ImportOptions {
18     session_key: string;
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;
25     merge_profile?: any;
26     fall_through_merge_profile?: any;
27     strip_field_groups?: number[];
28     exit_early: boolean;
29 }
30
31 @Component({
32   templateUrl: 'import.component.html'
33 })
34 export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
35
36     recordType: string;
37     selectedQueue: ComboboxEntry; // freetext enabled
38
39     // used for applying a default queue ID value when we have
40     // a load-time queue before the queue combobox entries exist.
41     startQueueId: number; 
42
43     bibTrashGroups: IdlObject[];
44     selectedTrashGroups: number[];
45
46     activeQueueId: number;
47     selectedBucket: number;
48     selectedBibSource: number;
49     selectedMatchSet: number;
50     selectedHoldingsProfile: number;
51     selectedMergeProfile: number;
52     selectedFallThruMergeProfile: number;
53     selectedFile: File;
54
55     defaultMatchSet: string;
56
57     importNonMatching: boolean;
58     mergeOnExact: boolean;
59     mergeOnSingleMatch: boolean;
60     mergeOnBestMatch: boolean;
61     minQualityRatio: number;
62     autoOverlayAcqCopies: boolean;
63
64     // True after the first upload, then remains true.
65     showProgress: boolean;
66
67     // Upload in progress.
68     isUploading: boolean;
69
70     // True only after successful upload
71     uploadComplete: boolean;
72
73     // Upload / processsing session key
74     // Generated by the server
75     sessionKey: string;
76
77     // Optional enqueue/import tracker session name.
78     sessionName: string;
79
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;
87
88     constructor(
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
96     ) {
97         this.applyDefaults();
98     }
99
100     applyDefaults() {
101         this.minQualityRatio = 0;
102         this.selectedBibSource = 1; // default to system local
103         this.recordType = 'bib';
104         this.bibTrashGroups = [];
105
106         if (this.vandelay.importSelection) {
107
108             if (!this.vandelay.importSelection.queue) {
109                 // Incomplete import selection, clear it.
110                 this.vandelay.importSelection = null;
111                 return;
112             }
113
114             const queue = this.vandelay.importSelection.queue;
115             this.recordType = queue.queue_type();
116             this.selectedMatchSet = queue.match_set();
117
118             // This will be propagated to selectedQueue as a combobox
119             // entry via the combobox
120             this.startQueueId = queue.id();
121
122             if (this.recordType === 'bib') {
123                 this.selectedBucket = queue.match_bucket();
124                 this.selectedHoldingsProfile = queue.item_attr_def();
125             }
126         }
127     }
128
129     ngOnInit() {}
130
131     ngAfterViewInit() {
132         this.loadStartupData();
133     }
134
135     ngOnDestroy() {
136         // If we successfully completed the most recent 
137         // upload/import assume the importSelection can be cleared.
138         if (this.uploadComplete) {
139             this.clearSelection();
140         }
141     }
142
143     importSelection(): VandelayImportSelection {
144         return this.vandelay.importSelection;
145     }
146
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.
151
152         const promises = [
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'])
165         ];
166
167         return Promise.all(promises);
168     }
169
170     // Format typeahead data sets
171     formatEntries(etype: string): ComboboxEntry[] {
172         const rtype = this.recordType;
173         let list;
174
175         switch (etype) {
176             case 'bibSources':
177                 return (this.vandelay.bibSources || []).map(
178                     s => { return {id: s.id(), label: s.source()}; });
179
180             case 'bibBuckets':
181                 list = this.vandelay.bibBuckets;
182                 break;
183
184             case 'allQueues':
185                 list = this.vandelay.allQueues[rtype];
186                 break;
187
188             case 'matchSets':
189                 list = this.vandelay.matchSets[rtype];
190                 break;
191
192             case 'importItemDefs':
193                 list = this.vandelay.importItemAttrDefs;
194                 break;
195
196             case 'mergeProfiles':
197                 list = this.vandelay.mergeProfiles;
198                 break;
199         }
200
201         return (list || []).map(item => {
202             return {id: item.id(), label: item.name()};
203         });
204     }
205
206     selectEntry($event: ComboboxEntry, etype: string) {
207         const id = $event ? $event.id : null;
208
209         switch (etype) {
210             case 'recordType':
211                 this.recordType = id;
212               
213             case 'bibSources':
214                 this.selectedBibSource = id;
215                 break;
216
217             case 'bibBuckets':
218                 this.selectedBucket = id;
219                 break;
220
221             case 'matchSets':
222                 this.selectedMatchSet = id;
223                 break;
224
225             case 'importItemDefs':
226                 this.selectedHoldingsProfile = id;
227                 break;
228
229             case 'mergeProfiles':
230                 this.selectedMergeProfile = id;
231                 break;
232
233             case 'FallThruMergeProfile':
234                 this.selectedFallThruMergeProfile = id;
235                 break;
236         }
237     }
238
239     fileSelected($event) {
240        this.selectedFile = $event.target.files[0]; 
241     }
242
243     // Required form data varies depending on context.
244     hasNeededData(): boolean {
245         if (this.vandelay.importSelection) {
246             return this.importActionSelected();
247         } else {
248             return this.selectedQueue 
249                 && Boolean(this.recordType) && Boolean(this.selectedFile)
250         }
251     }
252
253     importActionSelected(): boolean {
254         return this.importNonMatching
255             || this.mergeOnExact
256             || this.mergeOnSingleMatch
257             || this.mergeOnBestMatch;
258     }
259
260     // 1. create queue if necessary
261     // 2. upload MARC file
262     // 3. Enqueue MARC records
263     // 4. Import records
264     upload() {
265         this.sessionKey = null;
266         this.showProgress = true;
267         this.isUploading = true;
268         this.uploadComplete = false;
269         this.resetProgressBars();
270
271         this.resolveQueue()
272         .then(
273             queueId => {
274                 this.activeQueueId = queueId;
275                 return this.uploadFile();
276             },
277             err => Promise.reject('queue create failed')
278         ).then(
279             ok => this.processSpool(),
280             err => Promise.reject('process spool failed')
281         ).then(
282             ok => this.importRecords(),
283             err => Promise.reject('import records failed')
284         ).then(
285             ok => {
286                 this.isUploading = false;
287                 this.uploadComplete = true;
288             },
289             err => {
290                 console.log('file upload failed: ', err);
291                 this.isUploading = false;
292                 this.resetProgressBars();
293
294             }
295         );
296     }
297
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});
302     }
303
304     // Extract selected queue ID or create a new queue when requested.
305     resolveQueue(): Promise<number> {
306
307         if (this.selectedQueue.freetext) {
308             // Free text queue selector means create a new entry.
309             // TODO: first check for name dupes
310
311             return this.vandelay.createQueue(
312                 this.selectedQueue.label,
313                 this.recordType,
314                 this.selectedHoldingsProfile,
315                 this.selectedMatchSet,
316                 this.selectedBucket
317             );
318
319         } else {
320             return Promise.resolve(this.selectedQueue.id);
321         }
322     }
323
324     uploadFile(): Promise<any> {
325
326         if (this.vandelay.importSelection) {
327             // Nothing to upload when processing pre-queued records.
328             return Promise.resolve();
329         }
330         
331         const formData: FormData = new FormData();
332
333         formData.append('ses', this.auth.token());
334         formData.append('marc_upload', 
335             this.selectedFile, this.selectedFile.name);
336
337         if (this.selectedBibSource) {
338             formData.append('bib_source', ''+this.selectedBibSource);
339         }
340
341         const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData, 
342             {reportProgress: true, responseType: 'text'});
343
344         return this.http.request(req).pipe(tap(
345             evt => {
346                 if (evt.type === HttpEventType.UploadProgress) {
347                     this.uploadProgress.update(
348                         {value: evt.loaded, max: evt.total});
349
350                 } else if (evt instanceof HttpResponse) {
351                     this.sessionKey = evt.body as string;
352                     console.log(
353                         'Vandelay file uploaded OK with key '+this.sessionKey);
354                 }
355             },
356
357             (err: HttpErrorResponse) => {
358                 console.error(err);
359                 this.toast.danger(err.error);
360             }
361         )).toPromise();
362     }
363
364     processSpool():  Promise<any> {
365
366         if (this.vandelay.importSelection) {
367             // Nothing to enqueue when processing pre-queued records
368             return Promise.resolve();
369         }
370
371         const method = `open-ils.vandelay.${this.recordType}.process_spool`;
372
373         return new Promise((resolve, reject) => {
374             this.net.request(
375                 'open-ils.vandelay', method, 
376                 this.auth.token(), this.sessionKey, this.activeQueueId,
377                 null, null, this.selectedBibSource, 
378                 (this.sessionName || null), true
379             ).subscribe(
380                 tracker => {
381                     const e = this.evt.parse(tracker);
382                     if (e) { console.error(e); return reject(); }
383
384                     // Spooling is in progress, track the results.
385                     this.vandelay.pollSessionTracker(tracker.id())
386                     .subscribe(
387                         trkr => {
388                             this.enqueueProgress.update({
389                                 // enqueue API only tracks actions performed
390                                 max: null, 
391                                 value: trkr.actions_performed()
392                             });
393                         },
394                         err => { console.log(err); reject(); },
395                         () => {
396                             this.enqueueProgress.update({max: 1, value: 1});
397                             resolve();
398                         }
399                     );
400                 }
401             );
402         });
403     }
404
405     importRecords(): Promise<any> {
406
407         if (!this.importActionSelected()) {
408             return Promise.resolve();
409         }
410
411         const selection = this.vandelay.importSelection;
412
413         if (selection && !selection.importQueue) {
414             return this.importRecordQueue(selection.recordIds);
415         } else {
416             return this.importRecordQueue();
417         }
418     }
419
420     importRecordQueue(recIds?: number[]): Promise<any> {
421         const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
422
423         let method = `open-ils.vandelay.${rtype}_queue.import`;
424         const options: ImportOptions = this.compileImportOptions();
425
426         let target: number | number[] = this.activeQueueId;
427         if (recIds && recIds.length) {
428             method = `open-ils.vandelay.${rtype}_record.list.import`;
429             target = recIds;
430         }
431
432         return new Promise((resolve, reject) => {
433             this.net.request('open-ils.vandelay', 
434                 method, this.auth.token(), target, options)
435             .subscribe(
436                 tracker => {
437                     const e = this.evt.parse(tracker);
438                     if (e) { console.error(e); return reject(); }
439
440                     // Spooling is in progress, track the results.
441                     this.vandelay.pollSessionTracker(tracker.id())
442                     .subscribe(
443                         trkr => {
444                             this.importProgress.update({
445                                 max: trkr.total_actions(),
446                                 value: trkr.actions_performed()
447                             });
448                         },
449                         err => { console.log(err); reject(); },
450                         () => {
451                             this.importProgress.update({max: 1, value: 1});
452                             resolve();
453                         }
454                     );
455                 }
456             );
457         });
458     }
459
460     compileImportOptions(): ImportOptions {
461
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,
472             exit_early: true
473         };
474
475         if (this.vandelay.importSelection) {
476             options.overlay_map = this.vandelay.importSelection.overlayMap;
477         }
478
479         return options;
480     }
481
482     clearSelection() {
483         this.vandelay.importSelection = null;
484         this.startQueueId = null;
485     }
486
487     openQueue() {
488         console.log('opening queue ' + this.activeQueueId);
489     }
490 }
491