LP1852782 MARC edit inline authority record creation.
authorBill Erickson <berickxx@gmail.com>
Thu, 19 Dec 2019 23:00:27 +0000 (18:00 -0500)
committerBill Erickson <berickxx@gmail.com>
Fri, 21 Feb 2020 16:44:38 +0000 (11:44 -0500)
Implement support for creating a new authority record from a bib field
in "immediate" (non-editing)  mode.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
15 files changed:
Open-ILS/src/eg2/src/app/staff/share/marc-edit/authority-linking-dialog.component.html
Open-ILS/src/eg2/src/app/staff/share/marc-edit/authority-linking-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.html
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor.component.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/fixed-field.component.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/marc-edit.module.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.html
Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/tagtable.service.ts
Open-ILS/src/eg2/src/styles.css
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/Authority.pm

index c0e9558..df25167 100644 (file)
@@ -7,9 +7,14 @@
   </span>
 </ng-template>
 
+<!-- MARC edit-ception! -->
+<eg-marc-editor-dialog #marcEditDialog recordType="authority">
+</eg-marc-editor-dialog>
+
 <!-- display a single heading as MARC or as the human friendlier string -->
 <ng-template #headingField let-field="field" let-from="from" let-also="also">
-  <button class="btn btn-sm btn-outline-info p-1 mr-1" 
+  <button class="btn btn-sm p-1 mr-1" 
+    [ngClass]="{'btn-outline-primary': !(from || also), 'btn-outline-info': (from || also)}"
     (click)="applyHeading(field)" i18n>Apply</button>
   <ng-container *ngIf="showAs == 'heading'">
     <span *ngIf="from" i18n>See From: {{field.heading}}</span>
       <div class="ml-2 p-1">
         <div class="mb-1" i18n>Create new authority from this field</div>
         <div>
-          <button class="btn btn-outline-info" [disabled]="true">
-            Immediately
-          </button>
-          <button class="btn btn-outline-info ml-2" [disabled]="true">
-            Create and Edit
-          </button>
+          <button class="btn btn-outline-info" 
+            (click)="createNewAuthority()">Immediately</button>
+          <button class="btn btn-outline-info ml-2" 
+            (click)="createNewAuthority(true)">Create and Edit</button>
         </div>
       </div>
     </div>
index 2015ec1..1c7d3a5 100644 (file)
@@ -1,10 +1,14 @@
-import {Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
+import {Component, ViewChild, Input, Output, OnInit, EventEmitter} from '@angular/core';
 import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
 import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
 import {MarcField} from './marcrecord';
+import {MarcEditContext} from './editor-context';
 import {Pager} from '@eg/share/util/pager';
+import {MarcEditorDialogComponent} from './editor-dialog.component';
 
 /**
  * MARC Authority Linking Dialog
@@ -22,6 +26,7 @@ export class AuthorityLinkingDialogComponent
     @Input() thesauri: string = null;
     @Input() controlSet: number = null;
     @Input() pager: Pager;
+    @Input() context: MarcEditContext;
 
     browseData: any[] = [];
 
@@ -32,8 +37,15 @@ export class AuthorityLinkingDialogComponent
 
     selectedSubfields: string[] = [];
 
+    cni: string; // Control Number Identifier
+
+    @ViewChild('marcEditDialog', {static: false})
+        marcEditDialog: MarcEditorDialogComponent;
+
     constructor(
         private modal: NgbModal,
+        private auth: AuthService,
+        private org: OrgService,
         private pcrud: PcrudService,
         private net: NetService) {
         super(modal);
@@ -61,9 +73,14 @@ export class AuthorityLinkingDialogComponent
 
     initData() {
 
-       this.pager.offset = 0;
+        this.pager.offset = 0;
+
+        this.org.settings(['cat.marc_control_number_identifier']).then(s => {
+            this.cni = s['cat.marc_control_number_identifier'] ||
+                'Set cat.marc_control_number_identifier in Library Settings';
+        });
 
-       this.pcrud.search('acsbf',
+        this.pcrud.search('acsbf',
             {tag: this.bibField.tag},
             {flesh: 1, flesh_fields: {acsbf: ['authority_field']}},
             {atomic:  true, anonymous: true}
@@ -130,5 +147,41 @@ export class AuthorityLinkingDialogComponent
         return this.authMeta ?
             this.authMeta.sf_list().includes(sf) : false;
     }
+
+    setSubfieldZero(authId: number) {
+        const sfZero = this.bibField.subfields.filter(sf => sf[0] === '0')[0];
+        if (sfZero) {
+            this.context.deleteSubfield(this.bibField, sfZero);
+        }
+        this.context.insertSubfield(this.bibField,
+            ['0', `(${this.cni})${authId}`, this.bibField.subfields.length]);
+    }
+
+    createNewAuthority(editFirst?: boolean) {
+
+        const method = editFirst ?
+            'open-ils.cat.authority.record.create_from_bib.readonly' :
+            'open-ils.cat.authority.record.create_from_bib';
+
+        this.net.request(
+            'open-ils.cat', method,
+            this.fieldHash(), this.cni, this.auth.token()
+        ).subscribe(record => {
+            if (editFirst) {
+                this.marcEditDialog.recordXml = record;
+                this.marcEditDialog.open({size: 'xl'})
+                .subscribe(saveEvent => {
+                    if (saveEvent && saveEvent.recordId) {
+                        this.setSubfieldZero(saveEvent.recordId);
+                    }
+                    this.close();
+                });
+            } else {
+                this.setSubfieldZero(record.id());
+                this.close();
+            }
+        });
+    }
 }
 
+
index 88be84c..1bcc19d 100644 (file)
@@ -6,8 +6,8 @@ import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
 import {MarcEditContext, FieldFocusRequest, MARC_EDITABLE_FIELD_TYPE,
     TextUndoRedoAction} from './editor-context';
 import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
-import {TagTableService} from './tagtable.service';
 import {StringComponent} from '@eg/share/string/string.component';
+import {TagTable} from './tagtable.service';
 
 /**
  * MARC Editable Content Component
@@ -73,9 +73,11 @@ export class EditableContentComponent
     @ViewChild('insertAfter', {static: false}) insertAfterStr: StringComponent;
     @ViewChild('deleteField', {static: false}) deleteFieldStr: StringComponent;
 
-    constructor(
-        private renderer: Renderer2,
-        private tagTable: TagTableService) {}
+    constructor(private renderer: Renderer2) {}
+
+    tt(): TagTable { // for brevity
+        return this.context.tagTable;
+    }
 
     ngOnInit() {
         this.setupFieldType();
@@ -197,8 +199,7 @@ export class EditableContentComponent
     }
 
     applyFFOptions() {
-        return this.tagTable.getFfFieldMeta(
-            this.fixedFieldCode, this.record.recordType())
+        return this.tt().getFfFieldMeta(this.fixedFieldCode)
         .then(fieldMeta => {
             if (fieldMeta) {
                 this.maxLength = fieldMeta.length || 1;
@@ -216,20 +217,19 @@ export class EditableContentComponent
                 return this.tagContextMenuEntries();
 
             case 'sfc':
-                return this.tagTable.getSubfieldCodes(this.field.tag);
+                return this.tt().getSubfieldCodes(this.field.tag);
 
             case 'sfv':
-                return this.tagTable.getSubfieldValues(
+                return this.tt().getSubfieldValues(
                     this.field.tag, this.subfield[0]);
 
             case 'ind1':
             case 'ind2':
-                return this.tagTable.getIndicatorValues(
+                return this.tt().getIndicatorValues(
                     this.field.tag, this.fieldType);
 
             case 'ffld':
-                return this.tagTable.getFfValues(
-                    this.fixedFieldCode, this.record.recordType());
+                return this.tt().getFfValues(this.fixedFieldCode);
         }
 
         return null;
@@ -261,7 +261,7 @@ export class EditableContentComponent
             {divider: true}
         );
 
-        this.tagTable.getFieldTags().forEach(e => this.tagMenuEntries.push(e));
+        this.tt().getFieldTags().forEach(e => this.tagMenuEntries.push(e));
 
         return this.tagMenuEntries;
     }
index 16a8cae..28335f8 100644 (file)
@@ -1,6 +1,7 @@
 import {EventEmitter} from '@angular/core';
 import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
 import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
+import {TagTable} from './tagtable.service';
 
 /* Per-instance MARC editor context. */
 
@@ -65,6 +66,8 @@ export class MarcEditContext {
     undoStack: UndoRedoAction[] = [];
     redoStack: UndoRedoAction[] = [];
 
+    tagTable: TagTable;
+
     // True if any changes have been made.
     // For the 'rich' editor, this is any un-do-able actions.
     // For the text edtior it's any text change.
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-dialog.component.html
new file mode 100644 (file)
index 0000000..1fc6efa
--- /dev/null
@@ -0,0 +1,14 @@
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>MARC Editor</h4>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <eg-marc-editor #marcEditor (recordSaved)="handleRecordSaved($event)" 
+      [recordType]="recordType" [recordXml]="recordXml"></eg-marc-editor>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-dialog.component.ts
new file mode 100644 (file)
index 0000000..67e836b
--- /dev/null
@@ -0,0 +1,44 @@
+import {Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {MarcEditContext} from './editor-context';
+
+
+/**
+ * Spawn a MARC editor within a dialog.
+ */
+
+@Component({
+  selector: 'eg-marc-editor-dialog',
+  templateUrl: './editor-dialog.component.html'
+})
+
+export class MarcEditorDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() context: MarcEditContext;
+    @Input() recordXml: string;
+    @Input() recordType: 'biblio' | 'authority' = 'biblio';
+
+    constructor(
+        private modal: NgbModal,
+        private auth: AuthService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        private net: NetService) {
+        super(modal);
+    }
+
+    ngOnInit() {}
+
+    handleRecordSaved(saved) {
+        this.close(saved);
+    }
+}
+
+
index 7daa2c8..f574604 100644 (file)
     </span>
   </h3>
     
-  <div class="mr-2">
-    <eg-combobox #sourceSelector
-      [entries]="sources"
-      placeholder="Select a Source..."
-      i18n-placeholder>
-    </eg-combobox>
-  </div>
+  <ng-container *ngIf="recordType === 'biblio'">
+    <div class="mr-2">
+      <eg-combobox #sourceSelector
+        [entries]="sources"
+        placeholder="Select a Source..."
+        i18n-placeholder>
+      </eg-combobox>
+    </div>
+  </ng-container>
 
   <ng-container *ngIf="record && record.id">
     <button *ngIf="!record.deleted" class="btn btn-warning" 
index 841ca07..c03e0e7 100644 (file)
@@ -18,6 +18,7 @@ import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 interface MarcSavedEvent {
     marcXml: string;
     bibSource?: number;
+    recordId?: number;
 }
 
 /**
@@ -46,6 +47,10 @@ export class MarcEditorComponent implements OnInit {
         this.fromId(id);
     }
 
+    get recordId(): number {
+        return this.record ? this.record.id : null;
+    }
+
     @Input() set recordXml(xml: string) {
         if (xml) {
             this.fromXml(xml);
@@ -101,6 +106,8 @@ export class MarcEditorComponent implements OnInit {
         this.store.getItem('cat.marcedit.flateditor').then(
             useFlat => this.editorTab = useFlat ? 'flat' : 'rich');
 
+        if (this.recordType !== 'biblio') { return; }
+
         this.pcrud.retrieveAll('cbs').subscribe(
             src => this.sources.push({id: +src.id(), label: src.source()}),
             _ => {},
@@ -149,41 +156,87 @@ export class MarcEditorComponent implements OnInit {
         let sourceName: string = null;
         let sourceId: number = null;
 
-        if (this.sourceSelector.selected) {
+        if (this.sourceSelector && this.sourceSelector.selected) {
             sourceName = this.sourceSelector.selected.label;
             sourceId = this.sourceSelector.selected.id;
         }
 
+        const emission = {
+            marcXml: xml, bibSource: sourceId, recordId: this.recordId};
+
         if (this.inPlaceMode) {
             // Let the caller have the modified XML and move on.
-            this.recordSaved.emit({marcXml: xml, bibSource: sourceId});
+            this.recordSaved.emit(emission);
             return Promise.resolve();
         }
 
+        let promise;
+
         if (this.record.id) { // Editing an existing record
 
-            const method = 'open-ils.cat.biblio.record.xml.update';
+            promise = this.modifyRecord(xml, sourceName, sourceId);
 
-            return this.net.request('open-ils.cat', method,
-                this.auth.token(), this.record.id, xml, sourceName
-            ).toPromise().then(response => {
+        } else {
 
-                const evt = this.evt.parse(response);
-                if (evt) {
-                    console.error(evt);
-                    this.failMsg.current().then(msg => this.toast.warning(msg));
-                    this.dataSaving = false;
-                    return;
-                }
+            promise = this.createRecord(xml, sourceName);
+        }
 
-                this.successMsg.current().then(msg => this.toast.success(msg));
-                this.recordSaved.emit({marcXml: xml, bibSource: sourceId});
-                return response;
-            });
+        // NOTE we do not reinitialize our record with the MARC returned
+        // from the server after a create/update, which means our record
+        // may be out of sync, e.g. missing 901* values.  It's the
+        // callers onsibility to tear us down and rebuild us.
+        return promise.then(marcXml => {
+            if (!marcXml) { return null; }
+            this.successMsg.current().then(msg => this.toast.success(msg));
+            emission.marcXml = marcXml;
+            emission.recordId = this.recordId;
+            this.recordSaved.emit(emission);
+            return marcXml;
+        });
+    }
 
-        } else {
-            // TODO: create a new record
-        }
+    modifyRecord(marcXml: string, sourceName: string, sourceId: number): Promise<any> {
+        const method = 'open-ils.cat.biblio.record.marc.replace';
+
+        return this.net.request('open-ils.cat', method,
+            this.auth.token(), this.record.id, marcXml, sourceName
+
+        ).toPromise().then(response => {
+
+            const evt = this.evt.parse(response);
+            if (evt) {
+                console.error(evt);
+                this.failMsg.current().then(msg => this.toast.warning(msg));
+                this.dataSaving = false;
+                return null;
+            }
+
+            return response.marc();
+        });
+    }
+
+    createRecord(marcXml: string, sourceName?: string): Promise<any> {
+
+        const method = this.recordType === 'biblio' ?
+            'open-ils.cat.biblio.record.xml.create' :
+            'open-ils.cat.authority.record.import';
+
+        return this.net.request('open-ils.cat', method,
+            this.auth.token(), marcXml, sourceName
+        ).toPromise().then(response => {
+
+            const evt = this.evt.parse(response);
+
+            if (evt) {
+                console.error(evt);
+                this.failMsg.current().then(msg => this.toast.warning(msg));
+                this.dataSaving = false;
+                return null;
+            }
+
+            this.record.id = response.id();
+            return response.marc();
+        });
     }
 
     fromId(id: number): Promise<any> {
@@ -226,7 +279,7 @@ export class MarcEditorComponent implements OnInit {
                 }
                 return this.fromId(this.record.id)
                 .then(_ => this.recordSaved.emit(
-                    {marcXml: this.record.toXml()}));
+                    {marcXml: this.record.toXml(), recordId: this.recordId}));
             });
         });
     }
@@ -248,7 +301,7 @@ export class MarcEditorComponent implements OnInit {
 
                 return this.fromId(this.record.id)
                 .then(_ => this.recordSaved.emit(
-                    {marcXml: this.record.toXml()}));
+                    {marcXml: this.record.toXml(), recordId: this.recordId}));
             });
         });
     }
index a81255d..6905ec6 100644 (file)
@@ -25,7 +25,7 @@ export class FixedFieldComponent implements OnInit {
     fieldMeta: IdlObject;
     randId = Math.floor(Math.random() * 10000000);
 
-    constructor(private tagTable: TagTableService) {}
+    constructor() {}
 
     ngOnInit() {
         this.init().then(_ =>
@@ -37,8 +37,7 @@ export class FixedFieldComponent implements OnInit {
 
         // If no field metadata is found for this fixed field code and
         // record type combo, the field will be hidden in the UI.
-        return this.tagTable.getFfFieldMeta(
-            this.fieldCode, this.record.recordType())
+        return this.context.tagTable.getFfFieldMeta(this.fieldCode)
         .then(fieldMeta => this.fieldMeta = fieldMeta);
     }
 }
index e34928d..be62ae9 100644 (file)
@@ -9,6 +9,7 @@ import {FixedFieldComponent} from './fixed-field.component';
 import {TagTableService} from './tagtable.service';
 import {EditableContentComponent} from './editable-content.component';
 import {AuthorityLinkingDialogComponent} from './authority-linking-dialog.component';
+import {MarcEditorDialogComponent} from './editor-dialog.component';
 
 @NgModule({
     declarations: [
@@ -18,6 +19,7 @@ import {AuthorityLinkingDialogComponent} from './authority-linking-dialog.compon
         FixedFieldsEditorComponent,
         FixedFieldComponent,
         EditableContentComponent,
+        MarcEditorDialogComponent,
         AuthorityLinkingDialogComponent
     ],
     imports: [
index 68e0f68..cf0f0ae 100644 (file)
@@ -7,7 +7,8 @@
   </div>
 </ng-container>
 
-<eg-authority-linking-dialog #authLinker></eg-authority-linking-dialog>
+<eg-authority-linking-dialog #authLinker [context]="context">
+</eg-authority-linking-dialog>
 
 <ng-template #subfieldChunk let-field="field" let-subfield="subfield">
 
       <div class="col-lg-3">
         <div><button class="btn btn-outline-dark"
           (click)="showHelp = !showHelp" i18n>Help</button></div>
-        <div class="mt-2"><button class="btn btn-outline-dark"
-          (click)="validate()" i18n>Validate</button></div>
+        <ng-container *ngIf="context.recordType === 'biblio'">
+          <div class="mt-2"><button class="btn btn-outline-dark"
+            (click)="validate()" i18n>Validate</button></div>
+        </ng-container>
         <div class="mt-2">
           <button type="button" class="btn btn-outline-info"
             [disabled]="undoCount() < 1" (click)="undo()">
index 480c7ea..e4b3da6 100644 (file)
@@ -62,11 +62,13 @@ export class MarcRichEditorComponent implements OnInit {
         if (!this.record) { return Promise.resolve(); }
 
         return Promise.all([
-            this.tagTable.loadTagTable({marcRecordType: this.context.recordType}),
-            this.tagTable.getFfPosTable(this.record.recordType()),
-            this.tagTable.getFfValueTable(this.record.recordType()),
+            this.tagTable.loadTags({
+                marcRecordType: this.context.recordType,
+                ffType: this.record.recordType()
+            }).then(table => this.context.tagTable = table),
             this.tagTable.getControlledBibTags().then(
-                tags => this.controlledBibTags = tags)
+                tags => this.controlledBibTags = tags),
+            this.fetchSettings()
         ]).then(_ =>
             // setTimeout forces all of our sub-components to rerender
             // themselves each time init() is called.  Without this,
@@ -77,6 +79,11 @@ export class MarcRichEditorComponent implements OnInit {
         );
     }
 
+    fetchSettings(): Promise<any> {
+        // Fetch at rich editor load time to cache.
+        return this.org.settings(['cat.marc_control_number_identifier']);
+    }
+
     stackSubfieldsChange() {
         if (this.stackSubfields) {
             this.store.setItem('cat.marcedit.stack_subfields', true);
@@ -111,8 +118,8 @@ export class MarcRichEditorComponent implements OnInit {
 
     validate() {
         const fields = [];
-        this.record.fields.filter(f => this.isControlledBibTag(f.tag))
 
+        this.record.fields.filter(f => this.isControlledBibTag(f.tag))
         .forEach(f => {
             f.authValid = false;
             fields.push({
@@ -120,7 +127,7 @@ export class MarcRichEditorComponent implements OnInit {
                 tag: f.tag,
                 ind1: f.ind1,
                 ind2: f.ind2,
-                subfields: f.subfields.map(sf => ({code: sf[0], value: sf[1]}))
+                subfields: f.subfields.map(sf => [sf[0], sf[1]])
             });
         });
 
@@ -141,7 +148,7 @@ export class MarcRichEditorComponent implements OnInit {
 
     openLinkerDialog(field: MarcField) {
         this.authLinker.bibField = field;
-        this.authLinker.open({size: 'lg'}).subscribe(newField => {
+        this.authLinker.open({size: 'xl'}).subscribe(newField => {
             if (!newField) { return; }
 
             // Performs an insert followed by a delete, so the two
index 3d2fc73..ce6ddb7 100644 (file)
@@ -6,52 +6,69 @@ import {IdlObject} from '@eg/core/idl.service';
 import {AuthService} from '@eg/core/auth.service';
 import {NetService} from '@eg/core/net.service';
 import {PcrudService} from '@eg/core/pcrud.service';
-import {EventService} from '@eg/core/event.service';
 import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
 
+const DEFAULT_MARC_FORMAT = 'marc21';
+
 interface TagTableSelector {
     marcFormat?: string;
-    marcRecordType?: string;
+    marcRecordType: 'biblio' | 'authority' | 'serial';
+
+    // MARC record fixed field "Type" value.
+    ffType: string;
 }
 
-const defaultTagTableSelector: TagTableSelector = {
-    marcFormat     : 'marc21',
-    marcRecordType : 'biblio'
-};
+export class TagTable {
 
-@Injectable()
-export class TagTableService {
+    store: StoreService;
+    auth: AuthService;
+    net: NetService;
+    pcrud: PcrudService;
 
-    // Current set of tags in list and map form.
-    tagMap: {[tag: string]: any} = {};
-    ffPosMap: {[rtype: string]: any[]} = {};
-    ffValueMap: {[rtype: string]: any} = {};
-    controlledBibTags: string[];
+    selector: TagTableSelector;
 
-    extractedValuesCache:
-        {[valueType: string]: {[which: string]: any}} = {};
+    // Current set of tags in list and map form.
+    tagMap: {[tag: string]: any};
+    ffPosTable: any;
+    ffValueTable: any;
+    fieldTags: ContextMenuEntry[];
+
+    // Cache of compiled, sorted, munged data.  Stuff the UI requests
+    // frequently for selectors, etc.
+    cache: {[valueType: string]: {[which: string]: any}} = {
+        indicators: {},
+        sfcodes: {},
+        sfvalues: {},
+        ffvalues: {}
+    };
 
     constructor(
-        private store: StoreService,
-        private auth: AuthService,
-        private net: NetService,
-        private pcrud: PcrudService,
-        private evt: EventService
+        store: StoreService,
+        auth: AuthService,
+        net: NetService,
+        pcrud: PcrudService,
+        selector: TagTableSelector
     ) {
+        this.store = store;
+        this.auth = auth;
+        this.net = net;
+        this.pcrud = pcrud;
+        this.selector = selector;
+    }
+
 
-        this.extractedValuesCache = {
-            fieldtags: {},
-            indicators: {},
-            sfcodes: {},
-            sfvalues: {},
-            ffvalues: {}
-        };
+    load(): Promise<any> {
+        return Promise.all([
+            this.loadTagTable(),
+            this.getFfPosTable(),
+            this.getFfValueTable(),
+        ]);
     }
 
     // Various data needs munging for display.  Cached the modified
     // values since they are refernced repeatedly by the UI code.
     fromCache(dataType: string, which?: string, which2?: string): ContextMenuEntry[] {
-        const part1 = this.extractedValuesCache[dataType][which];
+        const part1 = this.cache[dataType][which];
         if (which2) {
             if (part1) {
                 return part1[which2];
@@ -63,7 +80,7 @@ export class TagTableService {
 
     toCache(dataType: string, which: string,
         which2: string, values: ContextMenuEntry[]): ContextMenuEntry[] {
-        const base = this.extractedValuesCache[dataType];
+        const base = this.cache[dataType];
         const part1 = base[which];
 
         if (which2) {
@@ -76,69 +93,62 @@ export class TagTableService {
         return values;
     }
 
-    getFfPosTable(rtype: string): Promise<any> {
-        const storeKey = 'FFPosTable_' + rtype;
+    getFfPosTable(): Promise<any> {
+        const storeKey = 'FFPosTable_' + this.selector.ffType;
 
-        if (this.ffPosMap[rtype]) {
-            return Promise.resolve(this.ffPosMap[rtype]);
+        if (this.ffPosTable) {
+            return Promise.resolve(this.ffPosTable);
         }
 
-        this.ffPosMap[rtype] = this.store.getLocalItem(storeKey);
+        this.ffPosTable = this.store.getLocalItem(storeKey);
 
-        if (this.ffPosMap[rtype]) {
-            return Promise.resolve(this.ffPosMap[rtype]);
+        if (this.ffPosTable) {
+            return Promise.resolve(this.ffPosTable);
         }
 
         return this.net.request(
             'open-ils.fielder', 'open-ils.fielder.cmfpm.atomic',
-            {query: {tag: {'!=' : '006'}, rec_type: rtype}}
+            {query: {tag: {'!=' : '006'}, rec_type: this.selector.ffType}}
 
         ).toPromise().then(table => {
             this.store.setLocalItem(storeKey, table);
-            return this.ffPosMap[rtype] = table;
+            return this.ffPosTable = table;
         });
     }
 
-    getFfValueTable(rtype: string): Promise<any> {
+    // ffType is the fixed field Type value. BKS, AUT, etc.
+    // See config.marc21_rec_type_map
+    getFfValueTable(): Promise<any> {
 
-        const storeKey = 'FFValueTable_' + rtype;
+        const storeKey = 'FFValueTable_' + this.selector.ffType;
 
-        if (this.ffValueMap[rtype]) {
-            return Promise.resolve(this.ffValueMap[rtype]);
+        if (this.ffValueTable) {
+            return Promise.resolve(this.ffValueTable);
         }
 
-        this.ffValueMap[rtype] = this.store.getLocalItem(storeKey);
+        this.ffValueTable = this.store.getLocalItem(storeKey);
 
-        if (this.ffValueMap[rtype]) {
-            return Promise.resolve(this.ffValueMap[rtype]);
+        if (this.ffValueTable) {
+            return Promise.resolve(this.ffValueTable);
         }
 
         return this.net.request(
             'open-ils.cat',
-            'open-ils.cat.biblio.fixed_field_values.by_rec_type', rtype
+            'open-ils.cat.biblio.fixed_field_values.by_rec_type',
+            this.selector.ffType
 
         ).toPromise().then(table => {
             this.store.setLocalItem(storeKey, table);
-            return this.ffValueMap[rtype] = table;
+            return this.ffValueTable = table;
         });
     }
 
-    loadTagTable(selector?: TagTableSelector): Promise<any> {
+    loadTagTable(): Promise<any> {
 
-        if (selector) {
-            if (!selector.marcFormat) {
-                selector.marcFormat = defaultTagTableSelector.marcFormat;
-            }
-            if (!selector.marcRecordType) {
-                selector.marcRecordType =
-                    defaultTagTableSelector.marcRecordType;
-            }
-        } else {
-            selector = defaultTagTableSelector;
-        }
+        const sel = this.selector;
 
         const cacheKey =
-            `current_tag_table_${selector.marcFormat}_${selector.marcRecordType}`;
+            `current_tag_table_${sel.marcFormat}_${sel.marcRecordType}`;
 
         this.tagMap = this.store.getLocalItem(cacheKey);
 
@@ -146,18 +156,19 @@ export class TagTableService {
             return Promise.resolve(this.tagMap);
         }
 
-        return this.fetchTagTable(selector).then(_ => {
+        return this.fetchTagTable().then(_ => {
             this.store.setLocalItem(cacheKey, this.tagMap);
             return this.tagMap;
         });
     }
 
-    fetchTagTable(selector?: TagTableSelector): Promise<any> {
+    fetchTagTable(): Promise<any> {
         this.tagMap = [];
         return this.net.request(
             'open-ils.cat',
             'open-ils.cat.tag_table.all.retrieve.local',
-            this.auth.token(), selector.marcFormat, selector.marcRecordType
+            this.auth.token(), this.selector.marcFormat,
+            this.selector.marcRecordType
         ).pipe(tap(tagData => {
             this.tagMap[tagData.tag] = tagData;
         })).toPromise();
@@ -179,16 +190,17 @@ export class TagTableService {
 
     getFieldTags(): ContextMenuEntry[] {
 
-        const cached = this.fromCache('fieldtags');
-        if (cached) { return cached; }
+        if (!this.fieldTags) {
+            this.fieldTags = Object.keys(this.tagMap)
+            .filter(tag => Boolean(this.tagMap[tag]))
+            .map(tag => ({
+                value: tag,
+                label: `${tag}: ${this.tagMap[tag].name}`
+            }))
+            .sort((a, b) => a.label < b.label ? -1 : 1);
+        }
 
-        return Object.keys(this.tagMap)
-        .filter(tag => Boolean(this.tagMap[tag]))
-        .map(tag => ({
-            value: tag,
-            label: `${tag}: ${this.tagMap[tag].name}`
-        }))
-        .sort((a, b) => a.label < b.label ? -1 : 1);
+        return this.fieldTags;
     }
 
     getSubfieldValues(tag: string, sfCode: string): ContextMenuEntry[] {
@@ -235,29 +247,29 @@ export class TagTableService {
     }
 
 
-    getFfFieldMeta(fieldCode: string, recordType: string): Promise<IdlObject> {
-        return this.getFfPosTable(recordType).then(table => {
+    getFfFieldMeta(fieldCode: string): Promise<IdlObject> {
+        return this.getFfPosTable().then(table => {
 
-            // Note the AngJS MARC editor stores the full POS table
-            // for all record types in every copy of the table, hence
-            // the seemingly extraneous check in recordType.
+            // Best I can tell, the AngJS MARC editor stores the
+            // full POS table for all record types in every copy of
+            // the table, hence the seemingly extraneous check in ffType.
             return table.filter(
                 field =>
                     field.fixed_field === fieldCode
-                 && field.rec_type === recordType
+                 && field.rec_type === this.selector.ffType
             )[0];
         });
     }
 
 
     // Assumes getFfPosTable and getFfValueTable have already been
-    // invoked for the request record type.
-    getFfValues(fieldCode: string, recordType: string): ContextMenuEntry[] {
+    // invoked for the requested record type.
+    getFfValues(fieldCode: string): ContextMenuEntry[] {
 
-        const cached = this.fromCache('ffvalues', recordType, fieldCode);
+        const cached = this.fromCache('ffvalues', fieldCode);
         if (cached) { return cached; }
 
-        let values = this.ffValueMap[recordType];
+        let values = this.ffValueTable;
 
         if (!values || !values[fieldCode]) { return null; }
 
@@ -269,7 +281,40 @@ export class TagTableService {
             .map(val => ({value: val[0], label: `${val[0]}: ${val[1]}`}))
             .sort((a, b) => a.label < b.label ? -1 : 1);
 
-        return this.toCache('ffvalues', recordType, fieldCode, values);
+        return this.toCache('ffvalues', fieldCode, null, values);
+    }
+
+}
+
+@Injectable()
+export class TagTableService {
+
+    tagTables: {[marcRecordType: string]: TagTable} = {};
+    controlledBibTags: string[];
+
+    constructor(
+        private store: StoreService,
+        private auth: AuthService,
+        private net: NetService,
+        private pcrud: PcrudService,
+    ) {}
+
+    loadTags(selector: TagTableSelector): Promise<TagTable> {
+        if (!selector.marcFormat) {
+            selector.marcFormat = DEFAULT_MARC_FORMAT;
+        }
+
+        // Tag tables of a given marc record type are identical.
+        if (this.tagTables[selector.marcRecordType]) {
+            return Promise.resolve(this.tagTables[selector.marcRecordType]);
+        }
+
+        const tt = new TagTable(
+            this.store, this.auth, this.net, this.pcrud, selector);
+
+        this.tagTables[selector.marcRecordType] = tt;
+
+        return tt.load().then(_ => tt);
     }
 
     getControlledBibTags(): Promise<string[]> {
index 9c120cc..169cf63 100644 (file)
@@ -220,3 +220,8 @@ body>.dropdown-menu {z-index: 2100;}
   color: black;
 }
 
+/* Allow for larger XL dialogs */
+@media (min-width: 1300px) { .modal-xl { max-width: 1200px; } }
+@media (min-width: 1600px) { .modal-xl { max-width: 1500px; } }
+@media (min-width: 1700px) { .modal-xl { max-width: 1600px; } }
+
index f601c0e..82d04f5 100644 (file)
@@ -374,7 +374,7 @@ __PACKAGE__->register_method(
 );
 
 # Returns the first found field.
-sub get_auth_field {
+sub get_auth_field_by_tag {
     my ($atag, $cset_id) = @_;
 
     my $e = new_editor();
@@ -400,7 +400,7 @@ sub bib_field_overlay_authority_field {
     # not consistent with the control set, it may produce unexpected
     # results.
     my $sf_list = '';
-    my $acsaf = get_auth_field($atag, $cset_id);
+    my $acsaf = get_auth_field_by_tag($atag, $cset_id);
 
     if ($acsaf) {
         $sf_list = $acsaf->sf_list;
@@ -409,7 +409,7 @@ sub bib_field_overlay_authority_field {
 
         # Handle 4XX and 5XX
         (my $alt_atag = $atag) =~ s/^./1/;
-        $acsaf = get_auth_field($alt_atag, $cset_id) if $alt_atag ne $atag;
+        $acsaf = get_auth_field_by_tag($alt_atag, $cset_id) if $alt_atag ne $atag;
 
         $sf_list = $acsaf->sf_list if $acsaf;
     }
@@ -515,7 +515,11 @@ sub validate_bib_fields {
                 acsaf => ['id', 'tag', 'sf_list', 'control_set']
             },
             from => {acsbf => {acsaf => {}}},
-            where => $where
+            where => $where,
+            order_by => [
+                {class => 'acsaf', field => 'main_entry', direction => 'desc'},
+                {class => 'acsaf', field => 'tag'}
+            ]
         });
 
         my @seen_subfields;
@@ -553,8 +557,8 @@ sub validate_bib_fields {
             $record->append_fields($field);
 
             my $match = $U->simplereq(
-                'open-ils.cat', 
-                'open-ils.cat.authority.simple_heading.from_xml',
+                'open-ils.search', 
+                'open-ils.search.authority.simple_heading.from_xml',
                 $record->as_xml_record, $control_set);
 
             if ($match) {
@@ -631,14 +635,24 @@ sub bib_field_authority_linking_browse {
 
     return [] unless $bib_field;
 
-    my $term = join(' ', map {$_->[0]} @{$bib_field->{subfields}});
+    my $term = join(' ', map {$_->[1]} @{$bib_field->{subfields}});
 
     return [] unless $term;
 
     my $axis = $e->json_query({
         select => {abaafm => ['axis']},
         from => {acsbf => {acsaf => {join => 'abaafm'}}},
-        where => {'+acsbf' => {tag => $bib_field->{tag}}}
+        where => {'+acsbf' => {tag => $bib_field->{tag}}},
+        order_by => [
+            {class => 'acsaf', field => 'main_entry', direction => 'desc'},
+            {class => 'acsaf', field => 'tag'},
+
+            # This lets us favor the 'subject' axis to the 'topic' axis.
+            # Topic is a subset of subject.  It's not clear if a field
+            # can link only to the 'topic' axes.  In stock EG, the one
+            # 'topic' field also links to 'subject'.
+            {class => 'abaafm', field => 'axis'}
+        ]
     })->[0];
 
     return [] unless $axis && ($axis = $axis->{axis});