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