Skip to content
Snippets Groups Projects
Commit 2bc180f2 authored by David Sehnal's avatar David Sehnal
Browse files

mol-plugin: refactoring transform controls

parent c3df0b51
Branches
Tags
No related merge requests found
......@@ -6,7 +6,7 @@
import { StateAction } from 'mol-state/action';
import { Transformer } from 'mol-state';
import { StateTransformParameters } from './ui/state/parameters';
import { StateTransformParameters } from './ui/state/common';
export { PluginSpec }
......
......@@ -7,38 +7,34 @@
import * as React from 'react'
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { camelCaseToWords } from 'mol-util/string';
export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
params: P,
values: any,
onChange: ParamOnChange,
isEnabled?: boolean,
isDisabled?: boolean,
onEnter?: () => void
}
export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
render() {
const common = {
onChange: this.props.onChange,
isEnabled: this.props.isEnabled,
onEnter: this.props.onEnter,
}
const params = this.props.params;
const values = this.props.values;
return <div style={{ width: '100%' }}>
{Object.keys(params).map(key => {
const param = params[key];
if (param.type === 'value') return null;
if (param.type === 'mapped') return <MappedControl param={param} key={key} {...common} name={key} value={values[key]} />
if (param.type === 'group') return <GroupControl param={param} key={key} {...common} name={key} value={values[key]} />
return <ParamWrapper control={controlFor(param)} param={param} key={key} {...common} name={key} value={values[key]} />
const Control = controlFor(param);
if (!Control) return null;
return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
})}
</div>;
}
}
function controlFor(param: PD.Any): ValueControl {
function controlFor(param: PD.Any): ParamControl | undefined {
switch (param.type) {
case 'value': return void 0;
case 'boolean': return BoolControl;
case 'number': return NumberControl;
case 'multi-select': return MultiSelectControl;
......@@ -46,66 +42,57 @@ function controlFor(param: PD.Any): ValueControl {
case 'select': return SelectControl;
case 'text': return TextControl;
case 'interval': return IntervalControl;
case 'group': throw Error('Must be handled separately');
case 'mapped': throw Error('Must be handled separately');
case 'group': return GroupControl;
case 'mapped': return MappedControl;
case 'line-graph': return void 0;
}
throw new Error('not supported');
}
type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, onChange: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
// type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, onChange: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
type ValueControlProps<P extends PD.Base<any> = PD.Base<any>> = { value: any, param: P, isEnabled?: boolean, onChange: (v: any) => void, onEnter?: () => void }
type ValueControl = React.ComponentClass<ValueControlProps<any>>
export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> { name: string, value: P['defaultValue'], param: P, isDisabled?: boolean, onChange: ParamOnChange, onEnter?: () => void }
export type ParamControl = React.ComponentClass<ParamProps<any>>
export class ParamWrapper extends React.PureComponent<ParamWrapperProps> {
onChange = (value: any) => {
export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>> {
protected update(value: any) {
this.props.onChange({ param: this.props.param, name: this.props.name, value });
}
abstract renderControl(): JSX.Element;
render() {
const label = this.props.param.label || camelCaseToWords(this.props.name);
return <div style={{ padding: '0 3px', borderBottom: '1px solid #ccc' }}>
<div style={{ lineHeight: '20px', float: 'left' }} title={this.props.param.description}>{this.props.param.label}</div>
<div style={{ lineHeight: '20px', float: 'left' }} title={this.props.param.description}>{label}</div>
<div style={{ float: 'left', marginLeft: '5px' }}>
<this.props.control value={this.props.value} param={this.props.param} onChange={this.onChange} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
{this.renderControl()}
</div>
<div style={{ clear: 'both' }} />
</div>;
}
}
export class BoolControl extends React.PureComponent<ValueControlProps> {
onClick = () => {
this.props.onChange(!this.props.value);
}
render() {
return <button onClick={this.onClick} disabled={!this.props.isEnabled}>{this.props.value ? '✓ On' : '✗ Off'}</button>;
export class BoolControl extends SimpleParam<PD.Boolean> {
onClick = () => { this.update(!this.props.value); }
renderControl() {
return <button onClick={this.onClick} disabled={this.props.isDisabled}>{this.props.value ? '✓ On' : '✗ Off'}</button>;
}
}
export class NumberControl extends React.PureComponent<ValueControlProps<PD.Numeric>, { value: string }> {
// state = { value: this.props.value }
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.onChange(+e.target.value);
// this.setState({ value: e.target.value });
}
render() {
return <input type='range'
value={'' + this.props.value}
min={this.props.param.min}
max={this.props.param.max}
step={this.props.param.step}
onChange={this.onChange}
/>;
export class NumberControl extends SimpleParam<PD.Numeric> {
onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.update(+e.target.value); }
renderControl() {
return <input type='range' value={'' + this.props.value} min={this.props.param.min} max={this.props.param.max} step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />;
}
}
export class TextControl extends React.PureComponent<ValueControlProps<PD.Text>> {
export class TextControl extends SimpleParam<PD.Text> {
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (value !== this.props.value) {
this.props.onChange(value);
this.update(value);
}
}
......@@ -116,67 +103,60 @@ export class TextControl extends React.PureComponent<ValueControlProps<PD.Text>>
}
}
render() {
renderControl() {
return <input type='text'
value={this.props.value || ''}
onChange={this.onChange}
onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
disabled={this.props.isDisabled}
/>;
}
}
export class SelectControl extends React.PureComponent<ValueControlProps<PD.Select<any>>> {
onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.setState({ value: e.target.value });
this.props.onChange(e.target.value);
}
render() {
return <select value={this.props.value || ''} onChange={this.onChange}>
export class SelectControl extends SimpleParam<PD.Select<any>> {
onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { this.update(e.target.value); }
renderControl() {
return <select value={this.props.value || ''} onChange={this.onChange} disabled={this.props.isDisabled}>
{this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
</select>;
}
}
export class MultiSelectControl extends React.PureComponent<ValueControlProps<PD.MultiSelect<any>>> {
export class MultiSelectControl extends SimpleParam<PD.MultiSelect<any>> {
// onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
// this.setState({ value: e.target.value });
// this.props.onChange(e.target.value);
// }
render() {
renderControl() {
return <span>multiselect TODO</span>;
// return <select value={this.props.value || ''} onChange={this.onChange}>
// {this.props.param.options.map(([value, label]) => <option key={label} value={value}>{label}</option>)}
// </select>;
}
}
export class IntervalControl extends React.PureComponent<ValueControlProps<PD.Interval>> {
export class IntervalControl extends SimpleParam<PD.Interval> {
// onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
// this.setState({ value: e.target.value });
// this.props.onChange(e.target.value);
// }
render() {
renderControl() {
return <span>interval TODO</span>;
}
}
export class ColorControl extends React.PureComponent<ValueControlProps<PD.Color>> {
export class ColorControl extends SimpleParam<PD.Color> {
// onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
// this.setState({ value: e.target.value });
// this.props.onChange(e.target.value);
// }
render() {
renderControl() {
return <span>color TODO</span>;
}
}
type GroupWrapperProps = { name: string, value: PD.Group<any>['defaultValue'], param: PD.Group<any>, onChange: ParamOnChange, onEnter?: () => void, isEnabled?: boolean }
export class GroupControl extends React.PureComponent<GroupWrapperProps> {
export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>> {
change(value: PD.Mapped<any>['defaultValue'] ) {
this.props.onChange({ name: this.props.name, param: this.props.param, value });
}
......@@ -191,13 +171,12 @@ export class GroupControl extends React.PureComponent<GroupWrapperProps> {
const params = this.props.param.params;
return <div>
<ParameterControls params={params} onChange={this.onChangeParam} values={value.params} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
<ParameterControls params={params} onChange={this.onChangeParam} values={value.params} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
</div>
}
}
type MappedWrapperProps = { name: string, value: PD.Mapped<any>['defaultValue'], param: PD.Mapped<any>, onChange: ParamOnChange, onEnter?: () => void, isEnabled?: boolean }
export class MappedControl extends React.PureComponent<MappedWrapperProps> {
export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> {
change(value: PD.Mapped<any>['defaultValue'] ) {
this.props.onChange({ name: this.props.name, param: this.props.param, value });
}
......@@ -214,16 +193,20 @@ export class MappedControl extends React.PureComponent<MappedWrapperProps> {
render() {
const value: PD.Mapped<any>['defaultValue'] = this.props.value;
const param = this.props.param.map(value.name);
const Mapped = controlFor(param);
return <div>
<ParamWrapper control={SelectControl} param={this.props.param.select}
isEnabled={this.props.isEnabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
const select = <SelectControl param={this.props.param.select}
isDisabled={this.props.isDisabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
name={'name'} value={value.name} />
if (!Mapped) {
return select;
}
return <div>
{select}
<div style={{ borderLeft: '5px solid #777', paddingLeft: '5px' }}>
{param.type === 'group'
? <GroupControl param={param} value={value} name='param' onChange={this.onChangeParam} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
: param.type === 'mapped' || param.type === 'value' ? null
: <ParamWrapper control={controlFor(param)} param={param} onChange={this.onChangeParam} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} name={'value'} value={value} />}
<Mapped param={param} value={value} name='param' onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
</div>
</div>
}
......
......@@ -4,15 +4,12 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as React from 'react';
import { PluginCommands } from 'mol-plugin/command';
import { PluginContext } from 'mol-plugin/context';
import { State, Transform } from 'mol-state';
import { StateAction } from 'mol-state/action';
import { Subject } from 'rxjs';
import { PurePluginComponent } from '../base';
import { StateTransformParameters } from './parameters';
import { memoizeOne } from 'mol-util/memoize';
import { PluginContext } from 'mol-plugin/context';
import { StateTransformParameters, TransformContolBase } from './common';
export { ApplyActionContol };
......@@ -33,62 +30,23 @@ namespace ApplyActionContol {
}
}
class ApplyActionContol extends PurePluginComponent<ApplyActionContol.Props, ApplyActionContol.ComponentState> {
private busy: Subject<boolean>;
onEnter = () => {
if (this.state.error) return;
this.apply();
}
source = this.props.state.cells.get(this.props.nodeRef)!.obj!;
getInfo = memoizeOne((t: Transform.Ref) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
events: StateTransformParameters.Props['events'] = {
onEnter: this.onEnter,
onChange: (params, isInitial, errors) => {
this.setState({ params, isInitial, error: errors && errors[0] })
}
}
// getInitialParams() {
// const p = this.props.action.definition.params;
// if (!p || !p.default) return {};
// return p.default(this.source, this.plugin);
// }
// initialErrors() {
// const p = this.props.action.definition.params;
// if (!p || !p.validate) return void 0;
// const errors = p.validate(this.info.initialValues, this.source, this.plugin);
// return errors && errors[0];
// }
state = { nodeRef: this.props.nodeRef, error: void 0, isInitial: true, params: this.getInfo(this.props.nodeRef).initialValues, busy: false };
apply = async () => {
this.setState({ busy: true });
try {
await PluginCommands.State.ApplyAction.dispatch(this.plugin, {
class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, ApplyActionContol.ComponentState> {
applyAction() {
return PluginCommands.State.ApplyAction.dispatch(this.plugin, {
state: this.props.state,
action: this.props.action.create(this.state.params),
ref: this.props.nodeRef
});
} finally {
this.busy.next(false);
}
}
getInfo() { return this._getInfo(this.props.nodeRef); }
getHeader() { return this.props.action.definition.display; }
getHeaderFallback() { return this.props.action.id; }
isBusy() { return !!this.state.error || this.state.busy; }
applyText() { return 'Apply'; }
init() {
this.busy = new Subject();
this.subscribe(this.busy, busy => this.setState({ busy }));
}
private _getInfo = memoizeOne((t: Transform.Ref) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
refresh = () => {
this.setState({ params: this.getInfo(this.props.nodeRef).initialValues, isInitial: true, error: void 0 });
}
state = { nodeRef: this.props.nodeRef, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
static getDerivedStateFromProps(props: ApplyActionContol.Props, state: ApplyActionContol.ComponentState) {
if (props.nodeRef === state.nodeRef) return null;
......@@ -104,21 +62,4 @@ class ApplyActionContol extends PurePluginComponent<ApplyActionContol.Props, App
};
return newState;
}
render() {
const info = this.getInfo(this.props.nodeRef);
const action = this.props.action;
return <div>
<div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div>
<StateTransformParameters info={info} events={this.events} params={this.state.params} isEnabled={!this.state.busy} />
<div style={{ textAlign: 'right' }}>
<span style={{ color: 'red' }}>{this.state.error}</span>
{this.state.isInitial ? void 0 : <button title='Refresh Params' onClick={this.refresh} disabled={this.state.busy}></button>}
<button onClick={this.apply} disabled={!!this.state.error || this.state.busy}>Create</button>
</div>
</div>
}
}
\ No newline at end of file
......@@ -5,15 +5,15 @@
*/
import { StateObject, State, Transform, StateObjectCell, Transformer } from 'mol-state';
import { shallowEqual } from 'mol-util/object';
import * as React from 'react';
import { PurePluginComponent } from '../base';
import { ParameterControls, ParamOnChange } from '../controls/parameters';
import { StateAction } from 'mol-state/action';
import { PluginContext } from 'mol-plugin/context';
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { Subject } from 'rxjs';
export { StateTransformParameters };
export { StateTransformParameters, TransformContolBase };
class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> {
getDefinition() {
......@@ -25,18 +25,10 @@ class StateTransformParameters extends PurePluginComponent<StateTransformParamet
validate(params: any) {
// TODO
return void 0;
// const validate = this.props.info.definition.validate;
// if (!validate) return void 0;
// const result = validate(params, this.props.info.source, this.plugin);
// if (!result || result.length === 0) return void 0;
// return result.map(r => r[0]);
}
areInitial(params: any) {
const areEqual = this.props.info.definition.areEqual;
if (!areEqual) return shallowEqual(params, this.props.info.initialValues);
return areEqual(params, this.props.info.initialValues);
return PD.areEqual(this.props.info.params, params, this.props.info.initialValues);
}
onChange: ParamOnChange = ({ name, value }) => {
......@@ -45,7 +37,7 @@ class StateTransformParameters extends PurePluginComponent<StateTransformParamet
};
render() {
return <ParameterControls params={this.props.info.params} values={this.props.params} onChange={this.onChange} onEnter={this.props.events.onEnter} isEnabled={this.props.isEnabled} />;
return <ParameterControls params={this.props.info.params} values={this.props.params} onChange={this.onChange} onEnter={this.props.events.onEnter} isDisabled={this.props.isDisabled} />;
}
}
......@@ -64,7 +56,7 @@ namespace StateTransformParameters {
onEnter: () => void,
}
params: any,
isEnabled?: boolean
isDisabled?: boolean
}
export type Class = React.ComponentClass<Props>
......@@ -97,3 +89,80 @@ namespace StateTransformParameters {
}
}
}
namespace TransformContolBase {
export interface State {
params: any,
error?: string,
busy: boolean,
isInitial: boolean
}
}
abstract class TransformContolBase<P, S extends TransformContolBase.State> extends PurePluginComponent<P, S> {
abstract applyAction(): Promise<void>;
abstract getInfo(): StateTransformParameters.Props['info'];
abstract getHeader(): Transformer.Definition['display'];
abstract getHeaderFallback(): string;
abstract isBusy(): boolean;
abstract applyText(): string;
abstract state: S;
private busy: Subject<boolean>;
private onEnter = () => {
if (this.state.error) return;
this.apply();
}
events: StateTransformParameters.Props['events'] = {
onEnter: this.onEnter,
onChange: (params, isInitial, errors) => this.setState({ params, isInitial, error: errors && errors[0] })
}
apply = async () => {
this.setState({ busy: true });
try {
await this.applyAction();
} finally {
this.busy.next(false);
}
}
init() {
this.busy = new Subject();
this.subscribe(this.busy, busy => this.setState({ busy }));
}
refresh = () => {
this.setState({ params: this.getInfo().initialValues, isInitial: true, error: void 0 });
}
setDefault = () => {
const info = this.getInfo();
const params = PD.getDefaultValues(info.params);
this.setState({ params, isInitial: PD.areEqual(info.params, params, info.initialValues), error: void 0 });
}
render() {
const info = this.getInfo();
if (info.isEmpty) return <div>Nothing to update</div>;
const display = this.getHeader();
return <div>
<div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}>
<button onClick={this.setDefault} disabled={this.state.busy} style={{ float: 'right'}} title='Set default params'></button>
<h3>{(display && display.name) || this.getHeaderFallback()}</h3>
</div>
<StateTransformParameters info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
<div style={{ textAlign: 'right' }}>
<span style={{ color: 'red' }}>{this.state.error}</span>
{this.state.isInitial ? void 0 : <button title='Refresh params' onClick={this.refresh} disabled={this.state.busy}></button>}
<button onClick={this.apply} disabled={this.isBusy()}>{this.applyText()}</button>
</div>
</div>
}
}
\ No newline at end of file
......@@ -5,11 +5,8 @@
*/
import { State, Transform } from 'mol-state';
import * as React from 'react';
import { Subject } from 'rxjs';
import { PurePluginComponent } from '../base';
import { StateTransformParameters } from './parameters';
import { memoizeOne } from 'mol-util/memoize';
import { StateTransformParameters, TransformContolBase } from './common';
export { UpdateTransformContol };
......@@ -28,43 +25,17 @@ namespace UpdateTransformContol {
}
}
class UpdateTransformContol extends PurePluginComponent<UpdateTransformContol.Props, UpdateTransformContol.ComponentState> {
private busy: Subject<boolean>;
class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Props, UpdateTransformContol.ComponentState> {
applyAction() { return this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params); }
getInfo() { return this._getInfo(this.props.transform); }
getHeader() { return this.props.transform.transformer.definition.display; }
getHeaderFallback() { return this.props.transform.transformer.definition.name; }
isBusy() { return !!this.state.error || this.state.busy || this.state.isInitial; }
applyText() { return 'Update'; }
onEnter = () => {
if (this.state.error) return;
this.apply();
}
getInfo = memoizeOne((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform));
events: StateTransformParameters.Props['events'] = {
onEnter: this.onEnter,
onChange: (params, isInitial, errors) => {
this.setState({ params, isInitial, error: errors && errors[0] })
}
}
private _getInfo = memoizeOne((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform));
state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo(this.props.transform).initialValues, busy: false };
apply = async () => {
this.setState({ busy: true });
try {
await this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params);
} finally {
this.busy.next(false);
}
}
init() {
this.busy = new Subject();
this.subscribe(this.busy, busy => this.setState({ busy }));
}
refresh = () => {
this.setState({ params: this.props.transform.params, isInitial: true, error: void 0 });
}
state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
static getDerivedStateFromProps(props: UpdateTransformContol.Props, state: UpdateTransformContol.ComponentState) {
if (props.transform === state.transform) return null;
......@@ -76,23 +47,4 @@ class UpdateTransformContol extends PurePluginComponent<UpdateTransformContol.Pr
};
return newState;
}
render() {
const info = this.getInfo(this.props.transform);
if (info.isEmpty) return <div>Nothing to update</div>;
const tr = this.props.transform.transformer;
return <div>
<div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(tr.definition.display && tr.definition.display.name) || tr.id}</h3></div>
<StateTransformParameters info={info} events={this.events} params={this.state.params} isEnabled={!this.state.busy} />
<div style={{ textAlign: 'right' }}>
<span style={{ color: 'red' }}>{this.state.error}</span>
{this.state.isInitial ? void 0 : <button title='Refresh Params' onClick={this.refresh} disabled={this.state.busy}></button>}
<button onClick={this.apply} disabled={!!this.state.error || this.state.busy || this.state.isInitial}>Update</button>
</div>
</div>
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment