moving more code over from list, etc. to grid
authorBill Erickson <berick@esilibrary.com>
Mon, 28 Apr 2014 19:34:34 +0000 (15:34 -0400)
committerBill Erickson <berick@esilibrary.com>
Mon, 28 Apr 2014 19:34:34 +0000 (15:34 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
23 files changed:
Open-ILS/src/templates/staff/cat/bucket/record/index.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_bucket_create.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_bucket_edit.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_bucket_export.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_bucket_info.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_load_shared.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_pending.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_search.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2
Open-ILS/src/templates/staff/circ/checkin/index.tt2
Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2
Open-ILS/src/templates/staff/circ/checkin/t_hold_shelf_dialog.tt2
Open-ILS/src/templates/staff/circ/checkin/t_transit_dialog.tt2
Open-ILS/src/templates/staff/circ/patron/t_noncat_dialog.tt2
Open-ILS/src/templates/staff/css/style.css.tt2
Open-ILS/src/templates/staff/parts/t_autogrid.tt2
Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
Open-ILS/web/js/ui/default/staff/circ/patron/holds.js
Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js
Open-ILS/web/js/ui/default/staff/services/grid.js

index 27f4c80..4ebfb1e 100644 (file)
@@ -7,6 +7,7 @@
 
 [% BLOCK APP_JS %]
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/list.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/bucket/record/app.js"></script>
 [% END %]
@@ -23,19 +24,19 @@ changing routes with each tab selection anyway.
   <li ng-class="{active : tab == 'search'}">
     <a href="./cat/bucket/record/search/{{bucketSvc.currentBucket.id()}}">
         [% l('Record Query') %]
-        <span ng-cloak>({{bucketSvc.searchList.totalCount}})</span>
+        <span ng-cloak>({{bucketSvc.queryRecords.length}})</span>
     </a>
   </li>
   <li ng-class="{active : tab == 'pending'}">
     <a href="./cat/bucket/record/pending/{{bucketSvc.currentBucket.id()}}">
         [% l('Pending Records') %]
-        <span ng-cloak>({{bucketSvc.pendingList.count()}})</span>
+        <span ng-cloak>({{bucketSvc.pendingList.length}})</span>
     </a>
   </li>
   <li ng-class="{active : tab == 'view'}">
     <a href="./cat/bucket/record/view/{{bucketSvc.currentBucket.id()}}">
         [% l('Bucket View') %]
-        <span ng-cloak>({{bucketSvc.viewList.totalCount}})</span>
+        <span ng-cloak>({{bucketSvc.currentBucket.items().length}})</span>
     </a>
   </li>
 </ul>
index 517c35f..e6bb3fe 100644 (file)
@@ -2,36 +2,34 @@
 
 <!-- use <form> so we get submit-on-enter for free -->
 <form class="form-validated" novalidate name="form" ng-submit="ok(args)">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <button type="button" class="close" 
-          ng-click="cancel()" aria-hidden="true">&times;</button>
-        <h4 class="modal-title">[% l('Create Bucket') %]</h4>
+  <div>
+    <div class="modal-header">
+      <button type="button" class="close" 
+        ng-click="cancel()" aria-hidden="true">&times;</button>
+      <h4 class="modal-title">[% l('Create Bucket') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="form-group">
+        <label for="edit-bucket-name">[% l('Name') %]</label>
+        <input type="text" class="form-control" focus-me='focusMe' required
+          id="edit-bucket-name" ng-model="args.name" placeholder="[% l('Name...') %]"/>
       </div>
-      <div class="modal-body">
-        <div class="form-group">
-          <label for="edit-bucket-name">[% l('Name') %]</label>
-          <input type="text" class="form-control" focus-me='focusMe' required
-            id="edit-bucket-name" ng-model="args.name" placeholder="[% l('Name...') %]"/>
-        </div>
-        <div class="form-group">
-          <label for="edit-bucket-desc">[% l('Description') %]</label>
-          <input type="text" class="form-control" id="edit-bucket-desc"
-            ng-model="args.desc" placeholder="[% l('Description...') %]"/>
-        </div>
-         <div class="checkbox">
-          <label>
-            <input ng-model="args.pub" type="checkbox"/> 
-            [% l('Publicly Visible?') %]
-          </label>
-        </div>
+      <div class="form-group">
+        <label for="edit-bucket-desc">[% l('Description') %]</label>
+        <input type="text" class="form-control" id="edit-bucket-desc"
+          ng-model="args.desc" placeholder="[% l('Description...') %]"/>
       </div>
-      <div class="modal-footer">
-        <input type="submit" ng-disabled="form.$invalid" 
-            class="btn btn-primary" value="[% l('Create Bucket') %]"/>
-        <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+       <div class="checkbox">
+        <label>
+          <input ng-model="args.pub" type="checkbox"/> 
+          [% l('Publicly Visible?') %]
+        </label>
       </div>
-    </div> <!-- modal-content -->
-  </div> <!-- modal-dialog -->
+    </div>
+    <div class="modal-footer">
+      <input type="submit" ng-disabled="form.$invalid" 
+          class="btn btn-primary" value="[% l('Create Bucket') %]"/>
+      <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
 </form>
index f0acbf4..288c577 100644 (file)
@@ -1,36 +1,34 @@
 <!-- edit bucket dialog -->
 <form class="form-validated" novalidate ng-submit="ok(args)" name="form">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <button type="button" class="close" 
-          ng-click="cancel()" aria-hidden="true">&times;</button>
-        <h4 class="modal-title">[% l('Edit Bucket') %]</h4>
+  <div>
+    <div class="modal-header">
+      <button type="button" class="close" 
+        ng-click="cancel()" aria-hidden="true">&times;</button>
+      <h4 class="modal-title">[% l('Edit Bucket') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="form-group">
+        <label for="edit-bucket-name">[% l('Name') %]</label>
+        <input type="text" class="form-control" focus-me='focusMe' required
+          id="edit-bucket-name" ng-model="args.name" placeholder="[% l('Name...') %]"/>
       </div>
-      <div class="modal-body">
-        <div class="form-group">
-          <label for="edit-bucket-name">[% l('Name') %]</label>
-          <input type="text" class="form-control" focus-me='focusMe' required
-            id="edit-bucket-name" ng-model="args.name" placeholder="[% l('Name...') %]"/>
-        </div>
-        <div class="form-group">
-          <label for="edit-bucket-desc">[% l('Description') %]</label>
-          <input type="text" class="form-control" id="edit-bucket-desc"
-            ng-model="args.desc" placeholder="[% l('Description...') %]"/>
-        </div>
-         <div class="checkbox">
-          <label>
-            <input ng-model="args.pub" type="checkbox"> 
-            [% l('Publicly Visible?') %]
-          </label>
-        </div>
+      <div class="form-group">
+        <label for="edit-bucket-desc">[% l('Description') %]</label>
+        <input type="text" class="form-control" id="edit-bucket-desc"
+          ng-model="args.desc" placeholder="[% l('Description...') %]"/>
       </div>
-      <div class="modal-footer">
-        <input type="submit" class="btn btn-primary" 
-            ng-disabled="form.$invalid" value="[% l('Apply Changes') %]"/>
-        <button class="btn btn-warning" ng-click="cancel()"
-            ng-class="{disabled : actionPending}">[% l('Cancel') %]</button>
+       <div class="checkbox">
+        <label>
+          <input ng-model="args.pub" type="checkbox"> 
+          [% l('Publicly Visible?') %]
+        </label>
       </div>
-    </div> <!-- modal-content -->
-  </div> <!-- modal-dialog -->
+    </div>
+    <div class="modal-footer">
+      <input type="submit" class="btn btn-primary" 
+          ng-disabled="form.$invalid" value="[% l('Apply Changes') %]"/>
+      <button class="btn btn-warning" ng-click="cancel()"
+          ng-class="{disabled : actionPending}">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
 </form>
index 9a36d09..ffc26d0 100644 (file)
@@ -1,43 +1,41 @@
 <!-- export bucket dialog -->
 <form ng-submit="ok(args)">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <button type="button" class="close" 
-          ng-click="cancel()" aria-hidden="true">&times;</button>
-        <h4 class="modal-title">[% l('Export Records') %]</h4>
+  <div>
+    <div class="modal-header">
+      <button type="button" class="close" 
+        ng-click="cancel()" aria-hidden="true">&times;</button>
+      <h4 class="modal-title">[% l('Export Records') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="form-group">
+        <label for="export-bucket-format">[% l('Record Format') %]</label>
+        <select class="form-control" ng-model="args.format" id="export-bucket-format">
+          <option value="XML">[% l('MARC XML') %]</option>
+          <option value="USMARC">[% l('USMARC') %]</option>
+          <option value="UNIMARC">[% l('UNIMARC') %]</option>
+          <option value="BRE">[% l('Evergreen Record Entry') %]</option>
+        </select>
       </div>
-      <div class="modal-body">
-        <div class="form-group">
-          <label for="export-bucket-format">[% l('Record Format') %]</label>
-          <select class="form-control" ng-model="args.format" id="export-bucket-format">
-            <option value="XML">[% l('MARC XML') %]</option>
-            <option value="USMARC">[% l('USMARC') %]</option>
-            <option value="UNIMARC">[% l('UNIMARC') %]</option>
-            <option value="BRE">[% l('Evergreen Record Entry') %]</option>
-          </select>
-        </div>
-        <div class="form-group">
-          <label for="export-bucket-encoding">[% l('Encoding') %]</label>
-          <select class="form-control" ng-model="args.encoding" id="export-bucket-encoding">
-            <option value="UTF-8">[% l('UTF-8') %]</option>
-            <option value="MARC8">[% l('MARC8') %]</option>
-          </select>
-        </div>
-
-         <div class="checkbox">
-          <label>
-            <input ng-model="args.holdings" type="checkbox"> 
-            [% l('Include Items?') %]
-          </label>
-        </div>
+      <div class="form-group">
+        <label for="export-bucket-encoding">[% l('Encoding') %]</label>
+        <select class="form-control" ng-model="args.encoding" id="export-bucket-encoding">
+          <option value="UTF-8">[% l('UTF-8') %]</option>
+          <option value="MARC8">[% l('MARC8') %]</option>
+        </select>
       </div>
-      <div class="modal-footer">
-        <input type="submit" class="btn btn-primary"
-            ng-click="ok(args)" value="[% l('Export') %]"/>
-        <button class="btn btn-warning" 
-            ng-click="cancel()">[% l('Cancel') %]</button>
+
+       <div class="checkbox">
+        <label>
+          <input ng-model="args.holdings" type="checkbox"> 
+          [% l('Include Items?') %]
+        </label>
       </div>
-    </div> <!-- modal-content -->
-  </div> <!-- modal-dialog -->
+    </div>
+    <div class="modal-footer">
+      <input type="submit" class="btn btn-primary"
+          ng-click="ok(args)" value="[% l('Export') %]"/>
+      <button class="btn btn-warning" 
+          ng-click="cancel()">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
 </form>
index 1adee8c..877fcf6 100644 (file)
@@ -1,8 +1,8 @@
 
 <div ng-show="bucket()">
-  <strong>[% l('Bucket {{bucket().name()}}') %]</strong> 
+  <strong>[% l('Bucket: {{bucket().name()}}') %]</strong> 
   <span>
-    <ng-pluralize count="bucketSvc.viewList.totalCount"
+    <ng-pluralize count="bucketSvc.currentBucket.items().length"
       when="{'one': '[% l("1 item") %]', 'other': '[% l("{} items") %]'}">
     </ng-pluralize>
   </span> 
index a547cd9..9aab308 100644 (file)
@@ -1,27 +1,25 @@
 <!-- load bucket by id ("shared") -->
 <form class="form-validated" novalidate name="form" ng-submit="ok(args)">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <button type="button" class="close" 
-          ng-click="cancel()" aria-hidden="true">&times;</button>
-        <h4 class="modal-title">[% l('Load Shared Bucket Bucket by ID') %]</h4>
+  <div>
+    <div class="modal-header">
+      <button type="button" class="close" 
+        ng-click="cancel()" aria-hidden="true">&times;</button>
+      <h4 class="modal-title">[% l('Load Shared Bucket Bucket by ID') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="form-group">
+        <label for="load-bucket-id">[% l('Bucket ID') %]</label>
+        <!-- NOTE: type='number' / required -->
+        <input type="number" class="form-control" focus-me='focusMe' required
+          id="load-bucket-id" ng-model="args.id" placeholder="[% l('Bucket ID...') %]"/>
       </div>
-      <div class="modal-body">
-        <div class="form-group">
-          <label for="load-bucket-id">[% l('Bucket ID') %]</label>
-          <!-- NOTE: type='number' / required -->
-          <input type="number" class="form-control" focus-me='focusMe' required
-            id="load-bucket-id" ng-model="args.id" placeholder="[% l('Bucket ID...') %]"/>
-        </div>
-      </div>
-      <div class="modal-footer">
-        <input type="submit" ng-disabled="form.$invalid" 
-            class="btn btn-primary" value="[% l('Load Bucket') %]"/>
-        <button class="btn btn-warning" 
-            ng-click="cancel()">[% l('Cancel') %]</button>
-      </div>
-    </div> <!-- modal-content -->
-  </div> <!-- modal-dialog -->
+    </div>
+    <div class="modal-footer">
+      <input type="submit" ng-disabled="form.$invalid" 
+          class="btn btn-primary" value="[% l('Load Bucket') %]"/>
+      <button class="btn btn-warning" 
+          ng-click="cancel()">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
 </form>
 
index 94a444d..64c0b03 100644 (file)
@@ -1,36 +1,47 @@
-<br/>
-
 <div class="row">
   <div class="col-md-6">
     [% INCLUDE 'staff/cat/bucket/record/t_bucket_info.tt2' %]
   </div>
+</div>
 
-  <div class="col-md-6 text-right">
-    [% INCLUDE 'staff/cat/bucket/record/t_bucket_selector.tt2' %]
-
-    <div class="btn-group text-left">
-      <!-- first page -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : pageList.onFirstPage()}" 
-        ng-click="pageList.offset = 0;draw()">[% l('Start') %]</button>
-
-      <!-- previous page -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : pageList.onFirstPage()}"
-        ng-click="pageList.decrementPage();draw()">&laquo;</button>
-
-      <!-- next page -->
-      <!-- todo: paging needs a total count value to be fully functional -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : !pageList.hasNextPage()}"
-        ng-click="pageList.incrementPage();draw()">&raquo;</button>
+<div class="col-md-10 col-md-offset-1" ng-show="forbidden">
+  <div class="alert alert-warning">
+    [% l('The selected bucket "{{bucketId}}" is not visible to this login.') %]
+  </div>
+</div>
 
-      <div class="btn-group">
-        <button type="button" class="btn btn-default dropdown-toggle" 
-            ng-class="{disabled : action_pending}" data-toggle="dropdown">
-          [% l('Actions') %] <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu pull-right">
+<eg-grid
+  ng-hide="forbidden"
+  id-field="id"
+  idl-class="rmsr"
+  auto-fields="true"
+  items-provider="gridDataProvider"
+  menu-label="[% l('Buckets') %]"
+  persist-key="eg.staff.cat.bucket.record.view">
+
+  <!-- global menu -->
+  <eg-grid-menu-item label="[% l('New Bucket') %]" 
+    handler="openCreateBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Edit Bucket') %]" 
+    handler="openEditBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Delete Bucket') %]" 
+    handler="openDeleteBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Shared Bucket') %]" 
+    handler="openSharedBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item divider="true"></eg-grid-menu-item>
+  <eg-grid-menu-item ng-repeat="bkt in bucketSvc.allBuckets" 
+    label="{{bkt.name()}}" handler-data="bkt" 
+    handler="loadBucketFromMenu"></eg-grid-menu-item>
+
+  <!-- actions drop-down -->
+  <eg-grid-action label="[% l('Add To Bucket') %]" 
+    handler="addToBucket"></eg-grid-action>
+  <eg-grid-action label="[% l('Clear List') %]" 
+    handler="clearPendingList"></eg-grid-action>
+
+</eg-grid>
+
+<!--
 
           <li ng-class="{disabled : !bucket() || !pageList.selectedItems().length}">
             <a href="javascript:;" ng-click="addToBucket()">[% l('Add Selected To Bucket') %]</a>
           <li ng-class="{disabled : !pageList.count()}">
             <a href="javascript:;" ng-click="pageList.reset()">[% l('Clear List') %]</a>
           </li>
-        </ul>
-      </div>
+-->
 
-      <div class="btn-group col-picker">
-        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
-            <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu pull-right">
-          <li ng-repeat="col in pageList.allColumns">
-            <a href='javascript:;' 
-              ng-click="pageList.displayColumns[col.name] = 
-                !pageList.displayColumns[col.name]">
-                <span ng-if="pageList.displayColumns[col.name]" class="label label-success">&#x2713;</span>
-                <span ng-if="!pageList.displayColumns[col.name]" class="label label-warning">&#x2717;</span>
-                <span>{{col.label}}</span>
-            </a>
-          </li>
-          <li role="presentation" class="divider"></li>
-          <li>
-            <a href='javascript:;' ng-click="pageList.showAllColumns()">[% l('Show All Columns') %]</a>
-          </li>
-          <li>
-            <a href='javascript:;' ng-click="pageList.hideAllColumns()">[% l('Hide All Columns') %]</a>
-          </li>
-          <li class='disabled'>
-            <a href='javascript:;' ng-click="">[% l('Save Columns') %]</a>
-          </li>
-        </ul>
-      </div>
-    </div>
-  </div>
-</div>
-
-<br/>
-
-<div class="row">
-  <div class="col-md-12">
-
-    <table class="list table table-hover _table-striped table-condensed"
-      ng-show="pageList.count()"
-      ng-init="pageList.defaultColumns([
-        'id', 'author', 'isbn', 'issn', 'pubdate', 
-        'publisher', 'tcn_value', 'title'
-      ])">
-      <thead>
-        <tr>
-          <th>#</th>
-          <th>
-            <a href='javascript:;' 
-              ng-click="pageList.toggleSelectAll()">&#x2713;</a>
-          </th>
-          <th ng-repeat="field in pageList.allColumns" 
-            ng-show="pageList.displayColumns[field.name]">
-            <a href='javascript:;' 
-              ng-click="sort(field.name);draw()">{{field.label}}</a>
-          </th>
-        </tr>
-      </thead>
-      <tbody>
-        <tr ng-repeat="rec in pageList.items"
-            ng-class="{selected : pageList.selected[rec.id]}"
-            ng-click="applyRowSelection($event, rec.id)">
-          <td>{{$index + 1 + pageList.offset}}</td>
-          <td><span ng-if="pageList.selected[rec.id]">&#x2713;</span>
-          </td>
-          <td ng-repeat="field in pageList.allColumns"
-            ng-show="pageList.displayColumns[field.name]">
-            <div ng-switch="field.name">
-              <div ng-switch-when="title">
-                <a target='_top' href='[% ctx.base_path %]/opac/record/{{rec.id}}'>
-                  {{rec[field.name]}}
-                </a>
-              </div>
-              <div ng-switch-default>
-                {{rec[field.name]}}
-              </div>
-            </div>
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
-</div>
index a72c9d3..d5ad337 100644 (file)
@@ -1,83 +1,12 @@
-
-<br/>
-
 <div class="row">
   <div class="col-md-6">
     [% INCLUDE 'staff/cat/bucket/record/t_bucket_info.tt2' %]
   </div>
