adui_dioxus/components/
upload.rs

1use dioxus::{
2    html::events::{DragEvent, FormData},
3    prelude::*,
4};
5use dioxus_html::HasFileData;
6use std::{collections::HashMap, rc::Rc, time::SystemTime};
7
8#[cfg(target_arch = "wasm32")]
9use std::cell::RefCell;
10
11#[cfg(target_arch = "wasm32")]
12use {
13    js_sys::{Array, Uint8Array},
14    wasm_bindgen::{JsCast, closure::Closure},
15    wasm_bindgen_futures::spawn_local,
16    web_sys::{Blob, FormData as WebFormData, ProgressEvent, XmlHttpRequest},
17};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum UploadStatus {
21    Ready,
22    Uploading,
23    Done,
24    Error,
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub enum UploadListType {
29    #[default]
30    Text,
31    Picture,
32    PictureCard,
33}
34
35#[derive(Clone, Debug, PartialEq)]
36pub struct UploadFile {
37    pub uid: String,
38    pub name: String,
39    pub status: UploadStatus,
40    pub size: Option<u64>,
41    pub url: Option<String>,
42    pub error: Option<String>,
43    pub percent: Option<f32>,
44    pub response: Option<String>,
45}
46
47impl UploadFile {
48    pub fn done(name: impl Into<String>, size: Option<u64>) -> Self {
49        Self {
50            uid: format!("upload-{}", unique_id()),
51            name: name.into(),
52            status: UploadStatus::Done,
53            size,
54            url: None,
55            error: None,
56            percent: Some(100.0),
57            response: None,
58        }
59    }
60
61    pub fn uploading(name: impl Into<String>, size: Option<u64>) -> Self {
62        Self {
63            uid: format!("upload-{}", unique_id()),
64            name: name.into(),
65            status: UploadStatus::Uploading,
66            size,
67            url: None,
68            error: None,
69            percent: Some(0.0),
70            response: None,
71        }
72    }
73}
74
75#[derive(Clone, Debug, PartialEq)]
76pub struct UploadChangeInfo {
77    pub file: UploadFile,
78    pub file_list: Vec<UploadFile>,
79}
80
81#[derive(Clone, Debug, PartialEq)]
82pub struct UploadListConfig {
83    pub show_remove_icon: bool,
84}
85
86impl Default for UploadListConfig {
87    fn default() -> Self {
88        Self {
89            show_remove_icon: true,
90        }
91    }
92}
93
94#[derive(Clone, Debug)]
95pub struct UploadFileMeta {
96    pub name: String,
97    pub size: Option<u64>,
98    pub mime: Option<String>,
99}
100
101pub type BeforeUploadFn = Rc<dyn Fn(&UploadFileMeta) -> bool>;
102
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104pub enum UploadHttpMethod {
105    #[default]
106    Post,
107    Put,
108    Patch,
109}
110
111impl UploadHttpMethod {
112    #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
113    fn as_str(&self) -> &'static str {
114        match self {
115            UploadHttpMethod::Post => "POST",
116            UploadHttpMethod::Put => "PUT",
117            UploadHttpMethod::Patch => "PATCH",
118        }
119    }
120}
121
122/// Accept configuration for file type filtering.
123#[derive(Clone, Debug, PartialEq)]
124pub struct AcceptConfig {
125    /// MIME types (e.g., "image/*", "application/pdf").
126    pub mime_types: Option<Vec<String>>,
127    /// File extensions (e.g., [".jpg", ".png"]).
128    pub extensions: Option<Vec<String>>,
129}
130
131/// Upload request options for custom request handler.
132#[derive(Clone)]
133pub struct UploadRequestOptions {
134    pub file: UploadFileMeta,
135    pub action: String,
136    pub data: HashMap<String, String>,
137    pub headers: Vec<(String, String)>,
138    pub on_progress: Option<Rc<dyn Fn(f32)>>,
139    pub on_success: Option<Rc<dyn Fn(String)>>,
140    pub on_error: Option<Rc<dyn Fn(String)>>,
141}
142
143/// Progress configuration for upload progress display.
144#[derive(Clone, Debug, PartialEq)]
145pub struct UploadProgressConfig {
146    /// Stroke width for progress bar.
147    pub stroke_width: Option<f32>,
148    /// Show progress info.
149    pub show_info: Option<bool>,
150}
151
152/// Actions available in item render function.
153#[derive(Clone)]
154pub struct ItemActions {
155    pub download: Rc<dyn Fn()>,
156    pub preview: Rc<dyn Fn()>,
157    pub remove: Rc<dyn Fn()>,
158}
159
160/// Locale configuration for upload text.
161#[derive(Clone, Debug, Default, PartialEq)]
162pub struct UploadLocale {
163    pub uploading: Option<String>,
164    pub remove_file: Option<String>,
165    pub download_file: Option<String>,
166    pub upload_error: Option<String>,
167    pub preview_file: Option<String>,
168}
169
170#[derive(Props, Clone)]
171pub struct UploadProps {
172    /// Upload action URL. Can be a string or a function: (file) -> String
173    #[props(optional)]
174    pub action: Option<String>,
175    /// Upload action function: (file) -> String
176    #[props(optional)]
177    pub action_fn: Option<Rc<dyn Fn(&UploadFileMeta) -> String>>,
178    /// Whether to upload directory instead of files.
179    #[props(default)]
180    pub directory: bool,
181    #[props(default)]
182    pub multiple: bool,
183    #[props(default)]
184    pub disabled: bool,
185    #[props(default)]
186    pub list_type: UploadListType,
187    #[props(optional)]
188    pub field_name: Option<String>,
189    #[props(default)]
190    pub method: UploadHttpMethod,
191    #[props(default)]
192    pub with_credentials: bool,
193    #[props(optional)]
194    pub headers: Option<Vec<(String, String)>>,
195    /// Additional data to send with upload. Can be a map or a function: (file) -> HashMap
196    #[props(optional)]
197    pub data: Option<HashMap<String, String>>,
198    /// Data function: (file) -> HashMap
199    #[props(optional)]
200    pub data_fn: Option<Rc<dyn Fn(&UploadFile) -> HashMap<String, String>>>,
201    #[props(optional)]
202    pub accept: Option<String>,
203    /// Accept configuration object (mime types, extensions, etc.).
204    #[props(optional)]
205    pub accept_config: Option<AcceptConfig>,
206    #[props(optional)]
207    pub file_list: Option<Vec<UploadFile>>,
208    #[props(optional)]
209    pub default_file_list: Option<Vec<UploadFile>>,
210    #[props(optional)]
211    pub before_upload: Option<BeforeUploadFn>,
212    #[props(optional)]
213    pub on_change: Option<EventHandler<UploadChangeInfo>>,
214    #[props(optional)]
215    pub on_remove: Option<EventHandler<UploadFile>>,
216    /// Callback when file is dropped (for drag-and-drop).
217    #[props(optional)]
218    pub on_drop: Option<EventHandler<()>>,
219    /// Callback when file is previewed.
220    #[props(optional)]
221    pub on_preview: Option<EventHandler<UploadFile>>,
222    /// Callback when file is downloaded.
223    #[props(optional)]
224    pub on_download: Option<EventHandler<UploadFile>>,
225    #[props(optional)]
226    pub show_upload_list: Option<UploadListConfig>,
227    /// Custom upload request handler: (options, info) -> void
228    #[props(optional)]
229    pub custom_request: Option<Rc<dyn Fn(UploadRequestOptions)>>,
230    /// Preview file handler: (file) -> Promise<String>
231    #[props(optional)]
232    pub preview_file: Option<Rc<dyn Fn(&UploadFile) -> String>>,
233    /// Icon render function: (file, listType) -> Element
234    #[props(optional)]
235    pub icon_render: Option<Rc<dyn Fn(&UploadFile, UploadListType) -> Element>>,
236    /// Image URL check function: (file) -> bool
237    #[props(optional)]
238    pub is_image_url: Option<Rc<dyn Fn(&UploadFile) -> bool>>,
239    /// Progress configuration for upload progress display.
240    #[props(optional)]
241    pub progress: Option<UploadProgressConfig>,
242    /// Custom item render function: (originNode, file, fileList, actions) -> Element
243    #[props(optional)]
244    pub item_render:
245        Option<Rc<dyn Fn(Element, &UploadFile, &[UploadFile], ItemActions) -> Element>>,
246    /// Maximum number of files allowed.
247    #[props(optional)]
248    pub max_count: Option<usize>,
249    /// Whether to open file dialog on click.
250    #[props(default = true)]
251    pub open_file_dialog_on_click: bool,
252    /// Locale configuration for upload text.
253    #[props(optional)]
254    pub locale: Option<UploadLocale>,
255    #[props(optional)]
256    pub description: Option<Element>,
257    #[props(optional)]
258    pub class: Option<String>,
259    #[props(optional)]
260    pub style: Option<String>,
261    #[props(default)]
262    pub dragger: bool,
263    pub children: Element,
264}
265
266impl PartialEq for UploadProps {
267    fn eq(&self, other: &Self) -> bool {
268        self.action == other.action
269            && self.multiple == other.multiple
270            && self.disabled == other.disabled
271            && self.list_type == other.list_type
272            && self.field_name == other.field_name
273            && self.method == other.method
274            && self.with_credentials == other.with_credentials
275            && self.headers == other.headers
276            && self.accept == other.accept
277            && self.file_list == other.file_list
278            && self.default_file_list == other.default_file_list
279            && self.show_upload_list == other.show_upload_list
280            && self.description == other.description
281            && self.class == other.class
282            && self.style == other.style
283            && self.dragger == other.dragger
284            && self.children == other.children
285    }
286}
287
288#[component]
289#[allow(clippy::unit_arg, clippy::clone_on_copy)] // Non-wasm stub passes unit as request_store; cloning () is harmless and keeps the call shape aligned with the wasm implementation.
290pub fn Upload(props: UploadProps) -> Element {
291    let UploadProps {
292        action,
293        multiple,
294        disabled,
295        list_type,
296        field_name,
297        method,
298        with_credentials,
299        headers,
300        accept,
301        file_list,
302        default_file_list,
303        before_upload,
304        on_change,
305        on_remove,
306        show_upload_list,
307        description,
308        class,
309        style,
310        children,
311        dragger,
312        ..
313    } = props;
314    let field_name = field_name.unwrap_or_else(|| "file".to_string());
315
316    let controlled = file_list.is_some();
317    let initial_files = file_list
318        .clone()
319        .or_else(|| default_file_list.clone())
320        .unwrap_or_default();
321    let mut files_signal = use_signal(|| initial_files.clone());
322    if let Some(controlled_list) = file_list {
323        files_signal.set(controlled_list);
324    }
325
326    #[cfg(target_arch = "wasm32")]
327    let upload_requests =
328        use_hook(|| Rc::new(RefCell::new(HashMap::<String, XmlHttpRequest>::new())));
329    #[cfg(not(target_arch = "wasm32"))]
330    let _upload_requests = ();
331
332    let list_config = show_upload_list.unwrap_or_default();
333    let abort_upload: Rc<dyn Fn(&str)> = {
334        #[cfg(target_arch = "wasm32")]
335        {
336            let store = upload_requests.clone();
337            Rc::new(move |uid: &str| {
338                if let Some(xhr) = store.borrow_mut().remove(uid) {
339                    let _ = xhr.abort();
340                }
341            })
342        }
343        #[cfg(not(target_arch = "wasm32"))]
344        {
345            let _ = &_upload_requests;
346            Rc::new(|_: &str| {})
347        }
348    };
349    let input_id = format!("adui-upload-input-{}", unique_id());
350    let accept_attr = accept.unwrap_or_default();
351    let dragging = use_signal(|| false);
352    let class_attr = format!(
353        "adui-upload adui-upload-type-{} {}",
354        match list_type {
355            UploadListType::Text => "text",
356            UploadListType::Picture => "picture",
357            UploadListType::PictureCard => "picture-card",
358        },
359        class.unwrap_or_default()
360    );
361
362    let headers = Rc::new(headers.clone().unwrap_or_default());
363    let process_files = {
364        let headers = headers.clone();
365        #[cfg(target_arch = "wasm32")]
366        let request_store = upload_requests.clone();
367        #[cfg(not(target_arch = "wasm32"))]
368        let request_store = ();
369
370        Rc::new(move |files: Vec<dioxus_html::FileData>| {
371            if disabled || files.is_empty() {
372                return;
373            }
374            for file in files {
375                let meta = UploadFileMeta {
376                    name: file.name(),
377                    size: Some(file.size()),
378                    mime: file.content_type(),
379                };
380                if let Some(filter) = before_upload.as_ref()
381                    && !(filter)(&meta)
382                {
383                    continue;
384                }
385
386                let entry = if action.is_some() {
387                    UploadFile::uploading(meta.name.clone(), meta.size)
388                } else {
389                    UploadFile::done(meta.name.clone(), meta.size)
390                };
391                let uid = entry.uid.clone();
392
393                if let Some((changed, snapshot)) = mutate_files(files_signal, controlled, |list| {
394                    list.push(entry.clone());
395                    Some(entry.clone())
396                }) && let Some(handler) = on_change.as_ref()
397                {
398                    handler.call(UploadChangeInfo {
399                        file: changed,
400                        file_list: snapshot,
401                    });
402                }
403
404                if let Some(action_url) = action.clone() {
405                    start_upload_task(
406                        file.clone(),
407                        meta.clone(),
408                        uid,
409                        action_url,
410                        field_name.clone(),
411                        method,
412                        with_credentials,
413                        (*headers).clone(),
414                        files_signal,
415                        controlled,
416                        on_change,
417                        request_store.clone(),
418                    );
419                }
420            }
421        })
422    };
423
424    let onchange = {
425        let process_files = process_files.clone();
426        move |evt: Event<FormData>| {
427            if disabled {
428                return;
429            }
430            process_files(evt.files());
431        }
432    };
433
434    let mut selector_classes = vec!["adui-upload-selector".to_string()];
435    if disabled {
436        selector_classes.push("adui-upload-disabled".into());
437    }
438    if dragger {
439        selector_classes.push("adui-upload-dragger".into());
440        if *dragging.read() {
441            selector_classes.push("adui-upload-dragger-hover".into());
442        }
443    }
444    let selector_class = selector_classes.join(" ");
445    let mut dragging_for_over = dragging;
446    let mut dragging_for_leave = dragging;
447    let mut dragging_for_drop = dragging;
448    let process_files_drop = process_files.clone();
449
450    rsx! {
451        div { class: "{class_attr}", style: style.unwrap_or_default(),
452            label {
453                r#for: input_id.clone(),
454                class: "{selector_class}",
455                ondragover: move |evt: DragEvent| {
456                    if !dragger || disabled {
457                        return;
458                    }
459                    evt.prevent_default();
460                    dragging_for_over.set(true);
461                },
462                ondragleave: move |evt: DragEvent| {
463                    if !dragger || disabled {
464                        return;
465                    }
466                    evt.prevent_default();
467                    dragging_for_leave.set(false);
468                },
469                ondrop: move |evt: DragEvent| {
470                    if !dragger || disabled {
471                        return;
472                    }
473                    evt.prevent_default();
474                    dragging_for_drop.set(false);
475                    process_files_drop(evt.files());
476                },
477                {children}
478                if let Some(desc) = description {
479                    div { class: "adui-upload-description", {desc} }
480                }
481            }
482            input {
483                id: input_id,
484                r#type: "file",
485                multiple: multiple,
486                disabled: disabled,
487                accept: accept_attr,
488                onchange: onchange,
489                style: "display:none",
490            }
491            {render_upload_list(files_signal.read().clone(), list_type, list_config, files_signal, controlled, disabled, on_remove, on_change, abort_upload.clone())}
492        }
493    }
494}
495
496#[allow(clippy::too_many_arguments)] // Render helper mirrors Ant Design API surface for flexibility.
497fn render_upload_list(
498    files: Vec<UploadFile>,
499    list_type: UploadListType,
500    config: UploadListConfig,
501    files_signal: Signal<Vec<UploadFile>>,
502    controlled: bool,
503    disabled: bool,
504    on_remove: Option<EventHandler<UploadFile>>,
505    on_change: Option<EventHandler<UploadChangeInfo>>,
506    abort_upload: Rc<dyn Fn(&str)>,
507) -> Element {
508    if files.is_empty() {
509        return rsx! { div {} };
510    }
511    rsx! {
512        ul { class: format!("adui-upload-list adui-upload-list-{}", match list_type {
513                UploadListType::Text => "text",
514                UploadListType::Picture => "picture",
515                UploadListType::PictureCard => "picture-card",
516            }),
517            {files.into_iter().map(|file| {
518                let file_entry = file.clone();
519                let file_for_remove = file.clone();
520                let abort_upload = abort_upload.clone();
521                rsx!(li { key: "{file_entry.uid}", class: "adui-upload-list-item",
522                    span { class: "adui-upload-list-item-name", "{file_entry.name}" }
523                    if config.show_remove_icon {
524                        button {
525                            r#type: "button",
526                            class: "adui-upload-list-item-remove",
527                            onclick: move |_| {
528                                if disabled {
529                                    return;
530                                }
531                                abort_upload(&file_for_remove.uid);
532                                if let Some((removed, snapshot)) =
533                                    mutate_files(files_signal, controlled, |list| {
534                                        list.iter()
535                                            .position(|f| f.uid == file_for_remove.uid)
536                                            .map(|pos| list.remove(pos))
537                                    }) {
538                                    if let Some(handler) = on_remove.as_ref() {
539                                        handler.call(removed.clone());
540                                    }
541                                    if let Some(handler) = on_change.as_ref() {
542                                        handler.call(UploadChangeInfo {
543                                            file: removed,
544                                            file_list: snapshot,
545                                        });
546                                    }
547                                }
548                            },
549                            "删除"
550                        }
551                    }
552                    if let Some(err) = file_entry.error.clone() {
553                        span { class: "adui-upload-list-item-error", "{err}" }
554                    }
555                })
556            })}
557        }
558    }
559}
560
561fn mutate_files(
562    mut files_signal: Signal<Vec<UploadFile>>,
563    controlled: bool,
564    mutator: impl FnOnce(&mut Vec<UploadFile>) -> Option<UploadFile>,
565) -> Option<(UploadFile, Vec<UploadFile>)> {
566    let mut list = files_signal.read().clone();
567    let changed = mutator(&mut list)?;
568    if !controlled {
569        files_signal.set(list.clone());
570    }
571    Some((changed, list))
572}
573
574fn update_file_state(
575    files_signal: Signal<Vec<UploadFile>>,
576    controlled: bool,
577    uid: &str,
578    on_change: Option<EventHandler<UploadChangeInfo>>,
579    mut updater: impl FnMut(&mut UploadFile),
580) {
581    if let Some((changed, snapshot)) = mutate_files(files_signal, controlled, |list| {
582        list.iter_mut().find(|item| item.uid == uid).map(|entry| {
583            updater(entry);
584            entry.clone()
585        })
586    }) && let Some(handler) = on_change
587    {
588        handler.call(UploadChangeInfo {
589            file: changed,
590            file_list: snapshot,
591        });
592    }
593}
594
595#[cfg(target_arch = "wasm32")]
596#[allow(clippy::too_many_arguments)] // Upload pipeline needs all parameters; grouped for wasi boundary.
597fn start_upload_task(
598    file: dioxus_html::FileData,
599    meta: UploadFileMeta,
600    uid: String,
601    action: String,
602    field_name: String,
603    method: UploadHttpMethod,
604    with_credentials: bool,
605    headers: Vec<(String, String)>,
606    files_signal: Signal<Vec<UploadFile>>,
607    controlled: bool,
608    on_change: Option<EventHandler<UploadChangeInfo>>,
609    request_store: Rc<RefCell<HashMap<String, XmlHttpRequest>>>,
610) {
611    spawn_local(async move {
612        let bytes = match file.read_bytes().await {
613            Ok(data) => data,
614            Err(err) => {
615                update_file_state(files_signal, controlled, &uid, on_change, |entry| {
616                    entry.status = UploadStatus::Error;
617                    entry.error = Some(err.to_string());
618                });
619                return;
620            }
621        };
622
623        let xhr = match XmlHttpRequest::new() {
624            Ok(req) => req,
625            Err(_) => {
626                update_file_state(files_signal, controlled, &uid, on_change, |entry| {
627                    entry.status = UploadStatus::Error;
628                    entry.error = Some("无法创建请求".into());
629                });
630                return;
631            }
632        };
633
634        if xhr.open_with_async(method.as_str(), &action, true).is_err() {
635            update_file_state(files_signal, controlled, &uid, on_change, |entry| {
636                entry.status = UploadStatus::Error;
637                entry.error = Some("打开上传连接失败".into());
638            });
639            return;
640        }
641        xhr.set_with_credentials(with_credentials);
642        for (key, value) in headers.iter() {
643            let _ = xhr.set_request_header(key, value);
644        }
645        request_store.borrow_mut().insert(uid.clone(), xhr.clone());
646
647        let progress_signal = files_signal;
648        let progress_uid = uid.clone();
649        let progress_on_change = on_change;
650        let progress_closure =
651            Closure::<dyn FnMut(ProgressEvent)>::wrap(Box::new(move |event: ProgressEvent| {
652                if event.length_computable() {
653                    let total = event.total();
654                    if total > 0.0 {
655                        let percent = ((event.loaded() / total) * 100.0).clamp(0.0, 100.0) as f32;
656                        update_file_state(
657                            progress_signal,
658                            controlled,
659                            &progress_uid,
660                            progress_on_change,
661                            |entry| entry.percent = Some(percent),
662                        );
663                    }
664                }
665            }));
666        if let Ok(upload) = xhr.upload() {
667            upload.set_onprogress(Some(progress_closure.as_ref().unchecked_ref()));
668        }
669        progress_closure.forget();
670
671        let success_signal = files_signal;
672        let success_uid = uid.clone();
673        let success_on_change = on_change;
674        let success_store = request_store.clone();
675        let xhr_clone = xhr.clone();
676        let load_closure =
677            Closure::<dyn FnMut(_)>::wrap(Box::new(move |_event: web_sys::Event| {
678                success_store.borrow_mut().remove(&success_uid);
679                let status = xhr_clone.status().unwrap_or(0);
680                let response = xhr_clone.response_text().ok().flatten();
681                if (200..300).contains(&status) {
682                    update_file_state(
683                        success_signal,
684                        controlled,
685                        &success_uid,
686                        success_on_change,
687                        |entry| {
688                            entry.status = UploadStatus::Done;
689                            entry.percent = Some(100.0);
690                            entry.response = response.clone();
691                            entry.error = None;
692                        },
693                    );
694                } else {
695                    update_file_state(
696                        success_signal,
697                        controlled,
698                        &success_uid,
699                        success_on_change,
700                        |entry| {
701                            entry.status = UploadStatus::Error;
702                            entry.error = Some(format!("HTTP {}", xhr_clone.status().unwrap_or(0)));
703                        },
704                    );
705                }
706            }));
707        xhr.set_onload(Some(load_closure.as_ref().unchecked_ref()));
708        load_closure.forget();
709
710        let error_signal = files_signal;
711        let error_uid = uid.clone();
712        let error_on_change = on_change;
713        let error_store = request_store.clone();
714        let error_closure =
715            Closure::<dyn FnMut(_)>::wrap(Box::new(move |_event: web_sys::Event| {
716                error_store.borrow_mut().remove(&error_uid);
717                update_file_state(
718                    error_signal,
719                    controlled,
720                    &error_uid,
721                    error_on_change,
722                    |entry| {
723                        entry.status = UploadStatus::Error;
724                        entry.error = Some("上传失败".into());
725                    },
726                );
727            }));
728        xhr.set_onerror(Some(error_closure.as_ref().unchecked_ref()));
729        xhr.set_onabort(Some(error_closure.as_ref().unchecked_ref()));
730        error_closure.forget();
731
732        let mut array = Uint8Array::new_with_length(bytes.len() as u32);
733        array.copy_from(bytes.as_ref());
734        let buffer = array.buffer();
735        let sequence = Array::new();
736        sequence.push(&buffer);
737        let blob = Blob::new_with_u8_array_sequence(&sequence).unwrap();
738        let form = WebFormData::new().unwrap();
739        form.append_with_blob_and_filename(&field_name, &blob, &meta.name)
740            .unwrap();
741        let _ = xhr.send_with_opt_form_data(Some(&form));
742    });
743}
744
745#[cfg(not(target_arch = "wasm32"))]
746#[allow(clippy::too_many_arguments)] // Stub keeps the same signature as the web implementation for API parity.
747fn start_upload_task(
748    _file: dioxus_html::FileData,
749    _meta: UploadFileMeta,
750    uid: String,
751    _action: String,
752    _field_name: String,
753    _method: UploadHttpMethod,
754    _with_credentials: bool,
755    _headers: Vec<(String, String)>,
756    files_signal: Signal<Vec<UploadFile>>,
757    controlled: bool,
758    on_change: Option<EventHandler<UploadChangeInfo>>,
759    _request_store: (),
760) {
761    update_file_state(files_signal, controlled, &uid, on_change, |entry| {
762        entry.status = UploadStatus::Error;
763        entry.error = Some("Upload is only supported on web targets".into());
764    });
765}
766
767fn unique_id() -> u128 {
768    #[cfg(target_arch = "wasm32")]
769    {
770        (js_sys::Date::now() * 1000.0) as u128
771    }
772    #[cfg(not(target_arch = "wasm32"))]
773    {
774        SystemTime::now()
775            .duration_since(SystemTime::UNIX_EPOCH)
776            .map(|d| d.as_micros())
777            .unwrap_or_default()
778    }
779}
780
781#[cfg(test)]
782mod upload_tests {
783    use super::*;
784
785    #[test]
786    fn upload_status_all_variants() {
787        assert_eq!(UploadStatus::Ready, UploadStatus::Ready);
788        assert_eq!(UploadStatus::Uploading, UploadStatus::Uploading);
789        assert_eq!(UploadStatus::Done, UploadStatus::Done);
790        assert_eq!(UploadStatus::Error, UploadStatus::Error);
791        assert_ne!(UploadStatus::Ready, UploadStatus::Done);
792    }
793
794    #[test]
795    fn upload_list_type_default() {
796        assert_eq!(UploadListType::default(), UploadListType::Text);
797    }
798
799    #[test]
800    fn upload_list_type_all_variants() {
801        assert_eq!(UploadListType::Text, UploadListType::Text);
802        assert_eq!(UploadListType::Picture, UploadListType::Picture);
803        assert_eq!(UploadListType::PictureCard, UploadListType::PictureCard);
804        assert_ne!(UploadListType::Text, UploadListType::Picture);
805    }
806
807    #[test]
808    fn upload_http_method_default() {
809        assert_eq!(UploadHttpMethod::default(), UploadHttpMethod::Post);
810    }
811
812    #[test]
813    fn upload_http_method_all_variants() {
814        assert_eq!(UploadHttpMethod::Post, UploadHttpMethod::Post);
815        assert_eq!(UploadHttpMethod::Put, UploadHttpMethod::Put);
816        assert_eq!(UploadHttpMethod::Patch, UploadHttpMethod::Patch);
817        assert_ne!(UploadHttpMethod::Post, UploadHttpMethod::Put);
818    }
819
820    #[test]
821    fn upload_http_method_as_str() {
822        #[cfg(target_arch = "wasm32")]
823        {
824            assert_eq!(UploadHttpMethod::Post.as_str(), "POST");
825            assert_eq!(UploadHttpMethod::Put.as_str(), "PUT");
826            assert_eq!(UploadHttpMethod::Patch.as_str(), "PATCH");
827        }
828    }
829
830    #[test]
831    fn upload_file_done() {
832        let file = UploadFile::done("test.txt", Some(1024));
833        assert_eq!(file.name, "test.txt");
834        assert_eq!(file.size, Some(1024));
835        assert_eq!(file.status, UploadStatus::Done);
836        assert_eq!(file.percent, Some(100.0));
837        assert!(file.uid.starts_with("upload-"));
838    }
839
840    #[test]
841    fn upload_file_uploading() {
842        let file = UploadFile::uploading("test.txt", Some(1024));
843        assert_eq!(file.name, "test.txt");
844        assert_eq!(file.size, Some(1024));
845        assert_eq!(file.status, UploadStatus::Uploading);
846        assert_eq!(file.percent, Some(0.0));
847        assert!(file.uid.starts_with("upload-"));
848    }
849
850    #[test]
851    fn upload_list_config_default() {
852        let config = UploadListConfig::default();
853        assert_eq!(config.show_remove_icon, true);
854    }
855
856    #[test]
857    #[cfg(not(target_arch = "wasm32"))]
858    fn unique_id_generates_value() {
859        let id1 = unique_id();
860        // Small delay to ensure different timestamps
861        std::thread::sleep(std::time::Duration::from_millis(1));
862        let id2 = unique_id();
863        // IDs should be different (or at least non-zero)
864        assert!(id1 > 0 || id2 > 0);
865    }
866}