]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.ts
LP1929741 ACQ Selection List & PO Angluar Port
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / marc-edit / editor.component.ts
1 import {Component, Input, Output, OnInit, EventEmitter, ViewChild} from '@angular/core';
2 import {IdlService} from '@eg/core/idl.service';
3 import {EventService} from '@eg/core/event.service';
4 import {NetService} from '@eg/core/net.service';
5 import {AuthService} from '@eg/core/auth.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {ToastService} from '@eg/share/toast/toast.service';
9 import {ServerStoreService} from '@eg/core/server-store.service';
10 import {StringComponent} from '@eg/share/string/string.component';
11 import {MarcRecord} from './marcrecord';
12 import {ComboboxEntry, ComboboxComponent
13   } from '@eg/share/combobox/combobox.component';
14 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
15 import {MarcEditContext, MARC_RECORD_TYPE} from './editor-context';
16 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
17 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
18
19
20 export interface MarcSavedEvent {
21     marcXml: string;
22     bibSource?: number;
23     recordId?: number;
24 }
25
26 /**
27  * MARC Record editor main interface.
28  */
29
30 @Component({
31   selector: 'eg-marc-editor',
32   templateUrl: './editor.component.html'
33 })
34
35 export class MarcEditorComponent implements OnInit {
36
37     editorTab: 'rich' | 'flat';
38     sources: ComboboxEntry[];
39     context: MarcEditContext;
40
41     // True if the save request is in flight
42     dataSaving: boolean;
43
44     @Input() recordType: MARC_RECORD_TYPE = 'biblio';
45
46     _pendingRecordId: number;
47     @Input() set recordId(id: number) {
48         if (this.record && this.record.id === id) { return; }
49
50         // Avoid fetching the record by ID before OnInit since we may
51         // not yet know our recordType.
52         if (this.initCalled) {
53             this._pendingRecordId = null;
54             this.fromId(id);
55
56          } else {
57             // fetch later in OnInit
58             this._pendingRecordId = id;
59          }
60     }
61
62     get recordId(): number {
63         return this.record ? this.record.id : this._pendingRecordId;
64     }
65
66     @Input() set recordXml(xml: string) {
67         if (xml) {
68             this.fromXml(xml);
69         }
70     }
71
72     get record(): MarcRecord {
73         return this.context.record;
74     }
75
76     // Tell us which record source to select by default.
77     // Useful for new records and in-place editing from bare XML.
78     @Input() recordSource: number;
79
80     // If true, saving records to the database is assumed to
81     // happen externally.  IOW, the record editor is just an
82     // in-place MARC modification interface.
83     @Input() inPlaceMode: boolean;
84
85     // In inPlaceMode, this is emitted in lieu of saving the record
86     // in th database.  When inPlaceMode is false, this is emitted after
87     // the record is successfully saved.
88     @Output() recordSaved: EventEmitter<MarcSavedEvent>;
89
90     @ViewChild('sourceSelector', {static: false}) sourceSelector: ComboboxComponent;
91     @ViewChild('confirmDelete', {static: false}) confirmDelete: ConfirmDialogComponent;
92     @ViewChild('confirmUndelete', {static: false}) confirmUndelete: ConfirmDialogComponent;
93     @ViewChild('cannotDelete', {static: false}) cannotDelete: ConfirmDialogComponent;
94     @ViewChild('successMsg', {static: false}) successMsg: StringComponent;
95     @ViewChild('failMsg', {static: false}) failMsg: StringComponent;
96
97     fastItemLabel: string;
98     fastItemBarcode: string;
99     showFastAdd: boolean;
100     initCalled = false;
101
102     constructor(
103         private evt: EventService,
104         private idl: IdlService,
105         private net: NetService,
106         private auth: AuthService,
107         private org: OrgService,
108         private pcrud: PcrudService,
109         private toast: ToastService,
110         private holdings: HoldingsService,
111         private store: ServerStoreService
112     ) {
113         this.sources = [];
114         this.recordSaved = new EventEmitter<MarcSavedEvent>();
115         this.context = new MarcEditContext();
116
117         this.recordSaved.subscribe(_ => this.dataSaving = false);
118     }
119
120     ngOnInit() {
121
122         this.initCalled = true;
123
124         this.context.recordType = this.recordType;
125
126         this.store.getItem('cat.marcedit.flateditor').then(
127             useFlat => this.editorTab = useFlat ? 'flat' : 'rich');
128
129         if (!this.record && this.recordId) {
130             this.fromId(this.recordId);
131         }
132
133         if (this.recordType !== 'biblio') { return; }
134
135         this.pcrud.retrieveAll('cbs').subscribe(
136             src => this.sources.push({id: +src.id(), label: src.source()}),
137             _ => {},
138             () => {
139                 this.sources = this.sources.sort((a, b) =>
140                     a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
141                 );
142
143                 if (this.recordSource) {
144                     this.sourceSelector.applyEntryId(this.recordSource);
145                 }
146             }
147         );
148     }
149
150     changesPending(): boolean {
151         return this.context.changesPending;
152     }
153
154     clearPendingChanges() {
155         this.context.changesPending = false;
156     }
157
158     // Remember the last used tab as the preferred tab.
159     tabChange(evt: NgbNavChangeEvent) {
160
161         // Avoid undo persistence across tabs since that could result
162         // in changes getting lost.
163         this.context.resetUndos();
164
165         if (evt.nextId === 'flat') {
166             this.store.setItem('cat.marcedit.flateditor', true);
167         } else {
168             this.store.removeItem('cat.marcedit.flateditor');
169         }
170     }
171
172     saveRecord(): Promise<any> {
173         const xml = this.record.toXml();
174         this.dataSaving = true;
175
176         // Save actions clears any pending changes.
177         this.context.changesPending = false;
178         this.context.resetUndos();
179
180         let sourceName: string = null;
181         let sourceId: number = null;
182
183         if (this.sourceSelector && this.sourceSelector.selected) {
184             sourceName = this.sourceSelector.selected.label;
185             sourceId = this.sourceSelector.selected.id;
186         }
187
188         const emission = {
189             marcXml: xml, bibSource: sourceId, recordId: this.recordId};
190
191         if (this.inPlaceMode) {
192             // Let the caller have the modified XML and move on.
193             this.recordSaved.emit(emission);
194             return Promise.resolve();
195         }
196
197         let promise;
198
199         if (this.record.id) { // Editing an existing record
200
201             promise = this.modifyRecord(xml, sourceName, sourceId);
202
203         } else {
204
205             promise = this.createRecord(xml, sourceName);
206         }
207
208         // NOTE we do not reinitialize our record with the MARC returned
209         // from the server after a create/update, which means our record
210         // may be out of sync, e.g. missing 901* values.  It's the
211         // callers responsibility to tear us down and rebuild us.
212         return promise.then(marcXml => {
213             if (!marcXml) { return null; }
214             this.successMsg.current().then(msg => this.toast.success(msg));
215             emission.marcXml = marcXml;
216             emission.recordId = this.recordId;
217             this.recordSaved.emit(emission);
218             this.fastAdd();
219             return marcXml;
220         });
221     }
222
223     modifyRecord(marcXml: string, sourceName: string, sourceId: number): Promise<any> {
224         const method = this.recordType === 'biblio' ?
225             'open-ils.cat.biblio.record.xml.update' :
226             'open-ils.cat.authority.record.overlay';
227
228         return this.net.request('open-ils.cat', method,
229             this.auth.token(), this.record.id, marcXml, sourceName
230
231         ).toPromise().then(response => {
232
233             const evt = this.evt.parse(response);
234             if (evt) {
235                 console.error(evt);
236                 this.failMsg.current().then(msg => this.toast.warning(msg));
237                 this.dataSaving = false;
238                 return null;
239             }
240
241             // authority.record.overlay resturns a '1' on success.
242             return typeof response === 'object' ? response.marc() : marcXml;
243         });
244     }
245
246     createRecord(marcXml: string, sourceName?: string): Promise<any> {
247
248         const method = this.recordType === 'biblio' ?
249             'open-ils.cat.biblio.record.xml.create' :
250             'open-ils.cat.authority.record.import';
251
252         return this.net.request('open-ils.cat', method,
253             this.auth.token(), marcXml, sourceName
254         ).toPromise().then(response => {
255
256             const evt = this.evt.parse(response);
257
258             if (evt) {
259                 console.error(evt);
260                 this.failMsg.current().then(msg => this.toast.warning(msg));
261                 this.dataSaving = false;
262                 return null;
263             }
264
265             this.record.id = response.id();
266             return response.marc();
267         });
268     }
269
270     fromId(id: number): Promise<any> {
271         const idlClass = this.recordType === 'authority' ? 'are' : 'bre';
272
273         return this.pcrud.retrieve(idlClass, id)
274         .toPromise().then(rec => {
275             this.context.record = new MarcRecord(rec.marc());
276             this.record.id = id;
277             this.record.deleted = rec.deleted() === 't';
278             if (idlClass === 'bre' && rec.source()) {
279                 this.sourceSelector.applyEntryId(+rec.source());
280             }
281         });
282     }
283
284     fromXml(xml: string) {
285         this.context.record = new MarcRecord(xml);
286         this.record.id = null;
287     }
288
289     deleteRecord(): Promise<any> {
290
291         return this.confirmDelete.open().toPromise()
292         .then(yes => {
293             if (!yes) { return; }
294
295             let promise;
296             if (this.recordType === 'authority') {
297                 promise = this.deleteAuthorityRecord();
298             } else {
299                 promise = this.deleteBibRecord();
300             }
301
302             return promise.then(ok => {
303                 if (!ok) { return; }
304
305                 return this.fromId(this.record.id).then(_ => {
306                     this.recordSaved.emit({
307                         marcXml: this.record.toXml(),
308                         recordId: this.recordId
309                     });
310                 });
311             });
312         });
313     }
314
315     deleteAuthorityRecord(): Promise<boolean> {
316         return this.pcrud.retrieve('are', this.record.id).toPromise()
317         .then(rec => this.pcrud.remove(rec).toPromise())
318         .then(resp => resp !== null);
319     }
320
321     deleteBibRecord(): Promise<boolean> {
322
323         return this.net.request('open-ils.cat',
324             'open-ils.cat.biblio.record_entry.delete',
325             this.auth.token(), this.record.id).toPromise()
326
327         .then(resp => {
328
329             const evt = this.evt.parse(resp);
330             if (evt) {
331                 if (evt.textcode === 'RECORD_NOT_EMPTY') {
332                     return this.cannotDelete.open().toPromise()
333                     .then(_ => false);
334                 } else {
335                     console.error(evt);
336                     alert(evt);
337                     return false;
338                 }
339             }
340
341             return true;
342         });
343     }
344
345     undeleteRecord(): Promise<any> {
346
347         return this.confirmUndelete.open().toPromise()
348         .then(yes => {
349             if (!yes) { return; }
350
351             let promise;
352             if (this.recordType === 'authority') {
353                 promise = this.undeleteAuthorityRecord();
354             } else {
355                 promise = this.undeleteBibRecord();
356             }
357
358             return promise.then(ok => {
359                 if (!ok) { return; }
360                 return this.fromId(this.record.id)
361                 .then(_ => {
362                     this.recordSaved.emit({
363                         marcXml: this.record.toXml(),
364                         recordId: this.recordId
365                     });
366                 });
367             });
368         });
369     }
370
371     undeleteAuthorityRecord(): Promise<any> {
372         return this.pcrud.retrieve('are', this.record.id).toPromise()
373         .then(rec => {
374             rec.deleted('f');
375             return this.pcrud.update(rec).toPromise();
376         }).then(resp => resp !== null);
377     }
378
379     undeleteBibRecord(): Promise<any> {
380
381         return this.net.request('open-ils.cat',
382             'open-ils.cat.biblio.record_entry.undelete',
383             this.auth.token(), this.record.id).toPromise()
384
385         .then(resp => {
386
387             const evt = this.evt.parse(resp);
388             if (evt) {
389                 console.error(evt);
390                 alert(evt);
391                 return false;
392             }
393
394             return true;
395         });
396     }
397
398     // Spawns the copy editor with the requested barcode and
399     // call number label.  Called after our record is saved.
400     fastAdd() {
401         if (this.showFastAdd && this.fastItemLabel && this.fastItemBarcode) {
402
403             const fastItem = {
404                 label: this.fastItemLabel,
405                 barcode: this.fastItemBarcode,
406                 fast_add: true
407             };
408
409             this.holdings.spawnAddHoldingsUi(this.recordId, null, [fastItem]);
410         }
411     }
412 }
413