+</div>
 
-  <div class="col-md-6 text-right">
-    [% INCLUDE 'staff/cat/bucket/record/t_bucket_selector.tt2' %]
-
-    <div class="btn-group text-left">
-      <!-- first page -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : pageList.onFirstPage()}" 
-        ng-click="pageList.offset = 0;draw()">[% l('Start') %]</button>
-
-      <!-- previous page -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : pageList.onFirstPage()}"
-        ng-click="pageList.decrementPage();draw()">&laquo;</button>
-
-      <!-- next page -->
-      <!-- todo: paging needs a total count value to be fully functional -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : !pageList.hasNextPage()}"
-        ng-click="pageList.incrementPage();draw()">&raquo;</button>
-
-      <div class="btn-group">
-        <button type="button" class="btn btn-default dropdown-toggle" 
-            ng-class="{disabled : action_pending}" data-toggle="dropdown">
-          [% l('Actions') %] <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu pull-right">
-
-          <li ng-class="{disabled : !pageList.selectedItems().length}">
-            <a href="javascript:;" ng-click="addToPending()">
-              [% l('Add Selected To Pending List') %]</a></li>
-
-          <li ng-class="{disabled : !pageList.count()}">
-            <a href="javascript:;" ng-click="addToPending(true)">
-              [% l('Add All To Pending List') %]</a></li>
-
-          <li ng-class="{disabled : !bucket() || !pageList.selectedItems().length}">
-            <a href="javascript:;" ng-click="addToBucket()">
-                [% l('Add Selected To Bucket') %]</a></li>
-
-          <li ng-class="{disabled : !bucket() || !pageList.count()}">
-            <a href="javascript:;" ng-click="addToBucket(true)">
-              [% l('Add All To Bucket') %]</a></li>
-        </ul>
-      </div>
-
-      <div class="btn-group col-picker">
-        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
-            <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu pull-right">
-          <li ng-repeat="col in pageList.allColumns">
-            <a href='javascript:;' 
-              ng-click="pageList.displayColumns[col.name] = 
-                !pageList.displayColumns[col.name]">
-                <span ng-if="pageList.displayColumns[col.name]" class="label label-success">&#x2713;</span>
-                <span ng-if="!pageList.displayColumns[col.name]" class="label label-warning">&#x2717;</span>
-                <span>{{col.label}}</span>
-            </a>
-          </li>
-          <li role="presentation" class="divider"></li>
-          <li>
-            <a href='javascript:;' ng-click="pageList.showAllColumns()">[% l('Show All Columns') %]</a>
-          </li>
-          <li>
-            <a href='javascript:;' ng-click="pageList.hideAllColumns()">[% l('Hide All Columns') %]</a>
-          </li>
-          <li class='disabled'>
-            <a href='javascript:;' ng-click="">[% l('Save Columns') %]</a>
-          </li>
-        </ul>
-      </div>
-    </div>
+<div class="col-md-10 col-md-offset-1" ng-show="forbidden">
+  <div class="alert alert-warning">
+    [% l('The selected bucket "{{bucketId}}" is not visible to this login.') %]
   </div>
 </div>
 
       <div class="input-group">
         <span class="input-group-addon">[% l('Record Query') %]</span>
         <input type="text" class="form-control" focus-me="focusMe"
