From e418aac95618d2d0d593aaf5cd8cb882b30c6c79 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 19 Sep 2025 19:18:55 -0700 Subject: [PATCH] ENG-7732: plumb through `Upload.on_drop_rejected` Allow user to supply an event handler for dropped files that do not meet the defined criteria. By default it will display an error toast with the file names and rejection reasons. --- pyi_hashes.json | 2 +- reflex/components/core/upload.py | 67 +++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index 16e1ded11f4..a9bf03f6e17 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -19,7 +19,7 @@ "reflex/components/core/helmet.pyi": "43f8497c8fafe51e29dca1dd535d143a", "reflex/components/core/html.pyi": "ea5919db8c8172913185977df900f36b", "reflex/components/core/sticky.pyi": "a9b4492e423f1dd4ccbf270c8ea90157", - "reflex/components/core/upload.pyi": "360fb929edf960aca289a37d0433fc38", + "reflex/components/core/upload.pyi": "77e828bbc55dd6593efdba1504e0cb5e", "reflex/components/core/window_events.pyi": "76bf03a273a1fbbb3b333e10d5d08c30", "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", "reflex/components/datadisplay/code.pyi": "b86769987ef4d1cbdddb461be88539fd", diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index a920779c85e..0187ee1e88c 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -17,6 +17,7 @@ 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.components.sonner.toast import toast from reflex.constants import Dirs from reflex.constants.compiler import Hooks, Imports from reflex.environment import environment @@ -36,7 +37,8 @@ from reflex.vars import VarData from reflex.vars.base import Var, get_unique_variable_name from reflex.vars.function import FunctionVar -from reflex.vars.sequence import LiteralStringVar +from reflex.vars.object import ObjectVar +from reflex.vars.sequence import ArrayVar, LiteralStringVar DEFAULT_UPLOAD_ID: str = "default" @@ -178,6 +180,34 @@ def _on_drop_spec(files: Var) -> tuple[Var[Any]]: return (files,) +def _default_drop_rejected(rejected_files: ArrayVar[list[dict[str, Any]]]) -> EventSpec: + """Event handler for showing a toast with rejected file info. + + Args: + rejected_files: The files that were rejected. + + Returns: + An event spec that shows a toast with the rejected file info when triggered. + """ + + def _format_rejected_file_record(rf: ObjectVar[dict[str, Any]]) -> str: + rf = rf.to(ObjectVar, dict[str, dict[str, Any]]) + file = rf["file"].to(ObjectVar, dict[str, Any]) + errors = rf["errors"].to(ArrayVar, list[dict[str, Any]]) + return ( + f"{file['path']}: {errors.foreach(lambda err: err['message']).join(', ')}" + ) + + return toast.error( + title="Files not Accepted", + description=rejected_files.to(ArrayVar) + .foreach(_format_rejected_file_record) + .join("\n\n"), + close_button=True, + style={"white_space": "pre-line"}, + ) + + class UploadFilesProvider(Component): """AppWrap component that provides a dict of selected files by ID via useContext.""" @@ -191,6 +221,9 @@ class GhostUpload(Fragment): # Fired when files are dropped. on_drop: EventHandler[_on_drop_spec] + # Fired when dropped files do not meet the specified criteria. + on_drop_rejected: EventHandler[_on_drop_spec] + class Upload(MemoizationLeaf): """A file upload component.""" @@ -234,6 +267,9 @@ class Upload(MemoizationLeaf): # Fired when files are dropped. on_drop: EventHandler[_on_drop_spec] + # Fired when dropped files do not meet the specified criteria. + on_drop_rejected: EventHandler[_on_drop_spec] + # Style rules to apply when actively dragging. drag_active_style: Style | None = field(default=None, is_javascript_property=False) @@ -295,6 +331,10 @@ def create(cls, *children, **props) -> Component: on_drop[ix] = event upload_props["on_drop"] = on_drop + if upload_props.get("on_drop_rejected") is None: + # If on_drop_rejected is not provided, show an error toast. + upload_props["on_drop_rejected"] = _default_drop_rejected + 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() @@ -313,22 +353,22 @@ def create(cls, *children, **props) -> Component: ), ) - event_var, callback_str = StatefulComponent._get_memoized_event_triggers( - GhostUpload.create(on_drop=upload_props["on_drop"]) - )["on_drop"] - - upload_props["on_drop"] = event_var + event_triggers = StatefulComponent._get_memoized_event_triggers( + GhostUpload.create( + on_drop=upload_props["on_drop"], + on_drop_rejected=upload_props["on_drop_rejected"], + ) + ) + callback_hooks = [] + for trigger_name, (event_var, callback_str) in event_triggers.items(): + upload_props[trigger_name] = event_var + callback_hooks.append(callback_str) upload_props = { format.to_camel_case(key): value for key, value in upload_props.items() } - use_dropzone_arguments = Var.create( - { - "onDrop": event_var, - **upload_props, - } - ) + use_dropzone_arguments = Var.create(upload_props) left_side = ( "const { " @@ -344,11 +384,10 @@ def create(cls, *children, **props) -> Component: imports=Imports.EVENTS, hooks={Hooks.EVENTS: None}, ), - event_var._get_all_var_data(), use_dropzone_arguments._get_all_var_data(), VarData( hooks={ - callback_str: None, + **dict.fromkeys(callback_hooks, None), f"{left_side} = {right_side};": None, }, imports={