1 import {Component, OnInit, 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} 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
60 this.route.paramMap.subscribe((params: ParamMap) => {
61 this.bucket = +params.get('bucketId');
62 this.recordId = +params.get('recordId');
66 } else if (this.recordId) {
79 rulesetToRecord(resetRuleData?: boolean) {
80 this.record = new MarcRecord();
82 this.templateRules.forEach(rule => {
84 if (!rule.marcTag) { return; }
86 let ruleText = rule.marcTag + (rule.marcSubfields || '');
87 if (rule.advSubfield) {
89 `[${rule.advSubfield || ''} ~ ${rule.advRegex || ''}]`;
92 // Merge behavior is encoded in the 905 field.
93 const ruleTag = this.record.newField({
97 subfields: [[rule.ruleType, ruleText, 0]]
100 this.record.insertOrderedFields(ruleTag);
102 if (rule.ruleType === 'd') {
107 const dataRec = new MarcRecord();
108 if (resetRuleData || !rule.marcData) {
110 // Build a new value for the 'MARC Data' field based on
111 // changes to the selected tag or subfields.
113 const subfields = rule.marcSubfields ?
114 rule.marcSubfields.split('').map((sf, idx) => [sf, '', idx])
117 dataRec.appendFields(
126 console.log(dataRec.toBreaker());
127 rule.marcData = dataRec.toBreaker().split(/\n/)[1];
131 // Absorb the breaker data already in the 'MARC Data' field
132 // so it can be added to the template record in progress.
134 dataRec.breakerText = rule.marcData;
135 dataRec.absorbBreakerChanges();
138 this.record.appendFields(dataRec.fields[0]);
142 breakerRows(): number {
144 const breaker = this.record.toBreaker();
146 return breaker.split(/\n/).length + 1;
149 // eslint-disable-next-line no-magic-numbers
154 return this.record ? this.record.toBreaker() : '';
158 this.templateRules.push({ruleType: 'r'});
161 removeRule(idx: number) {
162 this.templateRules.splice(idx, 1);
165 getBuckets(): Promise<any> {
166 if (this.buckets) { return Promise.resolve(); }
168 return this.net.request(
170 'open-ils.actor.container.retrieve_by_class',
171 this.auth.token(), this.auth.user().id(),
172 'biblio', ['staff_client', 'vandelay_queue']
174 ).pipe(tap(buckets => {
175 this.buckets = buckets
176 .sort((b1, b2) => b1.name() < b2.name() ? -1 : 1)
177 .map(b => ({id: b.id(), label: b.name()}));
182 bucketChanged(entry: ComboboxEntry) {
183 this.bucket = entry ? entry.id : null;
186 fileSelected($event) {
187 this.csvFile = $event.target.files[0];
190 disableSave(): boolean {
191 if (!this.record || !this.source || this.processing) {
195 if (this.source === 'b') {
198 } else if (this.source === 'c') {
199 return (this.csvColumn < 0 || !this.csvFile);
201 } else if (this.source === 'r') {
202 return !this.recordId;
207 this.processing = true;
208 this.progressValue = null;
209 this.progressMax = null;
210 this.numSucceeded = 0;
212 this.setReplaceMode();
213 this.postForm().then(_ => this.pollProgress());
217 if (this.record.subfield('905', 'r').length === 0) {
218 // Force replace mode w/ no-op replace rule.
219 this.record.appendFields(
220 this.record.newField({
224 subfields : [['r', '901c']]
230 postForm(): Promise<any> {
232 const formData: FormData = new FormData();
233 formData.append('ses', this.auth.token());
234 formData.append('skipui', '1');
235 formData.append('template', this.record.toXml());
236 formData.append('recordSource', this.source);
237 formData.append('xactPerRecord', '1');
239 if (this.source === 'b') {
240 formData.append('containerid', this.bucket + '');
242 } else if (this.source === 'c') {
243 formData.append('idcolumn', this.csvColumn + '');
244 formData.append('idfile', this.csvFile, this.csvFile.name);
246 } else if (this.source === 'r') {
247 formData.append('recid', this.recordId + '');
250 return this.http.post(
251 MERGE_TEMPLATE_PATH, formData, {responseType: 'text'})
252 .pipe(tap(cacheKey => this.session = cacheKey))
256 pollProgress(): Promise<any> {
257 console.debug('Polling session ', this.session);
259 return this.cache.getItem(this.session, 'batch_edit_progress')
261 // {"success":"t","complete":1,"failed":0,"succeeded":252}
264 console.error('No batch edit session found for ', this.session);
268 this.progressValue = progress.succeeded;
269 this.progressMax = progress.total;
270 this.numSucceeded = progress.succeeded;
271 this.numFailed = progress.failed;
273 if (progress.complete) {
274 this.processing = false;
278 setTimeout(() => this.pollProgress(), SESSION_POLL_INTERVAL * 1000);