From 047c9c6f3dda5d4de5428da3689e92d5c0eddecb Mon Sep 17 00:00:00 2001 From: jacob-xhio <163027616+jacob-xhio@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:24:16 -0700 Subject: [PATCH] Simple Routing Panel in the "Other" tab (#703) * Add example of simple route-based management of a single parameter (in this case, grid selection). --------- Co-authored-by: Anselm McClain --- client-app/src/desktop/AppModel.ts | 7 +- client-app/src/desktop/tabs/other/OtherTab.ts | 4 +- .../tabs/other/routing/SimpleRoutingPanel.tsx | 110 ++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 client-app/src/desktop/tabs/other/routing/SimpleRoutingPanel.tsx diff --git a/client-app/src/desktop/AppModel.ts b/client-app/src/desktop/AppModel.ts index d2adf3bf5..c6f3add49 100755 --- a/client-app/src/desktop/AppModel.ts +++ b/client-app/src/desktop/AppModel.ts @@ -179,7 +179,12 @@ export class AppModel extends BaseAppModel { {name: 'pinPad', path: '/pinPad'}, {name: 'placeholder', path: '/placeholder'}, {name: 'popups', path: '/popups'}, - {name: 'timestamp', path: '/timestamp'} + {name: 'timestamp', path: '/timestamp'}, + { + name: 'simpleRouting', + path: '/simpleRouting', + children: [{name: 'recordId', path: '/:recordId'}] + } ] }, { diff --git a/client-app/src/desktop/tabs/other/OtherTab.ts b/client-app/src/desktop/tabs/other/OtherTab.ts index 01a4d563d..873c5a7c0 100644 --- a/client-app/src/desktop/tabs/other/OtherTab.ts +++ b/client-app/src/desktop/tabs/other/OtherTab.ts @@ -17,6 +17,7 @@ import {pinPadPanel} from './PinPadPanel'; import {placeholderPanel} from './PlaceholderPanel'; import {popupsPanel} from './PopupsPanel'; import {relativeTimestampPanel} from './relativetimestamp/RelativeTimestampPanel'; +import {simpleRoutingPanel} from './routing/SimpleRoutingPanel'; export const otherTab = hoistCmp.factory(() => tabContainer({ @@ -44,7 +45,8 @@ export const otherTab = hoistCmp.factory(() => {id: 'pinPad', title: 'PIN Pad', content: pinPadPanel}, {id: 'placeholder', title: 'Placeholder', content: placeholderPanel}, {id: 'popups', content: popupsPanel}, - {id: 'timestamp', content: relativeTimestampPanel} + {id: 'timestamp', content: relativeTimestampPanel}, + {id: 'simpleRouting', content: simpleRoutingPanel} ] }, className: 'toolbox-tab' diff --git a/client-app/src/desktop/tabs/other/routing/SimpleRoutingPanel.tsx b/client-app/src/desktop/tabs/other/routing/SimpleRoutingPanel.tsx new file mode 100644 index 000000000..b7e811734 --- /dev/null +++ b/client-app/src/desktop/tabs/other/routing/SimpleRoutingPanel.tsx @@ -0,0 +1,110 @@ +import {grid, GridModel} from '@xh/hoist/cmp/grid'; +import {creates, hoistCmp, HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {Icon} from '@xh/hoist/icon'; +import React from 'react'; +import {wrapper} from '../../../common'; + +export const simpleRoutingPanel = hoistCmp.factory({ + displayName: 'SimpleRoutingPanel', + model: creates(() => new SimpleRoutingPanelModel()), + + render({model}) { + const routedUrl = `${window.location.origin}/app/other/simpleRouting/123`; + return wrapper({ + description: [ +

+ Hoist provides functionality for route parameters to interact with UI + components. The grid below has its selected record synced with a routable URL. +

, +

+ Given a URL such as {routedUrl}, where 123{' '} + is a record ID, we can auto-select the matching record in the grid. Updates to + application state can be pushed back to the URL - try selecting a different + record in the grid and observe the URL change. +

, +

+ Note that this routing relies on an appropriate route path being defined in the + config returned by AppModel.getRoutes(). +

+ ], + item: panel({ + title: 'Simple Routing', + icon: Icon.gridPanel(), + item: grid(), + height: 500, + width: 700 + }) + }); + } +}); + +@managed +class SimpleRoutingPanelModel extends HoistModel { + private readonly BASE_ROUTE = 'default.other.simpleRouting'; + + @managed gridModel = new GridModel({ + columns: [{field: 'id'}, {field: 'company', flex: 1}] + }); + + constructor() { + super(); + this.addReaction( + { + // Track lastLoadCompleted to sync route -> grid after initial load. + track: () => [XH.routerState.params, this.lastLoadCompleted], + run: () => this.updateGridFromRoute() + }, + { + track: () => this.gridModel.selectedId, + run: () => this.updateRouteFromGrid() + } + ); + } + + async updateGridFromRoute() { + const {gridModel, BASE_ROUTE} = this, + {name: currRouteName, params} = XH.routerState, + {recordId} = params; + + // No-op if not on the current base route. + if (!currRouteName.startsWith(BASE_ROUTE)) return; + + if (recordId) { + await gridModel.selectAsync(Number(recordId)); + + // Check and alert if requested record not found, and clean up route to match. + if (!gridModel.selectedRecord) { + XH.dangerToast(`Record ${recordId} not found.`); + XH.navigate(BASE_ROUTE, {replace: true}); + } + } else { + gridModel.clearSelection(); + } + } + + updateRouteFromGrid() { + const {gridModel, BASE_ROUTE} = this, + {name: currRouteName, params} = XH.routerState, + {selectedId} = gridModel, + {recordId} = params; + + // No-op if not on the current base route, or if route and selection already match. + if (!currRouteName.startsWith(BASE_ROUTE) || recordId === selectedId) return; + + if (selectedId) { + XH.navigate( + 'default.other.simpleRouting.recordId', + {recordId: selectedId}, + {replace: true} // avoids adding steps to browser history + ); + } else { + XH.navigate('default.other.simpleRouting', {replace: true}); + } + } + + override async doLoadAsync(loadSpec: LoadSpec) { + const {trades} = await XH.fetchJson({url: 'trade', loadSpec}); + this.gridModel.loadData(trades); + } +}