Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
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
Expand Down Expand Up @@ -1828,7 +1828,10 @@ async def upload_file(request: Request):
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.")
Expand Down
64 changes: 48 additions & 16 deletions reflex/components/core/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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
Expand All @@ -28,6 +29,7 @@
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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -266,25 +271,46 @@ def create(cls, *children, **props) -> Component:
# 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"])
Expand All @@ -303,7 +329,13 @@ def create(cls, *children, **props) -> Component:
}
)

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(
Expand Down
2 changes: 2 additions & 0 deletions tests/units/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import pytest
import sqlmodel
from fastapi.responses import StreamingResponse
from pytest_mock import MockerFixture
from starlette.applications import Starlette
from starlette.datastructures import UploadFile
Expand Down Expand Up @@ -830,6 +831,7 @@ def getlist(key: str):

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
Expand Down
Loading