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';
18 interface MarcSavedEvent {
25 * MARC Record editor main interface.
29 selector: 'eg-marc-editor',
30 templateUrl: './editor.component.html'
33 export class MarcEditorComponent implements OnInit {
35 editorTab: 'rich' | 'flat';
36 sources: ComboboxEntry[];
37 context: MarcEditContext;
39 // True if the save request is in flight
42 @Input() recordType: 'biblio' | 'authority' = 'biblio';
44 @Input() set recordId(id: number) {
46 if (this.record && this.record.id === id) { return; }
50 get recordId(): number {
51 return this.record ? this.record.id : null;
54 @Input() set recordXml(xml: string) {
60 get record(): MarcRecord {
61 return this.context.record;
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;
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;
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>;
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;
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
96 this.recordSaved = new EventEmitter<MarcSavedEvent>();
97 this.context = new MarcEditContext();
99 this.recordSaved.subscribe(_ => this.dataSaving = false);
104 this.context.recordType = this.recordType;
106 this.store.getItem('cat.marcedit.flateditor').then(
107 useFlat => this.editorTab = useFlat ? 'flat' : 'rich');
109 if (this.recordType !== 'biblio') { return; }
111 this.pcrud.retrieveAll('cbs').subscribe(
112 src => this.sources.push({id: +src.id(), label: src.source()}),
115 this.sources = this.sources.sort((a, b) =>
116 a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
119 if (this.recordSource) {
120 this.sourceSelector.applyEntryId(this.recordSource);
126 changesPending(): boolean {
127 return this.context.changesPending;
130 clearPendingChanges() {
131 this.context.changesPending = false;
134 // Remember the last used tab as the preferred tab.
135 tabChange(evt: NgbTabChangeEvent) {
137 // Avoid undo persistence across tabs since that could result
138 // in changes getting lost.
139 this.context.resetUndos();
141 if (evt.nextId === 'flat') {
142 this.store.setItem('cat.marcedit.flateditor', true);
144 this.store.removeItem('cat.marcedit.flateditor');
148 saveRecord(): Promise<any> {
149 const xml = this.record.toXml();
150 this.dataSaving = true;
152 // Save actions clears any pending changes.
153 this.context.changesPending = false;
154 this.context.resetUndos();
156 let sourceName: string = null;
157 let sourceId: number = null;
159 if (this.sourceSelector && this.sourceSelector.selected) {
160 sourceName = this.sourceSelector.selected.label;
161 sourceId = this.sourceSelector.selected.id;
165 marcXml: xml, bibSource: sourceId, recordId: this.recordId};
167 if (this.inPlaceMode) {
168 // Let the caller have the modified XML and move on.
169 this.recordSaved.emit(emission);
170 return Promise.resolve();
175 if (this.record.id) { // Editing an existing record
177 promise = this.modifyRecord(xml, sourceName, sourceId);
181 promise = this.createRecord(xml, sourceName);
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);
198 modifyRecord(marcXml: string, sourceName: string, sourceId: number): Promise<any> {
199 const method = 'open-ils.cat.biblio.record.marc.replace';
201 return this.net.request('open-ils.cat', method,
202 this.auth.token(), this.record.id, marcXml, sourceName
204 ).toPromise().then(response => {
206 const evt = this.evt.parse(response);
209 this.failMsg.current().then(msg => this.toast.warning(msg));
210 this.dataSaving = false;
214 return response.marc();
218 createRecord(marcXml: string, sourceName?: string): Promise<any> {
220 const method = this.recordType === 'biblio' ?
221 'open-ils.cat.biblio.record.xml.create' :
222 'open-ils.cat.authority.record.import';
224 return this.net.request('open-ils.cat', method,
225 this.auth.token(), marcXml, sourceName
226 ).toPromise().then(response => {
228 const evt = this.evt.parse(response);
232 this.failMsg.current().then(msg => this.toast.warning(msg));
233 this.dataSaving = false;
237 this.record.id = response.id();
238 return response.marc();
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());
247 this.record.deleted = bib.deleted() === 't';
249 this.sourceSelector.applyEntryId(+bib.source());
254 fromXml(xml: string) {
255 this.context.record = new MarcRecord(xml);
256 this.record.id = null;
259 deleteRecord(): Promise<any> {
261 return this.confirmDelete.open().toPromise()
263 if (!yes) { return; }
265 return this.net.request('open-ils.cat',
266 'open-ils.cat.biblio.record_entry.delete',
267 this.auth.token(), this.record.id).toPromise()
271 const evt = this.evt.parse(resp);
273 if (evt.textcode === 'RECORD_NOT_EMPTY') {
274 return this.cannotDelete.open().toPromise();
280 return this.fromId(this.record.id)
281 .then(_ => this.recordSaved.emit(
282 {marcXml: this.record.toXml(), recordId: this.recordId}));
287 undeleteRecord(): Promise<any> {
289 return this.confirmUndelete.open().toPromise()
291 if (!yes) { return; }
293 return this.net.request('open-ils.cat',
294 'open-ils.cat.biblio.record_entry.undelete',
295 this.auth.token(), this.record.id).toPromise()
299 const evt = this.evt.parse(resp);
300 if (evt) { console.error(evt); return alert(evt); }
302 return this.fromId(this.record.id)
303 .then(_ => this.recordSaved.emit(
304 {marcXml: this.record.toXml(), recordId: this.recordId}));