From c79719f7befa3b09e4578f039d69ca5faf247c29 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 5 Mar 2024 12:07:36 -0800 Subject: [PATCH] Expose `on_drop` event trigger for rx.upload component. (#2766) * Expose `on_drop` event trigger for rx.upload component. If `on_drop` is provided, it should be an EventSpec accepting the special rx.upload_files() arg. When `on_drop` is provided, it will be called immediately when files are selected. The default functionality of saving a file list to be uploaded later will not be available. * update pyi file * Undeprecate explicit EventChain --- reflex/components/component.py | 9 ----- reflex/components/core/upload.py | 63 ++++++++++++++++++++++++++++--- reflex/components/core/upload.pyi | 11 +++++- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 0b5af5681..cf6f67c82 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -374,21 +374,12 @@ class Component(BaseComponent, ABC): arg_spec = triggers.get(event_trigger, lambda: []) - wrapped = False # If the input is a single event handler, wrap it in a list. if isinstance(value, (EventHandler, EventSpec)): - wrapped = True value = [value] # If the input is a list of event handlers, create an event chain. if isinstance(value, List): - if not wrapped: - console.deprecate( - feature_name="EventChain", - reason="to avoid confusion, only use yield API", - deprecation_version="0.2.8", - removal_version="0.5.0", - ) events: list[EventSpec] = [] for v in value: if isinstance(v, EventHandler): diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 4a1be832b..0d8eaea03 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -3,14 +3,21 @@ from __future__ import annotations import os from pathlib import Path -from typing import Any, ClassVar, Dict, List, Optional, Union +from typing import Any, Callable, ClassVar, Dict, List, Optional, Union from reflex import constants from reflex.components.chakra.forms.input import Input from reflex.components.chakra.layout.box import Box from reflex.components.component import Component, MemoizationLeaf from reflex.constants import Dirs -from reflex.event import CallableEventSpec, EventChain, EventSpec, call_script +from reflex.event import ( + CallableEventSpec, + EventChain, + EventSpec, + call_event_fn, + call_script, + parse_args_spec, +) from reflex.utils import imports from reflex.vars import BaseVar, CallableVar, Var, VarData @@ -135,6 +142,18 @@ def get_upload_url(file_path: str) -> str: return f"{uploaded_files_url_prefix}/{file_path}" +def _on_drop_spec(files: Var): + """Args spec for the on_drop event trigger. + + Args: + files: The files to upload. + + Returns: + Signature for on_drop handler including the files to upload. + """ + return [files] + + class UploadFilesProvider(Component): """AppWrap component that provides a dict of selected files by ID via useContext.""" @@ -198,7 +217,7 @@ class Upload(MemoizationLeaf): cls.is_used = True # get only upload component props - supported_props = cls.get_props() + supported_props = cls.get_props().union({"on_drop"}) upload_props = { key: value for key, value in props.items() if key in supported_props } @@ -218,8 +237,27 @@ class Upload(MemoizationLeaf): # Create the component. upload_props["id"] = props.get("id", DEFAULT_UPLOAD_ID) + + if upload_props.get("on_drop") is None: + # 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, Callable): + # Call the lambda to get the event chain. + on_drop = call_event_fn(on_drop, _on_drop_spec) # type: ignore + 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 + ), + ) + upload_props["on_drop"] = on_drop return super().create( - zone, on_drop=upload_file(upload_props["id"]), **upload_props + zone, + **upload_props, ) def get_event_triggers(self) -> dict[str, Union[Var, Any]]: @@ -230,9 +268,24 @@ class Upload(MemoizationLeaf): """ return { **super().get_event_triggers(), - constants.EventTriggers.ON_DROP: lambda e0: [e0], + constants.EventTriggers.ON_DROP: _on_drop_spec, } + @classmethod + def _update_arg_tuple_for_on_drop(cls, arg_value: tuple[Var, Var]): + """Helper to update caller-provided EventSpec args for direct use with on_drop. + + Args: + arg_value: The arg tuple to update (if necessary). + + Returns: + The updated arg_value tuple when arg is "files", otherwise the original arg_value. + """ + if arg_value[0]._var_name == "files": + placeholder = parse_args_spec(_on_drop_spec)[0] + return (arg_value[0], placeholder) + return arg_value + def _render(self): out = super()._render() out.args = ("getRootProps", "getInputProps") diff --git a/reflex/components/core/upload.pyi b/reflex/components/core/upload.pyi index d8e450144..8614c6617 100644 --- a/reflex/components/core/upload.pyi +++ b/reflex/components/core/upload.pyi @@ -9,13 +9,20 @@ from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style import os from pathlib import Path -from typing import Any, ClassVar, Dict, List, Optional, Union +from typing import Any, Callable, ClassVar, Dict, List, Optional, Union from reflex import constants from reflex.components.chakra.forms.input import Input from reflex.components.chakra.layout.box import Box from reflex.components.component import Component, MemoizationLeaf from reflex.constants import Dirs -from reflex.event import CallableEventSpec, EventChain, EventSpec, call_script +from reflex.event import ( + CallableEventSpec, + EventChain, + EventSpec, + call_event_fn, + call_script, + parse_args_spec, +) from reflex.utils import imports from reflex.vars import BaseVar, CallableVar, Var, VarData