[rangecontrol] add debouncing (throttle) to range controls

parent ea89cf3d
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
"crypto": "~1.0.1", "crypto": "~1.0.1",
"d3": "~7.6.1", "d3": "~7.6.1",
"debounce": "^2.0.0", "debounce": "^2.0.0",
"debouncing": "^22.7.25",
"echarts": "~5.1.2", "echarts": "~5.1.2",
"echarts-for-react": "~3.0.1", "echarts-for-react": "~3.0.1",
"graphology": "~0.25.1", "graphology": "~0.25.1",
......
...@@ -3,7 +3,7 @@ workspace: ...@@ -3,7 +3,7 @@ workspace:
gargantext: gargantext:
path: ./ path: ./
dependencies: dependencies:
- aff - aff: ">=7.1.0 <8.0.0"
- aff-promise: ">=4.0.0 <5.0.0" - aff-promise: ">=4.0.0 <5.0.0"
- affjax: ">=13.0.0 <14.0.0" - affjax: ">=13.0.0 <14.0.0"
- affjax-web: ">=1.0.0 <2.0.0" - affjax-web: ">=1.0.0 <2.0.0"
...@@ -21,7 +21,7 @@ workspace: ...@@ -21,7 +21,7 @@ workspace:
- d3: "*" - d3: "*"
- data-default: "*" - data-default: "*"
- datetime: ">=6.1.0 <7.0.0" - datetime: ">=6.1.0 <7.0.0"
- debounce - debouncing: ">=0.1.0 <0.2.0"
- debug: ">=6.0.2 <7.0.0" - debug: ">=6.0.2 <7.0.0"
- dom-filereader: ">=7.0.0 <8.0.0" - dom-filereader: ">=7.0.0 <8.0.0"
- dom-simple: ">=0.4.0 <0.5.0" - dom-simple: ">=0.4.0 <0.5.0"
...@@ -59,7 +59,7 @@ workspace: ...@@ -59,7 +59,7 @@ workspace:
- profunctor-lenses: ">=8.0.0 <9.0.0" - profunctor-lenses: ">=8.0.0 <9.0.0"
- random: ">=6.0.0 <7.0.0" - random: ">=6.0.0 <7.0.0"
- react: ">=11.0.0 <12.0.0" - react: ">=11.0.0 <12.0.0"
- reactix - reactix: ">=0.6.1 <0.7.0"
- record: ">=4.0.0 <5.0.0" - record: ">=4.0.0 <5.0.0"
- record-extra: ">=5.0.1 <6.0.0" - record-extra: ">=5.0.1 <6.0.0"
- routing: ">=11.0.0 <12.0.0" - routing: ">=11.0.0 <12.0.0"
...@@ -81,7 +81,6 @@ workspace: ...@@ -81,7 +81,6 @@ workspace:
- unordered-collections: ">=3.1.0 <4.0.0" - unordered-collections: ">=3.1.0 <4.0.0"
- unsafe-coerce: ">=6.0.0 <7.0.0" - unsafe-coerce: ">=6.0.0 <7.0.0"
- uri: ">=9.0.0 <10.0.0" - uri: ">=9.0.0 <10.0.0"
- use-debounce
- uuid: ">=9.0.0 <10.0.0" - uuid: ">=9.0.0 <10.0.0"
- validation: ">=6.0.0 <7.0.0" - validation: ">=6.0.0 <7.0.0"
- web-file: ">=4.0.0 <5.0.0" - web-file: ">=4.0.0 <5.0.0"
...@@ -120,7 +119,7 @@ workspace: ...@@ -120,7 +119,7 @@ workspace:
- d3 - d3
- data-default - data-default
- datetime - datetime
- debounce - debouncing
- debug - debug
- distributive - distributive
- dom-filereader - dom-filereader
...@@ -225,7 +224,6 @@ workspace: ...@@ -225,7 +224,6 @@ workspace:
- unsafe-coerce - unsafe-coerce
- unsafe-reference - unsafe-reference
- uri - uri
- use-debounce
- uuid - uuid
- validation - validation
- variant - variant
...@@ -238,7 +236,7 @@ workspace: ...@@ -238,7 +236,7 @@ workspace:
- web-xhr - web-xhr
package_set: package_set:
address: address:
registry: 50.11.0 registry: 50.13.1
compiler: ">=0.15.15 <0.16.0" compiler: ">=0.15.15 <0.16.0"
content: content:
abc-parser: 2.0.1 abc-parser: 2.0.1
...@@ -253,6 +251,7 @@ workspace: ...@@ -253,6 +251,7 @@ workspace:
affjax-node: 1.0.0 affjax-node: 1.0.0
affjax-web: 1.0.0 affjax-web: 1.0.0
ansi: 7.0.0 ansi: 7.0.0
apexcharts: 0.5.0
applicative-phases: 1.0.0 applicative-phases: 1.0.0
argonaut: 9.0.0 argonaut: 9.0.0
argonaut-aeson-generic: 0.4.1 argonaut-aeson-generic: 0.4.1
...@@ -314,6 +313,7 @@ workspace: ...@@ -314,6 +313,7 @@ workspace:
coroutines: 7.0.0 coroutines: 7.0.0
css: 6.0.0 css: 6.0.0
css-frameworks: 1.0.1 css-frameworks: 1.0.1
csv-stream: 1.1.7
data-mvc: 0.0.2 data-mvc: 0.0.2
datetime: 6.1.0 datetime: 6.1.0
datetime-parsing: 0.2.0 datetime-parsing: 0.2.0
...@@ -551,7 +551,7 @@ workspace: ...@@ -551,7 +551,7 @@ workspace:
pointed-list: 0.5.1 pointed-list: 0.5.1
polymorphic-vectors: 4.0.0 polymorphic-vectors: 4.0.0
posix-types: 6.0.0 posix-types: 6.0.0
postgresql: 1.3.0 postgresql: 1.5.1
precise: 6.0.0 precise: 6.0.0
precise-datetime: 7.0.0 precise-datetime: 7.0.0
prelude: 6.0.1 prelude: 6.0.1
...@@ -732,16 +732,14 @@ workspace: ...@@ -732,16 +732,14 @@ workspace:
data-default: data-default:
git: https://github.com/garganscript/purescript-data-default.git git: https://github.com/garganscript/purescript-data-default.git
ref: v0.4.0 ref: v0.4.0
debounce: debouncing: 0.1.0
path: /data/git-work/github/PURESCRIPT/purescript-debounce
graphql-client: graphql-client:
git: https://github.com/garganscript/purescript-graphql-client.git git: https://github.com/garganscript/purescript-graphql-client.git
ref: spago-next-9.3.2 ref: spago-next-9.3.2
markdown-it: markdown-it:
git: https://github.com/garganscript/purescript-markdown-it.git git: https://github.com/garganscript/purescript-markdown-it.git
ref: spago-next ref: spago-next
reactix: reactix: 0.6.1
path: /data/git-work/github/PURESCRIPT/purescript-reactix
sequences: sequences:
git: https://github.com/garganscript/purescript-sequences.git git: https://github.com/garganscript/purescript-sequences.git
ref: v3.0.2-spago-next ref: v3.0.2-spago-next
...@@ -755,8 +753,6 @@ workspace: ...@@ -755,8 +753,6 @@ workspace:
tuples-native: tuples-native:
git: https://github.com/garganscript/purescript-tuples-native.git git: https://github.com/garganscript/purescript-tuples-native.git
ref: v2.3.0-spago-next ref: v2.3.0-spago-next
use-debounce:
path: /data/git-work/github/PURESCRIPT/purescript-use-debounce
packages: packages:
aff: aff:
type: registry type: registry
...@@ -1060,12 +1056,12 @@ packages: ...@@ -1060,12 +1056,12 @@ packages:
- partial - partial
- prelude - prelude
- tuples - tuples
debounce: debouncing:
type: local type: registry
path: /data/git-work/github/PURESCRIPT/purescript-debounce version: 0.1.0
integrity: sha256-iAfmC8stYPctDdevBWYgydUZ02OB96uYYSACF5Zzyy4=
dependencies: dependencies:
- effect - effect
- ffi-simple
- prelude - prelude
debug: debug:
type: registry type: registry
...@@ -1847,8 +1843,9 @@ packages: ...@@ -1847,8 +1843,9 @@ packages:
- typelevel-prelude - typelevel-prelude
- unsafe-coerce - unsafe-coerce
reactix: reactix:
type: local type: registry
path: /data/git-work/github/PURESCRIPT/purescript-reactix version: 0.6.1
integrity: sha256-mM6JZFWfeMhgMJa9oGdzNchkp/Xcnv2e/oGc0nZp6EQ=
dependencies: dependencies:
- dom-simple - dom-simple
- effect - effect
...@@ -2276,15 +2273,6 @@ packages: ...@@ -2276,15 +2273,6 @@ packages:
- these - these
- transformers - transformers
- unfoldable - unfoldable
use-debounce:
type: local
path: /data/git-work/github/PURESCRIPT/purescript-use-debounce
dependencies:
- console
- effect
- ffi-simple
- prelude
- reactix
uuid: uuid:
type: registry type: registry
version: 9.0.0 version: 9.0.0
......
workspace: workspace:
packageSet: packageSet:
registry: 50.11.0 registry: 50.13.1
extraPackages: extraPackages:
# garganscript packages # garganscript packages
d3: d3:
git: https://github.com/garganscript/purescript-d3.git git: https://github.com/garganscript/purescript-d3.git
ref: v0.11.0 ref: v0.11.0
debounce: debouncing: 0.1.0
path: /data/git-work/github/PURESCRIPT/purescript-debounce reactix: 0.6.1
string-search: string-search:
git: https://gitlab.iscpif.fr/gargantext/purescript-string-search.git git: https://gitlab.iscpif.fr/gargantext/purescript-string-search.git
ref: spago-next ref: spago-next
...@@ -35,19 +35,11 @@ workspace: ...@@ -35,19 +35,11 @@ workspace:
git: https://github.com/garganscript/purescript-spec-discovery.git git: https://github.com/garganscript/purescript-spec-discovery.git
ref: v8.2.0-spago-next ref: v8.2.0-spago-next
use-debounce:
path: /data/git-work/github/PURESCRIPT/purescript-use-debounce
reactix:
path: /data/git-work/github/PURESCRIPT/purescript-reactix
package: package:
name: gargantext name: gargantext
dependencies: dependencies:
# debugging # debugging
- debug: ">=6.0.2 <7.0.0" - aff: ">=7.1.0 <8.0.0"
# - psci-support: ">=6.0.0 <7.0.0"
- aff
- aff-promise: ">=4.0.0 <5.0.0" - aff-promise: ">=4.0.0 <5.0.0"
- affjax: ">=13.0.0 <14.0.0" - affjax: ">=13.0.0 <14.0.0"
- affjax-web: ">=1.0.0 <2.0.0" - affjax-web: ">=1.0.0 <2.0.0"
...@@ -65,7 +57,8 @@ package: ...@@ -65,7 +57,8 @@ package:
- d3: "*" - d3: "*"
- data-default: "*" - data-default: "*"
- datetime: ">=6.1.0 <7.0.0" - datetime: ">=6.1.0 <7.0.0"
- debounce - debouncing: ">=0.1.0 <0.2.0"
- debug: ">=6.0.2 <7.0.0"
- dom-filereader: ">=7.0.0 <8.0.0" - dom-filereader: ">=7.0.0 <8.0.0"
- dom-simple: ">=0.4.0 <0.5.0" - dom-simple: ">=0.4.0 <0.5.0"
- effect: ">=4.0.0 <5.0.0" - effect: ">=4.0.0 <5.0.0"
...@@ -102,8 +95,7 @@ package: ...@@ -102,8 +95,7 @@ package:
- profunctor-lenses: ">=8.0.0 <9.0.0" - profunctor-lenses: ">=8.0.0 <9.0.0"
- random: ">=6.0.0 <7.0.0" - random: ">=6.0.0 <7.0.0"
- react: ">=11.0.0 <12.0.0" - react: ">=11.0.0 <12.0.0"
#- reactix: ">=0.6.0 <0.7.0" - reactix: ">=0.6.1 <0.7.0"
- reactix
- record: ">=4.0.0 <5.0.0" - record: ">=4.0.0 <5.0.0"
- record-extra: ">=5.0.1 <6.0.0" - record-extra: ">=5.0.1 <6.0.0"
- routing: ">=11.0.0 <12.0.0" - routing: ">=11.0.0 <12.0.0"
...@@ -126,7 +118,6 @@ package: ...@@ -126,7 +118,6 @@ package:
- unsafe-coerce: ">=6.0.0 <7.0.0" - unsafe-coerce: ">=6.0.0 <7.0.0"
- uri: ">=9.0.0 <10.0.0" - uri: ">=9.0.0 <10.0.0"
- uuid: ">=9.0.0 <10.0.0" - uuid: ">=9.0.0 <10.0.0"
- use-debounce
- validation: ">=6.0.0 <7.0.0" - validation: ">=6.0.0 <7.0.0"
- web-file: ">=4.0.0 <5.0.0" - web-file: ">=4.0.0 <5.0.0"
- web-html: ">=4.1.0 <5.0.0" - web-html: ">=4.1.0 <5.0.0"
...@@ -150,7 +141,6 @@ package: ...@@ -150,7 +141,6 @@ package:
- UnusedName - UnusedName
- UnusedTypeVar - UnusedTypeVar
test: test:
main: Test.Main main: Test.Main
dependencies: dependencies:
......
...@@ -6,9 +6,12 @@ module Gargantext.Components.GraphExplorer.Toolbar.RangeControl ...@@ -6,9 +6,12 @@ module Gargantext.Components.GraphExplorer.Toolbar.RangeControl
, nodeSizeControl , nodeSizeControl
) where ) where
import Data.Maybe (Maybe(..))
import Data.Tuple.Nested((/\)) import Data.Tuple.Nested((/\))
import Debug (spy) import Debug (spy)
import Effect.Debounce as Debounce import Effect.Aff (launchAff_)
import Effect.Class (liftEffect)
import Effect.Debouncing as Debounce
import FFI.Simple (delay) import FFI.Simple (delay)
import Gargantext.Components.RangeSlider as RS import Gargantext.Components.RangeSlider as RS
import Gargantext.Hooks.Sigmax.Types as SigmaxTypes import Gargantext.Hooks.Sigmax.Types as SigmaxTypes
...@@ -16,7 +19,6 @@ import Gargantext.Utils.Range as Range ...@@ -16,7 +19,6 @@ import Gargantext.Utils.Range as Range
import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Reactix as R2
import Prelude import Prelude
import Reactix as R import Reactix as R
import Reactix.Debounce as RD
import Reactix.DOM.HTML as H import Reactix.DOM.HTML as H
import Toestand as T import Toestand as T
...@@ -24,6 +26,9 @@ import Toestand as T ...@@ -24,6 +26,9 @@ import Toestand as T
here :: R2.Here here :: R2.Here
here = R2.here "Gargantext.Components.GraphExplorer.Toolbar.RangeControl" here = R2.here "Gargantext.Components.GraphExplorer.Toolbar.RangeControl"
defaultThrottleInterval :: Int
defaultThrottleInterval = 500
type Props = type Props =
( caption :: String ( caption :: String
, sliderProps :: Record RS.Props , sliderProps :: Record RS.Props
...@@ -63,48 +68,20 @@ edgeConfluenceControlCpt = here.component "edgeConfluenceControl" cpt ...@@ -63,48 +68,20 @@ edgeConfluenceControlCpt = here.component "edgeConfluenceControl" cpt
, state , state
} _ = do } _ = do
let onChange' rng = do
-- here.log2 "[edgeWeightControl] debounce rng" rng
let _ = spy "debounce rng" rng
T.write_ rng state
onChange = Debounce.debounce onChange' 1000
pure $ edgeConfluenceControlInternal { forceAtlasState
, onChange
, range
, state }
type EdgeConfluenceControlInternalProps =
( forceAtlasState :: T.Box SigmaxTypes.ForceAtlasState
, onChange :: Debounce.Debounce
, range :: Range.NumberRange
, state :: T.Box Range.NumberRange
)
edgeConfluenceControlInternal :: R2.Leaf EdgeConfluenceControlInternalProps
edgeConfluenceControlInternal = R2.leaf edgeConfluenceControlInternalCpt
edgeConfluenceControlInternalCpt :: R.Component EdgeConfluenceControlInternalProps
edgeConfluenceControlInternalCpt = here.component "edgeConfluenceControlInternal" cpt
where
cpt { forceAtlasState
, onChange
, range: Range.Closed { min, max }
, state
} _ = do
forceAtlasState' <- R2.useLive' forceAtlasState forceAtlasState' <- R2.useLive' forceAtlasState
state' <- T.useLive T.unequal state state' <- T.useLive T.unequal state
pure $ rangeControl { pure $ rangeControl {
caption: "Edge Confluence Weight" caption: "Edge Confluence Weight"
, sliderProps: { , sliderProps: {
bounds: Range.Closed { min, max } bounds: range
, epsilon: 0.01 , epsilon: 0.01
, height: 5.0 , height: 5.0
, initialValue: state' , initialValue: state'
, onChange: \rng -> Debounce.call onChange rng , onChange: \rng -> T.write_ rng state
, status: SigmaxTypes.forceAtlasComponentStatus forceAtlasState' , status: SigmaxTypes.forceAtlasComponentStatus forceAtlasState'
, step: 1.0 , step: 1.0
, throttleInterval: Just defaultThrottleInterval
, width: 10.0 , width: 10.0
} }
} }
...@@ -127,34 +104,8 @@ edgeWeightControlCpt = here.component "edgeWeightControl" cpt ...@@ -127,34 +104,8 @@ edgeWeightControlCpt = here.component "edgeWeightControl" cpt
, state , state
} _ = do } _ = do
transition <- R2.useTransition
test /\ setTest <- R.useState' 1
let onChange' rng = do
-- here.log2 "[edgeWeightControl] debounce rng" rng
let _ = spy "debounce rng" rng
let _ = spy "debounce state" state
let _ = spy "transition" transition
let _ = spy "test" test
-- R2.startTransition transition (\_ -> T.write_ rng state)
setTest (_ + 1)
T.write_ rng state
-- T.modify_ SigmaxTypes.toggleForceAtlasState forceAtlasState
onChange = Debounce.debounce onChange' 1000
-- rd <- RD.useDebouncedCallback (
-- \rng -> do
-- let _ = spy "debounce rng" rng
-- let _ = spy "test" test
-- setTest (_ + 1)
-- T.write_ rng state
-- ) 200
forceAtlasState' <- R2.useLive' forceAtlasState forceAtlasState' <- R2.useLive' forceAtlasState
state' <- T.useLive T.unequal state state' <- T.useLive T.unequal state
R.useEffect' $ do
here.log2 "[edgeWeightControl] state'" state'
here.log2 "[edgeWeightControl] test" test
pure $ rangeControl { pure $ rangeControl {
caption: "Edge Weight" caption: "Edge Weight"
...@@ -163,52 +114,13 @@ edgeWeightControlCpt = here.component "edgeWeightControl" cpt ...@@ -163,52 +114,13 @@ edgeWeightControlCpt = here.component "edgeWeightControl" cpt
, initialValue: state' , initialValue: state'
, epsilon: (max - min) / 100.0 , epsilon: (max - min) / 100.0
, height: 5.0 , height: 5.0
, onChange: \rng -> Debounce.call onChange rng , onChange: \rng -> T.write_ rng state
-- , onChange: \rng -> Debounce.call onChange rng
-- , onChange: \rng -> RD.callDebouncedCallback rd rng -- , onChange: \rng -> RD.callDebouncedCallback rd rng
-- , onChange: onChange' -- , onChange: onChange'
, status: SigmaxTypes.forceAtlasComponentStatus forceAtlasState' , status: SigmaxTypes.forceAtlasComponentStatus forceAtlasState'
, step: 1.0 , step: 1.0
, width: 10.0 , throttleInterval: Just defaultThrottleInterval
}
}
-- pure $ edgeWeightControlInternal { forceAtlasState
-- , onChange
-- , range
-- , state }
type EdgeWeightControlInternalProps =
( forceAtlasState :: T.Box SigmaxTypes.ForceAtlasState
, onChange :: Debounce.Debounce
, range :: Range.NumberRange
, state :: T.Box Range.NumberRange
)
edgeWeightControlInternal :: R2.Leaf EdgeWeightControlInternalProps
edgeWeightControlInternal = R2.leaf edgeWeightControlInternalCpt
edgeWeightControlInternalCpt :: R.Component EdgeWeightControlInternalProps
edgeWeightControlInternalCpt = here.component "edgeWeightControlInternal" cpt
where
cpt { forceAtlasState
, onChange
, range: Range.Closed { min, max }
, state
} _ = do
forceAtlasState' <- R2.useLive' forceAtlasState
state' <- T.useLive T.unequal state
let _ = spy "[edgeWeightControlInteral] onChange" onChange
pure $ rangeControl {
caption: "Edge Weight"
, sliderProps: {
bounds: Range.Closed { min, max }
, initialValue: state'
, epsilon: (max - min) / 100.0
, height: 5.0
, onChange: \rng -> Debounce.call onChange rng
, status: SigmaxTypes.forceAtlasComponentStatus forceAtlasState'
, step: 1.0
, width: 10.0 , width: 10.0
} }
} }
...@@ -243,6 +155,7 @@ nodeSizeControlCpt = here.component "nodeSizeControl" cpt ...@@ -243,6 +155,7 @@ nodeSizeControlCpt = here.component "nodeSizeControl" cpt
, onChange: \rng -> T.write_ rng state , onChange: \rng -> T.write_ rng state
, status: SigmaxTypes.forceAtlasComponentStatus forceAtlasState' , status: SigmaxTypes.forceAtlasComponentStatus forceAtlasState'
, step: 1.0 , step: 1.0
, throttleInterval: Just defaultThrottleInterval
, width: 10.0 , width: 10.0
} }
} }
...@@ -21,6 +21,7 @@ import DOM.Simple.Event as Event ...@@ -21,6 +21,7 @@ import DOM.Simple.Event as Event
import DOM.Simple.EventListener as EL import DOM.Simple.EventListener as EL
import DOM.Simple (DOMRect) import DOM.Simple (DOMRect)
import Effect (Effect) import Effect (Effect)
import Effect.Debouncing as Debounce
import Reactix as R import Reactix as R
import Reactix.DOM.HTML as H import Reactix.DOM.HTML as H
import Toestand as T import Toestand as T
...@@ -51,6 +52,7 @@ type Props = ...@@ -51,6 +52,7 @@ type Props =
, width :: Number , width :: Number
, height :: Number , height :: Number
, onChange :: Range.NumberRange -> Effect Unit , onChange :: Range.NumberRange -> Effect Unit
, throttleInterval :: Maybe Int -- then Nothing, no throttling is done
, status :: ComponentStatus ) , status :: ComponentStatus )
data Knob = MinKnob | MaxKnob data Knob = MinKnob | MaxKnob
...@@ -65,6 +67,11 @@ rangeSlider props = R.createElement rangeSliderCpt props [] ...@@ -65,6 +67,11 @@ rangeSlider props = R.createElement rangeSliderCpt props []
rangeSliderCpt :: R.Component Props rangeSliderCpt :: R.Component Props
rangeSliderCpt = here.component "rangeSlider" cpt where rangeSliderCpt = here.component "rangeSlider" cpt where
cpt props _ = do cpt props _ = do
let throttled = Debounce.throttle props.onChange (fromMaybe 0 props.throttleInterval)
let onChangeThrottled = case props.throttleInterval of
Nothing -> props.onChange
Just ti -> \rng -> Debounce.call throttled rng
-- rounding precision (i.e. how many decimal digits are in epsilon) -- rounding precision (i.e. how many decimal digits are in epsilon)
let (Range.Closed { min: minR, max: maxR }) = props.initialValue let (Range.Closed { min: minR, max: maxR }) = props.initialValue
let decPrecision num = let decPrecision num =
...@@ -116,7 +123,8 @@ rangeSliderCpt = here.component "rangeSlider" cpt where ...@@ -116,7 +123,8 @@ rangeSliderCpt = here.component "rangeSlider" cpt where
case reproject drag scalePos props.bounds props.epsilon (R2.domMousePosition event) of case reproject drag scalePos props.bounds props.epsilon (R2.domMousePosition event) of
Just val -> do Just val -> do
setKnob knob value value' val setKnob knob value value' val
props.onChange $ knobSetter knob value' val -- props.onChange $ knobSetter knob value' val
onChangeThrottled $ knobSetter knob value' val
Nothing -> destroy unit Nothing -> destroy unit
let onMouseUp = EL.callback $ \(_event :: Event.MouseEvent) -> do let onMouseUp = EL.callback $ \(_event :: Event.MouseEvent) -> do
--props.onChange $ knobSetter knob value val --props.onChange $ knobSetter knob value val
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment