LP1852782 MARC editor Physical Characteristics Wizard
authorBill Erickson <berickxx@gmail.com>
Mon, 23 Dec 2019 22:33:18 +0000 (17:33 -0500)
committerBill Erickson <berickxx@gmail.com>
Fri, 21 Feb 2020 16:44:38 +0000 (11:44 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
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/marc-edit.module.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.ts [new file with mode: 0644]
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

index 1bcc19d..2b06e60 100644 (file)
@@ -128,7 +128,12 @@ export class EditableContentComponent
             this.editInput.select();
         }
 
-        if (!req) {
+        if (req) {
+            if (req.newText) {
+                this.setContent(req.newText);
+            }
+        } else {
+
             // Focus request may have come from keyboard navigation,
             // clicking, etc.  Model the event as a focus request
             // so it can be tracked the same.
index 28335f8..49ed524 100644 (file)
@@ -15,6 +15,11 @@ export interface FieldFocusRequest {
     target: MARC_EDITABLE_FIELD_TYPE;
     sfOffset?: number; // focus a specific subfield by its offset
     ffCode?: string; // fixed field code
+
+    // If set, an external source wants to modify the text content
+    // of an editable component (in a way that retains undo/redo
+    // functionality).
+    newText?: string;
 }
 
 export class UndoRedoAction {
index be62ae9..d9d2feb 100644 (file)
@@ -10,6 +10,7 @@ import {TagTableService} from './tagtable.service';
 import {EditableContentComponent} from './editable-content.component';
 import {AuthorityLinkingDialogComponent} from './authority-linking-dialog.component';
 import {MarcEditorDialogComponent} from './editor-dialog.component';
+import {PhysCharDialogComponent} from './phys-char-dialog.component';
 
 @NgModule({
     declarations: [
@@ -20,6 +21,7 @@ import {MarcEditorDialogComponent} from './editor-dialog.component';
         FixedFieldComponent,
         EditableContentComponent,
         MarcEditorDialogComponent,
+        PhysCharDialogComponent,
         AuthorityLinkingDialogComponent
     ],
     imports: [
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.html
new file mode 100644 (file)
index 0000000..161388a
--- /dev/null
@@ -0,0 +1,59 @@
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Physical Characteristics Wizard</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">
+
+    <div class="form-group row">
+      <label for="007-value" 
+        class="col-lg-4 col-form-label" i18n>007 Value</label>
+      <div class="col-lg-5 well-table">
+        <div class="well-value">
+          <!-- avoid newlines and spaces in the <pre> content -->
+          <pre class="pb-0 mb-0 pt-1">{{splitFieldData()[0]}}<span 
+            class="text-danger">{{splitFieldData()[1]}}</span>{{splitFieldData()[2]}}</pre>
+        </div>
+      </div>
+      <div class="col-lg-3 d-flex">
+        <button class="btn btn-outline-dark m-1 p-1 flex-1" (click)="reset()" i18n>Reset</button>
+        <button class="btn btn-outline-dark m-1 p-1 flex-1" (click)="reset(true)" i18n>Clear</button>
+      </div>
+    </div>
+
+    <div class="form-group row">
+      <label for="value-selector" class="col-lg-4 col-form-label">
+        <ng-container *ngIf="!selectorLabel" i18n>Category of Material</ng-container>
+        <ng-container *ngIf="selectorLabel">{{selectorLabel}}</ng-container>
+      </label>
+      <div class="col-lg-5">
+        <select id='value-selector' class="form-control" 
+          [(ngModel)]="selectorValue" (change)="selectorChanged()">
+          <option value=" " i18n>&lt;Unset&gt;</option>
+          <option *ngFor="let entry of selectorOptions" 
+            [ngValue]="entry.id" i18n>{{entry.id}}: {{entry.label}}</option>
+        </select>
+      </div>
+      <div class="col-lg-3 d-flex">
+        <button type="button" class="btn btn-outline-dark flex-1 m-1 p-1" 
+          (click)="prev()" [disabled]="step === 0" i18n>Previous</button>
+
+        <button type="button" class="btn btn-outline-dark flex-1 m-1 p-1"
+          (click)="next()" [disabled]="isLastStep()" i18n>Next</button>
+      </div>
+    </div>
+  </div>
+
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close(fieldData)" i18n>Apply</button>
+
+    <button type="button" class="btn btn-warning" 
+      (click)="close()" i18n>Cancel</button>
+  </div>
+
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.ts
new file mode 100644 (file)
index 0000000..6337002
--- /dev/null
@@ -0,0 +1,220 @@
+import {Component, ViewChild, Input, Output, OnInit, EventEmitter} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {MarcEditorDialogComponent} from './editor-dialog.component';
+import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/**
+ * 007 Physical Characteristics Dialog
+ *
+ * Note the dialog does not many direct changes to the bib field.
+ * It simply emits the new value on close, or null of the
+ * dialog canceled.
+ */
+
+@Component({
+  selector: 'eg-phys-char-dialog',
+  templateUrl: './phys-char-dialog.component.html'
+})
+
+export class PhysCharDialogComponent
+    extends DialogComponent implements OnInit {
+
+    // The 007 data
+    @Input() fieldData = '';
+
+    initialValue: string;
+
+    selectorLabel: string = null;
+    selectorValue: string;
+    selectorOptions: ComboboxEntry[] = [];
+
+    typeMap: ComboboxEntry[] = [];
+
+    sfMap: {[ptypeKey: string]: any[]} = {};
+    valueMap: {[ptypeKey: string]: ComboboxEntry[]} = {};
+
+    currentPtype: string;
+
+    // step is the 1-based position in the list of data slots for the
+    // currently selected type. step==0 means we are currently selecting
+    // the type.
+    step = 0;
+
+    // size and offset of the slot we're currently editing; this is
+    // maintained as a convenience for the highlighting of the currently
+    // active position
+    slotOffset = 0;
+    slotSize = 1;
+
+    constructor(
+        private modal: NgbModal,
+        private idl: IdlService,
+        private pcrud: PcrudService) {
+        super(modal);
+    }
+
+    ngOnInit() {
+        this.onOpen$.subscribe(_ => {
+            this.initialValue = this.fieldData;
+            this.reset();
+        });
+    }
+
+    // Chop the field data value into 3 parts, before, middle, and
+    // after, where 'middle' is the part we're currently editing.
+    splitFieldData(): string[] {
+        const data = this.fieldData;
+        return [
+            data.substring(0, this.slotOffset),
+            data.substring(this.slotOffset, this.slotOffset + this.slotSize),
+            data.substring(this.slotOffset + this.slotSize)
+        ];
+    }
+
+    setValuesForStep(): Promise<any> {
+        let promise;
+
+        if (this.step === 0) {
+            promise = this.getPhysCharTypeMap();
+        } else {
+            promise = this.currentSubfield().then(
+                subfield => this.getPhysCharValueMap(subfield.id()));
+        }
+
+        return promise.then(list => {
+            this.selectorOptions = list;
+            this.setSelectedOptionFromField();
+            this.setLabelForStep();
+        });
+    }
+
+    setLabelForStep() {
+        if (this.step === 0) {
+            this.selectorLabel = null;  // fall back to template value
+        } else {
+            this.currentSubfield().then(sf => this.selectorLabel = sf.label());
+        }
+    }
+
+    getStepSlot(): Promise<any[]> {
+        if (this.step === 0) { return Promise.resolve([0, 1]); }
+        return this.currentSubfield()
+            .then(sf => [sf.start_pos(), sf.length()]);
+    }
+
+    setSelectedOptionFromField() {
+        this.getStepSlot().then(slot => {
+            this.slotOffset = slot[0];
+            this.slotSize = slot[1];
+            this.selectorValue =
+                String.prototype.substr.apply(this.fieldData, slot) || ' ';
+        });
+    }
+
+    isLastStep(): boolean {
+        // This one is called w/ every digest, so avoid async
+        // calls.  Wait until we have loaded the current ptype
+        // subfields to determine if this is the last step.
+        return (
+            this.currentPtype &&
+            this.sfMap[this.currentPtype] &&
+            this.sfMap[this.currentPtype].length === this.step
+        );
+    }
+
+    selectorChanged() {
+
+        if (this.step === 0) {
+            this.currentPtype = this.selectorValue;
+            this.fieldData = this.selectorValue; // total reset
+
+        } else {
+            this.getStepSlot().then(slot => {
+
+                let value = this.fieldData;
+                const offset = slot[0];
+                const size = slot[1];
+                while (value.length < (offset + size)) { value += ' '; }
+
+                // Apply the value to the field in the required slot,
+                // then delete all data after "here", since those values
+                // no longer make sense.
+                const before = value.substr(0, offset);
+                this.fieldData = before + this.selectorValue.padEnd(size, ' ');
+                this.slotOffset = offset;
+                this.slotSize = size;
+            });
+        }
+    }
+
+    next() {
+        this.step++;
+        this.setValuesForStep();
+    }
+
+    prev() {
+        this.step--;
+        this.setValuesForStep();
+    }
+
+    currentSubfield(): Promise<any> {
+        return this.getPhysCharSubfieldMap(this.currentPtype)
+        .then(sfList => sfList[this.step - 1]);
+    }
+
+    reset(clear?: boolean) {
+        this.step = 0;
+        this.slotOffset = 0;
+        this.slotSize = 1;
+        this.fieldData = clear ? ' ' : this.initialValue;
+        this.currentPtype = this.fieldData.substr(0, 1);
+        this.setValuesForStep();
+    }
+
+    getPhysCharTypeMap(): Promise<ComboboxEntry[]> {
+        if (this.typeMap.length) {
+            return Promise.resolve(this.typeMap);
+        }
+
+        return this.pcrud.retrieveAll(
+            'cmpctm', {order_by: {cmpctm: 'label'}}, {atomic: true})
+        .toPromise().then(maps => {
+            return this.typeMap = maps.map(
+                map => ({id: map.ptype_key(), label: map.label()}));
+        });
+    }
+
+    getPhysCharSubfieldMap(ptypeKey: string): Promise<IdlObject[]> {
+
+        if (this.sfMap[ptypeKey]) {
+            return Promise.resolve(this.sfMap[ptypeKey]);
+        }
+
+        return this.pcrud.search('cmpcsm',
+            {ptype_key : ptypeKey},
+            {order_by : {cmpcsm : ['start_pos']}},
+            {atomic : true}
+        ).toPromise().then(maps => this.sfMap[ptypeKey] = maps);
+   }
+
+    getPhysCharValueMap(ptypeSubfield: string): Promise<ComboboxEntry[]> {
+
+        if (this.valueMap[ptypeSubfield]) {
+            return Promise.resolve(this.valueMap[ptypeSubfield]);
+        }
+
+        return this.pcrud.search('cmpcvm',
+            {ptype_subfield : ptypeSubfield},
+            {order_by : {cmpcsm : ['value']}},
+            {atomic : true}
+        ).toPromise().then(maps =>
+            this.valueMap[ptypeSubfield] = maps.map(
+                map => ({id: map.value(), label: map.label()}))
+        );
+   }
+}
+
+
index cf0f0ae..8a5df6d 100644 (file)
@@ -10,6 +10,8 @@
 <eg-authority-linking-dialog #authLinker [context]="context">
 </eg-authority-linking-dialog>
 
+<eg-phys-char-dialog #physCharDialog></eg-phys-char-dialog>
+
 <ng-template #subfieldChunk let-field="field" let-subfield="subfield">
 
   <!-- move these around depending on whether we are stacking subfields -->
         [context]="context" [field]="field" fieldType="cfld"
         ariaLabel="Control Field Content" i18n-ariaLabel moreClasses="p-1">
       </eg-marc-editable-content>
+
+      <ng-container *ngIf="field.tag === '007'">
+         <button class="btn btn-sm btn-info link-button"
+          (click)="openPhysCharDialog(field)">
+          <span class="material-icons">launch</span>
+        </button>
+      </ng-container>
     </div>
 
     <!-- data fields -->
index e4b3da6..4555e20 100644 (file)
@@ -9,6 +9,7 @@ import {TagTableService} from './tagtable.service';
 import {MarcRecord, MarcField} from './marcrecord';
 import {MarcEditContext} from './editor-context';
 import {AuthorityLinkingDialogComponent} from './authority-linking-dialog.component';
+import {PhysCharDialogComponent} from './phys-char-dialog.component';
 
 
 /**
@@ -35,6 +36,9 @@ export class MarcRichEditorComponent implements OnInit {
     @ViewChild('authLinker', {static: false})
         authLinker: AuthorityLinkingDialogComponent;
 
+    @ViewChild('physCharDialog', {static: false})
+        physCharDialog: PhysCharDialogComponent;
+
     constructor(
         private idl: IdlService,
         private net: NetService,
@@ -149,6 +153,11 @@ export class MarcRichEditorComponent implements OnInit {
     openLinkerDialog(field: MarcField) {
         this.authLinker.bibField = field;
         this.authLinker.open({size: 'xl'}).subscribe(newField => {
+
+            // The presence of newField here means the linker wants to
+            // replace the field with a new field from the authority
+            // record.  Otherwise, the original field may have been
+            // directly modified or the dialog canceled.
             if (!newField) { return; }
 
             // Performs an insert followed by a delete, so the two
@@ -161,6 +170,23 @@ export class MarcRichEditorComponent implements OnInit {
             this.context.setUndoGroupSize(2);
         });
     }
+
+    // 007 Physical characteristics wizard.
+    openPhysCharDialog(field: MarcField) {
+        this.physCharDialog.fieldData = field.data;
+
+        this.physCharDialog.open({size: 'lg'}).subscribe(
+            newData => {
+                if (newData) {
+                    this.context.requestFieldFocus({
+                        fieldId: field.fieldId,
+                        target: 'cfld',
+                        newText: newData
+                    });
+                }
+            }
+        );
+    }
 }