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';
20 export interface MarcSavedEvent {
27 * MARC Record editor main interface.
31 selector: 'eg-marc-editor',
32 templateUrl: './editor.component.html'
35 export class MarcEditorComponent implements OnInit {
37 editorTab: 'rich' | 'flat';
38 sources: ComboboxEntry[];
39 context: MarcEditContext;
41 // True if the save request is in flight
44 @Input() recordType: MARC_RECORD_TYPE = 'biblio';
46 _pendingRecordId: number;
47 @Input() set recordId(id: number) {
48 if (this.record && this.record.id === id) { return; }
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;
57 // fetch later in OnInit
58 this._pendingRecordId = id;
62 get recordId(): number {
63 return this.record ? this.record.id : this._pendingRecordId;
66 @Input() set recordXml(xml: string) {
72 get record(): MarcRecord {
73 return this.context.record;
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;
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;
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>;
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;
97 fastItemLabel: string;
98 fastItemBarcode: string;
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
114 this.recordSaved = new EventEmitter<MarcSavedEvent>();
115 this.context = new MarcEditContext();
117 this.recordSaved.subscribe(_ => this.dataSaving = false);
122 this.initCalled = true;
124 this.context.recordType = this.recordType;
126 this.store.getItem('cat.marcedit.flateditor').then(
127 useFlat => this.editorTab = useFlat ? 'flat' : 'rich');
129 if (!this.record && this.recordId) {
130 this.fromId(this.recordId);
134 changesPending(): boolean {
135 return this.context.changesPending;
138 clearPendingChanges() {
139 this.context.changesPending = false;
142 // Remember the last used tab as the preferred tab.
143 tabChange(evt: NgbNavChangeEvent) {
145 // Avoid undo persistence across tabs since that could result
146 // in changes getting lost.
147 this.context.resetUndos();
149 if (evt.nextId === 'flat') {
150 this.store.setItem('cat.marcedit.flateditor', true);
152 this.store.removeItem('cat.marcedit.flateditor');
156 saveRecord(): Promise<any> {
157 const xml = this.record.toXml();
158 this.dataSaving = true;
160 // Save actions clears any pending changes.
161 this.context.changesPending = false;
162 this.context.resetUndos();
164 let sourceName: string = null;
165 let sourceId: number = null;
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
170 if (this.sourceSelector && this.sourceSelector.selected) {
171 sourceName = this.sourceSelector.selected.label;
172 sourceId = this.sourceSelector.selected.id;
176 marcXml: xml, bibSource: sourceId, recordId: this.recordId};
178 if (this.inPlaceMode) {
179 // Let the caller have the modified XML and move on.
180 this.recordSaved.emit(emission);
181 return Promise.resolve();
186 if (this.record.id) { // Editing an existing record
188 promise = this.modifyRecord(xml, sourceName, sourceId);
192 promise = this.createRecord(xml, sourceName);
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);
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';
215 return this.net.request('open-ils.cat', method,
216 this.auth.token(), this.record.id, marcXml, sourceName
218 ).toPromise().then(response => {
220 const evt = this.evt.parse(response);
223 this.failMsg.current().then(msg => this.toast.warning(msg));
224 this.dataSaving = false;
228 // authority.record.overlay resturns a '1' on success.
229 return typeof response === 'object' ? response.marc() : marcXml;
233 createRecord(marcXml: string, sourceName?: string): Promise<any> {
235 const method = this.recordType === 'biblio' ?
236 'open-ils.cat.biblio.record.xml.create' :
237 'open-ils.cat.authority.record.import';
239 return this.net.request('open-ils.cat', method,
240 this.auth.token(), marcXml, sourceName
241 ).toPromise().then(response => {
243 const evt = this.evt.parse(response);
247 this.failMsg.current().then(msg => this.toast.warning(msg));
248 this.dataSaving = false;
252 this.record.id = response.id();
253 return response.marc();
257 fromId(id: number): Promise<any> {
258 const idlClass = this.recordType === 'authority' ? 'are' : 'bre';
260 return this.pcrud.retrieve(idlClass, id)
261 .toPromise().then(rec => {
262 this.context.record = new MarcRecord(rec.marc());
264 this.record.deleted = rec.deleted() === 't';
265 if (idlClass === 'bre') {
266 this.recordSource = +rec.source();
271 updateRecordSource(entry) {
272 this.recordSource = entry.id;
275 fromXml(xml: string) {
276 this.context.record = new MarcRecord(xml);
277 this.record.id = null;
280 deleteRecord(): Promise<any> {
282 return this.confirmDelete.open().toPromise()
284 if (!yes) { return; }
287 if (this.recordType === 'authority') {
288 promise = this.deleteAuthorityRecord();
290 promise = this.deleteBibRecord();
293 return promise.then(ok => {
296 return this.fromId(this.record.id).then(_ => {
297 this.recordSaved.emit({
298 marcXml: this.record.toXml(),
299 recordId: this.recordId
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);
312 deleteBibRecord(): Promise<boolean> {
314 return this.net.request('open-ils.cat',
315 'open-ils.cat.biblio.record_entry.delete',
316 this.auth.token(), this.record.id).toPromise()
320 const evt = this.evt.parse(resp);
322 if (evt.textcode === 'RECORD_NOT_EMPTY') {
323 return this.cannotDelete.open().toPromise()
336 undeleteRecord(): Promise<any> {
338 return this.confirmUndelete.open().toPromise()
340 if (!yes) { return; }
343 if (this.recordType === 'authority') {
344 promise = this.undeleteAuthorityRecord();
346 promise = this.undeleteBibRecord();
349 return promise.then(ok => {
351 return this.fromId(this.record.id)
353 this.recordSaved.emit({
354 marcXml: this.record.toXml(),
355 recordId: this.recordId
362 undeleteAuthorityRecord(): Promise<any> {
363 return this.pcrud.retrieve('are', this.record.id).toPromise()
366 return this.pcrud.update(rec).toPromise();
367 }).then(resp => resp !== null);
370 undeleteBibRecord(): Promise<any> {
372 return this.net.request('open-ils.cat',
373 'open-ils.cat.biblio.record_entry.undelete',
374 this.auth.token(), this.record.id).toPromise()
378 const evt = this.evt.parse(resp);
389 // Spawns the copy editor with the requested barcode and
390 // call number label. Called after our record is saved.
392 if (this.showFastAdd && this.fastItemLabel && this.fastItemBarcode) {
395 label: this.fastItemLabel,
396 barcode: this.fastItemBarcode,
400 this.holdings.spawnAddHoldingsUi(this.recordId, null, [fastItem]);