From cad0d77286bf4d7d3b2686f3a4030e2640b52078 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 23 Dec 2019 17:33:18 -0500 Subject: [PATCH] LP1852782 MARC editor Physical Characteristics Wizard Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg --- .../marc-edit/editable-content.component.ts | 7 +- .../staff/share/marc-edit/editor-context.ts | 5 + .../staff/share/marc-edit/marc-edit.module.ts | 2 + .../marc-edit/phys-char-dialog.component.html | 59 +++++ .../marc-edit/phys-char-dialog.component.ts | 220 ++++++++++++++++++ .../marc-edit/rich-editor.component.html | 9 + .../share/marc-edit/rich-editor.component.ts | 26 +++ 7 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.ts diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts index 1bcc19dd65..2b06e6031d 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts @@ -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. diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts index 28335f81cb..49ed524e1f 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts @@ -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 { 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 index be62ae9403..d9d2febe9f 100644 --- 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 @@ -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 index 0000000000..161388a32b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.html @@ -0,0 +1,59 @@ + + + + + + + + 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 index 0000000000..6337002cb6 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/phys-char-dialog.component.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + + 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 { + + 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()})) + ); + } +} + + 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 index cf0f0aebb6..8a5df6d60f 100644 --- 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 @@ -10,6 +10,8 @@ + + @@ -150,6 +152,13 @@ [context]="context" [field]="field" fieldType="cfld" ariaLabel="Control Field Content" i18n-ariaLabel moreClasses="p-1"> + + + + 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 index e4b3da641d..4555e2030d 100644 --- 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 @@ -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 + }); + } + } + ); + } } -- 2.43.2