[open-ils-commits] [GIT] Evergreen ILS branch master updated. 2c0df3989352f96e3a7edc5c1a99d570dfb9b610

Evergreen Git git at git.evergreen-ils.org
Thu Aug 1 09:53:57 EDT 2019


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  2c0df3989352f96e3a7edc5c1a99d570dfb9b610 (commit)
       via  d0ab509dab731f1f03cd32d05c49256f9cb30ba2 (commit)
       via  bb8b2321b16cded0ed6abc2143503776854b64c9 (commit)
       via  56163b124f7b2321b867324c68a7637d23ea8492 (commit)
       via  aef077e88ad73d7e9381f13d9cb378d29e399521 (commit)
       via  59a69b150f13a82c72de5ebbdde918ef18e696dc (commit)
       via  28bf803625dd359cfd327ad94aa18f4d625ad8e4 (commit)
       via  bd4c8c4669397bbd3b9cad6a686c3f004577d6bb (commit)
       via  fb3925bd56dc98d35d62822c918b5bc7add9b258 (commit)
       via  58a5fbc2c6fc2ed878daf324c243ecbed168aa62 (commit)
      from  18277d2154ada19acddfd8ef294f41084a8b87f2 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 2c0df3989352f96e3a7edc5c1a99d570dfb9b610
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Wed Jul 31 14:19:08 2019 -0700

    LP1831788: (follow-up) removing small linting errors, unused imports
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts
index f9bacf4dad..bb597cd86e 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts
@@ -1,6 +1,5 @@
-import {Component, Input, OnInit, Host} from '@angular/core';
-import {GridContext, GridColumn, GridColumnSet,
-    GridDataSource} from './grid';
+import {Component, Input, OnInit} from '@angular/core';
+import {GridContext, GridColumn, GridColumnSet} from './grid';
 
 @Component({
   selector: 'eg-grid-column-width',
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
index 95af28d8bf..b616b82480 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
@@ -1,5 +1,5 @@
 import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
-import {GridColumn, GridColumnSet} from './grid';
+import {GridColumn} from './grid';
 import {GridComponent} from './grid.component';
 
 @Component({
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
index e6a36523dc..f5931b4059 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
@@ -1,9 +1,7 @@
 import {Component, Input, OnInit, QueryList, ViewChildren} from '@angular/core';
-import {GridContext, GridColumn, GridRowSelector,
-    GridColumnSet, GridDataSource} from './grid';
+import {GridContext, GridColumn} from './grid';
 import {IdlObject} from '@eg/core/idl.service';
-import {ComboboxComponent,
-    ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
 import {OrgService} from '@eg/core/org.service';
@@ -218,7 +216,7 @@ export class GridFilterControlComponent implements OnInit {
 
         if ( (col.filterOperator !== 'null') && (col.filterOperator !== 'not null') &&
              (!col.filterValue || col.filterValue === '') &&
-             (col.filterValue != '0') ) {
+             (col.filterValue !== '0') ) {
             // if value is empty and we're _not_ checking for null/not null, clear
             // the filter
             delete this.context.dataSource.filters[col.name];
@@ -226,7 +224,6 @@ export class GridFilterControlComponent implements OnInit {
         } else {
             let op: string = col.filterOperator;
             let val: string = col.filterValue;
-            const name: string = col.name;
             if (col.filterOperator === 'null') {
                 op  = '=';
                 val = null;
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
index fbc826c062..ae55c56f32 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
@@ -1,8 +1,6 @@
-import {Component, Input, OnInit, Host} from '@angular/core';
+import {Component, Input, OnInit} from '@angular/core';
 import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
-import {Pager} from '@eg/share/util/pager';
-import {GridColumn, GridColumnSet, GridToolbarButton,
-    GridToolbarAction, GridContext, GridDataSource} from '@eg/share/grid/grid';
+import {GridToolbarButton, GridToolbarAction, GridContext} from '@eg/share/grid/grid';
 import {GridColumnWidthComponent} from './grid-column-width.component';
 import {GridPrintComponent} from './grid-print.component';
 
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
index b043a418be..69edbf3130 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
@@ -1,12 +1,10 @@
 import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
-    OnDestroy, HostListener, ViewEncapsulation, QueryList, ViewChildren} from '@angular/core';
-import {Subscription} from 'rxjs';
+    OnDestroy, ViewEncapsulation} from '@angular/core';
 import {IdlService} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
 import {FormatService} from '@eg/core/format.service';
 import {GridContext, GridColumn, GridDataSource, GridRowFlairEntry} from './grid';
-import {GridFilterControlComponent} from './grid-filter-control.component';
 
 /**
  * Main grid entry point.
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
index 18d4f7adbe..600f815564 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -417,7 +417,7 @@ export class GridRowSelector {
     // In some contexts (template checkboxes) the value for an index is
     // set to false to deselect instead of having it removed (via deselect()).
     // NOTE GridRowSelector has no knowledge of when a row is no longer
-    // present in the grid.  Use GridContext.getSelectedRows() to get 
+    // present in the grid.  Use GridContext.getSelectedRows() to get
     // list of selected rows that are still present in the grid.
     selected() {
         return Object.keys(this.indexes).filter(

commit d0ab509dab731f1f03cd32d05c49256f9cb30ba2
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Jul 31 16:15:21 2019 -0400

    LP#1831788: (follow-up) properly handle case where filter value is 0
    
    To test
    -------
    [1] In the sandbox's copy grid, display the status column and filter
        by the 'Available' status (ccs.id = 0).
    [2] Verify that the correct rows are returned.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
index fbc59186c2..e6a36523dc 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
@@ -217,7 +217,8 @@ export class GridFilterControlComponent implements OnInit {
         if (col.filterOperator === undefined) { col.filterOperator = '='; }
 
         if ( (col.filterOperator !== 'null') && (col.filterOperator !== 'not null') &&
-             (!col.filterValue || col.filterValue === '') ) {
+             (!col.filterValue || col.filterValue === '') &&
+             (col.filterValue != '0') ) {
             // if value is empty and we're _not_ checking for null/not null, clear
             // the filter
             delete this.context.dataSource.filters[col.name];
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
index 163a1cb723..a816caffb8 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
@@ -186,7 +186,7 @@ export class SandboxComponent implements OnInit {
             return this.pcrud.search('acp',
                 query, {
                 flesh: 1,
-                flesh_fields: {acp: ['location']},
+                flesh_fields: {acp: ['location','status']},
                 offset: pager.offset,
                 limit: pager.limit,
                 order_by: orderBy

commit bb8b2321b16cded0ed6abc2143503776854b64c9
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jul 31 14:21:18 2019 -0400

    LP1831788 Add EgCoreModule for CommonWidgetsModule, etc.
    
    Collect core objects into their own module so they may be imported
    without requiring task-specific modules to import EgCommonModule, which
    provides a lot more than most sub-modules need.
    
    In the case of CommonWidgetsModule, it required access to the
    FormatPipe, which is a core object, originally exported from
    EgCommonModule.  However, EgCommonModule was overkill for
    CommonWidgetsModule and importing it would likely have created other
    dependency problems.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts
index a4e402680d..76394c3924 100644
--- a/Open-ILS/src/eg2/src/app/common.module.ts
+++ b/Open-ILS/src/eg2/src/app/common.module.ts
@@ -1,11 +1,12 @@
 /**
  * Modules, services, and components used by all apps.
  */
-import {CommonModule, DatePipe, CurrencyPipe} from '@angular/common';
+import {CommonModule} from '@angular/common';
 import {NgModule, ModuleWithProviders} from '@angular/core';
 import {RouterModule} from '@angular/router';
 import {FormsModule} from '@angular/forms';
 import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {EgCoreModule} from '@eg/core/core.module';
 
 /*
 Note core services are injected into 'root'.
@@ -13,7 +14,6 @@ They do not have to be added to the providers list.
 */
 
 // consider moving these to core...
-import {FormatService, FormatValuePipe} from '@eg/core/format.service';
 import {HatchService} from '@eg/share/print/hatch.service';
 import {PrintService} from '@eg/share/print/print.service';
 
@@ -36,20 +36,21 @@ import {BoolDisplayComponent} from '@eg/share/util/bool.component';
     PromptDialogComponent,
     ProgressInlineComponent,
     ProgressDialogComponent,
-    BoolDisplayComponent,
-    FormatValuePipe
+    BoolDisplayComponent
   ],
   imports: [
     CommonModule,
     FormsModule,
     RouterModule,
-    NgbModule
+    NgbModule,
+    EgCoreModule
   ],
   exports: [
     CommonModule,
     RouterModule,
     NgbModule,
     FormsModule,
+    EgCoreModule,
     PrintComponent,
     DialogComponent,
     AlertDialogComponent,
@@ -58,7 +59,6 @@ import {BoolDisplayComponent} from '@eg/share/util/bool.component';
     ProgressInlineComponent,
     ProgressDialogComponent,
     BoolDisplayComponent,
-    FormatValuePipe
   ]
 })
 
@@ -69,11 +69,8 @@ export class EgCommonModule {
         return {
             ngModule: EgCommonModule,
             providers: [
-                DatePipe,
-                CurrencyPipe,
                 HatchService,
-                PrintService,
-                FormatService
+                PrintService
             ]
         };
     }
diff --git a/Open-ILS/src/eg2/src/app/core/core.module.ts b/Open-ILS/src/eg2/src/app/core/core.module.ts
new file mode 100644
index 0000000000..82052f591d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/core.module.ts
@@ -0,0 +1,29 @@
+/**
+ * Core objects.
+ * Note that core services are generally defined with
+ * @Injectable({providedIn: 'root'}) so they are globally available
+ * and do not require entry in our 'providers' array.
+ */
+import {NgModule} from '@angular/core';
+import {CommonModule, DatePipe, CurrencyPipe} from '@angular/common';
+import {FormatService, FormatValuePipe} from './format.service';
+
+ at NgModule({
+  declarations: [
+    FormatValuePipe
+  ],
+  imports: [
+    CommonModule
+  ],
+  exports: [
+    CommonModule,
+    FormatValuePipe
+  ],
+  providers: [
+    DatePipe,
+    CurrencyPipe
+  ]
+})
+
+export class EgCoreModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts b/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
index 0f15c98b67..71c5d3023c 100644
--- a/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
+++ b/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
@@ -7,6 +7,7 @@ import {NgModule, ModuleWithProviders} from '@angular/core';
 import {CommonModule} from '@angular/common';
 import {FormsModule} from '@angular/forms';
 import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {EgCoreModule} from '@eg/core/core.module';
 import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
 import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component';
 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
@@ -22,12 +23,14 @@ import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
   imports: [
     CommonModule,
     FormsModule,
-    NgbModule
+    NgbModule,
+    EgCoreModule
   ],
   exports: [
     CommonModule,
     FormsModule,
     NgbModule,
+    EgCoreModule,
     ComboboxComponent,
     ComboboxEntryComponent,
     DateSelectComponent,

commit 56163b124f7b2321b867324c68a7637d23ea8492
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jul 31 14:56:43 2019 -0400

    LP1831788 dialog dismissal and i18n repairs
    
    Update sandbox dialog error handling to treat all error conditions as
    errors.  Dialogs no longer produce an error on dismissal, they just
    complete the obvservable.
    
    Add i18n-placeholder attributes to some grid filter placeholder text.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html
index 6367fb6e83..742cdca481 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html
@@ -14,7 +14,8 @@
             </div>
           </div>
         </div>
-        <eg-combobox [idlClass]="col.idlFieldDef.class" (onChange)="applyLinkFilter($event, col)" placeholder="Enter value to filter by"></eg-combobox>
+        <eg-combobox [idlClass]="col.idlFieldDef.class" (onChange)="applyLinkFilter($event, col)" 
+          i18n-placeholder placeholder="Enter value to filter by"></eg-combobox>
       </div>
     </div>
     <div *ngSwitchCase="'bool'">
@@ -67,7 +68,8 @@
             </div>
           </div>
         </div>
-        <input type="text" class="form-control" [(ngModel)]="col.filterValue" (keyup.enter)="applyFilter(col)" [disabled]="col.filterInputDisabled" placeholder="Enter value to filter by">
+        <input type="text" class="form-control" [(ngModel)]="col.filterValue" (keyup.enter)="applyFilter(col)" 
+          [disabled]="col.filterInputDisabled" i18n-placeholder placeholder="Enter value to filter by">
       </div>
     </div>
     <div *ngSwitchCase="'int'">
@@ -250,7 +252,8 @@
             </div>
           </div>
         </div>
-        <eg-org-select [applyOrgId]="col.filterValue" (onChange)="applyOrgFilter($event, col)" placeholder="Enter library to filter by" #ousel></eg-org-select>
+        <eg-org-select [applyOrgId]="col.filterValue" (onChange)="applyOrgFilter($event, col)" 
+          i18n-placeholder placeholder="Enter library to filter by" #ousel></eg-org-select>
       </div>
     </div>
     <div *ngSwitchDefault>I don't know how to filter {{col.name}} - {{col.datatype}}</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
index 1d47a070db..163a1cb723 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
@@ -351,13 +351,11 @@ export class SandboxComponent implements OnInit {
                     resolve(ok);
                 },
                 rejection => {
-                    if (!rejection.dismissed) {
-                        this.updateFailedString.current()
-                            .then(str => this.toast.danger(str));
-                        reject(rejection);
-                    }
+                    this.updateFailedString.current()
+                        .then(str => this.toast.danger(str));
+                    reject(rejection);
                 }
-            )
+            );
         });
     }
 }

