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';
19 export interface MarcSavedEvent {
26 * MARC Record editor main interface.
30 selector: 'eg-marc-editor',
31 templateUrl: './editor.component.html'
34 export class MarcEditorComponent implements OnInit {
36 editorTab: 'rich' | 'flat';
37 sources: ComboboxEntry[];
38 context: MarcEditContext;
40 // True if the save request is in flight
43 @Input() recordType: 'biblio' | 'authority' = 'biblio';
45 _pendingRecordId: number;
46 @Input() set recordId(id: number) {
47 if (this.record && this.record.id === id) { return; }
49 // Avoid fetching the record by ID before OnInit since we may
50 // not yet know our recordType.
51 if (this.initCalled) {
52 this._pendingRecordId = null;
56 // fetch later in OnInit
57 this._pendingRecordId = id;
61 get recordId(): number {
62 return this.record ? this.record.id : this._pendingRecordId;
65 @Input() set recordXml(xml: string) {
71 get record(): MarcRecord {
72 return this.context.record;
75 // Tell us which record source to select by default.
76 // Useful for new records and in-place editing from bare XML.
77 @Input() recordSource: number;
79 // If true, saving records to the database is assumed to
80 // happen externally. IOW, the record editor is just an
81 // in-place MARC modification interface.
82 @Input() inPlaceMode: boolean;
84 // In inPlaceMode, this is emitted in lieu of saving the record
85 // in th database. When inPlaceMode is false, this is emitted after
86 // the record is successfully saved.
87 @Output() recordSaved: EventEmitter<MarcSavedEvent>;
89 @ViewChild('sourceSelector', {static: false}) sourceSelector: ComboboxComponent;
90 @ViewChild('confirmDelete', {static: false}) confirmDelete: ConfirmDialogComponent;
91 @ViewChild('confirmUndelete', {static: false}) confirmUndelete: ConfirmDialogComponent;
92 @ViewChild('cannotDelete', {static: false}) cannotDelete: ConfirmDialogComponent;
93 @ViewChild('successMsg', {static: false}) successMsg: StringComponent;
94 @ViewChild('failMsg', {static: false}) failMsg: StringComponent;
96 fastItemLabel: string;
97 fastItemBarcode: string;
102 private evt: EventService,
103 private idl: IdlService,
104 private net: NetService,
105 private auth: AuthService,
106 private org: OrgService,
107 private pcrud: PcrudService,
108 private toast: ToastService,
109 private holdings: HoldingsService,
110 private store: ServerStoreService
113 this.recordSaved = new EventEmitter<MarcSavedEvent>();
114 this.context = new MarcEditContext();
116 this.recordSaved.subscribe(_ => this.dataSaving = false);
121 this.initCalled = true;
123 this.context.recordType = this.recordType;
125 this.store.getItem('cat.marcedit.flateditor').then(
126 useFlat => this.editorTab = useFlat ? 'flat' : 'rich');
128 if (!this.record && this.recordId) {
129 this.fromId(this.recordId);
132 if (this.recordType !== 'biblio') { return; }
134 this.pcrud.retrieveAll('cbs').subscribe(
135 src => this.sources.push({id: +src.id(), label: src.source()}),
138 this.sources = this.sources.sort((a, b) =>
139 a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
142 if (this.recordSource) {
143 this.sourceSelector.applyEntryId(this.recordSource);
149 changesPending(): boolean {
150 return this.context.changesPending;
153 clearPendingChanges() {
154 this.context.changesPending = false;
157 // Remember the last used tab as the preferred tab.
158 tabChange(evt: NgbTabChangeEvent) {
160 // Avoid undo persistence across tabs since that could result
161 // in changes getting lost.
162 this.context.resetUndos();
164 if (evt.nextId === 'flat') {
165 this.store.setItem('cat.marcedit.flateditor', true);
167 this.store.removeItem('cat.marcedit.flateditor');
171 saveRecord(): Promise<any> {
172 const xml = this.record.toXml();
173 this.dataSaving = true;
175 // Save actions clears any pending changes.
176 this.context.changesPending = false;
177 this.context.resetUndos();
179 let sourceName: string = null;
180 let sourceId: number = null;
182 if (this.sourceSelector && this.sourceSelector.selected) {
183 sourceName = this.sourceSelector.selected.label;
184 sourceId = this.sourceSelector.selected.id;
188 marcXml: xml, bibSource: sourceId, recordId: this.recordId};
190 if (this.inPlaceMode) {
191 // Let the caller have the modified XML and move on.
192 this.recordSaved.emit(emission);
193 return Promise.resolve();
198 if (this.record.id) { // Editing an existing record
200 promise = this.modifyRecord(xml, sourceName, sourceId);
204 promise = this.createRecord(xml, sourceName);
207 // NOTE we do not reinitialize our record with the MARC returned
208 // from the server after a create/update, which means our record
209 // may be out of sync, e.g. missing 901* values. It's the
210 // callers responsibility to tear us down and rebuild us.
211 return promise.then(marcXml => {
212 if (!marcXml) { return null; }
213 this.successMsg.current().then(msg => this.toast.success(msg));
214 emission.marcXml = marcXml;
215 emission.recordId = this.recordId;
216 this.recordSaved.emit(emission);
222 modifyRecord(marcXml: string, sourceName: string, sourceId: number): Promise<any> {
223 const method = this.recordType === 'biblio' ?
224 'open-ils.cat.biblio.record.xml.update' :
225 'open-ils.cat.authority.record.overlay';
227 return this.net.request('open-ils.cat', method,
228 this.auth.token(), this.record.id, marcXml, sourceName
230 ).toPromise().then(response => {
232 const evt = this.evt.parse(response);
235 this.failMsg.current().then(msg => this.toast.warning(msg));
236 this.dataSaving = false;
240 // authority.record.overlay resturns a '1' on success.
241 return typeof response === 'object' ? response.marc() : marcXml;
245 createRecord(marcXml: string, sourceName?: string): Promise<any> {
247 const method = this.recordType === 'biblio' ?
248 'open-ils.cat.biblio.record.xml.create' :
249 'open-ils.cat.authority.record.import';
251 return this.net.request('open-ils.cat', method,
252 this.auth.token(), marcXml, sourceName
253 ).toPromise().then(response => {
255 const evt = this.evt.parse(response);
259 this.failMsg.current().then(msg => this.toast.warning(msg));
260 this.dataSaving = false;
264 this.record.id = response.id();
265 return response.marc();
269 fromId(id: number): Promise<any> {
270 const idlClass = this.recordType === 'authority' ? 'are' : 'bre';
272 return this.pcrud.retrieve(idlClass, id)
273 .toPromise().then(rec => {
274 this.context.record = new MarcRecord(rec.marc());
276 this.record.deleted = rec.deleted() === 't';
277 if (idlClass === 'bre' && rec.source()) {
278 this.sourceSelector.applyEntryId(+rec.source());
283 fromXml(xml: string) {
284 this.context.record = new MarcRecord(xml);
285 this.record.id = null;
288 deleteRecord(): Promise<any> {
290 return this.confirmDelete.open().toPromise()
292 if (!yes) { return; }
295 if (this.recordType === 'authority') {
296 promise = this.deleteAuthorityRecord();
298 promise = this.deleteBibRecord();
301 return promise.then(ok => {
304 return this.fromId(this.record.id).then(_ => {
305 this.recordSaved.emit({
306 marcXml: this.record.toXml(),
307 recordId: this.recordId
314 deleteAuthorityRecord(): Promise<boolean> {
315 return this.pcrud.retrieve('are', this.record.id).toPromise()
316 .then(rec => this.pcrud.remove(rec).toPromise())
317 .then(resp => resp !== null);
320 deleteBibRecord(): Promise<boolean> {
322 return this.net.request('open-ils.cat',
323 'open-ils.cat.biblio.record_entry.delete',
324 this.auth.token(), this.record.id).toPromise()
328 const evt = this.evt.parse(resp);
330 if (evt.textcode === 'RECORD_NOT_EMPTY') {
331 return this.cannotDelete.open().toPromise()
344 undeleteRecord(): Promise<any> {
346 return this.confirmUndelete.open().toPromise()
348 if (!yes) { return; }
351 if (this.recordType === 'authority') {
352 promise = this.undeleteAuthorityRecord();
354 promise = this.undeleteBibRecord();
357 return promise.then(ok => {
359 return this.fromId(this.record.id)
361 this.recordSaved.emit({
362 marcXml: this.record.toXml(),
363 recordId: this.recordId
370 undeleteAuthorityRecord(): Promise<any> {
371 return this.pcrud.retrieve('are', this.record.id).toPromise()
374 return this.pcrud.update(rec).toPromise();
375 }).then(resp => resp !== null);
378 undeleteBibRecord(): Promise<any> {
380 return this.net.request('open-ils.cat',
381 'open-ils.cat.biblio.record_entry.undelete',
382 this.auth.token(), this.record.id).toPromise()
386 const evt = this.evt.parse(resp);
397 // Spawns the copy editor with the requested barcode and
398 // call number label. Called after our record is saved.
400 if (this.showFastAdd && this.fastItemLabel && this.fastItemBarcode) {
403 label: this.fastItemLabel,
404 barcode: this.fastItemBarcode,
408 this.holdings.spawnAddHoldingsUi(this.recordId, null, [fastItem]);