LP1850938 Angular Catalog Preferences Page
authorBill Erickson <berickxx@gmail.com>
Thu, 31 Oct 2019 21:56:39 +0000 (17:56 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 21 Feb 2020 16:44:38 +0000 (11:44 -0500)
Adds a new "Catalog Preferences" interface, accessible directly from the
catalog.  The UI houses the search preferences (default search lib,
preferred library, default search tab), a new staff-specific
hits-per-page setting.  Other preferences may be added later.

Adds support for selecting a default search tab using the existing
'eg.search.adv_pane' setting.

Reduce API call count by loading more of the catalog preference settings
in the main batch invoked by the page resolver.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Open-ILS/src/eg2/src/app/staff/catalog/basket-actions.component.html
Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/staff/catalog/prefs.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/prefs.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts
Open-ILS/src/eg2/src/app/staff/catalog/routing.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/search-templates.component.ts
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-prefs.sql [new file with mode: 0644]

index 2f32c22..752557c 100644 (file)
       </a>
     </div>
   </div>
-  <div class="">
+  <div class="pr-1">
     <div ngbDropdown placement="bottom-right">
       <button class="btn btn-light" id="basketActions"
         [disabled]="!basketCount()"
         ngbDropdownToggle i18n>Basket Actions</button>
       <div ngbDropdownMenu aria-labelledby="basketActions">
-      <button class="dropdown-item" 
-        (click)="applyAction('view')" i18n>View Basket</button>
-      <button class="dropdown-item" 
-        (click)="applyAction('hold')" i18n>Place Hold</button>
-      <button class="dropdown-item" 
-        (click)="applyAction('print')" i18n>Print Title Details</button>
-      <button class="dropdown-item" 
-        (click)="applyAction('email')" i18n>Email Title Details</button>
-      <button class="dropdown-item" 
-        (click)="applyAction('bucket')" i18n>Add Basket to Bucket</button>
-      <button class="dropdown-item" 
-        (click)="applyAction('export_marc')" i18n>Export Records</button>
-      <button class="dropdown-item" 
-        (click)="applyAction('clear')" i18n>Clear Basket</button>
+        <button class="dropdown-item"
+          (click)="applyAction('view')" i18n>View Basket</button>
+        <button class="dropdown-item"
+          (click)="applyAction('hold')" i18n>Place Hold</button>
+        <button class="dropdown-item"
+          (click)="applyAction('print')" i18n>Print Title Details</button>
+        <button class="dropdown-item"
+          (click)="applyAction('email')" i18n>Email Title Details</button>
+        <button class="dropdown-item"
+          (click)="applyAction('bucket')" i18n>Add Basket to Bucket</button>
+        <button class="dropdown-item"
+          (click)="applyAction('export_marc')" i18n>Export Records</button>
+        <button class="dropdown-item"
+          (click)="applyAction('clear')" i18n>Clear Basket</button>
+      </div>
     </div>
   </div>
+  <div>
+    <!-- Note this Prefernces links is not specific to Basket handling,
+        but it's here since it allowed for consistent formatting -->
+    <a routerLink="/staff/catalog/prefs" queryParamsHandling="merge">
+      <button class="btn btn-light" i18n>Catalog Preferences</button>
+    </a>
+  </div>
 </div>
index 810b950..d9e2143 100644 (file)
@@ -30,6 +30,7 @@ import {CnBrowseComponent} from './cnbrowse.component';
 import {CnBrowseResultsComponent} from './cnbrowse/results.component';
 import {SearchTemplatesComponent} from './search-templates.component';
 import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
+import {PreferencesComponent} from './prefs.component';
 
 @NgModule({
   declarations: [
@@ -54,6 +55,7 @@ import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
     SearchTemplatesComponent,
     CnBrowseComponent,
     OpacViewComponent,
+    PreferencesComponent,
     CnBrowseResultsComponent
   ],
   imports: [
index 86501fc..e6da358 100644 (file)
@@ -23,6 +23,9 @@ export class StaffCatalogService {
     // TODO: does unapi support pref-lib for result-page copy counts?
     prefOrg: IdlObject;
 
+    // Default search tab
+    defaultTab: string;
+
     // Cache the currently selected detail record (i.g. catalog/record/123)
     // summary so the record detail component can avoid duplicate fetches
     // during record tab navigation.
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/prefs.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/prefs.component.html
new file mode 100644 (file)
index 0000000..b7ed34e
--- /dev/null
@@ -0,0 +1,84 @@
+<eg-catalog-search-form #searchForm></eg-catalog-search-form>
+
+<eg-staff-banner bannerText="Catalog Preferences"></eg-staff-banner>
+
+<eg-string #successMsg i18n-text text="Setting Update Succeeded"></eg-string>
+<eg-string #failMsg i18n-text text="Setting Update Failed"></eg-string>
+
+<div class="row border-bottom border-secondary p-2 m-2">
+  <div class="col-lg-2 offset-lg-1">
+    <label for="default-lib-selector" class="font-weight-bold" i18n>
+      Default Search Library
+    </label>
+  </div>
+  <div class="col-lg-2">
+    <eg-org-select domId="default-lib-selector" 
+      (onChange)="orgChanged($event, 'eg.search.search_lib')"
+      [applyOrgId]="settings['eg.search.search_lib']">
+    </eg-org-select>
+  </div>
+  <div class="col-lg-6" i18n>
+    The default search library setting determines what library is
+    searched from the advanced search screen and portal page by
+    default. Manual selection of a search library will override it. One
+    recommendation is to set the search library to the highest point you
+    would normally want to search.
+  </div>
+</div>
+
+<div class="row border-bottom border-secondary p-2 m-2">
+  <div class="col-lg-2 offset-lg-1">
+    <label for="pref-lib-selector" class="font-weight-bold" i18n>
+      Preferred Library
+    </label>
+  </div>
+  <div class="col-lg-2">
+    <eg-org-select domId="pref-lib-selector" 
+      (onChange)="orgChanged($event, 'eg.search.pref_lib')"
+      [applyOrgId]="settings['eg.search.pref_lib']">
+    </eg-org-select>
+  </div>
+  <div class="col-lg-6" i18n>
+    The preferred library is used to show copies and URIs regardless
+    of the library searched. One recommendation is to set this to your
+    workstation library so that local copies show up first in search
+    results.
+  </div>
+</div>
+
+<div class="row border-bottom border-secondary p-2 m-2">
+  <div class="col-lg-2 offset-lg-1">
+    <label for="def-pane-selector" class="font-weight-bold" i18n>
+      Default Search Pane
+    </label>
+  </div>
+  <div class="col-lg-2">
+    <eg-combobox [selectedId]="settings['eg.search.adv_pane']"
+      (onChange)="paneChanged($event)">
+      <eg-combobox-entry entryId="advanced" entryLabel="Keyword Search"></eg-combobox-entry>
+      <eg-combobox-entry entryId="numeric" entryLabel="Numeric Search"></eg-combobox-entry>
+      <eg-combobox-entry entryId="expert" entryLabel="MARC Search"></eg-combobox-entry>
+      <eg-combobox-entry entryId="browse" entryLabel="Browse"></eg-combobox-entry>
+      <eg-combobox-entry entryId="cnbrowse" entryLabel="Shelf Browse"></eg-combobox-entry>
+    </eg-combobox>
+  </div>
+  <div class="col-lg-6" i18n>
+    Focus this search tab by default when opening new catalog pages.
+  </div>
+</div>
+
+<div class="row border-bottom border-secondary p-2 m-2">
+  <div class="col-lg-2 offset-lg-1">
+    <label for="pref-lib-selector" class="font-weight-bold" i18n>
+      Search Results Per Page
+    </label>
+  </div>
+  <div class="col-lg-2">
+    <input type="number" min="1" max="100" class="form-control"
+      [(ngModel)]="settings['eg.catalog.results.count']"
+      (change)="countChanged()"/>
+  </div>
+  <div class="col-lg-6" i18n>
+    The number of search results to display per page.
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/prefs.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/prefs.component.ts
new file mode 100644 (file)
index 0000000..7d81a07
--- /dev/null
@@ -0,0 +1,73 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+import {StaffCatalogService} from './catalog.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/* Component for managing catalog preferences */
+
+const CATALOG_PREFS = [
+    'eg.search.search_lib',
+    'eg.search.pref_lib',
+    'eg.search.adv_pane',
+    'eg.catalog.results.count'
+];
+
+@Component({
+  templateUrl: 'prefs.component.html'
+})
+export class PreferencesComponent implements OnInit {
+
+    settings: Object = {};
+
+    @ViewChild('successMsg', {static: false}) successMsg: StringComponent;
+    @ViewChild('failMsg', {static: false}) failMsg: StringComponent;
+
+    constructor(
+        private store: ServerStoreService,
+        private toast: ToastService,
+        private staffCat: StaffCatalogService,
+    ) {}
+
+    ngOnInit() {
+        this.staffCat.createContext();
+
+        // Pre-fetched by the resolver.
+        this.store.getItemBatch(CATALOG_PREFS)
+        .then(settings => this.settings = settings);
+    }
+
+    orgChanged(org: IdlObject, setting: string) {
+        const localVar = setting === 'eg.search.search_lib' ?
+            'defaultSearchOrg' : 'prefOrg';
+
+        this.updateValue(setting, org ? org.id() : null)
+        .then(val => this.staffCat[localVar] = val);
+    }
+
+    paneChanged(entry: ComboboxEntry) {
+        this.updateValue('eg.search.adv_pane', entry ? entry.id : null)
+        .then(value => this.staffCat.defaultTab = value);
+    }
+
+    countChanged() {
+        this.updateValue('eg.catalog.results.count',
+            this.settings['eg.catalog.results.count'] || null)
+        .then(value => {
+            this.staffCat.searchContext.pager.limit = value || 20;
+        });
+    }
+
+    updateValue(setting: string, value: any): Promise<any> {
+        const promise = (value === null) ?
+            this.store.removeItem(setting) :
+            this.store.setItem(setting, value);
+
+        return promise
+            .then(_ => this.toast.success(this.successMsg.text))
+            .then(_ => value);
+    }
+}
+
index 1b6dac1..842893c 100644 (file)
@@ -43,18 +43,27 @@ export class CatalogResolver implements Resolve<Promise<any[]>> {
         return this.store.getItemBatch([
             'eg.search.search_lib',
             'eg.search.pref_lib',
+            'eg.search.adv_pane',
+            'eg.catalog.results.count',
             'cat.holdings_show_empty_org',
             'cat.holdings_show_empty',
             'cat.marcedit.stack_subfields',
             'cat.marcedit.flateditor',
             'cat.holdings_show_copies',
             'cat.holdings_show_vols',
+            'opac.staff_saved_search.size',
+            'eg.catalog.search_templates',
             'opac.staff_saved_search.size'
         ]).then(settings => {
             this.staffCat.defaultSearchOrg =
                 this.org.get(settings['eg.search.search_lib']);
             this.staffCat.prefOrg =
                 this.org.get(settings['eg.search.pref_lib']);
+            this.staffCat.defaultTab = settings['eg.search.adv_pane'];
+            if (settings['eg.catalog.results.count']) {
+               this.staffCat.defaultSearchLimit =
+                  Number(settings['eg.catalog.results.count']);
+            }
         });
     }
 }
index b702905..85e958b 100644 (file)
@@ -8,6 +8,7 @@ import {HoldComponent} from './hold/hold.component';
 import {BrowseComponent} from './browse.component';
 import {CnBrowseComponent} from './cnbrowse.component';
 import {CanDeactivateGuard} from '@eg/share/util/can-deactivate.guard';
+import {PreferencesComponent} from './prefs.component';
 
 const routes: Routes = [{
   path: '',
@@ -35,6 +36,10 @@ const routes: Routes = [{
     path: 'cnbrowse',
     component: CnBrowseComponent,
     resolve: {catResolver : CatalogResolver}
+  }, {
+    path: 'prefs',
+    component: PreferencesComponent,
+    resolve: {catResolver : CatalogResolver}
   }
 ];
 
index 42e7086..357f7be 100644 (file)
@@ -7,6 +7,13 @@ import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search
 import {StaffCatalogService} from './catalog.service';
 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 
+// Maps opac-style default tab names to local tab names.
+const LEGACY_TAB_NAME_MAP = {
+    expert: 'marc',
+    numeric: 'ident',
+    advanced: 'term'
+};
+
 @Component({
   selector: 'eg-catalog-search-form',
   styleUrls: ['search-form.component.css'],
@@ -87,9 +94,18 @@ export class SearchFormComponent implements OnInit, AfterViewInit {
                     this.searchTab = 'ident';
                 } else if (this.context.browseSearch.isSearchable()) {
                     this.searchTab = 'browse';
-                } else {
-                    // Default tab
+                } else if (this.context.termSearch.isSearchable()) {
                     this.searchTab = 'term';
+
+                } else {
+
+                    this.searchTab =
+                        LEGACY_TAB_NAME_MAP[this.staffCat.defaultTab]
+                        || this.staffCat.defaultTab || 'term';
+
+                }
+
+                if (this.searchTab === 'term') {
                     this.refreshCopyLocations();
                 }
             }
index 8a1eb7d..4697450 100644 (file)
@@ -62,10 +62,9 @@ export class SearchTemplatesComponent extends DialogComponent implements OnInit
 
     ngOnInit() {
         this.context = this.staffCat.searchContext;
-        console.log('ngOnInit() with selected = ', this.staffCat.selectedTemplate);
 
-        this.org.settings('opac.staff_saved_search.size').then(sets => {
-            const size = sets['opac.staff_saved_search.size'] || 0;
+        this.serverStore.getItem('opac.staff_saved_search.size')
+        .then(size => {
             if (!size) { return; }
 
             this.recentSearchesCount = Number(size);
index a49cf02..257117b 100644 (file)
@@ -20308,3 +20308,13 @@ VALUES (
     )
 );
 
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.catalog.results.count', 'gui', 'integer',
+    oils_i18n_gettext(
+        'eg.catalog.results.count',
+        'Catalog Results Page Size',
+        'cwst', 'label'
+    )
+);
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-prefs.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-prefs.sql
new file mode 100644 (file)
index 0000000..1380afa
--- /dev/null
@@ -0,0 +1,15 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.catalog.results.count', 'gui', 'integer',
+    oils_i18n_gettext(
+        'eg.catalog.results.count',
+        'Catalog Results Page Size',
+        'cwst', 'label'
+    )
+);
+
+COMMIT;