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={