1use crate::components::checkbox::Checkbox;
11use crate::components::config_provider::ComponentSize;
12use crate::components::empty::Empty;
13use crate::components::icon::{Icon, IconKind};
14use crate::components::pagination::Pagination;
15use crate::components::spin::Spin;
16use crate::foundation::{
17 ClassListExt, StyleStringExt, TableClassNames, TableSemantic, TableStyles,
18};
19use dioxus::prelude::*;
20use serde_json::Value;
21use std::collections::HashMap;
22use std::rc::Rc;
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
26pub enum ColumnAlign {
27 #[default]
28 Left,
29 Center,
30 Right,
31}
32
33impl ColumnAlign {
34 fn as_class(&self) -> &'static str {
35 match self {
36 ColumnAlign::Left => "adui-table-align-left",
37 ColumnAlign::Center => "adui-table-align-center",
38 ColumnAlign::Right => "adui-table-align-right",
39 }
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum SortOrder {
46 Ascend,
47 Descend,
48}
49
50impl SortOrder {
51 fn as_class(&self) -> &'static str {
52 match self {
53 SortOrder::Ascend => "adui-table-column-sort-ascend",
54 SortOrder::Descend => "adui-table-column-sort-descend",
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum ColumnFixed {
62 Left,
63 Right,
64}
65
66pub type ColumnRenderFn = fn(Option<&Value>, &Value, usize) -> Element;
69
70#[derive(Clone, Debug, PartialEq)]
72pub struct ColumnFilter {
73 pub text: String,
74 pub value: String,
75}
76
77pub type ColumnFilterFn = fn(&str, &Value) -> bool;
80
81pub type ColumnSorterFn = fn(&Value, &Value) -> std::cmp::Ordering;
84
85#[derive(Clone)]
87pub struct TableColumn {
88 pub key: String,
89 pub title: String,
90 pub data_index: Option<String>,
91 pub width: Option<f32>,
92 pub align: Option<ColumnAlign>,
93 pub fixed: Option<ColumnFixed>,
94 pub sortable: bool,
96 pub default_sort_order: Option<SortOrder>,
98 #[allow(clippy::type_complexity)]
100 pub sorter: Option<ColumnSorterFn>,
101 pub filters: Option<Vec<ColumnFilter>>,
103 #[allow(clippy::type_complexity)]
105 pub on_filter: Option<ColumnFilterFn>,
106 #[allow(clippy::type_complexity)]
108 pub render: Option<ColumnRenderFn>,
109 pub hidden: bool,
111 pub ellipsis: bool,
113}
114
115impl PartialEq for TableColumn {
116 fn eq(&self, other: &Self) -> bool {
117 self.key == other.key
118 && self.title == other.title
119 && self.data_index == other.data_index
120 && self.width == other.width
121 && self.align == other.align
122 && self.fixed == other.fixed
123 && self.sortable == other.sortable
124 && self.default_sort_order == other.default_sort_order
125 && self.filters == other.filters
126 && self.hidden == other.hidden
127 && self.ellipsis == other.ellipsis
128 }
129}
130
131impl TableColumn {
132 pub fn new(key: impl Into<String>, title: impl Into<String>) -> Self {
133 let key_str = key.into();
134 Self {
135 key: key_str.clone(),
136 title: title.into(),
137 data_index: Some(key_str),
138 width: None,
139 align: None,
140 fixed: None,
141 sortable: false,
142 default_sort_order: None,
143 sorter: None,
144 filters: None,
145 on_filter: None,
146 render: None,
147 hidden: false,
148 ellipsis: false,
149 }
150 }
151
152 pub fn data_index(mut self, index: impl Into<String>) -> Self {
154 self.data_index = Some(index.into());
155 self
156 }
157
158 pub fn width(mut self, width: f32) -> Self {
160 self.width = Some(width);
161 self
162 }
163
164 pub fn align(mut self, align: ColumnAlign) -> Self {
166 self.align = Some(align);
167 self
168 }
169
170 pub fn sortable(mut self) -> Self {
172 self.sortable = true;
173 self
174 }
175
176 pub fn render(mut self, render: ColumnRenderFn) -> Self {
178 self.render = Some(render);
179 self
180 }
181
182 pub fn fixed(mut self, fixed: ColumnFixed) -> Self {
184 self.fixed = Some(fixed);
185 self
186 }
187
188 pub fn ellipsis(mut self) -> Self {
190 self.ellipsis = true;
191 self
192 }
193}
194
195#[derive(Clone, Default, PartialEq)]
197pub struct RowSelection {
198 pub selected_row_keys: Vec<String>,
200 pub on_change: Option<EventHandler<Vec<String>>>,
202 pub selection_type: SelectionType,
204 pub preserve_selected_row_keys: bool,
206}
207
208#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
210pub enum SelectionType {
211 #[default]
212 Checkbox,
213 Radio,
214}
215
216#[derive(Clone)]
218pub struct ExpandableConfig {
219 pub expanded_row_keys: Vec<String>,
221 pub on_expand: Option<EventHandler<(bool, String)>>,
223 pub expand_icon: Option<Element>,
225 pub expanded_row_render: Option<Rc<dyn Fn(&Value, usize, usize, bool) -> Element>>,
227 pub show_expand_icon: bool,
229}
230
231impl Default for ExpandableConfig {
232 fn default() -> Self {
233 Self {
234 expanded_row_keys: Vec::new(),
235 on_expand: None,
236 expand_icon: None,
237 expanded_row_render: None,
238 show_expand_icon: true,
239 }
240 }
241}
242
243impl PartialEq for ExpandableConfig {
244 fn eq(&self, other: &Self) -> bool {
245 self.expanded_row_keys == other.expanded_row_keys
246 && self.show_expand_icon == other.show_expand_icon
247 }
249}
250
251#[derive(Clone, Debug, Default, PartialEq)]
253pub struct TableScroll {
254 pub y: Option<f32>,
256 pub x: Option<f32>,
258 pub scroll_to_first_row_on_change: bool,
260}
261
262#[derive(Clone, Debug, Default, PartialEq)]
264pub struct StickyConfig {
265 pub offset_top: Option<f32>,
267 pub offset_bottom: Option<f32>,
269 pub get_container: Option<String>,
271}
272
273#[derive(Clone)]
275pub struct SummaryConfig {
276 pub render: Option<Rc<dyn Fn(&[TableColumn], &[Value]) -> Element>>,
278 pub fixed: Option<ColumnFixed>,
280}
281
282impl PartialEq for SummaryConfig {
283 fn eq(&self, other: &Self) -> bool {
284 self.fixed == other.fixed
285 }
287}
288
289pub type RowClassNameFn = Rc<dyn Fn(&Value, usize) -> Option<String>>;
291
292pub type RowPropsFn = Rc<dyn Fn(&Value, usize) -> HashMap<String, String>>;
294
295#[derive(Clone, Debug, Default, PartialEq)]
297pub struct TableLocale {
298 pub filter_title: Option<String>,
300 pub filter_confirm: Option<String>,
302 pub filter_reset: Option<String>,
304 pub filter_empty_text: Option<String>,
306 pub select_all: Option<String>,
308 pub select_none: Option<String>,
310 pub select_invert: Option<String>,
312 pub sort_title: Option<String>,
314 pub expand: Option<String>,
316 pub collapse: Option<String>,
318 pub empty_text: Option<String>,
320}
321
322#[derive(Clone, Debug, Default, PartialEq)]
324pub struct TableChangeEvent {
325 pub pagination: Option<TablePaginationState>,
326 pub sorter: Option<TableSorterState>,
327 pub filters: HashMap<String, Vec<String>>,
328}
329
330#[derive(Clone, Debug, Default, PartialEq)]
332pub struct TablePaginationState {
333 pub current: u32,
334 pub page_size: u32,
335 pub total: u32,
336}
337
338#[derive(Clone, Debug, Default, PartialEq)]
340pub struct TableSorterState {
341 pub column_key: Option<String>,
342 pub order: Option<SortOrder>,
343}
344
345#[derive(Props, Clone)]
347pub struct TableProps {
348 pub columns: Vec<TableColumn>,
350 pub data: Vec<Value>,
352 #[props(optional)]
354 pub row_key_field: Option<String>,
355 #[props(optional)]
357 pub row_class_name: Option<String>,
358 #[props(optional)]
360 pub row_class_name_fn: Option<RowClassNameFn>,
361 #[props(optional)]
363 pub row_props_fn: Option<RowPropsFn>,
364 #[props(default)]
366 pub bordered: bool,
367 #[props(optional)]
369 pub size: Option<ComponentSize>,
370 #[props(default)]
372 pub loading: bool,
373 #[props(optional)]
375 pub is_empty: Option<bool>,
376 #[props(optional)]
378 pub empty: Option<Element>,
379 #[props(optional)]
381 pub row_selection: Option<RowSelection>,
382 #[props(optional)]
384 pub scroll: Option<TableScroll>,
385 #[props(optional)]
387 pub sticky: Option<StickyConfig>,
388 #[props(optional)]
390 pub expandable: Option<ExpandableConfig>,
391 #[props(optional)]
393 pub summary: Option<SummaryConfig>,
394 #[props(optional)]
396 pub on_change: Option<EventHandler<TableChangeEvent>>,
397 #[props(optional)]
401 pub get_popup_container: Option<String>,
402 #[props(default)]
404 pub r#virtual: bool,
405 #[props(optional)]
407 pub locale: Option<TableLocale>,
408 #[props(default = true)]
410 pub show_header: bool,
411 #[props(optional)]
412 pub class: Option<String>,
413 #[props(optional)]
414 pub style: Option<String>,
415 #[props(optional)]
417 pub class_names: Option<TableClassNames>,
418 #[props(optional)]
420 pub styles: Option<TableStyles>,
421 #[props(optional)]
423 pub pagination_total: Option<u32>,
424 #[props(optional)]
425 pub pagination_current: Option<u32>,
426 #[props(optional)]
427 pub pagination_page_size: Option<u32>,
428 #[props(optional)]
429 pub pagination_on_change: Option<EventHandler<(u32, u32)>>,
430}
431
432impl PartialEq for TableProps {
433 fn eq(&self, other: &Self) -> bool {
434 self.columns == other.columns
436 && self.data == other.data
437 && self.row_key_field == other.row_key_field
438 && self.row_class_name == other.row_class_name
439 && self.bordered == other.bordered
440 && self.size == other.size
441 && self.loading == other.loading
442 && self.is_empty == other.is_empty
443 && self.empty == other.empty
444 && self.row_selection == other.row_selection
445 && self.scroll == other.scroll
446 && self.sticky == other.sticky
447 && self.expandable == other.expandable
448 && self.summary == other.summary
449 && self.on_change == other.on_change
450 && self.show_header == other.show_header
451 && self.class == other.class
452 && self.style == other.style
453 && self.class_names == other.class_names
454 && self.styles == other.styles
455 && self.get_popup_container == other.get_popup_container
456 && self.r#virtual == other.r#virtual
457 && self.locale == other.locale
458 && self.pagination_total == other.pagination_total
459 && self.pagination_current == other.pagination_current
460 && self.pagination_page_size == other.pagination_page_size
461 }
463}
464
465#[component]
467pub fn Table(props: TableProps) -> Element {
468 let TableProps {
469 columns,
470 data,
471 row_key_field,
472 row_class_name,
473 row_class_name_fn: _,
474 row_props_fn: _,
475 bordered,
476 size,
477 loading,
478 is_empty,
479 empty,
480 row_selection,
481 scroll,
482 sticky: _,
483 expandable: _,
484 summary: _,
485 on_change,
486 show_header,
487 class,
488 style,
489 class_names,
490 styles,
491 get_popup_container: _,
492 r#virtual: _virtual_scrolling,
493 locale: _,
494 pagination_total,
495 pagination_current,
496 pagination_page_size,
497 pagination_on_change,
498 } = props;
499
500 let sort_state: Signal<Option<(String, SortOrder)>> = use_signal(|| None);
502
503 let visible_columns: Vec<&TableColumn> = columns.iter().filter(|c| !c.hidden).collect();
505
506 let mut class_list = vec!["adui-table".to_string()];
508 if bordered {
509 class_list.push("adui-table-bordered".into());
510 }
511 if let Some(sz) = size {
512 match sz {
513 ComponentSize::Small => class_list.push("adui-table-sm".into()),
514 ComponentSize::Middle => {}
515 ComponentSize::Large => class_list.push("adui-table-lg".into()),
516 }
517 }
518 if row_selection.is_some() {
519 class_list.push("adui-table-selection".into());
520 }
521 class_list.push_semantic(&class_names, TableSemantic::Root);
522 if let Some(extra) = class {
523 class_list.push(extra);
524 }
525 let class_attr = class_list
526 .into_iter()
527 .filter(|s| !s.is_empty())
528 .collect::<Vec<_>>()
529 .join(" ");
530
531 let mut style_attr = style.unwrap_or_default();
532 style_attr.append_semantic(&styles, TableSemantic::Root);
533
534 let scroll_style = if let Some(ref sc) = scroll {
536 let mut s = String::new();
537 if let Some(y) = sc.y {
538 s.push_str(&format!("max-height: {}px; overflow-y: auto;", y));
539 }
540 if let Some(x) = sc.x {
541 s.push_str(&format!("overflow-x: auto; min-width: {}px;", x));
542 }
543 s
544 } else {
545 String::new()
546 };
547
548 let show_empty = !loading && is_empty.unwrap_or(data.is_empty());
549
550 let get_row_key = |row: &Value, idx: usize| -> String {
552 if let Some(field) = &row_key_field {
553 get_cell_text(row, field)
554 } else {
555 idx.to_string()
556 }
557 };
558
559 let has_selection = row_selection.is_some();
561 let selection_type = row_selection
562 .as_ref()
563 .map(|r| r.selection_type)
564 .unwrap_or_default();
565 let selected_keys = row_selection
566 .as_ref()
567 .map(|r| r.selected_row_keys.clone())
568 .unwrap_or_default();
569
570 let all_keys: Vec<String> = data
571 .iter()
572 .enumerate()
573 .map(|(idx, row)| get_row_key(row, idx))
574 .collect();
575
576 let all_selected = !all_keys.is_empty() && all_keys.iter().all(|k| selected_keys.contains(k));
577 let some_selected = !selected_keys.is_empty() && !all_selected;
578
579 let on_select_change = row_selection.as_ref().and_then(|r| r.on_change);
581
582 let on_change_cb = on_change;
584 let pagination_total_for_change = pagination_total;
585 let pagination_current_for_change = pagination_current;
586 let pagination_page_size_for_change = pagination_page_size;
587
588 rsx! {
589 div { class: "{class_attr}", style: "{style_attr}",
590 if show_header {
591 div { class: "adui-table-header",
592 div { class: "adui-table-row adui-table-row-header",
593 if has_selection {
595 div { class: "adui-table-cell adui-table-cell-selection",
596 if matches!(selection_type, SelectionType::Checkbox) {
597 Checkbox {
598 checked: all_selected,
599 indeterminate: some_selected,
600 on_change: move |_| {
601 if let Some(cb) = on_select_change {
602 if all_selected {
603 cb.call(Vec::new());
604 } else {
605 cb.call(all_keys.clone());
606 }
607 }
608 }
609 }
610 }
611 }
612 }
613 {visible_columns.iter().map(|col| {
615 let mut cell_classes = vec!["adui-table-cell".to_string(), "adui-table-cell-header".to_string()];
616 if let Some(align) = col.align {
617 cell_classes.push(align.as_class().to_string());
618 }
619 if col.sortable {
620 cell_classes.push("adui-table-column-sortable".into());
621 }
622 if let Some((ref key, order)) = *sort_state.read() {
623 if key == &col.key {
624 cell_classes.push(order.as_class().to_string());
625 }
626 }
627 if col.ellipsis {
628 cell_classes.push("adui-table-cell-ellipsis".into());
629 }
630
631 let width_style = col.width.map(|w| format!("width: {}px;", w)).unwrap_or_default();
632 let title = col.title.clone();
633 let cell_class = cell_classes.join(" ");
634
635 let col_key = col.key.clone();
636 let sortable = col.sortable;
637 let mut sort_signal = sort_state;
638
639 rsx! {
640 div {
641 class: "{cell_class}",
642 style: "{width_style}",
643 onclick: move |_| {
644 if sortable {
645 let current = sort_signal.read().clone();
646 let new_order = match current {
647 Some((ref k, SortOrder::Ascend)) if k == &col_key => {
648 Some((col_key.clone(), SortOrder::Descend))
649 }
650 Some((ref k, SortOrder::Descend)) if k == &col_key => {
651 None
652 }
653 _ => Some((col_key.clone(), SortOrder::Ascend))
654 };
655 sort_signal.set(new_order.clone());
656
657 if let Some(cb) = on_change_cb {
659 cb.call(TableChangeEvent {
660 pagination: pagination_total_for_change.map(|t| TablePaginationState {
661 total: t,
662 current: pagination_current_for_change.unwrap_or(1),
663 page_size: pagination_page_size_for_change.unwrap_or(10),
664 }),
665 sorter: Some(TableSorterState {
666 column_key: new_order.as_ref().map(|(k, _)| k.clone()),
667 order: new_order.as_ref().map(|(_, o)| *o),
668 }),
669 filters: HashMap::new(),
670 });
671 }
672 }
673 },
674 span { class: "adui-table-column-title", "{title}" }
675 if sortable {
676 span { class: "adui-table-column-sorter",
677 Icon { kind: IconKind::ArrowUp, size: 10.0 }
678 Icon { kind: IconKind::ArrowDown, size: 10.0 }
679 }
680 }
681 }
682 }
683 })}
684 }
685 }
686 }
687
688 div {
690 class: "adui-table-body",
691 style: "{scroll_style}",
692 if loading {
693 Spin {
694 spinning: Some(true),
695 tip: Some("加载中...".to_string()),
696 div { class: "adui-table-body-inner",
697 {render_rows(&visible_columns, &data, &row_key_field, &row_class_name, has_selection, selection_type, &selected_keys, on_select_change)}
698 }
699 }
700 } else if show_empty {
701 div { class: "adui-table-empty",
702 if let Some(node) = empty {
703 {node}
704 } else {
705 Empty {}
706 }
707 }
708 } else {
709 div { class: "adui-table-body-inner",
710 {render_rows(&visible_columns, &data, &row_key_field, &row_class_name, has_selection, selection_type, &selected_keys, on_select_change)}
711 }
712 }
713 }
714
715 if let Some(total) = pagination_total {
716 div { class: "adui-table-pagination",
717 Pagination {
718 total: total,
719 current: pagination_current,
720 page_size: pagination_page_size,
721 show_total: false,
722 show_size_changer: false,
723 on_change: move |(page, size)| {
724 if let Some(cb) = pagination_on_change {
725 cb.call((page, size));
726 }
727 if let Some(cb) = on_change_cb {
729 cb.call(TableChangeEvent {
730 pagination: Some(TablePaginationState {
731 total,
732 current: page,
733 page_size: size,
734 }),
735 sorter: None,
736 filters: HashMap::new(),
737 });
738 }
739 },
740 }
741 }
742 }
743 }
744 }
745}
746
747fn render_rows(
748 columns: &[&TableColumn],
749 data: &[Value],
750 row_key_field: &Option<String>,
751 row_class_name: &Option<String>,
752 has_selection: bool,
753 selection_type: SelectionType,
754 selected_keys: &[String],
755 on_select_change: Option<EventHandler<Vec<String>>>,
756) -> Element {
757 rsx! {
758 {data.iter().enumerate().map(|(idx, row)| {
759 let key = if let Some(field) = row_key_field {
760 get_cell_text(row, field)
761 } else {
762 idx.to_string()
763 };
764 let row_class = row_class_name.clone().unwrap_or_default();
765 let is_selected = selected_keys.contains(&key);
766 let key_for_select = key.clone();
767 let selected_keys_clone = selected_keys.to_vec();
768
769 rsx! {
770 div {
771 key: "{key}",
772 class: "adui-table-row {row_class}",
773 class: if is_selected { "adui-table-row-selected" } else { "" },
774 if has_selection {
776 div { class: "adui-table-cell adui-table-cell-selection",
777 Checkbox {
778 checked: is_selected,
779 on_change: move |_checked| {
780 if let Some(cb) = on_select_change {
781 let mut new_keys = selected_keys_clone.clone();
782 if is_selected {
783 new_keys.retain(|k| k != &key_for_select);
784 } else {
785 if matches!(selection_type, SelectionType::Radio) {
786 new_keys.clear();
787 }
788 new_keys.push(key_for_select.clone());
789 }
790 cb.call(new_keys);
791 }
792 }
793 }
794 }
795 }
796 {columns.iter().map(|col| {
798 let mut cell_classes = vec!["adui-table-cell".to_string()];
799 if let Some(align) = col.align {
800 cell_classes.push(align.as_class().to_string());
801 }
802 if col.ellipsis {
803 cell_classes.push("adui-table-cell-ellipsis".into());
804 }
805 let cell_class = cell_classes.join(" ");
806
807 let data_index = col.data_index.as_ref().unwrap_or(&col.key);
808 let cell_value = row.get(data_index);
809
810 let content = if let Some(render_fn) = col.render {
812 render_fn(cell_value, row, idx)
813 } else {
814 let text = get_cell_text(row, data_index);
815 rsx! { "{text}" }
816 };
817
818 rsx! {
819 div { class: "{cell_class}", {content} }
820 }
821 })}
822 }
823 }
824 })}
825 }
826}
827
828fn get_cell_text(row: &Value, key: &str) -> String {
830 match row.get(key) {
831 Some(Value::String(s)) => s.clone(),
832 Some(Value::Number(n)) => n.to_string(),
833 Some(Value::Bool(b)) => b.to_string(),
834 _ => "".to_string(),
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841
842 #[test]
843 fn column_builder_works() {
844 let col = TableColumn::new("id", "ID")
845 .width(100.0)
846 .align(ColumnAlign::Center)
847 .sortable();
848
849 assert_eq!(col.key, "id");
850 assert_eq!(col.title, "ID");
851 assert_eq!(col.width, Some(100.0));
852 assert_eq!(col.align, Some(ColumnAlign::Center));
853 assert!(col.sortable);
854 }
855
856 #[test]
857 fn column_builder_with_all_options() {
858 let mut col = TableColumn::new("name", "Name")
859 .width(200.0)
860 .align(ColumnAlign::Right)
861 .fixed(ColumnFixed::Left)
862 .sortable()
863 .ellipsis();
864 col.default_sort_order = Some(SortOrder::Ascend);
865 col.hidden = false;
866
867 assert_eq!(col.key, "name");
868 assert_eq!(col.title, "Name");
869 assert_eq!(col.width, Some(200.0));
870 assert_eq!(col.align, Some(ColumnAlign::Right));
871 assert_eq!(col.fixed, Some(ColumnFixed::Left));
872 assert!(col.sortable);
873 assert_eq!(col.default_sort_order, Some(SortOrder::Ascend));
874 assert!(col.ellipsis);
875 assert!(!col.hidden);
876 }
877
878 #[test]
879 fn column_align_all_variants() {
880 assert_eq!(ColumnAlign::Left.as_class(), "adui-table-align-left");
881 assert_eq!(ColumnAlign::Center.as_class(), "adui-table-align-center");
882 assert_eq!(ColumnAlign::Right.as_class(), "adui-table-align-right");
883 }
884
885 #[test]
886 fn column_align_default() {
887 assert_eq!(ColumnAlign::default(), ColumnAlign::Left);
888 }
889
890 #[test]
891 fn sort_order_classes() {
892 assert_eq!(
893 SortOrder::Ascend.as_class(),
894 "adui-table-column-sort-ascend"
895 );
896 assert_eq!(
897 SortOrder::Descend.as_class(),
898 "adui-table-column-sort-descend"
899 );
900 }
901
902 #[test]
903 fn sort_order_equality() {
904 assert_eq!(SortOrder::Ascend, SortOrder::Ascend);
905 assert_eq!(SortOrder::Descend, SortOrder::Descend);
906 assert_ne!(SortOrder::Ascend, SortOrder::Descend);
907 }
908
909 #[test]
910 fn column_fixed_variants() {
911 assert_eq!(ColumnFixed::Left, ColumnFixed::Left);
912 assert_eq!(ColumnFixed::Right, ColumnFixed::Right);
913 assert_ne!(ColumnFixed::Left, ColumnFixed::Right);
914 }
915
916 #[test]
917 fn get_cell_text_from_string() {
918 let mut row = serde_json::Map::new();
919 row.insert("name".to_string(), Value::String("John".to_string()));
920 let row_value = Value::Object(row);
921 assert_eq!(get_cell_text(&row_value, "name"), "John");
922 }
923
924 #[test]
925 fn get_cell_text_from_number() {
926 let mut row = serde_json::Map::new();
927 row.insert("age".to_string(), Value::Number(serde_json::Number::from(25)));
928 let row_value = Value::Object(row);
929 assert_eq!(get_cell_text(&row_value, "age"), "25");
930 }
931
932 #[test]
933 fn get_cell_text_from_bool() {
934 let mut row = serde_json::Map::new();
935 row.insert("active".to_string(), Value::Bool(true));
936 let row_value = Value::Object(row);
937 assert_eq!(get_cell_text(&row_value, "active"), "true");
938 }
939
940 #[test]
941 fn get_cell_text_missing_key() {
942 let mut row = serde_json::Map::new();
943 row.insert("name".to_string(), Value::String("John".to_string()));
944 let row_value = Value::Object(row);
945 assert_eq!(get_cell_text(&row_value, "missing"), "");
946 }
947}