Skip to content

Commit c90b73a

Browse files
authored
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.
1 parent 26f1a7b commit c90b73a

4 files changed

Lines changed: 56 additions & 19 deletions

File tree

pyi_hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"reflex/components/core/debounce.pyi": "affda049624c266c7d5620efa3b7041b",
2121
"reflex/components/core/html.pyi": "b12117b42ef79ee90b6b4dec50baeb86",
2222
"reflex/components/core/sticky.pyi": "c65131cf7c2312c68e1fddaa0cc27150",
23-
"reflex/components/core/upload.pyi": "53e06193fa23a603737bc49b1c6c2565",
23+
"reflex/components/core/upload.pyi": "4680da6f7b3df704a682cc6441b1ac18",
2424
"reflex/components/datadisplay/__init__.pyi": "cf087efa8b3960decc6b231cc986cfa9",
2525
"reflex/components/datadisplay/code.pyi": "3d8f0ab4c2f123d7f80d15c7ebc553d9",
2626
"reflex/components/datadisplay/dataeditor.pyi": "cb03d732e2fe771a8d46c7bcda671f92",

reflex/app.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from starlette.datastructures import UploadFile as StarletteUploadFile
3030
from starlette.exceptions import HTTPException
3131
from starlette.middleware import cors
32-
from starlette.requests import Request
32+
from starlette.requests import ClientDisconnect, Request
3333
from starlette.responses import JSONResponse, Response, StreamingResponse
3434
from starlette.staticfiles import StaticFiles
3535
from typing_extensions import deprecated
@@ -1828,7 +1828,10 @@ async def upload_file(request: Request):
18281828
from reflex.utils.exceptions import UploadTypeError, UploadValueError
18291829

18301830
# Get the files from the request.
1831-
files = await request.form()
1831+
try:
1832+
files = await request.form()
1833+
except ClientDisconnect:
1834+
return Response() # user cancelled
18321835
files = files.getlist("files")
18331836
if not files:
18341837
raise UploadValueError("No files were uploaded.")

reflex/components/core/upload.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
MemoizationLeaf,
1414
StatefulComponent,
1515
)
16+
from reflex.components.core.cond import cond
1617
from reflex.components.el.elements.forms import Input
1718
from reflex.components.radix.themes.layout.box import Box
1819
from reflex.config import environment
@@ -28,6 +29,7 @@
2829
parse_args_spec,
2930
run_script,
3031
)
32+
from reflex.style import Style
3133
from reflex.utils import format
3234
from reflex.utils.imports import ImportVar
3335
from reflex.vars import VarData
@@ -231,6 +233,9 @@ class Upload(MemoizationLeaf):
231233
# Fired when files are dropped.
232234
on_drop: EventHandler[_on_drop_spec]
233235

236+
# Style rules to apply when actively dragging.
237+
drag_active_style: Style | None = None
238+
234239
@classmethod
235240
def create(cls, *children, **props) -> Component:
236241
"""Create an upload component.
@@ -266,25 +271,46 @@ def create(cls, *children, **props) -> Component:
266271
# If on_drop is not provided, save files to be uploaded later.
267272
upload_props["on_drop"] = upload_file(upload_props["id"])
268273
else:
269-
on_drop = upload_props["on_drop"]
270-
if isinstance(on_drop, (EventHandler, EventSpec)):
271-
# Call the lambda to get the event chain.
272-
on_drop = call_event_handler(on_drop, _on_drop_spec)
273-
elif isinstance(on_drop, Callable):
274-
# Call the lambda to get the event chain.
275-
on_drop = call_event_fn(on_drop, _on_drop_spec)
276-
if isinstance(on_drop, EventSpec):
277-
# Update the provided args for direct use with on_drop.
278-
on_drop = on_drop.with_args(
279-
args=tuple(
280-
cls._update_arg_tuple_for_on_drop(arg_value)
281-
for arg_value in on_drop.args
282-
),
283-
)
274+
on_drop = (
275+
[on_drop_prop]
276+
if not isinstance(on_drop_prop := upload_props["on_drop"], Sequence)
277+
else list(on_drop_prop)
278+
)
279+
for ix, event in enumerate(on_drop):
280+
if isinstance(event, (EventHandler, EventSpec)):
281+
# Call the lambda to get the event chain.
282+
event = call_event_handler(event, _on_drop_spec)
283+
elif isinstance(event, Callable):
284+
# Call the lambda to get the event chain.
285+
event = call_event_fn(event, _on_drop_spec)
286+
if isinstance(event, EventSpec):
287+
# Update the provided args for direct use with on_drop.
288+
event = event.with_args(
289+
args=tuple(
290+
cls._update_arg_tuple_for_on_drop(arg_value)
291+
for arg_value in event.args
292+
),
293+
)
294+
on_drop[ix] = event
284295
upload_props["on_drop"] = on_drop
285296

286297
input_props_unique_name = get_unique_variable_name()
287298
root_props_unique_name = get_unique_variable_name()
299+
is_drag_active_unique_name = get_unique_variable_name()
300+
drag_active_css_class_unique_name = get_unique_variable_name() + "-drag-active"
301+
302+
# Handle special style when dragging over the drop zone.
303+
if "drag_active_style" in props:
304+
props.setdefault("style", Style())[
305+
f"&:where(.{drag_active_css_class_unique_name})"
306+
] = props.pop("drag_active_style")
307+
props["class_name"].append(
308+
cond(
309+
Var(is_drag_active_unique_name),
310+
drag_active_css_class_unique_name,
311+
"",
312+
),
313+
)
288314

289315
event_var, callback_str = StatefulComponent._get_memoized_event_triggers(
290316
GhostUpload.create(on_drop=upload_props["on_drop"])
@@ -303,7 +329,13 @@ def create(cls, *children, **props) -> Component:
303329
}
304330
)
305331

306-
left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} "
332+
left_side = (
333+
"const { "
334+
f"getRootProps: {root_props_unique_name}, "
335+
f"getInputProps: {input_props_unique_name}, "
336+
f"isDragActive: {is_drag_active_unique_name}"
337+
"}"
338+
)
307339
right_side = f"useDropzone({use_dropzone_arguments!s})"
308340

309341
var_data = VarData.merge(

tests/units/test_app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import pytest
1616
import sqlmodel
17+
from fastapi.responses import StreamingResponse
1718
from pytest_mock import MockerFixture
1819
from starlette.applications import Starlette
1920
from starlette.datastructures import UploadFile
@@ -830,6 +831,7 @@ def getlist(key: str):
830831

831832
upload_fn = upload(app)
832833
streaming_response = await upload_fn(request_mock)
834+
assert isinstance(streaming_response, StreamingResponse)
833835
async for state_update in streaming_response.body_iterator:
834836
assert (
835837
state_update

0 commit comments

Comments
 (0)