]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.ts
LP 2061136 follow-up: ng lint --fix
[working/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
134     changesPending(): boolean {
135         return this.context.changesPending;
136     }
137
138     clearPendingChanges() {
139         this.context.changesPending = false;
140     }
141
142     // Remember the last used tab as the preferred tab.
143     tabChange(evt: NgbNavChangeEvent) {
144
145         // Avoid undo persistence across tabs since that could result
146         // in changes getting lost.
147         this.context.resetUndos();
148
149         if (evt.nextId === 'flat') {
150             this.store.setItem('cat.marcedit.flateditor', true);
151         } else {
152             this.store.removeItem('cat.marcedit.flateditor');
153         }
154     }
155
156     saveRecord(): Promise<any> {
157         const xml = this.record.toXml();
158         this.dataSaving = true;
159
160         // Save actions clears any pending changes.
161         this.context.changesPending = false;
162         this.context.resetUndos();
163
164         let sourceName: string = null;
165         let sourceId: number = null;
166
167         // We don't just use this.recordSource here because we need
168         // to use the name (label) for an API call.  That seems like
169         // an API bug...
170         if (this.sourceSelector && this.sourceSelector.selected) {
171             sourceName = this.sourceSelector.selected.label;
172             sourceId = this.sourceSelector.selected.id;
173         }
174
175         const emission = {
176             marcXml: xml, bibSource: sourceId, recordId: this.recordId};
177
178         if (this.inPlaceMode) {
179             // Let the caller have the modified XML and move on.
180             this.recordSaved.emit(emission);
181             return Promise.resolve();
182         }
183
184         let promise;
185
186         if (this.record.id) { // Editing an existing record
187
188             promise = this.modifyRecord(xml, sourceName, sourceId);
189
190         } else {
191
192             promise = this.createRecord(xml, sourceName);
193         }
194
195         // NOTE we do not reinitialize our record with the MARC returned
196         // from the server after a create/update, which means our record
197         // may be out of sync, e.g. missing 901* values.  It's the
198         // callers responsibility to tear us down and rebuild us.
199         return promise.then(marcXml => {
200             if (!marcXml) { return null; }
201             this.successMsg.current().then(msg => this.toast.success(msg));
202             emission.marcXml = marcXml;
203             emission.recordId = this.recordId;
204             this.recordSaved.emit(emission);
205             this.fastAdd();
206             return marcXml;
207         });
208     }
209
210     modifyRecord(marcXml: string, sourceName: string, sourceId: number): Promise<any> {
211         const method = this.recordType === 'biblio' ?
212             'open-ils.cat.biblio.record.xml.update' :
213             'open-ils.cat.authority.record.overlay';
214
215         return this.net.request('open-ils.cat', method,
216             this.auth.token(), this.record.id, marcXml, sourceName
217
218         ).toPromise().then(response => {
219
220             const evt = this.evt.parse(response);
221             if (evt) {
222                 console.error(evt);
223                 this.failMsg.current().then(msg => this.toast.warning(msg));
224                 this.dataSaving = false;
225                 return null;
226             }
227
228             // authority.record.overlay resturns a '1' on success.
229             return typeof response === 'object' ? response.marc() : marcXml;
230         });
231     }
232
233     createRecord(marcXml: string, sourceName?: string): Promise<any> {
234
235         const method = this.recordType === 'biblio' ?
236             'open-ils.cat.biblio.record.xml.create' :
237             'open-ils.cat.authority.record.import';
238
239         return this.net.request('open-ils.cat', method,
240             this.auth.token(), marcXml, sourceName
241         ).toPromise().then(response => {
242
243             const evt = this.evt.parse(response);
244
245             if (evt) {
246                 console.error(evt);
247                 this.failMsg.current().then(msg => this.toast.warning(msg));
248                 this.dataSaving = false;
249                 return null;
250             }
251
252             this.record.id = response.id();
253             return response.marc();
254         });
255     }
256
257     fromId(id: number): Promise<any> {
258         const idlClass = this.recordType === 'authority' ? 'are' : 'bre';
259
260         return this.pcrud.retrieve(idlClass, id)
261             .toPromise().then(rec => {
262                 this.context.record = new MarcRecord(rec.marc());
263                 this.record.id = id;
264                 this.record.deleted = rec.deleted() === 't';
265                 if (idlClass === 'bre') {
266                     this.recordSource = +rec.source();
267                 }
268             });
269     }
270
271     updateRecordSource(entry) {
272         this.recordSource = entry.id;
273     }
274
275     fromXml(xml: string) {
276         this.context.record = new MarcRecord(xml);
277         this.record.id = null;
278     }
279
280     deleteRecord(): Promise<any> {
281
282         return this.confirmDelete.open().toPromise()
283             .then(yes => {
284                 if (!yes) { return; }
285
286                 let promise;
287                 if (this.recordType === 'authority') {
288                     promise = this.deleteAuthorityRecord();
289                 } else {
290                     promise = this.deleteBibRecord();
291                 }
292
293                 return promise.then(ok => {
294                     if (!ok) { return; }
295
296                     return this.fromId(this.record.id).then(_ => {
297                         this.recordSaved.emit({
298                             marcXml: this.record.toXml(),
299                             recordId: this.recordId
300                         });
301                     });
302                 });
303             });
304     }
305
306     deleteAuthorityRecord(): Promise<boolean> {
307         return this.pcrud.retrieve('are', this.record.id).toPromise()
308             .then(rec => this.pcrud.remove(rec).toPromise())
309             .then(resp => resp !== null);
310     }
311
312     deleteBibRecord(): Promise<boolean> {
313
314         return this.net.request('open-ils.cat',
315             'open-ils.cat.biblio.record_entry.delete',
316             this.auth.token(), this.record.id).toPromise()
317
318             .then(resp => {
319
320                 const evt = this.evt.parse(resp);
321                 if (evt) {
322                     if (evt.textcode === 'RECORD_NOT_EMPTY') {
323                         return this.cannotDelete.open().toPromise()
324                             .then(_ => false);
325                     } else {
326                         console.error(evt);
327                         alert(evt);
328                         return false;
329                     }
330                 }
331
332                 return true;
333             });
334     }
335
336     undeleteRecord(): Promise<any> {
337
338         return this.confirmUndelete.open().toPromise()
339             .then(yes => {
340                 if (!yes) { return; }
341
342                 let promise;
343                 if (this.recordType === 'authority') {
344                     promise = this.undeleteAuthorityRecord();
345                 } else {
346                     promise = this.undeleteBibRecord();
347                 }
348
349                 return promise.then(ok => {
350                     if (!ok) { return; }
351                     return this.fromId(this.record.id)
352                         .then(_ => {
353                             this.recordSaved.emit({
354                                 marcXml: this.record.toXml(),
355                                 recordId: this.recordId
356                             });
357                         });
358                 });
359             });
360     }
361
362     undeleteAuthorityRecord(): Promise<any> {
363         return this.pcrud.retrieve('are', this.record.id).toPromise()
364             .then(rec => {
365                 rec.deleted('f');
366                 return this.pcrud.update(rec).toPromise();
367             }).then(resp => resp !== null);
368     }
369
370     undeleteBibRecord(): Promise<any> {
371
372         return this.net.request('open-ils.cat',
373             'open-ils.cat.biblio.record_entry.undelete',
374             this.auth.token(), this.record.id).toPromise()
375
376             .then(resp => {
377
378                 const evt = this.evt.parse(resp);
379                 if (evt) {
380                     console.error(evt);
381                     alert(evt);
382                     return false;
383                 }
384
385                 return true;
386             });
387     }
388
389     // Spawns the copy editor with the requested barcode and
390     // call number label.  Called after our record is saved.
391     fastAdd() {
392         if (this.showFastAdd && this.fastItemLabel && this.fastItemBarcode) {
393
394             const fastItem = {
395                 label: this.fastItemLabel,
396                 barcode: this.fastItemBarcode,
397                 fast_add: true
398             };
399
400             this.holdings.spawnAddHoldingsUi(this.recordId, null, [fastItem]);
401         }
402     }
403 }
404