-          ng-model="bucketSvc.queryString" placeholder="[% l('Query...') %]">
+        ng-model="bucketSvc.queryString" placeholder="[% l('Query...') %]">
       </div>
     </form>
   </div>
 </div>
-
 <br/>
 <div class="row" ng-show="searchInProgress">
   <div class="col-md-6">
 </div>
 
 
-<div class="row">
-  <div class="col-md-12">
-
-    <table class="list table table-hover _table-striped table-condensed"
-      ng-show="pageList.count()"
-      ng-init="pageList.defaultColumns([
-        'id', 'author', 'isbn', 'issn', 'pubdate', 
-        'publisher', 'tcn_value', 'title'
-      ])">
-      <thead>
-        <tr>
-          <th>#</th>
-          <th>
-            <a href='javascript:;' 
-              ng-click="pageList.toggleSelectAll()">&#x2713;</a>
-          </th>
-          <th ng-repeat="field in pageList.allColumns" 
-            ng-show="pageList.displayColumns[field.name]">
-            <a href='javascript:;' 
-              ng-click="sort(field.name);draw()">{{field.label}}</a>
-          </th>
-        </tr>
-      </thead>
-      <tbody>
-        <tr ng-repeat="rec in pageList.items"
-            ng-class="{selected : pageList.selected[rec.id]}"
-            ng-click="applyRowSelection($event, rec.id)">
-          <td>{{$index + 1 + pageList.offset}}</td>
-          <td><span ng-if="pageList.selected[rec.id]">&#x2713;</span>
-          </td>
-          <td ng-repeat="field in pageList.allColumns"
-            ng-show="pageList.displayColumns[field.name]">
-            <div ng-switch="field.name">
-              <div ng-switch-when="title">
-                <a target='_top' href='[% ctx.base_path %]/opac/record/{{rec.id}}'>
-                  {{rec[field.name]}}
-                </a>
-              </div>
-              <div ng-switch-default>
-                {{rec[field.name]}}
-              </div>
-            </div>
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
-</div>
+<eg-grid
+  ng-hide="forbidden"
+  id-field="id"
+  idl-class="rmsr"
+  auto-fields="true"
+  query="gridQuery"
+  menu-label="[% l('Buckets') %]"
+  persist-key="eg.staff.cat.bucket.record.view">
+
+  <!-- global menu -->
+  <eg-grid-menu-item label="[% l('New Bucket') %]" 
+    handler="openCreateBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Edit Bucket') %]" 
+    handler="openEditBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Delete Bucket') %]" 
+    handler="openDeleteBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Shared Bucket') %]" 
+    handler="openSharedBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item divider="true"></eg-grid-menu-item>
+  <eg-grid-menu-item ng-repeat="bkt in bucketSvc.allBuckets" 
+    label="{{bkt.name()}}" handler-data="bkt" 
+    handler="loadBucketFromMenu"></eg-grid-menu-item>
+
+  <!-- actions drop-down -->
+  <eg-grid-action label="[% l('Add To Pending') %]"
+    handler="addToPending"></eg-grid-action>
+  <eg-grid-action label="[% l('Add To Bucket') %]" 
+    handler="addToBucket"></eg-grid-action>
+
+</eg-grid>
index 9885b70..06fc8a7 100644 (file)
-<br/>
 
 <div class="row">
   <div class="col-md-6">
     [% INCLUDE 'staff/cat/bucket/record/t_bucket_info.tt2' %]
   </div>
-
-  <div class="col-md-6 text-right">
-    [% INCLUDE 'staff/cat/bucket/record/t_bucket_selector.tt2' %]
-
-    <!-- TODO: paging and actions drop-downs can be extracted out and shared -->
-    <div class="btn-group text-left">
-
-      <!-- first page -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : pageList.onFirstPage()}" 
-        ng-click="pageList.offset = 0;draw()">[% l('Start') %]</button>
-
-      <!-- previous page -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : pageList.onFirstPage()}"
-        ng-click="pageList.decrementPage();draw()">&laquo;</button>
-
-      <!-- next page -->
-      <!-- todo: paging needs a total count value to be fully functional -->
-      <button type="button" class="btn btn-default" 
-        ng-class="{disabled : !pageList.hasNextPage()}"
-        ng-click="pageList.incrementPage();draw()">&raquo;</button>
-
-      <div class="btn-group">
-        <button type="button" class="btn btn-default dropdown-toggle" 
-            ng-class="{disabled : action_pending}" data-toggle="dropdown">
-          [% l('Actions') %] <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu pull-right">
-          <li ng-class="{disabled : !bucket()}">
-            <a href="javascript:;" ng-click="detachRecords()">
-              [% l('Remove Selected Records') %]</a>
-          </li>
-          <li ng-class="{disabled : !bucket()}">
-            <a href='' ng-click="openExportBucketDialog()">
-                [% l('Export Bucket Records') %]
-            </a>
-          </li>
-        </ul>
-      </div>
-
-      <div class="btn-group col-picker">
-        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
-            <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu pull-right">
-          <li ng-repeat="col in pageList.allColumns">
-            <a href='javascript:;' 
-              ng-click="pageList.displayColumns[col.name] = 
-                !pageList.displayColumns[col.name]">
-                <span ng-if="pageList.displayColumns[col.name]" class="label label-success">&#x2713;</span>
-                <span ng-if="!pageList.displayColumns[col.name]" class="label label-warning">&#x2717;</span>
-                <span>{{col.label}}</span>
-            </a>
-          </li>
-          <li role="presentation" class="divider"></li>
-          <li>
-            <a href='javascript:;' ng-click="pageList.showAllColumns()">[% l('Show All Columns') %]</a>
-          </li>
-          <li>
-            <a href='javascript:;' ng-click="pageList.hideAllColumns()">[% l('Hide All Columns') %]</a>
-          </li>
-          <li class='disabled'>
-            <a href='javascript:;' ng-click="">[% l('Save Columns') %]</a>
-          </li>
-        </ul>
-      </div>
-    </div>
-  </div>
 </div>
 
-<br/>
-<div class="row">
-
-  <div class="col-md-10 col-md-offset-1" ng-show="forbidden">
-    <div class="alert alert-warning">
-      [% l('The selected bucket "{{bucketId}}" is not visible to this login.') %]
-    </div>
-  </div>
-
-  <div class="col-md-12">
-    <table class="list table table-hover table-condensed" ng-show="pageList.count()"
-      ng-init="pageList.defaultColumns([
-        'id', 'author', 'isbn', 'issn', 'pubdate', 
-        'publisher', 'tcn_value', 'title'
-      ])">
-      <thead>
-        <tr>
-          <th>#</th>
-          <th>
-            <a href='javascript:;' 
-              ng-click="pageList.toggleSelectAll()">&#x2713;</a>
-          </th>
-          <th ng-repeat="field in pageList.allColumns" 
-            ng-show="pageList.displayColumns[field.name]">
-            <a href='javascript:;' 
-              ng-click="sort(field.name);draw()">{{field.label}}</a>
-          </th>
-        </tr>
-      </thead>
-      <tbody>
-        <tr ng-repeat="rec in pageList.items"
-            ng-class="{selected : pageList.selected[rec.item_id]}"
-            ng-click="applyRowSelection($event, rec.item_id)">
-          <td>{{$index + 1 + pageList.offset}}</td>
-          <td><span ng-if="pageList.selected[rec.item_id]">&#x2713;</span>
-          </td>
-          <td ng-repeat="field in pageList.allColumns"
-            ng-show="pageList.displayColumns[field.name]">
-            <div ng-switch="field.name">
-              <div ng-switch-when="title">
-                <a target='_top' href='[% ctx.base_path %]/opac/record/{{rec.id}}'>
-                  {{rec[field.name]}}
-                </a>
-              </div>
-              <div ng-switch-default>
-                {{rec[field.name]}}
-              </div>
-            </div>
-          </td>
-        </tr>
-      </tbody>
-    </table>
+<div class="col-md-10 col-md-offset-1" ng-show="forbidden">
+  <div class="alert alert-warning">
+    [% l('The selected bucket "{{bucketId}}" is not visible to this login.') %]
   </div>
 </div>
+
+<eg-grid
+  ng-hide="forbidden"
+  id-field="id"
+  idl-class="rmsr"
+  auto-fields="true"
+  query="gridQuery"
+  menu-label="[% l('Buckets') %]"
+  persist-key="eg.staff.cat.bucket.record.view">
+
+  <!-- global menu -->
+  <eg-grid-menu-item label="[% l('New Bucket') %]" 
+    handler="openCreateBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Edit Bucket') %]" 
+    handler="openEditBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Delete Bucket') %]" 
+    handler="openDeleteBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item label="[% l('Shared Bucket') %]" 
+    handler="openSharedBucketDialog"></eg-grid-menu-item>
+  <eg-grid-menu-item divider="true"></eg-grid-menu-item>
+  <eg-grid-menu-item ng-repeat="bkt in bucketSvc.allBuckets" 
+    label="{{bkt.name()}}" handler-data="bkt" 
+    handler="loadBucketFromMenu"></eg-grid-menu-item>
+
+  <!-- actions drop-down -->
+  <eg-grid-action label="[% l('Remove Selected Records') %]" 
+    handler="detachRecords"></eg-grid-action>
+  <eg-grid-action label="[% l('Export Records') %]" 
+    handler="openExportBucketDialog"></eg-grid-action>
+
+</eg-grid>
index a4f6b55..8541814 100644 (file)
@@ -6,7 +6,7 @@
 %]
 
 [% BLOCK APP_JS %]
-<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/list.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/checkin/app.js"></script>
@@ -42,11 +42,6 @@ COPY_ALERT_MSG_DIALOG_TITLE :
         </div>
       </form>
   </div>
-  <div class="col-md-2 col-md-offset-6 text-right">
-    <div class="btn-group text-left">
-      [% INCLUDE 'staff/parts/column_picker.tt2' listname='checkins' %]
-    </div>
-  </div>
 </div>
 
 [% INCLUDE 'staff/circ/checkin/t_checkin_table.tt2' %]
index 11fa277..44564c6 100644 (file)
@@ -1,50 +1,18 @@
+<!-- checkins list -->
 
-[%
-# checkin table columns
-COLUMNS = [
-{label => l('Barcode'),    name => 'copy_barcode' display => 1},
-{label => l('Circ ID'),    name => 'payload.circ.id', display => 1},
-{label => l('Due Date'),   name => 'payload.circ.due_date' display => 1},
-# once we are handling all response types, we probably don't need to show 
-# Response.  Or, at least, make it more friendly / localizable
-{label => l('Response'),   name => 'textcode', display => 1},
-{label => l('Title'),      name => 'payload.record.title', display => 1},
-{label => l('Author'),     name => 'payload.record.author', display => 1},
-{label => l('Call Number'),name => 'payload.copy.call_number.label', display => 1},
-{label => l('Alert Msg'),  name => 'payload.copy.alert_message' display => 1},
-]
-%]
+<eg-grid
+  id-field="id"
+  features="-display,-sort,-multisort"
+  main-label="[% l('Items Checked In') %]"
+  items-provider="gridDataProvider"
+  persist-key="eg.staff.circ.checkin">
+  <eg-grid-field label="[% ('Circ ID') %]"      path='payload.circ.id' visible></eg-grid-field>
+  <eg-grid-field label="[% ('Barcode') %]"      path='copy_barcode' visible></eg-grid-field>
+  <eg-grid-field label="[% l('Due Date') %]"    path='payload.circ.due_date' visible></eg-grid-field>
+  <eg-grid-field label="[% l('Response') %]"    path="textcode" visible></eg-grid-field>
+  <eg-grid-field label="[% l('Title') %]"       path="payload.record.title" visible></eg-grid-field>
+  <eg-grid-field label="[% l('Author') %]"      path="payload.record.author" visible></eg-grid-field>
+  <eg-grid-field label="[% l('Call Number') %]" path="payload.copy.call_number.label" visible></eg-grid-field>
+  <eg-grid-field label="[% l('Alert Msg') %]"   path="payload.copy.alert_message" visible></eg-grid-field>
+</eg-grid>
 
