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#[derive(Clone, Debug, PartialEq)]
124pub struct AcceptConfig {
125 pub mime_types: Option<Vec<String>>,
127 pub extensions: Option<Vec<String>>,
129}
130
131#[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#[derive(Clone, Debug, PartialEq)]
145pub struct UploadProgressConfig {
146 pub stroke_width: Option<f32>,
148 pub show_info: Option<bool>,
150}
151
152#[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#[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 #[props(optional)]
174 pub action: Option<String>,
175 #[props(optional)]
177 pub action_fn: Option<Rc<dyn Fn(&UploadFileMeta) -> String>>,
178 #[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 #[props(optional)]
197 pub data: Option<HashMap<String, String>>,
198 #[props(optional)]
200 pub data_fn: Option<Rc<dyn Fn(&UploadFile) -> HashMap<String, String>>>,
201 #[props(optional)]
202 pub accept: Option<String>,
203 #[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 #[props(optional)]
218 pub on_drop: Option<EventHandler<()>>,
219 #[props(optional)]
221 pub on_preview: Option<EventHandler<UploadFile>>,
222 #[props(optional)]
224 pub on_download: Option<EventHandler<UploadFile>>,
225 #[props(optional)]
226 pub show_upload_list: Option<UploadListConfig>,
227 #[props(optional)]
229 pub custom_request: Option<Rc<dyn Fn(UploadRequestOptions)>>,
230 #[props(optional)]
232 pub preview_file: Option<Rc<dyn Fn(&UploadFile) -> String>>,
233 #[props(optional)]
235 pub icon_render: Option<Rc<dyn Fn(&UploadFile, UploadListType) -> Element>>,
236 #[props(optional)]
238 pub is_image_url: Option<Rc<dyn Fn(&UploadFile) -> bool>>,
239 #[props(optional)]
241 pub progress: Option<UploadProgressConfig>,
242 #[props(optional)]
244 pub item_render:
245 Option<Rc<dyn Fn(Element, &UploadFile, &[UploadFile], ItemActions) -> Element>>,
246 #[props(optional)]
248 pub max_count: Option<usize>,
249 #[props(default = true)]
251 pub open_file_dialog_on_click: bool,
252 #[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)] pub 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)] fn 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)] fn 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)] fn 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 std::thread::sleep(std::time::Duration::from_millis(1));
862 let id2 = unique_id();
863 assert!(id1 > 0 || id2 > 0);
865 }
866}