LPXXX move fixed fields to editable-content user/berick/lpxxx-ang-marc-enriched-editor
authorBill Erickson <berickxx@gmail.com>
Mon, 9 Dec 2019 21:41:09 +0000 (16:41 -0500)
committerBill Erickson <berickxx@gmail.com>
Mon, 9 Dec 2019 21:41:09 +0000 (16:41 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
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/fixed-field.component.html
Open-ILS/src/eg2/src/app/staff/share/marc-edit/fixed-field.component.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

index 72d432c..5f4b1ea 100644 (file)
@@ -31,6 +31,8 @@ export class EditableContentComponent
     // array of subfield code and subfield value
     @Input() subfield: MarcSubfield;
 
+    @Input() fixedFieldCode: string;
+
     // space-separated list of additional CSS classes to append
     @Input() moreClasses: string;
 
@@ -49,6 +51,14 @@ export class EditableContentComponent
     undoRedoSub: Subscription;
     isLeader: boolean; // convenience
 
+    // Cache of fixed field menu options
+    ffValues: ContextMenuEntry[] = [];
+
+    // Track the fixed field value locally since extracting the value
+    // in real time from the record, which adds padding to the text,
+    // causes usability problems.
+    ffValue: string;
+
     constructor(
         private renderer: Renderer2,
         private tagTable: TagTableService) {}
@@ -81,6 +91,9 @@ export class EditableContentComponent
             if (req.fieldId !== this.field.fieldId) { return false; }
         } else if (req.target === 'ldr') {
             return this.isLeader;
+        } else if (req.target === 'ffld' && 
+            req.ffCode !== this.fixedFieldCode) {
+            return false;
         }
 
         if (req.sfOffset !== undefined && 
@@ -106,7 +119,8 @@ export class EditableContentComponent
             req = {
                 fieldId: this.field ? this.field.fieldId : -1,
                 target: this.fieldType,
-                sfOffset: this.subfield ? this.subfield[2] : undefined
+                sfOffset: this.subfield ? this.subfield[2] : undefined,
+                ffCode: this.fixedFieldCode
             };
         }
 
@@ -136,6 +150,12 @@ export class EditableContentComponent
                 this.watchForUndoRedoRequests();
                 break;
 
+            case 'ffld':
+                this.applyFFOptions();
+                this.watchForFocusRequests();
+                this.watchForUndoRedoRequests();
+                break;
+
             case 'ind1':
             case 'ind2':
                 this.maxLength = 1;
@@ -157,6 +177,19 @@ export class EditableContentComponent
         }
     }
 
+    applyFFOptions() {
+        return this.tagTable.getFfFieldMeta(
+            this.fixedFieldCode, this.record.recordType())
+        .then(fieldMeta => {
+            if (fieldMeta) {
+                this.maxLength = fieldMeta.length || 1;
+            }
+        });
+
+        // Fixed field options change when the record type changes.
+        this.context.recordChange.subscribe(_ => this.applyFFOptions());
+    }
+
     // These are served dynamically to handle cases where a tag or
     // subfield is modified in place.
     contextMenuEntries(): ContextMenuEntry[] {
@@ -177,6 +210,10 @@ export class EditableContentComponent
             case 'ind2':
                 return this.tagTable.getIndicatorValues(
                     this.field.tag, this.fieldType);
+
+            case 'ffld': 
+                return this.tagTable.getFfValues(
+                    this.fixedFieldCode, this.record.recordType());
         }
 
         return null;
@@ -191,9 +228,28 @@ export class EditableContentComponent
             case 'tag': return this.field.tag;
             case 'sfc': return this.subfield[0];
             case 'sfv': return this.subfield[1];
-            case 'ind1': // thunk
-            case 'ind2': return this.field[this.fieldType];
+            case 'ind1': return this.field.ind1;
+            case 'ind2': return this.field.ind2;
+
+            case 'ffld': 
+                // When actively editing a fixed field, track its value
+                // in a local variable instead of pulling the value
+                // from record.extractFixedField(), which applies
+                // additional formattting, causing usability problems
+                // (e.g. unexpected spaces).  Once focus is gone, the
+                // view will be updated with the correctly formatted
+                // value.
+
+                if ( this.ffValue === undefined ||
+                    !this.context.lastFocused ||
+                    !this.focusRequestIsMe(this.context.lastFocused)) {
+
+                    this.ffValue = 
+                        this.record.extractFixedField(this.fixedFieldCode);
+                }
+                return this.ffValue;
         }
