LP1821382 Angular grid optoin to disable paging
[evergreen-equinox.git] / Open-ILS / src / eg2 / src / app / share / grid / grid.ts
index 8a777e4..64f3321 100644 (file)
@@ -1,9 +1,8 @@
 /**
  * Collection of grid related classses and interfaces.
  */
-import {TemplateRef} from '@angular/core';
-import {Observable} from 'rxjs/Observable';
-import {Subscription} from 'rxjs/Subscription';
+import {TemplateRef, EventEmitter} from '@angular/core';
+import {Observable, Subscription} from 'rxjs';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
@@ -21,6 +20,8 @@ export class GridColumn {
     hidden: boolean;
     visible: boolean;
     sort: number;
+    // IDL class of the object which contains this field.
+    // Not to be confused with the class of a linked object.
     idlClass: string;
     idlFieldDef: any;
     datatype: string;
@@ -31,6 +32,7 @@ export class GridColumn {
     isDragTarget: boolean;
     isSortable: boolean;
     isMultiSortable: boolean;
+    disableTooltip: boolean;
     comparator: (valueA: any, valueB: any) => number;
 
     // True if the column was automatically generated.
@@ -190,6 +192,7 @@ export class GridColumnSet {
             const idlInfo = this.idlInfoFromDotpath(col.path);
             if (idlInfo) {
                 col.idlFieldDef = idlInfo.idlField;
+                col.idlClass = idlInfo.idlClass.name;
                 if (!col.label) {
                     col.label = col.idlFieldDef.label || col.idlFieldDef.name;
                     col.datatype = col.idlFieldDef.datatype;
@@ -397,7 +400,7 @@ export class GridRowSelector {
 
 export interface GridRowFlairEntry {
     icon: string;   // name of material icon
-    title: string;  // tooltip string
+    title?: string;  // tooltip string
 }
 
 export class GridColumnPersistConf {
@@ -422,6 +425,7 @@ export class GridContext {
     useLocalSort: boolean;
     persistKey: string;
     disableMultiSelect: boolean;
+    disableSelect: boolean;
     dataSource: GridDataSource;
     columnSet: GridColumnSet;
     rowSelector: GridRowSelector;
@@ -437,6 +441,12 @@ export class GridContext {
     defaultVisibleFields: string[];
     defaultHiddenFields: string[];
     overflowCells: boolean;
+    showLinkSelectors: boolean;
+    disablePaging: boolean;
+
+    // Allow calling code to know when the select-all-rows-in-page
+    // action has occurred.
+    selectRowsInPageEmitter: EventEmitter<void>;
 
     // Services injected by our grid component
     idl: IdlService;
@@ -455,7 +465,6 @@ export class GridContext {
         this.store = store;
         this.format = format;
         this.pager = new Pager();
-        this.pager.limit = 10;
         this.rowSelector = new GridRowSelector();
         this.toolbarButtons = [];
         this.toolbarCheckboxes = [];
@@ -463,11 +472,15 @@ export class GridContext {
     }
 
     init() {
+        this.selectRowsInPageEmitter = new EventEmitter<void>();
         this.columnSet = new GridColumnSet(this.idl, this.idlClass);
         this.columnSet.isSortable = this.isSortable === true;
         this.columnSet.isMultiSortable = this.isMultiSortable === true;
         this.columnSet.defaultHiddenFields = this.defaultHiddenFields;
         this.columnSet.defaultVisibleFields = this.defaultVisibleFields;
+        if (!this.pager.limit) {
+            this.pager.limit = this.disablePaging ? MAX_ALL_ROW_COUNT : 10;
+        }
         this.generateColumns();
     }
 
@@ -538,18 +551,34 @@ export class GridContext {
     sortLocalData() {
 
         const sortDefs = this.dataSource.sort.map(sort => {
+            const column = this.columnSet.getColByName(sort.name);
+
             const def = {
                 name: sort.name,
                 dir: sort.dir,
-                col: this.columnSet.getColByName(sort.name)
+                col: column
             };
 
             if (!def.col.comparator) {
-                def.col.comparator = (a, b) => {
-                    if (a < b) { return -1; }
-                    if (a > b) { return 1; }
-                    return 0;
-                };
+                switch (def.col.datatype) {
+                    case 'id':
+                    case 'money':
+                    case 'int':
+                        def.col.comparator = (a, b) => {
+                            a = Number(a);
+                            b = Number(b);
+                            if (a < b) { return -1; }
+                            if (a > b) { return 1; }
+                            return 0;
+                        };
+                        break;
+                    default:
+                        def.col.comparator = (a, b) => {
+                            if (a < b) { return -1; }
+                            if (a > b) { return 1; }
+                            return 0;
+                        };
+                }
             }
 
             return def;
@@ -570,8 +599,6 @@ export class GridContext {
                 const diff = sortDef.col.comparator(valueA, valueB);
                 if (diff === 0) { continue; }
 
-                console.log(valueA, valueB, diff);
-
                 return sortDef.dir === 'DESC' ? -diff : diff;
             }
 
@@ -624,16 +651,17 @@ export class GridContext {
 
     getRowColumnValue(row: any, col: GridColumn): string {
         let val;
-        if (col.name in row) {
+
+        if (col.path) {
+            val = this.nestedItemFieldValue(row, col);
+        } else if (col.name in row) {
             val = this.getObjectFieldValue(row, col.name);
-        } else {
-            if (col.path) {
-                val = this.nestedItemFieldValue(row, col);
-            }
         }
 
         return this.format.transform({
             value: val,
+            idlClass: col.idlClass,
+            idlField: col.idlFieldDef ? col.idlFieldDef.name : col.name,
             datatype: col.datatype,
             datePlusTime: Boolean(col.datePlusTime)
         });
@@ -657,7 +685,7 @@ export class GridContext {
         for (let i = 0; i < steps.length; i++) {
             const step = steps[i];
 
-            if (typeof obj !== 'object') {
+            if (obj === null || obj === undefined || typeof obj !== 'object') {
                 // We have run out of data to step through before
                 // reaching the end of the path.  Conclude fleshing via
                 // callback if provided then exit.
@@ -682,6 +710,12 @@ export class GridContext {
             if (!col.datatype) {
                 col.datatype = idlField.datatype;
             }
+            if (!col.idlFieldDef) {
+                idlField = col.idlFieldDef;
+            }
+            if (!col.idlClass) {
+                col.idlClass = idlClassDef.name;
+            }
             if (!col.label) {
                 col.label = idlField.label || idlField.name;
             }
@@ -706,6 +740,12 @@ export class GridContext {
         this.lastSelectedIndex = index;
     }
 
+    selectMultipleRows(indexes: any[]) {
+        this.rowSelector.clear();
+        this.rowSelector.select(indexes);
+        this.lastSelectedIndex = indexes[indexes.length - 1];
+    }
+
     // selects or deselects an item, without affecting the others.
     // returns true if the item is selected; false if de-selected.
     toggleSelectOneRow(index: any) {
@@ -745,12 +785,84 @@ export class GridContext {
         }
     }
 
+    // shift-up-arrow
+    // Select the previous row in addition to any currently selected row.
+    // However, if the previous row is already selected, assume the user
+    // has reversed direction and now wants to de-select the last selected row.
+    selectMultiRowsPrevious() {
+        if (!this.lastSelectedIndex) { return; }
+        const pos = this.getRowPosition(this.lastSelectedIndex);
+        const selectedIndexes = this.rowSelector.selected();
+
+        const promise = // load the previous page of data if needed
+            (pos === this.pager.offset) ? this.toPrevPage() : Promise.resolve();
+
+        promise.then(
+            ok => {
+                const row = this.dataSource.data[pos - 1];
+                const newIndex = this.getRowIndex(row);
+                if (selectedIndexes.filter(i => i === newIndex).length > 0) {
+                    // Prev row is already selected.  User is reversing direction.
+                    this.rowSelector.deselect(this.lastSelectedIndex);
+                    this.lastSelectedIndex = newIndex;
+                } else {
+                    this.selectMultipleRows(selectedIndexes.concat(newIndex));
+                }
+            },
+            err => {}
+        );
+    }
+
+    // shift-down-arrow
+    // Select the next row in addition to any currently selected row.
+    // However, if the next row is already selected, assume the user
+    // has reversed direction and wants to de-select the last selected row.
+    selectMultiRowsNext() {
+        if (!this.lastSelectedIndex) { return; }
+        const pos = this.getRowPosition(this.lastSelectedIndex);
+        const selectedIndexes = this.rowSelector.selected();
+
+        const promise = // load the next page of data if needed
+            (pos === (this.pager.offset + this.pager.limit - 1)) ?
+            this.toNextPage() : Promise.resolve();
+
+        promise.then(
+            ok => {
+                const row = this.dataSource.data[pos + 1];
+                const newIndex = this.getRowIndex(row);
+                if (selectedIndexes.filter(i => i === newIndex).length > 0) {
+                    // Next row is already selected.  User is reversing direction.
+                    this.rowSelector.deselect(this.lastSelectedIndex);
+                    this.lastSelectedIndex = newIndex;
+                } else {
+                    this.selectMultipleRows(selectedIndexes.concat(newIndex));
+                }
+            },
+            err => {}
+        );
+    }
+
+    getFirstRowInPage(): any {
+        return this.dataSource.data[this.pager.offset];
+    }
+
+    getLastRowInPage(): any {
+        return this.dataSource.data[this.pager.offset + this.pager.limit - 1];
+    }
+
     selectFirstRow() {
-        this.selectRowByPos(this.pager.offset);
+        this.selectOneRow(this.getRowIndex(this.getFirstRowInPage()));
     }
 
     selectLastRow() {
-        this.selectRowByPos(this.pager.offset + this.pager.limit - 1);
+        this.selectOneRow(this.getRowIndex(this.getLastRowInPage()));
+    }
+
+    selectRowsInPage() {
+        const rows = this.dataSource.getPageOfRows(this.pager);
+        const indexes = rows.map(r => this.getRowIndex(r));
+        this.rowSelector.select(indexes);
+        this.selectRowsInPageEmitter.emit();
     }
 
     toPrevPage(): Promise<any> {
@@ -858,9 +970,19 @@ export class GridContext {
             col.name = field.name;
             col.label = field.label || field.name;
             col.idlFieldDef = field;
+            col.idlClass = this.columnSet.idlClass;
             col.datatype = field.datatype;
             col.isIndex = (field.name === pkeyField);
             col.isAuto = true;
+
+            if (this.showLinkSelectors) {
+                const selector = this.idl.getLinkSelector(
+                    this.columnSet.idlClass, field.name);
+                if (selector) {
+                    col.path = field.name + '.' + selector;
+                }
+            }
+
             this.columnSet.add(col);
         });
     }
@@ -889,20 +1011,25 @@ export class GridContext {
 // Actions apply to specific rows
 export class GridToolbarAction {
     label: string;
-    action: (rows: any[]) => any;
+    onClick: EventEmitter<any []>;
+    action: (rows: any[]) => any; // DEPRECATED
+    group: string;
+    isGroup: boolean; // used for group placeholder entries
     disableOnRows: (rows: any[]) => boolean;
 }
 
 // Buttons are global actions
 export class GridToolbarButton {
     label: string;
-    action: () => any;
+    onClick: EventEmitter<any []>;
+    action: () => any; // DEPRECATED
     disabled: boolean;
 }
 
 export class GridToolbarCheckbox {
     label: string;
-    onChange: (checked: boolean) => void;
+    isChecked: boolean;
+    onChange: EventEmitter<boolean>;
 }
 
 export class GridDataSource {
@@ -910,6 +1037,7 @@ export class GridDataSource {
     data: any[];
     sort: any[];
     allRowsRetrieved: boolean;
+    requestingData: boolean;
     getRows: (pager: Pager, sort: any[]) => Observable<any>;
 
     constructor() {
@@ -945,16 +1073,23 @@ export class GridDataSource {
             return Promise.resolve();
         }
 
+        // If we have to call out for data, set inFetch
+        this.requestingData = true;
+
         return new Promise((resolve, reject) => {
             let idx = pager.offset;
             return this.getRows(pager, this.sort).subscribe(
-                row => this.data[idx++] = row,
+                row => {
+                    this.data[idx++] = row;
+                    this.requestingData = false;
+                },
                 err => {
                     console.error(`grid getRows() error ${err}`);
                     reject(err);
                 },
                 ()  => {
                     this.checkAllRetrieved(pager, idx);
+                    this.requestingData = false;
                     resolve();
                 }
             );