1 import {Component, OnInit, ViewChild, Renderer2} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {HttpClient} from '@angular/common/http';
4 import {tap} from 'rxjs/operators';
5 import {NetService} from '@eg/core/net.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
9 import {MarcRecord, MarcField} from '@eg/staff/share/marc-edit/marcrecord';
10 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
11 import {ServerStoreService} from '@eg/core/server-store.service';
13 const SESSION_POLL_INTERVAL = 2; // seconds
14 const MERGE_TEMPLATE_PATH = '/opac/extras/merge_template';
16 interface TemplateRule {
17 ruleType: 'r' | 'a' | 'd';
19 marcSubfields?: string;
26 templateUrl: 'marcbatch.component.html'
28 export class MarcBatchComponent implements OnInit {
31 source: 'b' | 'c' | 'r' = 'b';
32 buckets: ComboboxEntry[];
37 templateRules: TemplateRule[] = [];
41 progressMax: number = null;
42 progressValue: number = null;
47 private router: Router,
48 private route: ActivatedRoute,
49 private http: HttpClient,
50 private renderer: Renderer2,
51 private net: NetService,
52 private pcrud: PcrudService,
53 private auth: AuthService,
54 private store: ServerStoreService,
55 private cache: AnonCacheService
67 rulesetToRecord(resetRuleData?: boolean) {
68 this.record = new MarcRecord();
70 this.templateRules.forEach(rule => {
72 if (!rule.marcTag) { return; }
74 let ruleText = rule.marcTag + (rule.marcSubfields || '');
75 if (rule.advSubfield) {
77 `[${rule.advSubfield || ''} ~ ${rule.advRegex || ''}]`;
80 // Merge behavior is encoded in the 905 field.
81 const ruleTag = this.record.newField({
85 subfields: [[rule.ruleType, ruleText, 0]]
88 this.record.insertOrderedFields(ruleTag);
90 if (rule.ruleType === 'd') {
95 const dataRec = new MarcRecord();
96 if (resetRuleData || !rule.marcData) {
98 // Build a new value for the 'MARC Data' field based on
99 // changes to the selected tag or subfields.
101 const subfields = rule.marcSubfields ?
102 rule.marcSubfields.split('').map((sf, idx) => [sf, '', idx])
105 dataRec.appendFields(
114 console.log(dataRec.toBreaker());
115 rule.marcData = dataRec.toBreaker().split(/\n/)[1];
119 // Absorb the breaker data already in the 'MARC Data' field
120 // so it can be added to the template record in progress.
122 dataRec.breakerText = rule.marcData;
123 dataRec.absorbBreakerChanges();
126 this.record.appendFields(dataRec.fields[0]);
130 breakerRows(): number {
132 const breaker = this.record.toBreaker();
134 return breaker.split(/\n/).length + 1;
141 return this.record ? this.record.toBreaker() : '';
145 this.templateRules.push({ruleType: 'r'});
148 removeRule(idx: number) {
149 this.templateRules.splice(idx, 1);
152 getBuckets(): Promise<any> {
153 if (this.buckets) { return Promise.resolve(); }
155 return this.net.request(
157 'open-ils.actor.container.retrieve_by_class',
158 this.auth.token(), this.auth.user().id(),
159 'biblio', ['staff_client', 'vandelay_queue']
161 ).pipe(tap(buckets => {
162 this.buckets = buckets
163 .sort((b1, b2) => b1.name() < b2.name() ? -1 : 1)
164 .map(b => ({id: b.id(), label: b.name()}));
169 bucketChanged(entry: ComboboxEntry) {
170 this.bucket = entry ? entry.id : null;
173 fileSelected($event) {
174 this.csvFile = $event.target.files[0];
177 disableSave(): boolean {
178 if (!this.record || !this.source || this.processing) {
182 if (this.source === 'b') {
185 } else if (this.source === 'c') {
186 return (!this.csvColumn || !this.csvFile);
188 } else if (this.source === 'r') {
189 return !this.recordId;
194 this.processing = true;
195 this.progressValue = null;
196 this.progressMax = null;
197 this.numSucceeded = 0;
199 this.setReplaceMode();
200 this.postForm().then(_ => this.pollProgress());
204 if (this.record.subfield('905', 'r').length === 0) {
205 // Force replace mode w/ no-op replace rule.
206 this.record.appendFields(
207 this.record.newField({
211 subfields : [['r', '901c']]
217 postForm(): Promise<any> {
219 const formData: FormData = new FormData();
220 formData.append('ses', this.auth.token());
221 formData.append('skipui', '1');
222 formData.append('template', this.record.toXml());
223 formData.append('recordSource', this.source);
224 formData.append('xactPerRecord', '1');
226 if (this.source === 'b') {
227 formData.append('containerid', this.bucket + '');
229 } else if (this.source === 'c') {
230 formData.append('idcolumn', this.csvColumn + '');
231 formData.append('idfile', this.csvFile, this.csvFile.name);
233 } else if (this.source === 'r') {
234 formData.append('recid', this.recordId + '');
237 return this.http.post(
238 MERGE_TEMPLATE_PATH, formData, {responseType: 'text'})
239 .pipe(tap(cacheKey => this.session = cacheKey))
243 pollProgress(): Promise<any> {
244 console.debug('Polling session ', this.session);
246 return this.cache.getItem(this.session, 'batch_edit_progress')
248 // {"success":"t","complete":1,"failed":0,"succeeded":252}
251 console.error('No batch edit session found for ', this.session);
255 this.progressValue = progress.succeeded;
256 this.progressMax = progress.total;
257 this.numSucceeded = progress.succeeded;
258 this.numFailed = progress.failed;
260 if (progress.complete) {
261 this.processing = false;
265 setTimeout(() => this.pollProgress(), SESSION_POLL_INTERVAL * 1000);