[open-ils-commits] [GIT] Evergreen ILS branch rel_3_3 updated. 8cb0a80a074c57f5f2fd1e097fe7b60f0ac1c7b4

Evergreen Git git at git.evergreen-ils.org
Thu Apr 18 17:16:50 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, rel_3_3 has been updated
       via  8cb0a80a074c57f5f2fd1e097fe7b60f0ac1c7b4 (commit)
       via  b8052f3da11a1217af5ee3f1a796137b9d44af16 (commit)
       via  69be8cd7fc2158a8c503ab048c22b7249ee34b45 (commit)
       via  cadbc9d6a76c34da67ff6241b1ad69cf48461767 (commit)
       via  f3d3ae84ffc1bc4c8e89641110a98e9749a5720e (commit)
       via  f07c0da1bcde61653244b2fd57a70bb1fd9d7a69 (commit)
       via  192d24f83120e2495ff7e08ae2c617b7c71c490f (commit)
      from  0c311b5a410d8af89a7210e582397c608d8b9e1e (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 8cb0a80a074c57f5f2fd1e097fe7b60f0ac1c7b4
Author: Dan Wells <dbw2 at calvin.edu>
Date:   Thu Apr 18 10:14:04 2019 -0400

    Trivial change to file header comment
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/holds.service.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/holds.service.ts
index 784dcec4eb..91a45ae570 100644
--- a/Open-ILS/src/eg2/src/app/staff/share/holds/holds.service.ts
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/holds.service.ts
@@ -1,5 +1,5 @@
 /**
- * Common code for mananging holdings
+ * Common code for mananging holds
  */
 import {Injectable} from '@angular/core';
 import {Observable} from 'rxjs';

commit b8052f3da11a1217af5ee3f1a796137b9d44af16
Author: Dan Wells <dbw2 at calvin.edu>
Date:   Thu Apr 18 15:19:18 2019 -0400

    Stamping upgrade script for holds prefetch setting
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 06969d1e25..b236d92e96 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1159', :eg_version); -- cesardv/willis/csharp
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1160', :eg_version); -- berick/dbwells
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-holds-prefetch.sql b/Open-ILS/src/sql/Pg/upgrade/1160.data.catalog-holds-prefetch.sql
similarity index 81%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-holds-prefetch.sql
rename to Open-ILS/src/sql/Pg/upgrade/1160.data.catalog-holds-prefetch.sql
index 8de3ff210e..74109ef73e 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-holds-prefetch.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1160.data.catalog-holds-prefetch.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
---SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1160', :eg_version);
 
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (

commit 69be8cd7fc2158a8c503ab048c22b7249ee34b45
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Mar 4 11:12:33 2019 -0500

    LP1818288 WS Option to pre-fetch record holds
    
    Adds a workstation setting allowing staff to decide whether to pre-fetch
    all holds on the record detail holds tab, to perform sorting paging in
    the client, or to leave the sorting/paging on the server.
    
    Improves client-side sorting in the grid.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
index 8765917ceb..7ee3019477 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
@@ -12,6 +12,10 @@ export class GridToolbarCheckboxComponent implements OnInit {
     // Note most input fields should match class fields for GridColumn
     @Input() label: string;
 
+    // Set the render time value.
+    // This does NOT fire the onChange handler.
+    @Input() initialValue: boolean;
+
     // This is an input instead of an Output because the handler is
     // passed off to the grid context for maintenance -- events
     // are not fired directly from this component.
@@ -32,6 +36,7 @@ export class GridToolbarCheckboxComponent implements OnInit {
         const cb = new GridToolbarCheckbox();
         cb.label = this.label;
         cb.onChange = this.onChange;
+        cb.isChecked = this.initialValue;
 
         this.grid.context.toolbarCheckboxes.push(cb);
     }
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 52c3ae1099..5dd307f4f9 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
@@ -18,6 +18,7 @@
       <ng-container *ngFor="let cb of gridContext.toolbarCheckboxes">
         <label class="form-check-label">
           <input class="form-check-input" type="checkbox"
+            [(ngModel)]="cb.isChecked"
             (click)="cb.onChange.emit($event.target.checked)"/>
             {{cb.label}}
         </label>
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 f7e681dfcb..7c30438ccc 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -542,18 +542,34 @@ export class GridContext {
     sortLocalData() {
 
         const sortDefs = this.dataSource.sort.map(sort => {
+            const column = this.columnSet.getColByName(sort.name);
+
             const def = {
                 name: sort.name,
                 dir: sort.dir,
-                col: this.columnSet.getColByName(sort.name)
+                col: column
             };
 
             if (!def.col.comparator) {
-                def.col.comparator = (a, b) => {
-                    if (a < b) { return -1; }
-                    if (a > b) { return 1; }
-                    return 0;
-                };
+                switch (def.col.datatype) {
+                    case 'id':
+                    case 'money':
+                    case 'int':
+                        def.col.comparator = (a, b) => {
+                            a = Number(a);
+                            b = Number(b);
+                            if (a < b) { return -1; }
+                            if (a > b) { return 1; }
+                            return 0;
+                        };
+                        break;
+                    default:
+                        def.col.comparator = (a, b) => {
+                            if (a < b) { return -1; }
+                            if (a > b) { return 1; }
+                            return 0;
+                        };
+                }
             }
 
             return def;
@@ -574,8 +590,6 @@ export class GridContext {
                 const diff = sortDef.col.comparator(valueA, valueB);
                 if (diff === 0) { continue; }
 
-                console.log(valueA, valueB, diff);
-
                 return sortDef.dir === 'DESC' ? -diff : diff;
             }
 
@@ -927,6 +941,7 @@ export class GridToolbarButton {
 
 export class GridToolbarCheckbox {
     label: string;
+    isChecked: boolean;
     onChange: EventEmitter<boolean>;
 }
 
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
index ff3475076d..ad75118182 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
@@ -52,6 +52,7 @@
       <ngb-tab title="View Holds" i18n-title id="holds">
         <ng-template ngbTabContent>
           <eg-holds-grid [recordId]="recordId"
+            preFetchSetting="catalog.record.holds.prefetch"
             persistKey="cat.catalog.wide_holds"
             [defaultSort]="[{name:'request_time',dir:'asc'}]"
             [initialPickupLib]="currentSearchOrg()"></eg-holds-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html
index 62d269b306..db3d31b25a 100644
--- a/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html
@@ -16,7 +16,7 @@
     </eg-hold-detail>
   </ng-container>
 
-  <ng-container *ngIf="mode == 'list'">
+  <ng-container *ngIf="mode == 'list' && initComplete()">
 
     <div class="row" *ngIf="!hidePickupLibFilter">
       <div class="col-lg-4">
@@ -31,9 +31,14 @@
     </div>
 
     <eg-grid #holdsGrid [dataSource]="gridDataSource" [sortable]="true"
+      [useLocalSort]="enablePreFetch"
       [multiSortable]="true" [persistKey]="persistKey"
       (onRowActivate)="showDetail($event)">
 
+      <eg-grid-toolbar-checkbox (onChange)="preFetchHolds($event)"
+        [initialValue]="enablePreFetch" i18n-label label="Pre-Fetch All Holds">
+      </eg-grid-toolbar-checkbox>
+
       <eg-grid-toolbar-action
         i18n-label label="Show Hold Details" i18n-group group="Hold"
         (onClick)="showDetails($event)"></eg-grid-toolbar-action>
@@ -74,7 +79,7 @@
         i18-group group="Hold" i18n-label label="Cancel Hold"
         (onClick)="showCancelDialog($event)"></eg-grid-toolbar-action>
 
-      <eg-grid-column i18n-label label="Hold ID" path='id' [index]="true">
+      <eg-grid-column i18n-label label="Hold ID" path='id' [index]="true" datatype="id">
       </eg-grid-column>
 
       <ng-template #barcodeTmpl let-hold="row">
@@ -107,12 +112,12 @@
           name='title' [cellTemplate]="titleTmpl"></eg-grid-column>
       <eg-grid-column i18n-label label="Author" path='author'
           [hidden]="true"></eg-grid-column>
-      <eg-grid-column i18n-label label="Potential Items" path='potentials'>
+      <eg-grid-column i18n-label label="Potential Items" path='potentials' datatype="int">
       </eg-grid-column>
       <eg-grid-column i18n-label label="Status" path='status_string'>
       </eg-grid-column>
       <eg-grid-column i18n-label label="Queue Position"
-          path='relative_queue_position' [hidden]="true"></eg-grid-column>
+          path='relative_queue_position' [hidden]="true" datatype="int"></eg-grid-column>
       <eg-grid-column path='usr_id' i18n-label label="User ID" [hidden]="true"></eg-grid-column>
       <eg-grid-column path='usr_usrname' i18n-label label="Username" [hidden]="true"></eg-grid-column>
 
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts
index e0e894d871..af27574528 100644
--- a/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts
@@ -5,6 +5,7 @@ import {NetService} from '@eg/core/net.service';
 import {OrgService} from '@eg/core/org.service';
 import {AuthService} from '@eg/core/auth.service';
 import {Pager} from '@eg/share/util/pager';
+import {ServerStoreService} from '@eg/core/server-store.service';
 import {GridDataSource} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
@@ -33,6 +34,12 @@ export class HoldsGridComponent implements OnInit {
     // Grid persist key
     @Input() persistKey: string;
 
+    @Input() preFetchSetting: string;
+        // If set, all holds are fetched on grid load and sorting/paging all
+    // happens in the client.  If false, sorting and paging occur on
+    // the server.
+    enablePreFetch: boolean;
+
     // How to sort when no sort parameters have been applied
     // via grid controls.  This uses the eg-grid sort format:
     // [{name: fname, dir: 'asc'}, {name: fname2, dir: 'desc'}]
@@ -46,7 +53,6 @@ export class HoldsGridComponent implements OnInit {
     detailHold: any;
     editHolds: number[];
     transferTarget: number;
-    copyStatuses: {[id: string]: IdlObject};
 
     @ViewChild('holdsGrid') private holdsGrid: GridComponent;
     @ViewChild('progressDialog')
@@ -104,34 +110,59 @@ export class HoldsGridComponent implements OnInit {
     constructor(
         private net: NetService,
         private org: OrgService,
+        private store: ServerStoreService,
         private auth: AuthService
     ) {
         this.gridDataSource = new GridDataSource();
-        this.copyStatuses = {};
+        this.enablePreFetch = null;
     }
 
     ngOnInit() {
         this.initDone = true;
         this.pickupLib = this.org.get(this.initialPickupLib);
 
-        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+        if (this.preFetchSetting) {
 
-            if (this.defaultSort && sort.length === 0) {
-                // Only use initial sort if sorting has not been modified
-                // by the grid's own sort controls.
-                sort = this.defaultSort;
-            }
+                this.store.getItem(this.preFetchSetting).then(
+                    applied => this.enablePreFetch = Boolean(applied)
+                );
+
+        }
 
-            // sorting not currently supported
+        if (!this.defaultSort) {
+            this.defaultSort = [{name: 'request_time', dir: 'asc'}];
+        }
+
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            sort = sort.length > 0 ? sort : this.defaultSort;
             return this.fetchHolds(pager, sort);
         };
     }
 
+    // Returns true after all data/settings/etc required to render the
+    // grid have been fetched.
+    initComplete(): boolean {
+        return this.enablePreFetch !== null;
+    }
+
     pickupLibChanged(org: IdlObject) {
         this.pickupLib = org;
         this.holdsGrid.reload();
     }
 
+    preFetchHolds(apply: boolean) {
+        this.enablePreFetch = apply;
+
+        if (apply) {
+            setTimeout(() => this.holdsGrid.reload());
+        }
+
+        if (this.preFetchSetting) {
+            // fire and forget
+            this.store.setItem(this.preFetchSetting, apply);
+        }
+    }
+
     applyFilters(): any {
         const filters: any = {
             is_staff_request: true,
@@ -167,11 +198,16 @@ export class HoldsGridComponent implements OnInit {
         const filters = this.applyFilters();
 
         const orderBy: any = [];
-        sort.forEach(obj => {
-            const subObj: any = {};
-            subObj[obj.name] = {dir: obj.dir, nulls: 'last'};
-            orderBy.push(subObj);
-        });
+        if (sort.length > 0) {
+            sort.forEach(obj => {
+                const subObj: any = {};
+                subObj[obj.name] = {dir: obj.dir, nulls: 'last'};
+                orderBy.push(subObj);
+            });
+        }
+
+        const limit = this.enablePreFetch ? null : pager.limit;
+        const offset = this.enablePreFetch ? 0 : pager.offset;
 
         let observer: Observer<any>;
         const observable = new Observable(obs => observer = obs);
@@ -183,10 +219,7 @@ export class HoldsGridComponent implements OnInit {
         this.net.request(
             'open-ils.circ',
             'open-ils.circ.hold.wide_hash.stream',
-            // Pre-fetch all holds consistent with AngJS version
-            this.auth.token(), filters, orderBy
-            // Alternatively, fetch holds in pages.
-            // this.auth.token(), filters, orderBy, pager.limit, pager.offset
+            this.auth.token(), filters, orderBy, limit, offset
         ).subscribe(
             holdData => {
 
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 184d80c411..3c8a257018 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -19874,7 +19874,6 @@ INSERT INTO config.org_unit_setting_type
         'bool'
     );
 
-
 INSERT INTO config.usr_activity_type 
     (id, ewhat, ehow, egroup, enabled, transient, label)
 VALUES (
@@ -19897,3 +19896,13 @@ VALUES (
     oils_i18n_gettext(30, 'Generic Verify', 'cuat', 'label')
 );
 
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'catalog.record.holds.prefetch', 'cat', 'bool',
+    oils_i18n_gettext(
+        'catalog.record.holds.prefetch',
+        'Pre-Fetch Record Holds',
+        'cwst', 'label'
+    )
+);
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-holds-prefetch.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-holds-prefetch.sql
new file mode 100644
index 0000000000..8de3ff210e
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.catalog-holds-prefetch.sql
@@ -0,0 +1,15 @@
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'catalog.record.holds.prefetch', 'cat', 'bool',
+    oils_i18n_gettext(
+        'catalog.record.holds.prefetch',
+        'Pre-Fetch Record Holds',
+        'cwst', 'label'
+    )
+);
+
+COMMIT;

commit cadbc9d6a76c34da67ff6241b1ad69cf48461767
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Mar 4 11:11:24 2019 -0500

    LP1818288 Grid checkboxes emit events
    
    Modify the grid toolbar checkbox onClick handler to emit events instead
    of requesting a reference to a function.  This is more consistent with
    other event-handling code.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
index f078797525..8765917ceb 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
@@ -1,4 +1,4 @@
-import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {Component, Input, OnInit, Host, Output, EventEmitter} from '@angular/core';
 import {GridToolbarCheckbox} from './grid';
 import {GridComponent} from './grid.component';
 
@@ -15,10 +15,12 @@ export class GridToolbarCheckboxComponent implements OnInit {
     // This is an input instead of an Output because the handler is
     // passed off to the grid context for maintenance -- events
     // are not fired directly from this component.
-    @Input() onChange: (checked: boolean) => void;
+    @Output() onChange: EventEmitter<boolean>;
 
     // get a reference to our container grid.
-    constructor(@Host() private grid: GridComponent) {}
+    constructor(@Host() private grid: GridComponent) {
+        this.onChange = new EventEmitter<boolean>();
+    }
 
     ngOnInit() {
 
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 c5afb48796..52c3ae1099 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
@@ -5,7 +5,7 @@
 
     <!-- buttons -->
     <div class="btn-grp" *ngIf="gridContext.toolbarButtons.length">
-      <button *ngFor="let btn of gridContext.toolbarButtons" 
+      <button *ngFor="let btn of gridContext.toolbarButtons"
         [disabled]="btn.disabled"
         class="btn btn-outline-dark mr-1" (click)="performButtonAction(btn)">
         {{btn.label}}
@@ -13,12 +13,12 @@
     </div>
 
     <!-- checkboxes -->
-    <div class="form-check form-check-inline" 
+    <div class="form-check form-check-inline"
       *ngIf="gridContext.toolbarCheckboxes.length">
       <ng-container *ngFor="let cb of gridContext.toolbarCheckboxes">
         <label class="form-check-label">
-          <input class="form-check-input" type="checkbox" 
-            (click)="cb.onChange($event.target.checked)"/>
+          <input class="form-check-input" type="checkbox"
+            (click)="cb.onChange.emit($event.target.checked)"/>
             {{cb.label}}
         </label>
       </ng-container>
@@ -31,7 +31,7 @@
   <div ngbDropdown class="mr-1" placement="bottom-right">
     <button ngbDropdownToggle [disabled]="!gridContext.toolbarActions.length"
         class="btn btn-outline-dark no-dropdown-caret">
-      <span title="Actions For Selected Rows" i18n-title 
+      <span title="Actions For Selected Rows" i18n-title
         class="material-icons mat-icon-in-button">playlist_add_check</span>
     </button>
     <div class="dropdown-menu" ngbDropdownMenu>
@@ -52,28 +52,28 @@
     </div>
   </div>
 
-  <button [disabled]="gridContext.pager.isFirstPage()" type="button" 
+  <button [disabled]="gridContext.pager.isFirstPage()" type="button"
     class="btn btn-outline-dark mr-1" (click)="gridContext.pager.toFirst()">
-    <span title="First Page" i18n-title 
+    <span title="First Page" i18n-title
         class="material-icons mat-icon-in-button">first_page</span>
   </button>
-  <button [disabled]="gridContext.pager.isFirstPage()" type="button" 
+  <button [disabled]="gridContext.pager.isFirstPage()" type="button"
     class="btn btn-outline-dark mr-1" (click)="gridContext.pager.decrement()">
-    <span title="Previous Page" i18n-title 
+    <span title="Previous Page" i18n-title
         class="material-icons mat-icon-in-button">keyboard_arrow_left</span>
   </button>
-  <button [disabled]="gridContext.pager.isLastPage()" type="button" 
+  <button [disabled]="gridContext.pager.isLastPage()" type="button"
     class="btn btn-outline-dark mr-1" (click)="gridContext.pager.increment()">
-    <span title="Next Page" i18n-title 
+    <span title="Next Page" i18n-title
         class="material-icons mat-icon-in-button">keyboard_arrow_right</span>
   </button>
 
   <!--
   Hiding jump-to-last since there's no analog in the angularjs grid and
   it has limited value since the size of the data set is often unknown.
-  <button [disabled]="!gridContext.pager.resultCount || gridContext.pager.isLastPage()" 
+  <button [disabled]="!gridContext.pager.resultCount || gridContext.pager.isLastPage()"
     type="button" class="btn btn-outline-dark mr-1" (click)="gridContext.pager.toLast()">
-    <span title="First Page" i18n-title 
+    <span title="First Page" i18n-title
         class="material-icons mat-icon-in-button">last_page</span>
   </button>
   -->
@@ -85,7 +85,7 @@
       </span>
     </button>
     <div class="dropdown-menu" ngbDropdownMenu>
-      <a class="dropdown-item" 
+      <a class="dropdown-item"
         *ngFor="let count of [5, 10, 25, 50, 100]"
         (click)="gridContext.pager.setLimit(count)">
         <span class="ml-2">{{count}}</span>
@@ -93,14 +93,14 @@
     </div>
   </div>
 
-  <button type="button" 
-    class="btn btn-outline-dark mr-1" 
+  <button type="button"
+    class="btn btn-outline-dark mr-1"
     (click)="gridContext.overflowCells=!gridContext.overflowCells">
     <span *ngIf="!gridContext.overflowCells"
-      title="Expand Cells Vertically" i18n-title 
+      title="Expand Cells Vertically" i18n-title
       class="material-icons mat-icon-in-button">expand_more</span>
     <span *ngIf="gridContext.overflowCells"
-      title="Collaps Cells Vertically" i18n-title 
+      title="Collaps Cells Vertically" i18n-title
       class="material-icons mat-icon-in-button">expand_less</span>
   </button>
 
@@ -108,31 +108,31 @@
   </eg-grid-column-config>
   <div ngbDropdown placement="bottom-right">
     <button ngbDropdownToggle class="btn btn-outline-dark no-dropdown-caret">
-      <span title="Show Grid Options" i18n-title 
+      <span title="Show Grid Options" i18n-title
         class="material-icons mat-icon-in-button">settings</span>
     </button>
     <div class="dropdown-menu" ngbDropdownMenu>
-      <a class="dropdown-item label-with-material-icon" 
+      <a class="dropdown-item label-with-material-icon"
         (click)="columnConfDialog.open({size:'lg'})">
         <span class="material-icons">build</span>
         <span class="ml-2" i18n>Manage Columns</span>
       </a>
-      <a class="dropdown-item label-with-material-icon" 
+      <a class="dropdown-item label-with-material-icon"
         (click)="colWidthConfig.isVisible = !colWidthConfig.isVisible">
         <span class="material-icons">compare_arrows</span>
         <span class="ml-2" i18n>Manage Column Widths</span>
       </a>
-      <a class="dropdown-item label-with-material-icon" 
+      <a class="dropdown-item label-with-material-icon"
         (click)="saveGridConfig()">
         <span class="material-icons">save</span>
         <span class="ml-2" i18n>Save Grid Settings</span>
       </a>
-      <a class="dropdown-item label-with-material-icon" 
+      <a class="dropdown-item label-with-material-icon"
         (click)="gridContext.columnSet.reset()">
         <span class="material-icons">restore</span>
         <span class="ml-2" i18n>Reset Columns</span>
       </a>
-      <a class="dropdown-item label-with-material-icon" 
+      <a class="dropdown-item label-with-material-icon"
         (click)="generateCsvExportUrl($event)"
         [download]="csvExportFileName"
         [href]="csvExportUrl">
@@ -146,7 +146,7 @@
 
       <div class="dropdown-divider"></div>
 
-      <a class="dropdown-item label-with-material-icon" 
+      <a class="dropdown-item label-with-material-icon"
         (click)="col.visible=!col.visible" *ngFor="let col of gridContext.columnSet.columns">
         <span *ngIf="col.visible" class="badge badge-success">&#x2713;</span>
         <span *ngIf="!col.visible" class="badge badge-warning">&#x2717;</span>
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 92591a76e3..f7e681dfcb 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -927,7 +927,7 @@ export class GridToolbarButton {
 
 export class GridToolbarCheckbox {
     label: string;
-    onChange: (checked: boolean) => void;
+    onChange: EventEmitter<boolean>;
 }
 
 export class GridDataSource {
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.html
index 8bc896f5b6..3185a38697 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.html
@@ -8,11 +8,11 @@
   </div>
 </div>
 
-<eg-grid #itemsGrid 
+<eg-grid #itemsGrid
   showFields="record,import_error,imported_as,import_time,owning_lib,call_number,barcode"
   persistKey="cat.vandelay.queue.items"
   idlClass="vii" [dataSource]="gridSource">
-  <eg-grid-toolbar-checkbox [onChange]="limitToImportErrors"
+  <eg-grid-toolbar-checkbox (onChange)="limitToImportErrors($event)"
     i18n-label label="Limit to Import Failures"></eg-grid-toolbar-checkbox>
 
 </eg-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
index 7340e3d719..cae7c54a8e 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue-items.component.ts
@@ -18,7 +18,6 @@ export class QueueItemsComponent {
     queueType: string;
     queueId: number;
     filterImportErrors: boolean;
-    limitToImportErrors: (checked: boolean) => void;
 
     gridSource: GridDataSource;
     @ViewChild('itemsGrid') itemsGrid: GridComponent;
@@ -49,11 +48,12 @@ export class QueueItemsComponent {
                 }
             );
         };
+    }
 
-        this.limitToImportErrors = (checked: boolean) => {
-            this.filterImportErrors = checked;
-            this.itemsGrid.reload();
-        };
+    limitToImportErrors(checked: boolean) {
+        this.filterImportErrors = checked;
+        this.itemsGrid.reload();
     }
+
 }
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
index 5584700eed..e4ef84565b 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.html
@@ -4,7 +4,7 @@
 
 <ng-container *ngIf="queueSummary && queueSummary.queue">
 
-  <eg-confirm-dialog 
+  <eg-confirm-dialog
     #confirmDelDlg
     i18n-dialogTitle i18n-dialogBody
     dialogTitle="Confirm Delete"
@@ -51,7 +51,7 @@
           <li class="list-group-item">
             <div class="d-flex">
               <div class="flex-1">
-                <a [routerLink]="" (click)="importSelected()" 
+                <a [routerLink]="" (click)="importSelected()"
                   i18n>Import Selected Records</a>
               </div>
               <div class="flex-1">
@@ -68,7 +68,7 @@
                 </a>
               </div>
               <div class="flex-1">
-                <a [routerLink]="" (click)="exportNonImported()" 
+                <a [routerLink]="" (click)="exportNonImported()"
                   i18n>Export Non-Imported Records</a>
               </div>
             </div>
@@ -117,11 +117,11 @@
   <a *ngIf="queueType=='auth'" href="/eg/staff/cat/catalog/authority/{{row.imported_as}}/marc_edit">
     {{row.imported_as}}
   </a>
-  
+
 </ng-template>
 
 
-<!-- 
+<!--
 Most columns are generated programmatically from queued record attribute
 definitions.  Hide a number of stock record attributes by default
 because there are a lot of them.
@@ -134,23 +134,22 @@ because there are a lot of them.
   hideFields="language,pagination,price,rec_identifier,eg_tcn_source,eg_identifier,item_barcode,zsource">
 
   <eg-grid-toolbar-checkbox i18n-label label="Records With Matches"
-    [onChange]="limitToMatches"></eg-grid-toolbar-checkbox>
+    (onChange)="limitToMatches($event)"></eg-grid-toolbar-checkbox>
 
   <eg-grid-toolbar-checkbox i18n-label label="Non-Imported Records"
-    [onChange]="limitToNonImported"></eg-grid-toolbar-checkbox>
+    (onChange)="limitToNonImported($event)"></eg-grid-toolbar-checkbox>
 
   <eg-grid-toolbar-checkbox i18n-label label="Records with Import Errors"
-    [onChange]="limitToImportErrors"></eg-grid-toolbar-checkbox>
+    (onChange)="limitToImportErrors($event)"></eg-grid-toolbar-checkbox>
 
-  <eg-grid-column name="id" [index]="true" 
-    [hidden]="true"></eg-grid-column>
-  <eg-grid-column i18n-label label="Matches" 
+  <eg-grid-column name="id" [index]="true" [hidden]="true"></eg-grid-column>
+  <eg-grid-column i18n-label label="Matches"
     name="+matches" [cellTemplate]="matchesTmpl"></eg-grid-column>
-  <eg-grid-column name="import_error" i18n-label 
+  <eg-grid-column name="import_error" i18n-label
     label="Import Errors" [cellTemplate]="errorsTmpl"></eg-grid-column>
-  <eg-grid-column name="import_time" i18n-label 
+  <eg-grid-column name="import_time" i18n-label
     label="Import Date" datatype="timestamp"></eg-grid-column>
-  <eg-grid-column name="imported_as" i18n-label 
+  <eg-grid-column name="imported_as" i18n-label
     label="Imported As" [cellTemplate]="importedAsTmpl"></eg-grid-column>
 </eg-grid>
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
index fbc62ba783..9055f21cd4 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queue.component.ts
@@ -31,10 +31,6 @@ export class QueueComponent implements OnInit, AfterViewInit {
         withErrors: false
     };
 
-    limitToMatches: (checked: boolean) => void;
-    limitToNonImported: (checked: boolean) => void;
-    limitToImportErrors: (checked: boolean) => void;
-
     // keep a local copy for convenience
     attrDefs: IdlObject[];
 
@@ -61,23 +57,24 @@ export class QueueComponent implements OnInit, AfterViewInit {
             return this.loadQueueRecords(pager);
         };
 
-        this.limitToMatches = (checked: boolean) => {
-            this.filters.matches = checked;
-            this.queueGrid.reload();
-        };
+    }
 
-        this.limitToNonImported = (checked: boolean) => {
-            this.filters.nonImported = checked;
-            this.queueGrid.reload();
-        };
+    ngOnInit() {
+    }
 
-        this.limitToImportErrors = (checked: boolean) => {
-            this.filters.withErrors = checked;
-            this.queueGrid.reload();
-        };
+    limitToMatches(checked: boolean) {
+        this.filters.matches = checked;
+        this.queueGrid.reload();
     }
 
-    ngOnInit() {
+    limitToNonImported(checked: boolean) {
+        this.filters.nonImported = checked;
+        this.queueGrid.reload();
+    }
+
+    limitToImportErrors(checked: boolean) {
+        this.filters.withErrors = checked;
+        this.queueGrid.reload();
     }
 
     queuePageOffset(): number {

commit f3d3ae84ffc1bc4c8e89641110a98e9749a5720e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Mar 1 15:42:07 2019 -0500

    LP1818288 Release notes - record holds tab
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/Client/ang-staff-catalog-record-holds.adoc b/docs/RELEASE_NOTES_NEXT/Client/ang-staff-catalog-record-holds.adoc
new file mode 100644
index 0000000000..c18e123de6
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Client/ang-staff-catalog-record-holds.adoc
@@ -0,0 +1,15 @@
+(Experimental) Angular Staff Catalog Record Holds Tab
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Adds support for the Holds tab in the record detail view of the Angular
+staff catalog.  Includes grid and hold-related actions.
+
+ * Holds grid
+ * Batch cancel holds
+ * Batch retarget holds
+ * Batch edit holds
+  ** Unified form to modify notify options, dates, etc.
+ * hold detail page (menu and row double-click)
+ * Batch mark items damaged
+ * Batch mark items missing
+ * Show last few circulations
+ * Retrieve patron

commit f07c0da1bcde61653244b2fd57a70bb1fd9d7a69
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Feb 16 11:42:14 2019 -0800

    LP1818288 Ang staff catalog record detail holds tab/actions
    
    Adds support for the Holds tab in the record detail view of the Angular
    staff catalog.  Includes grid and hold-related actions.
    
    * Holds grid built from the new wide-holds API.
    * batch cancel holds
    * batch retarget holds
    * batch edit holds
    ** Unified form to modify notify options, dates, etc.
    * hold detail page (menu and row double-click)
    * batch mark items damaged
    * batch mark items missing
    * show last few circulations
    * retrieve patron
    * support for indented menu groups a la angjs grids for grouping the
      hold actions menu.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts
index 9361042074..ec06a91b80 100644
--- a/Open-ILS/src/eg2/src/app/common.module.ts
+++ b/Open-ILS/src/eg2/src/app/common.module.ts
@@ -13,7 +13,7 @@ They do not have to be added to the providers list.
 */
 
 // consider moving these to core...
-import {FormatService} from '@eg/core/format.service';
+import {FormatService, FormatValuePipe} from '@eg/core/format.service';
 import {PrintService} from '@eg/share/print/print.service';
 
 // Globally available components
@@ -33,7 +33,8 @@ import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
     ConfirmDialogComponent,
     PromptDialogComponent,
     ProgressInlineComponent,
-    ProgressDialogComponent
+    ProgressDialogComponent,
+    FormatValuePipe
   ],
   imports: [
     CommonModule,
@@ -52,7 +53,8 @@ import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
     ConfirmDialogComponent,
     PromptDialogComponent,
     ProgressInlineComponent,
-    ProgressDialogComponent
+    ProgressDialogComponent,
+    FormatValuePipe
   ]
 })
 
diff --git a/Open-ILS/src/eg2/src/app/core/format.service.ts b/Open-ILS/src/eg2/src/app/core/format.service.ts
index e788cd0e40..8108eec91b 100644
--- a/Open-ILS/src/eg2/src/app/core/format.service.ts
+++ b/Open-ILS/src/eg2/src/app/core/format.service.ts
@@ -1,4 +1,4 @@
-import {Injectable} from '@angular/core';
+import {Injectable, Pipe, PipeTransform} from '@angular/core';
 import {DatePipe, CurrencyPipe} from '@angular/common';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
@@ -131,3 +131,14 @@ export class FormatService {
     }
 }
 
+
+// Pipe-ify the above formating logic for use in templates
+ at Pipe({name: 'formatValue'})
+export class FormatValuePipe implements PipeTransform {
+    constructor(private formatter: FormatService) {}
+    // Add other filter params as needed to fill in the FormatParams
+    transform(value: string, datatype: string): string {
+        return this.formatter.transform({value: value, datatype: datatype});
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html
index 575bbde5c8..7e65f7628e 100644
--- a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html
+++ b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html
@@ -1,7 +1,7 @@
 
 <div class="input-group">
-  <input 
-    class="form-control" 
+  <input
+    class="form-control"
     ngbDatepicker
     #datePicker="ngbDatepicker"
     [attr.id]="domId.length ? domId : null"
@@ -11,11 +11,11 @@
     [disabled]="_disabled"
     [required]="required"
     [(ngModel)]="current"
-    (dateSelect)="onDateSelect($event)">
+    (dateSelect)="onDateSelect($event)"/>
   <div class="input-group-append">
     <button class="btn btn-outline-secondary" [disabled]="_disabled"
       (click)="datePicker.toggle()" type="button">
-      <span title="Select Date" i18n-title                       
+      <span title="Select Date" i18n-title
         class="material-icons mat-icon-in-button">calendar_today</span>
     </button>
   </div>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
index b7531a2a20..e17fe8dcc7 100644
--- a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
@@ -41,7 +41,7 @@ export class DialogComponent implements OnInit {
         this.onOpen$ = new EventEmitter<any>();
     }
 
-    open(options?: NgbModalOptions): Promise<any> {
+    async open(options?: NgbModalOptions): Promise<any> {
 
         if (this.modalRef !== null) {
             console.warn('Dismissing existing dialog');
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
index 0a3337633d..4f8555404f 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
@@ -1,4 +1,4 @@
-import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {Component, Input, Output, OnInit, Host, TemplateRef, EventEmitter} from '@angular/core';
 import {GridToolbarAction} from './grid';
 import {GridComponent} from './grid.component';
 
@@ -11,15 +11,26 @@ export class GridToolbarActionComponent implements OnInit {
 
     // Note most input fields should match class fields for GridColumn
     @Input() label: string;
+
+    // Register to click events
+    @Output() onClick: EventEmitter<any []>;
+
+    // DEPRECATED: Pass a reference to a function that is called on click.
     @Input() action: (rows: any[]) => any;
 
+    // When present, actions will be grouped by the provided label.
+    @Input() group: string;
+
     // Optional: add a function that returns true or false.
     // If true, this action will be disabled; if false
     // (default behavior), the action will be enabled.
     @Input() disableOnRows: (rows: any[]) => boolean;
 
+
     // get a reference to our container grid.
-    constructor(@Host() private grid: GridComponent) {}
+    constructor(@Host() private grid: GridComponent) {
+        this.onClick = new EventEmitter<any []>();
+    }
 
     ngOnInit() {
 
@@ -31,8 +42,9 @@ export class GridToolbarActionComponent implements OnInit {
         const action = new GridToolbarAction();
         action.label = this.label;
         action.action = this.action;
+        action.onClick = this.onClick;
+        action.group = this.group;
         action.disableOnRows = this.disableOnRows;
-
         this.grid.context.toolbarActions.push(action);
     }
 }
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts
index 8287483863..62b6dd5f13 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts
@@ -1,4 +1,4 @@
-import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {Component, Input, Output, OnInit, Host, TemplateRef, EventEmitter} from '@angular/core';
 import {GridToolbarButton} from './grid';
 import {GridComponent} from './grid.component';
 
@@ -11,8 +11,14 @@ export class GridToolbarButtonComponent implements OnInit {
 
     // Note most input fields should match class fields for GridColumn
     @Input() label: string;
+
+    // Register to click events
+    @Output() onClick: EventEmitter<any>;
+
+    // DEPRECATED: Pass a reference to a function that is called on click.
     @Input() action: () => any;
 
+
     @Input() set disabled(d: boolean) {
         // Support asynchronous disabled values by appling directly
         // to our button object as values arrive.
@@ -25,7 +31,9 @@ export class GridToolbarButtonComponent implements OnInit {
 
     // get a reference to our container grid.
     constructor(@Host() private grid: GridComponent) {
+        this.onClick = new EventEmitter<any>();
         this.button = new GridToolbarButton();
+        this.button.onClick = this.onClick;
     }
 
     ngOnInit() {
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 5eaa81ff62..c5afb48796 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
@@ -7,7 +7,7 @@
     <div class="btn-grp" *ngIf="gridContext.toolbarButtons.length">
       <button *ngFor="let btn of gridContext.toolbarButtons" 
         [disabled]="btn.disabled"
-        class="btn btn-outline-dark mr-1" (click)="btn.action()">
+        class="btn btn-outline-dark mr-1" (click)="performButtonAction(btn)">
         {{btn.label}}
       </button>
     </div>
@@ -38,7 +38,16 @@
       <button class="dropdown-item" (click)="performAction(action)"
         *ngFor="let action of gridContext.toolbarActions"
         [disabled]="shouldDisableAction(action)">
-        <span class="ml-2">{{action.label}}</span>
+        <ng-container *ngIf="action.isGroup">
+          <span class="ml-2 font-weight-bold font-italic">{{action.label}}</span>
+        </ng-container>
+        <ng-container *ngIf="action.group && !action.isGroup">
+          <!-- grouped entries are indented -->
+          <span class="ml-4">{{action.label}}</span>
+        </ng-container>
+        <ng-container *ngIf="!action.group && !action.isGroup">
+          <span class="ml-2">{{action.label}}</span>
+        </ng-container>
       </button>
     </div>
   </div>
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 399a4c7211..82c199c36b 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
@@ -17,13 +17,53 @@ export class GridToolbarComponent implements OnInit {
     @Input() colWidthConfig: GridColumnWidthComponent;
     @Input() gridPrinter: GridPrintComponent;
 
+    renderedGroups: {[group: string]: boolean};
+
     csvExportInProgress: boolean;
     csvExportUrl: SafeUrl;
     csvExportFileName: string;
 
-    constructor(private sanitizer: DomSanitizer) {}
+    constructor(private sanitizer: DomSanitizer) {
+        this.renderedGroups = {};
+    }
+
+    ngOnInit() {
+        this.sortActions();
+    }
+
+    sortActions() {
+        const actions = this.gridContext.toolbarActions;
+
+        const unGrouped = actions.filter(a => !a.group)
+        .sort((a, b) => {
+            return a.label < b.label ? -1 : 1;
+        });
+
+        const grouped = actions.filter(a => Boolean(a.group))
+        .sort((a, b) => {
+            if (a.group === b.group) {
+                return a.label < b.label ? -1 : 1;
+            } else {
+                return a.group < b.group ? -1 : 1;
+            }
+        });
 
-    ngOnInit() {}
+        // Insert group markers for rendering
+        const seen: any = {};
+        const grouped2: any[] = [];
+        grouped.forEach(action => {
+            if (!seen[action.group]) {
+                seen[action.group] = true;
+                const act = new GridToolbarAction();
+                act.label = action.group;
+                act.isGroup = true;
+                grouped2.push(act);
+            }
+            grouped2.push(action);
+        });
+
+        this.gridContext.toolbarActions = unGrouped.concat(grouped2);
+    }
 
     saveGridConfig() {
         // TODO: when server-side settings are supported, this operation
@@ -38,7 +78,15 @@ export class GridToolbarComponent implements OnInit {
     }
 
     performAction(action: GridToolbarAction) {
-        action.action(this.gridContext.getSelectedRows());
+        const rows = this.gridContext.getSelectedRows();
+        action.onClick.emit(rows);
+        if (action.action) { action.action(rows); }
+    }
+
+    performButtonAction(button: GridToolbarButton) {
+        const rows = this.gridContext.getSelectedRows();
+        button.onClick.emit();
+        if (button.action) { button.action(); }
     }
 
     shouldDisableAction(action: GridToolbarAction) {
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 3743488c39..92591a76e3 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -1,7 +1,7 @@
 /**
  * Collection of grid related classses and interfaces.
  */
-import {TemplateRef} from '@angular/core';
+import {TemplateRef, EventEmitter} from '@angular/core';
 import {Observable, Subscription} from 'rxjs';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
@@ -910,14 +910,18 @@ export class GridContext {
 // Actions apply to specific rows
 export class GridToolbarAction {
     label: string;
-    action: (rows: any[]) => any;
+    onClick: EventEmitter<any []>;
+    action: (rows: any[]) => any; // DEPRECATED
+    group: string;
+    isGroup: boolean; // used for group placeholder entries
     disableOnRows: (rows: any[]) => boolean;
 }
 
 // Buttons are global actions
 export class GridToolbarButton {
     label: string;
-    action: () => any;
+    onClick: EventEmitter<any []>;
+    action: () => any; // DEPRECATED
     disabled: boolean;
 }
 
diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
index d4ffd53cc0..cf7b93d80f 100644
--- a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
+++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
@@ -14,6 +14,7 @@
     class="form-control"
     [attr.id]="domId.length ? domId : null"
     [placeholder]="placeholder"
+    [disabled]="disabled"
     [(ngModel)]="selected" 
     [ngbTypeahead]="filter"
     [resultTemplate]="displayTemplate"
diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
index f7dddb2a23..f455c36bf3 100644
--- a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
@@ -27,10 +27,12 @@ export class OrgSelectComponent implements OnInit {
 
     selected: OrgDisplay;
     hidden: number[] = [];
-    disabled: number[] = [];
     click$ = new Subject<string>();
     startOrg: IdlObject;
 
+    // Disable the entire input
+    @Input() disabled: boolean;
+
     @ViewChild('instance') instance: NgbTypeahead;
 
     // Placeholder text for selector input
@@ -56,8 +58,9 @@ export class OrgSelectComponent implements OnInit {
     }
 
     // List of org unit IDs to disable in the selector
+    _disabledOrgs: number[] = [];
     @Input() set disableOrgs(ids: number[]) {
-        if (ids) { this.disabled = ids; }
+        if (ids) { this._disabledOrgs = ids; }
     }
 
     // Apply an org unit value at load time.
diff --git a/Open-ILS/src/eg2/src/app/share/string/string.component.ts b/Open-ILS/src/eg2/src/app/share/string/string.component.ts
index f092a7ef5f..3322fd07ce 100644
--- a/Open-ILS/src/eg2/src/app/share/string/string.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/string/string.component.ts
@@ -14,7 +14,12 @@ import {StringService} from '@eg/share/string/string.service';
   selector: 'eg-string',
   template: `
     <span style='display:none'>
-    <ng-container *ngTemplateOutlet="template; context:ctx"></ng-container>
+      <ng-container *ngIf="template">
+        <ng-container *ngTemplateOutlet="template; context:ctx"></ng-container>
+      </ng-container>
+      <ng-container *ngIf="!template">
+        <span>{{text}}</span>
+      </ng-container>
     </span>
   `
 })
@@ -64,11 +69,11 @@ export class StringComponent implements OnInit {
     // NOTE: talking to the native DOM element is not so great, but
     // hopefully we can retire the String* code entirely once
     // in-code translations are supported (Ang6?)
-    current(ctx?: any): Promise<string> {
+    async current(ctx?: any): Promise<string> {
         if (ctx) { this.ctx = ctx; }
-        return new Promise(resolve => {
-            setTimeout(() => resolve(this.elm.nativeElement.textContent));
-        });
+        return new Promise<string>(resolve =>
+            setTimeout(() => resolve(this.elm.nativeElement.textContent))
+        );
     }
 }
 
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
index 2d30199441..b158ac1442 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
@@ -2,6 +2,8 @@ import {NgModule} from '@angular/core';
 import {StaffCommonModule} from '@eg/staff/common.module';
 import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
 import {CatalogRoutingModule} from './routing.module';
+import {HoldsModule} from '@eg/staff/share/holds/holds.module';
+import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
 import {CatalogComponent} from './catalog.component';
 import {SearchFormComponent} from './search-form.component';
 import {ResultsComponent} from './result/results.component';
@@ -13,10 +15,8 @@ import {ResultRecordComponent} from './result/record.component';
 import {StaffCatalogService} from './catalog.service';
 import {RecordPaginationComponent} from './record/pagination.component';
 import {RecordActionsComponent} from './record/actions.component';
-import {HoldingsService} from '@eg/staff/share/holdings.service';
 import {BasketActionsComponent} from './basket-actions.component';
 import {HoldComponent} from './hold/hold.component';
-import {HoldService} from '@eg/staff/share/hold.service';
 import {PartsComponent} from './record/parts.component';
 import {PartMergeDialogComponent} from './record/part-merge-dialog.component';
 import {BrowseComponent} from './browse.component';
@@ -39,17 +39,16 @@ import {BrowseResultsComponent} from './browse/results.component';
     PartsComponent,
     PartMergeDialogComponent,
     BrowseComponent,
-    BrowseResultsComponent
+    BrowseResultsComponent,
   ],
   imports: [
     StaffCommonModule,
     CatalogCommonModule,
-    CatalogRoutingModule
+    CatalogRoutingModule,
+    HoldsModule
   ],
   providers: [
-    StaffCatalogService,
-    HoldingsService,
-    HoldService
+    StaffCatalogService
   ]
 })
 
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html
index 998aa212fe..1f79387232 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html
@@ -1,16 +1,16 @@
 <div class="row">
-  <div class="col-lg-3">
+  <div class="col-lg-4">
     <h3 i18n>Place Hold 
       <small *ngIf="user">
        ({{user.family_name()}}, {{user.first_given_name()}})
       </small>
     </h3>
   </div>
-  <div class="col-lg-3 text-right">
-    <button class="btn btn-outline-dark btn-sm"
-      [disabled]="true" i18n>
-      <span class="material-icons mat-icon-in-button align-middle" title="Search for Patron">search</span>
-      <span class="align-middle">Search for Patron</span>
+  <div class="col-lg-2 text-right">
+    <button class="btn btn-outline-dark btn-sm" [disabled]="true">
+      <span class="material-icons mat-icon-in-button align-middle" 
+        i18n-title title="Search for Patron">search</span>
+      <span class="align-middle" i18n>Search for Patron</span>
     </button>
   </div>
 </div>
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts
index 3cfbb19ba9..8322b7a145 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts
@@ -13,8 +13,8 @@ import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.s
 import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
 import {CatalogService} from '@eg/share/catalog/catalog.service';
 import {StaffCatalogService} from '../catalog.service';
-import {HoldService, HoldRequest,
-    HoldRequestTarget} from '@eg/staff/share/hold.service';
+import {HoldsService, HoldRequest,
+    HoldRequestTarget} from '@eg/staff/share/holds/holds.service';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 
 class HoldContext {
@@ -78,7 +78,7 @@ export class HoldComponent implements OnInit {
         private bib: BibRecordService,
         private cat: CatalogService,
         private staffCat: StaffCatalogService,
-        private holds: HoldService,
+        private holds: HoldsService,
         private perm: PermService
     ) {
         this.holdContexts = [];
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html
index 1a76b282f7..c52609925e 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html
@@ -19,6 +19,10 @@
     <button class="btn btn-info ml-1" i18n>View in Catalog</button>
   </a>
 
+  <a routerLink="/staff/catalog/hold/T" [queryParams]="{target: recId}">
+    <button class="btn btn-info ml-1" i18n>Place Hold</button>
+  </a>
+
   <button class="btn btn-info ml-1" (click)="addVolumes()" i18n>
     Add Holdings
   </button>
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts
index b65bfae3a9..23ed6960cb 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts
@@ -7,7 +7,7 @@ import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
 import {StaffCatalogService} from '../catalog.service';
 import {StringService} from '@eg/share/string/string.service';
 import {ToastService} from '@eg/share/toast/toast.service';
-import {HoldingsService} from '@eg/staff/share/holdings.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
 
 @Component({
   selector: 'eg-catalog-record-actions',
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
index 0bfc6fbd55..ff3475076d 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
@@ -21,7 +21,7 @@
   <div id='staff-catalog-bib-tabs-container' class='mt-3'>
     <div class="row">
       <div class="col-lg-12 text-right">
-        <button class="btn btn-secondary btn-sm" 
+        <button class="btn btn-secondary btn-sm"
             [disabled]="recordTab == defaultTab"
             (click)="setDefaultTab()" i18n>Set Default View</button>
       </div>
@@ -51,13 +51,10 @@
       </ngb-tab>
       <ngb-tab title="View Holds" i18n-title id="holds">
         <ng-template ngbTabContent>
-          <div class="alert alert-info mt-3" i18n>
-            Holds tab not yet implemented.  See the
-            <a target="_blank"
-              href="/eg/staff/cat/catalog/record/{{recordId}}/holds">
-              AngularJS Holds Tab.
-            </a>
-          </div>
+          <eg-holds-grid [recordId]="recordId"
+            persistKey="cat.catalog.wide_holds"
+            [defaultSort]="[{name:'request_time',dir:'asc'}]"
+            [initialPickupLib]="currentSearchOrg()"></eg-holds-grid>
         </ng-template>
       </ngb-tab>
       <ngb-tab title="Monograph Parts" i18n-title id="monoparts">
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
index 2a98e36b18..c70b5658be 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
@@ -103,6 +103,13 @@ export class RecordComponent implements OnInit {
             this.bib.fleshBibUsers([summary.record]);
         });
     }
+
+    currentSearchOrg(): IdlObject {
+        if (this.staffCat && this.staffCat.searchContext) {
+            return this.staffCat.searchContext.searchOrg;
+        }
+        return null;
+    }
 }
 
 
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
index 5b16f71816..785e69e682 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
@@ -97,7 +97,13 @@ export class SearchFormComponent implements OnInit, AfterViewInit {
                 selector = '#first-query-input';
         }
 
-        this.renderer.selectRootElement(selector).focus();
+        try {
+            // TODO: sometime the selector is not available in the DOM
+            // until even later (even with setTimeouts).  Need to fix this.
+            // Note the error is thrown from selectRootElement(), not the
+            // call to .focus() on a null reference.
+            this.renderer.selectRootElement(selector).focus();
+        } catch (E) {}
     }
 
     /**
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts
new file mode 100644
index 0000000000..382e9060d7
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts
@@ -0,0 +1,25 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {HoldingsService} from './holdings.service';
+import {MarkDamagedDialogComponent} from './mark-damaged-dialog.component';
+import {MarkMissingDialogComponent} from './mark-missing-dialog.component';
+
+ at NgModule({
+    declarations: [
+      MarkDamagedDialogComponent,
+      MarkMissingDialogComponent
+    ],
+    imports: [
+        StaffCommonModule
+    ],
+    exports: [
+      MarkDamagedDialogComponent,
+      MarkMissingDialogComponent
+    ],
+    providers: [
+        HoldingsService
+    ]
+})
+
+export class HoldingsModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts
similarity index 89%
rename from Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts
rename to Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts
index cf58409982..4b28f70369 100644
--- a/Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts
+++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts
@@ -4,6 +4,8 @@
 import {Injectable, EventEmitter} from '@angular/core';
 import {NetService} from '@eg/core/net.service';
 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
+import {AuthService} from '@eg/core/auth.service';
+import {EventService} from '@eg/core/event.service';
 
 interface NewVolumeData {
     owner: number;
@@ -15,6 +17,8 @@ export class HoldingsService {
 
     constructor(
         private net: NetService,
+        private auth: AuthService,
+        private evt: EventService,
         private anonCache: AnonCacheService
     ) {}
 
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-damaged-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-damaged-dialog.component.html
new file mode 100644
index 0000000000..ddcf6b1134
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-damaged-dialog.component.html
@@ -0,0 +1,108 @@
+<eg-string #successMsg text="Successfully Marked Item Damaged" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Mark Item Damaged" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">
+      <span i18n>Mark Item Damaged</span>
+    </h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row">
+        <div class="col-lg-1">Barcode:</div>
+        <div class="col-lg-11 font-weight-bold">{{copy.barcode()}}</div>
+    </div>
+    <div class="row">
+      <div class="col-lg-1">Title:</div>
+      <div class="col-lg-11">{{bibSummary.display.title}}</div>
+    </div>
+    <div class="row">
+        <div class="col-lg-1">Author:</div>
+        <div class="col-lg-11">{{bibSummary.display.author}}</div>
+    </div>
+    <div class="card mt-3" *ngIf="chargeResponse">
+      <div class="card-header" i18n>
+        Item was previously checked out
+      </div>
+      <div class="card-body">
+        <ul class="list-group list-group-flush">
+          <li class="list-group-item" i18n>
+            Item was last checked out by patron
+            <a href="/eg/staff/circ/patron/{{chargeResponse.circ.usr().id()}}/checkout">
+              {{chargeResponse.circ.usr().family_name()}},
+              {{chargeResponse.circ.usr().first_given_name()}} 
+              ({{chargeResponse.circ.usr().usrname()}})
+            </a>.
+          </li>
+          <li class="list-group-item" i18n>
+            Item was due
+            {{chargeResponse.circ.due_date() | formatValue:'timestamp'}} 
+            and returned 
+            {{chargeResponse.circ.checkin_time() | date:'MM/dd/yy H:mm a'}}.
+          </li>
+          <li class="list-group-item">
+            <span i18n>
+              Calucated fine amount is 
+              <span class="font-weight-bold text-danger">
+                {{chargeResponse.charge | currency}}
+              </span>
+            </span>
+          </li>
+          <ng-container *ngIf="amountChangeRequested">
+            <li class="list-group-item">
+              <div class="row">
+                <div class="col-lg-3" i8n>Billing Type</div>
+                <div class="col-lg-6">
+                  <eg-combobox 
+                    placeholder="Billing Type..." i18n-placeholder
+                    (onChange)="newBtype = $event.id"
+                    [entries]="billingTypes"></eg-combobox>
+                </div>
+              </div>  
+            </li>
+            <li class="list-group-item">
+              <div class="row">
+                <div class="col-lg-3" i8n>Charge Amount</div>
+                <div class="col-lg-6">
+                  <input class="form-control" type="number" step="0.01" min="0"
+                  [(ngModel)]="newCharge"/>
+                </div>
+              </div>  
+            </li>
+            <li class="list-group-item">
+              <div class="row">
+                <div class="col-lg-3" i8n>Note</div>
+                <div class="col-lg-6">
+                  <textarea class="form-control" rows="3"
+                    [(ngModel)]="newNote"></textarea>
+                </div>
+              </div>  
+            </li>
+          </ng-container><!-- amount change requested -->
+        </ul>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <ng-container *ngIf="!chargeResponse">
+      <button type="button" class="btn btn-warning" 
+        (click)="dismiss('canceled')" i18n>Cancel</button>
+      <button type="button" class="btn btn-success" 
+        (click)="markDamaged()" i18n>Mark Damaged</button>
+    </ng-container>
+    <ng-container *ngIf="chargeResponse">
+      <button type="button" class="btn btn-warning" 
+        (click)="dismiss('canceled')" i18n>Cancel</button>
+      <button class="btn btn-info mr-2" 
+        (click)="amountChangeRequested = true" i18n>Change Amount</button>
+      <button class="btn btn-secondary mr-2" 
+        (click)="markDamaged({apply_fines:'noapply'})" i18n>No Charge</button>
+      <button class="btn btn-success mr-2" 
+        (click)="markDamaged({apply_fines:'apply'})" i18n>OK</button>
+    </ng-container>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-damaged-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-damaged-dialog.component.ts
new file mode 100644
index 0000000000..70d7f8fa9a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-damaged-dialog.component.ts
@@ -0,0 +1,154 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/**
+ * Dialog for marking items damaged and asessing related bills.
+ */
+
+ at Component({
+  selector: 'eg-mark-damaged-dialog',
+  templateUrl: 'mark-damaged-dialog.component.html'
+})
+
+export class MarkDamagedDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() copyId: number;
+    copy: IdlObject;
+    bibSummary: BibRecordSummary;
+    billingTypes: ComboboxEntry[];
+
+    // Overide the API suggested charge amount
+    amountChangeRequested: boolean;
+    newCharge: number;
+    newNote: string;
+    newBtype: number;
+
+    @ViewChild('successMsg') private successMsg: StringComponent;
+    @ViewChild('errorMsg') private errorMsg: StringComponent;
+
+
+    // Charge data returned from the server requesting additional charge info.
+    chargeResponse: any;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private pcrud: PcrudService,
+        private org: OrgService,
+        private bib: BibRecordService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+        this.billingTypes = [];
+    }
+
+    ngOnInit() {}
+
+    /**
+     * Fetch the item/record, then open the dialog.
+     * Dialog promise resolves with true/false indicating whether
+     * the mark-damanged action occured or was dismissed.
+     */
+    async open(args: NgbModalOptions): Promise<boolean> {
+        this.reset();
+
+        if (!this.copyId) {
+            return Promise.reject('copy ID required');
+        }
+
+        await this.getBillingTypes();
+        await this.getData();
+        return super.open(args);
+    }
+
+    // Fetch-cache billing types
+    async getBillingTypes(): Promise<any> {
+        if (this.billingTypes.length > 1) {
+            return Promise.resolve();
+        }
+        return this.pcrud.search('cbt',
+            {owner: this.org.fullPath(this.auth.user().ws_ou(), true)},
+            {}, {atomic: true}
+        ).toPromise().then(bts => {
+            this.billingTypes = bts
+                .sort((a, b) => a.name() < b.name() ? -1 : 1)
+                .map(bt => ({id: bt.id(), label: bt.name()}));
+        });
+    }
+
+    async getData(): Promise<any> {
+        return this.pcrud.retrieve('acp', this.copyId,
+            {flesh: 1, flesh_fields: {acp: ['call_number']}}).toPromise()
+        .then(copy => {
+            this.copy = copy;
+            return this.bib.getBibSummary(
+                copy.call_number().record()).toPromise();
+        }).then(summary => {
+                this.bibSummary = summary;
+        });
+    }
+
+    reset() {
+        this.copy = null;
+        this.bibSummary = null;
+        this.chargeResponse = null;
+        this.newCharge = null;
+        this.newNote = null;
+        this.amountChangeRequested = false;
+    }
+
+    bTypeChange(entry: ComboboxEntry) {
+        this.newBtype = entry.id;
+    }
+
+    markDamaged(args: any) {
+        this.chargeResponse = null;
+
+        if (args && args.apply_fines === 'apply') {
+            args.override_amount = this.newCharge;
+            args.override_btype = this.newBtype;
+            args.override_note = this.newNote;
+        }
+
+        this.net.request(
+            'open-ils.circ', 'open-ils.circ.mark_item_damaged',
+            this.auth.token(), this.copyId, args
+        ).subscribe(
+            result => {
+                console.debug('Mark damaged returned', result);
+
+                if (Number(result) === 1) {
+                    this.successMsg.current().then(msg => this.toast.success(msg));
+                    this.close(true);
+                    return;
+                }
+
+                const evt = this.evt.parse(result);
+
+                if (evt.textcode === 'DAMAGE_CHARGE') {
+                    // More info needed from staff on how to hangle charges.
+                    this.chargeResponse = evt.payload;
+                    this.newCharge = this.chargeResponse.charge;
+                }
+            },
+            err => {
+                this.errorMsg.current().then(m => this.toast.danger(m));
+                console.error(err);
+            }
+        );
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.html
new file mode 100644
index 0000000000..5e85a861a9
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.html
@@ -0,0 +1,44 @@
+
+
+<eg-string #successMsg
+    text="Successfully Marked Item Missing" i18n-text></eg-string>
+<eg-string #errorMsg 
+    text="Failed To Mark Item Missing" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+    <div class="modal-header bg-info">
+      <h4 class="modal-title">
+        <span i18n>Mark Item Missing</span>
+      </h4>
+      <button type="button" class="close" 
+        i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+        <span aria-hidden="true">×</span>
+      </button>
+    </div>
+    <div class="modal-body">
+      <div class="row d-flex justify-content-center">
+          <h5>Mark {{copyIds.length}} Item(s) Missing?</h5>
+      </div>
+      <div class="row" *ngIf="numSucceeded > 0">
+        <div class="col-lg-12" i18n>
+          {{numSucceeded}} Items(s) Successfully Marked Missing
+        </div>
+      </div>
+      <div class="row" *ngIf="numFailed > 0">
+        <div class="col-lg-12">
+          <div class="alert alert-warning">
+            {{numFailed}} Items(s) Failed to be Marked Missing
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <ng-container *ngIf="!chargeResponse">
+        <button type="button" class="btn btn-warning" 
+          (click)="dismiss('canceled')" i18n>Cancel</button>
+        <button type="button" class="btn btn-success" 
+          (click)="markItemsMissing()" i18n>Mark Missing</button>
+      </ng-container>
+    </div>
+  </ng-template>
+  
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.ts
new file mode 100644
index 0000000000..14e8ceb7a7
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.ts
@@ -0,0 +1,79 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+
+
+/**
+ * Dialog for marking items missing.
+ */
+
+ at Component({
+  selector: 'eg-mark-missing-dialog',
+  templateUrl: 'mark-missing-dialog.component.html'
+})
+
+export class MarkMissingDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() copyIds: number[];
+
+    numSucceeded: number;
+    numFailed: number;
+
+    @ViewChild('successMsg')
+        private successMsg: StringComponent;
+
+    @ViewChild('errorMsg')
+        private errorMsg: StringComponent;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {}
+
+    async markOneItemMissing(ids: number[]): Promise<any> {
+        if (ids.length === 0) {
+            return Promise.resolve();
+        }
+
+        const id = ids.pop();
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.mark_item_missing',
+            this.auth.token(), id
+        ).toPromise().then(async(result) => {
+            if (Number(result) === 1) {
+                this.numSucceeded++;
+                this.toast.success(await this.successMsg.current());
+            } else {
+                this.numFailed++;
+                console.error('Mark missing failed ', this.evt.parse(result));
+                this.toast.warning(await this.errorMsg.current());
+            }
+            return this.markOneItemMissing(ids);
+        });
+    }
+
+    async markItemsMissing(): Promise<any> {
+        this.numSucceeded = 0;
+        this.numFailed = 0;
+        const ids = [].concat(this.copyIds);
+        await this.markOneItemMissing(ids);
+        this.close(this.numSucceeded > 0);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/cancel-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/cancel-dialog.component.html
new file mode 100644
index 0000000000..d7417fa646
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/cancel-dialog.component.html
@@ -0,0 +1,60 @@
+<eg-string #successMsg
+    text="Successfully Canceled Hold" i18n-text></eg-string>
+<eg-string #errorMsg
+    text="Failed To Cancel Hold" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+    <div class="modal-header bg-info">
+      <h4 class="modal-title">
+        <span i18n>Cancel Hold</span>
+      </h4>
+      <button type="button" class="close"
+        i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+        <span aria-hidden="true">×</span>
+      </button>
+    </div>
+    <div class="modal-body">
+      <div class="row d-flex justify-content-center">
+          <h5>Cancel {{holdIds.length}} Holds?</h5>
+      </div>
+      <div class="row mt-2">
+        <div class="col-lg-4">
+          <label for="cance-reasons" i18n>Cancel Reason</label>
+        </div>
+        <div class="col-lg-8">
+          <eg-combobox id='cancel-reasons' [entries]="cancelReasons"
+            [startId]="5" (onChange)="cancelReason = $event ? $event.id : null">
+          </eg-combobox>
+        </div>
+      </div>
+      <div class="row mt-2">
+        <div class="col-lg-4">
+          <label for="cance-note" i18n>Cancel Note</label>
+        </div>
+        <div class="col-lg-8">
+          <textarea id='cancel-note' class="form-control"
+            [(ngModel)]="cancelNote"></textarea>
+        </div>
+        </div>
+      <div class="row mt-2" *ngIf="numSucceeded > 0">
+        <div class="col-lg-12" i18n>
+          {{numSucceeded}} Hold(s) Successfully Canceled
+        </div>
+        <div class="row" *ngIf="numFailed > 0">
+          <div class="col-lg-12">
+            <div class="alert alert-warning">
+              {{numFailed}} Hold(s) Failed to Cancel.
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <ng-container *ngIf="!chargeResponse">
+        <button type="button" class="btn btn-warning"
+          (click)="dismiss('canceled')" i18n>Cancel</button>
+        <button type="button" class="btn btn-success"
+          (click)="cancelBatch()" i18n>Cancel Hold</button>
+      </ng-container>
+    </div>
+  </ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/cancel-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/cancel-dialog.component.ts
new file mode 100644
index 0000000000..98af5143d3
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/cancel-dialog.component.ts
@@ -0,0 +1,98 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/**
+ * Dialog for canceling hold requests.
+ */
+
+ at Component({
+  selector: 'eg-hold-cancel-dialog',
+  templateUrl: 'cancel-dialog.component.html'
+})
+
+export class HoldCancelDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() holdIds: number[];
+    @ViewChild('successMsg') private successMsg: StringComponent;
+    @ViewChild('errorMsg') private errorMsg: StringComponent;
+
+    changesApplied: boolean;
+    numSucceeded: number;
+    numFailed: number;
+    cancelReason: number;
+    cancelReasons: ComboboxEntry[];
+    cancelNote: string;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private pcrud: PcrudService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+        this.cancelReasons = [];
+    }
+
+    ngOnInit() {
+        // Avoid fetching cancel reasons in ngOnInit becaues that causes
+        // them to load regardless of whether the dialog is ever used.
+    }
+
+    open(args: NgbModalOptions): Promise<boolean> {
+
+        if (this.cancelReasons.length === 0) {
+            this.pcrud.retrieveAll('ahrcc', {}, {atomic: true}).toPromise()
+            .then(reasons => {
+                this.cancelReasons =
+                    reasons.map(r => ({id: r.id(), label: r.label()}));
+            });
+        }
+
+        return super.open(args);
+    }
+
+    async cancelNext(ids: number[]): Promise<any> {
+        if (ids.length === 0) {
+            return Promise.resolve();
+        }
+
+        return this.net.request(
+            'open-ils.circ', 'open-ils.circ.hold.cancel',
+            this.auth.token(), ids.pop(),
+            this.cancelReason, this.cancelNote
+        ).toPromise().then(
+            async(result) => {
+                if (Number(result) === 1) {
+                    this.numSucceeded++;
+                    this.toast.success(await this.successMsg.current());
+                } else {
+                    this.numFailed++;
+                    console.error(this.evt.parse(result));
+                    this.toast.warning(await this.errorMsg.current());
+                }
+                this.cancelNext(ids);
+            }
+        );
+    }
+
+    async cancelBatch(): Promise<any> {
+        this.numSucceeded = 0;
+        this.numFailed = 0;
+        const ids = [].concat(this.holdIds);
+        await this.cancelNext(ids);
+        this.close(this.numSucceeded > 0);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.html
new file mode 100644
index 0000000000..daeeb8957c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.html
@@ -0,0 +1,99 @@
+
+<eg-staff-banner bannerText="Hold Details (#{{hold.id}})" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row">
+  <div class="col-lg-3">
+    <button (click)="showListView()" class="btn btn-info" i18n>List View</button>
+  </div>
+</div>
+
+<div class="well-table">
+  <div class="well-row">
+    <div class="well-label" i18n>Request Date</div>
+    <div class="well-value">{{hold.request_time | formatValue:'timestamp'}}</div>
+    <div class="well-label" i18n>Capture Date</div>
+    <div class="well-value">{{hold.capture_time | formatValue:'timestamp'}}</div>
+    <div class="well-label" i18n>Available On</div>
+    <div class="well-value">{{hold.shelf_time | formatValue:'timestamp'}}</div>
+  </div>
+  <div class="well-row">
+    <div class="well-label" i18n>hold Type</div>
+    <div class="well-value">
+      {{hold.hold_type}}
+      <!-- TODO: add part data to wide holds 
+      <span *ngIf="hold.hold_type == 'P'"> - {{hold.part_label}}</span>
+      -->
+    </div>
+    <div class="well-label" i18n>Current Item</div>
+    <div class="well-value">
+      <a href="/eg/staff/cat/item/{{hold.cp_id}}">{{hold.cp_barcode}}</a>
+    </div>
+    <div class="well-label" i18n>Call Number</div>
+    <div class="well-value">{{hold.cn_full_label}}</div>
+  </div>
+  <div class="well-row">
+    <div class="well-label" i18n>Pickup Lib</div>
+    <div class="well-value">{{hold.pl_shortname}}</div>
+    <div class="well-label" i18n>Status</div>
+    <div class="well-value">
+      <ng-container [ngSwitch]="hold.hold_status">
+        <div *ngSwitchCase="-1" i18n>Unknown Error</div>
+        <div *ngSwitchCase="1" i18n>Waiting for Item</div>
+        <div *ngSwitchCase="2" i18n>Waiting for Capture</div>
+        <div *ngSwitchCase="3" i18n>In Transit</div>
+        <div *ngSwitchCase="4" i18n>Ready for Pickup</div>
+        <div *ngSwitchCase="5" i18n>Hold Shelf Delay</div>
+        <div *ngSwitchCase="6" i18n>Canceled</div>
+        <div *ngSwitchCase="7" i18n>Suspended</div>
+        <div *ngSwitchCase="8" i18n>Wrong Shelf</div>
+        <div *ngSwitchCase="9" i18n>Fulfilled</div>
+      </ng-container>
+    </div>
+    <div class="well-label" i18n>Behind Desk</div>
+    <div class="well-value">{{hold.behind_desk == '1'}}</div>
+  </div>
+  <div class="well-row">
+    <div class="well-label" i18n>Current Shelf Lib</div>
+    <div class="well-value">{{getOrgName(hold.current_shelf_lib)}}</div>
+    <div class="well-label" i18n>Current Shelving Location</div>
+    <div class="well-value">{{hold.acpl_name}}</div>
+    <div class="well-label" i18n>Force Item Quality</div>
+    <div class="well-value">{{hold.mint_condition == '1'}}</div>
+  </div>
+  <div class="well-row">
+    <div class="well-label" i18n>Email Notify</div>
+    <div class="well-value">{{hold.email_notify == '1'}}</div>
+    <div class="well-label" i18n>Phone Notify</div>
+    <div class="well-value">{{hold.phone_notify}}</div>
+    <div class="well-label" i18n>SMS Notify</div>
+    <div class="well-value">{{hold.sms_notify}}</div>
+  </div>
+  <div class="well-row">
+    <div class="well-label" i18n>Cancel Cause</div>
+    <div class="well-value">{{hold.cancel_cause}}</div><!-- TODO: label -->
+    <div class="well-label" i18n>Cancel Time</div>
+    <div class="well-value">{{hold.cancel_time | formatValue:'timestamp'}}</div>
+    <div class="well-label" i18n>Cancel Note</div>
+    <div class="well-value">{{hold.cancel_note}}</div>
+  </div>
+  <div class="well-row">
+    <div class="well-label" i18n>Patron Name</div>
+    <div class="well-value">
+      <a href="/eg/staff/circ/patron/{{hold.usr_id}}/checkout">
+        {{hold.usr_display_name}}
+      </a>
+    </div>
+    <!-- force consistent width -->
+    <div class="well-label" i18n>Patron Barcode</div>
+    <div class="well-value">
+      <a href="/eg/staff/circ/patron/{{hold.usr_id}}/checkout">
+        {{hold.ucard_barcode}}
+      </a>
+    </div>
+    <!-- for balance -->
+    <div class="well-label" i18n></div>
+    <div class="well-label" i18n></div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.ts
new file mode 100644
index 0000000000..67b3801e0d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.ts
@@ -0,0 +1,67 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {Observable, Observer, of} from 'rxjs';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+
+/** Hold details read-only view */
+
+ at Component({
+  selector: 'eg-hold-detail',
+  templateUrl: 'detail.component.html'
+})
+export class HoldDetailComponent implements OnInit {
+
+    _holdId: number;
+    @Input() set holdId(id: number) {
+        this._holdId = id;
+        if (this.initDone) {
+            this.fetchHold();
+        }
+    }
+
+    hold: any; // wide hold reference
+    @Input() set wideHold(wh: any) {
+        this.hold = wh;
+    }
+
+    initDone: boolean;
+    @Output() onShowList: EventEmitter<any>;
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private auth: AuthService,
+    ) {
+        this.onShowList = new EventEmitter<any>();
+    }
+
+    ngOnInit() {
+        this.initDone = true;
+        this.fetchHold();
+    }
+
+    fetchHold() {
+        if (!this._holdId) { return; }
+
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.hold.wide_hash.stream',
+            this.auth.token(), {id: this._holdId}
+        ).subscribe(wideHold => {
+            this.hold = wideHold;
+        });
+    }
+
+    getOrgName(id: number) {
+        if (id) {
+            return this.org.get(id).shortname();
+        }
+    }
+
+    showListView() {
+        this.onShowList.emit();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html
new file mode 100644
index 0000000000..62d269b306
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html
@@ -0,0 +1,244 @@
+<!-- hold grid with jump-off points to detail page and other actions -->
+
+<!-- our on-demand dialogs-->
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
+<eg-hold-transfer-dialog #transferDialog></eg-hold-transfer-dialog>
+<eg-mark-damaged-dialog #markDamagedDialog></eg-mark-damaged-dialog>
+<eg-mark-missing-dialog #markMissingDialog></eg-mark-missing-dialog>
+<eg-hold-retarget-dialog #retargetDialog></eg-hold-retarget-dialog>
+<eg-hold-cancel-dialog #cancelDialog></eg-hold-cancel-dialog>
+<eg-hold-manage-dialog #manageDialog></eg-hold-manage-dialog>
+
+<div class='eg-holds w-100 mt-3'>
+
+  <ng-container *ngIf="mode == 'detail'">
+    <eg-hold-detail [wideHold]="detailHold" (onShowList)="mode='list'">
+    </eg-hold-detail>
+  </ng-container>
+
+  <ng-container *ngIf="mode == 'list'">
+
+    <div class="row" *ngIf="!hidePickupLibFilter">
+      <div class="col-lg-4">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <div class="input-group-text" i18n>Pickup Library</div>
+          </div>
+          <eg-org-select [initialOrg]="pickupLib" (onChange)="pickupLibChanged($event)">
+          </eg-org-select>
+        </div>
+      </div>
+    </div>
+
+    <eg-grid #holdsGrid [dataSource]="gridDataSource" [sortable]="true"
+      [multiSortable]="true" [persistKey]="persistKey"
+      (onRowActivate)="showDetail($event)">
+
+      <eg-grid-toolbar-action
+        i18n-label label="Show Hold Details" i18n-group group="Hold"
+        (onClick)="showDetails($event)"></eg-grid-toolbar-action>
+
+      <eg-grid-toolbar-action
+        i18n-label label="Modify Hold(s)" group="Hold" i18n-group
+        (onClick)="showManageDialog($event)">
+      </eg-grid-toolbar-action>
+
+      <eg-grid-toolbar-action
+        i18n-label label="Show Last Few Circulations" group="Item" i18n-group
+        (onClick)="showRecentCircs($event)"></eg-grid-toolbar-action>
+
+      <eg-grid-toolbar-action
+        i18n-label label="Retrieve Patron" group="Patron" i18n-group
+        (onClick)="showPatron($event)">
+      </eg-grid-toolbar-action>
+
+      <eg-grid-toolbar-action
+        i18n-group group="Hold" i18n-label label="Transfer To Marked Title"
+        (onClick)="showTransferDialog($event)">
+      </eg-grid-toolbar-action>
+
+      <eg-grid-toolbar-action
+        group="Item" i18n-group i18n-label label="Mark Item Damaged"
+        (onClick)="showMarkDamagedDialog($event)"></eg-grid-toolbar-action>
+
+      <eg-grid-toolbar-action
+        i18n-group group="Item" i18n-label label="Mark Item Missing"
+        (onClick)="showMarkMissingDialog($event)">
+      </eg-grid-toolbar-action>
+
+      <eg-grid-toolbar-action
+        i18n-group group="Hold" i18n-label label="Find Another Target"
+        (onClick)="showRetargetDialog($event)"></eg-grid-toolbar-action>
+
+      <eg-grid-toolbar-action
+        i18-group group="Hold" i18n-label label="Cancel Hold"
+        (onClick)="showCancelDialog($event)"></eg-grid-toolbar-action>
+
+      <eg-grid-column i18n-label label="Hold ID" path='id' [index]="true">
+      </eg-grid-column>
+
+      <ng-template #barcodeTmpl let-hold="row">
+        <a href="/eg/staff/cat/item/{{cp_id}}/summary">
+          {{hold.cp_barcode}}
+        </a>
+      </ng-template>
+      <eg-grid-column i18n-label label="Current Item" name='cp_barcode'
+        [cellTemplate]="barcodeTmpl">
+      </eg-grid-column>
+
+      <eg-grid-column i18n-label label="Patron Barcode"
+          path='ucard_barcode' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Patron alias" path="usr_alias"></eg-grid-column>
+      <eg-grid-column i18n-label label="Request Date"
+          path='request_time' datatype="timestamp"></eg-grid-column>
+      <eg-grid-column i18n-label label="Capture Date" path='capture_time'
+          datatype="timestamp"></eg-grid-column>
+      <eg-grid-column i18n-label label="Available Date" path='shelf_time'
+          datatype="timestamp"></eg-grid-column>
+      <eg-grid-column i18n-label label="Hold Type" path='hold_type'></eg-grid-column>
+      <eg-grid-column i18n-label label="Pickup Library" path='pl_shortname'></eg-grid-column>
+
+      <ng-template #titleTmpl let-hold="row">
+        <a class="no-href" routerLink="/staff/catalog/record/{{hold.record_id}}">
+          {{hold.title}}
+        </a>
+      </ng-template>
+      <eg-grid-column i18n-label label="Title" [hidden]="true"
+          name='title' [cellTemplate]="titleTmpl"></eg-grid-column>
+      <eg-grid-column i18n-label label="Author" path='author'
+          [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Potential Items" path='potentials'>
+      </eg-grid-column>
+      <eg-grid-column i18n-label label="Status" path='status_string'>
+      </eg-grid-column>
+      <eg-grid-column i18n-label label="Queue Position"
+          path='relative_queue_position' [hidden]="true"></eg-grid-column>
+      <eg-grid-column path='usr_id' i18n-label label="User ID" [hidden]="true"></eg-grid-column>
+      <eg-grid-column path='usr_usrname' i18n-label label="Username" [hidden]="true"></eg-grid-column>
+
+      <eg-grid-column path='usr_first_given_name' i18n-label label="First Name" [hidden]="true"></eg-grid-column>
+      <eg-grid-column path='usr_family_name' i18n-label label="Last Name" [hidden]="true"></eg-grid-column>
+      <eg-grid-column path='rusr_id' i18n-label label="Requestor ID" [hidden]="true"></eg-grid-column>
+      <eg-grid-column path='rusr_usrname' i18n-label label="Requestor Username" [hidden]="true"></eg-grid-column>
+
+     <eg-grid-column i18n-label label="Item Status" path="cs_name" [hidden]="true"></eg-grid-column>
+
+      <eg-grid-column path='acnp_label' i18n-label label="CN Prefix" [hidden]="true"></eg-grid-column>
+      <eg-grid-column path='acns_label' i18n-label label="CN Suffix" [hidden]="true"></eg-grid-column>
+      <eg-grid-column path='mvr.*' parent-idl-class="mvr" [hidden]="true"></eg-grid-column>
+
+      <eg-grid-column i18n-label label="Fulfillment Date/Time" path='fulfillment_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Checkin Time" path='checkin_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Return Time" path='return_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Last Targeting Date/Time" path='prev_check_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Expire Time" path='expire_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Hold Cancel Date/Time" path='cancel_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Cancelation note" path='cancel_note' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Hold Target" path='target' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Current Copy" path='current_copy' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Fulfilling Staff" path='fulfillment_staff' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Fulfilling Library" path='fulfillment_lib' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Requesting Library" path='request_lib' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Requesting User" path='requestor' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="User" path='usr' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Selection Library" path='selection_ou' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Item Selection Depth" path='selection_depth' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Holdable Formats (for M-type hold)" path='holdable_formats' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Notifications Phone Number" path='phone_notify' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Notifications SMS Number" path='sms_notify' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Notify by Email?" path='email_notify' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="SMS Carrier" path='sms_carrier' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Currently Frozen" path='frozen' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Activation Date" path='thaw_date' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Top of Queue" path='cut_in_line' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Is Mint Condition" path='mint_condition' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Shelf Expire Time" path='shelf_expire_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Current Shelf Library" path='current_shelf_lib' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Behind Desk" path='behind_desk' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Status" path='hold_status' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Clearable" path='clear_me' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Is Staff-placed Hold" path='is_staff_hold' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Cancelation Cause ID" path='cc_id' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Cancelation Cause" path='cc_label' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Pickup Library" path='pl_shortname'></eg-grid-column>
+      <eg-grid-column i18n-label label="Pickup Library Name" path='pl_name' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Pickup Library Email" path='pl_email' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Pickup Library Phone" path='pl_phone' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Pickup Library Opac Visible" path='pl_opac_visible' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit ID" path='tr_id' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit Send Time" path='tr_source_send_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit Receive Time" path='tr_dest_recv_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit Copy" path='tr_target_copy' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit Source" path='tr_source' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit Destination" path='tr_dest' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit Copy Status" path='tr_copy_status' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit Hold" path='tr_hold' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Transit Cancel Time" path='tr_cancel_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Hold Note Count" path='note_count' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="User Display Name" path='usr_display_name' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Requestor Username" path='rusr_usrname' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy ID" path='cp_id' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Number on Volume" path='cp_copy_number' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Fine Level" path='cp_fine_level' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Can Circulate" path='cp_circulate' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Deposit Amount" path='cp_deposit_amount' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Is Deposit Required" path='cp_deposit' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Is Reference" path='cp_ref' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Is Holdable" path='cp_holdable' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Price" path='cp_price' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Barcode" path='cp_barcode' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Circulation Modifier" path='cp_circ_modifier' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Circulate as MARC Type" path='cp_circ_as_type' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Precat Dummy Title" path='cp_dummy_title' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Precat Dummy Author" path='cp_dummy_author' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Alert Message (deprecated)" path='cp_alert_message' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy OPAC Visible" path='cp_opac_visible' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Deleted" path='cp_deleted' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Floating Group" path='cp_floating' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Precat Dummy ISBN" path='cp_dummy_isbn' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Status Change Time" path='cp_status_change_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Active Date" path='cp_active_date' datatype="timestamp" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Is Mint Condition" path='cp_mint_condition' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Cost" path='cp_cost' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Status Is Holdable" path='cs_holdable' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Status Is OPAC Visible" path='cs_opac_visible' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Status Is Copy-Active" path='cs_copy_active' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Status Is Deleted" path='cs_restrict_copy_delete' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Status Is Available" path='cs_is_available' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Issuance i18n-label label" path='issuance_label' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Call Number ID" path='cn_id' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="CN i18n-label label" path='cn_label' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="CN i18n-label label Class" path='cn_label_class' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="CN Sort Key" path='cn_label_sortkey' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Part ID" path='p_id' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Part i18n-label label" path='p_label' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Part Sort Key" path='p_label_sortkey' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Part Is Deleted" path='p_deleted' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="CN Full i18n-label label" path='cn_full_label' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Record ID" path='record_id' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location ID" path='acpl_id' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location" path='acpl_name' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location Holdable" path='acpl_holdable' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location Hold-Verify" path='acpl_hold_verify' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location OPAC Visible" path='acpl_opac_visible' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location Can Circulate" path='acpl_circulate' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location Prefix" path='acpl_label_prefix' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location Suffix" path='acpl_label_suffix' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location Checkin Alert" path='acpl_checkin_alert' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location Is Deleted" path='acpl_deleted' datatype="bool" [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location URL" path='acpl_url' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Copy Location Order" path='copy_location_order_position' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Default Estimated Wait Time" path='default_estimated_wait' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Minimum Estimated Wait Time" path='min_estimated_wait' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Peer Hold Count" path='other_holds' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Total Wait Time" path='total_wait_time' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Notify Count" path='notification_count' [hidden]="true"></eg-grid-column>
+      <eg-grid-column i18n-label label="Last Notify Time" path='last_notification_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+
+    </eg-grid>
+
+  </ng-container>
+
+</div>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts
new file mode 100644
index 0000000000..e0e894d871
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts
@@ -0,0 +1,366 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable, Observer, of} from 'rxjs';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {Pager} from '@eg/share/util/pager';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {MarkDamagedDialogComponent
+    } from '@eg/staff/share/holdings/mark-damaged-dialog.component';
+import {MarkMissingDialogComponent
+    } from '@eg/staff/share/holdings/mark-missing-dialog.component';
+import {HoldRetargetDialogComponent
+    } from '@eg/staff/share/holds/retarget-dialog.component';
+import {HoldTransferDialogComponent} from './transfer-dialog.component';
+import {HoldCancelDialogComponent} from './cancel-dialog.component';
+import {HoldManageDialogComponent} from './manage-dialog.component';
+
+/** Holds grid with access to detail page and other actions */
+
+ at Component({
+  selector: 'eg-holds-grid',
+  templateUrl: 'grid.component.html'
+})
+export class HoldsGridComponent implements OnInit {
+
+    // If either are set/true, the pickup lib selector will display
+    @Input() initialPickupLib: number | IdlObject;
+    @Input() hidePickupLibFilter: boolean;
+
+    // Grid persist key
+    @Input() persistKey: string;
+
+    // How to sort when no sort parameters have been applied
+    // via grid controls.  This uses the eg-grid sort format:
+    // [{name: fname, dir: 'asc'}, {name: fname2, dir: 'desc'}]
+    @Input() defaultSort: any[];
+
+    mode: 'list' | 'detail' | 'manage' = 'list';
+    initDone = false;
+    holdsCount: number;
+    pickupLib: IdlObject;
+    gridDataSource: GridDataSource;
+    detailHold: any;
+    editHolds: number[];
+    transferTarget: number;
+    copyStatuses: {[id: string]: IdlObject};
+
+    @ViewChild('holdsGrid') private holdsGrid: GridComponent;
+    @ViewChild('progressDialog')
+        private progressDialog: ProgressDialogComponent;
+    @ViewChild('transferDialog')
+        private transferDialog: HoldTransferDialogComponent;
+    @ViewChild('markDamagedDialog')
+        private markDamagedDialog: MarkDamagedDialogComponent;
+    @ViewChild('markMissingDialog')
+        private markMissingDialog: MarkMissingDialogComponent;
+    @ViewChild('retargetDialog')
+        private retargetDialog: HoldRetargetDialogComponent;
+    @ViewChild('cancelDialog')
+        private cancelDialog: HoldCancelDialogComponent;
+    @ViewChild('manageDialog')
+        private manageDialog: HoldManageDialogComponent;
+
+    // Bib record ID.
+    _recordId: number;
+    @Input() set recordId(id: number) {
+        this._recordId = id;
+        if (this.initDone) { // reload on update
+            this.holdsGrid.reload();
+        }
+    }
+
+    _userId: number;
+    @Input() set userId(id: number) {
+        this._userId = id;
+        if (this.initDone) {
+            this.holdsGrid.reload();
+        }
+    }
+
+    // Include holds canceled on or after the provided date.
+    // If no value is passed, canceled holds are not displayed.
+    _showCanceledSince: Date;
+    @Input() set showCanceledSince(show: Date) {
+        this._showCanceledSince = show;
+        if (this.initDone) { // reload on update
+            this.holdsGrid.reload();
+        }
+    }
+
+    // Include holds fulfilled on or after hte provided date.
+    // If no value is passed, fulfilled holds are not displayed.
+    _showFulfilledSince: Date;
+    @Input() set showFulfilledSince(show: Date) {
+        this._showFulfilledSince = show;
+        if (this.initDone) { // reload on update
+            this.holdsGrid.reload();
+        }
+    }
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private auth: AuthService
+    ) {
+        this.gridDataSource = new GridDataSource();
+        this.copyStatuses = {};
+    }
+
+    ngOnInit() {
+        this.initDone = true;
+        this.pickupLib = this.org.get(this.initialPickupLib);
+
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+
+            if (this.defaultSort && sort.length === 0) {
+                // Only use initial sort if sorting has not been modified
+                // by the grid's own sort controls.
+                sort = this.defaultSort;
+            }
+
+            // sorting not currently supported
+            return this.fetchHolds(pager, sort);
+        };
+    }
+
+    pickupLibChanged(org: IdlObject) {
+        this.pickupLib = org;
+        this.holdsGrid.reload();
+    }
+
+    applyFilters(): any {
+        const filters: any = {
+            is_staff_request: true,
+            fulfillment_time: this._showFulfilledSince ?
+                this._showFulfilledSince.toISOString() : null,
+            cancel_time: this._showCanceledSince ?
+                this._showCanceledSince.toISOString() : null,
+        };
+
+        if (this.pickupLib) {
+            filters.pickup_lib =
+                this.org.descendants(this.pickupLib, true);
+        }
+
+        if (this._recordId) {
+            filters.record_id = this._recordId;
+        }
+
+        if (this._userId) {
+            filters.usr_id = this._userId;
+        }
+
+        return filters;
+    }
+
+    fetchHolds(pager: Pager, sort: any[]): Observable<any> {
+
+        // We need at least one filter.
+        if (!this._recordId && !this.pickupLib && !this._userId) {
+            return of([]);
+        }
+
+        const filters = this.applyFilters();
+
+        const orderBy: any = [];
+        sort.forEach(obj => {
+            const subObj: any = {};
+            subObj[obj.name] = {dir: obj.dir, nulls: 'last'};
+            orderBy.push(subObj);
+        });
+
+        let observer: Observer<any>;
+        const observable = new Observable(obs => observer = obs);
+
+        this.progressDialog.open();
+        this.progressDialog.update({value: 0, max: 1});
+        let first = true;
+        let loadCount = 0;
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.hold.wide_hash.stream',
+            // Pre-fetch all holds consistent with AngJS version
+            this.auth.token(), filters, orderBy
+            // Alternatively, fetch holds in pages.
+            // this.auth.token(), filters, orderBy, pager.limit, pager.offset
+        ).subscribe(
+            holdData => {
+
+                if (first) { // First response is the hold count.
+                    this.holdsCount = Number(holdData);
+                    first = false;
+
+                } else { // Subsequent responses are hold data blobs
+
+                    this.progressDialog.update(
+                        {value: ++loadCount, max: this.holdsCount});
+
+                    observer.next(holdData);
+                }
+            },
+            err => {
+                this.progressDialog.close();
+                observer.error(err);
+            },
+            ()  => {
+                this.progressDialog.close();
+                observer.complete();
+            }
+        );
+
+        return observable;
+    }
+
+    showDetails(rows: any[]) {
+        this.showDetail(rows[0]);
+    }
+
+    showDetail(row: any) {
+        if (row) {
+            this.mode = 'detail';
+            this.detailHold = row;
+        }
+    }
+
+    showManager(rows: any[]) {
+        if (rows.length) {
+            this.mode = 'manage';
+            this.editHolds = rows.map(r => r.id);
+        }
+    }
+
+    handleModify(rowsModified: boolean) {
+        this.mode = 'list';
+
+        if (rowsModified) {
+            // give the grid a chance to render then ask it to reload
+            setTimeout(() => this.holdsGrid.reload());
+        }
+    }
+
+
+
+    showRecentCircs(rows: any[]) {
+        if (rows.length) {
+            const url =
+                '/eg/staff/cat/item/' + rows[0].cp_id + '/circ_list';
+            window.open(url, '_blank');
+        }
+    }
+
+    showPatron(rows: any[]) {
+        if (rows.length) {
+            const url =
+                '/eg/staff/circ/patron/' + rows[0].usr_id + '/checkout';
+            window.open(url, '_blank');
+        }
+    }
+
+    showManageDialog(rows: any[]) {
+        const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
+        if (holdIds.length > 0) {
+            this.manageDialog.holdIds = holdIds;
+            this.manageDialog.open({size: 'lg'}).then(
+                rowsModified => {
+                    if (rowsModified) {
+                        this.holdsGrid.reload();
+                    }
+                },
+                dismissed => {}
+            );
+        }
+    }
+
+    showTransferDialog(rows: any[]) {
+        const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
+        if (holdIds.length > 0) {
+            this.transferDialog.holdIds = holdIds;
+            this.transferDialog.open({}).then(
+                rowsModified => {
+                    if (rowsModified) {
+                        this.holdsGrid.reload();
+                    }
+                },
+                dismissed => {}
+            );
+        }
+    }
+
+    async showMarkDamagedDialog(rows: any[]) {
+        const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
+        if (copyIds.length === 0) { return; }
+
+        let rowsModified = false;
+
+        const markNext = async(ids: number[]) => {
+            if (ids.length === 0) {
+                return Promise.resolve();
+            }
+
+            this.markDamagedDialog.copyId = ids.pop();
+            this.markDamagedDialog.open({size: 'lg'}).then(
+                ok => {
+                    if (ok) { rowsModified = true; }
+                    return markNext(ids);
+                },
+                dismiss => markNext(ids)
+            );
+        };
+
+        await markNext(copyIds);
+        if (rowsModified) {
+            this.holdsGrid.reload();
+        }
+    }
+
+    showMarkMissingDialog(rows: any[]) {
+        const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
+        if (copyIds.length > 0) {
+            this.markMissingDialog.copyIds = copyIds;
+            this.markMissingDialog.open({}).then(
+                rowsModified => {
+                    if (rowsModified) {
+                        this.holdsGrid.reload();
+                    }
+                },
+                dismissed => {} // avoid console errors
+            );
+        }
+    }
+
+    showRetargetDialog(rows: any[]) {
+        const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
+        if (holdIds.length > 0) {
+            this.retargetDialog.holdIds = holdIds;
+            this.retargetDialog.open({}).then(
+                rowsModified => {
+                    if (rowsModified) {
+                        this.holdsGrid.reload();
+                    }
+                },
+                dismissed => {}
+            );
+        }
+    }
+
+    showCancelDialog(rows: any[]) {
+        const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
+        if (holdIds.length > 0) {
+            this.cancelDialog.holdIds = holdIds;
+            this.cancelDialog.open({}).then(
+                rowsModified => {
+                    if (rowsModified) {
+                        this.holdsGrid.reload();
+                    }
+                },
+                dismissed => {}
+            );
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/holds.module.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/holds.module.ts
new file mode 100644
index 0000000000..5bcb68aeaf
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/holds.module.ts
@@ -0,0 +1,41 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
+import {HoldsService} from './holds.service';
+import {HoldsGridComponent} from './grid.component';
+import {HoldDetailComponent} from './detail.component';
+import {HoldManageComponent} from './manage.component';
+import {HoldRetargetDialogComponent} from './retarget-dialog.component';
+import {HoldTransferDialogComponent} from './transfer-dialog.component';
+import {HoldCancelDialogComponent} from './cancel-dialog.component';
+import {HoldManageDialogComponent} from './manage-dialog.component';
+
+ at NgModule({
+    declarations: [
+        HoldsGridComponent,
+        HoldDetailComponent,
+        HoldManageComponent,
+        HoldRetargetDialogComponent,
+        HoldTransferDialogComponent,
+        HoldCancelDialogComponent,
+        HoldManageDialogComponent
+    ],
+    imports: [
+        StaffCommonModule,
+        HoldingsModule
+    ],
+    exports: [
+        HoldsGridComponent,
+        HoldDetailComponent,
+        HoldManageComponent,
+        HoldRetargetDialogComponent,
+        HoldTransferDialogComponent,
+        HoldCancelDialogComponent,
+        HoldManageDialogComponent
+    ],
+    providers: [
+        HoldsService
+    ]
+})
+
+export class HoldsModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/share/hold.service.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/holds.service.ts
similarity index 84%
rename from Open-ILS/src/eg2/src/app/staff/share/hold.service.ts
rename to Open-ILS/src/eg2/src/app/staff/share/holds/holds.service.ts
index 00e7374943..784dcec4eb 100644
--- a/Open-ILS/src/eg2/src/app/staff/share/hold.service.ts
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/holds.service.ts
@@ -1,7 +1,7 @@
 /**
  * Common code for mananging holdings
  */
-import {Injectable, EventEmitter} from '@angular/core';
+import {Injectable} from '@angular/core';
 import {Observable} from 'rxjs';
 import {map, mergeMap} from 'rxjs/operators';
 import {IdlObject} from '@eg/core/idl.service';
@@ -56,13 +56,14 @@ export interface HoldRequestTarget {
     metarecord_filters?: any;
 }
 
+/** Service for performing various hold-related actions */
+
 @Injectable()
-export class HoldService {
+export class HoldsService {
 
     constructor(
         private evt: EventService,
         private net: NetService,
-        private pcrud: PcrudService,
         private auth: AuthService,
         private bib: BibRecordService,
     ) {}
@@ -138,5 +139,31 @@ export class HoldService {
             }));
         }));
     }
+
+    /**
+      * Update a list of holds.
+      * Returns observable of results, one per hold.
+      * Result is either a Number (hold ID) or an EgEvent object.
+      */
+    updateHolds(holds: IdlObject[]): Observable<any> {
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.hold.update.batch',
+            this.auth.token(), holds
+        ).pipe(map(response => {
+
+            if (Number(response) > 0) { return Number(response); }
+
+            if (Array.isArray(response)) { response = response[0]; }
+
+            const evt = this.evt.parse(response);
+
+            console.warn('Hold update returned event', evt);
+            return evt;
+        }));
+    }
 }
 
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/manage-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/manage-dialog.component.html
new file mode 100644
index 0000000000..ac07dd6f35
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/manage-dialog.component.html
@@ -0,0 +1,18 @@
+<ng-template #dialogContent>
+    <div class="modal-header bg-info">
+      <ng-container *ngIf="holdIds.length == 1">
+        <h4 class="modal-title" i18n>Modify Hold (#{{holdIds[0]}})</h4>
+      </ng-container>
+      <ng-container *ngIf="holdIds.length > 1">
+        <h4 class="modal-title">Batch Modify {{holdIds.length}} Holds</h4>
+      </ng-container>
+      <button type="button" class="close"
+        i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+        <span aria-hidden="true">×</span>
+      </button>
+    </div>
+    <div class="modal-body">
+      <eg-hold-manage [holdIds]="holdIds" (onComplete)="onComplete($event)">
+      </eg-hold-manage>
+    </div>
+  </ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/manage-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/manage-dialog.component.ts
new file mode 100644
index 0000000000..93375c0eb7
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/manage-dialog.component.ts
@@ -0,0 +1,34 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Dialog wrapper for ManageHoldsComponent.
+ */
+
+ at Component({
+  selector: 'eg-hold-manage-dialog',
+  templateUrl: 'manage-dialog.component.html'
+})
+
+export class HoldManageDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() holdIds: number[];
+
+    constructor(
+        private modal: NgbModal) { // required for passing to parent
+        super(modal); // required for subclassing
+    }
+
+    open(args: NgbModalOptions): Promise<boolean> {
+        return super.open(args);
+    }
+
+    onComplete(changesMade: boolean) {
+        this.close(changesMade);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/manage.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/manage.component.html
new file mode 100644
index 0000000000..fd9896e2d6
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/manage.component.html
@@ -0,0 +1,270 @@
+
+<form #holdManageForm role="form" *ngIf="hold"
+  class="form-validated common-form striped-odd">
+
+  <div class="form-group row d-flex">
+    <div class="col-lg-2 d-flex">
+      <div class="" *ngIf="isBatch()">
+        <div class="form-check form-check-inline">
+          <input class="form-check-input" type="checkbox"
+            title="Activate Column Editing" i18n-title
+            name="active_pickup_lib" [(ngModel)]="activeFields.pickup_lib"/>
+        </div>
+      </div>
+      <div class="flex-1"><label i18n>Pickup Library:</label></div>
+    </div>
+    <div class="col-lg-4">
+      <!-- TODO: filter orgs as needed -->
+      <eg-org-select [initialOrgId]="hold.pickup_lib()"
+        [disabled]="isBatch() && !activeFields.pickup_lib"
+        (onChange)="pickupLibChanged($event)">
+      </eg-org-select>
+    </div>
+    <div class="col-lg-2 d-flex">
+      <div class="" *ngIf="isBatch()">
+        <div class="form-check form-check-inline">
+          <input class="form-check-input" type="checkbox"
+            title="Activate Column Editing" i18n-title
+            name="active_mint_condition" [(ngModel)]="activeFields.mint_condition"/>
+        </div>
+      </div>
+      <div class="flex-1">
+        <label i18n>Desired Item Condition:</label>
+      </div>
+    </div>
+    <div class="col-lg-4">
+      <div class="form-check form-check-inline">
+        <input class="form-check-input" type="checkbox" id="mint-condition"
+          name="mint" value="mint"
+          [disabled]="isBatch() && !activeFields.mint_condition"
+          [ngModel]="hold.mint_condition() == 't'"
+          (ngModelChange)="hold.mint_condition($event ? 't' : 'f')">
+        <label class="form-check-label" for="mint-condition">
+          Good Condition Only
+        </label>
+      </div>
+    </div>
+  </div>
+
+  <div class="form-group row">
+    <div class="col-lg-2 d-flex">
+      <div class="" *ngIf="isBatch()">
+        <div class="form-check form-check-inline">
+          <input class="form-check-input" type="checkbox"
+            title="Activate Column Editing" i18n-title
+            name="active_frozen" [(ngModel)]="activeFields.frozen"/>
+        </div>
+      </div>
+      <div class="flex-1">
+        <label for="frozen" i18n>Hold is Suspended:</label>
+      </div>
+    </div>
+    <div class="col-lg-4">
+      <div class="form-check form-check-inline">
+        <input class="form-check-input" type="checkbox"
+          id="frozen" name="frozen"
+          [disabled]="isBatch() && !activeFields.frozen"
+          [ngModel]="hold.frozen() == 't'"
+          (ngModelChange)="hold.frozen($event ? 't' : 'f')">
+      </div>
+    </div>
+    <div class="col-lg-2 d-flex">
+      <div class="" *ngIf="isBatch()">
+        <div class="form-check form-check-inline">
+          <input class="form-check-input" type="checkbox"
+            title="Activate Column Editing" i18n-title
+            name="active_cut_in_line" [(ngModel)]="activeFields.cut_in_line"/>
+        </div>
+      </div>
+      <div class="flex-1">
+        <label for="cut_in_line" i18n>Top of Queue:</label>
+      </div>
+    </div>
+    <div class="col-lg-4">
+      <div class="form-check form-check-inline">
+        <input class="form-check-input" type="checkbox"
+          id="cut_in_line" name="cut_in_line"
+          [disabled]="isBatch() && !activeFields.cut_in_line"
+          [ngModel]="hold.cut_in_line() == 't'"
+          (ngModelChange)="hold.cut_in_line($event ? 't' : 'f')">
+      </div>
+    </div>
+  </div>
+
+  <!-- wrap the date mod fields in a border to help
+      differentiate from other fields -->
+  <div class="w-100 border border-primary rounded">
+    <div class="form-group row">
+      <div class="col-lg-2 d-flex">
+        <div class="" *ngIf="isBatch()">
+          <div class="form-check form-check-inline">
+            <input class="form-check-input" type="checkbox"
+              title="Activate Column Editing" i18n-title
+              name="active_thaw_date" [(ngModel)]="activeFields.thaw_date"/>
+          </div>
+        </div>
+        <div class="flex-1"><label for="thaw_date" i18n>Activate Date:</label></div>
+      </div>
+      <div class="col-lg-4">
+        <eg-date-select
+          domId="thaw_date"
+          [disabled]="isBatch() && !activeFields.thaw_date"
+          (onChangeAsIso)="hold.thaw_date($event)"
+          [initialIso]="hold.thaw_date()">
+        </eg-date-select>
+      </div>
+      <div class="col-lg-2 d-flex">
+        <div class="" *ngIf="isBatch()">
+          <div class="form-check form-check-inline">
+            <input class="form-check-input" type="checkbox"
+              title="Activate Column Editing" i18n-title
+              name="active_request_time" [(ngModel)]="activeFields.request_time"/>
+          </div>
+        </div>
+        <div class="flex-1"><label for="request_time" i18n>Request Date:</label></div>
+      </div>
+      <div class="col-lg-4">
+        <eg-date-select
+          domId="request_time"
+          [disabled]="isBatch() && !activeFields.request_time"
+          (onChangeAsIso)="hold.request_time($event)"
+          [initialIso]="hold.request_time()">
+        </eg-date-select>
+      </div>
+    </div>
+
+    <div class="form-group row">
+      <div class="col-lg-2 d-flex">
+        <div class="" *ngIf="isBatch()">
+          <div class="form-check form-check-inline">
+            <input class="form-check-input" type="checkbox"
+              title="Activate Column Editing" i18n-title
+              name="active_expire_time" [(ngModel)]="activeFields.expire_time"/>
+          </div>
+        </div>
+        <div class="flex-1"><label for="expire_time" i18n>Expire Date:</label></div>
+      </div>
+      <div class="col-lg-4">
+        <eg-date-select
+          domId="expire_time"
+          [disabled]="isBatch() && !activeFields.expire_time"
+          (onChangeAsIso)="hold.expire_time($event)"
+          [initialIso]="hold.expire_time()">
+        </eg-date-select>
+      </div>
+      <div class="col-lg-2 d-flex">
+        <div class="" *ngIf="isBatch()">
+          <div class="form-check form-check-inline">
+            <input class="form-check-input" type="checkbox"
+              title="Activate Column Editing" i18n-title
+              name="active_shelf_expire_time" [(ngModel)]="activeFields.shelf_expire_time"/>
+          </div>
+        </div>
+        <div class="flex-1"><label for="shelf_expire_time" i18n>Shelf Expire Date:</label></div>
+      </div>
+      <div class="col-lg-4">
+        <eg-date-select
+          domId="shelf_expire_time"
+          [disabled]="isBatch() && !activeFields.shelf_expire_time"
+          (onChangeAsIso)="hold.shelf_expire_time($event)"
+          [initialIso]="hold.shelf_expire_time()">
+        </eg-date-select>
+      </div>
+    </div>
+  </div><!-- modify dates group border -->
+
+  <div class="form-group row">
+    <div class="col-lg-2 d-flex">
+      <div class="" *ngIf="isBatch()">
+        <div class="form-check form-check-inline">
+          <input class="form-check-input" type="checkbox"
+            title="Activate Column Editing" i18n-title
+            name="active_email_notify" [(ngModel)]="activeFields.email_notify"/>
+        </div>
+      </div>
+      <div class="flex-1"><label for="email" i18n>Send Emails:</label></div>
+    </div>
+    <div class="col-lg-4">
+      <div class="form-check form-check-inline">
+        <input class="form-check-input" type="checkbox" id="email"
+          name="email" [ngModel]="hold.email_notify() == 't'"
+          [disabled]="isBatch() && !activeFields.email_notify"
+          (ngModelChange)="hold.email_notify($event ? 't' : 'f')"/>
+      </div>
+    </div>
+    <div class="col-lg-2 d-flex">
+      <div class="" *ngIf="isBatch()">
+        <div class="form-check form-check-inline">
+          <input class="form-check-input" type="checkbox"
+            title="Activate Column Editing" i18n-title
+            name="active_phone_notify" [(ngModel)]="activeFields.phone_notify"/>
+        </div>
+      </div>
+      <div class="flex-1"><label for="phone" i18n>Phone Number:</label></div>
+    </div>
+    <div class="col-lg-4">
+      <input type="text" class="form-control" name="phone" id="phone"
+        placeholder="Phone Number..." i18n-placeholder
+          [disabled]="isBatch() && !activeFields.phone_notify"
+        [ngModel]="hold.phone_notify()"
+        (ngModelChange)="hold.phone_notify($event)"/>
+    </div>
+  </div>
+
+  <ng-container *ngIf="smsEnabled">
+    <div class="form-group row">
+      <div class="col-lg-2 d-flex">
+        <div class="" *ngIf="isBatch()">
+          <div class="form-check form-check-inline">
+            <input class="form-check-input" type="checkbox"
+              title="Activate Column Editing" i18n-title
+              name="active_sms_notify" [(ngModel)]="activeFields.sms_notify"/>
+          </div>
+        </div>
+        <div class="flex-1"><label for="sms_notify" i18n>Text/SMS Number:</label></div>
+      </div>
+      <div class="col-lg-4">
+        <input type="text" class="form-control" name="sms_notify" id="sms_notify"
+          placeholder="SMS Number..." i18n-placeholder
+          [disabled]="isBatch() && !activeFields.sms_notify"
+          [ngModel]="hold.sms_notify()"
+          (ngModelChange)="hold.sms_notify($event)"/>
+      </div>
+      <div class="col-lg-2 d-flex">
+        <div class="" *ngIf="isBatch()">
+          <div class="form-check form-check-inline">
+            <input class="form-check-input" type="checkbox"
+              title="Activate Column Editing" i18n-title
+              name="active_sms_carrier" [(ngModel)]="activeFields.sms_carrier"/>
+          </div>
+        </div>
+        <div class="flex-1">
+          <label for="sms_carrier" i18n>Text/SMS Number:</label>
+        </div>
+      </div>
+      <div class="col-lg-4">
+        <eg-combobox
+          id="sms_carrier"
+          [disabled]="isBatch() && !activeFields.sms_carrier"
+          (onChange)="hold.sms_carrier($event.id)"
+          [startId]="hold.sms_carrier()"
+          [entries]="smsCarriers"
+          placeholder="SMS Carrier..." i18n-placeholder>
+        </eg-combobox>
+      </div>
+    </div>
+  </ng-container>
+
+
+  <div class="row d-flex justify-content-end">
+    <div>
+      <button type="button" class="btn btn-warning" (click)="exit()" i18n>
+        Cancel
+      </button>
+      <button type="button" class="btn btn-success ml-2" (click)="save()" i18n>
+        Apply
+      </button>
+    </div>
+  </div>
+</form>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/manage.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/manage.component.ts
new file mode 100644
index 0000000000..f21e64946d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/manage.component.ts
@@ -0,0 +1,144 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {HoldsService} from './holds.service';
+
+/** Edit holds in single or batch mode. */
+
+ at Component({
+  selector: 'eg-hold-manage',
+  templateUrl: 'manage.component.html'
+})
+export class HoldManageComponent implements OnInit {
+
+    // One holds ID means standard edit mode.
+    // >1 hold IDs means batch edit mode.
+    @Input() holdIds: number[];
+
+    hold: IdlObject;
+    smsEnabled: boolean;
+    smsCarriers: ComboboxEntry[];
+    activeFields: {[key: string]: boolean};
+
+    // Emits true if changes were applied to the hold.
+    @Output() onComplete: EventEmitter<boolean>;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        private holds: HoldsService
+    ) {
+        this.onComplete = new EventEmitter<boolean>();
+        this.smsCarriers = [];
+        this.holdIds = [];
+        this.activeFields = {};
+    }
+
+    ngOnInit() {
+        this.org.settings('sms.enable').then(sets => {
+            this.smsEnabled = sets['sms.enable'];
+            if (!this.smsEnabled) { return; }
+
+            this.pcrud.search('csc', {active: 't'}, {order_by: {csc: 'name'}})
+            .subscribe(carrier => {
+                this.smsCarriers.push({
+                    id: carrier.id(),
+                    label: carrier.name()
+                });
+            });
+        });
+
+        this.fetchHold();
+    }
+
+    fetchHold() {
+        this.hold = null;
+
+        if (this.holdIds.length === 0) {
+            return;
+
+        } else if (this.isBatch()) {
+            // Use a dummy hold to store form values.
+            this.hold = this.idl.create('ahr');
+
+        } else {
+            // Form values are stored in the one hold we're editing.
+            this.pcrud.retrieve('ahr', this.holdIds[0])
+            .subscribe(hold => this.hold = hold);
+        }
+    }
+
+    toFormData() {
+
+    }
+
+    isBatch(): boolean {
+        return this.holdIds.length > 1;
+    }
+
+    pickupLibChanged(org: IdlObject) {
+        if (org) {
+            this.hold.pickup_lib(org.id());
+        }
+    }
+
+    save() {
+        if (this.isBatch()) {
+
+            // Fields with edit-active checkboxes
+            const fields = Object.keys(this.activeFields)
+                .filter(field => this.activeFields[field]);
+
+            const holds: IdlObject[] = [];
+            this.pcrud.search('ahr', {id: this.holdIds})
+            .subscribe(
+                hold => {
+                    // Copy form fields to each hold to update.
+                    fields.forEach(field => hold[field](this.hold[field]()));
+                    holds.push(hold);
+                },
+                err => {},
+                ()  => {
+                    this.saveBatch(holds);
+                }
+            );
+        } else {
+            this.saveBatch([this.hold]);
+        }
+    }
+
+    saveBatch(holds: IdlObject[]) {
+        let successCount = 0;
+        this.holds.updateHolds(holds)
+        .subscribe(
+            res  => {
+                if (Number(res) > 0) {
+                    successCount++;
+                    console.debug('hold update succeeded with ', res);
+                } else {
+                    // TODO: toast?
+                }
+            },
+            err => console.error('hold update failed with ', err),
+            ()  => {
+                if (successCount === holds.length) {
+                    this.onComplete.emit(true);
+                } else {
+                    // TODO: toast?
+                    console.error('Some holds failed to update');
+                }
+            }
+        );
+    }
+
+    exit() {
+        this.onComplete.emit(false);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/retarget-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/retarget-dialog.component.html
new file mode 100644
index 0000000000..37d349dd80
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/retarget-dialog.component.html
@@ -0,0 +1,41 @@
+<eg-string #successMsg
+    text="Successfully Retargetd Hold" i18n-text></eg-string>
+<eg-string #errorMsg
+    text="Failed To Retarget Hold" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+    <div class="modal-header bg-info">
+      <h4 class="modal-title">
+        <span i18n>Retarget Hold</span>
+      </h4>
+      <button type="button" class="close"
+        i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+        <span aria-hidden="true">×</span>
+      </button>
+    </div>
+    <div class="modal-body">
+      <div class="row d-flex justify-content-center">
+          <h5>Retarget {{holdIds.length}} Holds?</h5>
+      </div>
+      <div class="row" *ngIf="numSucceeded > 0">
+        <div class="col-lg-12" i18n>
+          {{numSucceeded}} Hold(s) Successfully Retargeted
+        </div>
+      </div>
+      <div class="row" *ngIf="numFailed > 0">
+        <div class="col-lg-12">
+          <div class="alert alert-warning">
+            {{numFailed}} Hold(s) Failed to Retarget.
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <ng-container *ngIf="!chargeResponse">
+        <button type="button" class="btn btn-warning"
+          (click)="dismiss('canceled')" i18n>Cancel</button>
+        <button type="button" class="btn btn-success"
+          (click)="retargetBatch()" i18n>Retarget</button>
+      </ng-container>
+    </div>
+  </ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/retarget-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/retarget-dialog.component.ts
new file mode 100644
index 0000000000..feca64d92a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/retarget-dialog.component.ts
@@ -0,0 +1,80 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+
+
+/**
+ * Dialog for retargeting holds.
+ */
+
+ at Component({
+  selector: 'eg-hold-retarget-dialog',
+  templateUrl: 'retarget-dialog.component.html'
+})
+
+export class HoldRetargetDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() holdIds: number | number[];
+    @ViewChild('successMsg') private successMsg: StringComponent;
+    @ViewChild('errorMsg') private errorMsg: StringComponent;
+
+    changesApplied: boolean;
+    numSucceeded: number;
+    numFailed: number;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {}
+
+    open(args: NgbModalOptions): Promise<boolean> {
+        this.holdIds = [].concat(this.holdIds); // array-ify ints
+        return super.open(args);
+    }
+
+    async retargetNext(ids: number[]): Promise<any> {
+        if (ids.length === 0) {
+            return Promise.resolve();
+        }
+
+        return this.net.request(
+            'open-ils.circ', 'open-ils.circ.hold.reset',
+            this.auth.token(), ids.pop()
+        ).toPromise().then(
+            async(result) => {
+                if (Number(result) === 1) {
+                    this.numSucceeded++;
+                    this.toast.success(await this.successMsg.current());
+                } else {
+                    this.numFailed++;
+                    console.error(this.evt.parse(result));
+                    this.toast.warning(await this.errorMsg.current());
+                }
+                this.retargetNext(ids);
+            }
+        );
+    }
+
+    async retargetBatch(): Promise<any> {
+        this.numSucceeded = 0;
+        this.numFailed = 0;
+        const ids = [].concat(this.holdIds);
+        await this.retargetNext(ids);
+        this.close(this.numSucceeded > 0);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/transfer-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/transfer-dialog.component.html
new file mode 100644
index 0000000000..80728caf8b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/transfer-dialog.component.html
@@ -0,0 +1,43 @@
+<eg-string #successMsg
+    text="Successfully Transfered Hold" i18n-text></eg-string>
+<eg-string #errorMsg
+    text="Failed To Transfer Hold" i18n-text></eg-string>
+<eg-string #targetNeeded
+    text="Transfer Target Required" i18n-text> </eg-string>
+
+<ng-template #dialogContent>
+    <div class="modal-header bg-info">
+      <h4 class="modal-title">
+        <span i18n>Transfer Hold(s) To Marked Target</span>
+      </h4>
+      <button type="button" class="close"
+        i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+        <span aria-hidden="true">×</span>
+      </button>
+    </div>
+    <div class="modal-body">
+      <div class="row d-flex justify-content-center">
+          <h5>Transfer {{holdIds.length}} Holds To Record {{transferTarget}}?</h5>
+      </div>
+      <div class="row" *ngIf="numSucceeded > 0">
+        <div class="col-lg-12" i18n>
+          {{numSucceeded}} Hold(s) Successfully Transferred.
+        </div>
+        <div class="row" *ngIf="numFailed > 0">
+          <div class="col-lg-12">
+            <div class="alert alert-warning">
+              {{numFailed}} Hold(s) Failed to Transfer.
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <ng-container *ngIf="!chargeResponse">
+        <button type="button" class="btn btn-warning"
+          (click)="dismiss('canceled')" i18n>Cancel</button>
+        <button type="button" class="btn btn-success"
+          (click)="transferBatch()" i18n>Transfer</button>
+      </ng-container>
+    </div>
+  </ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/transfer-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/transfer-dialog.component.ts
new file mode 100644
index 0000000000..5ce38ea02e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holds/transfer-dialog.component.ts
@@ -0,0 +1,87 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {StoreService} from '@eg/core/store.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+
+
+/**
+ * Dialog for transferring holds.
+ */
+
+ at Component({
+  selector: 'eg-hold-transfer-dialog',
+  templateUrl: 'transfer-dialog.component.html'
+})
+
+export class HoldTransferDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() holdIds: number | number[];
+
+    @ViewChild('successMsg') private successMsg: StringComponent;
+    @ViewChild('errorMsg') private errorMsg: StringComponent;
+    @ViewChild('targetNeeded') private targetNeeded: StringComponent;
+
+    transferTarget: number;
+    changesApplied: boolean;
+    numSucceeded: number;
+    numFailed: number;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private store: StoreService,
+        private net: NetService,
+        private evt: EventService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {}
+
+    async open(args: NgbModalOptions): Promise<boolean> {
+        this.holdIds = [].concat(this.holdIds); // array-ify ints
+
+        this.transferTarget =
+            this.store.getLocalItem('eg.circ.hold.title_transfer_target');
+
+        if (!this.transferTarget) {
+            this.toast.warning(await this.targetNeeded.current());
+            return Promise.reject('Transfer Target Required');
+        }
+
+        return super.open(args);
+    }
+
+    async transferHolds(): Promise<any> {
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.hold.change_title.specific_holds',
+            this.auth.token(), this.transferTarget, this.holdIds
+        ).toPromise().then(async(result) => {
+            if (Number(result) === 1) {
+                this.numSucceeded++;
+                this.toast.success(await this.successMsg.current());
+            } else {
+                this.numFailed++;
+                console.error('Retarget Failed', this.evt.parse(result));
+                this.toast.warning(await this.errorMsg.current());
+            }
+        });
+    }
+
+    async transferBatch(): Promise<any> {
+        this.numSucceeded = 0;
+        this.numFailed = 0;
+        await this.transferHolds();
+        this.close(this.numSucceeded > 0);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/styles.css b/Open-ILS/src/eg2/src/styles.css
index cf10855253..4ca3abab7d 100644
--- a/Open-ILS/src/eg2/src/styles.css
+++ b/Open-ILS/src/eg2/src/styles.css
@@ -48,6 +48,33 @@ h5 {font-size: .95rem}
 .flex-4 {flex: 4}
 .flex-5 {flex: 5}
 
+/** BS deprecated the well, but it's replacement is not quite the same.
+ * Define our own version and expand it to a full "table".
+ * */
+.well-row {
+  display: flex;
+}
+.well-table .well-label {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  margin: 4px;
+  padding: 4px;
+  min-height: 40px;
+}
+
+.well-table .well-value {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  background-color: #f5f5f5;
+  border-radius: 5px;
+  box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
+  padding: 4px;
+  margin: 4px;
+  min-height: 40px;
+}
+
 
 /* usefuf for mat-icon buttons without any background or borders */
 .material-icon-button {

commit 192d24f83120e2495ff7e08ae2c617b7c71c490f
Author: Dan Scott <dan at coffeecode.net>
Date:   Thu Apr 11 23:17:10 2019 -0400

    Docs: fix uneven lengths of code delimiter blocks
    
    The lengths of the starting and ending sets of hyphens are supposed to be the
    same. Asciidoc tools like asciidoctor can be less forgiving than the original
    asciidoc processor, with less than hilarious results.
    
    It would probably make sense to try to standardize on something like 60 hyphens
    to easily flag this problem for deviations from the norm, but for now here's
    the result of running the docs through asciidoctor and manually adjusting the
    output.
    
    Signed-off-by: Dan Scott <dan at coffeecode.net>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/admin/booking-admin.adoc b/docs/admin/booking-admin.adoc
index 6dd77844e9..d7c90639b2 100644
--- a/docs/admin/booking-admin.adoc
+++ b/docs/admin/booking-admin.adoc
@@ -48,7 +48,7 @@ image::media/booking-create-bookable-1.png[]
 ** sec(s), min(s)
 ** s, m, h
 ** 00:00:30, 00:01:00, 01:00:00
-===================================================================
+====================================================================
 
 * Fine Amount - The amount that will be charged at each Fine Interval.
 * Owning Library - The home library of the resource.
diff --git a/docs/cataloging/batch_importing_MARC.adoc b/docs/cataloging/batch_importing_MARC.adoc
index d142b66b85..355d318560 100644
--- a/docs/cataloging/batch_importing_MARC.adoc
+++ b/docs/cataloging/batch_importing_MARC.adoc
@@ -270,7 +270,7 @@ If you are overlaying existing copies which already have stat cats
 attached to them, the overlay process will keep those values unless the
 incoming copies contain updated values for matching categories.
 |Status ||
-|==================
+|=============================
 
 
 Import Records
diff --git a/docs/circulation/basic_holds.adoc b/docs/circulation/basic_holds.adoc
index 8e1e48015a..a9098bebad 100644
--- a/docs/circulation/basic_holds.adoc
+++ b/docs/circulation/basic_holds.adoc
@@ -15,7 +15,7 @@ Evergreen has different levels of holds. Library staff can place holds at all le
 |Parts        |P              |Patron wants a particular part of title (e.g. volume or disk number)    | Staff or patron selects part on the create/edit hold screen when setting holds notification options.   |Patron or staff  |Holdings with identical parts attached to a single MARC (title) record.
 |Volume       |V              |Patron or staff want any title associated with a particular call number | In the staff client, click on _Volume Hold_ under _Holdable?_ |Staff only |Holdings attached to a single call number (volume)
 |Copy         |C              |Patron or staff want a specific copy of an item |In the staff client, click on _Copy Hold_ under _Holdable?_ |Staff only |A specific copy (barcode)
-|===============================
+|==============================
 
 
 Title Level Hold
diff --git a/docs/circulation/circulating_items_web_client.adoc b/docs/circulation/circulating_items_web_client.adoc
index 65ee0c9408..c559857ca0 100644
--- a/docs/circulation/circulating_items_web_client.adoc
+++ b/docs/circulation/circulating_items_web_client.adoc
@@ -182,7 +182,7 @@ These options may be selected simultaneously. The selected option is displayed i
 
 image::media/checkin_options_web_client.png[]
 
-====================================================
+===================================================
   
 Renewal and Editing the Item's Due Date
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -256,7 +256,7 @@ Lost Item Billing
 - Marking an item Lost will automatically bill the patron the replacement cost of the item as recorded in the price field in the item record, and a processing fee as determined by your local policy. If the lost item has overdue charges, the overdue charges may be voided or retained based on local policy.
 - A lost-then-returned item will disappear from the Items Out screen only when all bills linked to this particular circulation have been resolved. Bills may include replacement charges, processing fees, and manual charges added to the existing bills. 
 - The replacement fee and processing fee for lost-then-returned items may be voided if set by local policy. Overdue fines may be reinstated on lost-then-returned items if set by local policy.
-==========================
+========================
 
 Refunds for Lost Items
 ^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/docs/installation/server_upgrade.adoc b/docs/installation/server_upgrade.adoc
index 601481c813..9554a9984c 100644
--- a/docs/installation/server_upgrade.adoc
+++ b/docs/installation/server_upgrade.adoc
@@ -117,21 +117,21 @@ chown -R opensrf:opensrf /openils
   As the *opensrf* user, update the server symlink in /openils/var/web/xul/:
 +
 [source, bash]
------------------------------------------------------------
+------------------------------------------------------------
 cd /openils/var/web/xul/
 rm server
 ln -sf rel_2_12_rc/server server
-----------------------------------------------------------
+------------------------------------------------------------
 +
 . As the *opensrf* user, update opensrf_core.xml and opensrf.xml by copying the
   new example files (/openils/conf/opensrf_core.xml.example and
   /openils/conf/opensrf.xml). The _-b_ option creates a backup copy of the old file.
 +
 [source, bash]
-----------------------------------------------------------
+------------------------------------------------------------
 cp -b /openils/conf/opensrf_core.xml.example /openils/conf/opensrf_core.xml
 cp -b /openils/conf/opensrf.xml.example /openils/conf/opensrf.xml
-----------------------------------------------------------
+------------------------------------------------------------
 +
 [CAUTION]
 Copying these configuration files will remove any customizations you have made to them. Remember to redo your customizations after copying them.
@@ -322,7 +322,7 @@ srfsh% login username password
 You should see a result like:
 +
 [source, bash]
-------------------------------------------------------
+--------------------------------------------------------------
 Received Data: "250bf1518c7527a03249858687714376"
     ------------------------------------
     Request Completed Successfully
@@ -346,7 +346,7 @@ Received Data: "250bf1518c7527a03249858687714376"
     Request Completed Successfully
     Request Time in seconds: 1.336568
     ------------------------------------
-----------------------------------------------------------
+--------------------------------------------------------------
 +
 If this does not work, it's time to do some <<install-troubleshooting-1,troubleshooting>>.
 +
diff --git a/docs/reports/reporter_add_data_source.adoc b/docs/reports/reporter_add_data_source.adoc
index ea99f646d4..10c3835943 100644
--- a/docs/reports/reporter_add_data_source.adoc
+++ b/docs/reports/reporter_add_data_source.adoc
@@ -79,7 +79,7 @@ Here's an example of a view created to incorporate some locally defined user
 statistical categories:
 
 .example view for reports
-----------
+------------------------------------------------------------
 create view extend_reporter.patronstats as
 select u.id, 
 grp.name as "ptype",
@@ -108,7 +108,7 @@ left join actor.stat_cat_entry_usr_map gr
 left join actor.stat_cat_entry_usr_map ag 
     on (u.id = ag.target_usr and ag.stat_cat = 1) 
 where u.active = 't' and u.deleted <> 't';
------------
+------------------------------------------------------------
 
 Add a new class to fm_IDL.xml for your data source
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -161,7 +161,7 @@ The following example is a class definition for the example view that was create
 in the previous section.
 
 .example class definition for reports
-----------
+------------------------------------------------------------
 <class id="erpstats" controller="open-ils.reporter-store" 
 oils_obj:fieldmapper="extend_reporter::patronstats" 
 oils_persist:tablename="extend_reporter.patronstats" oils_persist:readonly="true" 
@@ -191,7 +191,7 @@ reporter:label="Patron Statistics" reporter:core="true">
   <link field="home_lib_id" reltype="has_a" key="id" map="" class="aou"/>
 </links>
 </class>
----------
+------------------------------------------------------------
 
 NOTE: _fm_IDL.xml_ is used by other core Evergreen DAO services, including cstore 
 and permacrud. So changes to this file can affect the entire Evergreen 
diff --git a/docs/reports/reporter_daemon.adoc b/docs/reports/reporter_daemon.adoc
index 4496718c8d..0c4443e181 100644
--- a/docs/reports/reporter_daemon.adoc
+++ b/docs/reports/reporter_daemon.adoc
@@ -42,7 +42,7 @@ the reporter daemon can be started.
 Remember that if the server is restarted, the reporter daemon will need to be 
 restarted before you can view reports unless you have configured your server to 
 start the daemon automatically at start up time. 
-==============
+=============
 
 Stopping the Reporter Daemon
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

Summary of changes:
 Open-ILS/src/eg2/src/app/common.module.ts          |   8 +-
 Open-ILS/src/eg2/src/app/core/format.service.ts    |  13 +-
 .../share/date-select/date-select.component.html   |   8 +-
 .../eg2/src/app/share/dialog/dialog.component.ts   |   2 +-
 .../share/grid/grid-toolbar-action.component.ts    |  18 +-
 .../share/grid/grid-toolbar-button.component.ts    |  10 +-
 .../share/grid/grid-toolbar-checkbox.component.ts  |  13 +-
 .../src/app/share/grid/grid-toolbar.component.html |  64 ++--
 .../src/app/share/grid/grid-toolbar.component.ts   |  54 ++-
 Open-ILS/src/eg2/src/app/share/grid/grid.ts        |  43 ++-
 .../app/share/org-select/org-select.component.html |   1 +
 .../app/share/org-select/org-select.component.ts   |   7 +-
 .../eg2/src/app/share/string/string.component.ts   |  15 +-
 .../staff/cat/vandelay/queue-items.component.html  |   4 +-
 .../staff/cat/vandelay/queue-items.component.ts    |  10 +-
 .../app/staff/cat/vandelay/queue.component.html    |  27 +-
 .../src/app/staff/cat/vandelay/queue.component.ts  |  31 +-
 .../eg2/src/app/staff/catalog/catalog.module.ts    |  13 +-
 .../src/app/staff/catalog/hold/hold.component.html |  12 +-
 .../src/app/staff/catalog/hold/hold.component.ts   |   6 +-
 .../staff/catalog/record/actions.component.html    |   4 +
 .../app/staff/catalog/record/actions.component.ts  |   2 +-
 .../app/staff/catalog/record/record.component.html |  14 +-
 .../app/staff/catalog/record/record.component.ts   |   7 +
 .../src/app/staff/catalog/search-form.component.ts |   8 +-
 .../app/staff/share/holdings/holdings.module.ts    |  25 ++
 .../staff/share/{ => holdings}/holdings.service.ts |   4 +
 .../holdings/mark-damaged-dialog.component.html    | 108 ++++++
 .../holdings/mark-damaged-dialog.component.ts      | 154 ++++++++
 .../holdings/mark-missing-dialog.component.html    |  44 +++
 .../holdings/mark-missing-dialog.component.ts      |  79 ++++
 .../staff/share/holds/cancel-dialog.component.html |  60 ++++
 .../staff/share/holds/cancel-dialog.component.ts   |  98 +++++
 .../app/staff/share/holds/detail.component.html    |  99 +++++
 .../src/app/staff/share/holds/detail.component.ts  |  67 ++++
 .../src/app/staff/share/holds/grid.component.html  | 249 +++++++++++++
 .../src/app/staff/share/holds/grid.component.ts    | 399 +++++++++++++++++++++
 .../eg2/src/app/staff/share/holds/holds.module.ts  |  41 +++
 .../{hold.service.ts => holds/holds.service.ts}    |  35 +-
 .../staff/share/holds/manage-dialog.component.html |  18 +
 .../staff/share/holds/manage-dialog.component.ts   |  34 ++
 .../app/staff/share/holds/manage.component.html    | 270 ++++++++++++++
 .../src/app/staff/share/holds/manage.component.ts  | 144 ++++++++
 .../share/holds/retarget-dialog.component.html     |  41 +++
 .../staff/share/holds/retarget-dialog.component.ts |  80 +++++
 .../share/holds/transfer-dialog.component.html     |  43 +++
 .../staff/share/holds/transfer-dialog.component.ts |  87 +++++
 Open-ILS/src/eg2/src/styles.css                    |  27 ++
 Open-ILS/src/sql/Pg/002.schema.config.sql          |   2 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  11 +-
 .../upgrade/1160.data.catalog-holds-prefetch.sql   |  15 +
 .../Client/ang-staff-catalog-record-holds.adoc     |  15 +
 docs/admin/booking-admin.adoc                      |   2 +-
 docs/cataloging/batch_importing_MARC.adoc          |   2 +-
 docs/circulation/basic_holds.adoc                  |   2 +-
 docs/circulation/circulating_items_web_client.adoc |   4 +-
 docs/installation/server_upgrade.adoc              |  12 +-
 docs/reports/reporter_add_data_source.adoc         |   8 +-
 docs/reports/reporter_daemon.adoc                  |   2 +-
 59 files changed, 2524 insertions(+), 151 deletions(-)
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts
 rename Open-ILS/src/eg2/src/app/staff/share/{ => holdings}/holdings.service.ts (89%)
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holdings/mark-damaged-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holdings/mark-damaged-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/cancel-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/cancel-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/holds.module.ts
 rename Open-ILS/src/eg2/src/app/staff/share/{hold.service.ts => holds/holds.service.ts} (83%)
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/manage-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/manage-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/manage.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/manage.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/retarget-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/retarget-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/transfer-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holds/transfer-dialog.component.ts
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1160.data.catalog-holds-prefetch.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Client/ang-staff-catalog-record-holds.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list