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}
],