diff --git a/integration/test_upload.py b/integration/test_upload.py index c43d0ba1a..44fec6b46 100644 --- a/integration/test_upload.py +++ b/integration/test_upload.py @@ -49,7 +49,7 @@ def UploadFile(): id="token", ), rx.heading("Default Upload"), - rx.upload( + rx.upload.root( rx.vstack( rx.button("Select File"), rx.text("Drag and drop files here or click to select files"), @@ -73,7 +73,7 @@ def UploadFile(): id="clear_button", ), rx.heading("Secondary Upload"), - rx.upload( + rx.upload.root( rx.vstack( rx.button("Select File"), rx.text("Drag and drop files here or click to select files"), diff --git a/reflex/components/core/__init__.py b/reflex/components/core/__init__.py index 3bc27a157..80c73add8 100644 --- a/reflex/components/core/__init__.py +++ b/reflex/components/core/__init__.py @@ -16,7 +16,7 @@ from .responsive import ( tablet_only, ) from .upload import ( - Upload, + UploadNamespace, cancel_upload, clear_selected_files, get_upload_dir, @@ -31,4 +31,4 @@ debounce_input = DebounceInput.create foreach = Foreach.create html = Html.create match = Match.create -upload = Upload.create +upload = UploadNamespace() diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 343553d1e..a652f9462 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -9,7 +9,7 @@ from typing import Any, Callable, ClassVar, Dict, List, Optional, Union from reflex import constants from reflex.components.chakra.forms.input import Input from reflex.components.chakra.layout.box import Box -from reflex.components.component import Component, MemoizationLeaf +from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf from reflex.constants import Dirs from reflex.event import ( CallableEventSpec, @@ -299,3 +299,38 @@ class Upload(MemoizationLeaf): return { (5, "UploadFilesProvider"): UploadFilesProvider.create(), } + + +class StyledUpload(Upload): + """The styled Upload Component.""" + + @classmethod + def create(cls, *children, **props) -> Component: + """Create the styled upload component. + + Args: + *children: The children of the component. + **props: The properties of the component. + + Returns: + The styled upload component. + """ + # Set default props. + props.setdefault("border", "1px dashed var(--accent-12)") + props.setdefault("padding", "5em") + props.setdefault("textAlign", "center") + + # Mark the Upload component as used in the app. + Upload.is_used = True + + return super().create( + *children, + **props, + ) + + +class UploadNamespace(ComponentNamespace): + """Upload component namespace.""" + + root = Upload.create + __call__ = StyledUpload.create diff --git a/reflex/components/core/upload.pyi b/reflex/components/core/upload.pyi index ce069181a..e415761e0 100644 --- a/reflex/components/core/upload.pyi +++ b/reflex/components/core/upload.pyi @@ -13,7 +13,7 @@ from typing import Any, Callable, ClassVar, Dict, List, Optional, Union from reflex import constants from reflex.components.chakra.forms.input import Input from reflex.components.chakra.layout.box import Box -from reflex.components.component import Component, MemoizationLeaf +from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf from reflex.constants import Dirs from reflex.event import ( CallableEventSpec, @@ -219,3 +219,201 @@ class Upload(MemoizationLeaf): """ ... def get_event_triggers(self) -> dict[str, Union[Var, Any]]: ... + +class StyledUpload(Upload): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + accept: Optional[ + Union[Var[Optional[Dict[str, List]]], Optional[Dict[str, List]]] + ] = None, + disabled: Optional[Union[Var[bool], bool]] = None, + max_files: Optional[Union[Var[int], int]] = None, + max_size: Optional[Union[Var[int], int]] = None, + min_size: Optional[Union[Var[int], int]] = None, + multiple: Optional[Union[Var[bool], bool]] = None, + no_click: Optional[Union[Var[bool], bool]] = None, + no_drag: Optional[Union[Var[bool], bool]] = None, + no_keyboard: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, + on_blur: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_click: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_context_menu: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_double_click: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_drop: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_focus: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mount: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_down: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_enter: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_leave: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_move: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_out: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_over: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_up: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_scroll: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_unmount: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + **props + ) -> "StyledUpload": + """Create the styled upload component. + + Args: + *children: The children of the component. + accept: The list of accepted file types. This should be a dictionary of MIME types as keys and array of file formats as values. supported MIME types: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + disabled: Whether the dropzone is disabled. + max_files: The maximum number of files that can be uploaded. + max_size: The maximum file size (bytes) that can be uploaded. + min_size: The minimum file size (bytes) that can be uploaded. + multiple: Whether to allow multiple files to be uploaded. + no_click: Whether to disable click to upload. + no_drag: Whether to disable drag and drop. + no_keyboard: Whether to disable using the space/enter keys to upload. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The styled upload component. + """ + ... + +class UploadNamespace(ComponentNamespace): + root = Upload.create + + @staticmethod + def __call__( + *children, + accept: Optional[ + Union[Var[Optional[Dict[str, List]]], Optional[Dict[str, List]]] + ] = None, + disabled: Optional[Union[Var[bool], bool]] = None, + max_files: Optional[Union[Var[int], int]] = None, + max_size: Optional[Union[Var[int], int]] = None, + min_size: Optional[Union[Var[int], int]] = None, + multiple: Optional[Union[Var[bool], bool]] = None, + no_click: Optional[Union[Var[bool], bool]] = None, + no_drag: Optional[Union[Var[bool], bool]] = None, + no_keyboard: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, + on_blur: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_click: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_context_menu: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_double_click: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_drop: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_focus: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mount: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_down: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_enter: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_leave: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_move: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_out: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_over: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_up: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_scroll: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_unmount: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + **props + ) -> "StyledUpload": + """Create the styled upload component. + + Args: + *children: The children of the component. + accept: The list of accepted file types. This should be a dictionary of MIME types as keys and array of file formats as values. supported MIME types: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + disabled: Whether the dropzone is disabled. + max_files: The maximum number of files that can be uploaded. + max_size: The maximum file size (bytes) that can be uploaded. + min_size: The minimum file size (bytes) that can be uploaded. + multiple: Whether to allow multiple files to be uploaded. + no_click: Whether to disable click to upload. + no_drag: Whether to disable drag and drop. + no_keyboard: Whether to disable using the space/enter keys to upload. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The styled upload component. + """ + ... diff --git a/tests/components/core/test_upload.py b/tests/components/core/test_upload.py index 7dd0aedd4..e6b27effc 100644 --- a/tests/components/core/test_upload.py +++ b/tests/components/core/test_upload.py @@ -1,5 +1,7 @@ from reflex.components.core.upload import ( + StyledUpload, Upload, + UploadNamespace, _on_drop_spec, # type: ignore cancel_upload, get_upload_url, @@ -77,3 +79,47 @@ def test_upload_create(): ) assert isinstance(up_comp_4, Upload) assert up_comp_4.is_used + + +def test_styled_upload_create(): + styled_up_comp_1 = StyledUpload.create() + assert isinstance(styled_up_comp_1, StyledUpload) + assert styled_up_comp_1.is_used + + # reset is_used + StyledUpload.is_used = False + + styled_up_comp_2 = StyledUpload.create( + id="foo_id", + on_drop=TestUploadState.drop_handler([]), # type: ignore + ) + assert isinstance(styled_up_comp_2, StyledUpload) + assert styled_up_comp_2.is_used + + # reset is_used + StyledUpload.is_used = False + + styled_up_comp_3 = StyledUpload.create( + id="foo_id", + on_drop=TestUploadState.drop_handler, + ) + assert isinstance(styled_up_comp_3, StyledUpload) + assert styled_up_comp_3.is_used + + # reset is_used + StyledUpload.is_used = False + + styled_up_comp_4 = StyledUpload.create( + id="foo_id", + on_drop=TestUploadState.not_drop_handler([]), # type: ignore + ) + assert isinstance(styled_up_comp_4, StyledUpload) + assert styled_up_comp_4.is_used + + +def test_upload_namespace(): + up_ns = UploadNamespace() + assert isinstance(up_ns, UploadNamespace) + + assert isinstance(up_ns(id="foo_id"), StyledUpload) + assert isinstance(up_ns.root(id="foo_id"), Upload) diff --git a/tests/components/forms/test_uploads.py b/tests/components/forms/test_uploads.py index 5bf212219..36e1ebac7 100644 --- a/tests/components/forms/test_uploads.py +++ b/tests/components/forms/test_uploads.py @@ -3,6 +3,24 @@ import pytest import reflex as rx +@pytest.fixture +def upload_root_component(): + """A test upload component function. + + Returns: + A test upload component function. + """ + + def upload_root_component(): + return rx.upload.root( + rx.button("select file"), + rx.text("Drag and drop files here or click to select files"), + border="1px dotted black", + ) + + return upload_root_component() + + @pytest.fixture def upload_component(): """A test upload component function. @@ -41,13 +59,13 @@ def upload_component_with_props(): return upload_component_with_props() -def test_upload_component_render(upload_component): +def test_upload_root_component_render(upload_root_component): """Test that the render function is set correctly. Args: - upload_component: component fixture + upload_root_component: component fixture """ - upload = upload_component.render() + upload = upload_root_component.render() # upload assert upload["name"] == "ReactDropzone" @@ -82,6 +100,47 @@ def test_upload_component_render(upload_component): ) +def test_upload_component_render(upload_component): + """Test that the render function is set correctly. + + Args: + upload_component: component fixture + """ + upload = upload_component.render() + + # upload + assert upload["name"] == "ReactDropzone" + assert upload["props"] == [ + "id={`default`}", + "multiple={true}", + "onDrop={e => setFilesById(filesById => ({...filesById, default: e}))}", + "ref={ref_default}", + ] + assert upload["args"] == ("getRootProps", "getInputProps") + + # box inside of upload + [box] = upload["children"] + assert box["name"] == "Box" + assert box["props"] == [ + 'sx={{"border": "1px dotted black", "padding": "5em", "textAlign": "center"}}', + "{...getRootProps()}", + ] + + # input, button and text inside of box + [input, button, text] = box["children"] + assert input["name"] == "Input" + assert input["props"] == ["type={`file`}", "{...getInputProps()}"] + + assert button["name"] == "RadixThemesButton" + assert button["children"][0]["contents"] == "{`select file`}" + + assert text["name"] == "RadixThemesText" + assert ( + text["children"][0]["contents"] + == "{`Drag and drop files here or click to select files`}" + ) + + def test_upload_component_with_props_render(upload_component_with_props): """Test that the render function is set correctly.