1
0
Эх сурвалжийг харах

Upload: drag_active_style and multiple on_drop specs (#5207)

* Allow the user to specify `drag_active_style` that will be applied when a
  file is dragged over the dropzone.
* If the user provides a list of EventHandler/EventSpec, ensure that the
  `files` arg is updated to the on_drop param name for all of the given
  handlers.
Masen Furer 2 долоо хоног өмнө
parent
commit
c90b73af83

+ 1 - 1
pyi_hashes.json

@@ -20,7 +20,7 @@
   "reflex/components/core/debounce.pyi": "affda049624c266c7d5620efa3b7041b",
   "reflex/components/core/html.pyi": "b12117b42ef79ee90b6b4dec50baeb86",
   "reflex/components/core/sticky.pyi": "c65131cf7c2312c68e1fddaa0cc27150",
-  "reflex/components/core/upload.pyi": "53e06193fa23a603737bc49b1c6c2565",
+  "reflex/components/core/upload.pyi": "4680da6f7b3df704a682cc6441b1ac18",
   "reflex/components/datadisplay/__init__.pyi": "cf087efa8b3960decc6b231cc986cfa9",
   "reflex/components/datadisplay/code.pyi": "3d8f0ab4c2f123d7f80d15c7ebc553d9",
   "reflex/components/datadisplay/dataeditor.pyi": "cb03d732e2fe771a8d46c7bcda671f92",

+ 5 - 2
reflex/app.py

@@ -29,7 +29,7 @@ from starlette.datastructures import Headers
 from starlette.datastructures import UploadFile as StarletteUploadFile
 from starlette.exceptions import HTTPException
 from starlette.middleware import cors
-from starlette.requests import Request
+from starlette.requests import ClientDisconnect, Request
 from starlette.responses import JSONResponse, Response, StreamingResponse
 from starlette.staticfiles import StaticFiles
 from typing_extensions import deprecated
@@ -1828,7 +1828,10 @@ def upload(app: App):
         from reflex.utils.exceptions import UploadTypeError, UploadValueError
 
         # Get the files from the request.
-        files = await request.form()
+        try:
+            files = await request.form()
+        except ClientDisconnect:
+            return Response()  # user cancelled
         files = files.getlist("files")
         if not files:
             raise UploadValueError("No files were uploaded.")

+ 48 - 16
reflex/components/core/upload.py

@@ -13,6 +13,7 @@ from reflex.components.component import (
     MemoizationLeaf,
     StatefulComponent,
 )
+from reflex.components.core.cond import cond
 from reflex.components.el.elements.forms import Input
 from reflex.components.radix.themes.layout.box import Box
 from reflex.config import environment
@@ -28,6 +29,7 @@ from reflex.event import (
     parse_args_spec,
     run_script,
 )
+from reflex.style import Style
 from reflex.utils import format
 from reflex.utils.imports import ImportVar
 from reflex.vars import VarData
@@ -231,6 +233,9 @@ class Upload(MemoizationLeaf):
     # Fired when files are dropped.
     on_drop: EventHandler[_on_drop_spec]
 
+    # Style rules to apply when actively dragging.
+    drag_active_style: Style | None = None
+
     @classmethod
     def create(cls, *children, **props) -> Component:
         """Create an upload component.
@@ -266,25 +271,46 @@ class Upload(MemoizationLeaf):
             # If on_drop is not provided, save files to be uploaded later.
             upload_props["on_drop"] = upload_file(upload_props["id"])
         else:
-            on_drop = upload_props["on_drop"]
-            if isinstance(on_drop, (EventHandler, EventSpec)):
-                # Call the lambda to get the event chain.
-                on_drop = call_event_handler(on_drop, _on_drop_spec)
-            elif isinstance(on_drop, Callable):
-                # Call the lambda to get the event chain.
-                on_drop = call_event_fn(on_drop, _on_drop_spec)
-            if isinstance(on_drop, EventSpec):
-                # Update the provided args for direct use with on_drop.
-                on_drop = on_drop.with_args(
-                    args=tuple(
-                        cls._update_arg_tuple_for_on_drop(arg_value)
-                        for arg_value in on_drop.args
-                    ),
-                )
+            on_drop = (
+                [on_drop_prop]
+                if not isinstance(on_drop_prop := upload_props["on_drop"], Sequence)
+                else list(on_drop_prop)
+            )
+            for ix, event in enumerate(on_drop):
+                if isinstance(event, (EventHandler, EventSpec)):
+                    # Call the lambda to get the event chain.
+                    event = call_event_handler(event, _on_drop_spec)
+                elif isinstance(event, Callable):
+                    # Call the lambda to get the event chain.
+                    event = call_event_fn(event, _on_drop_spec)
+                if isinstance(event, EventSpec):
+                    # Update the provided args for direct use with on_drop.
+                    event = event.with_args(
+                        args=tuple(
+                            cls._update_arg_tuple_for_on_drop(arg_value)
+                            for arg_value in event.args
+                        ),
+                    )
+                on_drop[ix] = event
             upload_props["on_drop"] = on_drop
 
         input_props_unique_name = get_unique_variable_name()
         root_props_unique_name = get_unique_variable_name()
+        is_drag_active_unique_name = get_unique_variable_name()
+        drag_active_css_class_unique_name = get_unique_variable_name() + "-drag-active"
+
+        # Handle special style when dragging over the drop zone.
+        if "drag_active_style" in props:
+            props.setdefault("style", Style())[
+                f"&:where(.{drag_active_css_class_unique_name})"
+            ] = props.pop("drag_active_style")
+            props["class_name"].append(
+                cond(
+                    Var(is_drag_active_unique_name),
+                    drag_active_css_class_unique_name,
+                    "",
+                ),
+            )
 
         event_var, callback_str = StatefulComponent._get_memoized_event_triggers(
             GhostUpload.create(on_drop=upload_props["on_drop"])
@@ -303,7 +329,13 @@ class Upload(MemoizationLeaf):
             }
         )
 
-        left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} "
+        left_side = (
+            "const { "
+            f"getRootProps: {root_props_unique_name}, "
+            f"getInputProps: {input_props_unique_name}, "
+            f"isDragActive: {is_drag_active_unique_name}"
+            "}"
+        )
         right_side = f"useDropzone({use_dropzone_arguments!s})"
 
         var_data = VarData.merge(

+ 2 - 0
tests/units/test_app.py

@@ -14,6 +14,7 @@ from unittest.mock import AsyncMock
 
 import pytest
 import sqlmodel
+from fastapi.responses import StreamingResponse
 from pytest_mock import MockerFixture
 from starlette.applications import Starlette
 from starlette.datastructures import UploadFile
@@ -830,6 +831,7 @@ async def test_upload_file(tmp_path, state, delta, token: str, mocker):
 
     upload_fn = upload(app)
     streaming_response = await upload_fn(request_mock)
+    assert isinstance(streaming_response, StreamingResponse)
     async for state_update in streaming_response.body_iterator:
         assert (
             state_update