From 2bc180f202d964d22e702cc71e9bae9796358792 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Thu, 15 Nov 2018 22:08:51 +0100
Subject: [PATCH] mol-plugin: refactoring transform controls

---
 src/mol-plugin/spec.ts                        |   2 +-
 src/mol-plugin/ui/controls/parameters.tsx     | 131 ++++++++----------
 src/mol-plugin/ui/state/apply-action.tsx      |  91 +++---------
 .../ui/state/{parameters.tsx => common.tsx}   |  95 +++++++++++--
 src/mol-plugin/ui/state/update-transform.tsx  |  68 ++-------
 5 files changed, 166 insertions(+), 221 deletions(-)
 rename src/mol-plugin/ui/state/{parameters.tsx => common.tsx} (52%)

diff --git a/src/mol-plugin/spec.ts b/src/mol-plugin/spec.ts
index 2d2971909..40413fd87 100644
--- a/src/mol-plugin/spec.ts
+++ b/src/mol-plugin/spec.ts
@@ -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 }
 
diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx
index 2ac820fcc..354bbf449 100644
--- a/src/mol-plugin/ui/controls/parameters.tsx
+++ b/src/mol-plugin/ui/controls/parameters.tsx
@@ -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);
+
+        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>
-            <ParamWrapper control={SelectControl} param={this.props.param.select}
-                isEnabled={this.props.isEnabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
-                name={'name'} value={value.name} />
+            {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>
     }
diff --git a/src/mol-plugin/ui/state/apply-action.tsx b/src/mol-plugin/ui/state/apply-action.tsx
index 4e34aced2..2c616a6be 100644
--- a/src/mol-plugin/ui/state/apply-action.tsx
+++ b/src/mol-plugin/ui/state/apply-action.tsx
@@ -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, {
-                state: this.props.state,
-                action: this.props.action.create(this.state.params),
-                ref: this.props.nodeRef
-            });
-        } finally {
-            this.busy.next(false);
-        }
+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
+        });
     }
+    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
diff --git a/src/mol-plugin/ui/state/parameters.tsx b/src/mol-plugin/ui/state/common.tsx
similarity index 52%
rename from src/mol-plugin/ui/state/parameters.tsx
rename to src/mol-plugin/ui/state/common.tsx
index 86d6727bd..3ce020388 100644
--- a/src/mol-plugin/ui/state/parameters.tsx
+++ b/src/mol-plugin/ui/state/common.tsx
@@ -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>
@@ -96,4 +88,81 @@ namespace StateTransformParameters {
             isEmpty: Object.keys(params).length === 0
         }
     }
+}
+
+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
diff --git a/src/mol-plugin/ui/state/update-transform.tsx b/src/mol-plugin/ui/state/update-transform.tsx
index 591e1f755..1452b9dfe 100644
--- a/src/mol-plugin/ui/state/update-transform.tsx
+++ b/src/mol-plugin/ui/state/update-transform.tsx
@@ -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] })
-        }
-    }
-
-    state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo(this.props.transform).initialValues, busy: false };
+    private _getInfo = memoizeOne((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform));
 
-    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
-- 
GitLab