-<!-- tell JS about our columns so they can be dynamically managed -->
-<div ng-init="
-checkins.setColumns([
-[%- FOR col IN COLUMNS %]
-{label:'[% col.label %]',name:'[% col.name %]'[% IF col.display %],display:true[% END %]}[% IF !loop.last; ','; END -%]
-[% END %]
-])">
-</div>
-
-<div class="row pad-vert" ng-cloak>
-  <div class="col-md-12">
-    <table class="table table-hover table-condensed table-striped">
-      <thead>
-        <tr>
-          <th>#</th>
-          <th ng-repeat="col in checkins.allColumns" 
-            ng-show="checkins.displayColumns[col.name]">
-            {{col.label}}
-          </th>
-        </tr>
-      </thead>
-      <tbody>
-        <tr ng-repeat="checkin in checkins.items | reverse track by $index">
-          <td>{{checkins.count() - $index}}</td>
-          <td ng-repeat="col in checkins.allColumns" 
-            ng-show="checkins.displayColumns[col.name]">
-            {{checkins.fieldValue(checkin, col.name)}}
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
-</div>
index ed0c919..bcea59d 100644 (file)
@@ -1,66 +1,64 @@
-<div class="modal-dialog">
-  <div class="modal-content">
-    <div class="modal-header">
-      <button type="button" class="close" 
-        ng-click="ok()" aria-hidden="true">&times;</button>
-      <h4 class="modal-title">[% l('Hold Slip') %]</h4>
-    </div>
-    <div class="modal-body">
-      <!-- TODO: pub / priv shelf -->
-      <div ng-switch on="evt.payload.hold.behind_desk()">
-        <div ng-switch-when="t">
-          [% l('This item should be routed to the Private Holds Shelf') %]
-        </div>
-        <div ng-switch-when="f">
-          [% l('This item should be routed to the Public Holds Shelf') %]
-        </div>
-      </div>
-      <br/>
-      <div>
-        <span>[% l('Item Barcode:') %]</span>
-        <span>{{evt.payload.copy.barcode()}}</span>
-      </div>
-      <div>
-        <span>[% l('Title:') %]</span>
-        <span>{{evt.payload.record.title()}}</span>
-      </div>
-      <div>
-        <span>[% l('Author:') %]</span>
-        <span>{{evt.payload.record.author()}}</span>
-      </div>
-      <br/>
-      <div>
-      
-      <div ng-show="holdUser.alias()">
-        [% l('Hold for patron {{holdUser.alias()}}') %]
-      </div>
-      <div ng-hide="holdUser.alias()">
-        [% |l %]
-        Hold for patron {{holdUser.family_name()}}, 
-        {{holdUser.first_given_name()}} {{holdUser.second_given_name()}}
-        [% END %]
-      </div>
-      <div>
-        <span>[% l('Patron Barcode:') %]</span>
-        <span>{{holdUser.card().barcode()}}</span>
-      </div>
-      <br/>
-      <div>
-        <span>[% l('Request Date:') %]</span>
-        <span>{{evt.payload.hold.request_time() | date:'shortDate'}}</span>
+<div class="">
+  <div class="modal-header">
+    <button type="button" class="close" 
+      ng-click="ok()" aria-hidden="true">&times;</button>
+    <h4 class="modal-title">[% l('Hold Slip') %]</h4>
+  </div>
+  <div class="modal-body">
+    <!-- TODO: pub / priv shelf -->
+    <div ng-switch on="evt.payload.hold.behind_desk()">
+      <div ng-switch-when="t">
+        [% l('This item should be routed to the Private Holds Shelf') %]
       </div>
-      <div>
-        <span>[% l('Slip Date:') %]</span>
-        <span>{{now | date:'shortDate'}}</span>
+      <div ng-switch-when="f">
+        [% l('This item should be routed to the Public Holds Shelf') %]
       </div>
     </div>
-    <div class="modal-footer">
-      <!--
-      <input type="button" class="btn btn-primary" disabled="disabled"
-        ng-click="print()" value="[% l('Print') %]"/>
-      -->
-      <input type="submit" class="btn btn-warning"
-        ng-click="ok()" value="[% l('Do Not Print') %]"/>
+    <br/>
+    <div>
+      <span>[% l('Item Barcode:') %]</span>
+      <span>{{evt.payload.copy.barcode()}}</span>
     </div>
+    <div>
+      <span>[% l('Title:') %]</span>
+      <span>{{evt.payload.record.title()}}</span>
+    </div>
+    <div>
+      <span>[% l('Author:') %]</span>
+      <span>{{evt.payload.record.author()}}</span>
+    </div>
+    <br/>
+    <div>
+    
+    <div ng-show="holdUser.alias()">
+      [% l('Hold for patron {{holdUser.alias()}}') %]
+    </div>
+    <div ng-hide="holdUser.alias()">
+      [% |l %]
+      Hold for patron {{holdUser.family_name()}}, 
+      {{holdUser.first_given_name()}} {{holdUser.second_given_name()}}
+      [% END %]
+    </div>
+    <div>
+      <span>[% l('Patron Barcode:') %]</span>
+      <span>{{holdUser.card().barcode()}}</span>
+    </div>
+    <br/>
+    <div>
+      <span>[% l('Request Date:') %]</span>
+      <span>{{evt.payload.hold.request_time() | date:'shortDate'}}</span>
+    </div>
+    <div>
+      <span>[% l('Slip Date:') %]</span>
+      <span>{{now | date:'shortDate'}}</span>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <!--
+    <input type="button" class="btn btn-primary" disabled="disabled"
+      ng-click="print()" value="[% l('Print') %]"/>
+    -->
+    <input type="submit" class="btn btn-warning"
+      ng-click="ok()" value="[% l('Do Not Print') %]"/>
   </div>
 </div>
index e1217b8..7d53a83 100644 (file)
@@ -1,5 +1,5 @@
-<div class="modal-dialog">
-  <div class="modal-content">
+<div class="">
+  <div class="">
     <div class="modal-header">
       <button type="button" class="close" 
         ng-click="ok()" aria-hidden="true">&times;</button>
index 98bd966..c0c6e53 100644 (file)
@@ -1,7 +1,7 @@
 <!-- edit bucket dialog -->
 <form class="form-validated" novalidate ng-submit="ok(count)" name="form">
-  <div class="modal-dialog">
-    <div class="modal-content">
+  <div class="">
+    <div class="">
       <div class="modal-header">
         <button type="button" class="close" 
           ng-click="cancel()" aria-hidden="true">&times;</button>
