LP#1800481 Vandelay import form templates
authorBill Erickson <berickxx@gmail.com>
Mon, 29 Oct 2018 16:31:26 +0000 (12:31 -0400)
committerDan Wells <dbw2@calvin.edu>
Tue, 19 Feb 2019 22:56:13 +0000 (17:56 -0500)
Support saving MARC Import form values as named templates.  Values are
stored as (by defualt) workstation settings.  A template may be selected
as the default and templates may be deleted.

Includes release notes update angular vandelay.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Dan Wells <dbw2@calvin.edu>

Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.html
Open-ILS/src/eg2/src/app/staff/cat/vandelay/import.component.ts
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql [new file with mode: 0644]
docs/RELEASE_NOTES_NEXT/Cataloging/vandelay-angular-port.adoc

index e8adff3..323623c 100644 (file)
@@ -132,6 +132,12 @@ export class ComboboxComponent implements OnInit {
         this.applySelection();
     }
 
+    // Manually set the selected value by ID.
+    // This does NOT fire the onChange handler.
+    applyEntryId(entryId: any) {
+        this.selected = this.entrylist.filter(e => e.id === entryId)[0];
+    }
+
     onBlur() {
         // When the selected value is a string it means we have either
         // no value (user cleared the input) or a free-text value.
index c852332..58b3bb0 100644 (file)
 <div class="common-form striped-odd form-validated ml-3 mr-3">
   <div class="row">
     <div class="col-lg-3">
+      <label i18n>Apply/Create Form Template</label>
+    </div>
+    <div class="col-lg-3">
+      <eg-combobox #formTemplateSelector
+        (onChange)="templateSelectorChange($event)"
+        [allowFreeText]="true"
+        [startId]="selectedTemplate"
+        [startIdFiresOnChange]="true"
+        [entries]="formatTemplateEntries()"
+        placeholder="Apply or Create Form Template..." i18n-placeholder>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-6">
+      <button class="btn btn-success"
+        [disabled]="!selectedTemplate"
+        (click)="saveTemplate()" i18n>Save As New Template</button>
+      <button class="btn btn-outline-primary ml-3"
+        [disabled]="!selectedTemplate"
+        (click)="markTemplateDefault()" i18n>Mark Template as Default</button>
+      <button class="btn btn-danger ml-3"
+        [disabled]="!selectedTemplate"
+        (click)="deleteTemplate()" i18n>Delete Template</button>
+    </div>
+  </div>
+
+  <div class="row">
+    <div class="col-lg-3">
       <label i18n>Record Type</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox (onChange)="selectEntry($event, 'recordType')"
-        [disabled]="importSelection()"
-        [required]="true"
+      <eg-combobox #recordTypeSelector
+        (onChange)="selectEntry($event, 'recordType')"
+        [disabled]="importSelection()" [required]="true"
         [startId]="recordType" placeholder="Record Type..." i18n-placeholder>
         <eg-combobox-entry entryId="bib" entryLabel="Bibliographic Records" 
           i18n-entryLabel></eg-combobox-entry>
@@ -31,7 +58,8 @@
       <label i18n>Select a Record Source</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('bibSources')" 
+      <eg-combobox #bibSourceSelector
+        [entries]="formatEntries('bibSources')" 
         (onChange)="selectEntry($event, 'bibSources')"
         [startId]="selectedBibSource"
         placeholder="Record Source..." i18n-placeholder>
@@ -68,7 +96,8 @@
       <label i18n>Record Match Set</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('matchSets')" 
+      <eg-combobox #matchSetSelector
+        [entries]="formatEntries('matchSets')" 
         [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
         [startId]="selectedMatchSet || defaultMatchSet"
         (onChange)="selectEntry($event, 'matchSets')"
       <label i18n>Holdings Import Profile</label>
     </div>
     <div class="col-lg-3"> <!-- TODO disable for authority -->
-      <eg-combobox [entries]="formatEntries('importItemDefs')"
+      <eg-combobox #holdingsProfileSelector
+        [entries]="formatEntries('importItemDefs')"
         [startId]="selectedHoldingsProfile"
         [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
         (onChange)="selectEntry($event, 'importItemDefs')"
       <label i18n>Merge Profile</label>
     </div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('mergeProfiles')"
+      <eg-combobox #mergeProfileSelector
+        [entries]="formatEntries('mergeProfiles')"
         (onChange)="selectEntry($event, 'mergeProfiles')"
         placeholder="Merge Profile..." i18n-placeholder>
       </eg-combobox>
     <div class="col-lg-3">
       <label i18n>Insufficient Quality Fall-Through Profile</label></div>
     <div class="col-lg-3">
-      <eg-combobox [entries]="formatEntries('mergeProfiles')"
+      <eg-combobox #fallThruMergeProfileSelector
+        [entries]="formatEntries('mergeProfiles')"
         (onChange)="selectEntry($event, 'FallThruMergeProfile')"
         placeholder="Fall-Through Merge Profile..." i18n-placeholder>
       </eg-combobox>
index 3b36f6a..74341b9 100644 (file)
@@ -6,13 +6,33 @@ import {EventService} from '@eg/core/event.service';
 import {OrgService} from '@eg/core/org.service';
 import {AuthService} from '@eg/core/auth.service';
 import {ToastService} from '@eg/share/toast/toast.service';
-import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ComboboxComponent, 
+    ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {VandelayService, VandelayImportSelection,
   VANDELAY_UPLOAD_PATH} from './vandelay.service';
 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
 import {Subject} from 'rxjs/Subject';
+import {ServerStoreService} from '@eg/core/server-store.service';
+
+const TEMPLATE_SETTING_NAME = 'eg.cat.vandelay.import.templates';
+
+const TEMPLATE_ATTRS = [
+    'recordType',
+    'selectedBibSource',
+    'selectedMatchSet',
+    'mergeOnExact',
+    'importNonMatch',
+    'mergeOnBestMatch',
+    'mergeOnSingleMatch',
+    'autoOverlayAcqCopies',
+    'selectedHoldingsProfile',
+    'selectedMergeProfile',
+    'selectedFallThruMergeProfile',
+    'selectedTrashGroups',
+    'minQualityRatio'
+];
 
 interface ImportOptions {
     session_key: string;
@@ -25,6 +45,7 @@ interface ImportOptions {
     merge_profile?: any;
     fall_through_merge_profile?: any;
     strip_field_groups?: number[];
+    match_quality_ratio: number,
     exit_early: boolean;
 }
 
@@ -77,6 +98,10 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     // Optional enqueue/import tracker session name.
     sessionName: string;
 
+    selectedTemplate: string;
+    formTemplates: {[name: string]: any};
+    newTemplateName: string;
+
     @ViewChild('fileSelector') private fileSelector;
     @ViewChild('uploadProgress') 
         private uploadProgress: ProgressInlineComponent;
@@ -85,6 +110,22 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     @ViewChild('importProgress') 
         private importProgress: ProgressInlineComponent;
 
+    // Need these refs so values can be applied via external stimuli
+    @ViewChild('formTemplateSelector') 
+        private formTemplateSelector: ComboboxComponent;
+    @ViewChild('recordTypeSelector')
+        private recordTypeSelector: ComboboxComponent;
+    @ViewChild('bibSourceSelector')
+        private bibSourceSelector: ComboboxComponent;
+    @ViewChild('matchSetSelector')
+        private matchSetSelector: ComboboxComponent;
+    @ViewChild('holdingsProfileSelector')
+        private holdingsProfileSelector: ComboboxComponent;
+    @ViewChild('mergeProfileSelector')
+        private mergeProfileSelector: ComboboxComponent;
+    @ViewChild('fallThruMergeProfileSelector')
+        private fallThruMergeProfileSelector: ComboboxComponent;
+
     constructor(
         private http: HttpClient,
         private toast: ToastService,
@@ -92,6 +133,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         private net: NetService,
         private auth: AuthService,
         private org: OrgService,
+        private store: ServerStoreService,
         private vandelay: VandelayService
     ) {
         this.applyDefaults();
@@ -102,6 +144,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
         this.selectedBibSource = 1; // default to system local
         this.recordType = 'bib';
         this.bibTrashGroups = [];
+        this.formTemplates = {};
 
         if (this.vandelay.importSelection) {
 
@@ -161,12 +204,36 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             this.vandelay.getBibTrashGroups().then(
                 groups => this.bibTrashGroups = groups),
             this.org.settings(['vandelay.default_match_set']).then(
-                s => this.defaultMatchSet = s['vandelay.default_match_set'])
+                s => this.defaultMatchSet = s['vandelay.default_match_set']),
+            this.loadTemplates()
         ];
 
         return Promise.all(promises);
     }
 
+    loadTemplates() {
+        this.store.getItem(TEMPLATE_SETTING_NAME).then(
+            templates => {
+                this.formTemplates = templates || {};
+
+                Object.keys(this.formTemplates).forEach(name => {
+                    if (this.formTemplates[name].default) {
+                        this.selectedTemplate = name;
+                    }
+                });
+            }
+        );
+    }
+
+    formatTemplateEntries(): ComboboxEntry[] {
+        const entries = [];
+
+        Object.keys(this.formTemplates || {}).forEach(
+            name => entries.push({id: name, label: name}));
+
+        return entries;
+    }
+
     // Format typeahead data sets
     formatEntries(etype: string): ComboboxEntry[] {
         const rtype = this.recordType;
@@ -469,6 +536,7 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
             merge_profile: this.selectedMergeProfile,
             fall_through_merge_profile: this.selectedFallThruMergeProfile,
             strip_field_groups: this.selectedTrashGroups,
+            match_quality_ratio: this.minQualityRatio,
             exit_early: true
         };
 
@@ -487,5 +555,67 @@ export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
     openQueue() {
         console.log('opening queue ' + this.activeQueueId);
     }
+
+    saveTemplate() {
+
+        const template = {};
+        TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
+
+        console.debug("Saving import profile", template);
+
+        this.formTemplates[this.selectedTemplate] = template;
+        return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+    }
+
+    markTemplateDefault() {
+        
+        Object.keys(this.formTemplates).forEach(
+            name => delete this.formTemplates.default
+        );
+
+        this.formTemplates[this.selectedTemplate].default = true;
+
+        return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+    }
+
+    templateSelectorChange(entry: ComboboxEntry) {
+
+        if (!entry) {
+            this.selectedTemplate = '';
+            return;
+        }
+
+        this.selectedTemplate = entry.label; // label == name
+
+        if (entry.freetext) {
+            // User is entering a new template name.
+            // Nothing to apply.
+            return;
+        }
+
+        // User selected an existing template, apply it to the form.
+
+        const template = this.formTemplates[entry.id];
+
+        // Copy the template values into "this"
+        TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
+
+        // Some values must be manually passed to the combobox'es
+
+        this.recordTypeSelector.applyEntryId(this.recordType);
+        this.bibSourceSelector.applyEntryId(this.selectedBibSource);
+        this.matchSetSelector.applyEntryId(this.selectedMatchSet);
+        this.holdingsProfileSelector
+            .applyEntryId(this.selectedHoldingsProfile);
+        this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
+        this.fallThruMergeProfileSelector
+            .applyEntryId(this.selectedFallThruMergeProfile);
+    }
+
+    deleteTemplate() {
+        delete this.formTemplates[this.selectedTemplate];
+        this.formTemplateSelector.selected = null;
+        return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+    }
 }
 
index e8ef9ed..c10e34f 100644 (file)
@@ -19520,6 +19520,16 @@ VALUES (
     'bool'
 );
 
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.cat.vandelay.import.templates', 'cat', 'object',
+    oils_i18n_gettext(
+        'eg.cat.vandelay.import.templates',
+        'Vandelay Import Form Templates',
+        'cwst', 'label'
+    )
+);
+
 
 INSERT into config.workstation_setting_type (name, grp, datatype, label)
 VALUES (
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.vandelay-template-settings.sql
new file mode 100644 (file)
index 0000000..bd104ef
--- /dev/null
@@ -0,0 +1,15 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('YYYY', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.cat.vandelay.import.templates', 'cat', 'object',
+    oils_i18n_gettext(
+        'eg.cat.vandelay.import.templates',
+        'Vandelay Import Form Templates',
+        'cwst', 'label'
+    )
+);
+
+COMMIT;
index 6b5173b..2baa53b 100644 (file)
@@ -6,6 +6,13 @@ Angular(6) instead of Dojo.  The functionality is consistent with the
 previous version of the interface, with minor UI adjustments to match
 the Angular style, plus one new interface called 'Recent Imports'
 
+Import Templates
+++++++++++++++++
+
+Users may now saves sets of import attributes from the MARC import form as 
+named templates.  Users may select a default template, applied on page load 
+by default, and users may delete existing templates.
+
 Recent Imports Tab
 ++++++++++++++++++