+        return 'X';
     }
 
     setContent(value: string, propagatBigText?: boolean, skipUndoTrack?: boolean) {
@@ -206,8 +262,13 @@ export class EditableContentComponent
             case 'tag': this.field.tag = value; break;
             case 'sfc': this.subfield[0] = value; break;
             case 'sfv': this.subfield[1] = value; break;
-            case 'ind1': // thunk
-            case 'ind2': this.field[this.fieldType] = value; break;
+            case 'ind1': this.field.ind1 = value; break;
+            case 'ind2': this.field.ind2 = value; break;
+            case 'ffld': 
+                // Track locally and propagate to the record.
+                this.ffValue = value;
+                this.record.setFixedField(this.fixedFieldCode, value);
+                break;
         }
 
         if (propagatBigText && this.bigText) {
@@ -224,6 +285,9 @@ export class EditableContentComponent
 
     trackTextChangeForUndo(value: string) {
 
+        // Human-driven changes invalidate the redo stack.
+        this.context.redoStack = [];
+
         const lastUndo = this.context.undoStack[0];
 
         if (lastUndo
@@ -280,7 +344,7 @@ export class EditableContentComponent
 
     inputSize(): number {
         if (this.maxLength) {
-            return this.maxLength;
+            return this.maxLength + 1;
         }
         // give at least 2+ chars space and grow with the content
         return Math.max(2, (this.getContent() || '').length) * 1.1;
@@ -349,8 +413,9 @@ export class EditableContentComponent
                 return;
         }
 
-        // None of the remaining key combos are supported by the LDR.
-        if (this.fieldType === 'ldr') { return; }
+        // None of the remaining key combos are supported by the LDR
+        // or fixed field editor.
+        if (this.fieldType === 'ldr' || this.fieldType === 'ffld') { return; }
 
         switch (evt.key) {
 
index 4e15a9a..1e976c8 100644 (file)
@@ -7,12 +7,13 @@ import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
 const STUB_DATA_00X = '                                        ';
 
 export type MARC_EDITABLE_FIELD_TYPE = 
-    'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv';
+    'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv' | 'ffld';
 
 export interface FieldFocusRequest {
     fieldId: number;
     target: MARC_EDITABLE_FIELD_TYPE;
     sfOffset?: number; // focus a specific subfield by its offset
+    ffCode?: string; // fixed field code
 }
 
 export class UndoRedoAction {
@@ -91,6 +92,7 @@ export class MarcEditContext {
     }
 
     requestUndo() {
+        console.debug('undo requested with stack size ', this.undoStack.length);
         const undo = this.undoStack.shift();
         if (undo) {
             undo.isRedo = false;
@@ -176,6 +178,9 @@ export class MarcEditContext {
 
     trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
 
+        // Human-driven changes invalidate the redo stack so clear it.
+        this.redoStack = [];
+
         const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
 
         let prevPos: FieldFocusRequest = null;
index 856e56d..e2e8976 100644 (file)
@@ -1,21 +1,16 @@
 
 <ng-container *ngIf="fieldMeta">
 
-  <div class="form-inline d-flex">
+  <div class="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($event.target.value)"
-      [ngModel]="fieldValue" 
-      [attr.maxlength]="fieldLength" [attr.size]="fieldLength"
-      [egContextMenu]="fieldValues"
-      (menuItemSelected)="valueChange($event.value)"
-      />
+      <div class="flex-5">
+        <eg-marc-editable-content [context]="context"
+          [fixedFieldCode]="fieldCode" fieldType="ffld" moreClasses="p-1">
+        </eg-marc-editable-content>
+      </div>
   </div>
 </ng-container>
-
index 703aaea..a81255d 100644 (file)
@@ -1,7 +1,8 @@
 import {Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
 import {MarcRecord} from './marcrecord';
 import {MarcEditContext} from './editor-context';
-import {TagTableService, ValueLabelPair} from './tagtable.service';
+import {TagTableService} from './tagtable.service';
 
 /**
  * MARC Fixed Field Editing Component
@@ -21,10 +22,7 @@ export class FixedFieldComponent implements OnInit {
 
     get record(): MarcRecord { return this.context.record; }
 
-    fieldValue: string;
-    fieldMeta: any;
-    fieldLength: number = null;
-    fieldValues: ValueLabelPair[] = null;
+    fieldMeta: IdlObject;
     randId = Math.floor(Math.random() * 10000000);
 
     constructor(private tagTable: TagTableService) {}
@@ -32,57 +30,16 @@ export class FixedFieldComponent implements OnInit {
     ngOnInit() {
         this.init().then(_ =>
             this.context.recordChange.subscribe(__ => this.init()));
-
     }
 
     init(): Promise<any> {
         if (!this.record) { return Promise.resolve(); }
 
-        this.fieldValues = null;
-        return this.tagTable.getFFPosTable(this.record.recordType())
-        .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.
-            this.fieldMeta = table.filter(field =>
-                    field.fixed_field === this.fieldCode
-                 && field.rec_type === this.record.recordType())[0];
-
-            if (!this.fieldMeta) {
-                // Not all record types have all field types.
-                return;
-            }
-
-            this.fieldLength = this.fieldMeta.length || 1;
-            this.fieldValue =
-                this.context.record.extractFixedField(this.fieldCode);
-
-            // Shuffling may occur with our fixed field as a result of
-            // external changes.
-            this.record.fixedFieldChange.subscribe(_ =>
-                this.fieldValue =
-                    this.context.record.extractFixedField(this.fieldCode)
-            );
-
-            return this.tagTable.getFFValueTable(this.record.recordType());
-
-        }).then(values => {
-            if (!values || !values[this.fieldCode]) { return; }
-
-            // extract the canned set of possible values for our
-            // fixed field.  Ignore those whose value exceeds the
-            // specified field length.
-            this.fieldValues = values[this.fieldCode]
-                .filter(val => val[0].length <= val[2])
-                .map(val => ({value: val[0], label: `${val[0]}: ${val[1]}`}))
-                .sort((a, b) => a.label < b.label ? -1 : 1);
-        });
-    }
-
-    valueChange(newVal: string) {
-        this.fieldValue = newVal;
-        this.context.record.setFixedField(this.fieldCode, this.fieldValue);
+        // 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())
+        .then(fieldMeta => this.fieldMeta = fieldMeta);
     }
 }
 
index 5f32ef6..cf21442 100644 (file)
           [disabled]="true"
           (click)="validate()" i18n>Validate</button></div>
         <div class="mt-2">
+          <button type="button" class="btn btn-outline-info" 
+            [disabled]="undoCount() < 1" (click)="undo()">
+            Undo <span class="badge badge-info">{{undoCount()}}</span>
+          </button>
+          <button type="button" class="btn btn-outline-info ml-2" 
+            [disabled]="redoCount() < 1" (click)="redo()">
+            Redo <span class="badge badge-info">{{redoCount()}}</span>
+          </button>
+        </div>
+        <div class="mt-2">
           <div class="form-check">
             <input class="form-check-input" type="checkbox" 
               [disabled]="true"
@@ -31,8 +41,9 @@
           </div>
         </div>
       </div>
+      <div class="col-lg-1">
+      </div>
     </div>
-
     <div *ngIf="showHelp" class="row m-2">
       <div class="col-lg-4">
         <ul>
     <!-- LEADER -->
     <div class="row pt-0 pb-0 pl-3 form-horizontal">
       <eg-marc-editable-content [context]="context" fieldType="tag" 
-        fieldText="LDR" i18n-fieldText moreClasses="p-1 pr-2">
+        fieldText="LDR" i18n-fieldText moreClasses="p-1">
       </eg-marc-editable-content>
 
       <eg-marc-editable-content [context]="context" fieldType="ldr"
-         moreClasses="p-1">
+         moreClasses="p-1 pr-2">
       </eg-marc-editable-content>
     </div>
 
index aa03c20..1c50c57 100644 (file)
@@ -45,11 +45,27 @@ export class MarcRichEditorComponent implements OnInit {
 
         return Promise.all([
             this.tagTable.loadTagTable({marcRecordType: this.context.recordType}),
-            this.tagTable.getFFPosTable(this.record.recordType()),
-            this.tagTable.getFFValueTable(this.record.recordType())
+            this.tagTable.getFfPosTable(this.record.recordType()),
+            this.tagTable.getFfValueTable(this.record.recordType())
         ]).then(_ => this.dataLoaded = true);
     }
 
+    undoCount(): number {
+        return this.context.undoStack.length;
+    }
+
+    redoCount(): number {
+        return this.context.redoStack.length;
+    }
+
+    undo() {
+        this.context.requestUndo();
+    }
+
+    redo() {
+        this.context.requestRedo();
+    }
+
     controlFields(): MarcField[] {
         return this.record.fields.filter(f => f.isControlfield());
     }
index 8e206c8..ec5abd0 100644 (file)
@@ -1,15 +1,12 @@
 import {Injectable, EventEmitter} from '@angular/core';
 import {map, tap} from 'rxjs/operators';
 import {StoreService} from '@eg/core/store.service';
+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';
-
-export interface ValueLabelPair {
-    value: string;
-    label: string;
-}
+import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
 
 interface TagTableSelector {
     marcFormat?: string;
@@ -44,13 +41,14 @@ export class TagTableService {
             fieldtags: {},
             indicators: {},
             sfcodes: {},
-            sfvalues: {}
+            sfvalues: {},
+            ffvalues: {}
         };
     }
 
     // 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): ValueLabelPair[] {
+    fromCache(dataType: string, which?: string, which2?: string): ContextMenuEntry[] {
         const part1 = this.extractedValuesCache[dataType][which];
         if (which2) {
             if (part1) {
@@ -62,7 +60,7 @@ export class TagTableService {
     }
 
     toCache(dataType: string, which: string, 
-        which2: string, values: ValueLabelPair[]): ValueLabelPair[] {
+        which2: string, values: ContextMenuEntry[]): ContextMenuEntry[] {
         const base = this.extractedValuesCache[dataType];
         const part1 = base[which];
 
@@ -76,7 +74,7 @@ export class TagTableService {
         return values;
     }
 
-    getFFPosTable(rtype: string): Promise<any> {
+    getFfPosTable(rtype: string): Promise<any> {
         const storeKey = 'FFPosTable_' + rtype;
 
         if (this.ffPosMap[rtype]) {
@@ -99,7 +97,7 @@ export class TagTableService {
         });
     }
 
-    getFFValueTable(rtype: string): Promise<any> {
+    getFfValueTable(rtype: string): Promise<any> {
 
         const storeKey = 'FFValueTable_' + rtype;
 
@@ -162,7 +160,7 @@ export class TagTableService {
         })).toPromise();
     }
 
-    getSubfieldCodes(tag: string): ValueLabelPair[] { 
+    getSubfieldCodes(tag: string): ContextMenuEntry[] { 
         if (!tag || !this.tagMap[tag]) { return null; }
 
         const cached = this.fromCache('sfcodes', tag);
@@ -176,7 +174,7 @@ export class TagTableService {
         return this.toCache('sfcodes', tag, null, list);
     }
 
-    getFieldTags(): ValueLabelPair[] {
+    getFieldTags(): ContextMenuEntry[] {
 
         const cached = this.fromCache('fieldtags');
         if (cached) { return cached; }
@@ -190,13 +188,13 @@ export class TagTableService {
         .sort((a, b) => a.label < b.label ? -1 : 1);
     }
 
-    getSubfieldValues(tag: string, sfCode: string): ValueLabelPair[] {
+    getSubfieldValues(tag: string, sfCode: string): ContextMenuEntry[] {
         if (!tag || !this.tagMap[tag]) { return []; }
 
         const cached = this.fromCache('sfvalues', tag, sfCode)
         if (cached) { return cached; }
 
-        const list: ValueLabelPair[] = [];
+        const list: ContextMenuEntry[] = [];
 
         this.tagMap[tag].subfields
         .filter(sf =>
@@ -215,7 +213,7 @@ export class TagTableService {
         return this.toCache('sfvalues', tag, sfCode, list);
     }
 
-    getIndicatorValues(tag: string, which: 'ind1' | 'ind2'): ValueLabelPair[] {
+    getIndicatorValues(tag: string, which: 'ind1' | 'ind2'): ContextMenuEntry[] {
         if (!tag || !this.tagMap[tag]) { return }
 
         const cached = this.fromCache('indicators', tag, which);
@@ -232,6 +230,45 @@ export class TagTableService {
 
         return this.toCache('indicators', tag, which, values);
     }
+
+
+    getFfFieldMeta(fieldCode: string, recordType: string): Promise<IdlObject> {
+        return this.getFfPosTable(recordType).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.
+            return table.filter(
+                field =>
+                    field.fixed_field === fieldCode
+                 && field.rec_type === recordType
+            )[0];
+        });
+    }
+
+
+    // Assumes getFfPosTable and getFfValueTable have already been
+    // invoked for the request record type.
+    getFfValues(fieldCode: string, recordType: string): ContextMenuEntry[] {
+
+        const cached = this.fromCache('ffvalues', recordType, fieldCode);
+        if (cached) { return cached; }
+
+        let values = this.ffValueMap[recordType];
+
+        if (!values || !values[fieldCode]) { return null; }
+
+        // extract the canned set of possible values for our
+        // fixed field.  Ignore those whose value exceeds the
+        // specified field length.
+        values = values[fieldCode]
+            .filter(val => val[0].length <= val[2])
+            .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);
+    }
 }
 
 
+