LP#1775466 More grid
authorBill Erickson <berickxx@gmail.com>
Sun, 13 May 2018 16:58:55 +0000 (12:58 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 6 Jun 2018 20:59:26 +0000 (16:59 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
21 files changed:
Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid-data-source.ts [deleted file]
Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.component.html
Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
Open-ILS/src/eg2/src/app/share/grid/grid.service.ts [deleted file]
Open-ILS/src/eg2/src/app/share/grid/grid.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/util/format.service.ts
Open-ILS/src/eg2/src/app/share/util/pager.ts
Open-ILS/src/eg2/src/app/staff/admin/server/config/billing_type.component.ts
Open-ILS/src/eg2/src/app/staff/admin/server/config/hard_due_date.component.ts
Open-ILS/src/eg2/src/app/staff/resolver.service.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts

index 80795b1..f1ce757 100644 (file)
@@ -1,6 +1,6 @@
-import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {Component, Input, OnInit} from '@angular/core';
 import {EgDialogComponent} from '@eg/share/dialog/dialog.component';
-import {EgGridService, EgGridColumn, EgGridColumnSet} from './grid.service';
+import {EgGridColumnSet} from './grid';
 
 @Component({
   selector: 'eg-grid-column-config',
@@ -9,9 +9,8 @@ import {EgGridService, EgGridColumn, EgGridColumnSet} from './grid.service';
 
 /**
  */
-export class EgGridColumnConfigComponent extends EgDialogComponent {
+export class EgGridColumnConfigComponent extends EgDialogComponent implements OnInit {
     @Input() columnSet: EgGridColumnSet;
-
 }
 
 
index e16457f..e09c86a 100644 (file)
@@ -1,7 +1,6 @@
 import {Component, Input, OnInit, Host} from '@angular/core';
-import {EgGridService, EgGridColumn, EgGridColumnSet} from './grid.service';
-import {EgGridDataSource} from './grid-data-source';
-import {EgGridComponent} from './grid.component';
+import {EgGridContext, EgGridColumn, EgGridColumnSet, 
+    EgGridDataSource} from './grid';
 
 @Component({
   selector: 'eg-grid-column-width',
@@ -10,16 +9,15 @@ import {EgGridComponent} from './grid.component';
 
 export class EgGridColumnWidthComponent implements OnInit {
 
-    @Input() columnSet: EgGridColumnSet;
+    @Input() gridContext: EgGridContext;
+    columnSet: EgGridColumnSet;
     isVisible: boolean;
 
-    constructor(
-        private gridSvc: EgGridService,
-        @Host() private grid: EgGridComponent
-    ) { }
+    constructor() {}
 
     ngOnInit() {
         this.isVisible = false;
+        this.columnSet = this.gridContext.columnSet;
     }
 
     expandColumn(col: EgGridColumn) {
index 1f2b49e..89b0c41 100644 (file)
@@ -1,5 +1,5 @@
 import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
-import {EgGridService, EgGridColumn, EgGridColumnSet} from './grid.service';
+import {EgGridColumn, EgGridColumnSet} from './grid';
 import {EgGridComponent} from './grid.component';
 
 @Component({
@@ -24,9 +24,7 @@ export class EgGridColumnComponent implements OnInit {
     @Input() cellTemplate: TemplateRef<any>;
 
     // get a reference to our container grid.
-    constructor(
-        private gridSvc: EgGridService,
-        @Host() private grid: EgGridComponent) {}
+    constructor(@Host() private grid: EgGridComponent) {}
 
     ngOnInit() {
 
@@ -45,7 +43,7 @@ export class EgGridColumnComponent implements OnInit {
         col.cellTemplate = this.cellTemplate;
         col.isSortable = this.sortable;
         col.isMultiSortable = this.multiSortable;
-        this.grid.columnSet.add(col);
+        this.grid.context.columnSet.add(col);
     }
 }
 
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-data-source.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-data-source.ts
deleted file mode 100644 (file)
index 2355483..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-import {EventEmitter} from '@angular/core';
-import {Observable} from 'rxjs/Rx';
-import {Pager} from '@eg/share/util/pager';
-
-export class EgGridDataSource {
-
-    data: any[];
-    sort: any[];
-    pager: Pager;
-    allRowsRetrieved: boolean;
-    getRows: (pager: Pager, sort: any[]) => Observable<any>;
-
-    constructor() {
-        this.sort = [];
-        this.reset();
-    }
-
-    reset() {
-        this.data = [];
-        this.allRowsRetrieved = false;
-        if (this.pager) this.pager.resultCount = null;
-    }
-
-    setAllRetrieved() {
-        this.allRowsRetrieved = true;
-        this.pager.resultCount = this.data.length;
-    }
-
-    // called from the template -- no data fetching
-    getPageOfRows(pager: Pager): any[] {
-        if (this.data) {
-            return this.data.slice(
-                pager.offset, pager.limit + pager.offset);
-        }
-        return [];
-    }
-
-    // called on initial component load and user action (e.g. paging, sorting).
-    requestPage(pager: Pager): Promise<any> {
-
-        if (
-            // already have the current page
-            this.getPageOfRows(pager).length > 0 
-            // already have all data
-            || this.allRowsRetrieved
-            // have no way to get more data.
-            || !this.getRows
-        ) {
-            return Promise.resolve();
-        }
-
-        return new Promise((resolve, reject) => {
-            let idx = pager.offset;
-            return this.getRows(pager, this.sort).subscribe(
-                row => this.data[idx++] = row,
-                err => {
-                    console.error(`grid getRows() error ${err}`);
-                    reject(err);
-                },
-                ()  => {
-                    this.checkAllRetrieved(pager, idx);
-                    resolve();
-                }
-            );
-        });
-    }
-
-    // See if the last getRows() call resulted in the final set of data.
-    checkAllRetrieved(pager: Pager, idx: number) {
-        if (this.allRowsRetrieved) return;
-
-        if (idx == 0 || idx < (pager.limit + pager.offset)) {
-            // last query returned nothing or less than one page.
-            // confirm we have all of the preceding pages.
-            if (!this.data.includes(undefined)) {
-                this.allRowsRetrieved = true;
-                pager.resultCount = this.data.length;
-            }
-        }
-    }
-}
-
-
index 18623f0..827c985 100644 (file)
@@ -6,7 +6,7 @@
   <div class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny">
     <span i18n="number|Row Number Header">#</span>
   </div>
-  <div *ngFor="let col of columnSet.displayColumns()" 
+  <div *ngFor="let col of gridContext.columnSet.displayColumns()" 
     draggable="true" 
     (dragstart)="dragColumn = col"
     (drop)="onColumnDrop(col)"
index fc08727..6cb3a8a 100644 (file)
@@ -1,9 +1,6 @@
-import {Component, Input, OnInit, Host} from '@angular/core';
-import {EgGridService, EgGridColumn, 
-    EgGridRowSelector, EgGridColumnSet} from './grid.service';
-import {EgGridDataSource} from './grid-data-source';
-import {EgGridComponent} from './grid.component';
-import {Pager} from '@eg/share/util/pager';
+import {Component, Input, OnInit} from '@angular/core';
+import {EgGridContext, EgGridColumn, EgGridRowSelector, 
+    EgGridColumnSet, EgGridDataSource} from './grid';
 
 @Component({
   selector: 'eg-grid-header',
@@ -12,19 +9,12 @@ import {Pager} from '@eg/share/util/pager';
 
 export class EgGridHeaderComponent implements OnInit {
 
-    @Input() columnSet: EgGridColumnSet;
-    @Input() rowSelector: EgGridRowSelector;
-    @Input() dataSource: EgGridDataSource;
-    @Input() pager: Pager;
+    @Input() gridContext: EgGridContext;
     dragColumn: EgGridColumn;
 
-    constructor(
-        private gridSvc: EgGridService,
-        @Host() private grid: EgGridComponent
-    ) { }
+    constructor() {}
 
-    ngOnInit() {
-    }
+    ngOnInit() {}
 
     onColumnDragEnter($event: any, col: any) {
         if (this.dragColumn && this.dragColumn.name != col.name)
@@ -38,51 +28,51 @@ export class EgGridHeaderComponent implements OnInit {
     }
 
     onColumnDrop(col: EgGridColumn) {
-        this.columnSet.insertBefore(this.dragColumn, col);
-        this.columnSet.columns.forEach(c => c.isDragTarget = false);
+        this.gridContext.columnSet.insertBefore(this.dragColumn, col);
+        this.gridContext.columnSet.columns.forEach(c => c.isDragTarget = false);
     }
 
     sortOneColumn(col: EgGridColumn) {
         let dir = 'ASC';
-        let sort = this.dataSource.sort;
+        let sort = this.gridContext.dataSource.sort;
 
         if (sort.length && sort[0].name == col.name && sort[0].dir == 'ASC') {
             dir = 'DESC';
         }
 
-        this.dataSource.sort = [{name: col.name, dir: dir}];
-        this.grid.reload();
+        this.gridContext.dataSource.sort = [{name: col.name, dir: dir}];
+        this.gridContext.reload();
     }
 
     // Returns true if the provided column is sorting in the 
     // specified direction.
     isColumnSorting(col: EgGridColumn, dir: string): boolean {
-        let sort = this.dataSource.sort.filter(c => c.name == col.name)[0];
+        let sort = this.gridContext.dataSource.sort.filter(c => c.name == col.name)[0];
         return sort && sort.dir == dir;
     }
 
     handleBatchSelect($event) {
         if ($event.target.checked) {
-            if (this.rowSelector.isEmpty() || !this.allRowsAreSelected()) {
+            if (this.gridContext.rowSelector.isEmpty() || !this.allRowsAreSelected()) {
                 // clear selections from other pages to avoid confusion.
-                this.rowSelector.clear();
+                this.gridContext.rowSelector.clear();
                 this.selectAll();
             }
         } else {
-            this.rowSelector.clear();
+            this.gridContext.rowSelector.clear();
         }
     }
 
     selectAll() {
-        let rows = this.dataSource.getPageOfRows(this.pager);
-        let indexes = rows.map(r => this.grid.getRowIndex(r));
-        this.rowSelector.select(indexes);
+        let rows = this.gridContext.dataSource.getPageOfRows(this.gridContext.pager);
+        let indexes = rows.map(r => this.gridContext.getRowIndex(r));
+        this.gridContext.rowSelector.select(indexes);
     }
 
     allRowsAreSelected(): boolean {
-        let rows = this.dataSource.getPageOfRows(this.pager);
-        let indexes = rows.map(r => this.grid.getRowIndex(r));
-        return this.rowSelector.contains(indexes);
+        let rows = this.gridContext.dataSource.getPageOfRows(this.gridContext.pager);
+        let indexes = rows.map(r => this.gridContext.getRowIndex(r));
+        return this.gridContext.rowSelector.contains(indexes);
     }
 }
 
index c73337a..eb8abc2 100644 (file)
@@ -1,5 +1,5 @@
 import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
-import {EgGridService, EgGridToolbarAction} from './grid.service';
+import {EgGridToolbarAction} from './grid';
 import {EgGridComponent} from './grid.component';
 
 @Component({
@@ -14,10 +14,7 @@ export class EgGridToolbarActionComponent implements OnInit {
     @Input() action: (rows: any[]) => any;
 
     // get a reference to our container grid.
-    constructor(
-        private gridSvc: EgGridService,
-        @Host() private grid: EgGridComponent) {
-    }
+    constructor(@Host() private grid: EgGridComponent) {}
 
     ngOnInit() {
 
@@ -30,7 +27,7 @@ export class EgGridToolbarActionComponent implements OnInit {
         action.label = this.label;
         action.action = this.action;
 
-        this.grid.toolbarActions.push(action);
+        this.grid.context.toolbarActions.push(action);
     }
 }
 
index c353fc9..1f5f9d1 100644 (file)
@@ -1,5 +1,5 @@
 import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
-import {EgGridService, EgGridToolbarButton} from './grid.service';
+import {EgGridToolbarButton} from './grid';
 import {EgGridComponent} from './grid.component';
 
 @Component({
@@ -14,10 +14,7 @@ export class EgGridToolbarButtonComponent implements OnInit {
     @Input() action: () => any;
 
     // get a reference to our container grid.
-    constructor(
-        private gridSvc: EgGridService,
-        @Host() private grid: EgGridComponent) {
-    }
+    constructor(@Host() private grid: EgGridComponent) {}
 
     ngOnInit() {
 
@@ -30,7 +27,7 @@ export class EgGridToolbarButtonComponent implements OnInit {
         btn.label = this.label;
         btn.action = this.action;
 
-        this.grid.toolbarButtons.push(btn);
+        this.grid.context.toolbarButtons.push(btn);
     }
 }
 
index 316c095..60499a9 100644 (file)
@@ -1,9 +1,10 @@
 
 <div class="eg-grid-toolbar mb-2">
 
-  <div class="btn-toolbar" *ngIf="toolbarButtons.length">
+  <div class="btn-toolbar" *ngIf="gridContext.toolbarButtons.length">
     <div class="btn-grp">
-      <button *ngFor="let btn of toolbarButtons" class="btn btn-outline-dark" (click)="btn.action()">
+      <button *ngFor="let btn of gridContext.toolbarButtons" 
+        class="btn btn-outline-dark" (click)="btn.action()">
         {{btn.label}}
       </button>
     </div>
   <div class="flex-1"></div>
 
   <div ngbDropdown class="ml-1" placement="bottom-right">
-    <button ngbDropdownToggle [disabled]="!toolbarActions.length"
+    <button ngbDropdownToggle [disabled]="!gridContext.toolbarActions.length"
         class="btn btn-outline-dark no-dropdown-caret">
       <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>
       <a class="dropdown-item" (click)="performAction(action)"
-        *ngFor="let action of toolbarActions">
+        *ngFor="let action of gridContext.toolbarActions">
         <span class="ml-2">{{action.label}}</span>
       </a>
     </div>
 
   <div class="btn-toolbar ml-1">
     <div class="btn-grp">
-      <button [disabled]="pager.isFirstPage()" type="button" 
-        class="btn btn-outline-dark" (click)="pager.toFirst()">
+      <button [disabled]="gridContext.pager.isFirstPage()" type="button" 
+        class="btn btn-outline-dark" (click)="gridContext.pager.toFirst()">
         <span title="First Page" i18n-title 
             class="material-icons mat-icon-in-button">first_page</span>
       </button>
-      <button [disabled]="pager.isFirstPage()" type="button" 
-        class="btn btn-outline-dark" (click)="pager.decrement()">
+      <button [disabled]="gridContext.pager.isFirstPage()" type="button" 
+        class="btn btn-outline-dark" (click)="gridContext.pager.decrement()">
         <span title="Previous Page" i18n-title 
             class="material-icons mat-icon-in-button">keyboard_arrow_left</span>
       </button>
-      <button [disabled]="pager.isLastPage()" type="button" 
-        class="btn btn-outline-dark" (click)="pager.increment()">
+      <button [disabled]="gridContext.pager.isLastPage()" type="button" 
+        class="btn btn-outline-dark" (click)="gridContext.pager.increment()">
         <span title="Next Page" i18n-title 
             class="material-icons mat-icon-in-button">keyboard_arrow_right</span>
       </button>
-      <button [disabled]="!pager.resultCount || pager.isLastPage()" 
-        type="button" class="btn btn-outline-dark" (click)="pager.toLast()">
+      <button [disabled]="!gridContext.pager.resultCount || gridContext.pager.isLastPage()" 
+        type="button" class="btn btn-outline-dark" (click)="gridContext.pager.toLast()">
         <span title="First Page" i18n-title 
             class="material-icons mat-icon-in-button">last_page</span>
       </button>
     </div>
   </div>
 
-  <eg-grid-column-config #columnConfDialog [columnSet]="columnSet">
+  <eg-grid-column-config #columnConfDialog [columnSet]="gridContext.columnSet">
   </eg-grid-column-config>
   <div ngbDropdown class="ml-1" placement="bottom-right">
     <button ngbDropdownToggle class="btn btn-outline-dark no-dropdown-caret">
         <span class="ml-2" i18n>Save Columns</span>
       </a>
       <a class="dropdown-item label-with-material-icon" 
-        (click)="columnSet.reset()">
+        (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" 
+        (click)="downloadCsv()">
+        <span class="material-icons">cloud_download</span>
+        <span class="ml-2" i18n>Download CSV</span>
+      </a>
 
       <div class="dropdown-divider"></div>
 
       <a class="dropdown-item label-with-material-icon" 
-        (click)="col.visible=!col.visible" *ngFor="let col of columnSet.columns">
+        (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>
         <span class="ml-2">{{col.label}}</span>
index 8cc95b9..0e8c745 100644 (file)
@@ -1,10 +1,7 @@
 import {Component, Input, OnInit, Host} from '@angular/core';
-import {EgGridDataSource} from './grid-data-source';
 import {Pager} from '@eg/share/util/pager';
-import {EgGridService, EgGridColumn, EgGridColumnSet, EgGridToolbarButton,
-    EgGridToolbarAction} from '@eg/share/grid/grid.service';
-import {EgGridComponent} from './grid.component';
-import {EgDialogComponent} from '@eg/share/dialog/dialog.component';
+import {EgGridColumn, EgGridColumnSet, EgGridToolbarButton, 
+    EgGridToolbarAction, EgGridContext, EgGridDataSource} from '@eg/share/grid/grid';
 import {EgGridColumnWidthComponent} from './grid-column-width.component';
 
 @Component({
@@ -14,28 +11,19 @@ import {EgGridColumnWidthComponent} from './grid-column-width.component';
 
 export class EgGridToolbarComponent implements OnInit {
 
-    @Input() dataSource: EgGridDataSource;
-    @Input() pager: Pager;
-    @Input() toolbarButtons: EgGridToolbarButton[];
-    @Input() toolbarActions: EgGridToolbarAction[];
-    @Input() columnSet: EgGridColumnSet;
+    @Input() gridContext: EgGridContext;
     @Input() colWidthConfig: EgGridColumnWidthComponent;
-    @Input() persistKey: string;
 
-    constructor(private gridSvc: EgGridService,
-        @Host() private grid: EgGridComponent) {
-    }
+    constructor() {}
 
-    ngOnInit() {
-    }
+    ngOnInit() {}
 
     saveColumns() {
         // TODO: when server-side settings are supported, this operation
         // may offer to save to user/workstation OR org unit settings
         // depending on perms.
 
-        this.gridSvc.saveColumns(this.columnSet, this.persistKey)
-        .then(
+        this.gridContext.saveColumns().then(
             // hide the with config after saving
             ok => this.colWidthConfig.isVisible = false,
             err => console.error(`Error saving columns: ${err}`)
@@ -43,8 +31,12 @@ export class EgGridToolbarComponent implements OnInit {
     }
 
     performAction(action: EgGridToolbarAction) {
-        action.action(this.grid.getSelectedRows());
+        action.action(this.gridContext.getSelectedRows());
     }
+
+    downloadCsv() {
+    }
+
 }
 
 
index 8fb597a..4b67986 100644 (file)
@@ -1,15 +1,12 @@
 
 <div class="eg-grid">
-  <eg-grid-toolbar 
-    [dataSource]="dataSource" [pager]="pager" 
-    [columnSet]="columnSet"
-    [toolbarButtons]="toolbarButtons" [toolbarActions]="toolbarActions"
-    [colWidthConfig]="colWidthConfig" persistKey="{{persistKey}}">
+  <eg-grid-toolbar [gridContext]="context" [colWidthConfig]="colWidthConfig">
   </eg-grid-toolbar>
-  <eg-grid-header [columnSet]="columnSet" [pager]="pager"
-    [dataSource]="dataSource" [rowSelector]="rowSelector">
-  </eg-grid-header>
-  <eg-grid-column-width #colWidthConfig [columnSet]="columnSet"></eg-grid-column-width>
+
+  <eg-grid-header [gridContext]="context"></eg-grid-header>
+
+  <eg-grid-column-width #colWidthConfig [gridContext]="context">
+  </eg-grid-column-width>
 
   <div class="row" *ngIf="dataSource.data.length == 0">
     <div class="col-lg-12 text-center alert alert-light font-italic" i18n>
   </div>
 
   <div class="eg-grid-row eg-grid-body-row"
-    [ngClass]="{'selected': rowSelector.contains(getRowIndex(row))}"
-    *ngFor="let row of dataSource.getPageOfRows(pager); let idx = index">
+    [ngClass]="{'selected': context.rowSelector.contains(context.getRowIndex(row))}"
+    *ngFor="let row of dataSource.getPageOfRows(context.pager); let idx = index">
 
     <div class="eg-grid-cell eg-grid-checkbox-cell eg-grid-cell-skinny">
-      <input type='checkbox' [(ngModel)]="rowSelector.indexes[getRowIndex(row)]">
+      <input type='checkbox' [(ngModel)]="context.rowSelector.indexes[context.getRowIndex(row)]">
     </div>
     <div class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny">
-      {{pager.rowNumber(idx)}}
+      {{context.pager.rowNumber(idx)}}
     </div>
     <div class="eg-grid-cell eg-grid-body-cell" [ngStyle]="{flex:col.flex}"
       (dblclick)="onRowDblClick(row)"
       (click)="onRowClick($event, row, idx)"
-      *ngFor="let col of columnSet.displayColumns()">
-      {{getDisplayValue(row, col)}}
+      *ngFor="let col of context.columnSet.displayColumns()">
+      {{context.getRowColumnValue(row, col)}}
     </div>
   <div>
 </div>
index 031694f..817f044 100644 (file)
@@ -1,12 +1,11 @@
 import {Component, Input, OnInit, AfterViewInit, EventEmitter, OnDestroy,
     HostListener, ViewEncapsulation} from '@angular/core';
 import {Subscription} from "rxjs/Subscription";
-import {EgGridDataSource} from './grid-data-source';
 import {EgIdlService} from '@eg/core/idl.service';
 import {EgOrgService} from '@eg/core/org.service';
-import {Pager} from '@eg/share/util/pager';
-import {EgGridService, EgGridColumn, EgGridColumnSet, EgGridToolbarButton, 
-    EgGridRowSelector, EgGridToolbarAction} from '@eg/share/grid/grid.service';
+import {EgStoreService} from '@eg/core/store.service';
+import {EgFormatService} from '@eg/share/util/format.service';
+import {EgGridContext, EgGridColumn, EgGridDataSource} from './grid';
 
 @Component({
   selector: 'eg-grid',
@@ -25,212 +24,91 @@ export class EgGridComponent implements OnInit, AfterViewInit, OnDestroy {
     @Input() persistKey: string;
     @Input() disableMultiSelect: boolean;
 
-    pager: Pager;
-    columnSet: EgGridColumnSet;
-    rowSelector: EgGridRowSelector;
+    context: EgGridContext;
     onRowActivate$: EventEmitter<any>;
     onRowClick$: EventEmitter<any>;
-    toolbarButtons: EgGridToolbarButton[];
-    toolbarActions: EgGridToolbarAction[];
-    lastSelectedIndex: any;
-    pageChanges: Subscription;
     
-    constructor(private gridSvc: EgGridService) {
-        this.pager = new Pager();
-        this.rowSelector = new EgGridRowSelector();
-        this.pager.limit = 10; // TODO config
+    constructor(
+        private idl: EgIdlService,
+        private org: EgOrgService,
+        private store: EgStoreService,
+        private format: EgFormatService
+    ) {
+        this.context = 
+            new EgGridContext(this.idl, this.org, this.store, this.format);
         this.onRowActivate$ = new EventEmitter<any>();
         this.onRowClick$ = new EventEmitter<any>();
-        this.toolbarButtons = [];
-        this.toolbarActions = [];
     }
 
     ngOnInit() {
-        this.columnSet = new EgGridColumnSet(this.idlClass);
-        this.columnSet.isSortable = this.isSortable === true;
-        this.columnSet.isMultiSortable = this.isMultiSortable === true;
-        this.gridSvc.generateColumns(this.columnSet);
+        this.context.idlClass = this.idlClass;
+        this.context.dataSource = this.dataSource;
+        this.context.persistKey = this.persistKey
+        this.context.isSortable = this.isSortable === true;
+        this.context.isMultiSortable = this.isMultiSortable === true;
+        this.context.disableMultiSelect = this.disableMultiSelect === true;
+        this.context.init();
     }
 
-    // Apply column configuation and fetch data after our child
-    // components have told us what columns we have available.
     ngAfterViewInit() {
-        this.gridSvc.getColumnsConfig(this.persistKey)
-        .then(conf => this.columnSet.applyColumnSettings(conf))
-        .then(ok => this.dataSource.requestPage(this.pager))
-        .then(ok => this.listenToPager())
+        this.context.initData();
     }
 
     ngOnDestroy() {
-        this.dontListenToPager();
-    }
-
-    // Subscribe or unsubscribe to page-change events from the pager.
-    listenToPager() {
-        if (this.pageChanges) return;
-        this.pageChanges = this.pager.onChange$.subscribe(
-            val => this.dataSource.requestPage(this.pager));
-    }
-    
-    dontListenToPager() {
-        if (!this.pageChanges) return;
-        this.pageChanges.unsubscribe();
-        this.pageChanges = null
+        this.context.destroy();
     }
 
     // Grid keyboard navigation handlers.
     @HostListener('window:keydown', ['$event']) onKeyDown(evt: KeyboardEvent) {
         if (evt.key == 'ArrowUp') {
-            this.selectPreviousRow();
+            this.context.selectPreviousRow();
 
         } else if (evt.key == 'ArrowDown') {
-            this.selectNextRow();
+            this.context.selectNextRow();
 
         } else if (evt.key == 'ArrowLeft') {
-            this.toPrevPage().then(ok => this.selectFirstRow(), err => {});
+            this.context.toPrevPage()
+                .then(ok => this.context.selectFirstRow(), err => {});
 
         } else if (evt.key == 'ArrowRight') {
-            this.toNextPage().then(ok => this.selectFirstRow(), err => {});
+            this.context.toNextPage()
+                .then(ok => this.context.selectFirstRow(), err => {});
 
         } else if (evt.key == 'Enter') {
-            if (this.lastSelectedIndex)
-                this.onRowDblClick(this.getRowByIndex(this.lastSelectedIndex));
+            if (this.context.lastSelectedIndex) {
+                this.onRowActivate$.emit(
+                    this.context.getRowByIndex(
+                        this.context.lastSelectedIndex)
+                );
+            }
         }
     }
 
     reload() {
-        this.pager.offset = 0;
-        this.dataSource.reset();
-        this.dataSource.requestPage(this.pager);
-    }
-
-    getDisplayValue(row: any, col: EgGridColumn): string {
-        return this.gridSvc.getRowColumnValue(row, col);
-    }
-
-    // Returns the index (AKA pkey) value for the row
-    getRowIndex(row: any): any {
-        return this.gridSvc.getRowIndex(row, this.columnSet);
-    }
-
-    // Returns the array position of the row-by-index in the 
-    // dataSource array.
-    getRowPosition(index: any): number {
-        // for-loop for early exit
-        for (let idx = 0; idx < this.dataSource.data.length; idx++) { 
-            if (index == this.getRowIndex(this.dataSource.data[idx]))
-                return idx;
-        }
-    }
-
-    getRowByIndex(index: any): any {
-        for (let idx = 0; idx < this.dataSource.data.length; idx++) { 
-            if (index == this.getRowIndex(this.dataSource.data[idx]))
-                return this.dataSource.data[idx];
-        }
-    }
-
-    // Returns all selected rows, regardless of whether they are 
-    // currently visible in the grid display.
-    getSelectedRows(): any[] {
-        let selected = [];
-        this.rowSelector.selected().forEach(index => {
-            let row = this.getRowByIndex(index);
-            if (row) selected.push(row);
-        });
-        return selected;
-    }
-
-    onRowDblClick(row: any) {
-        this.onRowActivate$.emit(row);
+        this.context.reload();
     }
 
     onRowClick($event: any, row: any, idx: number) {
-        let index = this.getRowIndex(row);
+        let index = this.context.getRowIndex(row);
 
-        if (this.disableMultiSelect) {
-            this.selectOneRow(index);
+        if (this.context.disableMultiSelect) {
+            this.context.selectOneRow(index);
         } else if ($event.ctrlKey || $event.metaKey /* mac command */) {
-            if (this.toggleSelectOneRow(index)) 
-                this.lastSelectedIndex = index;
+            if (this.context.toggleSelectOneRow(index)) 
+                this.context.lastSelectedIndex = index;
 
         } else if ($event.shiftKey) {
             // TODO shift range click
 
         } else {
-            this.selectOneRow(index);
+            this.context.selectOneRow(index);
         }
 
         this.onRowClick$.emit(row);
     }
 
-    selectOneRow(index: any) {
-        this.rowSelector.clear();
-        this.rowSelector.select(index);
-        this.lastSelectedIndex = index;
-    }
-
-    // selects or deselects an item, without affecting the others.
-    // returns true if the item is selected; false if de-selected.
-    toggleSelectOneRow(index: any) {
-        if (this.rowSelector.contains(index)) {
-            this.rowSelector.deselect(index);
-            return false;
-        } 
-
-        this.rowSelector.select(index);
-        return true;
-    }
-
-    selectRowByPos(pos: number) {
-        let row = this.dataSource.data[pos];
-        if (row) this.selectOneRow(this.getRowIndex(row));
-    }
-
-    selectPreviousRow() {
-        if (!this.lastSelectedIndex) return;
-        let pos = this.getRowPosition(this.lastSelectedIndex);
-        if (pos == this.pager.offset) {
-            this.toPrevPage().then(ok => this.selectLastRow(), err => {});
-        } else {
-            this.selectRowByPos(pos - 1);
-        }
-    }
-
-    selectNextRow() {
-        if (!this.lastSelectedIndex) return;
-        let pos = this.getRowPosition(this.lastSelectedIndex);
-        if (pos == (this.pager.offset + this.pager.limit - 1)) {
-            this.toNextPage().then(ok => this.selectFirstRow(), err => {});
-        } else {
-            this.selectRowByPos(pos + 1);
-        }
-    }
-
-    selectFirstRow() {
-        this.selectRowByPos(this.pager.offset);
-    }
-
-    selectLastRow() {
-        this.selectRowByPos(this.pager.offset + this.pager.limit - 1);
-    }
-
-    toPrevPage(): Promise<any> {
-        if (this.pager.isFirstPage()) return Promise.reject('on first');
-        // temp ignore pager events since we're calling requestPage manually.
-        this.dontListenToPager();
-        this.pager.decrement();
-        this.listenToPager();
-        return this.dataSource.requestPage(this.pager);
-    }
-
-    toNextPage(): Promise<any> {
-        if (this.pager.isLastPage()) return Promise.reject('on last');
-        // temp ignore pager events since we're calling requestPage manually.
-        this.dontListenToPager();
-        this.pager.increment();
-        this.listenToPager();
-        return this.dataSource.requestPage(this.pager);
+    onRowDblClick(row: any) {
+        this.onRowActivate$.emit(row);
     }
 }
 
index 6d36c24..076f43d 100644 (file)
@@ -4,7 +4,6 @@ import {EgGridComponent} from './grid.component';
 import {EgGridColumnComponent} from './grid-column.component';
 import {EgGridHeaderComponent} from './grid-header.component';
 import {EgGridToolbarComponent} from './grid-toolbar.component';
-import {EgGridService} from './grid.service';
 import {EgGridToolbarButtonComponent} from './grid-toolbar-button.component';
 import {EgGridToolbarActionComponent} from './grid-toolbar-action.component';
 import {EgGridColumnConfigComponent} from './grid-column-config.component';
@@ -34,7 +33,6 @@ import {EgGridColumnWidthComponent} from './grid-column-width.component';
         EgGridToolbarActionComponent
     ],
     providers: [
-        EgGridService
     ]
 })
 
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.service.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.service.ts
deleted file mode 100644 (file)
index 196f510..0000000
+++ /dev/null
@@ -1,318 +0,0 @@
-import {Injectable, TemplateRef} from '@angular/core';
-import {EgIdlService, EgIdlObject} from '@eg/core/idl.service';
-import {EgOrgService} from '@eg/core/org.service';
-import {EgPcrudService} from '@eg/core/pcrud.service';
-import {EgStoreService} from '@eg/core/store.service';
-import {EgFormatService} from '@eg/share/util/format.service';
-
-
-@Injectable()
-export class EgGridService {
-
-    constructor(
-        private idl: EgIdlService,
-        private org: EgOrgService,
-        private store: EgStoreService,
-        private pcrud: EgPcrudService,
-        private format: EgFormatService
-    ) {
-    }
-
-    getRowIndex(row: any, columnSet: EgGridColumnSet): any {
-        let col = columnSet.indexColumn;
-        if (!col) throw new Error('grid index column required');
-        return this.getRowColumnValue(row, col);
-    }
-
-    getRowColumnValue(row: any, col: EgGridColumn): string {
-        let val;
-        if (typeof row[col.name] == 'function') {
-            val = row[col.name]();
-        } else {
-            val = row[col.name]
-        }
-        return this.format.transform({value: val, datatype: col.datatype});
-    }
-
-    generateColumns(columnSet: EgGridColumnSet) {
-
-        // generate columns for all non-virtual fields on the IDL class
-        if (columnSet.idlClass) {
-            this.idl.classes[columnSet.idlClass].fields.forEach(field => {
-                if (field.virtual) return;
-                let col = new EgGridColumn();
-                col.name = field.name;
-                col.label = field.label || field.name;
-                col.idlFieldDef = field;
-                col.datatype = field.datatype;
-                if (field.name == this.idl.classes[columnSet.idlClass].pkey)
-                    col.isIndex = true;
-                columnSet.add(col);
-            });
-        }
-
-        return columnSet;
-    }
-
-
-    saveColumns(columnSet: EgGridColumnSet, persistKey: string): Promise<any> {
-        if (!persistKey)
-            throw new Error('Grid persistKey required to save columns');
-        let compiled: EgGridColumnPersistConf[] = columnSet.compileSaveObject();
-        return this.store.setItem('eg.grid.' + persistKey, compiled);
-    }
-
-
-    // TODO: saveColumnsAsOrgSetting(...)
-
-    getColumnsConfig(persistKey: string): Promise<EgGridColumnPersistConf[]> {
-        if (!persistKey) return Promise.resolve([]);
-        return this.store.getItem('eg.grid.' + persistKey);
-    }
-
-}
-
-
-export class EgGridColumn {
-    name: string;
-    path: string;
-    label: string;
-    flex: number;
-    align: string;
-    hidden: boolean;
-    visible: boolean;
-    sort: number;
-    idlClass: string;
-    idlFieldDef: any;
-    datatype: string;
-    cellTemplate: TemplateRef<any>;
-    isIndex: boolean;
-    isDragTarget: boolean;
-    isSortable: boolean;
-    isMultiSortable: boolean;
-}
-
-export class EgGridColumnPersistConf {
-    name: string;
-    flex?: number;
-    sort?: number;
-    align?: string;
-}
-
-export class EgGridColumnSet {
-    columns: EgGridColumn[];
-    idlClass: string;
-    indexColumn: EgGridColumn;
-    isSortable: boolean;
-    isMultiSortable: boolean;
-    stockVisible: string[];
-
-    constructor(idlClass?: string) {
-        this.columns = [];
-        this.stockVisible = [];
-        this.idlClass = idlClass;
-    }
-
-    add(col: EgGridColumn) {
-        // avoid dupes
-        if (this.getColByName(col.name)) return;
-
-        if (col.isIndex) this.indexColumn = col;
-        if (!col.flex) col.flex = 2;
-        if (!col.label) col.label = col.name;
-        if (!col.align) col.align = 'left';
-        if (!col.datatype) col.datatype = 'text';
-
-        col.visible = !col.hidden;
-
-        // track which fields are visible on page load.
-        if (col.visible) this.stockVisible.push(col.name);
-
-        this.applyColumnSortability(col);
-
-        this.columns.push(col);
-    }
-
-    getColByName(name: string): EgGridColumn {
-        return this.columns.filter(c => c.name == name)[0];
-    }
-
-    reset() {
-        this.columns.forEach(col => {
-            col.flex = 2;
-            col.sort = 0;
-            col.align = 'left';
-            col.visible = this.stockVisible.includes(col.name);
-        });
-    }
-
-    applyColumnSortability(col: EgGridColumn) {
-        // column sortability defaults to the sortability of the column set.
-        if (col.isSortable === undefined && this.isSortable)
-            col.isSortable = true;
-
-        if (col.isMultiSortable === undefined && this.isMultiSortable)
-            col.isMultiSortable = true;
-
-        if (col.isMultiSortable) col.isSortable = true;
-    }
-
-    displayColumns(): EgGridColumn[] {
-        return this.columns.filter(c => c.visible);
-    }
-
-    insertBefore(source: EgGridColumn, target: EgGridColumn) {
-        let targetIdx = -1;
-        let sourceIdx = -1;
-        this.columns.forEach((col, idx) => {
-            if (col.name == target.name) targetIdx = idx; });
-
-        this.columns.forEach((col, idx) => {
-            if (col.name == source.name) sourceIdx = idx; });
-
-        if (sourceIdx >= 0)
-            this.columns.splice(sourceIdx, 1);
-
-        this.columns.splice(targetIdx, 0, source);
-    }
-
-    // Move visible columns to the front of the list.
-    moveVisibleToFront() {
-        let newCols = this.displayColumns();
-        this.columns.forEach(col => { if (!col.visible) newCols.push(col) });
-        this.columns = newCols;
-    }
-
-    moveColumn(col: EgGridColumn, diff: number) {
-        var srcIdx, targetIdx;
-
-        this.columns.forEach((c, i) => {
-          if (c.name == col.name) srcIdx = i 
-        });
-
-        targetIdx = srcIdx + diff;
-        if (targetIdx < 0) {
-            targetIdx = 0;
-        } else if (targetIdx >= this.columns.length) {
-            // Target index follows the last visible column.
-            var lastVisible = 0;
-            this.columns.forEach((c, idx) => {
-                if (c.visible) lastVisible = idx;
-            });
-
-            // When moving a column (down) causes one or more
-            // visible columns to shuffle forward, our column
-            // moves into the slot of the last visible column.
-            // Otherwise, put it into the slot directly following 
-            // the last visible column.
-            targetIdx = srcIdx <= lastVisible ? lastVisible : lastVisible + 1;
-        }
-
-        // Splice column out of old position, insert at new position.
-        this.columns.splice(srcIdx, 1);
-        this.columns.splice(targetIdx, 0, col);
-    }
-
-    compileSaveObject(): EgGridColumnPersistConf[] {
-        // only store information about visible columns.
-        let conf = this.displayColumns();
-
-        // scrunch the data down to just the needed info
-        return conf.map(col => {
-            let c: EgGridColumnPersistConf = {name : col.name};
-            if (col.align != 'left') c.align = col.align;
-            if (col.flex != 2) c.flex = col.flex;
-            if (Number(col.sort)) c.sort = Number(c.sort);
-            return c;
-        });
-    }
-
-    applyColumnSettings(conf: EgGridColumnPersistConf[]) {
-        if (!conf || conf.length == 0) return;
-
-        let newCols = [];
-
-        conf.forEach(colConf => {
-            let col = this.getColByName(colConf.name);
-            if (!col) return; // no such column in this grid.
-
-            col.visible = true;
-            if (colConf.align) col.align = colConf.align;
-            if (colConf.flex)  col.flex = colConf.flex;
-            if (colConf.sort)  col.sort = colConf.sort;
-
-            // Add to new columns array, avoid dupes.
-            if (newCols.filter(c => c.name == col.name).length == 0)
-                newCols.push(col);
-        });
-
-        // columns which are not expressed within the saved 
-        // configuration are marked as non-visible and 
-        // appended to the end of the new list of columns.
-        this.columns.forEach(c => {
-            if (conf.filter(cf => cf.name == c.name).length == 0) {
-                c.visible = false;
-                newCols.push(c);
-            }
-        });
-
-        this.columns = newCols;
-    }
-}
-
-// Actions apply to specific rows
-export class EgGridToolbarAction {
-    label: string;
-    action: (rows: any[]) => any;
-}
-
-// Buttons are global actions
-export class EgGridToolbarButton {
-    label: string;
-    action: () => any;
-}
-
-export class EgGridRowSelector {
-    indexes: {[string:string] : boolean};
-
-    constructor() {
-        this.clear();
-    }
-
-    // Returns true if all of the requested indexes exist in the selector.
-    contains(index: string | string[]): boolean {
-        let indexes = [].concat(index);
-        for (let i = 0; i < indexes.length; i++) { // early exit
-            if (!this.indexes[indexes[i]])
-                return false; 
-        }
-        return true;
-    }
-
-    select(index: string | string[]) {
-        let indexes = [].concat(index);
-        indexes.forEach(i => this.indexes[i] = true);
-    }
-
-    deselect(index: string | string[]) {
-        let indexes = [].concat(index);
-        indexes.forEach(i => delete this.indexes[i]);
-    }
-
-    // Returns the list of selected index values.
-    // in some contexts (template checkboxes) the value for an index is 
-    // set to false to deselect instead of having it removed (via deselect()).
-    selected() {
-        return Object.keys(this.indexes).filter(
-            ind => {return Boolean(this.indexes[ind])})
-    }
-
-    isEmpty(): boolean {
-        return this.selected().length == 0;
-    }
-
-    clear() {
-        this.indexes = {};
-    }
-}
-
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
new file mode 100644 (file)
index 0000000..4d1e18d
--- /dev/null
@@ -0,0 +1,554 @@
+/**
+ * Collection of grid related classses and interfaces.
+ */
+import {TemplateRef} from '@angular/core';
+import {Observable} from 'rxjs/Rx';
+import {Subscription} from "rxjs/Subscription";
+import {EgIdlService, EgIdlObject} from '@eg/core/idl.service';
+import {EgOrgService} from '@eg/core/org.service';
+import {EgStoreService} from '@eg/core/store.service';
+import {EgFormatService} from '@eg/share/util/format.service';
+import {Pager} from '@eg/share/util/pager';
+
+export class EgGridContext {
+
+    pager: Pager;
+    idlClass: string;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+    persistKey: string;
+    disableMultiSelect: boolean;
+    dataSource: EgGridDataSource;
+    columnSet: EgGridColumnSet;
+    rowSelector: EgGridRowSelector;
+    toolbarButtons: EgGridToolbarButton[];
+    toolbarActions: EgGridToolbarAction[];
+    lastSelectedIndex: any;
+    pageChanges: Subscription;
+
+    // Services injected by our grid component
+    idl: EgIdlService;
+    org: EgOrgService;
+    store: EgStoreService;
+    format: EgFormatService;
+
+    constructor(
+        idl: EgIdlService, org: EgOrgService,
+        store: EgStoreService, format: EgFormatService) {
+    
+        this.idl = idl;
+        this.org = org;
+        this.store = store;
+        this.format = format;
+        this.pager = new Pager();
+        this.pager.limit = 10; // TODO config
+        this.rowSelector = new EgGridRowSelector();
+        this.toolbarButtons = [];
+        this.toolbarActions = [];
+    }
+
+    init() {
+        this.columnSet = new EgGridColumnSet(this.idlClass);
+        this.columnSet.isSortable = this.isSortable === true;
+        this.columnSet.isMultiSortable = this.isMultiSortable === true;
+        this.generateColumns();
+    }
+
+    // Load initial settings and data.
+    initData() {
+        this.getColumnsConfig(this.persistKey)
+        .then(conf => this.columnSet.applyColumnSettings(conf))
+        .then(ok => this.dataSource.requestPage(this.pager))
+        .then(ok => this.listenToPager())
+    }
+
+    destroy() {
+        this.ignorePager();
+
+    }
+    reload() {
+        this.pager.reset();
+        this.dataSource.reset();
+        this.dataSource.requestPage(this.pager);
+    }
+
+    // Subscribe or unsubscribe to page-change events from the pager.
+    listenToPager() {
+        if (this.pageChanges) return;
+        this.pageChanges = this.pager.onChange$.subscribe(
+            val => this.dataSource.requestPage(this.pager));
+        // TODO: index rows as they arrive
+    }
+
+    ignorePager() {
+        if (!this.pageChanges) return;
+        this.pageChanges.unsubscribe();
+        this.pageChanges = null
+    }
+
+    getRowIndex(row: any): any {
+        let col = this.columnSet.indexColumn;
+        if (!col) throw new Error('grid index column required');
+        return this.getRowColumnValue(row, col);
+    }
+
+    // Returns the array position of the row-by-index in the 
+    // dataSource array.
+    getRowPosition(index: any): number {
+        // for-loop for early exit
+        for (let idx = 0; idx < this.dataSource.data.length; idx++) { 
+            if (index == this.getRowIndex(this.dataSource.data[idx]))
+                return idx;
+        }
+    }
+
+    getRowByIndex(index: any): any {
+        for (let idx = 0; idx < this.dataSource.data.length; idx++) { 
+            if (index == this.getRowIndex(this.dataSource.data[idx]))
+                return this.dataSource.data[idx];
+        }
+    }
+
+    // Returns all selected rows, regardless of whether they are 
+    // currently visible in the grid display.
+    getSelectedRows(): any[] {
+        let selected = [];
+        this.rowSelector.selected().forEach(index => {
+            let row = this.getRowByIndex(index);
+            if (row) selected.push(row);
+        });
+        return selected;
+    }
+
+    getRowColumnValue(row: any, col: EgGridColumn): string {
+        let val;
+        if (typeof row[col.name] == 'function') {
+            val = row[col.name]();
+        } else {
+            val = row[col.name]
+        }
+        return this.format.transform({value: val, datatype: col.datatype});
+    }
+
+    selectOneRow(index: any) {
+        this.rowSelector.clear();
+        this.rowSelector.select(index);
+        this.lastSelectedIndex = index;
+    }
+
+    // selects or deselects an item, without affecting the others.
+    // returns true if the item is selected; false if de-selected.
+    toggleSelectOneRow(index: any) {
+        if (this.rowSelector.contains(index)) {
+            this.rowSelector.deselect(index);
+            return false;
+        } 
+
+        this.rowSelector.select(index);
+        return true;
+    }
+
+    selectRowByPos(pos: number) {
+        let row = this.dataSource.data[pos];
+        if (row) this.selectOneRow(this.getRowIndex(row));
+    }
+
+    selectPreviousRow() {
+        if (!this.lastSelectedIndex) return;
+        let pos = this.getRowPosition(this.lastSelectedIndex);
+        if (pos == this.pager.offset) {
+            this.toPrevPage().then(ok => this.selectLastRow(), err => {});
+        } else {
+            this.selectRowByPos(pos - 1);
+        }
+    }
+
+    selectNextRow() {
+        if (!this.lastSelectedIndex) return;
+        let pos = this.getRowPosition(this.lastSelectedIndex);
+        if (pos == (this.pager.offset + this.pager.limit - 1)) {
+            this.toNextPage().then(ok => this.selectFirstRow(), err => {});
+        } else {
+            this.selectRowByPos(pos + 1);
+        }
+    }
+
+    selectFirstRow() {
+        this.selectRowByPos(this.pager.offset);
+    }
+
+    selectLastRow() {
+        this.selectRowByPos(this.pager.offset + this.pager.limit - 1);
+    }
+
+    toPrevPage(): Promise<any> {
+        if (this.pager.isFirstPage()) return Promise.reject('on first');
+        // temp ignore pager events since we're calling requestPage manually.
+        this.ignorePager();
+        this.pager.decrement();
+        this.listenToPager();
+        return this.dataSource.requestPage(this.pager);
+    }
+
+    toNextPage(): Promise<any> {
+        if (this.pager.isLastPage()) return Promise.reject('on last');
+        // temp ignore pager events since we're calling requestPage manually.
+        this.ignorePager();
+        this.pager.increment();
+        this.listenToPager();
+        return this.dataSource.requestPage(this.pager);
+    }
+
+
+    generateColumns() {
+
+        // generate columns for all non-virtual fields on the IDL class
+        if (this.columnSet.idlClass) {
+            this.idl.classes[this.columnSet.idlClass].fields.forEach(field => {
+                if (field.virtual) return;
+                let col = new EgGridColumn();
+                col.name = field.name;
+                col.label = field.label || field.name;
+                col.idlFieldDef = field;
+                col.datatype = field.datatype;
+                if (field.name == this.idl.classes[this.columnSet.idlClass].pkey)
+                    col.isIndex = true;
+                this.columnSet.add(col);
+            });
+        }
+    }
+
+
+    saveColumns(): Promise<any> {
+        if (!this.persistKey)
+            throw new Error('Grid persistKey required to save columns');
+        let compiled: EgGridColumnPersistConf[] = this.columnSet.compileSaveObject();
+        return this.store.setItem('eg.grid.' + this.persistKey, compiled);
+    }
+
+
+    // TODO: saveColumnsAsOrgSetting(...)
+
+    getColumnsConfig(persistKey: string): Promise<EgGridColumnPersistConf[]> {
+        if (!persistKey) return Promise.resolve([]);
+        return this.store.getItem('eg.grid.' + persistKey);
+    }
+
+}
+
+export class EgGridColumn {
+    name: string;
+    path: string;
+    label: string;
+    flex: number;
+    align: string;
+    hidden: boolean;
+    visible: boolean;
+    sort: number;
+    idlClass: string;
+    idlFieldDef: any;
+    datatype: string;
+    cellTemplate: TemplateRef<any>;
+    isIndex: boolean;
+    isDragTarget: boolean;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+}
+
+export class EgGridColumnPersistConf {
+    name: string;
+    flex?: number;
+    sort?: number;
+    align?: string;
+}
+
+export class EgGridColumnSet {
+    columns: EgGridColumn[];
+    idlClass: string;
+    indexColumn: EgGridColumn;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+    stockVisible: string[];
+
+    constructor(idlClass?: string) {
+        this.columns = [];
+        this.stockVisible = [];
+        this.idlClass = idlClass;
+    }
+
+    add(col: EgGridColumn) {
+        // avoid dupes
+        if (this.getColByName(col.name)) return;
+
+        if (col.isIndex) this.indexColumn = col;
+        if (!col.flex) col.flex = 2;
+        if (!col.label) col.label = col.name;
+        if (!col.align) col.align = 'left';
+        if (!col.datatype) col.datatype = 'text';
+
+        col.visible = !col.hidden;
+
+        // track which fields are visible on page load.
+        if (col.visible) this.stockVisible.push(col.name);
+
+        this.applyColumnSortability(col);
+
+        this.columns.push(col);
+    }
+
+    getColByName(name: string): EgGridColumn {
+        return this.columns.filter(c => c.name == name)[0];
+    }
+
+    reset() {
+        this.columns.forEach(col => {
+            col.flex = 2;
+            col.sort = 0;
+            col.align = 'left';
+            col.visible = this.stockVisible.includes(col.name);
+        });
+    }
+
+    applyColumnSortability(col: EgGridColumn) {
+        // column sortability defaults to the sortability of the column set.
+        if (col.isSortable === undefined && this.isSortable)
+            col.isSortable = true;
+
+        if (col.isMultiSortable === undefined && this.isMultiSortable)
+            col.isMultiSortable = true;
+
+        if (col.isMultiSortable) col.isSortable = true;
+    }
+
+    displayColumns(): EgGridColumn[] {
+        return this.columns.filter(c => c.visible);
+    }
+
+    insertBefore(source: EgGridColumn, target: EgGridColumn) {
+        let targetIdx = -1;
+        let sourceIdx = -1;
+        this.columns.forEach((col, idx) => {
+            if (col.name == target.name) targetIdx = idx; });
+
+        this.columns.forEach((col, idx) => {
+            if (col.name == source.name) sourceIdx = idx; });
+
+        if (sourceIdx >= 0)
+            this.columns.splice(sourceIdx, 1);
+
+        this.columns.splice(targetIdx, 0, source);
+    }
+
+    // Move visible columns to the front of the list.
+    moveVisibleToFront() {
+        let newCols = this.displayColumns();
+        this.columns.forEach(col => { if (!col.visible) newCols.push(col) });
+        this.columns = newCols;
+    }
+
+    moveColumn(col: EgGridColumn, diff: number) {
+        var srcIdx, targetIdx;
+
+        this.columns.forEach((c, i) => {
+          if (c.name == col.name) srcIdx = i 
+        });
+
+        targetIdx = srcIdx + diff;
+        if (targetIdx < 0) {
+            targetIdx = 0;
+        } else if (targetIdx >= this.columns.length) {
+            // Target index follows the last visible column.
+            var lastVisible = 0;
+            this.columns.forEach((c, idx) => {
+                if (c.visible) lastVisible = idx;
+            });
+
+            // When moving a column (down) causes one or more
+            // visible columns to shuffle forward, our column
+            // moves into the slot of the last visible column.
+            // Otherwise, put it into the slot directly following 
+            // the last visible column.
+            targetIdx = srcIdx <= lastVisible ? lastVisible : lastVisible + 1;
+        }
+
+        // Splice column out of old position, insert at new position.
+        this.columns.splice(srcIdx, 1);
+        this.columns.splice(targetIdx, 0, col);
+    }
+
+    compileSaveObject(): EgGridColumnPersistConf[] {
+        // only store information about visible columns.
+        let conf = this.displayColumns();
+
+        // scrunch the data down to just the needed info
+        return conf.map(col => {
+            let c: EgGridColumnPersistConf = {name : col.name};
+            if (col.align != 'left') c.align = col.align;
+            if (col.flex != 2) c.flex = col.flex;
+            if (Number(col.sort)) c.sort = Number(c.sort);
+            return c;
+        });
+    }
+
+    applyColumnSettings(conf: EgGridColumnPersistConf[]) {
+        if (!conf || conf.length == 0) return;
+
+        let newCols = [];
+
+        conf.forEach(colConf => {
+            let col = this.getColByName(colConf.name);
+            if (!col) return; // no such column in this grid.
+
+            col.visible = true;
+            if (colConf.align) col.align = colConf.align;
+            if (colConf.flex)  col.flex = colConf.flex;
+            if (colConf.sort)  col.sort = colConf.sort;
+
+            // Add to new columns array, avoid dupes.
+            if (newCols.filter(c => c.name == col.name).length == 0)
+                newCols.push(col);
+        });
+
+        // columns which are not expressed within the saved 
+        // configuration are marked as non-visible and 
+        // appended to the end of the new list of columns.
+        this.columns.forEach(c => {
+            if (conf.filter(cf => cf.name == c.name).length == 0) {
+                c.visible = false;
+                newCols.push(c);
+            }
+        });
+
+        this.columns = newCols;
+    }
+}
+
+// Actions apply to specific rows
+export class EgGridToolbarAction {
+    label: string;
+    action: (rows: any[]) => any;
+}
+
+// Buttons are global actions
+export class EgGridToolbarButton {
+    label: string;
+    action: () => any;
+}
+
+export class EgGridRowSelector {
+    indexes: {[string:string] : boolean};
+
+    constructor() {
+        this.clear();
+    }
+
+    // Returns true if all of the requested indexes exist in the selector.
+    contains(index: string | string[]): boolean {
+        let indexes = [].concat(index);
+        for (let i = 0; i < indexes.length; i++) { // early exit
+            if (!this.indexes[indexes[i]])
+                return false; 
+        }
+        return true;
+    }
+
+    select(index: string | string[]) {
+        let indexes = [].concat(index);
+        indexes.forEach(i => this.indexes[i] = true);
+    }
+
+    deselect(index: string | string[]) {
+        let indexes = [].concat(index);
+        indexes.forEach(i => delete this.indexes[i]);
+    }
+
+    // Returns the list of selected index values.
+    // in some contexts (template checkboxes) the value for an index is 
+    // set to false to deselect instead of having it removed (via deselect()).
+    selected() {
+        return Object.keys(this.indexes).filter(
+            ind => {return Boolean(this.indexes[ind])})
+    }
+
+    isEmpty(): boolean {
+        return this.selected().length == 0;
+    }
+
+    clear() {
+        this.indexes = {};
+    }
+}
+
+export class EgGridDataSource {
+
+    data: any[];
+    sort: any[];
+    allRowsRetrieved: boolean;
+    getRows: (pager: Pager, sort: any[]) => Observable<any>;
+    indexedRows: {[index:string] : any};
+
+    constructor() {
+        this.sort = [];
+        this.reset();
+    }
+
+    reset() {
+        this.data = [];
+        this.allRowsRetrieved = false;
+    }
+
+    // called from the template -- no data fetching
+    getPageOfRows(pager: Pager): any[] {
+        if (this.data) {
+            return this.data.slice(
+                pager.offset, pager.limit + pager.offset);
+        }
+        return [];
+    }
+
+    // called on initial component load and user action (e.g. paging, sorting).
+    requestPage(pager: Pager): Promise<any> {
+
+        if (
+            // already have the current page
+            this.getPageOfRows(pager).length > 0 
+            // already have all data
+            || this.allRowsRetrieved
+            // have no way to get more data.
+            || !this.getRows
+        ) {
+            return Promise.resolve();
+        }
+
+        return new Promise((resolve, reject) => {
+            let idx = pager.offset;
+            return this.getRows(pager, this.sort).subscribe(
+                row => this.data[idx++] = row,
+                err => {
+                    console.error(`grid getRows() error ${err}`);
+                    reject(err);
+                },
+                ()  => {
+                    this.checkAllRetrieved(pager, idx);
+                    resolve();
+                }
+            );
+        });
+    }
+
+    // See if the last getRows() call resulted in the final set of data.
+    checkAllRetrieved(pager: Pager, idx: number) {
+        if (this.allRowsRetrieved) return;
+
+        if (idx == 0 || idx < (pager.limit + pager.offset)) {
+            // last query returned nothing or less than one page.
+            // confirm we have all of the preceding pages.
+            if (!this.data.includes(undefined)) {
+                this.allRowsRetrieved = true;
+                pager.resultCount = this.data.length;
+            }
+        }
+    }
+}
+
+
index 551984a..f1c2b0a 100644 (file)
@@ -3,17 +3,24 @@ import {DatePipe, CurrencyPipe} from '@angular/common';
 import {EgIdlService, EgIdlObject} from '@eg/core/idl.service';
 import {EgOrgService} from '@eg/core/org.service';
 
+declare var OpenSRF;
+
 export interface EgFormatParams {
     value: any;
     idlClass?: string;
     idlField?: string;
     datatype?: string;
     orgField?: string; // 'shortname' || 'name'
+    datePlusTime?: boolean;
 }
 
 @Injectable()
 export class EgFormatService {
 
+    dateFormat: string = 'shortDate';
+    dateTimeFormat: string = 'short';
+    wsOrgTimezone: string = OpenSRF.tz;
+
     constructor(
         private datePipe: DatePipe,
         private currencyPipe: CurrencyPipe,
@@ -53,8 +60,11 @@ export class EgFormatService {
 
             case 'timestamp':
                 let date = new Date(value);
-                // TODO: date format settings and options.
-                return this.datePipe.transform(date, 'yyyy-MM-dd');
+                let fmt = this.dateFormat || 'shortDate';
+                if (params.datePlusTime) {
+                    fmt = this.dateTimeFormat || 'short';
+                }
+                return this.datePipe.transform(date, fmt);
 
             case 'money':
                 return this.currencyPipe.transform(value);
index 6d420b8..0272dcb 100644 (file)
@@ -14,6 +14,11 @@ export class Pager {
         this.onChange$ = new EventEmitter<number>();
     }
 
+    reset() {
+        this.resultCount = null;
+        this.offset = 0;
+    }
+
     isFirstPage(): boolean {
         return this.offset == 0;
     }
index fd6494f..c529db7 100644 (file)
@@ -1,6 +1,6 @@
 import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core';
 import {EgIdlObject} from '@eg/core/idl.service';
-import {EgGridDataSource} from '@eg/share/grid/grid-data-source';
+import {EgGridDataSource} from '@eg/share/grid/grid';
 import {EgGridComponent} from '@eg/share/grid/grid.component';
 import {EgToastService} from '@eg/share/toast/toast.service';
 import {Pager} from '@eg/share/util/pager';
index 89945b7..d2d73f8 100644 (file)
@@ -1,6 +1,6 @@
 import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core';
 import {EgIdlObject} from '@eg/core/idl.service';
-import {EgGridDataSource} from '@eg/share/grid/grid-data-source';
+import {EgGridDataSource} from '@eg/share/grid/grid';
 import {EgGridComponent} from '@eg/share/grid/grid.component';
 import {EgToastService} from '@eg/share/toast/toast.service';
 import {Pager} from '@eg/share/util/pager';
index 301979f..d15b867 100644 (file)
@@ -7,6 +7,8 @@ import {EgStoreService} from '@eg/core/store.service';
 import {EgNetService} from '@eg/core/net.service';
 import {EgAuthService, EgAuthWsState} from '@eg/core/auth.service';
 import {EgPermService} from '@eg/core/perm.service';
+import {EgOrgService} from '@eg/core/org.service';
+import {EgFormatService} from '@eg/share/util/format.service';
 
 const LOGIN_PATH = '/staff/login';
 const WS_MANAGE_PATH = '/staff/admin/workstation/workstations/manage';
@@ -25,9 +27,11 @@ export class EgStaffResolver implements Resolve<Observable<any>> {
         private route: ActivatedRoute, 
         private ngLocation: Location,
         private store: EgStoreService,
+        private org: EgOrgService,
         private net: EgNetService,
         private auth: EgAuthService,
         private perm: EgPermService,
+        private format: EgFormatService
     ) {}
 
     resolve(
@@ -121,7 +125,19 @@ export class EgStaffResolver implements Resolve<Observable<any>> {
      */
     loadStartupData(): Promise<void> {
         console.debug('EgStaffResolver:loadStartupData()');
-        return Promise.resolve();
+
+        // Fetch settings needed globally.  This will cache the values
+        // in the org service.
+        return this.org.settings([
+            'lib.timezone',
+            'webstaff.format.dates',
+            'webstaff.format.date_and_time',
+            'ui.staff.max_recent_patrons'
+        ]).then(settings => {
+            this.format.wsOrgTimezone = settings['lib.timezone'];
+            this.format.dateFormat = settings['webstaff.format.dates'];
+            this.format.dateTimeFormat = settings['webstaff.format.date_and_time'];
+        });
     }
 }
 
index fe35949..d34e773 100644 (file)
@@ -3,7 +3,7 @@ import {EgProgressDialogComponent} from '@eg/share/dialog/progress.component';
 import {EgToastService} from '@eg/share/toast/toast.service';
 import {EgStringService} from '@eg/share/string/string.service';
 import {Observable} from 'rxjs/Rx';
-import {EgGridDataSource} from '@eg/share/grid/grid-data-source';
+import {EgGridDataSource} from '@eg/share/grid/grid';
 import {EgIdlService, EgIdlObject} from '@eg/core/idl.service';
 import {EgPcrudService} from '@eg/core/pcrud.service';
 import {Pager} from '@eg/share/util/pager';