diff --git a/client-app/src/desktop/AppModel.ts b/client-app/src/desktop/AppModel.ts index c6f3add49..50beb22d8 100755 --- a/client-app/src/desktop/AppModel.ts +++ b/client-app/src/desktop/AppModel.ts @@ -130,6 +130,7 @@ export class AppModel extends BaseAppModel { {name: 'rest', path: '/rest'}, {name: 'inlineEditing', path: '/inlineEditing'}, {name: 'columnFiltering', path: '/columnFiltering'}, + {name: 'externalSort', path: '/externalSort'}, {name: 'zoneGrid', path: '/zoneGrid'}, {name: 'dataview', path: '/dataview'}, {name: 'agGrid', path: '/agGrid'} diff --git a/client-app/src/desktop/tabs/grids/ExternalSortGridPanel.tsx b/client-app/src/desktop/tabs/grids/ExternalSortGridPanel.tsx new file mode 100644 index 000000000..7e9e626d1 --- /dev/null +++ b/client-app/src/desktop/tabs/grids/ExternalSortGridPanel.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import {creates, hoistCmp} from '@xh/hoist/core'; +import {hframe, filler, span} from '@xh/hoist/cmp/layout'; +import {grid, gridCountLabel} from '@xh/hoist/cmp/grid'; +import {storeFilterField} from '@xh/hoist/cmp/store'; +import {colChooserButton, exportButton, refreshButton} from '@xh/hoist/desktop/cmp/button'; +import {select} from '@xh/hoist/desktop/cmp/input'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; +import {Icon} from '@xh/hoist/icon'; +import {ExternalSortGridPanelModel} from './ExternalSortGridPanelModel'; +import {wrapper} from '../../common'; +import {gridOptionsPanel} from '../../common/grid/options/GridOptionsPanel'; + +export const externalSortGridPanel = hoistCmp.factory({ + model: creates(ExternalSortGridPanelModel), + render() { + return wrapper({ + description: [ +

+ Grids can optionally manage their sort externally. In the below example, we + react to + GridModel.sortBy to offload sorting to external logic. Sorted rows + can be limited after sorting, facilitating showing a subset of large datasets. +

, +

This pattern could be used to similarly offload sorting to the server.

+ ], + item: panel({ + title: 'Grids › External Sort', + icon: Icon.gridPanel(), + className: 'tb-grid-wrapper-panel tb-external-sort-panel', + mask: 'onLoad', + tbar: tbar(), + item: hframe(grid(), gridOptionsPanel()) + }) + }); + } +}); + +const tbar = hoistCmp.factory(() => { + return toolbar( + refreshButton(), + toolbarSep(), + span('Max rows:'), + select({ + bind: 'maxRows', + options: [ + {value: null, label: 'None'}, + {value: 50, label: '50'}, + {value: 100, label: '100'}, + {value: 500, label: '500'} + ], + width: 100, + enableFilter: false + }), + filler(), + gridCountLabel({unit: 'companies'}), + storeFilterField(), + colChooserButton(), + exportButton() + ); +}); diff --git a/client-app/src/desktop/tabs/grids/ExternalSortGridPanelModel.ts b/client-app/src/desktop/tabs/grids/ExternalSortGridPanelModel.ts new file mode 100644 index 000000000..e955bd220 --- /dev/null +++ b/client-app/src/desktop/tabs/grids/ExternalSortGridPanelModel.ts @@ -0,0 +1,127 @@ +import {XH, HoistModel, managed, LoadSpec, PlainObject} from '@xh/hoist/core'; +import {bindable, makeObservable} from '@xh/hoist/mobx'; +import {GridModel, localDateCol, ExcelFormat} from '@xh/hoist/cmp/grid'; +import {fmtNumberTooltip, millionsRenderer, numberRenderer} from '@xh/hoist/format'; + +export class ExternalSortGridPanelModel extends HoistModel { + @managed gridModel: GridModel; + @bindable.ref trades: PlainObject[]; + @bindable maxRows: number = null; + + constructor() { + super(); + makeObservable(this); + + this.gridModel = this.createGridModel(); + + this.addReaction({ + track: () => [this.trades, this.maxRows, this.gridModel.sortBy], + run: () => this.sortAndLoadGridAsync() + }); + } + + override async doLoadAsync(loadSpec: LoadSpec) { + const {trades} = await XH.fetchJson({url: 'trade'}); + this.trades = trades; + } + + //------------------------ + // Implementation + //------------------------ + createGridModel(): GridModel { + return new GridModel({ + externalSort: true, + sortBy: 'profit_loss|desc|abs', + emptyText: 'No records found...', + colChooserModel: true, + enableExport: true, + exportOptions: { + columns: ['id', 'company', 'VISIBLE'], + filename: 'hoist-sample-export' + }, + columns: [ + { + field: 'id', + hidden: true + }, + { + field: 'company', + flex: 1, + minWidth: 200, + headerName: ({gridModel}) => { + let ret = 'Company'; + if (gridModel.selectedRecord) { + ret += ` (${gridModel.selectedRecord.data.company})`; + } + + return ret; + }, + exportName: 'Company', + headerTooltip: 'Select a company & continue' + }, + { + field: 'city', + minWidth: 150, + maxWidth: 200, + tooltip: (val, {record}) => `${record.data.company} is located in ${val}`, + cellClass: val => { + return val === 'New York' ? 'xh-text-color-accent' : ''; + } + }, + { + field: 'trade_volume', + width: 150, + tooltip: val => fmtNumberTooltip(val), + renderer: millionsRenderer({ + precision: 1, + label: true + }), + excelFormat: ExcelFormat.NUM_DELIMITED, + chooserDescription: 'Daily Volume of Shares (Estimated, avg. YTD)' + }, + { + field: 'profit_loss', + width: 150, + absSort: true, + tooltip: val => fmtNumberTooltip(val, {ledger: true}), + renderer: numberRenderer({ + precision: 0, + ledger: true, + colorSpec: true + }), + excelFormat: ExcelFormat.LEDGER_COLOR, + chooserDescription: 'Annual Profit & Loss YTD (EBITDA)' + }, + { + field: 'trade_date', + ...localDateCol, + width: 150, + chooserDescription: 'Date of last trade (including related derivatives)' + } + ] + }); + } + + async sortAndLoadGridAsync(): Promise { + const {trades, maxRows, gridModel} = this, + {sortBy} = gridModel; + + let data = [...trades]; + + // Sort according to GridModel.sortBy[] + sortBy.forEach(it => { + const compFn = it.comparator.bind(it), + direction = it.sort === 'desc' ? -1 : 1; + + data.sort((a, b) => compFn(a[it.colId], b[it.colId]) * direction); + }); + + // Limit sorted rows by maxRows + if (maxRows) { + data = data.slice(0, maxRows); + } + + gridModel.loadData(data); + await gridModel.preSelectFirstAsync(); + } +} diff --git a/client-app/src/desktop/tabs/grids/GridsTab.ts b/client-app/src/desktop/tabs/grids/GridsTab.ts index 13060a769..68c1e9e82 100644 --- a/client-app/src/desktop/tabs/grids/GridsTab.ts +++ b/client-app/src/desktop/tabs/grids/GridsTab.ts @@ -1,6 +1,7 @@ import {hoistCmp} from '@xh/hoist/core'; import {tabContainer} from '@xh/hoist/cmp/tab'; import {standardGridPanel} from './StandardGridPanel'; +import {externalSortGridPanel} from './ExternalSortGridPanel'; import {columnGroupsGridPanel} from './ColumnGroupsGridPanel'; import {restGridPanel} from './RestGridPanel'; import {dataViewPanel} from './DataViewPanel'; @@ -29,6 +30,7 @@ export const gridsTab = hoistCmp.factory(() => content: treeGridWithCheckboxPanel }, {id: 'groupedCols', title: 'Grouped Columns', content: columnGroupsGridPanel}, + {id: 'externalSort', content: externalSortGridPanel}, {id: 'rest', title: 'REST Editor', content: restGridPanel}, {id: 'agGrid', title: 'ag-Grid Wrapper', content: agGridView} ],