Skip to content

Commit a3c14e7

Browse files
authored
ENG-7732: plumb through Upload.on_drop_rejected (#5806)
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.
1 parent 21f7629 commit a3c14e7

File tree

2 files changed

+54
-15
lines changed

2 files changed

+54
-15
lines changed

pyi_hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"reflex/components/core/helmet.pyi": "43f8497c8fafe51e29dca1dd535d143a",
2020
"reflex/components/core/html.pyi": "ea5919db8c8172913185977df900f36b",
2121
"reflex/components/core/sticky.pyi": "a9b4492e423f1dd4ccbf270c8ea90157",
22-
"reflex/components/core/upload.pyi": "360fb929edf960aca289a37d0433fc38",
22+
"reflex/components/core/upload.pyi": "77e828bbc55dd6593efdba1504e0cb5e",
2323
"reflex/components/core/window_events.pyi": "76bf03a273a1fbbb3b333e10d5d08c30",
2424
"reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e",
2525
"reflex/components/datadisplay/code.pyi": "b86769987ef4d1cbdddb461be88539fd",

reflex/components/core/upload.py

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from reflex.components.core.cond import cond
1818
from reflex.components.el.elements.forms import Input
1919
from reflex.components.radix.themes.layout.box import Box
20+
from reflex.components.sonner.toast import toast
2021
from reflex.constants import Dirs
2122
from reflex.constants.compiler import Hooks, Imports
2223
from reflex.environment import environment
@@ -36,7 +37,8 @@
3637
from reflex.vars import VarData
3738
from reflex.vars.base import Var, get_unique_variable_name
3839
from reflex.vars.function import FunctionVar
39-
from reflex.vars.sequence import LiteralStringVar
40+
from reflex.vars.object import ObjectVar
41+
from reflex.vars.sequence import ArrayVar, LiteralStringVar
4042

4143
DEFAULT_UPLOAD_ID: str = "default"
4244

@@ -178,6 +180,34 @@ def _on_drop_spec(files: Var) -> tuple[Var[Any]]:
178180
return (files,)
179181

180182

183+
def _default_drop_rejected(rejected_files: ArrayVar[list[dict[str, Any]]]) -> EventSpec:
184+
"""Event handler for showing a toast with rejected file info.
185+
186+
Args:
187+
rejected_files: The files that were rejected.
188+
189+
Returns:
190+
An event spec that shows a toast with the rejected file info when triggered.
191+
"""
192+
193+
def _format_rejected_file_record(rf: ObjectVar[dict[str, Any]]) -> str:
194+
rf = rf.to(ObjectVar, dict[str, dict[str, Any]])
195+
file = rf["file"].to(ObjectVar, dict[str, Any])
196+
errors = rf["errors"].to(ArrayVar, list[dict[str, Any]])
197+
return (
198+
f"{file['path']}: {errors.foreach(lambda err: err['message']).join(', ')}"
199+
)
200+
201+
return toast.error(
202+
title="Files not Accepted",
203+
description=rejected_files.to(ArrayVar)
204+
.foreach(_format_rejected_file_record)
205+
.join("\n\n"),
206+
close_button=True,
207+
style={"white_space": "pre-line"},
208+
)
209+
210+
181211
class UploadFilesProvider(Component):
182212
"""AppWrap component that provides a dict of selected files by ID via useContext."""
183213

@@ -191,6 +221,9 @@ class GhostUpload(Fragment):
191221
# Fired when files are dropped.
192222
on_drop: EventHandler[_on_drop_spec]
193223

224+
# Fired when dropped files do not meet the specified criteria.
225+
on_drop_rejected: EventHandler[_on_drop_spec]
226+
194227

195228
class Upload(MemoizationLeaf):
196229
"""A file upload component."""
@@ -234,6 +267,9 @@ class Upload(MemoizationLeaf):
234267
# Fired when files are dropped.
235268
on_drop: EventHandler[_on_drop_spec]
236269

270+
# Fired when dropped files do not meet the specified criteria.
271+
on_drop_rejected: EventHandler[_on_drop_spec]
272+
237273
# Style rules to apply when actively dragging.
238274
drag_active_style: Style | None = field(default=None, is_javascript_property=False)
239275

@@ -295,6 +331,10 @@ def create(cls, *children, **props) -> Component:
295331
on_drop[ix] = event
296332
upload_props["on_drop"] = on_drop
297333

334+
if upload_props.get("on_drop_rejected") is None:
335+
# If on_drop_rejected is not provided, show an error toast.
336+
upload_props["on_drop_rejected"] = _default_drop_rejected
337+
298338
input_props_unique_name = get_unique_variable_name()
299339
root_props_unique_name = get_unique_variable_name()
300340
is_drag_active_unique_name = get_unique_variable_name()
@@ -313,22 +353,22 @@ def create(cls, *children, **props) -> Component:
313353
),
314354
)
315355

316-
event_var, callback_str = StatefulComponent._get_memoized_event_triggers(
317-
GhostUpload.create(on_drop=upload_props["on_drop"])
318-
)["on_drop"]
319-
320-
upload_props["on_drop"] = event_var
356+
event_triggers = StatefulComponent._get_memoized_event_triggers(
357+
GhostUpload.create(
358+
on_drop=upload_props["on_drop"],
359+
on_drop_rejected=upload_props["on_drop_rejected"],
360+
)
361+
)
362+
callback_hooks = []
363+
for trigger_name, (event_var, callback_str) in event_triggers.items():
364+
upload_props[trigger_name] = event_var
365+
callback_hooks.append(callback_str)
321366

322367
upload_props = {
323368
format.to_camel_case(key): value for key, value in upload_props.items()
324369
}
325370

326-
use_dropzone_arguments = Var.create(
327-
{
328-
"onDrop": event_var,
329-
**upload_props,
330-
}
331-
)
371+
use_dropzone_arguments = Var.create(upload_props)
332372

333373
left_side = (
334374
"const { "
@@ -344,11 +384,10 @@ def create(cls, *children, **props) -> Component:
344384
imports=Imports.EVENTS,
345385
hooks={Hooks.EVENTS: None},
346386
),
347-
event_var._get_all_var_data(),
348387
use_dropzone_arguments._get_all_var_data(),
349388
VarData(
350389
hooks={
351-
callback_str: None,
390+
**dict.fromkeys(callback_hooks, None),
352391
f"{left_side} = {right_side};": None,
353392
},
354393
imports={

0 commit comments

Comments
 (0)