import {CnBrowseComponent} from './cnbrowse.component';
import {CnBrowseResultsComponent} from './cnbrowse/results.component';
import {SearchTemplatesComponent} from './search-templates.component';
+import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
@NgModule({
declarations: [
CatalogRoutingModule,
HoldsModule,
HoldingsModule,
- BookingModule
+ BookingModule,
+ MarcEditModule
],
providers: [
StaffCatalogService
<!-- NOTE some tabs send the user over to the AngJS app -->
<ngb-tab title="MARC Edit" i18n-title id="marc_edit">
<ng-template ngbTabContent>
- <div class="alert alert-info mt-3" i18n>
- MARC Edit not yet implemented. See the
- <a target="_blank"
- href="/eg/staff/cat/catalog/record/{{recordId}}/marc_edit">
- AngularJS MARC Edit Tab.
- </a>
+ <div class="mt-3">
+ <eg-marc-editor (recordSaved)="handleMarcRecordSaved()"
+ [recordId]="recordId"></eg-marc-editor>
</div>
</ng-template>
</ngb-tab>
}
return null;
}
+
+ handleMarcRecordSaved() {
+ this.staffCat.currentDetailRecordSummary = null;
+ this.loadRecord();
+ }
}
--- /dev/null
+
+<eg-confirm-dialog #confirmDelete
+ i18n-dialogTitle dialogTitle="Confirm Delete"
+ i18n-dialogBody dialogBody="Delete Record ID {{record ? record.id : ''}}?">
+</eg-confirm-dialog>
+
+<eg-confirm-dialog #confirmUndelete
+ i18n-dialogTitle dialogTitle="Confirm Undelete"
+ i18n-dialogBody dialogBody="Undelete Record ID {{record ? record.id : ''}}?">
+</eg-confirm-dialog>
+
+<eg-alert-dialog #cannotDelete
+ i18n-dialogBody
+ dialogBody="Records with holdings attached cannot be deleted.">
+</eg-alert-dialog>
+
+<div class="row d-flex p-2 m-2">
+ <div class="flex-1"></div>
+ <div class="mr-2">
+ <eg-combobox #sourceSelector
+ [entries]="sources"
+ placeholder="Select a Source..."
+ i18n-placeholder>
+ </eg-combobox>
+ </div>
+
+ <ng-container *ngIf="record && record.id">
+ <button *ngIf="!record.deleted" class="btn btn-warning"
+ (click)="deleteRecord()" i18n>Delete Record</button>
+ <button *ngIf="record.deleted" class="btn btn-info"
+ (click)="undeleteRecord()" i18n>Undelete Record</button>
+ </ng-container>
+
+ <button class="btn btn-success ml-2" (click)="saveRecord()"
+ [disabled]="record && record.deleted" i18n>Save Changes</button>
+</div>
+
+<div class="row">
+ <div class="col-lg-12">
+ <ngb-tabset [activeId]="editorTab">
+ <ngb-tab title="Enhanced MARC Editor" i18n-title id="rich">
+ <ng-template ngbTabContent>
+ <div class="alert alert-info mt-3" i18n>
+ Enhanced MARC Editor is not yet implemented. See the
+ <ng-container *ngIf="record && record.id">
+ <a target="_blank"
+ href="/eg/staff/cat/catalog/record/{{record.id}}/marc_edit">
+ AngularJS MARC Editor.
+ </a>
+ </ng-container>
+ <ng-container *ngIf="!record || !record.id">
+ <a target="_blank" href="/eg/staff/cat/catalog/new_bib">
+ AngularJS MARC Editor.
+ </a>
+ </ng-container>
+ </div>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="Flat Text Editor" i18n-title id="flat">
+ <ng-template ngbTabContent>
+ <eg-marc-flat-editor></eg-marc-flat-editor>
+ </ng-template>
+ </ngb-tab>
+ </ngb-tabset>
+ </div>
+</div>
+
+<div class="row d-flex p-2 m-2 flex-row-reverse">
+ <button class="btn btn-success" (click)="saveRecord()"
+ [disabled]="record && record.deleted" i18n>Save Changes</button>
+</div>
+
+
--- /dev/null
+import {Component, Input, Output, OnInit, EventEmitter, ViewChild} from '@angular/core';
+import {IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {MarcRecord} from './marcrecord';
+import {ComboboxEntry, ComboboxComponent
+ } from '@eg/share/combobox/combobox.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+
+
+/**
+ * MARC Record editor main interface.
+ */
+
+@Component({
+ selector: 'eg-marc-editor',
+ templateUrl: './editor.component.html'
+})
+
+export class MarcEditorComponent implements OnInit {
+
+ record: MarcRecord;
+ editorTab: 'rich' | 'flat';
+ sources: ComboboxEntry[];
+
+ @Input() set recordId(id: number) {
+ if (!id) { return; }
+ if (this.record && this.record.id === id) { return; }
+ this.fromId(id);
+ }
+
+ @Input() set recordXml(xml: string) {
+ if (xml) { this.fromXml(xml); }
+ }
+
+ // If true, saving records to the database is assumed to
+ // happen externally. IOW, the record editor is just an
+ // in-place MARC modification interface.
+ inPlaceMode: boolean;
+
+ // In inPlaceMode, this is emitted in lieu of saving the record
+ // in th database. When inPlaceMode is false, this is emitted after
+ // the record is successfully saved.
+ @Output() recordSaved: EventEmitter<string>;
+
+ @ViewChild('sourceSelector') sourceSelector: ComboboxComponent;
+ @ViewChild('confirmDelete') confirmDelete: ConfirmDialogComponent;
+ @ViewChild('confirmUndelete') confirmUndelete: ConfirmDialogComponent;
+ @ViewChild('cannotDelete') cannotDelete: ConfirmDialogComponent;
+
+ constructor(
+ private evt: EventService,
+ private idl: IdlService,
+ private net: NetService,
+ private auth: AuthService,
+ private org: OrgService,
+ private pcrud: PcrudService
+ ) {
+ this.sources = [];
+ this.recordSaved = new EventEmitter<string>();
+ }
+
+ ngOnInit() {
+ // Default to flat for now since it's all that's supported.
+ this.editorTab = 'flat';
+
+ this.pcrud.retrieveAll('cbs').subscribe(
+ src => this.sources.push({id: +src.id(), label: src.source()}),
+ _ => {},
+ () => {
+ this.sources = this.sources.sort((a, b) =>
+ a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
+ );
+ }
+ );
+ }
+
+ saveRecord(): Promise<any> {
+ const xml = this.record.toXml();
+
+ if (this.inPlaceMode) {
+ // Let the caller have the modified XML and move on.
+ this.recordSaved.emit(xml);
+ return Promise.resolve();
+ }
+
+ const source = this.sourceSelector.selected ?
+ this.sourceSelector.selected.label : null; // 'label' not a typo
+
+ if (this.record.id) { // Editing an existing record
+
+ const method = 'open-ils.cat.biblio.record.marc.replace';
+
+ return this.net.request('open-ils.cat', method,
+ this.auth.token(), this.record.id, xml, source
+ ).toPromise().then(response => {
+
+ const evt = this.evt.parse(response);
+ if (evt) {
+ console.error(evt);
+ // TODO: toast
+ }
+
+ // TODO: toast
+ this.recordSaved.emit(xml);
+ return response;
+ });
+
+ } else {
+ // TODO: create a new record
+ }
+ }
+
+ fromId(id: number): Promise<any> {
+ return this.pcrud.retrieve('bre', id)
+ .toPromise().then(bib => {
+ this.record = new MarcRecord(bib.marc());
+ this.record.id = id;
+ this.record.deleted = bib.deleted() === 't';
+ if (bib.source()) {
+ this.sourceSelector.applyEntryId(+bib.source());
+ }
+ });
+ }
+
+ fromXml(xml: string) {
+ this.record = new MarcRecord(xml);
+ this.record.id = null;
+ }
+
+ deleteRecord(): Promise<any> {
+
+ return this.confirmDelete.open().toPromise()
+ .then(yes => {
+ if (!yes) { return; }
+
+ return this.net.request('open-ils.cat',
+ 'open-ils.cat.biblio.record_entry.delete',
+ this.auth.token(), this.record.id).toPromise()
+
+ .then(resp => {
+
+ const evt = this.evt.parse(resp);
+ if (evt) {
+ if (evt.textcode === 'RECORD_NOT_EMPTY') {
+ return this.cannotDelete.open().toPromise();
+ } else {
+ console.error(evt);
+ return alert(evt);
+ }
+ }
+ return this.fromId(this.record.id)
+ .then(_ => this.recordSaved.emit(this.record.toXml()));
+ });
+ });
+ }
+
+ undeleteRecord(): Promise<any> {
+
+ return this.confirmUndelete.open().toPromise()
+ .then(yes => {
+ if (!yes) { return; }
+
+ return this.net.request('open-ils.cat',
+ 'open-ils.cat.biblio.record_entry.undelete',
+ this.auth.token(), this.record.id).toPromise()
+
+ .then(resp => {
+
+ const evt = this.evt.parse(resp);
+ if (evt) { console.error(evt); return alert(evt); }
+
+ return this.fromId(this.record.id)
+ .then(_ => this.recordSaved.emit(this.record.toXml()));
+ });
+ });
+ }
+}
+
--- /dev/null
+
+
+.flat-editor-content {
+ font-family: 'Lucida Console', Monaco, monospace;
+ display: inline-block;
+ /*
+ min-width: 1ch;
+ margin: 0 -1px;
+ */
+ padding: 0;
+}
--- /dev/null
+
+<div *ngIf="record">
+ <textarea class="form-control flat-editor-content"
+ (blur)="record.absorbBreakerChanges()"
+ [(ngModel)]="record.breakerText" rows="{{rowCount()}}" spellcheck="false">
+ </textarea>
+</div>
--- /dev/null
+import {Component, Input, OnInit, Host} from '@angular/core';
+import {IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {MarcEditorComponent} from './editor.component';
+import {MarcRecord} from './marcrecord';
+
+/**
+ * MARC Record flat text (marc-breaker) editor.
+ */
+
+@Component({
+ selector: 'eg-marc-flat-editor',
+ templateUrl: './flat-editor.component.html',
+ styleUrls: ['flat-editor.component.css']
+})
+
+export class MarcFlatEditorComponent implements OnInit {
+
+ get record(): MarcRecord {
+ return this.editor.record;
+ }
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService,
+ private store: ServerStoreService,
+ @Host() private editor: MarcEditorComponent
+ ) {
+ }
+
+ ngOnInit() {}
+
+ // When we have breaker text, limit the vertical expansion of the
+ // text area to the size of the data plus a little padding.
+ rowCount(): number {
+ if (this.record && this.record.breakerText) {
+ return this.record.breakerText.split(/\n/).length + 2;
+ }
+ return 40;
+ }
+}
+
+
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {MarcEditorComponent} from './editor.component';
+import {MarcRichEditorComponent} from './rich-editor.component';
+import {MarcFlatEditorComponent} from './flat-editor.component';
+
+@NgModule({
+ declarations: [
+ MarcEditorComponent,
+ MarcRichEditorComponent,
+ MarcFlatEditorComponent
+ ],
+ imports: [
+ StaffCommonModule
+ ],
+ exports: [
+ MarcEditorComponent
+ ],
+ providers: [
+ ]
+})
+
+export class MarcEditModule { }
+
--- /dev/null
+/**
+ * Simple wrapper class for our external MARC21.Record JS library.
+ */
+
+declare var MARC21;
+
+export class MarcRecord {
+
+ id: number; // Database ID when known.
+ deleted: boolean;
+ record: any; // MARC21.Record object
+ breakerText: string;
+
+ constructor(xml: string) {
+ this.record = new MARC21.Record({marcxml: xml});
+ this.breakerText = this.record.toBreaker();
+ }
+
+ toXml(): string {
+ return this.record.toXmlString();
+ }
+
+ toBreaker(): string {
+ return this.record.toBreaker();
+ }
+
+ absorbBreakerChanges() {
+ this.record = new MARC21.Record({marcbreaker: this.breakerText});
+ }
+}
+
--- /dev/null
+import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
+ OnDestroy} from '@angular/core';
+import {IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+
+/**
+ * MARC Record rich editor interface.
+ */
+
+@Component({
+ selector: 'eg-marc-rich-editor',
+ templateUrl: './rich-editor.component.html',
+ styleUrls: ['rich-editor.component.css']
+})
+
+export class MarcRichEditorComponent implements OnInit {
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService
+ ) {
+ }
+
+ ngOnInit() {}
+}
+
+
+