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/demos/basic.py b/src/demos/basic.py index 19ea203..2d315c2 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 65a15cf..2e594ed 100644 --- a/src/interval_sdk/classes/io.py +++ b/src/interval_sdk/classes/io.py @@ -45,6 +45,7 @@ SelectTableState, InputEmailProps, InputNumberProps, + InputSliderProps, InputBooleanProps, InputRichTextProps, InputUrlProps, @@ -279,6 +280,72 @@ def get_value(val: float): return InputIOPromise(c, renderer=self._renderer, get_value=get_value) + @overload + def slider( + self, + label: str, + *, + min: int, + max: int, + step: Optional[int] = None, + help_text: Optional[str] = None, + default_value: Optional[int] = None, + disabled: Optional[bool] = None, + ) -> InputIOPromise[Literal["INPUT_SLIDER"], int]: + ... + + @overload + def slider( + self, + label: str, + *, + min: Union[int, float], + max: Union[int, float], + step: Optional[Union[int, float]] = None, + help_text: Optional[str] = None, + default_value: Optional[Union[int, float]] = None, + disabled: Optional[bool] = None, + ) -> InputIOPromise[Literal["INPUT_SLIDER"], float]: + ... + + def slider( + self, + label: str, + *, + 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, + 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 ( + 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 + + 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 f6e3a03..22b57b5 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", @@ -531,6 +532,15 @@ class InputNumberProps(BaseModel): disabled: Optional[bool] +class InputSliderProps(BaseModel): + min: Union[float, int] + max: 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] @@ -814,6 +824,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 200ec6e..1c37783 100644 --- a/src/tests/test_main.py +++ b/src/tests/test_main.py @@ -586,6 +586,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 ):