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