LP1834665 Angular catalog MARC flat text editor
authorBill Erickson <berickxx@gmail.com>
Fri, 28 Jun 2019 16:29:07 +0000 (12:29 -0400)
committerBill Erickson <berickxx@gmail.com>
Mon, 26 Aug 2019 16:02:36 +0000 (12:02 -0400)
Adds a set of components for editing MARC records.  The main component
acts as a container with various actions (source selector, delete,
undelete, and save options).  The body of this component is a tabbed
interface, one tab for the Enriched editor and one for the Flat Text
editor.

The Enriched editor tab directs the user to the AngJS version of the page.
the Flat Text editor tab implements the standard MARC flat text editor
interface.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
13 files changed:
Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/marc-edit.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/marcrecord.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.ts [new file with mode: 0644]

index e0fbff8..064c215 100644 (file)
@@ -27,6 +27,7 @@ import {ConjoinedComponent} from './record/conjoined.component';
 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: [
@@ -58,7 +59,8 @@ import {SearchTemplatesComponent} from './search-templates.component';
     CatalogRoutingModule,
     HoldsModule,
     HoldingsModule,
-    BookingModule
+    BookingModule,
+    MarcEditModule
   ],
   providers: [
     StaffCatalogService
index f562123..98476aa 100644 (file)
       <!-- 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>
index c70b565..e397444 100644 (file)
@@ -110,6 +110,11 @@ export class RecordComponent implements OnInit {
         }
         return null;
     }
+
+    handleMarcRecordSaved() {
+        this.staffCat.currentDetailRecordSummary = null;
+        this.loadRecord();
+    }
 }
 
 
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.html
new file mode 100644 (file)
index 0000000..fdaf7e5
--- /dev/null
@@ -0,0 +1,73 @@
+
+<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>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.ts
new file mode 100644 (file)
index 0000000..cea1990
--- /dev/null
@@ -0,0 +1,182 @@
+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()));
+            });
+        });
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.css b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.css
new file mode 100644 (file)
index 0000000..12e912b
--- /dev/null
@@ -0,0 +1,11 @@
+
+
+.flat-editor-content {
+    font-family: 'Lucida Console', Monaco, monospace; 
+    display: inline-block; 
+    /*
+    min-width: 1ch; 
+    margin: 0 -1px; 
+    */
+    padding: 0;
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.html
new file mode 100644 (file)
index 0000000..eaf54a9
--- /dev/null
@@ -0,0 +1,7 @@
+
+<div *ngIf="record">
+  <textarea class="form-control flat-editor-content" 
+    (blur)="record.absorbBreakerChanges()"
+    [(ngModel)]="record.breakerText" rows="{{rowCount()}}" spellcheck="false">
+  </textarea>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/flat-editor.component.ts
new file mode 100644 (file)
index 0000000..b5e2f41
--- /dev/null
@@ -0,0 +1,45 @@
+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;
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/marc-edit.module.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/marc-edit.module.ts
new file mode 100644 (file)
index 0000000..a18eb0b
--- /dev/null
@@ -0,0 +1,24 @@
+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 { }
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/marcrecord.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/marcrecord.ts
new file mode 100644 (file)
index 0000000..1b0c488
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+  * 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});
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.css b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.css
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.html
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.ts
new file mode 100644 (file)
index 0000000..7f8ac33
--- /dev/null
@@ -0,0 +1,28 @@
+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() {}
+}
+
+
+