Skip to content
2 changes: 2 additions & 0 deletions client-app/src/Bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
RenderApiModule,
RowApiModule,
RowAutoHeightModule,
RowDragModule,
RowSelectionModule,
RowStyleModule,
ScrollApiModule,
Expand All @@ -76,6 +77,7 @@ ModuleRegistry.registerModules([
RenderApiModule,
RowApiModule,
RowAutoHeightModule,
RowDragModule,
RowSelectionModule,
RowStyleModule,
ScrollApiModule,
Expand Down
7 changes: 7 additions & 0 deletions client-app/src/admin/AppModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {PortfolioService} from '../core/svc/PortfolioService';
import {phaseRestPanel, projectRestPanel} from './roadmap';
import {
asyncLoopPanel,
columnChooserTestPanel,
storeColumnFilterPanel,
viewColumnFilterPanel,
CubeTestPanel,
Expand Down Expand Up @@ -47,6 +48,7 @@ export class AppModel extends HoistAdminAppModel {
path: '/tests',
children: [
{name: 'asyncLoop', path: '/asyncLoop'},
{name: 'columnChooser', path: '/columnChooser'},
{name: 'cube', path: '/cube'},
{name: 'dataView', path: '/dataView'},
{name: 'fetchAPI', path: '/fetchAPI'},
Expand Down Expand Up @@ -87,6 +89,11 @@ export class AppModel extends HoistAdminAppModel {
switcher,
tabs: [
{id: 'asyncLoop', title: 'Async Loops', content: asyncLoopPanel},
{
id: 'columnChooser',
title: 'Column Chooser',
content: columnChooserTestPanel
},
{id: 'cube', title: 'Cube Data', content: CubeTestPanel},
{id: 'dataView', content: dataViewTestPanel},
{id: 'fetchAPI', title: 'Fetch API', content: FetchApiTestPanel},
Expand Down
172 changes: 172 additions & 0 deletions client-app/src/admin/tests/columnChooser/AddColumnDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import {ColumnSpec} from '@xh/hoist/cmp/grid';
import {form, FormModel} from '@xh/hoist/cmp/form';
import {filler, vbox} from '@xh/hoist/cmp/layout';
import {hoistCmp, HoistModel, managed, uses} from '@xh/hoist/core';
import {required} from '@xh/hoist/data';
import {button} from '@xh/hoist/desktop/cmp/button';
import {formField} from '@xh/hoist/desktop/cmp/form';
import {select, switchInput, textArea, textInput} from '@xh/hoist/desktop/cmp/input';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
import {numberRenderer} from '@xh/hoist/format';
import {Icon} from '@xh/hoist/icon';
import {dialog} from '@xh/hoist/kit/blueprint';
import {action, makeObservable, observable} from '@xh/hoist/mobx';

/** Host that owns the grid the new column will be added to. */
export interface AddColumnHost {
groupIds: string[];
addColumn(spec: ColumnSpec, group: string): void;
}

let seq = 0;

export class AddColumnDialogModel extends HoistModel {
readonly host: AddColumnHost;

@observable isOpen = false;

@managed formModel = new FormModel({
fields: [
{name: 'chooserName', displayName: 'Name', rules: [required]},
{name: 'chooserDescription', displayName: 'Description'},
{name: 'type', displayName: 'Type', initialValue: 'string'},
{name: 'group', displayName: 'Column Group'},
{name: 'pinned', displayName: 'Pinned'},
{name: 'excludeFromChooser', displayName: 'Exclude from Chooser', initialValue: false},
{name: 'movable', displayName: 'Movable', initialValue: true},
{name: 'hideable', displayName: 'Hideable', initialValue: true},
{name: 'hidden', displayName: 'Hidden', initialValue: false}
]
});

constructor(host: AddColumnHost) {
super();
makeObservable(this);
this.host = host;
}

@action
open() {
this.formModel.init({
type: 'string',
movable: true,
hideable: true,
excludeFromChooser: false,
hidden: false
});
this.isOpen = true;
}

@action
close() {
this.isOpen = false;
}

async submitAsync() {
const {formModel, host} = this;
if (!(await formModel.validateAsync())) return;

const v = formModel.values,
id = `custom_${++seq}`,
spec: ColumnSpec = {
colId: id,
field: {name: id, type: v.type},
chooserName: v.chooserName,
chooserDescription: v.chooserDescription || undefined,
headerName: v.chooserName,
excludeFromChooser: v.excludeFromChooser,
movable: v.movable,
hideable: v.hideable,
hidden: v.hidden,
width: 120
};
if (v.pinned) spec.pinned = v.pinned;
if (v.type === 'number') {
spec.align = 'right';
spec.renderer = numberRenderer({precision: 0});
}

host.addColumn(spec, v.group || '');
this.close();
}
}

export const addColumnDialog = hoistCmp.factory<AddColumnDialogModel>({
model: uses(AddColumnDialogModel),
render({model}) {
return dialog({
title: 'Add Column',
icon: Icon.add(),
style: {width: 460},
isOpen: model.isOpen,
onClose: () => model.close(),
usePortal: false,
item: formContents()
});
}
});

const formContents = hoistCmp.factory<AddColumnDialogModel>(({model}) =>
panel({
item: form({
model: model.formModel,
fieldDefaults: {minimal: true, inline: false},
item: vbox({
className: 'xh-pad',
items: [
formField({field: 'chooserName', item: textInput({autoFocus: true})}),
formField({field: 'chooserDescription', item: textArea({height: 60})}),
formField({
field: 'type',
item: select({
options: [
{value: 'string', label: 'String'},
{value: 'number', label: 'Number'},
{value: 'bool', label: 'Bool'}
]
})
}),
formField({
field: 'group',
item: select({
options: model.host.groupIds,
enableCreate: true,
placeholder: '(top level)'
})
}),
formField({
field: 'pinned',
item: select({
enableClear: true,
options: [
{value: 'left', label: 'Left'},
{value: 'right', label: 'Right'}
]
})
}),
formField({field: 'hidden', item: switchInput()}),
formField({field: 'hideable', item: switchInput()}),
formField({field: 'movable', item: switchInput()}),
formField({field: 'excludeFromChooser', item: switchInput()})
]
})
}),
bbar: bbar()
})
);

const bbar = hoistCmp.factory<AddColumnDialogModel>(({model}) =>
toolbar(
filler(),
button({text: 'Cancel', onClick: () => model.close()}),
button({
text: 'Add',
icon: Icon.add(),
intent: 'success',
minimal: false,
disabled: !model.formModel.isValid,
onClick: () => model.submitAsync()
})
)
);
156 changes: 156 additions & 0 deletions client-app/src/admin/tests/columnChooser/ColumnChooserTestPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {ColumnOrGroupSpec, ColumnSpec, grid, GridModel} from '@xh/hoist/cmp/grid';
import {filler, hframe, span} from '@xh/hoist/cmp/layout';
import {storeFilterField} from '@xh/hoist/cmp/store';
import {creates, hoistCmp, HoistModel, managed, PlainObject, XH} from '@xh/hoist/core';
import {button, colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
import {columnChooser} from '@xh/hoist/desktop/cmp/grid';
import {buttonGroupInput, jsonInput, switchInput} from '@xh/hoist/desktop/cmp/input';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
import {Icon} from '@xh/hoist/icon';
import {bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
import {addColumnDialog, AddColumnDialogModel, AddColumnHost} from './AddColumnDialog';
import {
collectGroupIds,
CustomColumn,
generateGridData,
GridSize,
mergeCustomColumns
} from './generateColumns';

export const columnChooserTestPanel = hoistCmp.factory({
model: creates(() => ColumnChooserTestModel),
render({model}) {
return panel({
title: 'Tests › Column Chooser',
icon: Icon.gridPanel(),
tbar: [
span('Columns'),
buttonGroupInput({
bind: 'size',
items: [
button({text: 'Small', value: 'small'}),
button({text: 'Medium', value: 'medium'}),
button({text: 'Large', value: 'large'})
]
}),
toolbarSep(),
switchInput({bind: 'lockColumnGroups', label: 'Lock Groups', labelSide: 'left'}),
switchInput({bind: 'enableColumnPinning', label: 'Pinning', labelSide: 'left'}),
toolbarSep(),
button({
text: 'Add Column',
icon: Icon.add(),
onClick: () => model.addColumnModel.open()
}),
filler(),
storeFilterField({gridModel: model.gridModel}),
colChooserButton({gridModel: model.gridModel}),
exportButton({gridModel: model.gridModel})
],
items: [
hframe(
panel({
title: 'Embedded Chooser',
icon: Icon.gridPanel(),
modelConfig: {
side: 'left',
defaultSize: 340,
collapsible: true,
resizable: true
},
item: columnChooser({gridModel: model.gridModel, flex: 1})
}),
panel({flex: 1, item: grid({model: model.gridModel})}),
panel({
title: 'Column State',
icon: Icon.json(),
modelConfig: {
side: 'right',
defaultSize: 380,
collapsible: true,
resizable: true
},
item: jsonInput({
value: model.columnStateJson,
readonly: true,
language: 'json',
flex: 1,
width: '100%'
})
})
),
addColumnDialog({model: model.addColumnModel})
]
});
}
});

class ColumnChooserTestModel extends HoistModel implements AddColumnHost {
@bindable size: GridSize = 'medium';
@bindable lockColumnGroups = true;
@bindable enableColumnPinning = true;

@observable.ref customColumns: CustomColumn[] = [];
@managed @observable.ref gridModel: GridModel;
@managed addColumnModel = new AddColumnDialogModel(this);

private baseColumns: ColumnOrGroupSpec[] = [];
private records: PlainObject[] = [];

@computed
get columnStateJson(): string {
return JSON.stringify(this.gridModel.columnState, null, 2);
}

/** Generated group ids + any new groups created via the Add Column form. */
get groupIds(): string[] {
const base = collectGroupIds(this.baseColumns),
custom = this.customColumns.map(c => c.group).filter(g => g && !base.includes(g));
return [...base, ...Array.from(new Set(custom))];
}

constructor() {
super();
makeObservable(this);
this.gridModel = this.createGridModel();

this.addReaction({
track: () => [
this.size,
this.lockColumnGroups,
this.enableColumnPinning,
this.customColumns
],
run: () => {
XH.safeDestroy(this.gridModel);
this.gridModel = this.createGridModel();
this.loadAsync().catchDefault();
}
});
}

addColumn(spec: ColumnSpec, group: string) {
this.customColumns = [...this.customColumns, {spec, group}];
}

override async doLoadAsync() {
this.gridModel.loadData(this.records);
}

private createGridModel(): GridModel {
const {columns, records} = generateGridData(this.size);
this.baseColumns = columns;
this.records = records;
return new GridModel({
store: {idSpec: 'id'},
emptyText: 'No records found...',
colChooserModel: true,
enableExport: true,
useVirtualColumns: true,
lockColumnGroups: this.lockColumnGroups,
enableColumnPinning: this.enableColumnPinning,
columns: mergeCustomColumns(columns, this.customColumns)
});
}
}
Loading
Loading