From 77fef3bac2608ae73e851e16626305ee2d5aa671 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 12 Jul 2023 16:22:17 -0500 Subject: [PATCH 1/2] Add python implementation and tests for input.slider --- pyproject.toml | 2 +- src/interval_sdk/classes/io.py | 62 ++++++++++++++++++++++ src/interval_sdk/io_schema.py | 15 ++++++ src/tests/test_main.py | 94 ++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac812e8..b02ccb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "interval-sdk" -version = "1.4.4" +version = "1.5.0-dev0" description = "The frontendless framework for high growth companies. Interval automatically generates apps by inlining the UI in your backend code. It's a faster and more maintainable way to build internal tools, rapid prototypes, and more." authors = [ "Jacob Mischka ", diff --git a/src/interval_sdk/classes/io.py b/src/interval_sdk/classes/io.py index c948db4..0ae9c11 100644 --- a/src/interval_sdk/classes/io.py +++ b/src/interval_sdk/classes/io.py @@ -44,6 +44,7 @@ SelectTableState, InputEmailProps, InputNumberProps, + InputSliderProps, InputBooleanProps, InputRichTextProps, InputUrlProps, @@ -278,6 +279,67 @@ def get_value(val: float): return InputIOPromise(c, renderer=self._renderer, get_value=get_value) + @overload + def slider( + self, + label: str, + *, + min: Optional[Union[float, int]] = None, + max: Optional[Union[float, int]] = None, + step: Optional[int] = None, + help_text: Optional[str] = None, + default_value: Optional[Union[float, int]] = None, + disabled: Optional[bool] = None, + ) -> InputIOPromise[Literal["INPUT_SLIDER"], int]: + ... + + @overload + def slider( + self, + label: str, + *, + min: Optional[Union[float, int]] = None, + max: Optional[Union[float, int]] = None, + step: Optional[float] = None, + help_text: Optional[str] = None, + default_value: Optional[Union[float, int]] = None, + disabled: Optional[bool] = None, + ) -> InputIOPromise[Literal["INPUT_SLIDER"], int]: + ... + + def slider( + self, + label: str, + *, + min: Optional[Union[float, int]] = None, + max: Optional[Union[float, int]] = None, + step: Optional[Union[float, int]] = None, + help_text: Optional[str] = None, + default_value: Optional[Union[float, int]] = None, + disabled: Optional[bool] = None, + ): + c = Component( + method_name="INPUT_SLIDER", + label=label, + initial_props=InputSliderProps( + min=min, + max=max, + step=step, + help_text=help_text, + default_value=default_value, + disabled=disabled, + ), + display_resolves_immediately=self._display_resolves_immediately, + ) + + def get_value(val: float): + if step is None or isinstance(step, int): + return int(val) + + return val + + return InputIOPromise(c, renderer=self._renderer, get_value=get_value) + def boolean( self, label: str, diff --git a/src/interval_sdk/io_schema.py b/src/interval_sdk/io_schema.py index 0076269..ef5e591 100644 --- a/src/interval_sdk/io_schema.py +++ b/src/interval_sdk/io_schema.py @@ -51,6 +51,7 @@ "INPUT_TEXT", "INPUT_EMAIL", "INPUT_NUMBER", + "INPUT_SLIDER", "INPUT_BOOLEAN", "INPUT_RICH_TEXT", "INPUT_SPREADSHEET", @@ -530,6 +531,15 @@ class InputNumberProps(BaseModel): disabled: Optional[bool] +class InputSliderProps(BaseModel): + min: Optional[Union[float, int]] + max: Optional[Union[float, int]] + step: Optional[Union[float, int]] + help_text: Optional[str] + default_value: Optional[Union[float, int]] + disabled: Optional[bool] + + class InputBooleanProps(BaseModel): help_text: Optional[str] default_value: Optional[str] @@ -809,6 +819,11 @@ class SearchState(BaseModel): state=None, returns=float, ), + "INPUT_SLIDER": MethodDef( + props=InputSliderProps, + state=None, + returns=float, + ), "INPUT_BOOLEAN": MethodDef( props=InputBooleanProps, state=None, diff --git a/src/tests/test_main.py b/src/tests/test_main.py index d5fc450..082e67d 100644 --- a/src/tests/test_main.py +++ b/src/tests/test_main.py @@ -580,6 +580,100 @@ async def currency(io: IO): ) +class TestInputSlider: + async def test_input_slider_entry( + self, interval: Interval, page: BrowserPage, transactions: Transaction + ): + @interval.action + async def input_slider_entry(io: IO): + num1 = await io.input.slider( + "Enter a number between 1-100", + min=1, + max=100, + ) + + decimal = await io.input.slider( + "Select a decimal value", + min=num1, + max=num1 + 1, + step=0.1, + ) + + return {"num1": num1, "decimal": decimal, "sum": num1 + decimal} + + await transactions.console() + await transactions.run("input_slider_entry") + + await transactions.press_continue() + await transactions.expect_validation_error() + + await page.locator('input[type="range"]').last.fill("12") + await transactions.press_continue() + + await expect(page.locator('text="Select a decimal value"')).to_be_visible() + await page.locator('input[type="range"]').last.fill("12.5") + await transactions.press_continue() + await transactions.expect_success() + + async def test_input_slider_keyboard_entry( + self, interval: Interval, page: BrowserPage, transactions: Transaction + ): + @interval.action + async def input_slider_keyboard_entry(io: IO): + num1 = await io.input.slider( + "Enter a number between 1-100", + min=1, + max=100, + ) + + decimal = await io.input.slider( + "Select a decimal value", + min=num1, + max=num1 + 1, + step=0.1, + ) + + return {"num1": num1, "decimal": decimal, "sum": num1 + decimal} + + def last_range(): + return page.locator('input[type="range"]').last + + await transactions.console() + await transactions.run("input_slider_keyboard_entry") + + await transactions.press_continue() + await transactions.expect_validation_error() + + # each keystroke should be treated as a separate input, the threshold is 1.5s + await last_range().focus() + await last_range().type( + "79", delay=2000 + ) # intentional delay to test functionality + + assert await last_range().input_value() == "9" + + await transactions.press_continue() + + await expect(page.locator('text="Select a decimal value"')).to_be_visible() + + # this should be treated as a single input, "15" + await last_range().focus() + await last_range().type( + "15", delay=1000 + ) # intentional delay to test functionality + await transactions.press_continue() + await transactions.expect_validation_error( + "Please enter a number between 9 and 10." + ) + + await last_range().focus() + await last_range().type("9.5") + await transactions.press_continue() + await transactions.expect_success() + + # Not currently implementing Input entry test, it mainly tests the UI and waitForFunction is confusing in python + + async def test_input_rich_text( interval: Interval, page: BrowserPage, transactions: Transaction ): From 61678d7a06cc23575830c7507426aae92359c7b2 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Thu, 13 Jul 2023 13:15:31 -0500 Subject: [PATCH 2/2] Fix python definition for input.slider, require min/max and fix types --- src/demos/basic.py | 15 +++++++++++++++ src/interval_sdk/classes/io.py | 27 ++++++++++++++++----------- src/interval_sdk/io_schema.py | 4 ++-- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/demos/basic.py b/src/demos/basic.py index 7240cbf..f089d44 100644 --- a/src/demos/basic.py +++ b/src/demos/basic.py @@ -171,6 +171,21 @@ async def hello_interval(): return {"hello": "from python!"} +@interval.action +async def default_slider(io: IO): + nint = await io.input.slider("Ints", min=0, max=10) + nfloat = await io.input.slider("Floats", min=0.0, max=1.0, step=0.1) + ndefault = await io.input.slider("Default", min=0, max=10, default_value=2.0) + + print(nint, nfloat, ndefault) + + return { + "nint": nint, + "nfloat": nfloat, + "ndefault": ndefault, + } + + async def manual_action_handler(io: IO): await io.display.markdown("IO works!") diff --git a/src/interval_sdk/classes/io.py b/src/interval_sdk/classes/io.py index 0ae9c11..64e817d 100644 --- a/src/interval_sdk/classes/io.py +++ b/src/interval_sdk/classes/io.py @@ -284,11 +284,11 @@ def slider( self, label: str, *, - min: Optional[Union[float, int]] = None, - max: Optional[Union[float, int]] = None, + min: int, + max: int, step: Optional[int] = None, help_text: Optional[str] = None, - default_value: Optional[Union[float, int]] = None, + default_value: Optional[int] = None, disabled: Optional[bool] = None, ) -> InputIOPromise[Literal["INPUT_SLIDER"], int]: ... @@ -298,21 +298,21 @@ def slider( self, label: str, *, - min: Optional[Union[float, int]] = None, - max: Optional[Union[float, int]] = None, - step: Optional[float] = None, + min: Union[int, float], + max: Union[int, float], + step: Optional[Union[int, float]] = None, help_text: Optional[str] = None, - default_value: Optional[Union[float, int]] = None, + default_value: Optional[Union[int, float]] = None, disabled: Optional[bool] = None, - ) -> InputIOPromise[Literal["INPUT_SLIDER"], int]: + ) -> InputIOPromise[Literal["INPUT_SLIDER"], float]: ... def slider( self, label: str, *, - min: Optional[Union[float, int]] = None, - max: Optional[Union[float, int]] = None, + min: Union[float, int], + max: Union[float, int], step: Optional[Union[float, int]] = None, help_text: Optional[str] = None, default_value: Optional[Union[float, int]] = None, @@ -333,7 +333,12 @@ def slider( ) def get_value(val: float): - if step is None or isinstance(step, int): + if ( + isinstance(min, int) + and isinstance(max, int) + and (step is None or isinstance(step, int)) + and (default_value is None or isinstance(default_value, int)) + ): return int(val) return val diff --git a/src/interval_sdk/io_schema.py b/src/interval_sdk/io_schema.py index ef5e591..52a76bd 100644 --- a/src/interval_sdk/io_schema.py +++ b/src/interval_sdk/io_schema.py @@ -532,8 +532,8 @@ class InputNumberProps(BaseModel): class InputSliderProps(BaseModel): - min: Optional[Union[float, int]] - max: Optional[Union[float, int]] + min: Union[float, int] + max: Union[float, int] step: Optional[Union[float, int]] help_text: Optional[str] default_value: Optional[Union[float, int]]