LPXXX editor continued
authorBill Erickson <berickxx@gmail.com>
Fri, 22 Nov 2019 17:32:54 +0000 (12:32 -0500)
committerBill Erickson <berickxx@gmail.com>
Fri, 6 Dec 2019 15:37:03 +0000 (10:37 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.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.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/tagtable.service.ts

diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.css b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.css
new file mode 100644 (file)
index 0000000..88b31f2
--- /dev/null
@@ -0,0 +1,20 @@
+
+:host >>> .popover {
+  font-family: 'Lucida Console', Monaco, monospace;
+  max-width: 550px;
+}
+
+:host >>> .popover-body {
+  max-height: 400px;
+  overflow-y: auto;
+  overflow-x: auto;
+}
+
+:host >>> .popover-body .menu-entry {
+  white-space: nowrap;
+}
+
+:host >>> .popover-body .menu-entry:hover {
+  background-color: #f8f9fa; /* bootstrap color */
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.html
new file mode 100644 (file)
index 0000000..b9d301b
--- /dev/null
@@ -0,0 +1,29 @@
+
+<ng-template #menuTemplate>
+  <div *ngFor="let entry of menuEntries" class="menu-entry {{entryClasses}}">
+    <a (click)="entryClicked(entry)">
+      <span>{{entry.label}}</span>
+    </a>
+  </div>
+</ng-template>
+
+<!--
+<div class="form-inline d-flex">
+  <div class="flex-4">
+    <span id='label-{{randId}}' class="text-left font-weight-bold">
+      {{fieldLabel}}
+    </span>
+  </div>
+  <input 
+    [attr.aria-labelledby]="'label-' + randId"
+    class="form-control rounded-0 flex-5" type="text" 
+    (change)="valueChange()"
+    [(ngModel)]="fieldValue" 
+    [attr.maxlength]="fieldLength" [attr.size]="fieldLength"
+    [ngbPopover]="menuContent"
+    #p="ngbPopover" triggers="manual"
+    (contextmenu)="contextMenu($event, p)"
+    />
+</div>
+-->
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/context-menu.component.ts
new file mode 100644 (file)
index 0000000..fc255f6
--- /dev/null
@@ -0,0 +1,72 @@
+import {Component, Input, Output, OnInit, EventEmitter, Directive, 
+    ViewChild, TemplateRef} from '@angular/core';
+import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Right-click context menu component.
+ *
+ * No state is maintained (i.e. no current value), events are 
+ * simply emitted as entries are chosen.
+ *
+ * <eg-context-menu-entries #tRef [menuEntries]="ContextMenuEntry[]"
+ *   (entrySelected)="$event === ContextMenuEntry">
+ * <input ... [egContextMenu]="tRef" ... />
+ */
+
+export interface ContextMenuEntry {
+    value: string;
+    label: string;
+}
+
+@Component({
+  selector: 'eg-context-menu-entries',
+  templateUrl: './context-menu.component.html',
+  styleUrls: ['context-menu.component.css']
+})
+
+export class ContextMenuComponent implements OnInit {
+
+    @Input() menuEntries: ContextMenuEntry[] = [];
+
+    // Additional CSS classes (space separated) to apply to the entry links
+    @Input() entryClasses = '';
+
+    @Output() entrySelected: EventEmitter<ContextMenuEntry>;
+
+    @ViewChild('menuTemplate', {static: false}) public menuTemplate: TemplateRef<any>;
+
+    constructor() {
+        this.entrySelected = new EventEmitter<ContextMenuEntry>();
+    }
+
+    ngOnInit() {}
+
+    entryClicked(entry: ContextMenuEntry) {
+        this.entrySelected.emit(entry);
+    }
+}
+
+
+@Directive({
+  selector: '[egContextMenu]',
+  exportAs: 'egContextMenu'
+})
+export class ContextMenuDirective extends NgbPopover {
+
+    @Input() set egContextMenu(menuComp: ContextMenuComponent) {
+        this.ngbPopover = menuComp.menuTemplate;
+    }
+
+    // Only one active menu is allowed at a time.
+    static activeMenu: ContextMenuDirective;
+
+    open() {
+        super.open();
+        if (ContextMenuDirective.activeMenu) {
+           ContextMenuDirective.activeMenu.close();
+        }
+        ContextMenuDirective.activeMenu = this;
+    }
+}
+
+
index c0ece94..c51b456 100644 (file)
@@ -40,6 +40,7 @@ export class MarcEditContext {
     // NgbPopovers don't always close when we want them to,
     // specifcially when context-clicking to open other popovers.
     closePopovers() {
+    // TODO
         this.popOvers.forEach(p => p.close());
     }
 
index 1f6bb8e..f0112ed 100644 (file)
@@ -1,18 +1,13 @@
 import {Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
 import {MarcRecord} from './marcrecord';
 import {MarcEditContext} from './editor-context';
-import {TagTableService} from './tagtable.service';
+import {TagTableService, ValueLabelPair} from './tagtable.service';
 import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
 
 /**
  * MARC Fixed Field Editing Component
  */
 
-interface FixedFieldValue {
-    value: string;
-    label: string;
-}
-
 @Component({
   selector: 'eg-fixed-field',
   templateUrl: './fixed-field.component.html',
@@ -30,7 +25,7 @@ export class FixedFieldComponent implements OnInit {
     fieldValue: string;
     fieldMeta: any;
     fieldLength: number = null;
-    fieldValues: FixedFieldValue[] = null;
+    fieldValues: ValueLabelPair[] = null;
     popOver: NgbPopover;
     randId = Math.floor(Math.random() * 10000000);
 
index 45e03ed..6825a48 100644 (file)
@@ -8,6 +8,9 @@ import {FixedFieldComponent} from './fixed-field.component';
 import {TagTableService} from './tagtable.service';
 import {EditableContentComponent} from './editable-content.component';
 
+// TODO: consider moving to share 
+import {ContextMenuDirective, ContextMenuComponent} from './context-menu.component';
+
 @NgModule({
     declarations: [
         MarcEditorComponent,
@@ -15,7 +18,9 @@ import {EditableContentComponent} from './editable-content.component';
         MarcFlatEditorComponent,
         FixedFieldsEditorComponent,
         FixedFieldComponent,
-        EditableContentComponent
+        EditableContentComponent,
+        ContextMenuDirective,
+        ContextMenuComponent
     ],
     imports: [
         StaffCommonModule
index 906a919..f03ce49 100644 (file)
@@ -41,6 +41,7 @@ export class MarcRichEditorComponent implements OnInit {
         if (!this.record) { return Promise.resolve(); }
 
         return Promise.all([
+            this.tagTable.loadTagTable({marcRecordType: this.record.recordType()}),
             this.tagTable.getFFPosTable(this.record.recordType()),
             this.tagTable.getFFValueTable(this.record.recordType())
         ]).then(_ => this.dataLoaded = true);
index d7b4701..024b1e9 100644 (file)
@@ -1,15 +1,31 @@
 import {Injectable, EventEmitter} from '@angular/core';
-import {map} from 'rxjs/operators';
+import {map, tap} from 'rxjs/operators';
 import {StoreService} from '@eg/core/store.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';
 
+export interface ValueLabelPair {
+    value: string;
+    label: string;
+}
+
+interface TagTableSelector {
+    marcFormat?: string;
+    marcRecordType?: string;
+}
+
+const defaultTagTableSelector: TagTableSelector = {
+    marcFormat     : 'marc21',
+    marcRecordType : 'biblio'
+}
 
 @Injectable()
 export class TagTableService {
 
+    // Current set of tags in list and map form.
+    tagMap: {[tag: string]: any} = {};
     ffPosMap: {[rtype: string]: any[]} = {};
     ffValueMap: {[rtype: string]: any} = {};
 
@@ -67,6 +83,89 @@ export class TagTableService {
             return this.ffValueMap[rtype] = table;
         });
     }
+
+    loadTagTable(selector?: TagTableSelector): Promise<any> {
+
+        if (selector) {
+            if (!selector.marcFormat) {
+                selector.marcFormat = defaultTagTableSelector.marcFormat;
+            }
+            if (!selector.marcRecordType) {
+                selector.marcRecordType = 
+                    defaultTagTableSelector.marcRecordType;
+            }
+        } else {
+            selector = defaultTagTableSelector;
+        }
+
+        // TODO load from local store cache
+
+        return this.fetchTagTable(selector);
+    }
+
+    fetchTagTable(selector?: TagTableSelector): Promise<any> {
+        return this.net.request(
+            'open-ils.cat',
+            'open-ils.cat.tag_table.all.retrieve.local',
+            this.auth.token(), selector.marcFormat, selector.marcRecordType
+        ).pipe(tap(tagData => {
+            this.tagMap[tagData.tag] = tagData;
+        })).toPromise();
+    }
+
+    getSubfieldCodes(tag: string): ValueLabelPair[] { 
+        if (!tag || !this.tagMap[tag]) { return null; }
+
+        return this.tagMap[tag].subfields
+        .map(sf => ({
+            value: sf.code, 
+            label: `${sf.code}: ${sf.description}`
+        })) 
+        .sort((a, b) => a.label < b.label ? -1 : 1);
+    }
+
+    getFieldTags(): ValueLabelPair[] {
+        return Object.keys(this.tagMap)
+        .map(tag => ({
+            value: tag,
+            label: `${tag}: ${this.tagMap[tag].name}`
+        }))
+        .sort((a, b) => a.label < b.label ? -1 : 1);
+    }
+
+    getSubfieldValues(tag: string, sfCode: string): ValueLabelPair[] {
+        if (!tag || !this.tagMap[tag]) { return []; }
+
+        const list: ValueLabelPair[] = [];
+
+        this.tagMap[tag].subfields
+        .filter(sf =>
+            sf.code === sfCode && sf.hasOwnProperty('value_list'))
+        .forEach(sf => {
+            sf.value_list.forEach(value => {
+
+                const label = (value.code == value.description) ?
+                    value.code : `${value.code}: ${value.description}`;
+
+                list.push({value: value.code, label: label});
+            })
+        });
+
+        return list.sort((a, b) => a.label < b.label ? -1 : 1);
+    }
+
+    getIndicatorValues(tag: string, pos: number): ValueLabelPair[] {
+        if (!tag || !this.tagMap[tag]) { return }
+
+        const value = this.tagMap[tag][`ind${pos}`];
+        if (!value) { return; }
+
+        return value.map(tag => ({
+            value: value.code,
+            label: `${value.code}: ${value.description}`
+        }))
+        .sort((a, b) => a.label < b.label ? -1 : 1);
+    }
 }