commit aef077e88ad73d7e9381f13d9cb378d29e399521
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Tue Jul 30 16:05:27 2019 -0400

    LP#1831788: (follow-up) rename grid method
    
    Specifically, reloadSansPagerReset() =>  reloadWithoutPagerReset()
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
index b1b29895f3..b043a418be 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
@@ -195,8 +195,8 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
     reload() {
         this.context.reload();
     }
-    reloadSansPagerReset() {
-        this.context.reloadSansPagerReset();
+    reloadWithoutPagerReset() {
+        this.context.reloadWithoutPagerReset();
     }
 
 
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
index 7740bddbc1..18d4f7adbe 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -564,7 +564,7 @@ export class GridContext {
         });
     }
 
-    reloadSansPagerReset() {
+    reloadWithoutPagerReset() {
         setTimeout(() => {
             this.dataSource.reset();
             this.dataSource.requestPage(this.pager);
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
index 0c637b5a7e..1d47a070db 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
@@ -347,7 +347,7 @@ export class SandboxComponent implements OnInit {
                 ok => {
                     this.successString.current()
                         .then(str => this.toast.success(str));
-                    this.acpGrid.reloadSansPagerReset();
+                    this.acpGrid.reloadWithoutPagerReset();
                     resolve(ok);
                 },
                 rejection => {

commit 59a69b150f13a82c72de5ebbdde918ef18e696dc
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Tue Jul 30 16:02:49 2019 -0400

    LP#1831788: (follow-up) update sandbox example
    
    - dialog adjusted for LP#1823041 changes
    - typo fixed
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
index 92c0e84b79..37e46d9c86 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
@@ -223,7 +223,7 @@
 <eg-fm-record-editor #acpEditDialog idlClass="acp" hiddenFields="call_number,creator,create_date,editor,edit_time,loan_duration,fine_level,dummy_author,dummy_isbn,ref,floating,holdable,circ_as_type,active_date,mint_condition,cost,deleted,deposit,deposit_amount,circulate,status_changed_time,copy_number">
 </eg-fm-record-editor>
 <eg-string #successString text="Updated succeeded!" i18n-text></eg-string>
-<eg-string #updatedFailedString text="Updated failed!" i18n-text></eg-string>
+<eg-string #updateFailedString text="Updated failed!" i18n-text></eg-string>
 
 <h4>PCRUD auto flesh and FormatService detection</h4>
 <div *ngIf="aMetarecord">Fingerprint: {{aMetarecord}}</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
index 0fd1664d98..0c637b5a7e 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
@@ -339,22 +339,26 @@ export class SandboxComponent implements OnInit {
       this.numConfirmDialog.open();
     }
 
-    showEditDialog(idlThing: IdlObject) {
+    showEditDialog(idlThing: IdlObject): Promise<any> {
         this.editDialog.mode = 'update';
         this.editDialog.recId = idlThing['id']();
-        return this.editDialog.open({size: 'lg'}).then(
-            ok => {
-                this.successString.current()
-                    .then(str => this.toast.success(str));
-                this.acpGrid.reloadSansPagerReset();
-            },
-            rejection => {
-                if (!rejection.dismissed) {
-                    this.updateFailedString.current()
-                        .then(str => this.toast.danger(str));
+        return new Promise((resolve, reject) => {
+            this.editDialog.open({size: 'lg'}).subscribe(
+                ok => {
+                    this.successString.current()
+                        .then(str => this.toast.success(str));
+                    this.acpGrid.reloadSansPagerReset();
+                    resolve(ok);
+                },
+                rejection => {
+                    if (!rejection.dismissed) {
+                        this.updateFailedString.current()
+                            .then(str => this.toast.danger(str));
+                        reject(rejection);
+                    }
                 }
-            }
-        );
+            )
+        });
     }
 }
 

commit 28bf803625dd359cfd327ad94aa18f4d625ad8e4
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Jun 5 13:27:43 2019 -0400

    LP#1831788: add user-level release notes
    
    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: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/Architecture/Angular_Grid_Improvements.adoc b/docs/RELEASE_NOTES_NEXT/Architecture/Angular_Grid_Improvements.adoc
new file mode 100644
index 0000000000..92927c9235
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Architecture/Angular_Grid_Improvements.adoc
@@ -0,0 +1,10 @@
+Angular Grid Improvements
+^^^^^^^^^^^^^^^^^^^^^^^^^
+Grids in new Angular staff interfaces now have options to
+
+* allow users to filter results per-column
+* make the grid header in tall/long grids sticky (i.e., the
+  grid header continues to be displayed while the user
+  scrolls through the grid
+* allow users to edit a record in a grid and save the results
+  without losing one's place in grid paging.

commit bd4c8c4669397bbd3b9cad6a686c3f004577d6bb
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Jun 5 11:18:14 2019 -0400

    LP#1813788: add example of grid improvements to sandbox
    
    This patch adds a new grid to the Angular sandbox page
    that demonstrates the following features introduced
    in the previous patch:
    
    - grid filtering
    - stick grid headers
    - grid reloads without losing one's current paging
      location
    
    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: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
index f6df154702..92c0e84b79 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
@@ -182,6 +182,7 @@
   [rowFlairIsEnabled]="true"
   [rowFlairCallback]="btGridRowFlairCallback"
   [cellClassCallback]="btGridCellClassCallback"
+  [stickyHeader]="true"
   [sortable]="true">
   <eg-grid-toolbar-action label="Action that needs a single row" i18n-label
     (onClick)="complimentEvergreen($event)" [disableOnRows]="notOneSelectedRow">
@@ -201,6 +202,29 @@
 
 <br/><br/>
 
+<h4>Grid with filtering</h4>
+<eg-grid #acpGrid idlClass="acp"
+  [dataSource]="acpSource"
+  [filterable]="true"
+  [sortable]="true"
+  [showLinkSelectors]="true"
+  [stickyHeader]="true"
+  showFields="barcode,location,circ_lib,price,dummy_title,create_date"
+>
+  <eg-grid-toolbar-action label="Edit Selected" i18n-label [action]="editSelected">
+  </eg-grid-toolbar-action>
+  <eg-grid-column [sortable]="true" [filterable]="false"  path="barcode"></eg-grid-column>
+  <eg-grid-column [sortable]="true" path="circ_lib"></eg-grid-column>
+  <eg-grid-column [sortable]="true" path="price"></eg-grid-column>
+  <eg-grid-column [sortable]="true" path="dummy_title"></eg-grid-column>
+  <eg-grid-column [sortable]="true" path="create_date"></eg-grid-column>
+</eg-grid>
+
+<eg-fm-record-editor #acpEditDialog idlClass="acp" hiddenFields="call_number,creator,create_date,editor,edit_time,loan_duration,fine_level,dummy_author,dummy_isbn,ref,floating,holdable,circ_as_type,active_date,mint_condition,cost,deleted,deposit,deposit_amount,circulate,status_changed_time,copy_number">
+</eg-fm-record-editor>
+<eg-string #successString text="Updated succeeded!" i18n-text></eg-string>
+<eg-string #updatedFailedString text="Updated failed!" i18n-text></eg-string>
+
 <h4>PCRUD auto flesh and FormatService detection</h4>
 <div *ngIf="aMetarecord">Fingerprint: {{aMetarecord}}</div>
 
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
index d3410b6d05..0fd1664d98 100644
--- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
@@ -13,10 +13,12 @@ import {Pager} from '@eg/share/util/pager';
 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {PrintService} from '@eg/share/print/print.service';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
-import {FormatService} from '@eg/core/format.service';
 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 import {FormGroup, FormControl} from '@angular/forms';
 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {FormatService} from '@eg/core/format.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {GridComponent} from '@eg/share/grid/grid.component';
 
 @Component({
   templateUrl: 'sandbox.component.html'
@@ -53,6 +55,12 @@ export class SandboxComponent implements OnInit {
     cbAsyncSource: (term: string) => Observable<ComboboxEntry>;
 
     btSource: GridDataSource = new GridDataSource();
+    acpSource: GridDataSource = new GridDataSource();
+    editSelected: (rows: IdlObject[]) => void;
+    @ViewChild('acpGrid') acpGrid: GridComponent;
+    @ViewChild('acpEditDialog') editDialog: FmRecordEditorComponent;
+    @ViewChild('successString') successString: StringComponent;
+    @ViewChild('updateFailedString') updateFailedString: StringComponent;
     world = 'world'; // for local template version
     btGridTestContext: any = {hello : this.world};
 
@@ -157,6 +165,50 @@ export class SandboxComponent implements OnInit {
             }));
         };
 
+        this.acpSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderBy: any = {acp: 'id'};
+            if (sort.length) {
+                orderBy.acp = sort[0].name + ' ' + sort[0].dir;
+            }
+
+            // base query to grab everything
+            const base: Object = {};
+            base[this.idl.classes['acp'].pkey] = {'!=' : null};
+            const query: any = new Array();
+            query.push(base);
+
+            // and add any filters
+            Object.keys(this.acpSource.filters).forEach(key => {
+                Object.keys(this.acpSource.filters[key]).forEach(key2 => {
+                    query.push(this.acpSource.filters[key][key2]);
+                });
+            });
+            return this.pcrud.search('acp',
+                query, {
+                flesh: 1,
+                flesh_fields: {acp: ['location']},
+                offset: pager.offset,
+                limit: pager.limit,
+                order_by: orderBy
+            });
+        };
+
+        this.editSelected = (idlThings: IdlObject[]) => {
+
+            // Edit each IDL thing one at a time
+            const editOneThing = (thing: IdlObject) => {
+                if (!thing) { return; }
+
+                this.showEditDialog(thing).then(
+                    () => editOneThing(idlThings.shift()));
+            };
+
+            editOneThing(idlThings.shift());
+        };
+        this.acpGrid.onRowActivate.subscribe(
+            (acpRec: IdlObject) => { this.showEditDialog(acpRec); }
+        );
+
         this.complimentEvergreen = (rows: IdlObject[]) => alert('Evergreen is great!');
         this.notOneSelectedRow = (rows: IdlObject[]) => (rows.length !== 1);
 
@@ -287,6 +339,23 @@ export class SandboxComponent implements OnInit {
       this.numConfirmDialog.open();
     }
 
+    showEditDialog(idlThing: IdlObject) {
+        this.editDialog.mode = 'update';
+        this.editDialog.recId = idlThing['id']();
+        return this.editDialog.open({size: 'lg'}).then(
+            ok => {
+                this.successString.current()
+                    .then(str => this.toast.success(str));
+                this.acpGrid.reloadSansPagerReset();
+            },
+            rejection => {
+                if (!rejection.dismissed) {
+                    this.updateFailedString.current()
+                        .then(str => this.toast.danger(str));
+                }
+            }
+        );
+    }
 }
 
 

commit fb3925bd56dc98d35d62822c918b5bc7add9b258
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Jun 5 11:19:36 2019 -0400

    LP#1831788: add result filtering and other improvements to the Angular eg-grid
    
    This patch enables users to filter results in Angular eg-grids that
    use PCRUD-based data sources.
    
    Filtering can be enabled in an eg-grid defintion by adding the following
    attribute to <eg-grid>:
    
      [filterable]="true"
    
    If, for some reason, a particular column should not be filterable by the
    user, filtering can be disabled by passing false to the [filterable]
    attribute of an <eg-grid-column> element like this:
    
      <eg-grid-column [sortable]="true" [filterable]="false"  path="barcode"></eg-grid-column>
    
    When filtering is enabled, a new section of the grid header is displayed that
    includes, for each filterable column:
    
    * A drop-down menu letting the user specify an operator such as
      "is exactly", "exists" (i.e., is not null), "is greater than", and so
      forth. The drop-down also allows the user to clear a filter for a
      specific column or re-apply it after changing the operator.
    * An input widget for setting the value to filter on. The type of input
      displayed depend on the IDL type of the column. For example, a text field
      will use a normal text <input>; an OU field will use an eg-org-select,
      a link to another IDL class will use a combobox, a timestamp field
      will use an eg-date-select, and so forth.
    * A separate display of the current operator.
    
    When filtering is enabled, the grid will also display a "Remove Filters" button
    in the action bar.
    
    Under the hood, the widgets for entering filtering parameters expect
    the data source to have a "filters" key that in turn contains a
    dictionary of PCRUD-style filtering conditions indexed by column name.
    Consequently, a grid data source that wants to use filtering should
    look something like this:
    
        this.acpSource.getRows = (pager: Pager, sort: any[]) => {
            const orderBy: any = {acp: 'id'};
            if (sort.length) {
                orderBy.acp = sort[0].name + ' ' + sort[0].dir;
            }
    
            // base query to grab everything
            let base: Object = {};
            base[this.idl.classes['acp'].pkey] = {'!=' : null};
            var query: any = new Array();
            query.push(base);
    
            // and add any filters
            Object.keys(this.acpSource.filters).forEach(key => {
                Object.keys(this.acpSource.filters[key]).forEach(key2 => {
                    query.push(this.acpSource.filters[key][key2]);
                });
            });
            return this.pcrud.search('acp',
                query, {
                flesh: 1,
                flesh_fields: {acp: ['location']},
                offset: pager.offset,
                limit: pager.limit,
                order_by: orderBy
            });
        };
    
    This patch also adds two related grid options, sticky headers and the ability
    to reload the data source without losing one's current place in page.
    
    Sticky headers are enabled by adding the following attribute to the
    <eg-grid> element:
    
      [stickyHeader]="true"
    
    When this is enabled, as the user scrolls the grid from top to bottom, the
    header row, including the filter controls, will continue to remain visible
    at the top of the viewport until the user scrolls past the end of the
    grid entirely.
    
    Reloading grids without losing the current paging settings can now be
    done by a caller (such as code that opens an edit modal)  invoking a new
    reloadSansPagerReset() method.
    
    Implementation Notes
    --------------------
    [1] This patch adds special-case logic for handling the "dob" column,
        which is the sole date column in the Evergreen schema. Longer-term,
        it would be better to define a new "date" IDL field type that's
        distinct from "timestamp".
    [2] stickyHeader currently makes only the grid header sticky, not both
        the header and the action bar. This outcome is a result of z-index
        messiness with the ng-bootstrap dropdown menu which I couldn't get
        past. However, the forthcoming grid context menus hopefully will
        be a reasonable amelioration.
    [3] During testing it became evident that it would be handy to add
        support for open-ils.fielder as a grid data source at some
        point in the near future.
    
    To test
    -------
    General testing can be done using the new second grid in the
    Angular sandbox page added by the following test. Things to check
    include:
    
    - grid filter operators are displayed
    - hitting enter in text inputs activates the filter
    - the grid-level Remove Filters button works
    - per-column filter clearing works
    - operators have the expected results
    - The header of both grids on the sandbox page is sticky. This can
      be tested by increasing the row count in the second grid and
      scrolling.
    
    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: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
index fc18fc7258..95af28d8bf 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
@@ -27,6 +27,9 @@ export class GridColumnComponent implements OnInit {
     // If true, boolean fields support 3 values: true, false, null (unset)
     @Input() ternaryBool: boolean;
 
+    // result filtering
+    @Input() filterable: boolean;
+
     // Display date and time when datatype = timestamp
     @Input() datePlusTime: boolean;
 
@@ -57,6 +60,7 @@ export class GridColumnComponent implements OnInit {
         col.cellContext = this.cellContext;
         col.disableTooltip = this.disableTooltip;
         col.isSortable = this.sortable;
+        col.isFilterable = this.filterable;
         col.isMultiSortable = this.multiSortable;
         col.datatype = this.datatype;
         col.datePlusTime = this.datePlusTime;
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html
new file mode 100644
index 0000000000..6367fb6e83
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html
@@ -0,0 +1,281 @@
+<div *ngIf="col.isFilterable" class="eg-grid-filter-control">
+  <div [ngSwitch]="col.datatype">
+    <div *ngSwitchCase="'link'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+          <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyFilter(col)" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <eg-combobox [idlClass]="col.idlFieldDef.class" (onChange)="applyLinkFilter($event, col)" placeholder="Enter value to filter by"></eg-combobox>
+      </div>
+    </div>
+    <div *ngSwitchCase="'bool'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+          <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyBooleanFilter(col)" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <select class="custom-select" [(ngModel)]="col.filterValue" (change)="applyBooleanFilter(col)">
+          <option value="" i18n>Any</option>
+          <option value="t" i18n>True</option>
+          <option value="f" i18n>False</option>
+        </select>
+      </div>
+    </div>
+    <div *ngSwitchCase="'text'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+          <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <label for="eg-filter-op-select-{{col.name}}" i18n>Operator</label>
+              <select id="eg-filter-op-select-{{col.name}}" class="form-control" [(ngModel)]="col.filterOperator" (change)="operatorChanged(col)">
+                <option value="=" i18n>Is exactly</option>
+                <option value="!=" i18n>Is not</option>
+                <option value="like" i18n>Contains</option>
+                <option value="not like" i18n>Does not contain</option>
+                <option value="startswith" i18n>Starts with</option>
+                <option value="endswith" i18n>Ends with</option>
+                <option value="not null" i18n>Exists</option>
+                <option value="null" i18n>Does not exist</option>
+                <option value="<" i18n>Is less than</option>
+                <option value=">" i18n>Is greater than</option>
+                <option value="<=" i18n>Is less than or equal to</option>
+                <option value=">=" i18n>Is greater than or equal to</option>
+              </select>
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyFilter(col)" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <input type="text" class="form-control" [(ngModel)]="col.filterValue" (keyup.enter)="applyFilter(col)" [disabled]="col.filterInputDisabled" placeholder="Enter value to filter by">
+      </div>
+    </div>
+    <div *ngSwitchCase="'int'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+          <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <label for="eg-filter-op-select-{{col.name}}" i18n>Operator</label>
+              <select id="eg-filter-op-select-{{col.name}}" class="form-control" [(ngModel)]="col.filterOperator" (change)="operatorChanged(col)">
+                <option value="=" i18n>Is exactly</option>
+                <option value="!=" i18n>Is not</option>
+                <option value="not null" i18n>Exists</option>
+                <option value="null" i18n>Does not exist</option>
+                <option value="<" i18n>Is less than</option>
+                <option value=">" i18n>Is greater than</option>
+                <option value="<=" i18n>Is less than or equal to</option>
+                <option value=">=" i18n>Is greater than or equal to</option>
+              </select>
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyFilter(col)" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <input type="number" min="0" step="1" class="form-control" [(ngModel)]="col.filterValue" (keyup.enter)="applyFilter(col)" [disabled]="col.filterInputDisabled">
+      </div>
+    </div>
+    <div *ngSwitchCase="'id'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+          <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <label for="eg-filter-op-select-{{col.name}}" i18n>Operator</label>
+              <select id="eg-filter-op-select-{{col.name}}" class="form-control" [(ngModel)]="col.filterOperator" (change)="operatorChanged(col)">
+                <option value="=" i18n>Is exactly</option>
+                <option value="!=" i18n>Is not</option>
+                <option value="not null" i18n>Exists</option>
+                <option value="null" i18n>Does not exist</option>
+                <option value="<" i18n>Is less than</option>
+                <option value=">" i18n>Is greater than</option>
+                <option value="<=" i18n>Is less than or equal to</option>
+                <option value=">=" i18n>Is greater than or equal to</option>
+              </select>
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyFilter(col)" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <input type="number" min="0" step="1" class="form-control" [(ngModel)]="col.filterValue" (keyup.enter)="applyFilter(col)" [disabled]="col.filterInputDisabled">
+      </div>
+    </div>
+    <div *ngSwitchCase="'float'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+          <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <label for="eg-filter-op-select-{{col.name}}" i18n>Operator</label>
+              <select id="eg-filter-op-select-{{col.name}}"  class="form-control" [(ngModel)]="col.filterOperator" (change)="operatorChanged(col)">
+                <option value="=" i18n>Is exactly</option>
+                <option value="!=" i18n>Is not</option>
+                <option value="not null" i18n>Exists</option>
+                <option value="null" i18n>Does not exist</option>
+                <option value="<" i18n>Is less than</option>
+                <option value=">" i18n>Is greater than</option>
+                <option value="<=" i18n>Is less than or equal to</option>
+                <option value=">=" i18n>Is greater than or equal to</option>
+              </select>
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyFilter(col)" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <input type="number" class="form-control" [(ngModel)]="col.filterValue" (keyup.enter)="applyFilter(col)" [disabled]="col.filterInputDisabled">
+      </div>
+    </div>
+    <div *ngSwitchCase="'money'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+          <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <label for="eg-filter-op-select-{{col.name}}" i18n>Operator</label>
+              <select id="eg-filter-op-select-{{col.name}}" class="form-control" [(ngModel)]="col.filterOperator" (change)="operatorChanged(col)">
+                <option value="=" i18n>Is exactly</option>
+                <option value="!=" i18n>Is not</option>
+                <option value="not null" i18n>Exists</option>
+                <option value="null" i18n>Does not exist</option>
+                <option value="<" i18n>Is less than</option>
+                <option value=">" i18n>Is greater than</option>
+                <option value="<=" i18n>Is less than or equal to</option>
+                <option value=">=" i18n>Is greater than or equal to</option>
+              </select>
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyFilter(col)" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <input type="number" step="0.01" class="form-control" [(ngModel)]="col.filterValue" (keyup.enter)="applyFilter(col)" [disabled]="col.filterInputDisabled">
+      </div>
+    </div>
+    <div *ngSwitchCase="'timestamp'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+           <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <label for="eg-filter-op-select-{{col.name}}" i18n>Operator</label>
+              <select id="eg-filter-op-select-{{col.name}}" class="form-control" [(ngModel)]="col.filterOperator" (change)="operatorChanged(col)">
+                <option value="=" i18n>Is exactly</option>
+                <option value="!=" i18n>Is not</option>
+                <option value="not null" i18n>Exists</option>
+                <option value="null" i18n>Does not exist</option>
+                <option value="<" i18n>Is less than</option>
+                <option value=">" i18n>Is greater than</option>
+                <option value="<=" i18n>Is less than or equal to</option>
+                <option value=">=" i18n>Is greater than or equal to</option>
+                <option value="between" i18n>Between</option>
+              </select>
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyDateFilter(datesel.currentAsYmd(), col, dateendsel.currentAsYmd())" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <eg-date-select [initialYmd]="col.filterValue" (onChangeAsYmd)="applyDateFilter($event, col, dateendsel.currentAsYmd())" (onCleared)="clearDateFilter(col)"
+                        [disabled]="col.filterInputDisabled" #datesel></eg-date-select>
+        <div [hidden]="col.filterOperator !== 'between'" class="form-inline form-group">
+          <label for="eg-filter-end-date-select-{{col.name}}" style="width: 3em;" i18n>and</label>
+          <eg-date-select [hidden]="col.filterOperator !== 'between'" (onChangeAsYmd)="applyDateFilter(datesel.currentAsYmd(), col, $event)"
+                          [required]="col.filterOperator == 'between'" #dateendsel></eg-date-select>
+        </div>
+      </div>
+    </div>
+    <div *ngSwitchCase="'org_unit'">
+      <div class="input-group">
+        <div ngbDropdown class="d-inline-block" autoClose="outside" placement="bottom-left" [ngClass]="{'eg-grid-col-is-filtered' : col.isFiltered}">
+          <button ngbDropdownToggle class="form-control btn btn-sm btn-outline-dark text-button"><span class="material-icons mat-icon-in-button">filter_list</span></button>
+          <div ngbDropdownMenu class="eg-grid-filter-menu">
+            <div class="dropdown-item">
+              <label for="eg-filter-op-select-{{col.name}}" i18n>Operator</label>
+              <select id="eg-filter-op-select-{{col.name}}" class="form-control" [(ngModel)]="col.filterOperator" (change)="operatorChanged(col)">
+                <option value="=" i18n>Is (or includes)</option>
+                <option value="!=" i18n>Is not (or excludes)</option>
+              </select>
+            </div>
+            <div class="dropdown-item">
+              <div class="form-check">
+                <input type="checkbox"
+                  [(ngModel)]="col.filterIncludeOrgAncestors"
+                  class="form-check-input" id="include-ancestors">
+                <label class="form-check-label" for="include-ancestors" i18n>+ Ancestors</label>
+              </div>
+              <div class="form-check">
+                <input type="checkbox"
+                  [(ngModel)]="col.filterIncludeOrgDescendants"
+                  class="form-check-input" id="include-descendants">
+                <label class="form-check-label" for="include-descendants" i18n>+ Descendants</label>
+              </div>
+              <div style="padding-top: 2px;">
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); applyOrgFilter(ousel.selectedOrg(), col)" i18n>Apply filter</button>
+                <span style="padding-left: 2px;"></span>
+                <button class="btn btn-sm btn-outline-dark" (click)="closeDropdown(); clearFilter(col)" i18n>Clear filter</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <eg-org-select [applyOrgId]="col.filterValue" (onChange)="applyOrgFilter($event, col)" placeholder="Enter library to filter by" #ousel></eg-org-select>
+      </div>
+    </div>
+    <div *ngSwitchDefault>I don't know how to filter {{col.name}} - {{col.datatype}}</div>
+  </div>
+  <span *ngIf="col.datatype !== 'org_unit'" i18n class="eg-grid-filter-operator">Operator:
+    <span [ngSwitch]="col.filterOperator">
+      <span *ngSwitchCase="'='" i18n>Is exactly</span>
+      <span *ngSwitchCase="'!='" i18n>Is not</span>
+      <span *ngSwitchCase="'>'" i18n>Is greater than</span>
+      <span *ngSwitchCase="'>='" i18n>Is greater than or equal to</span>
+      <span *ngSwitchCase="'<'" i18n>Is less than</span>
+      <span *ngSwitchCase="'<='" i18n>Is less than or equal to</span>
+      <span *ngSwitchCase="'like'" i18n>Contains</span>
+      <span *ngSwitchCase="'not like'" i18n>Does not contain</span>
+      <span *ngSwitchCase="'startswith'" i18n>Starts with</span>
+      <span *ngSwitchCase="'endswith'" i18n>Ends with</span>
+      <span *ngSwitchCase="'null'" i18n>Does not exist</span>
+      <span *ngSwitchCase="'not null'" i18n>Exists</span>
+      <span *ngSwitchCase="'between'" i18n>Between</span>
+    </span>
+  </span>
+  <span *ngIf="col.datatype == 'org_unit'" i18n class="eg-grid-filter-operator">Operator:
+    <span [ngSwitch]="col.filterOperator">
+      <span *ngSwitchCase="'='" i18n>Is (or includes)</span>
+      <span *ngSwitchCase="'!='" i18n>Is not (or excludes)</span>
+    </span>
+  </span>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
new file mode 100644
index 0000000000..fbc59186c2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
@@ -0,0 +1,280 @@
+import {Component, Input, OnInit, QueryList, ViewChildren} from '@angular/core';
+import {GridContext, GridColumn, GridRowSelector,
+    GridColumnSet, GridDataSource} from './grid';
+import {IdlObject} from '@eg/core/idl.service';
+import {ComboboxComponent,
+    ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
+import {OrgService} from '@eg/core/org.service';
+import {NgbDropdown} from '@ng-bootstrap/ng-bootstrap';
+
+ at Component({
+  selector: 'eg-grid-filter-control',
+  templateUrl: './grid-filter-control.component.html'
+})
+
+export class GridFilterControlComponent implements OnInit {
+
+    @Input() context: GridContext;
+    @Input() col:     GridColumn;
+
+
+    @ViewChildren(ComboboxComponent)   filterComboboxes: QueryList<ComboboxComponent>;
+    @ViewChildren(DateSelectComponent) dateSelects: QueryList<DateSelectComponent>;
+    @ViewChildren(OrgSelectComponent)  orgSelects: QueryList<OrgSelectComponent>;
+    @ViewChildren(NgbDropdown)         dropdowns: QueryList<NgbDropdown>;
+
+    constructor(
+        private org: OrgService
+    ) {}
+
+    ngOnInit() { }
+
+    operatorChanged(col: GridColumn) {
+        if (col.filterOperator === 'null' || col.filterOperator === 'not null') {
+            col.filterInputDisabled = true;
+            col.filterValue = undefined;
+        } else {
+            col.filterInputDisabled = false;
+        }
+    }
+
+    applyOrgFilter(org: IdlObject, col: GridColumn) {
+        if (org == null) {
+            this.clearFilter(col);
+            return;
+        }
+        const ous: any[] = new Array();
+        if (col.filterIncludeOrgDescendants || col.filterIncludeOrgAncestors) {
+            if (col.filterIncludeOrgAncestors) {
+                ous.push(...this.org.ancestors(org, true));
+            }
+            if (col.filterIncludeOrgDescendants) {
+                ous.push(...this.org.descendants(org, true));
+            }
+        } else {
+            ous.push(org.id());
+        }
+        const filt: any = {};
+        filt[col.name] = {};
+        const op: string = (col.filterOperator === '=' ? 'in' : 'not in');
+        filt[col.name][op] = ous;
+        this.context.dataSource.filters[col.name] = [ filt ];
+        col.isFiltered = true;
+        this.context.reload();
+    }
+    applyLinkFilter($event, col: GridColumn) {
+        col.filterValue = $event.id;
+        this.applyFilter(col);
+    }
+
+    // TODO: this was copied from date-select and
+    // really belongs in a date service
+    localDateFromYmd(ymd: string): Date {
+        const parts = ymd.split('-');
+        return new Date(
+            Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
+    }
+    applyDateFilter(dateStr: string, col: GridColumn, endDateStr: string) {
+        if (col.filterOperator === 'null' || col.filterOperator === 'not null') {
+            this.applyFilter(col);
+        } else {
+            if (dateStr == null) {
+                this.clearFilter(col);
+                return;
+            }
+            const date: Date = this.localDateFromYmd(dateStr);
+            let date1 = new Date();
+            let date2 = new Date();
+            const op: string = col.filterOperator;
+            const filt: Object = {};
+            const filt2: Object = {};
+            const filters = new Array();
+            if (col.filterOperator === '>') {
+                date1 = date;
+                date1.setHours(23);
+                date1.setMinutes(59);
+                date1.setSeconds(59);
+                filt[op] = date1.toISOString();
+                if (col.name === 'dob') { filt[op] = dateStr; } // special case
+                filt2[col.name] = filt;
+                filters.push(filt2);
+            } else if (col.filterOperator === '>=') {
+                date1 = date;
+                filt[op] = date1.toISOString();
+                if (col.name === 'dob') { filt[op] = dateStr; } // special case
+                filt2[col.name] = filt;
+                filters.push(filt2);
+            } else if (col.filterOperator === '<') {
+                date1 = date;
+                filt[op] = date1.toISOString();
+                if (col.name === 'dob') { filt[op] = dateStr; } // special case
+                filt2[col.name] = filt;
+                filters.push(filt2);
+            } else if (col.filterOperator === '<=') {
+                date1 = date;
+                date1.setHours(23);
+                date1.setMinutes(59);
+                date1.setSeconds(59);
+                filt[op] = date1.toISOString();
+                if (col.name === 'dob') { filt[op] = dateStr; } // special case
+                filt2[col.name] = filt;
+                filters.push(filt2);
+            } else if (col.filterOperator === '=') {
+                date1 = new Date(date.valueOf());
+                filt['>='] = date1.toISOString();
+                if (col.name === 'dob') { filt['>='] = dateStr; } // special case
+                filt2[col.name] = filt;
+                filters.push(filt2);
+
+                date2 = new Date(date.valueOf());
+                date2.setHours(23);
+                date2.setMinutes(59);
+                date2.setSeconds(59);
+                const filt_a: Object = {};
+                const filt2_a: Object = {};
+                filt_a['<='] = date2.toISOString();
+                if (col.name === 'dob') { filt_a['<='] = dateStr; } // special case
+                filt2_a[col.name] = filt_a;
+                filters.push(filt2_a);
+            } else if (col.filterOperator === '!=') {
+                date1 = new Date(date.valueOf());
+                filt['<'] = date1.toISOString();
+                if (col.name === 'dob') { filt['<'] = dateStr; } // special case
+                filt2[col.name] = filt;
+
+                date2 = new Date(date.valueOf());
+                date2.setHours(23);
+                date2.setMinutes(59);
+                date2.setSeconds(59);
+                const filt_a: Object = {};
+                const filt2_a: Object = {};
+                filt_a['>'] = date2.toISOString();
+                if (col.name === 'dob') { filt_a['>'] = dateStr; } // special case
+                filt2_a[col.name] = filt_a;
+
+                const date_filt: any = { '-or': [] };
+                date_filt['-or'].push(filt2);
+                date_filt['-or'].push(filt2_a);
+                filters.push(date_filt);
+            } else if (col.filterOperator === 'between') {
+                date1 = date;
+                date2 = this.localDateFromYmd(endDateStr);
+
+                let date1op = '>=';
+                let date2op = '<=';
+                if (date1 > date2) {
+                    // don't make user care about the order
+                    // they enter the dates in
+                    date1op = '<=';
+                    date2op = '>=';
+                }
+                filt[date1op] = date1.toISOString();
+                if (col.name === 'dob') { filt['>='] = dateStr; } // special case
+                filt2[col.name] = filt;
+                filters.push(filt2);
+
+                date2.setHours(23);
+                date2.setMinutes(59);
+                date2.setSeconds(59);
+                const filt_a: Object = {};
+                const filt2_a: Object = {};
+                filt_a[date2op] = date2.toISOString();
+                if (col.name === 'dob') { filt_a['<='] = endDateStr; } // special case
+                filt2_a[col.name] = filt_a;
+                filters.push(filt2_a);
+            }
+            this.context.dataSource.filters[col.name] = filters;
+            col.isFiltered = true;
+            this.context.reload();
+        }
+    }
+    clearDateFilter(col: GridColumn) {
+        delete this.context.dataSource.filters[col.name];
+        col.isFiltered = false;
+        this.context.reload();
+    }
+    applyBooleanFilter(col: GridColumn) {
+        if (!col.filterValue || col.filterValue === '') {
+            delete this.context.dataSource.filters[col.name];
+            col.isFiltered = false;
+            this.context.reload();
+        } else {
+            const val: string = col.filterValue;
+            const op = '=';
+            const filt: Object = {};
+            filt[op] = val;
+            const filt2: Object = {};
+            filt2[col.name] = filt;
+            this.context.dataSource.filters[col.name] = [ filt2 ];
+            col.isFiltered = true;
+            this.context.reload();
+        }
+    }
+    applyFilter(col: GridColumn) {
+        // fallback if the operator somehow was not set yet
+        if (col.filterOperator === undefined) { col.filterOperator = '='; }
+
+        if ( (col.filterOperator !== 'null') && (col.filterOperator !== 'not null') &&
+             (!col.filterValue || col.filterValue === '') ) {
+            // if value is empty and we're _not_ checking for null/not null, clear
+            // the filter
+            delete this.context.dataSource.filters[col.name];
+            col.isFiltered = false;
+        } else {
+            let op: string = col.filterOperator;
+            let val: string = col.filterValue;
+            const name: string = col.name;
+            if (col.filterOperator === 'null') {
+                op  = '=';
+                val = null;
+            } else if (col.filterOperator === 'not null') {
+                op  = '!=';
+                val = null;
+            } else if (col.filterOperator === 'like' || col.filterOperator === 'not like') {
+                val = '%' + val + '%';
+            } else if (col.filterOperator === 'startswith') {
+                op = 'like';
+                val = val + '%';
+            } else if (col.filterOperator === 'endswith') {
+                op = 'like';
+                val = '%' + val;
+            }
+            const filt: any = {};
+            if (col.filterOperator === 'not like') {
+                filt['-not'] = {};
+                filt['-not'][col.name] = {};
+                filt['-not'][col.name]['like'] = val;
+                this.context.dataSource.filters[col.name] = [ filt ];
+                col.isFiltered = true;
+            } else {
+                filt[col.name] = {};
+                filt[col.name][op] = val;
+                this.context.dataSource.filters[col.name] = [ filt ];
+                col.isFiltered = true;
+            }
+        }
+        this.context.reload();
+    }
+    clearFilter(col: GridColumn) {
+        // clear filter values...
+        col.removeFilter();
+        // ... and inform the data source
+        delete this.context.dataSource.filters[col.name];
+        col.isFiltered = false;
+        this.reset();
+        this.context.reload();
+    }
+
+    closeDropdown() {
+        this.dropdowns.forEach(drp => { drp.close(); });
+    }
+
+    reset() {
+        this.filterComboboxes.forEach(ctl => { ctl.applyEntryId(null); });
+        this.dateSelects.forEach(ctl => { ctl.reset(); });
+        this.orgSelects.forEach(ctl => { ctl.reset(); });
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
index 96811a32aa..571d0740ea 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
@@ -36,4 +36,17 @@
     <span *ngIf="!col.isSortable">{{col.label}}</span>
   </div>
 </div>
+<div *ngIf="context.isFilterable"
+  class="eg-grid-row eg-grid-filter-controls-row">
+  <ng-container *ngIf="!context.disableSelect">
+    <div class="eg-grid-cell eg-grid-header-cell eg-grid-cell-skinny"></div>
+  </ng-container>
+  <div class="eg-grid-cell eg-grid-header-cell eg-grid-cell-skinny"></div>
+  <div *ngIf="context.rowFlairIsEnabled" 
+    class="eg-grid-cell eg-grid-header-cell"></div>
 
+  <div *ngFor="let col of context.columnSet.displayColumns()" 
+    class="eg-grid-cell eg-grid-filter-control-cell" [ngStyle]="{flex:col.flex}">
+    <eg-grid-filter-control [context]="context" [col]="col"></eg-grid-filter-control>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
index 591fc66c2b..cc53b26130 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
@@ -1,13 +1,14 @@
-import {Component, Input, OnInit} from '@angular/core';
+import {Component, Input, OnInit, AfterViewInit, QueryList, ViewChildren} from '@angular/core';
 import {GridContext, GridColumn, GridRowSelector,
     GridColumnSet, GridDataSource} from './grid';
+import {GridFilterControlComponent} from './grid-filter-control.component';
 
 @Component({
   selector: 'eg-grid-header',
   templateUrl: './grid-header.component.html'
 })
 
-export class GridHeaderComponent implements OnInit {
+export class GridHeaderComponent implements OnInit, AfterViewInit {
 
     @Input() context: GridContext;
 
@@ -15,6 +16,8 @@ export class GridHeaderComponent implements OnInit {
 
     batchRowCheckbox: boolean;
 
+    @ViewChildren(GridFilterControlComponent) filterControls: QueryList<GridFilterControlComponent>;
+
     constructor() {}
 
     ngOnInit() {
@@ -23,6 +26,10 @@ export class GridHeaderComponent implements OnInit {
         );
     }
 
+    ngAfterViewInit() {
+        this.context.filterControls = this.filterControls;
+    }
+
     onColumnDragEnter($event: any, col: any) {
         if (this.dragColumn && this.dragColumn.name !== col.name) {
             col.isDragTarget = true;
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
index d75ef88b60..35781a5014 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
@@ -4,7 +4,13 @@
   <div class="btn-toolbar">
 
     <!-- buttons -->
-    <div class="btn-grp" *ngIf="gridContext.toolbarButtons.length">
+    <div class="btn-grp" *ngIf="gridContext.toolbarButtons.length || gridContext.isFilterable">
+      <!-- special case for remove filters button -->
+      <button *ngIf="gridContext.isFilterable"
+        class="btn btn-outline-dark mr-1" (click)="gridContext.removeFilters()"
+        [disabled]="!gridContext.filtersSet()" i18n>
+        Remove Filters
+      </button>
       <button *ngFor="let btn of gridContext.toolbarButtons"
         [disabled]="btn.disabled"
         class="btn btn-outline-dark mr-1" (click)="performButtonAction(btn)">
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.css b/Open-ILS/src/eg2/src/app/share/grid/grid.component.css
index 9748c0c4b1..de4894d3fe 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.css
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.css
@@ -140,3 +140,32 @@
   box-shadow: none;
 }
 
+.eg-grid-filter-control-cell {
+    overflow: visible !important;
+}
+.eg-grid-col-is-filtered {
+    background: lightblue;
+}
+.eg-grid-filter-menu {
+  min-width: 17rem;
+}
+
+.eg-grid-sticky-header {
+  position: sticky;
+  top: 50px;
+  background: white;
+  z-index: 1;
+}
+
+.eg-grid-filter-operator {
+  font-style: italic;
+}
+
+/* override the dropdown menu effects for the filter menus */
+.eg-grid-filter-menu .dropdown-item:active {
+  color: #212529;
+  background-color: transparent;
+}
+.eg-grid-filter-menu .dropdown-item:hover {
+  background-color: transparent;
+}
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
index 20015cab19..e29eb67e63 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
@@ -8,7 +8,9 @@
     [disableSaveSettings]="!persistKey || ('disabled' === persistKey)">
   </eg-grid-toolbar>
 
-  <eg-grid-header [context]="context"></eg-grid-header>
+  <div #egGridStickyHeader [ngClass]="{'eg-grid-sticky-header' : context.stickyGridHeader}">
+    <eg-grid-header [context]="context"></eg-grid-header>
+  </div>
 
   <eg-grid-column-width #colWidthConfig [gridContext]="context">
   </eg-grid-column-width>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
index e4938cc739..b1b29895f3 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
@@ -1,11 +1,12 @@
 import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
-    OnDestroy, HostListener, ViewEncapsulation} from '@angular/core';
+    OnDestroy, HostListener, ViewEncapsulation, QueryList, ViewChildren} from '@angular/core';
 import {Subscription} from 'rxjs';
 import {IdlService} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
 import {FormatService} from '@eg/core/format.service';
 import {GridContext, GridColumn, GridDataSource, GridRowFlairEntry} from './grid';
+import {GridFilterControlComponent} from './grid-filter-control.component';
 
 /**
  * Main grid entry point.
@@ -105,6 +106,20 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
 
     @Input() disablePaging: boolean;
 
+    // result filtering
+    //
+    // filterable: true if the result filtering controls
+    // should be displayed
+    @Input() filterable: boolean;
+
+    // sticky grid header
+    //
+    // stickyHeader: true of the grid header should be
+    // "sticky", i.e., remain visible if if the table is long
+    // and the user has scrolled far enough that the header
+    // would go out of view
+    @Input() stickyHeader: boolean;
+
     context: GridContext;
 
     // These events are emitted from our grid-body component.
@@ -134,6 +149,8 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
         this.context.dataSource = this.dataSource;
         this.context.persistKey = this.persistKey;
         this.context.isSortable = this.sortable === true;
+        this.context.isFilterable = this.filterable === true;
+        this.context.stickyGridHeader = this.stickyHeader === true;
         this.context.isMultiSortable = this.multiSortable === true;
         this.context.useLocalSort = this.useLocalSort === true;
         this.context.disableSelect = this.disableSelect === true;
@@ -178,6 +195,11 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
     reload() {
         this.context.reload();
     }
+    reloadSansPagerReset() {
+        this.context.reloadSansPagerReset();
+    }
+
+
 }
 
 
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
index a6eb093dc0..b738ac6302 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
@@ -14,6 +14,7 @@ import {GridToolbarActionsMenuComponent} from './grid-toolbar-actions-menu.compo
 import {GridColumnConfigComponent} from './grid-column-config.component';
 import {GridColumnWidthComponent} from './grid-column-width.component';
 import {GridPrintComponent} from './grid-print.component';
+import {GridFilterControlComponent} from './grid-filter-control.component';
 
 
 @NgModule({
@@ -31,7 +32,8 @@ import {GridPrintComponent} from './grid-print.component';
         GridToolbarActionsMenuComponent,
         GridColumnConfigComponent,
         GridColumnWidthComponent,
-        GridPrintComponent
+        GridPrintComponent,
+        GridFilterControlComponent
     ],
     imports: [
         EgCommonModule,
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
index e7c7f711b6..7740bddbc1 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -1,13 +1,14 @@
 /**
  * Collection of grid related classses and interfaces.
  */
-import {TemplateRef, EventEmitter} from '@angular/core';
+import {TemplateRef, EventEmitter, QueryList} from '@angular/core';
 import {Observable, Subscription} from 'rxjs';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
 import {FormatService} from '@eg/core/format.service';
 import {Pager} from '@eg/share/util/pager';
+import {GridFilterControlComponent} from './grid-filter-control.component';
 
 const MAX_ALL_ROW_COUNT = 10000;
 
@@ -32,6 +33,8 @@ export class GridColumn {
     isIndex: boolean;
     isDragTarget: boolean;
     isSortable: boolean;
+    isFilterable: boolean;
+    isFiltered: boolean;
     isMultiSortable: boolean;
     disableTooltip: boolean;
     comparator: (valueA: any, valueB: any) => number;
@@ -39,6 +42,13 @@ export class GridColumn {
     // True if the column was automatically generated.
     isAuto: boolean;
 
+    // for filters
+    filterValue: string;
+    filterOperator: string;
+    filterInputDisabled: boolean;
+    filterIncludeOrgAncestors: boolean;
+    filterIncludeOrgDescendants: boolean;
+
     flesher: (obj: any, col: GridColumn, item: any) => any;
 
     getCellContext(row: any) {
@@ -48,6 +58,19 @@ export class GridColumn {
           userContext: this.cellContext
         };
     }
+
+    constructor() {
+        this.removeFilter();
+    }
+
+    removeFilter() {
+        this.isFiltered = false;
+        this.filterValue = undefined;
+        this.filterOperator = '=';
+        this.filterInputDisabled = false;
+        this.filterIncludeOrgAncestors = false;
+        this.filterIncludeOrgDescendants = false;
+    }
 }
 
 export class GridColumnSet {
@@ -55,6 +78,7 @@ export class GridColumnSet {
     idlClass: string;
     indexColumn: GridColumn;
     isSortable: boolean;
+    isFilterable: boolean;
     isMultiSortable: boolean;
     stockVisible: string[];
     idl: IdlService;
@@ -85,6 +109,7 @@ export class GridColumnSet {
         }
 
         this.applyColumnSortability(col);
+        this.applyColumnFilterability(col);
     }
 
     // Returns true if the new column was inserted, false otherwise.
@@ -224,6 +249,12 @@ export class GridColumnSet {
             col.isSortable = true;
         }
     }
+    applyColumnFilterability(col: GridColumn) {
+        // column filterability defaults to the afilterability of the column set.
+        if (col.isFilterable === undefined && this.isFilterable) {
+            col.isFilterable = true;
+        }
+    }
 
     displayColumns(): GridColumn[] {
         return this.columns.filter(c => c.visible);
@@ -425,6 +456,8 @@ export class GridContext {
     pager: Pager;
     idlClass: string;
     isSortable: boolean;
+    isFilterable: boolean;
+    stickyGridHeader: boolean;
     isMultiSortable: boolean;
     useLocalSort: boolean;
     persistKey: string;
@@ -453,6 +486,8 @@ export class GridContext {
     // action has occurred.
     selectRowsInPageEmitter: EventEmitter<void>;
 
+    filterControls: QueryList<GridFilterControlComponent>;
+
     // Services injected by our grid component
     idl: IdlService;
     org: OrgService;
@@ -480,6 +515,7 @@ export class GridContext {
         this.selectRowsInPageEmitter = new EventEmitter<void>();
         this.columnSet = new GridColumnSet(this.idl, this.idlClass);
         this.columnSet.isSortable = this.isSortable === true;
+        this.columnSet.isFilterable = this.isFilterable === true;
         this.columnSet.isMultiSortable = this.isMultiSortable === true;
         this.columnSet.defaultHiddenFields = this.defaultHiddenFields;
         this.columnSet.defaultVisibleFields = this.defaultVisibleFields;
@@ -528,6 +564,13 @@ export class GridContext {
         });
     }
 
+    reloadSansPagerReset() {
+        setTimeout(() => {
+            this.dataSource.reset();
+            this.dataSource.requestPage(this.pager);
+        });
+    }
+
     // Sort the existing data source instead of requesting sorted
     // data from the client.  Reset pager to page 1.  As with reload(),
     // give the client a chance to setting before redisplaying.
@@ -942,6 +985,16 @@ export class GridContext {
         });
     }
 
+    removeFilters(): void {
+        this.dataSource.filters = {};
+        this.columnSet.displayColumns().forEach(col => { col.removeFilter(); });
+        this.filterControls.forEach(ctl => ctl.reset());
+        this.reload();
+    }
+    filtersSet(): boolean {
+        return Object.keys(this.dataSource.filters).length > 0;
+    }
+
     gridToCsv(): Promise<string> {
 
         let csvStr = '';
@@ -1069,12 +1122,14 @@ export class GridDataSource {
 
     data: any[];
     sort: any[];
+    filters: Object;
     allRowsRetrieved: boolean;
     requestingData: boolean;
     getRows: (pager: Pager, sort: any[]) => Observable<any>;
 
     constructor() {
         this.sort = [];
+        this.filters = {};
         this.reset();
     }
 

commit 58a5fbc2c6fc2ed878daf324c243ecbed168aa62
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Mon Mar 25 15:27:05 2019 -0400

    LP#1831788: create CommonWidgetsModule
    
    This patch moves some commonly-shared components off to a separate
    module. The immediate motivation is to avoid circular dependencies
    when adding filtering widgets to eg-grid.
    
    Components included in CommonWidgetsModule should be "core" in the
    sense that they are unlikely to ever need to embed one another.
    
    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: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts b/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
new file mode 100644
index 0000000000..0f15c98b67
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
@@ -0,0 +1,38 @@
+/*
+  Module for grouping commonly used widgets that might be embedded
+  in other shared components. Components included here should be
+  unlikely to ever need to embed one another.
+*/
+import {NgModule, ModuleWithProviders} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component';
+import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
+
+ at NgModule({
+  declarations: [
+    ComboboxComponent,
+    ComboboxEntryComponent,
+    DateSelectComponent,
+    OrgSelectComponent
+  ],
+  imports: [
+    CommonModule,
+    FormsModule,
+    NgbModule
+  ],
+  exports: [
+    CommonModule,
+    FormsModule,
+    NgbModule,
+    ComboboxComponent,
+    ComboboxEntryComponent,
+    DateSelectComponent,
+    OrgSelectComponent
+  ],
+})
+
+export class CommonWidgetsModule { }
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
index 454dcfb0ed..a6eb093dc0 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
@@ -1,5 +1,6 @@
 import {NgModule} from '@angular/core';
 import {EgCommonModule} from '@eg/common.module';
+import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
 import {GridComponent} from './grid.component';
 import {GridColumnComponent} from './grid-column.component';
 import {GridHeaderComponent} from './grid-header.component';
@@ -33,7 +34,8 @@ import {GridPrintComponent} from './grid-print.component';
         GridPrintComponent
     ],
     imports: [
-        EgCommonModule
+        EgCommonModule,
+        CommonWidgetsModule
     ],
     exports: [
         // public components
diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts
index bbf959c98c..66c62c32eb 100644
--- a/Open-ILS/src/eg2/src/app/staff/common.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts
@@ -1,11 +1,9 @@
 import {NgModule, ModuleWithProviders} from '@angular/core';
 import {EgCommonModule} from '@eg/common.module';
+import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
 import {AudioService} from '@eg/share/util/audio.service';
 import {GridModule} from '@eg/share/grid/grid.module';
 import {StaffBannerComponent} from './share/staff-banner.component';
-import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
-import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component';
-import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
 import {OrgFamilySelectComponent} from '@eg/share/org-family-select/org-family-select.component';
 import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive';
 import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
@@ -17,7 +15,6 @@ import {StringComponent} from '@eg/share/string/string.component';
 import {StringService} from '@eg/share/string/string.service';
 import {TitleComponent} from '@eg/share/title/title.component';
 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
-import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {BucketDialogComponent} from '@eg/staff/share/buckets/bucket-dialog.component';
 import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
 import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
@@ -32,9 +29,6 @@ import {ReactiveFormsModule} from '@angular/forms';
 @NgModule({
   declarations: [
     StaffBannerComponent,
-    ComboboxComponent,
-    ComboboxEntryComponent,
-    OrgSelectComponent,
     OrgFamilySelectComponent,
     AccessKeyDirective,
     AccessKeyInfoComponent,
@@ -43,7 +37,6 @@ import {ReactiveFormsModule} from '@angular/forms';
     TitleComponent,
     OpChangeComponent,
     FmRecordEditorComponent,
-    DateSelectComponent,
     BucketDialogComponent,
     BibSummaryComponent,
     TranslateComponent,
@@ -52,16 +45,15 @@ import {ReactiveFormsModule} from '@angular/forms';
   ],
   imports: [
     EgCommonModule,
-    GridModule,
-    ReactiveFormsModule
+    ReactiveFormsModule,
+    CommonWidgetsModule,
+    GridModule
   ],
   exports: [
     EgCommonModule,
+    CommonWidgetsModule,
     GridModule,
     StaffBannerComponent,
-    ComboboxComponent,
-    ComboboxEntryComponent,
-    OrgSelectComponent,
     OrgFamilySelectComponent,
     AccessKeyDirective,
     AccessKeyInfoComponent,
@@ -70,7 +62,6 @@ import {ReactiveFormsModule} from '@angular/forms';
     TitleComponent,
     OpChangeComponent,
     FmRecordEditorComponent,
-    DateSelectComponent,
     BucketDialogComponent,
     BibSummaryComponent,
     TranslateComponent,

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/src/eg2/src/app/common.module.ts          |  17 +-
 Open-ILS/src/eg2/src/app/core/core.module.ts       |  29 +++
 .../src/eg2/src/app/share/common-widgets.module.ts |  41 +++
 .../app/share/grid/grid-column-width.component.ts  |   5 +-
 .../src/app/share/grid/grid-column.component.ts    |   6 +-
 .../share/grid/grid-filter-control.component.html  | 284 +++++++++++++++++++++
 .../share/grid/grid-filter-control.component.ts    | 278 ++++++++++++++++++++
 .../src/app/share/grid/grid-header.component.html  |  13 +
 .../src/app/share/grid/grid-header.component.ts    |  11 +-
 .../src/app/share/grid/grid-toolbar.component.html |   8 +-
 .../src/app/share/grid/grid-toolbar.component.ts   |   6 +-
 .../src/eg2/src/app/share/grid/grid.component.css  |  29 +++
 .../src/eg2/src/app/share/grid/grid.component.html |   4 +-
 .../src/eg2/src/app/share/grid/grid.component.ts   |  24 +-
 Open-ILS/src/eg2/src/app/share/grid/grid.module.ts |   8 +-
 Open-ILS/src/eg2/src/app/share/grid/grid.ts        |  59 ++++-
 Open-ILS/src/eg2/src/app/staff/common.module.ts    |  19 +-
 .../src/app/staff/sandbox/sandbox.component.html   |  24 ++
 .../eg2/src/app/staff/sandbox/sandbox.component.ts |  73 +++++-
 .../Architecture/Angular_Grid_Improvements.adoc    |  10 +
 20 files changed, 905 insertions(+), 43 deletions(-)
 create mode 100644 Open-ILS/src/eg2/src/app/core/core.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.ts
 create mode 100644 docs/RELEASE_NOTES_NEXT/Architecture/Angular_Grid_Improvements.adoc


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list