Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advanced Routing Panel in "Other" tab #705

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions client-app/src/desktop/AppModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {panelsTab} from './tabs/panels/PanelsTab';
import {fmtDateTimeSec} from '@xh/hoist/format';
import {span} from '@xh/hoist/cmp/layout';
import {BaseAppModel} from '../BaseAppModel';
import {isEmpty} from 'lodash';

export class AppModel extends BaseAppModel {
/** Singleton instance reference - installed by XH upon init. */
Expand All @@ -47,6 +48,8 @@ export class AppModel extends BaseAppModel {
override async initAsync() {
await super.initAsync();
await XH.installServicesAsync(GitHubService, PortfolioService);
// Set the queryParamsMode to 'loose' to allow for more flexible URL query parameters.
XH.router.setOption('queryParamsMode', 'loose');
jacob-xhio marked this conversation as resolved.
Show resolved Hide resolved

// Demo app-specific handling of EnvironmentService.serverVersion observable.
this.addReaction({
Expand Down Expand Up @@ -184,6 +187,11 @@ export class AppModel extends BaseAppModel {
name: 'simpleRouting',
path: '/simpleRouting',
children: [{name: 'recordId', path: '/:recordId'}]
},
{
name: 'advancedRouting',
path: '/advancedRouting',
...routeParamEncoders
}
]
},
Expand All @@ -210,3 +218,15 @@ export class AppModel extends BaseAppModel {
];
}
}

// Encoding of json route params as base64
export const routeParamEncoders = {
encodeParams: params => {
if (isEmpty(params)) return {};
return {q: window.btoa(JSON.stringify(params))};
},
decodeParams: params => {
if (!params.q) return {};
return JSON.parse(window.atob(params.q));
}
};
6 changes: 4 additions & 2 deletions client-app/src/desktop/tabs/other/OtherTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {placeholderPanel} from './PlaceholderPanel';
import {popupsPanel} from './PopupsPanel';
import {relativeTimestampPanel} from './relativetimestamp/RelativeTimestampPanel';
import {simpleRoutingPanel} from './routing/SimpleRoutingPanel';
import {advancedRoutingPanel} from './routing/AdvancedRoutingPanel';

export const otherTab = hoistCmp.factory(() =>
tabContainer({
Expand Down Expand Up @@ -45,8 +46,9 @@ 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: 'simpleRouting', content: simpleRoutingPanel}
{id: 'simpleRouting', title: 'Routing (Simple)', content: simpleRoutingPanel},
{id: 'advancedRouting', title: 'Routing (Advanced)', content: advancedRoutingPanel},
{id: 'timestamp', content: relativeTimestampPanel}
]
},
className: 'toolbox-tab'
Expand Down
203 changes: 203 additions & 0 deletions client-app/src/desktop/tabs/other/routing/AdvancedRoutingPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import {grid, GridModel} from '@xh/hoist/cmp/grid';
import {span} from '@xh/hoist/cmp/layout';
import {creates, hoistCmp, HoistModel, XH} from '@xh/hoist/core';
import {select, switchInput} from '@xh/hoist/desktop/cmp/input';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {action, makeObservable, observable} from '@xh/hoist/mobx';
import React from 'react';
import {State} from 'router5';
import {wrapper} from '../../../common';

export const advancedRoutingPanel = hoistCmp.factory({
displayName: 'AdvancedRoutingPanel',
model: creates(() => new AdvancedRoutingPanelModel()),

render({model}) {
return wrapper({
description: [
<p>
This example demonstrates how to use URL route parameters to store and restore
the state of a component. The state of the grid (grouping, sorting, and selected
record) is stored in the URL, and the state is restored when the URL is
revisited.
</p>,
<p>
Hoist applications are able to navigate to a specific URL and specify whether or
not to push onto the route history. In this example, selecting individual
records in the grid will not save the URL to the route history, but changing the{' '}
<code>groupBy</code> or <code>sortBy</code> fields will. Hoist also provides the
ability to prevent route deactivation, allowing the developer to present the
user with a pop-up before navigating away from the current route.
</p>,
<p>
The state is encoded in the URL as a <code>base64</code> string, which is then
decoded and parsed to restore the state.
</p>,
<p>
The current state encoding is: <br />
<br />
<code>groupBy: {XH.routerState.params.groupBy || 'None'}</code>
<br />
<code>sortBy: {XH.routerState.params.sortBy || 'None'}</code>
<br />
<code>selectedId: {XH.routerState.params.selectedId || 'None'}</code>
<br />
</p>,
<p></p>
],
item: panel({
ref: model.panelRef,
mask: 'onLoad',
item: grid(),
tbar: [
span('Group by:'),
select({
bind: 'groupBy',
options: [
{value: 'city', label: 'City'},
{value: 'trade_date', label: 'Trade Date'},
{value: 'city,trade_date', label: 'City › Trade Date'},
{value: null, label: 'None'}
],
width: 160
}),
span('Sort by:'),
select({
bind: 'sortBy',
options: [
{value: 'id|desc', label: 'Company ID (Desc)'},
{value: 'id|asc', label: 'Company ID (Asc)'},
{value: 'company|desc', label: 'Company Name (Desc)'},
{value: 'company|asc', label: 'Company Name (Asc)'},
{value: 'city|desc', label: 'City (Desc)'},
{value: 'city|asc', label: 'City (Asc)'},
{value: 'trade_date|desc', label: 'Trade Date (Desc)'},
{value: 'trade_date|asc', label: 'Trade Date (Asc)'},
{value: null, label: 'None'}
]
}),
switchInput({
bind: 'preventDeactivate',
label: 'Prevent Route Deactivation'
})
]
})
});
}
});

class AdvancedRoutingPanelModel extends HoistModel {
@observable groupBy = null;
@observable sortBy = null;
@observable preventDeactivate = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this @bindable

gridModel: GridModel = null;

constructor() {
super();
makeObservable(this);

this.gridModel = new GridModel({
columns: [
{field: 'id'},
{field: 'company', flex: 1},
{field: 'city', flex: 1},
{field: 'trade_date', flex: 1}
]
});

this.addReaction(
{
track: () => XH.routerState,
run: (newState, oldState) => this.processRouterState(newState, oldState)
},
{
track: () => [this.groupBy, this.sortBy, this.gridModel.selectedRecord?.id],
run: () => this.updateRoute()
}
);

window.addEventListener('beforeunload', e => {
if (!XH.routerState.name.startsWith('default.other.advancedRouting')) {
delete e.returnValue;
return;
}
if (this.preventDeactivate) e.preventDefault();
});
}

@action
private setGroupBy(groupBy: string) {
this.groupBy = groupBy;

const groupByArr = groupBy ? groupBy.split(',') : [];
this.gridModel.setGroupBy(groupByArr);
}

@action
private setSortBy(sortBy: string) {
this.sortBy = sortBy;

const sortByArr = sortBy ? sortBy.split(',') : [];
this.gridModel.setSortBy(sortByArr);
}

@action
private async setSelected(recordId: string | number) {
await this.gridModel.selectAsync(Number(recordId));
if (!this.gridModel.selectedId) {
XH.dangerToast(`Record ${recordId} not found`);
}
}

@action
private setPreventDeactivate(preventDeactivate: boolean) {
this.preventDeactivate = preventDeactivate;
}

@action
private async parseRouteParams() {
const {groupBy, sortBy, selectedId} = XH.routerState.params;
if (groupBy) this.setGroupBy(groupBy);
if (sortBy) this.setSortBy(sortBy);
if (selectedId) await this.setSelected(selectedId);
}

@action
private async processRouterState(newState?: State, oldState?: State) {
if (
!newState.name.startsWith('default.other.advancedRouting') &&
oldState.name.startsWith('default.other.advancedRouting')
)
return XH.navigate(newState.name, null, {replace: true});
else if (
newState.name.startsWith('default.other.advancedRouting') &&
!oldState.name.startsWith('default.other.advancedRouting')
)
this.updateRoute();
else if (newState.name.startsWith('default.other.advancedRouting'))
await this.parseRouteParams();
}

@action
private updateRoute() {
if (
XH.routerState.name.startsWith('default.other.advancedRouting') &&
!this.gridModel.empty
) {
const {groupBy, sortBy} = this;
const selectedId = this.gridModel.selectedRecord?.id;
XH.navigate(
'default.other.advancedRouting',
{groupBy, sortBy, selectedId},
// Only push URL to route history if groupBy or sortBy changes.
{replace: selectedId != XH.routerState.params.selectedId}
);
}
}

override async doLoadAsync(loadSpec) {
const {trades} = await XH.fetchJson({url: 'trade'});
this.gridModel.loadData(trades);
await this.parseRouteParams();
}
}
Loading