LP#1831785: eg-combobox support automatic pcrud-based IDL data sources
authorJason Etheridge <jason@EquinoxInitiative.org>
Wed, 5 Jun 2019 15:42:29 +0000 (11:42 -0400)
committerJane Sandberg <sandbej@linnbenton.edu>
Tue, 30 Jul 2019 14:58:19 +0000 (07:58 -0700)
This patch adds new idlClass and idlField attributes to eg-combobox to
enable it to automatically construct a pcrud-base data source. The
idlClass property specifies which table/class to use as the base
data source, while idlField specifies the label to display. If idlField
is not supplied, the label field defaults to "name".

It also adds an asyncSupportsEmptyTermClick option to specify that an
async data source (whether or not it is automatically built) is
expected to never return more than a couple hundred entries or so;
when supplied, it will allow fetching the entire contents of the
data source when the user clicks on the drop-down.

To test
-------
[1] Apply the patch and exercise the comboboxes on the
     Angular sandbox page (/eg2/en-US/staff/sandbox)

Sponsored-by: MassLNC
Sponsored-by: Georgia Public Library Service
Sponsored-by: Indiana State Library
Sponsored-by: CW MARS
Sponsored-by: King County Library System
Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html

index 63f964e..4a98fe3 100644 (file)
@@ -8,6 +8,7 @@ import {Observable, of, Subject} from 'rxjs';
 import {map, tap, reduce, mergeMap, mapTo, debounceTime, distinctUntilChanged, merge, filter} from 'rxjs/operators';
 import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
 import {StoreService} from '@eg/core/store.service';
+import {PcrudService} from '@eg/core/pcrud.service';
 
 export interface ComboboxEntry {
   id: any;
@@ -61,8 +62,16 @@ export class ComboboxComponent implements OnInit {
     @Input() startId: any;
     @Input() startIdFiresOnChange: boolean;
 
+    @Input() idlClass: string;
+    @Input() idlField: string;
     @Input() asyncDataSource: (term: string) => Observable<ComboboxEntry>;
 
+    // If true, an async data search is allowed to fetch all
+    // values when given an empty term. This should be used only
+    // if the maximum number of entries returned by the data source
+    // is known to be no more than a couple hundred.
+    @Input() asyncSupportsEmptyTermClick: boolean;
+
     // Useful for efficiently preventing duplicate async entries
     asyncIds: {[idx: string]: boolean};
 
@@ -94,6 +103,7 @@ export class ComboboxComponent implements OnInit {
     constructor(
       private elm: ElementRef,
       private store: StoreService,
+      private pcrud: PcrudService,
     ) {
         this.entrylist = [];
         this.asyncIds = {};
@@ -108,6 +118,18 @@ export class ComboboxComponent implements OnInit {
     }
 
     ngOnInit() {
+        if (this.idlClass) {
+            this.asyncDataSource = term => {
+                const field = this.idlField || 'name';
+                const args = {};
+                const extra_args = { order_by : {} };
+                args[field] = { 'ilike': `%${term}%`}; // could -or search on label
+                extra_args['order_by'][this.idlClass] = this.idlField || 'name';
+                return this.pcrud.search(this.idlClass, args, extra_args).pipe(map(data => {
+                    return {id: data.id(), label: data[field]()};
+                }));
+            };
+        }
     }
 
     openMe($event) {
@@ -197,12 +219,19 @@ export class ComboboxComponent implements OnInit {
             return of(term);
         }
 
+        let searchTerm: string;
+        searchTerm = term;
+        if (searchTerm === '_CLICK_' && this.asyncSupportsEmptyTermClick) {
+            searchTerm = '';
+        } else {
+        }
+
         return new Observable(observer => {
-            this.asyncDataSource(term).subscribe(
+            this.asyncDataSource(searchTerm).subscribe(
                 (entry: ComboboxEntry) => this.addAsyncEntry(entry),
                 err => {},
                 ()  => {
-                    observer.next(term);
+                    observer.next(searchTerm);
                     observer.complete();
                 }
             );
@@ -220,7 +249,7 @@ export class ComboboxComponent implements OnInit {
                 // action is a user click instead of a text entry.
                 // This tells the filter to show all values in sync mode.
                 this.click$.pipe(filter(() =>
-                    !this.instance.isPopupOpen() && !this.asyncDataSource
+                    !this.instance.isPopupOpen()
                 )).pipe(mapTo('_CLICK_'))
             ),
 
@@ -229,12 +258,7 @@ export class ComboboxComponent implements OnInit {
             map((term: string) => {
 
                 if (term === '' || term === '_CLICK_') {
-                    // Avoid displaying the existing entries on-click
-                    // for async sources, becuase that implies we have
-                    // the full data set. (setting?)
-                    if (this.asyncDataSource) {
-                        return [];
-                    } else {
+                    if (!this.asyncDataSource) {
                         // In sync mode, a post-focus empty search or
                         // click event displays the whole list.
                         return this.entrylist;
index 38908ae..d7cff9f 100644 (file)
@@ -69,7 +69,7 @@
   <div class="col-lg-3">
     <eg-help-popover helpText="You have to type to see any options in this dropdown."></eg-help-popover>
     <eg-combobox
-      placeholder="Combobox with dynamic data"
+      placeholder="Combobox with dynamic data that does not enable click if no search term is supplied"
       [asyncDataSource]="cbAsyncSource"></eg-combobox>
   </div>
 </div>
     </eg-org-select>
   </div>
 </div>
+<div class="row mb-3">
+  <div class="col-lg-4">
+  </div>
+  <div class="col-lg-3">
+    <eg-combobox placeholder="Combobox with @idlClass = 'aou' @idlField='shortname'" [idlClass]="'aou'" [idlField]="'shortname'" [asyncSupportsEmptyTermClick]="true">
+    </eg-combobox>
+  </div>
+  <div class="col-lg-3">
+    <eg-combobox placeholder="Combobox with @idlClass = 'csp'" [idlClass]="'csp'" [asyncSupportsEmptyTermClick]="true">
+    </eg-combobox>
+  </div>
+</div>
+<div class="row mb-3">
+  <div class="col-lg-4">
+  </div>
+  <div class="col-lg-3">
+    <eg-combobox placeholder="Combobox with @idlClass = 'aou'" [idlClass]="'aou'" [asyncSupportsEmptyTermClick]="true">
+    </eg-combobox>
+  </div>
+  <div class="col-lg-3">
+  </div>
+</div>
 <!-- /Progress Dialog Experiments ----------------------------- -->
 
 <!-- eg strings -->