index e9ebdf2..3ac4ad6 100644 (file)
@@ -106,6 +106,8 @@ table.list tr.selected td {
 
 #print-div { display: none; }
 
+/* by default, give all tab panes some top padding */
+.tab-pane { padding-top: 20px; }
 
 /* ----------------------------------------------------------------------
  * Grid
index 959d79a..98aec72 100644 (file)
@@ -9,8 +9,21 @@
   <div style="flex:1">
     <div class="eg-grid-primary-label">{{grid.mainLabel}}</div>
   </div>
+
+  <div class="btn-group" ng-if="grid.menuLabel" style="margin-right: 10px">
+    <button type="button" 
+      class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+      {{grid.menuLabel}}<span class="caret"></span>
+    </button>
+    <ul class="dropdown-menu">
+      <li ng-repeat="item in grid.menuItems" ng-class="{divider: item.divider}">
+        <a ng-if="!item.divider" href='' 
+          ng-click="item.handler(item, item.handlerData)">{{item.label}}</a>
+      </li>
+    </ul>
+  </div>
   
-  <!-- column picker -->
+  <!-- column picker, pager, etc. -->
   <div class="btn-group column-picker">
 
     <!-- first page -->
         <span class="glyphicon glyphicon-forward"></span>
     </button>
 
+    <!-- actions drop-down menu -->
+    <div class="btn-group" ng-if="grid.actions.length">                                                  
+      <button type="button" class="btn btn-default dropdown-toggle"          
+          ng-class="{disabled : false}" data-toggle="dropdown">     
+        [% l('Actions') %] <span class="caret"></span>                       
+      </button>                                                              
+      <ul class="dropdown-menu pull-right">                                  
+        <li ng-class="{disabled : false}" ng-repeat="action in grid.actions">
+          <a href="" ng-click="grid.actionLauncher(action)">{{action.label}}</a>
+        </li>                                                                
+      </ul>
+    </div>
+
     <div class="btn-group">
       <button type="button" title="[% ('Select Row Count') %]"
         class="btn btn-default dropdown-toggle" data-toggle="dropdown">
index eedd805..cbc598e 100644 (file)
@@ -13,7 +13,7 @@
  */
 
 angular.module('egCatRecordBuckets', 
-    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egListMod'])
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
 
 .config(function($routeProvider, $locationProvider) {
     $locationProvider.html5Mode(true);
@@ -67,8 +67,8 @@ angular.module('egCatRecordBuckets',
  * tab click (i.e. route persistence).
  */
 .factory('bucketSvc', 
-       ['$q','egList','egNet','egAuth','egIDL','egEvent',
-function($q,  egList,  egNet,  egAuth,  egIDL,  egEvent) {
+       ['$q','egNet','egAuth','egIDL','egEvent',
+function($q,  egNet,  egAuth,  egIDL,  egEvent) {
 
     var service = {
         allBuckets : [], // un-fleshed user buckets
@@ -77,9 +77,9 @@ function($q,  egList,  egNet,  egAuth,  egIDL,  egEvent) {
         currentBucket : null, // currently viewed bucket
 
         // per-page list collections
-        searchList  : egList.create(),
-        pendingList : egList.create(),
-        viewList  : egList.create({indexField : 'item_id'}),
+        searchList  : [],
+        pendingList : [],
+        viewList  : [],
 
         // fetches all staff/biblio buckets for the authenticated user
         // this function may only be called after startup.
@@ -189,6 +189,7 @@ function($q,  egList,  egNet,  egAuth,  egIDL,  egEvent) {
                 deferred.reject(evt);
                 return;
             }
+            console.log('detached bucket item ' + itemId);
             deferred.resolve(resp);
         });
 
@@ -235,18 +236,22 @@ function($scope,  $location,  $q,  $timeout,  $modal,
     // tabs: search, pending, view
     $scope.setTab = function(tab) { 
         $scope.tab = tab;
-        $scope.pageList = bucketSvc[tab + 'List'];
 
         // for bucket selector; must be called after route resolve
         bucketSvc.fetchUserBuckets(); 
     };
 
+    $scope.loadBucketFromMenu = function(item, bucket) {
+        if (bucket) return $scope.loadBucket(bucket.id());
+    }
+
     $scope.loadBucket = function(id) {
         $location.path(
             '/cat/bucket/record/' + 
                 $scope.tab + '/' + encodeURIComponent(id));
     }
 
+    // TODO: grid selected items..
     $scope.addToBucket = function(all) {
         /** TODO: open-ils.actor.container.item.create almost works
          * with batches, but not quite ... */
@@ -273,92 +278,12 @@ function($scope,  $location,  $q,  $timeout,  $modal,
                     // the list size).  The data stored is inconsistent, but since
                     // we are forcing a bucket refresh on the next rendering of 
                     // the view pane, the list will be repaired.
-                    bucketSvc.viewList.items.push(resp);
-                    bucketSvc.viewList.totalCount++;
+                    bucketSvc.currentBucket.items().push(resp);
                 });
             }
         );
     }
 
-
-    // same for all controllers
-    $scope.applyRowSelection = function($event, index) {
-        if ($event.ctrlKey || $event.metaKey) { // metaKey == mac command
-            $scope.pageList.toggleOneSelection(index);
-        } else {
-            $scope.pageList.selectOne(index);
-        }
-    }
-
-
-    /** ----------------
-     * this will all change when we stop using rmsr's
-     * TODO: stop using rmsr's
-     */
-    $scope.sort = function(field) {
-        $scope.pageList.offset = 0;
-        if (typeof $scope.pageList.sort == 'string' &&
-            $scope.pageList.sort == field) {
-            // already sorting on 'field', now sort descending
-            $scope.pageList.sort = {};
-            $scope.pageList.sort[field] = 'desc';
-        } else {
-            $scope.pageList.sort = field;
-        }
-    }
-
-    $scope.setupColumns = function() {
-        $scope.sortedFields = egIDL.classes.rmsr.fields.sort(
-            function(a, b) { return a.label < b.label ? -1 : 1 });
-
-        $scope.fields = {};
-        $scope.queryFields = {};
-        var cols = [];
-        angular.forEach($scope.sortedFields, function(field) {
-            if (field.virtual) return;
-            cols.push(field);
-            $scope.fields[field.name] = field;
-            $scope.queryFields[field.name] = field.name;
-        });
-
-        $scope.pageList.setColumns(cols);
-    }
-
-    $scope.getRecords = function(ids) {
-        if (ids.length == 0) return $q.when();
-
-        $scope.pageList.totalCount = ids.length;
-
-        // grab the lot in one go
-        return egNet.request(
-            'open-ils.fielder',
-            'open-ils.fielder.flattened_search',
-            egAuth.token(), "rmsr", $scope.queryFields, 
-            {id : ids},
-            {   sort : [$scope.pageList.sort || 'id'],
-                limit : $scope.pageList.limit,
-                offset : $scope.pageList.offset
-            }
-        ).then(
-            null, // success
-            null, // error
-            function(record) { // notify handler
-
-                // apply some data munging to make the list values 
-                // of 'rmsr' more human friendly.
-                if (record.isbn) {
-                    record.isbn = record.isbn.replace(/\{NULL\}/,'');
-                    record.isbn = record.isbn.replace(/\{(.*)\}/,'$1');
-                }
-                if (record.issn) {
-                    record.issn = record.issn.replace(/\{NULL\}/,'');
-                    record.issn = record.issn.replace(/\{(.*)\}/,'$1');
-                }
-                $scope.pageList.items.push(record);
-            }
-        );
-    }
-
     $scope.openCreateBucketDialog = function() {
         $modal.open({
             templateUrl: './cat/bucket/record/t_bucket_create',
@@ -373,7 +298,7 @@ function($scope,  $location,  $q,  $timeout,  $modal,
             bucketSvc.createBucket(args.name, args.desc).then(
                 function(id) {
                     if (!id) return;
-                    bucketSvc.viewList.reset();
+                    bucketSvc.viewList = [];
                     bucketSvc.allBuckets = []; // reset
                     $location.path(
                         '/cat/bucket/record/' + $scope.tab + '/' + id);
@@ -481,44 +406,42 @@ function($scope,  $location,  $q,  $timeout,  $modal,
 .controller('SearchCtrl',
        ['$scope','$routeParams','egAuth','egNet','egIDL','bucketSvc',
 function($scope,  $routeParams,  egAuth,  egNet,  egIDL,  bucketSvc) {
+
     $scope.setTab('search');
-    $scope.setupColumns();
     $scope.focusMe = true;
+    var idQueryHash = {};
+    $scope.gridQuery = function() { return idQueryHash }
 
     // add selected items directly to the pending list
-    $scope.addToPending = function(all) {
-        var recs = all ? $scope.pageList.items : $scope.pageList.selectedItems();
+    $scope.addToPending = function(recs) {
         angular.forEach(recs, function(rec) {
-            if (bucketSvc.pendingList.items.filter( // remove dupes
+            if (bucketSvc.pendingList.filter( // remove dupes
                 function(r) {return r.id == rec.id}).length) return;
-            bucketSvc.pendingList.items.push(rec);
+            bucketSvc.pendingList.push(rec);
         });
     }
 
     $scope.search = function() {
-        $scope.pageList.resetPageData();
+        $scope.searchList = [];
         $scope.searchInProgress = true;
         bucketSvc.queryRecords = [];
 
         egNet.request(
             'open-ils.search',
             'open-ils.search.biblio.multiclass.query', {   
-                // full search limit needs to be larger than page list limit
-                limit : $scope.pageList.limit * 10,
+                limit : 500 // meh
             }, bucketSvc.queryString, true
         ).then(function(resp) {
             $scope.searchInProgress = false;
             bucketSvc.queryRecords = resp.ids.map(function(id){return id[0]});
-            $scope.pageList.totalCount = bucketSvc.queryRecords.length;
-            $scope.getRecords(bucketSvc.queryRecords);
+            if (bucketSvc.queryRecords.length) {
+                idQueryHash = {id : bucketSvc.queryRecords};
+            } else {
+                idQueryHash = {};
+            }
         });
     }
 
-    $scope.draw = function() { 
-        $scope.pageList.resetPageData();
-        $scope.getRecords(bucketSvc.queryRecords);
-    }
-
     if ($routeParams.id && 
         (!bucketSvc.currentBucket || 
             bucketSvc.currentBucket.id() != $routeParams.id)) {
@@ -526,25 +449,27 @@ function($scope,  $routeParams,  egAuth,  egNet,  egIDL,  bucketSvc) {
         // fetch the bucket for display, then set the totalCount
         // (also for display), but avoid fully fetching the bucket,
         // since it's premature, in this UI.
-        bucketSvc.fetchBucket($routeParams.id)
-        .then(function(bucket) {
-            bucketSvc.viewList.totalCount = bucket.items().length;
-        });
+        bucketSvc.fetchBucket($routeParams.id);
     }
 }])
 
 .controller('PendingCtrl',
-       ['$scope','$routeParams','egAuth','egNet','egIDL','bucketSvc',
-function($scope,  $routeParams,  egAuth,  egNet,  egIDL,  bucketSvc) {
+       ['$scope','$routeParams','bucketSvc','egGridDataProvider',
+function($scope,  $routeParams,  bucketSvc , egGridDataProvider) {
     $scope.setTab('pending');
-    $scope.setupColumns();
 
-    $scope.draw = function() { 
-        // only called when sorting an existing list of records
-        var ids = $scope.pageList.items.map(function(r) {return r.id});
-        $scope.pageList.resetPageData();
-        $scope.getRecords(ids);
+    var provider = egGridDataProvider.instance({});
+    provider.get = function(offset, count) {
+        return provider.arrayNotifier(
+            bucketSvc.pendingList, offset, count);
     }
+    provider.itemFieldValue = provider.flatItemFieldValue;
+    $scope.gridDataProvider = provider;
+
+    $scope.resetPendingList = function() {
+        bucketSvc.pendingList = [];
+    }
+    
 
     if ($routeParams.id && 
         (!bucketSvc.currentBucket || 
@@ -553,84 +478,62 @@ function($scope,  $routeParams,  egAuth,  egNet,  egIDL,  bucketSvc) {
         // fetch the bucket for display, then set the totalCount
         // (also for display), but avoid fully fetching the bucket,
         // since it's premature, in this UI.
-        bucketSvc.fetchBucket($routeParams.id)
-        .then(function(bucket) {
-            bucketSvc.viewList.totalCount = bucket.items().length;
-        });
+        bucketSvc.fetchBucket($routeParams.id);
     }
 }])
 
 .controller('ViewCtrl',
-       ['$scope','$window','$timeout','$location','$routeParams','egAuth','egNet','egIDL','bucketSvc','egEvent',
-function($scope,  $window,  $timeout,  $location,  $routeParams,  egAuth,  egNet,  egIDL,  bucketSvc, egEvent) {
+       ['$scope','$q','$routeParams','bucketSvc',
+function($scope,  $q , $routeParams,  bucketSvc) {
 
     $scope.setTab('view');
-    $scope.setupColumns();
-
     $scope.bucketId = $routeParams.id;
 
-    // no bucket selected, clear out any cached data 
-    if (!$scope.bucketId) {
-        bucketSvc.currentBucket = null;
-        bucketSvc.viewList.reset();
-        return;
-    }
-    
-    $scope.detachRecords = function() {
-        var records = $scope.pageList.selectedItems();
-        angular.forEach(records, function(rec) {
-            bucketSvc.detachRecord(rec.item_id).then(function(resp) {
-                $scope.pageList.removeItem(rec.item_id);
-                $scope.pageList.totalCount--;
-            });
-        });
-    }
-
-    function getBucketRecords(recordIds) {
-        $scope.getRecords(recordIds).then(function() {
-            // link the bucket item to the record.
-            var matched = {};
-            angular.forEach($scope.bucket().items(), function(item) {
-                var rec;
-                var rid = item.target_biblio_record_entry();
-                if (matched[rid]) { 
-                    // dupe bib record.  clone it into the list
-                    rec = angular.copy(matched[rid]);
-                    $scope.pageList.items.push(rec);
-                } else {
-                    // find the record in the data we just fetched
-                    // note: don't use getItem, since our index field is 'item_id'
-                    rec = $scope.pageList.items.filter(
-                        function(r) {return r.id == rid})[0];
-                    matched[rid] = rec;
-                }
-                // rec will be unset if the record in question is not 
-                // visible in this page of data.
-                if (rec) rec.item_id = item.id();
-            });
-        });
-    }
+    // idQuery contents will change with each bucket loaded
+    // as the query changes, the grid will notice and refresh itself
+    var idQueryHash = {};
+    $scope.gridQuery = function() { return idQueryHash }
 
-    // fetch the bucket and linked records as needed to 
-    // populate the page list.
-    $scope.draw = function() {
-        $scope.pageList.resetPageData();
-        bucketSvc.fetchBucket($scope.bucketId).then(
+    function drawBucket() {
+        bucketSvc.bucketNeedsRefresh = true; // meh about this..
+        return bucketSvc.fetchBucket($scope.bucketId).then(
             function(bucket) {
-                ids = bucketSvc.currentBucket.items().map(
+                ids = bucket.items().map(
                     function(i){return i.target_biblio_record_entry()}
                 );
-                getBucketRecords(ids);
-            },
-            function(evt) { $scope.forbidden = true }
+                if (ids.length) {
+                    idQueryHash = {id : ids};
+                } else {
+                    idQueryHash = {}; // avoid empty array query errors
+                }
+            }
         );
-    };
+    }
+
+    $scope.detachRecords = function(records) {
+        var promises = [];
+        angular.forEach(records, function(rec) {
+            var item = bucketSvc.currentBucket.items().filter(
+                function(i) {
+                    return (i.target_biblio_record_entry() == rec.id)
+                }
+            );
+            if (item.length)
+                promises.push(bucketSvc.detachRecord(item[0].id()));
+        });
+
+        return $q.all(promises).then(drawBucket);
+    }
 
     // avoid re-fetching the records for a bucket if the bucket
     // is already loaded and we are navigating back to the 
     // view tab.
-    if (bucketSvc.bucketRefreshLevel($scope.bucketId) == 1 ||
-        bucketSvc.viewList.count() == 0 ) {
-        $scope.draw();
+    if ($scope.bucketId && (
+            bucketSvc.bucketRefreshLevel($scope.bucketId) == 1 ||
+            bucketSvc.currentBucket.items().length == 0) ) {
+
+        drawBucket()['catch'](
+            function() { $scope.forbidden = true }
+        );
     }
 }])
index 6d9c926..9b98ca8 100644 (file)
@@ -1,20 +1,20 @@
 angular.module('egCheckinApp', ['ngRoute', 'ui.bootstrap', 
-    'egCoreMod', 'egUiMod', 'egListMod', 'egUserMod'])
+    'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod'])
 
-.config(function($routeProvider, $locationProvider) {
+.config(function($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
-    // no routes needed 
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
 })
 
 /**
  * checkin service
  */
 .factory('checkinSvc',
-       ['$q','egList','egNet','egAuth','egUser','egEnv','egOrg','egList',
-function($q,  egList,  egNet,  egAuth,  egUser,  egEnv,  egOrg,  egList) {
+       ['$q','egNet','egAuth','egUser','egEnv','egOrg',
+function($q,  egNet,  egAuth,  egUser,  egEnv,  egOrg) {
 
     var service = {};
-    service.checkins = egList.create();
+    service.checkins = [];
     return service;
 }])
 
@@ -50,9 +50,9 @@ function($q,  egOrg,  egPCRUD) {
  * Manages checkin
  */
 .controller('CheckinCtrl',
-       ['$scope','$q','$modal','egStartup','checkinSvc','egNet', 'egAuth',
+       ['$scope','$q','$modal','egStartup','checkinSvc','egNet','egAuth','egGridDataProvider',
         'orgAddrSvc','egOrg','egPCRUD','egAlertDialog','egConfirmDialog','egCheckinStrings',
-function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet,  egAuth,  
+function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet , egAuth , egGridDataProvider, 
          orgAddrSvc,  egOrg,  egPCRUD,  egAlertDialog,  egConfirmDialog,  egCheckinStrings) {
 
     // run egStartup here since it's not handled via resolver
@@ -65,6 +65,29 @@ function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet,  egAuth,
     $scope.focusMe = true;
     $scope.checkins = checkinSvc.checkins;
 
+    var provider = egGridDataProvider.instance({});
+    provider.initialize = function() {
+        return {offset : 0};
+    }
+
+    provider.get = function(offset, count) {
+        return provider.arrayNotifier(
+            $scope.checkins, offset, count
+        );
+    }
+
+    provider.itemFieldValue = function(item, column) {
+        return provider.nestedItemFieldValue(item, column);
+    };
+
+    $scope.gridDataProvider = provider;
+
+    function addCheckin(evt) {
+        checkinSvc.checkins.push(evt);
+        provider.increment();
+    }
+
+
     $scope.checkin = function(args) {
         if (args && args.copy_barcode) {
             performCheckin(angular.copy(args));
@@ -87,7 +110,7 @@ function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet,  egAuth,
             }
 
             if (angular.isArray(evt)) evt = evt[0];
-            evt.id = checkinSvc.checkins.count();
+            evt.id = checkinSvc.checkins.length;
             evt.copy_barcode = args.copy_barcode;
             handleCheckinResponse(evt, args, override);
         });
@@ -104,7 +127,7 @@ function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet,  egAuth,
         switch (evt.textcode) {
             case 'SUCCESS':
             case 'NO_CHANGE':
-                checkinSvc.checkins.items.push(evt);
+                addCheckin(evt);
                 if (copy.status() == 8) { // on holds shelf 
                     if (hold && 
                         hold.pickup_lib() == egAuth.user().ws_ou()) {
@@ -117,7 +140,7 @@ function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet,  egAuth,
                 break;
                 
             case 'ROUTE_ITEM':
-                checkinSvc.checkins.items.push(evt);
+                addCheckin(evt);
                 openRouteDialog('./circ/checkin/t_transit_dialog', evt, args);
                 break;
             case 'ASSET_COPY_NOT_FOUND':
@@ -138,7 +161,7 @@ function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet,  egAuth,
                         cancel : function() { 
                             // on cancel, push the event on the list
                             // to show that it happened
-                            checkinSvc.checkins.items.push(evt);
+                            addCheckin(evt);
                         }
                     }
                 ).result.then(function() {$scope.focusMe = true})
@@ -148,7 +171,7 @@ function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet,  egAuth,
                 console.debug('checkin: ' + js2JSON(evt));
                 // push it on the list so the user can at least see 
                 // something happened.
-                $scope.checkins.items.push(evt);
+                addCheckin(evt);
         }
     }
 
@@ -170,11 +193,11 @@ function($scope,  $q,  $modal,  egStartup,  checkinSvc,  egNet,  egAuth,
             resolve : {
                 destAddr : function() {
                     if (!evt.org) return $q.when();
-                    // TODO: response payload should flesh dest addr
+                    // TODO SERVER: response payload should flesh dest addr
                     return orgAddrSvc.getAddr(evt.org, 'holds_address');
                 },
                 holdUser : function() {
-                    // TODO: response payload should flesh hold recipient
+                    // TODO SERVER: response payload should flesh hold recipient
                     if (!evt.payload.hold) return $q.when();
                     return egPCRUD.retrieve('au', 
                         evt.payload.hold.usr(), {
index b53e2f0..acb7203 100644 (file)
@@ -111,18 +111,6 @@ function($q , $timeout , egNet,  egAuth,  egUser,  egEnv,  egOrg) {
         // overridden for checkouts to this patron for this instance of
         // the interface.
         checkout_overrides : {},
-
-        // delivers the content of an array as a stream of promise notifications
-        arrayNotifier : function(arr) {
-            var def = $q.defer();
-            // promise notifications are only witnessed when delivered
-            // after the caller has his hands on the promise object
-            $timeout(function() {
-                angular.forEach(arr, def.notify);
-                def.resolve();
-            });
-            return def.promise;
-        }
     };
 
     // when we change the default patron, we need to clear out any
index 5e3f724..9100acc 100644 (file)
@@ -19,8 +19,8 @@ function($scope,  $q,  $modal,  $routeParams,  egNet,  egAuth,  egUser,
     // Grid Provider -------------------
     var provider = egGridDataProvider.instance({});
     provider.get = function(offset, count) {
-        return patronSvc.arrayNotifier(
-            $scope.checkouts.slice(offset, offset + count)
+        return provider.arrayNotifier(
+            $scope.checkouts, offset, count
         );
     }
     provider.itemFieldValue = function(item, column) {
index 2f1f0c2..9abba54 100644 (file)
@@ -35,8 +35,8 @@ function($scope,  $q,  $routeParams,  egNet,  egAuth,  egUser,  patronSvc,  egOr
 
         // see if we have the requested range cached
         if (patronSvc.holds[offset]) {
-            return patronSvc.arrayNotifier(
-                patronSvc.holds.slice(offset, offset + count)
+            return provider.arrayNotifier(
+                patronSvc.holds, offset, count
             );
         }
 
index 679faa9..24cca7d 100644 (file)
@@ -55,9 +55,8 @@ function($scope,  $q,  $routeParams,  egNet,  egAuth,  egUser,  patronSvc,  egPC
 
         // see if we have the requested range cached
         if (patronSvc.items_out[offset]) {
-            return patronSvc.arrayNotifier(
-                patronSvc.items_out.slice(offset, offset + count)
-            );
+            return provider.arrayNotifier(
+                patronSvc.items_out, offset, count);
         }
 
         // see if we have the circ IDs for this range already loaded
index f1ebb12..46a5c78 100644 (file)
@@ -10,8 +10,9 @@ angular.module('egGridMod',
             // IDL class hint (e.g. "aou")
             idlClass : '@',
 
-            // points to a structure in the calling scope which defines
-            // a PCRUD-compliant query.
+            // reference to a function returning a query for the default
+            // egGridFlatDataProvider handler.  Any time the return value
+            // changes, the grid will be refreshed.
             query : '=',
 
             // if true, grid columns are derived from all non-virtual
@@ -27,8 +28,7 @@ angular.module('egGridMod',
             // can determine the primary key directly from the IDL.
             idField : '@',
 
-            // egList containting our tabular data is provided for us
-            // and managed externally.
+            // Reference to externally provided egGridDataProvider
             itemsProvider : '=',
 
             // comma-separated list of supported or disabled grid features
@@ -42,6 +42,12 @@ angular.module('egGridMod',
 
             // optional primary grid label
             mainLabel : '@',
+
+            // if true, use the IDL class label as the mainLael
+            autoLabel : '=', 
+
+            // optional context menu label
+            menuLabel : '@'
         },
 
         // TODO: avoid hard-coded url
@@ -67,12 +73,19 @@ angular.module('egGridMod',
                 grid.limit = 25;
                 grid.items = [];
                 grid.selected = {}; // idField-based
+                grid.actions = [];
                 grid.totalCount = -1;
-                grid.dataProvider = $scope.itemsProvider;
                 grid.idlClass = $scope.idlClass;
                 grid.mainLabel = $scope.mainLabel;
                 grid.indexField = $scope.idField;
                 grid.showGridConf = false;
+                grid.dataProvider = $scope.itemsProvider;
+                grid.menuLabel = $scope.menuLabel;
+
+                grid.menuItems = [];
+                grid.addMenuItem = function(item) {
+                    grid.menuItems.push(item);
+                }
 
                 // default flex values for the index and selector columns
                 grid.indexFlex = 1;
@@ -89,7 +102,7 @@ angular.module('egGridMod',
 
                 if ($scope.autoFields) {
                     grid.indexField = egIDL.classes[grid.idlClass].pkey;
-                    if (!grid.mainLabel)
+                    if (grid.autoLabel)
                         grid.mainLabel = egIDL.classes[grid.idlClass].label;
                     grid.columnsProvider.compileAutoColumns();
                 }
@@ -114,6 +127,12 @@ angular.module('egGridMod',
                         columnsProvider : grid.columnsProvider,
                         query : $scope.query
                     });
+
+                    $scope.$watch(
+                        function() { return $scope.query() }, 
+                        function() { grid.collect() },
+                        true // object comparison
+                    );
                 }
 
                 // this allows the caller to pass in initializtion 
@@ -197,6 +216,20 @@ angular.module('egGridMod',
                 return item; 
             }
 
+            // fires the action handler function
+            grid.actionLauncher = function(action) {
+                action.handler(grid.getSelectedItems());
+            }
+
+            // returns the list of selected item objects
+            grid.getSelectedItems = function() {
+                return grid.items.filter(
+                    function(item) {
+                        return Boolean(grid.selected[grid.indexValue(item)]);
+                    }
+                );
+            }
+
             // selects one row after deselecting all of the others
             grid.selectOneItem = function(index) {
                 grid.selected = {};
@@ -526,6 +559,29 @@ angular.module('egGridMod',
     };
 })
 
+/**
+ * eg-grid-action : used for specifying actions which may be applied
+ * to items within the grid.
+ */
+.directive('egGridAction', function() {
+    return {
+        require : '^egGrid',
+        restrict : 'AE',
+        transclude : true,
+        scope : {
+            label   : '@', // Action label
+            handler : '='  // Action function handler
+        },
+        template : '<div></div>', // NOOP template
+        link : function(scope, element, attrs, egGridCtrl) {
+            egGridCtrl.actions.push({
+                label : scope.label,
+                handler : scope.handler
+            });
+        }
+    };
+})
+
 .factory('egGridColumnsProvider', ['egIDL', function(egIDL) {
 
     function ColumnsProvider(args) {
@@ -696,8 +752,8 @@ angular.module('egGridMod',
  * meet the needs of each individual grid.
  */
 .factory('egGridDataProvider', 
-           ['$filter','egNet','egAuth','egIDL',
-    function($filter , egNet , egAuth , egIDL) {
+           ['$q','$timeout','$filter','egNet','egAuth','egIDL',
+    function($q , $timeout , $filter , egNet , egAuth , egIDL) {
 
         function GridDataProvider(args) {
             var gridData = this;
@@ -708,6 +764,23 @@ angular.module('egGridMod',
             gridData.idlClass = args.idlClass;
             gridData.columnsProvider = args.columnsProvider;
 
+            // Delivers a stream of array data via promise.notify()
+            // Useful for passing an array of data to egGrid.get()
+            // If a count is provided, the array will be trimmed to
+            // the range defined by count and offset
+            gridData.arrayNotifier = function(arr, offset, count) {
+                if (!arr || arr.length == 0) return $q.when();
+                if (count) arr = arr.slice(offset, offset + count);
+                var def = $q.defer();
+                // promise notifications are only witnessed when delivered
+                // after the caller has his hands on the promise object
+                $timeout(function() {
+                    angular.forEach(arr, def.notify);
+                    def.resolve();
+                });
+                return def.promise;
+            }
+
             gridData.initialize = function() {
                 return {};
             }
@@ -786,14 +859,20 @@ angular.module('egGridMod',
 // Factory service for egGridDataManager instances, which are
 // responsible for collecting flattened grid data.
 .factory('egGridFlatDataProvider', 
-           ['egNet','egAuth','egGridDataProvider',
-    function(egNet , egAuth , egGridDataProvider) {
+           ['$q','egNet','egAuth','egGridDataProvider',
+    function($q , egNet , egAuth , egGridDataProvider) {
 
         return {
             instance : function(args) {
                 var provider = egGridDataProvider.instance(args);
 
                 provider.get = function(offset, count) {
+                    if (!angular.isFunction(provider.query)) 
+                        return $q.when();
+
+                    var query = provider.query();
+                    if (!query || angular.equals(query, {})) 
+                        return $q.when();
 
                     // find all of the currently visible columns
                     var queryFields = {}
@@ -807,8 +886,8 @@ angular.module('egGridMod',
                     return egNet.request(
                         'open-ils.fielder',
                         'open-ils.fielder.flattened_search',
-                        egAuth.token(), provider.idlClass, queryFields,
-                        provider.query,
+                        egAuth.token(), provider.idlClass, 
+                        queryFields, query,
                         {   sort : provider.sort,
                             limit : count,
                             offset : offset
@@ -932,6 +1011,27 @@ angular.module('egGridMod',
         }
     };
 })
+.directive('egGridMenuItem', function() {
+    return {
+        restrict : 'AE',
+        require : '^egGrid',
+        scope : {
+            label : '@',  
+            handler : '=', // onclick handler function
+            divider : '=', // if true, show a divider only
+            handlerData : '=' // if set, passed as second argument to handler
+        },
+        link : function(scope, element, attrs, egGridCtrl) {
+            egGridCtrl.addMenuItem({
+                label : scope.label,
+                handler : scope.handler,
+                divider : scope.divider,
+                handlerData : scope.handlerData
+            });
+        }
+    };
+})