1use schemars::JsonSchema;
7use serde::de::{self, Deserializer};
8use serde::ser::{SerializeMap, Serializer};
9use serde::{Deserialize, Serialize};
10
11use crate::action::Action;
12use crate::visibility::Visibility;
13
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
16#[serde(rename_all = "snake_case")]
17pub enum Size {
18 Xs,
19 Sm,
20 #[default]
21 Default,
22 Lg,
23}
24
25#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
27#[serde(rename_all = "snake_case")]
28pub enum IconPosition {
29 #[default]
30 Left,
31 Right,
32}
33
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
36#[serde(rename_all = "snake_case")]
37pub enum SortDirection {
38 #[default]
39 Asc,
40 Desc,
41}
42
43#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45#[serde(rename_all = "snake_case")]
46pub enum Orientation {
47 #[default]
48 Horizontal,
49 Vertical,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
54#[serde(rename_all = "snake_case")]
55pub enum ButtonVariant {
56 #[default]
57 Default,
58 Secondary,
59 Destructive,
60 Outline,
61 Ghost,
62 Link,
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
67#[serde(rename_all = "snake_case")]
68pub enum InputType {
69 #[default]
70 Text,
71 Email,
72 Password,
73 Number,
74 Textarea,
75 Hidden,
76 Date,
77 Time,
78 Url,
79 Tel,
80 Search,
81}
82
83#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[serde(rename_all = "snake_case")]
86pub enum AlertVariant {
87 #[default]
88 Info,
89 Success,
90 Warning,
91 Error,
92}
93
94#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
96#[serde(rename_all = "snake_case")]
97pub enum BadgeVariant {
98 #[default]
99 Default,
100 Secondary,
101 Destructive,
102 Outline,
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
107#[serde(rename_all = "snake_case")]
108pub enum TextElement {
109 #[default]
110 P,
111 H1,
112 H2,
113 H3,
114 Span,
115 Div,
116 Section,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
121#[serde(rename_all = "snake_case")]
122pub enum ColumnFormat {
123 Date,
124 DateTime,
125 Currency,
126 Boolean,
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
131pub struct Column {
132 pub key: String,
133 pub label: String,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub format: Option<ColumnFormat>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
140pub struct SelectOption {
141 pub value: String,
142 pub label: String,
143}
144
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148pub struct CardProps {
149 pub title: String,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub description: Option<String>,
152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
153 pub children: Vec<ComponentNode>,
154 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 pub footer: Vec<ComponentNode>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub max_width: Option<FormMaxWidth>,
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
162pub struct TableProps {
163 pub columns: Vec<Column>,
164 pub data_path: String,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub row_actions: Option<Vec<Action>>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub empty_message: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub sortable: Option<bool>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub sort_column: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub sort_direction: Option<SortDirection>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
179#[serde(rename_all = "snake_case")]
180pub enum FormMaxWidth {
181 #[default]
182 Default,
183 Narrow,
184 Wide,
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190pub struct FormProps {
191 pub action: Action,
192 pub fields: Vec<ComponentNode>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub method: Option<crate::action::HttpMethod>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub guard: Option<String>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub max_width: Option<FormMaxWidth>,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
207#[serde(rename_all = "snake_case")]
208pub enum ButtonType {
209 #[default]
210 Button,
211 Submit,
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
216pub struct ButtonProps {
217 pub label: String,
218 #[serde(default)]
219 pub variant: ButtonVariant,
220 #[serde(default)]
221 pub size: Size,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub disabled: Option<bool>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub icon: Option<String>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub icon_position: Option<IconPosition>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub button_type: Option<ButtonType>,
230}
231
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
234pub struct InputProps {
235 pub field: String,
237 pub label: String,
238 #[serde(default)]
239 pub input_type: InputType,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub placeholder: Option<String>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub required: Option<bool>,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub disabled: Option<bool>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub error: Option<String>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub description: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub default_value: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub data_path: Option<String>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub step: Option<String>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub list: Option<String>,
263}
264
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
267pub struct SelectProps {
268 pub field: String,
270 pub label: String,
271 pub options: Vec<SelectOption>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub placeholder: Option<String>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub required: Option<bool>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub disabled: Option<bool>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub error: Option<String>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub description: Option<String>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub default_value: Option<String>,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub data_path: Option<String>,
287}
288
289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
291pub struct AlertProps {
292 pub message: String,
293 #[serde(default)]
294 pub variant: AlertVariant,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub title: Option<String>,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
301pub struct BadgeProps {
302 pub label: String,
303 #[serde(default)]
304 pub variant: BadgeVariant,
305}
306
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
310pub struct ModalProps {
311 pub id: String,
312 pub title: String,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub description: Option<String>,
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
316 pub children: Vec<ComponentNode>,
317 #[serde(default, skip_serializing_if = "Vec::is_empty")]
318 pub footer: Vec<ComponentNode>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub trigger_label: Option<String>,
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
325pub struct TextProps {
326 pub content: String,
327 #[serde(default)]
328 pub element: TextElement,
329}
330
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
333pub struct CheckboxProps {
334 pub field: String,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub value: Option<String>,
340 pub label: String,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub description: Option<String>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub checked: Option<bool>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub data_path: Option<String>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub required: Option<bool>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub disabled: Option<bool>,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub error: Option<String>,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
359pub struct SwitchProps {
360 pub field: String,
362 pub label: String,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub description: Option<String>,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub checked: Option<bool>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub data_path: Option<String>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub required: Option<bool>,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub disabled: Option<bool>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub error: Option<String>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub action: Option<Action>,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
384pub struct SeparatorProps {
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub orientation: Option<Orientation>,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
391pub struct DescriptionItem {
392 pub label: String,
393 pub value: String,
394 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub format: Option<ColumnFormat>,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
400pub struct DescriptionListProps {
401 pub items: Vec<DescriptionItem>,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub columns: Option<u8>,
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409pub struct Tab {
410 pub value: String,
411 pub label: String,
412 #[serde(default, skip_serializing_if = "Vec::is_empty")]
413 pub children: Vec<ComponentNode>,
414}
415
416#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
419pub struct TabsProps {
420 pub default_tab: String,
421 pub tabs: Vec<Tab>,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
426pub struct BreadcrumbItem {
427 pub label: String,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub url: Option<String>,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
434pub struct BreadcrumbProps {
435 pub items: Vec<BreadcrumbItem>,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
440pub struct PaginationProps {
441 pub current_page: u32,
442 pub per_page: u32,
443 pub total: u32,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub base_url: Option<String>,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
450pub struct ProgressProps {
451 pub value: u8,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub max: Option<u8>,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub label: Option<String>,
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
461pub struct ImageProps {
462 pub src: String,
463 pub alt: String,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub aspect_ratio: Option<String>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub placeholder_label: Option<String>,
472}
473
474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
476pub struct AvatarProps {
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub src: Option<String>,
479 pub alt: String,
480 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub fallback: Option<String>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub size: Option<Size>,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
488pub struct SkeletonProps {
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub width: Option<String>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub height: Option<String>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub rounded: Option<bool>,
495}
496
497#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
499#[serde(rename_all = "snake_case")]
500pub enum ToastVariant {
501 #[default]
502 Info,
503 Success,
504 Warning,
505 Error,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
510pub struct ChecklistItem {
511 pub label: String,
512 #[serde(default)]
513 pub checked: bool,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub href: Option<String>,
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
520pub struct NotificationItem {
521 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub icon: Option<String>,
523 pub text: String,
524 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub timestamp: Option<String>,
526 #[serde(default)]
527 pub read: bool,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub action_url: Option<String>,
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
534pub struct SidebarNavItem {
535 pub label: String,
536 pub href: String,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
538 pub icon: Option<String>,
539 #[serde(default)]
540 pub active: bool,
541}
542
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
545pub struct SidebarGroup {
546 pub label: String,
547 #[serde(default)]
548 pub collapsed: bool,
549 pub items: Vec<SidebarNavItem>,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
554pub struct StatCardProps {
555 pub label: String,
556 pub value: String,
557 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub icon: Option<String>,
559 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub subtitle: Option<String>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub sse_target: Option<String>,
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
568pub struct ChecklistProps {
569 pub title: String,
570 pub items: Vec<ChecklistItem>,
571 #[serde(default = "default_true")]
572 pub dismissible: bool,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
574 pub dismiss_label: Option<String>,
575 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub data_key: Option<String>,
578}
579
580fn default_true() -> bool {
581 true
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
589pub struct ToastProps {
590 pub message: String,
591 #[serde(default)]
592 pub variant: ToastVariant,
593 #[serde(default, skip_serializing_if = "Option::is_none")]
595 pub timeout: Option<u32>,
596 #[serde(default = "default_true")]
597 pub dismissible: bool,
598}
599
600#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
602pub struct NotificationDropdownProps {
603 pub notifications: Vec<NotificationItem>,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub empty_text: Option<String>,
606}
607
608#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
610pub struct SidebarProps {
611 #[serde(default, skip_serializing_if = "Vec::is_empty")]
612 pub fixed_top: Vec<SidebarNavItem>,
613 #[serde(default, skip_serializing_if = "Vec::is_empty")]
614 pub groups: Vec<SidebarGroup>,
615 #[serde(default, skip_serializing_if = "Vec::is_empty")]
616 pub fixed_bottom: Vec<SidebarNavItem>,
617}
618
619#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
621pub struct HeaderProps {
622 pub business_name: String,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub notification_count: Option<u32>,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub user_name: Option<String>,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub user_avatar: Option<String>,
630 #[serde(default, skip_serializing_if = "Option::is_none")]
631 pub logout_url: Option<String>,
632}
633
634#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
636#[serde(rename_all = "snake_case")]
637pub enum GapSize {
638 None,
639 Sm,
640 #[default]
641 Md,
642 Lg,
643 Xl,
644}
645
646#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
649pub struct GridProps {
650 #[serde(default = "default_grid_columns")]
652 pub columns: u8,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub md_columns: Option<u8>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub lg_columns: Option<u8>,
659 #[serde(default)]
661 pub gap: GapSize,
662 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub scrollable: Option<bool>,
666 #[serde(default, skip_serializing_if = "Vec::is_empty")]
667 pub children: Vec<ComponentNode>,
668}
669
670fn default_grid_columns() -> u8 {
671 2
672}
673
674#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
677pub struct CollapsibleProps {
678 pub title: String,
679 #[serde(default)]
680 pub expanded: bool,
681 #[serde(default, skip_serializing_if = "Vec::is_empty")]
682 pub children: Vec<ComponentNode>,
683}
684
685#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
687pub struct EmptyStateProps {
688 pub title: String,
689 #[serde(default, skip_serializing_if = "Option::is_none")]
690 pub description: Option<String>,
691 #[serde(default, skip_serializing_if = "Option::is_none")]
692 pub action: Option<Action>,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
694 pub action_label: Option<String>,
695}
696
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
699#[serde(rename_all = "snake_case")]
700pub enum FormSectionLayout {
701 #[default]
702 Stacked,
703 TwoColumn,
704}
705
706#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
709pub struct FormSectionProps {
710 pub title: String,
711 #[serde(default, skip_serializing_if = "Option::is_none")]
712 pub description: Option<String>,
713 #[serde(default, skip_serializing_if = "Vec::is_empty")]
714 pub children: Vec<ComponentNode>,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub layout: Option<FormSectionLayout>,
718}
719
720#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
723pub struct PageHeaderProps {
724 pub title: String,
725 #[serde(default, skip_serializing_if = "Vec::is_empty")]
726 pub breadcrumb: Vec<BreadcrumbItem>,
727 #[serde(default, skip_serializing_if = "Vec::is_empty")]
728 pub actions: Vec<ComponentNode>,
729}
730
731#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
734pub struct ButtonGroupProps {
735 #[serde(default, skip_serializing_if = "Vec::is_empty")]
736 pub buttons: Vec<ComponentNode>,
737}
738
739#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
741pub struct DropdownMenuAction {
742 pub label: String,
743 pub action: Action,
744 #[serde(default)]
745 pub destructive: bool,
746}
747
748#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
750pub struct DropdownMenuProps {
751 pub menu_id: String,
752 pub trigger_label: String,
753 pub items: Vec<DropdownMenuAction>,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub trigger_variant: Option<ButtonVariant>,
756}
757
758#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
761pub struct DataTableProps {
762 pub columns: Vec<Column>,
763 pub data_path: String,
764 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub row_actions: Option<Vec<DropdownMenuAction>>,
766 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub empty_message: Option<String>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub row_key: Option<String>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
772 pub row_href: Option<String>,
773}
774
775#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
778pub struct KanbanColumnProps {
779 pub id: String,
780 pub title: String,
781 pub count: u32,
782 #[serde(default, skip_serializing_if = "Vec::is_empty")]
783 pub children: Vec<ComponentNode>,
784}
785
786#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
789pub struct KanbanBoardProps {
790 pub columns: Vec<KanbanColumnProps>,
791 #[serde(default, skip_serializing_if = "Option::is_none")]
792 pub mobile_default_column: Option<String>,
793}
794
795#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
800pub struct CalendarCellProps {
801 pub day: u8,
802 #[serde(default)]
803 pub is_today: bool,
804 #[serde(default)]
805 pub is_current_month: bool,
806 #[serde(default)]
807 pub event_count: u32,
808 #[serde(default, skip_serializing_if = "Vec::is_empty")]
811 pub dot_colors: Vec<String>,
812}
813
814#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
816#[serde(rename_all = "snake_case")]
817pub enum ActionCardVariant {
818 #[default]
819 Default,
820 Setup,
821 Danger,
822}
823
824#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
829pub struct ActionCardProps {
830 pub title: String,
831 pub description: String,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
833 pub icon: Option<String>,
834 #[serde(default)]
835 pub variant: ActionCardVariant,
836 #[serde(default, skip_serializing_if = "Option::is_none")]
838 pub href: Option<String>,
839}
840
841#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
846pub struct ProductTileProps {
847 pub product_id: String,
848 pub name: String,
849 pub price: String,
850 pub field: String,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub default_quantity: Option<u32>,
853}
854
855#[derive(Debug, Clone, PartialEq)]
862pub struct PluginProps {
863 pub plugin_type: String,
865 pub props: serde_json::Value,
867}
868
869impl Serialize for PluginProps {
870 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
871 let obj = self.props.as_object();
873 let extra_len = obj.map_or(0, |m| m.len());
874 let mut map = serializer.serialize_map(Some(1 + extra_len))?;
875 map.serialize_entry("type", &self.plugin_type)?;
876 if let Some(obj) = obj {
877 for (k, v) in obj {
878 if k != "type" {
879 map.serialize_entry(k, v)?;
880 }
881 }
882 }
883 map.end()
884 }
885}
886
887impl<'de> Deserialize<'de> for PluginProps {
888 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
889 let mut value = serde_json::Value::deserialize(deserializer)?;
890 let plugin_type = value
891 .get("type")
892 .and_then(|v| v.as_str())
893 .map(|s| s.to_string())
894 .ok_or_else(|| de::Error::missing_field("type"))?;
895 if let Some(obj) = value.as_object_mut() {
897 obj.remove("type");
898 }
899 Ok(PluginProps {
900 plugin_type,
901 props: value,
902 })
903 }
904}
905
906#[derive(Debug, Clone, PartialEq)]
913pub enum Component {
914 Card(CardProps),
915 Table(TableProps),
916 Form(FormProps),
917 Button(ButtonProps),
918 Input(InputProps),
919 Select(SelectProps),
920 Alert(AlertProps),
921 Badge(BadgeProps),
922 Modal(ModalProps),
923 Text(TextProps),
924 Checkbox(CheckboxProps),
925 Switch(SwitchProps),
926 Separator(SeparatorProps),
927 DescriptionList(DescriptionListProps),
928 Tabs(TabsProps),
929 Breadcrumb(BreadcrumbProps),
930 Pagination(PaginationProps),
931 Progress(ProgressProps),
932 Avatar(AvatarProps),
933 Skeleton(SkeletonProps),
934 StatCard(StatCardProps),
935 Checklist(ChecklistProps),
936 Toast(ToastProps),
937 NotificationDropdown(NotificationDropdownProps),
938 Sidebar(SidebarProps),
939 Header(HeaderProps),
940 Grid(GridProps),
941 Collapsible(CollapsibleProps),
942 EmptyState(EmptyStateProps),
943 FormSection(FormSectionProps),
944 PageHeader(PageHeaderProps),
945 ButtonGroup(ButtonGroupProps),
946 DropdownMenu(DropdownMenuProps),
947 KanbanBoard(KanbanBoardProps),
948 CalendarCell(CalendarCellProps),
949 ActionCard(ActionCardProps),
950 ProductTile(ProductTileProps),
951 DataTable(DataTableProps),
952 Image(ImageProps),
953 Plugin(PluginProps),
954}
955
956fn serialize_tagged<S: Serializer, T: Serialize>(
961 serializer: S,
962 type_name: &str,
963 props: &T,
964) -> Result<S::Ok, S::Error> {
965 let mut value = serde_json::to_value(props).map_err(serde::ser::Error::custom)?;
966 if let Some(obj) = value.as_object_mut() {
967 obj.insert(
968 "type".to_string(),
969 serde_json::Value::String(type_name.to_string()),
970 );
971 }
972 value.serialize(serializer)
973}
974
975impl Serialize for Component {
976 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
977 match self {
978 Component::Card(p) => serialize_tagged(serializer, "Card", p),
979 Component::Table(p) => serialize_tagged(serializer, "Table", p),
980 Component::Form(p) => serialize_tagged(serializer, "Form", p),
981 Component::Button(p) => serialize_tagged(serializer, "Button", p),
982 Component::Input(p) => serialize_tagged(serializer, "Input", p),
983 Component::Select(p) => serialize_tagged(serializer, "Select", p),
984 Component::Alert(p) => serialize_tagged(serializer, "Alert", p),
985 Component::Badge(p) => serialize_tagged(serializer, "Badge", p),
986 Component::Modal(p) => serialize_tagged(serializer, "Modal", p),
987 Component::Text(p) => serialize_tagged(serializer, "Text", p),
988 Component::Checkbox(p) => serialize_tagged(serializer, "Checkbox", p),
989 Component::Switch(p) => serialize_tagged(serializer, "Switch", p),
990 Component::Separator(p) => serialize_tagged(serializer, "Separator", p),
991 Component::DescriptionList(p) => serialize_tagged(serializer, "DescriptionList", p),
992 Component::Tabs(p) => serialize_tagged(serializer, "Tabs", p),
993 Component::Breadcrumb(p) => serialize_tagged(serializer, "Breadcrumb", p),
994 Component::Pagination(p) => serialize_tagged(serializer, "Pagination", p),
995 Component::Progress(p) => serialize_tagged(serializer, "Progress", p),
996 Component::Avatar(p) => serialize_tagged(serializer, "Avatar", p),
997 Component::Skeleton(p) => serialize_tagged(serializer, "Skeleton", p),
998 Component::StatCard(p) => serialize_tagged(serializer, "StatCard", p),
999 Component::Checklist(p) => serialize_tagged(serializer, "Checklist", p),
1000 Component::Toast(p) => serialize_tagged(serializer, "Toast", p),
1001 Component::NotificationDropdown(p) => {
1002 serialize_tagged(serializer, "NotificationDropdown", p)
1003 }
1004 Component::Sidebar(p) => serialize_tagged(serializer, "Sidebar", p),
1005 Component::Header(p) => serialize_tagged(serializer, "Header", p),
1006 Component::Grid(p) => serialize_tagged(serializer, "Grid", p),
1007 Component::Collapsible(p) => serialize_tagged(serializer, "Collapsible", p),
1008 Component::EmptyState(p) => serialize_tagged(serializer, "EmptyState", p),
1009 Component::FormSection(p) => serialize_tagged(serializer, "FormSection", p),
1010 Component::PageHeader(p) => serialize_tagged(serializer, "PageHeader", p),
1011 Component::ButtonGroup(p) => serialize_tagged(serializer, "ButtonGroup", p),
1012 Component::DropdownMenu(p) => serialize_tagged(serializer, "DropdownMenu", p),
1013 Component::KanbanBoard(p) => serialize_tagged(serializer, "KanbanBoard", p),
1014 Component::CalendarCell(p) => serialize_tagged(serializer, "CalendarCell", p),
1015 Component::ActionCard(p) => serialize_tagged(serializer, "ActionCard", p),
1016 Component::ProductTile(p) => serialize_tagged(serializer, "ProductTile", p),
1017 Component::DataTable(p) => serialize_tagged(serializer, "DataTable", p),
1018 Component::Image(p) => serialize_tagged(serializer, "Image", p),
1019 Component::Plugin(p) => p.serialize(serializer),
1020 }
1021 }
1022}
1023
1024impl<'de> Deserialize<'de> for Component {
1027 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1028 let value = serde_json::Value::deserialize(deserializer)?;
1029 let type_str = value
1030 .get("type")
1031 .and_then(|v| v.as_str())
1032 .ok_or_else(|| de::Error::missing_field("type"))?;
1033
1034 match type_str {
1035 "Card" => serde_json::from_value::<CardProps>(value)
1036 .map(Component::Card)
1037 .map_err(de::Error::custom),
1038 "Table" => serde_json::from_value::<TableProps>(value)
1039 .map(Component::Table)
1040 .map_err(de::Error::custom),
1041 "Form" => serde_json::from_value::<FormProps>(value)
1042 .map(Component::Form)
1043 .map_err(de::Error::custom),
1044 "Button" => serde_json::from_value::<ButtonProps>(value)
1045 .map(Component::Button)
1046 .map_err(de::Error::custom),
1047 "Input" => serde_json::from_value::<InputProps>(value)
1048 .map(Component::Input)
1049 .map_err(de::Error::custom),
1050 "Select" => serde_json::from_value::<SelectProps>(value)
1051 .map(Component::Select)
1052 .map_err(de::Error::custom),
1053 "Alert" => serde_json::from_value::<AlertProps>(value)
1054 .map(Component::Alert)
1055 .map_err(de::Error::custom),
1056 "Badge" => serde_json::from_value::<BadgeProps>(value)
1057 .map(Component::Badge)
1058 .map_err(de::Error::custom),
1059 "Modal" => serde_json::from_value::<ModalProps>(value)
1060 .map(Component::Modal)
1061 .map_err(de::Error::custom),
1062 "Text" => serde_json::from_value::<TextProps>(value)
1063 .map(Component::Text)
1064 .map_err(de::Error::custom),
1065 "Checkbox" => serde_json::from_value::<CheckboxProps>(value)
1066 .map(Component::Checkbox)
1067 .map_err(de::Error::custom),
1068 "Switch" => serde_json::from_value::<SwitchProps>(value)
1069 .map(Component::Switch)
1070 .map_err(de::Error::custom),
1071 "Separator" => serde_json::from_value::<SeparatorProps>(value)
1072 .map(Component::Separator)
1073 .map_err(de::Error::custom),
1074 "DescriptionList" => serde_json::from_value::<DescriptionListProps>(value)
1075 .map(Component::DescriptionList)
1076 .map_err(de::Error::custom),
1077 "Tabs" => serde_json::from_value::<TabsProps>(value)
1078 .map(Component::Tabs)
1079 .map_err(de::Error::custom),
1080 "Breadcrumb" => serde_json::from_value::<BreadcrumbProps>(value)
1081 .map(Component::Breadcrumb)
1082 .map_err(de::Error::custom),
1083 "Pagination" => serde_json::from_value::<PaginationProps>(value)
1084 .map(Component::Pagination)
1085 .map_err(de::Error::custom),
1086 "Progress" => serde_json::from_value::<ProgressProps>(value)
1087 .map(Component::Progress)
1088 .map_err(de::Error::custom),
1089 "Avatar" => serde_json::from_value::<AvatarProps>(value)
1090 .map(Component::Avatar)
1091 .map_err(de::Error::custom),
1092 "Skeleton" => serde_json::from_value::<SkeletonProps>(value)
1093 .map(Component::Skeleton)
1094 .map_err(de::Error::custom),
1095 "StatCard" => serde_json::from_value::<StatCardProps>(value)
1096 .map(Component::StatCard)
1097 .map_err(de::Error::custom),
1098 "Checklist" => serde_json::from_value::<ChecklistProps>(value)
1099 .map(Component::Checklist)
1100 .map_err(de::Error::custom),
1101 "Toast" => serde_json::from_value::<ToastProps>(value)
1102 .map(Component::Toast)
1103 .map_err(de::Error::custom),
1104 "NotificationDropdown" => serde_json::from_value::<NotificationDropdownProps>(value)
1105 .map(Component::NotificationDropdown)
1106 .map_err(de::Error::custom),
1107 "Sidebar" => serde_json::from_value::<SidebarProps>(value)
1108 .map(Component::Sidebar)
1109 .map_err(de::Error::custom),
1110 "Header" => serde_json::from_value::<HeaderProps>(value)
1111 .map(Component::Header)
1112 .map_err(de::Error::custom),
1113 "Grid" => serde_json::from_value::<GridProps>(value)
1114 .map(Component::Grid)
1115 .map_err(de::Error::custom),
1116 "Collapsible" => serde_json::from_value::<CollapsibleProps>(value)
1117 .map(Component::Collapsible)
1118 .map_err(de::Error::custom),
1119 "EmptyState" => serde_json::from_value::<EmptyStateProps>(value)
1120 .map(Component::EmptyState)
1121 .map_err(de::Error::custom),
1122 "FormSection" => serde_json::from_value::<FormSectionProps>(value)
1123 .map(Component::FormSection)
1124 .map_err(de::Error::custom),
1125 "PageHeader" => serde_json::from_value::<PageHeaderProps>(value)
1126 .map(Component::PageHeader)
1127 .map_err(de::Error::custom),
1128 "ButtonGroup" => serde_json::from_value::<ButtonGroupProps>(value)
1129 .map(Component::ButtonGroup)
1130 .map_err(de::Error::custom),
1131 "DropdownMenu" => serde_json::from_value::<DropdownMenuProps>(value)
1132 .map(Component::DropdownMenu)
1133 .map_err(de::Error::custom),
1134 "KanbanBoard" => serde_json::from_value::<KanbanBoardProps>(value)
1135 .map(Component::KanbanBoard)
1136 .map_err(de::Error::custom),
1137 "CalendarCell" => serde_json::from_value::<CalendarCellProps>(value)
1138 .map(Component::CalendarCell)
1139 .map_err(de::Error::custom),
1140 "ActionCard" => serde_json::from_value::<ActionCardProps>(value)
1141 .map(Component::ActionCard)
1142 .map_err(de::Error::custom),
1143 "ProductTile" => serde_json::from_value::<ProductTileProps>(value)
1144 .map(Component::ProductTile)
1145 .map_err(de::Error::custom),
1146 "DataTable" => serde_json::from_value::<DataTableProps>(value)
1147 .map(Component::DataTable)
1148 .map_err(de::Error::custom),
1149 "Image" => serde_json::from_value::<ImageProps>(value)
1150 .map(Component::Image)
1151 .map_err(de::Error::custom),
1152 _ => {
1153 let plugin_type = type_str.to_string();
1155 let mut props = value;
1156 if let Some(obj) = props.as_object_mut() {
1157 obj.remove("type");
1158 }
1159 Ok(Component::Plugin(PluginProps { plugin_type, props }))
1160 }
1161 }
1162 }
1163}
1164
1165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1172pub struct ComponentNode {
1173 pub key: String,
1174 #[serde(flatten)]
1175 pub component: Component,
1176 #[serde(default, skip_serializing_if = "Option::is_none")]
1177 pub action: Option<Action>,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub visibility: Option<Visibility>,
1180}
1181
1182impl ComponentNode {
1183 pub fn card(key: impl Into<String>, props: CardProps) -> Self {
1185 Self {
1186 key: key.into(),
1187 component: Component::Card(props),
1188 action: None,
1189 visibility: None,
1190 }
1191 }
1192
1193 pub fn table(key: impl Into<String>, props: TableProps) -> Self {
1195 Self {
1196 key: key.into(),
1197 component: Component::Table(props),
1198 action: None,
1199 visibility: None,
1200 }
1201 }
1202
1203 pub fn form(key: impl Into<String>, props: FormProps) -> Self {
1205 Self {
1206 key: key.into(),
1207 component: Component::Form(props),
1208 action: None,
1209 visibility: None,
1210 }
1211 }
1212
1213 pub fn button(key: impl Into<String>, props: ButtonProps) -> Self {
1215 Self {
1216 key: key.into(),
1217 component: Component::Button(props),
1218 action: None,
1219 visibility: None,
1220 }
1221 }
1222
1223 pub fn input(key: impl Into<String>, props: InputProps) -> Self {
1225 Self {
1226 key: key.into(),
1227 component: Component::Input(props),
1228 action: None,
1229 visibility: None,
1230 }
1231 }
1232
1233 pub fn select(key: impl Into<String>, props: SelectProps) -> Self {
1235 Self {
1236 key: key.into(),
1237 component: Component::Select(props),
1238 action: None,
1239 visibility: None,
1240 }
1241 }
1242
1243 pub fn alert(key: impl Into<String>, props: AlertProps) -> Self {
1245 Self {
1246 key: key.into(),
1247 component: Component::Alert(props),
1248 action: None,
1249 visibility: None,
1250 }
1251 }
1252
1253 pub fn badge(key: impl Into<String>, props: BadgeProps) -> Self {
1255 Self {
1256 key: key.into(),
1257 component: Component::Badge(props),
1258 action: None,
1259 visibility: None,
1260 }
1261 }
1262
1263 pub fn modal(key: impl Into<String>, props: ModalProps) -> Self {
1265 Self {
1266 key: key.into(),
1267 component: Component::Modal(props),
1268 action: None,
1269 visibility: None,
1270 }
1271 }
1272
1273 pub fn text(key: impl Into<String>, props: TextProps) -> Self {
1275 Self {
1276 key: key.into(),
1277 component: Component::Text(props),
1278 action: None,
1279 visibility: None,
1280 }
1281 }
1282
1283 pub fn checkbox(key: impl Into<String>, props: CheckboxProps) -> Self {
1285 Self {
1286 key: key.into(),
1287 component: Component::Checkbox(props),
1288 action: None,
1289 visibility: None,
1290 }
1291 }
1292
1293 pub fn switch(key: impl Into<String>, props: SwitchProps) -> Self {
1295 Self {
1296 key: key.into(),
1297 component: Component::Switch(props),
1298 action: None,
1299 visibility: None,
1300 }
1301 }
1302
1303 pub fn separator(key: impl Into<String>, props: SeparatorProps) -> Self {
1305 Self {
1306 key: key.into(),
1307 component: Component::Separator(props),
1308 action: None,
1309 visibility: None,
1310 }
1311 }
1312
1313 pub fn description_list(key: impl Into<String>, props: DescriptionListProps) -> Self {
1315 Self {
1316 key: key.into(),
1317 component: Component::DescriptionList(props),
1318 action: None,
1319 visibility: None,
1320 }
1321 }
1322
1323 pub fn tabs(key: impl Into<String>, props: TabsProps) -> Self {
1325 Self {
1326 key: key.into(),
1327 component: Component::Tabs(props),
1328 action: None,
1329 visibility: None,
1330 }
1331 }
1332
1333 pub fn breadcrumb(key: impl Into<String>, props: BreadcrumbProps) -> Self {
1335 Self {
1336 key: key.into(),
1337 component: Component::Breadcrumb(props),
1338 action: None,
1339 visibility: None,
1340 }
1341 }
1342
1343 pub fn pagination(key: impl Into<String>, props: PaginationProps) -> Self {
1345 Self {
1346 key: key.into(),
1347 component: Component::Pagination(props),
1348 action: None,
1349 visibility: None,
1350 }
1351 }
1352
1353 pub fn progress(key: impl Into<String>, props: ProgressProps) -> Self {
1355 Self {
1356 key: key.into(),
1357 component: Component::Progress(props),
1358 action: None,
1359 visibility: None,
1360 }
1361 }
1362
1363 pub fn avatar(key: impl Into<String>, props: AvatarProps) -> Self {
1365 Self {
1366 key: key.into(),
1367 component: Component::Avatar(props),
1368 action: None,
1369 visibility: None,
1370 }
1371 }
1372
1373 pub fn skeleton(key: impl Into<String>, props: SkeletonProps) -> Self {
1375 Self {
1376 key: key.into(),
1377 component: Component::Skeleton(props),
1378 action: None,
1379 visibility: None,
1380 }
1381 }
1382
1383 pub fn stat_card(key: impl Into<String>, props: StatCardProps) -> Self {
1385 Self {
1386 key: key.into(),
1387 component: Component::StatCard(props),
1388 action: None,
1389 visibility: None,
1390 }
1391 }
1392
1393 pub fn checklist(key: impl Into<String>, props: ChecklistProps) -> Self {
1395 Self {
1396 key: key.into(),
1397 component: Component::Checklist(props),
1398 action: None,
1399 visibility: None,
1400 }
1401 }
1402
1403 pub fn toast(key: impl Into<String>, props: ToastProps) -> Self {
1405 Self {
1406 key: key.into(),
1407 component: Component::Toast(props),
1408 action: None,
1409 visibility: None,
1410 }
1411 }
1412
1413 pub fn notification_dropdown(key: impl Into<String>, props: NotificationDropdownProps) -> Self {
1415 Self {
1416 key: key.into(),
1417 component: Component::NotificationDropdown(props),
1418 action: None,
1419 visibility: None,
1420 }
1421 }
1422
1423 pub fn sidebar(key: impl Into<String>, props: SidebarProps) -> Self {
1425 Self {
1426 key: key.into(),
1427 component: Component::Sidebar(props),
1428 action: None,
1429 visibility: None,
1430 }
1431 }
1432
1433 pub fn header(key: impl Into<String>, props: HeaderProps) -> Self {
1435 Self {
1436 key: key.into(),
1437 component: Component::Header(props),
1438 action: None,
1439 visibility: None,
1440 }
1441 }
1442
1443 pub fn grid(key: impl Into<String>, props: GridProps) -> Self {
1445 Self {
1446 key: key.into(),
1447 component: Component::Grid(props),
1448 action: None,
1449 visibility: None,
1450 }
1451 }
1452
1453 pub fn collapsible(key: impl Into<String>, props: CollapsibleProps) -> Self {
1455 Self {
1456 key: key.into(),
1457 component: Component::Collapsible(props),
1458 action: None,
1459 visibility: None,
1460 }
1461 }
1462
1463 pub fn empty_state(key: impl Into<String>, props: EmptyStateProps) -> Self {
1465 Self {
1466 key: key.into(),
1467 component: Component::EmptyState(props),
1468 action: None,
1469 visibility: None,
1470 }
1471 }
1472
1473 pub fn form_section(key: impl Into<String>, props: FormSectionProps) -> Self {
1475 Self {
1476 key: key.into(),
1477 component: Component::FormSection(props),
1478 action: None,
1479 visibility: None,
1480 }
1481 }
1482
1483 pub fn dropdown_menu(key: impl Into<String>, props: DropdownMenuProps) -> Self {
1485 Self {
1486 key: key.into(),
1487 component: Component::DropdownMenu(props),
1488 action: None,
1489 visibility: None,
1490 }
1491 }
1492
1493 pub fn kanban_board(key: impl Into<String>, props: KanbanBoardProps) -> Self {
1495 Self {
1496 key: key.into(),
1497 component: Component::KanbanBoard(props),
1498 action: None,
1499 visibility: None,
1500 }
1501 }
1502
1503 pub fn calendar_cell(key: impl Into<String>, props: CalendarCellProps) -> Self {
1505 Self {
1506 key: key.into(),
1507 component: Component::CalendarCell(props),
1508 action: None,
1509 visibility: None,
1510 }
1511 }
1512
1513 pub fn action_card(key: impl Into<String>, props: ActionCardProps) -> Self {
1515 Self {
1516 key: key.into(),
1517 component: Component::ActionCard(props),
1518 action: None,
1519 visibility: None,
1520 }
1521 }
1522
1523 pub fn product_tile(key: impl Into<String>, props: ProductTileProps) -> Self {
1525 Self {
1526 key: key.into(),
1527 component: Component::ProductTile(props),
1528 action: None,
1529 visibility: None,
1530 }
1531 }
1532
1533 pub fn data_table(key: impl Into<String>, props: DataTableProps) -> Self {
1535 Self {
1536 key: key.into(),
1537 component: Component::DataTable(props),
1538 action: None,
1539 visibility: None,
1540 }
1541 }
1542
1543 pub fn image(key: impl Into<String>, props: ImageProps) -> Self {
1545 Self {
1546 key: key.into(),
1547 component: Component::Image(props),
1548 action: None,
1549 visibility: None,
1550 }
1551 }
1552
1553 pub fn plugin_component(key: impl Into<String>, props: PluginProps) -> Self {
1557 Self {
1558 key: key.into(),
1559 component: Component::Plugin(props),
1560 action: None,
1561 visibility: None,
1562 }
1563 }
1564}
1565
1566#[cfg(test)]
1567mod tests {
1568 use super::*;
1569 use crate::action::HttpMethod;
1570 use crate::visibility::{VisibilityCondition, VisibilityOperator};
1571
1572 #[test]
1573 fn card_component_tagged_serialization() {
1574 let card = Component::Card(CardProps {
1575 title: "Test Card".to_string(),
1576 description: Some("A description".to_string()),
1577 children: vec![],
1578 footer: vec![],
1579 max_width: None,
1580 });
1581 let json = serde_json::to_value(&card).unwrap();
1582 assert_eq!(json["type"], "Card");
1583 assert_eq!(json["title"], "Test Card");
1584 assert_eq!(json["description"], "A description");
1585 }
1586
1587 #[test]
1588 fn button_variant_defaults_to_default() {
1589 let json = r#"{"type": "Button", "label": "Click me"}"#;
1590 let component: Component = serde_json::from_str(json).unwrap();
1591 match component {
1592 Component::Button(props) => {
1593 assert_eq!(props.variant, ButtonVariant::Default);
1594 assert_eq!(props.label, "Click me");
1595 }
1596 _ => panic!("expected Button"),
1597 }
1598 }
1599
1600 #[test]
1601 fn input_type_defaults_to_text() {
1602 let json = r#"{"type": "Input", "field": "email", "label": "Email"}"#;
1603 let component: Component = serde_json::from_str(json).unwrap();
1604 match component {
1605 Component::Input(props) => {
1606 assert_eq!(props.input_type, InputType::Text);
1607 assert_eq!(props.field, "email");
1608 }
1609 _ => panic!("expected Input"),
1610 }
1611 }
1612
1613 #[test]
1614 fn alert_variant_defaults_to_info() {
1615 let json = r#"{"type": "Alert", "message": "Hello"}"#;
1616 let component: Component = serde_json::from_str(json).unwrap();
1617 match component {
1618 Component::Alert(props) => assert_eq!(props.variant, AlertVariant::Info),
1619 _ => panic!("expected Alert"),
1620 }
1621 }
1622
1623 #[test]
1624 fn badge_variant_defaults_to_default() {
1625 let json = r#"{"type": "Badge", "label": "New"}"#;
1626 let component: Component = serde_json::from_str(json).unwrap();
1627 match component {
1628 Component::Badge(props) => assert_eq!(props.variant, BadgeVariant::Default),
1629 _ => panic!("expected Badge"),
1630 }
1631 }
1632
1633 #[test]
1634 fn text_element_defaults_to_p() {
1635 let json = r#"{"type": "Text", "content": "Hello world"}"#;
1636 let component: Component = serde_json::from_str(json).unwrap();
1637 match component {
1638 Component::Text(props) => {
1639 assert_eq!(props.element, TextElement::P);
1640 assert_eq!(props.content, "Hello world");
1641 }
1642 _ => panic!("expected Text"),
1643 }
1644 }
1645
1646 #[test]
1647 fn table_component_round_trips() {
1648 let table = Component::Table(TableProps {
1649 columns: vec![
1650 Column {
1651 key: "name".to_string(),
1652 label: "Name".to_string(),
1653 format: None,
1654 },
1655 Column {
1656 key: "created_at".to_string(),
1657 label: "Created".to_string(),
1658 format: Some(ColumnFormat::Date),
1659 },
1660 ],
1661 data_path: "/data/users".to_string(),
1662 row_actions: None,
1663 empty_message: Some("No users found".to_string()),
1664 sortable: None,
1665 sort_column: None,
1666 sort_direction: None,
1667 });
1668 let json = serde_json::to_string(&table).unwrap();
1669 let parsed: Component = serde_json::from_str(&json).unwrap();
1670 assert_eq!(parsed, table);
1671 }
1672
1673 #[test]
1674 fn select_component_round_trips() {
1675 let select = Component::Select(SelectProps {
1676 field: "role".to_string(),
1677 label: "Role".to_string(),
1678 options: vec![
1679 SelectOption {
1680 value: "admin".to_string(),
1681 label: "Administrator".to_string(),
1682 },
1683 SelectOption {
1684 value: "user".to_string(),
1685 label: "User".to_string(),
1686 },
1687 ],
1688 placeholder: Some("Select a role".to_string()),
1689 required: Some(true),
1690 disabled: None,
1691 error: None,
1692 description: None,
1693 default_value: None,
1694 data_path: None,
1695 });
1696 let json = serde_json::to_string(&select).unwrap();
1697 let parsed: Component = serde_json::from_str(&json).unwrap();
1698 assert_eq!(parsed, select);
1699 }
1700
1701 #[test]
1702 fn modal_component_round_trips() {
1703 let modal = Component::Modal(ModalProps {
1704 id: "modal-confirm".to_string(),
1705 title: "Confirm".to_string(),
1706 description: None,
1707 children: vec![ComponentNode {
1708 key: "msg".to_string(),
1709 component: Component::Text(TextProps {
1710 content: "Are you sure?".to_string(),
1711 element: TextElement::P,
1712 }),
1713 action: None,
1714 visibility: None,
1715 }],
1716 footer: vec![],
1717 trigger_label: Some("Open".to_string()),
1718 });
1719 let json = serde_json::to_string(&modal).unwrap();
1720 let parsed: Component = serde_json::from_str(&json).unwrap();
1721 assert_eq!(parsed, modal);
1722 }
1723
1724 #[test]
1725 fn form_component_round_trips() {
1726 let form = Component::Form(FormProps {
1727 action: Action {
1728 handler: "users.store".to_string(),
1729 url: None,
1730 method: HttpMethod::Post,
1731 confirm: None,
1732 on_success: None,
1733 on_error: None,
1734 target: None,
1735 },
1736 fields: vec![ComponentNode {
1737 key: "email-input".to_string(),
1738 component: Component::Input(InputProps {
1739 field: "email".to_string(),
1740 label: "Email".to_string(),
1741 input_type: InputType::Email,
1742 placeholder: Some("user@example.com".to_string()),
1743 required: Some(true),
1744 disabled: None,
1745 error: None,
1746 description: None,
1747 default_value: None,
1748 data_path: None,
1749 step: None,
1750 list: None,
1751 }),
1752 action: None,
1753 visibility: None,
1754 }],
1755 method: None,
1756 guard: None,
1757 max_width: None,
1758 });
1759 let json = serde_json::to_string(&form).unwrap();
1760 let parsed: Component = serde_json::from_str(&json).unwrap();
1761 assert_eq!(parsed, form);
1762 }
1763
1764 #[test]
1765 fn component_node_with_action_and_visibility() {
1766 let node = ComponentNode {
1767 key: "create-btn".to_string(),
1768 component: Component::Button(ButtonProps {
1769 label: "Create User".to_string(),
1770 variant: ButtonVariant::Default,
1771 size: Size::Default,
1772 disabled: None,
1773 icon: None,
1774 icon_position: None,
1775 button_type: None,
1776 }),
1777 action: Some(Action {
1778 handler: "users.create".to_string(),
1779 url: None,
1780 method: HttpMethod::Post,
1781 confirm: None,
1782 on_success: None,
1783 on_error: None,
1784 target: None,
1785 }),
1786 visibility: Some(Visibility::Condition(VisibilityCondition {
1787 path: "/auth/user/role".to_string(),
1788 operator: VisibilityOperator::Eq,
1789 value: Some(serde_json::Value::String("admin".to_string())),
1790 })),
1791 };
1792 let json = serde_json::to_string(&node).unwrap();
1793 let parsed: ComponentNode = serde_json::from_str(&json).unwrap();
1794 assert_eq!(parsed, node);
1795
1796 let value = serde_json::to_value(&node).unwrap();
1798 assert_eq!(value["type"], "Button");
1799 assert_eq!(value["key"], "create-btn");
1800 assert!(value.get("action").is_some());
1801 assert!(value.get("visibility").is_some());
1802 }
1803
1804 #[test]
1805 fn all_component_variants_serialize() {
1806 let components: Vec<Component> = vec![
1807 Component::Card(CardProps {
1808 title: "t".to_string(),
1809 description: None,
1810 children: vec![],
1811 footer: vec![],
1812 max_width: None,
1813 }),
1814 Component::Table(TableProps {
1815 columns: vec![],
1816 data_path: "/d".to_string(),
1817 row_actions: None,
1818 empty_message: None,
1819 sortable: None,
1820 sort_column: None,
1821 sort_direction: None,
1822 }),
1823 Component::Form(FormProps {
1824 action: Action {
1825 handler: "h.m".to_string(),
1826 url: None,
1827 method: HttpMethod::Post,
1828 confirm: None,
1829 on_success: None,
1830 on_error: None,
1831 target: None,
1832 },
1833 fields: vec![],
1834 method: None,
1835 guard: None,
1836 max_width: None,
1837 }),
1838 Component::Button(ButtonProps {
1839 label: "b".to_string(),
1840 variant: ButtonVariant::Default,
1841 size: Size::Default,
1842 disabled: None,
1843 icon: None,
1844 icon_position: None,
1845 button_type: None,
1846 }),
1847 Component::Input(InputProps {
1848 field: "f".to_string(),
1849 label: "l".to_string(),
1850 input_type: InputType::Text,
1851 placeholder: None,
1852 required: None,
1853 disabled: None,
1854 error: None,
1855 description: None,
1856 default_value: None,
1857 data_path: None,
1858 step: None,
1859 list: None,
1860 }),
1861 Component::Select(SelectProps {
1862 field: "f".to_string(),
1863 label: "l".to_string(),
1864 options: vec![],
1865 placeholder: None,
1866 required: None,
1867 disabled: None,
1868 error: None,
1869 description: None,
1870 default_value: None,
1871 data_path: None,
1872 }),
1873 Component::Alert(AlertProps {
1874 message: "m".to_string(),
1875 variant: AlertVariant::Info,
1876 title: None,
1877 }),
1878 Component::Badge(BadgeProps {
1879 label: "b".to_string(),
1880 variant: BadgeVariant::Default,
1881 }),
1882 Component::Modal(ModalProps {
1883 id: "modal-t".to_string(),
1884 title: "t".to_string(),
1885 description: None,
1886 children: vec![],
1887 footer: vec![],
1888 trigger_label: None,
1889 }),
1890 Component::Text(TextProps {
1891 content: "c".to_string(),
1892 element: TextElement::P,
1893 }),
1894 Component::Checkbox(CheckboxProps {
1895 field: "f".to_string(),
1896 value: None,
1897 label: "l".to_string(),
1898 description: None,
1899 checked: None,
1900 data_path: None,
1901 required: None,
1902 disabled: None,
1903 error: None,
1904 }),
1905 Component::Switch(SwitchProps {
1906 field: "f".to_string(),
1907 label: "l".to_string(),
1908 description: None,
1909 checked: None,
1910 data_path: None,
1911 required: None,
1912 disabled: None,
1913 error: None,
1914 action: None,
1915 }),
1916 Component::Separator(SeparatorProps { orientation: None }),
1917 Component::DescriptionList(DescriptionListProps {
1918 items: vec![DescriptionItem {
1919 label: "k".to_string(),
1920 value: "v".to_string(),
1921 format: None,
1922 }],
1923 columns: None,
1924 }),
1925 Component::Tabs(TabsProps {
1926 default_tab: "t1".to_string(),
1927 tabs: vec![Tab {
1928 value: "t1".to_string(),
1929 label: "Tab 1".to_string(),
1930 children: vec![],
1931 }],
1932 }),
1933 Component::Breadcrumb(BreadcrumbProps {
1934 items: vec![BreadcrumbItem {
1935 label: "Home".to_string(),
1936 url: Some("/".to_string()),
1937 }],
1938 }),
1939 Component::Pagination(PaginationProps {
1940 current_page: 1,
1941 per_page: 10,
1942 total: 100,
1943 base_url: None,
1944 }),
1945 Component::Progress(ProgressProps {
1946 value: 50,
1947 max: None,
1948 label: None,
1949 }),
1950 Component::Avatar(AvatarProps {
1951 src: None,
1952 alt: "User".to_string(),
1953 fallback: Some("U".to_string()),
1954 size: None,
1955 }),
1956 Component::Skeleton(SkeletonProps {
1957 width: None,
1958 height: None,
1959 rounded: None,
1960 }),
1961 Component::StatCard(StatCardProps {
1962 label: "Revenue".to_string(),
1963 value: "$1,234".to_string(),
1964 icon: None,
1965 subtitle: None,
1966 sse_target: None,
1967 }),
1968 Component::Checklist(ChecklistProps {
1969 title: "Tasks".to_string(),
1970 items: vec![],
1971 dismissible: true,
1972 dismiss_label: None,
1973 data_key: None,
1974 }),
1975 Component::Toast(ToastProps {
1976 message: "Saved!".to_string(),
1977 variant: ToastVariant::Success,
1978 timeout: None,
1979 dismissible: true,
1980 }),
1981 Component::NotificationDropdown(NotificationDropdownProps {
1982 notifications: vec![],
1983 empty_text: None,
1984 }),
1985 Component::Sidebar(SidebarProps {
1986 fixed_top: vec![],
1987 groups: vec![],
1988 fixed_bottom: vec![],
1989 }),
1990 Component::Header(HeaderProps {
1991 business_name: "Acme".to_string(),
1992 notification_count: None,
1993 user_name: None,
1994 user_avatar: None,
1995 logout_url: None,
1996 }),
1997 Component::Image(ImageProps {
1998 src: "/img/screenshot.png".to_string(),
1999 alt: "Page screenshot".to_string(),
2000 aspect_ratio: None,
2001 placeholder_label: None,
2002 }),
2003 ];
2004 assert_eq!(components.len(), 27, "should have 27 component variants");
2005 let expected_types = [
2006 "Card",
2007 "Table",
2008 "Form",
2009 "Button",
2010 "Input",
2011 "Select",
2012 "Alert",
2013 "Badge",
2014 "Modal",
2015 "Text",
2016 "Checkbox",
2017 "Switch",
2018 "Separator",
2019 "DescriptionList",
2020 "Tabs",
2021 "Breadcrumb",
2022 "Pagination",
2023 "Progress",
2024 "Avatar",
2025 "Skeleton",
2026 "StatCard",
2027 "Checklist",
2028 "Toast",
2029 "NotificationDropdown",
2030 "Sidebar",
2031 "Header",
2032 "Image",
2033 ];
2034 for (component, expected_type) in components.iter().zip(expected_types.iter()) {
2035 let json = serde_json::to_value(component).unwrap();
2036 assert_eq!(
2037 json["type"], *expected_type,
2038 "component should serialize with type={expected_type}"
2039 );
2040 let roundtripped: Component = serde_json::from_value(json).unwrap();
2041 assert_eq!(&roundtripped, component);
2042 }
2043 }
2044
2045 #[test]
2046 fn size_enum_serialization() {
2047 let cases = [
2048 (Size::Xs, "xs"),
2049 (Size::Sm, "sm"),
2050 (Size::Default, "default"),
2051 (Size::Lg, "lg"),
2052 ];
2053 for (size, expected) in &cases {
2054 let json = serde_json::to_value(size).unwrap();
2055 assert_eq!(json, *expected);
2056 let parsed: Size = serde_json::from_value(json).unwrap();
2057 assert_eq!(&parsed, size);
2058 }
2059 }
2060
2061 #[test]
2062 fn icon_position_serialization() {
2063 let cases = [(IconPosition::Left, "left"), (IconPosition::Right, "right")];
2064 for (pos, expected) in &cases {
2065 let json = serde_json::to_value(pos).unwrap();
2066 assert_eq!(json, *expected);
2067 let parsed: IconPosition = serde_json::from_value(json).unwrap();
2068 assert_eq!(&parsed, pos);
2069 }
2070 }
2071
2072 #[test]
2073 fn sort_direction_serialization() {
2074 let cases = [(SortDirection::Asc, "asc"), (SortDirection::Desc, "desc")];
2075 for (dir, expected) in &cases {
2076 let json = serde_json::to_value(dir).unwrap();
2077 assert_eq!(json, *expected);
2078 let parsed: SortDirection = serde_json::from_value(json).unwrap();
2079 assert_eq!(&parsed, dir);
2080 }
2081 }
2082
2083 #[test]
2084 fn button_with_size_and_icon() {
2085 let button = Component::Button(ButtonProps {
2086 label: "Save".to_string(),
2087 variant: ButtonVariant::Default,
2088 size: Size::Lg,
2089 disabled: None,
2090 icon: Some("save".to_string()),
2091 icon_position: Some(IconPosition::Left),
2092 button_type: None,
2093 });
2094 let json = serde_json::to_value(&button).unwrap();
2095 assert_eq!(json["size"], "lg");
2096 assert_eq!(json["icon"], "save");
2097 assert_eq!(json["icon_position"], "left");
2098 let parsed: Component = serde_json::from_value(json).unwrap();
2099 assert_eq!(parsed, button);
2100 }
2101
2102 #[test]
2103 fn card_with_footer() {
2104 let card = Component::Card(CardProps {
2105 title: "Actions".to_string(),
2106 description: None,
2107 children: vec![],
2108 max_width: None,
2109 footer: vec![ComponentNode {
2110 key: "cancel".to_string(),
2111 component: Component::Button(ButtonProps {
2112 label: "Cancel".to_string(),
2113 variant: ButtonVariant::Outline,
2114 size: Size::Default,
2115 disabled: None,
2116 icon: None,
2117 icon_position: None,
2118 button_type: None,
2119 }),
2120 action: None,
2121 visibility: None,
2122 }],
2123 });
2124 let json = serde_json::to_value(&card).unwrap();
2125 assert!(json["footer"].is_array());
2126 assert_eq!(json["footer"][0]["label"], "Cancel");
2127 let parsed: Component = serde_json::from_value(json).unwrap();
2128 assert_eq!(parsed, card);
2129 }
2130
2131 #[test]
2132 fn input_with_error_and_description() {
2133 let input = Component::Input(InputProps {
2134 field: "email".to_string(),
2135 label: "Email".to_string(),
2136 input_type: InputType::Email,
2137 placeholder: None,
2138 required: Some(true),
2139 disabled: Some(false),
2140 error: Some("Invalid email".to_string()),
2141 description: Some("Your work email".to_string()),
2142 default_value: Some("user@example.com".to_string()),
2143 data_path: None,
2144 step: None,
2145 list: None,
2146 });
2147 let json = serde_json::to_value(&input).unwrap();
2148 assert_eq!(json["error"], "Invalid email");
2149 assert_eq!(json["description"], "Your work email");
2150 assert_eq!(json["default_value"], "user@example.com");
2151 assert_eq!(json["disabled"], false);
2152 let parsed: Component = serde_json::from_value(json).unwrap();
2153 assert_eq!(parsed, input);
2154 }
2155
2156 #[test]
2157 fn select_with_default_value() {
2158 let select = Component::Select(SelectProps {
2159 field: "role".to_string(),
2160 label: "Role".to_string(),
2161 options: vec![SelectOption {
2162 value: "admin".to_string(),
2163 label: "Admin".to_string(),
2164 }],
2165 placeholder: None,
2166 required: None,
2167 disabled: Some(true),
2168 error: Some("Required field".to_string()),
2169 description: Some("User role".to_string()),
2170 default_value: Some("admin".to_string()),
2171 data_path: None,
2172 });
2173 let json = serde_json::to_value(&select).unwrap();
2174 assert_eq!(json["default_value"], "admin");
2175 assert_eq!(json["error"], "Required field");
2176 assert_eq!(json["description"], "User role");
2177 assert_eq!(json["disabled"], true);
2178 let parsed: Component = serde_json::from_value(json).unwrap();
2179 assert_eq!(parsed, select);
2180 }
2181
2182 #[test]
2183 fn alert_with_title() {
2184 let alert = Component::Alert(AlertProps {
2185 message: "Something happened".to_string(),
2186 variant: AlertVariant::Warning,
2187 title: Some("Warning".to_string()),
2188 });
2189 let json = serde_json::to_value(&alert).unwrap();
2190 assert_eq!(json["title"], "Warning");
2191 assert_eq!(json["message"], "Something happened");
2192 let parsed: Component = serde_json::from_value(json).unwrap();
2193 assert_eq!(parsed, alert);
2194 }
2195
2196 #[test]
2197 fn modal_with_footer_and_description() {
2198 let modal = Component::Modal(ModalProps {
2199 id: "modal-delete-item".to_string(),
2200 title: "Delete Item".to_string(),
2201 description: Some("This action cannot be undone.".to_string()),
2202 children: vec![],
2203 footer: vec![ComponentNode {
2204 key: "confirm".to_string(),
2205 component: Component::Button(ButtonProps {
2206 label: "Delete".to_string(),
2207 variant: ButtonVariant::Destructive,
2208 size: Size::Default,
2209 disabled: None,
2210 icon: None,
2211 icon_position: None,
2212 button_type: None,
2213 }),
2214 action: None,
2215 visibility: None,
2216 }],
2217 trigger_label: Some("Delete".to_string()),
2218 });
2219 let json = serde_json::to_value(&modal).unwrap();
2220 assert_eq!(json["description"], "This action cannot be undone.");
2221 assert!(json["footer"].is_array());
2222 assert_eq!(json["footer"][0]["label"], "Delete");
2223 let parsed: Component = serde_json::from_value(json).unwrap();
2224 assert_eq!(parsed, modal);
2225 }
2226
2227 #[test]
2228 fn table_with_sort_props() {
2229 let table = Component::Table(TableProps {
2230 columns: vec![Column {
2231 key: "name".to_string(),
2232 label: "Name".to_string(),
2233 format: None,
2234 }],
2235 data_path: "/data/users".to_string(),
2236 row_actions: None,
2237 empty_message: None,
2238 sortable: Some(true),
2239 sort_column: Some("name".to_string()),
2240 sort_direction: Some(SortDirection::Desc),
2241 });
2242 let json = serde_json::to_value(&table).unwrap();
2243 assert_eq!(json["sortable"], true);
2244 assert_eq!(json["sort_column"], "name");
2245 assert_eq!(json["sort_direction"], "desc");
2246 let parsed: Component = serde_json::from_value(json).unwrap();
2247 assert_eq!(parsed, table);
2248 }
2249
2250 #[test]
2251 fn aligned_button_variants_serialize() {
2252 let cases = [
2253 (ButtonVariant::Default, "default"),
2254 (ButtonVariant::Secondary, "secondary"),
2255 (ButtonVariant::Destructive, "destructive"),
2256 (ButtonVariant::Outline, "outline"),
2257 (ButtonVariant::Ghost, "ghost"),
2258 (ButtonVariant::Link, "link"),
2259 ];
2260 for (variant, expected) in &cases {
2261 let json = serde_json::to_value(variant).unwrap();
2262 assert_eq!(
2263 json, *expected,
2264 "ButtonVariant::{variant:?} should serialize as {expected}"
2265 );
2266 let parsed: ButtonVariant = serde_json::from_value(json).unwrap();
2267 assert_eq!(&parsed, variant);
2268 }
2269 }
2270
2271 #[test]
2272 fn aligned_badge_variants_serialize() {
2273 let cases = [
2274 (BadgeVariant::Default, "default"),
2275 (BadgeVariant::Secondary, "secondary"),
2276 (BadgeVariant::Destructive, "destructive"),
2277 (BadgeVariant::Outline, "outline"),
2278 ];
2279 for (variant, expected) in &cases {
2280 let json = serde_json::to_value(variant).unwrap();
2281 assert_eq!(
2282 json, *expected,
2283 "BadgeVariant::{variant:?} should serialize as {expected}"
2284 );
2285 let parsed: BadgeVariant = serde_json::from_value(json).unwrap();
2286 assert_eq!(&parsed, variant);
2287 }
2288 }
2289
2290 #[test]
2291 fn checkbox_round_trips() {
2292 let checkbox = Component::Checkbox(CheckboxProps {
2293 field: "terms".to_string(),
2294 value: None,
2295 label: "Accept Terms".to_string(),
2296 description: Some("You must accept the terms".to_string()),
2297 checked: Some(true),
2298 data_path: None,
2299 required: Some(true),
2300 disabled: Some(false),
2301 error: None,
2302 });
2303 let json = serde_json::to_value(&checkbox).unwrap();
2304 assert_eq!(json["type"], "Checkbox");
2305 assert_eq!(json["field"], "terms");
2306 assert_eq!(json["checked"], true);
2307 assert_eq!(json["description"], "You must accept the terms");
2308 let parsed: Component = serde_json::from_value(json).unwrap();
2309 assert_eq!(parsed, checkbox);
2310 }
2311
2312 #[test]
2313 fn switch_round_trips() {
2314 let switch = Component::Switch(SwitchProps {
2315 field: "notifications".to_string(),
2316 label: "Enable Notifications".to_string(),
2317 description: Some("Receive email notifications".to_string()),
2318 checked: Some(false),
2319 data_path: None,
2320 required: None,
2321 disabled: Some(false),
2322 error: None,
2323 action: None,
2324 });
2325 let json = serde_json::to_value(&switch).unwrap();
2326 assert_eq!(json["type"], "Switch");
2327 assert_eq!(json["field"], "notifications");
2328 assert_eq!(json["checked"], false);
2329 let parsed: Component = serde_json::from_value(json).unwrap();
2330 assert_eq!(parsed, switch);
2331 }
2332
2333 #[test]
2334 fn separator_defaults_to_horizontal() {
2335 let json = r#"{"type": "Separator"}"#;
2336 let component: Component = serde_json::from_str(json).unwrap();
2337 match component {
2338 Component::Separator(props) => {
2339 assert_eq!(props.orientation, None);
2340 let explicit = Component::Separator(SeparatorProps {
2343 orientation: Some(Orientation::Horizontal),
2344 });
2345 let v = serde_json::to_value(&explicit).unwrap();
2346 assert_eq!(v["orientation"], "horizontal");
2347 let parsed: Component = serde_json::from_value(v).unwrap();
2348 assert_eq!(parsed, explicit);
2349 }
2350 _ => panic!("expected Separator"),
2351 }
2352 }
2353
2354 #[test]
2355 fn description_list_with_format() {
2356 let dl = Component::DescriptionList(DescriptionListProps {
2357 items: vec![
2358 DescriptionItem {
2359 label: "Created".to_string(),
2360 value: "2026-01-15".to_string(),
2361 format: Some(ColumnFormat::Date),
2362 },
2363 DescriptionItem {
2364 label: "Name".to_string(),
2365 value: "Alice".to_string(),
2366 format: None,
2367 },
2368 ],
2369 columns: Some(2),
2370 });
2371 let json = serde_json::to_value(&dl).unwrap();
2372 assert_eq!(json["type"], "DescriptionList");
2373 assert_eq!(json["columns"], 2);
2374 assert_eq!(json["items"][0]["format"], "date");
2375 assert!(json["items"][1].get("format").is_none());
2376 let parsed: Component = serde_json::from_value(json).unwrap();
2377 assert_eq!(parsed, dl);
2378 }
2379
2380 #[test]
2381 fn checkbox_with_error() {
2382 let checkbox = Component::Checkbox(CheckboxProps {
2383 field: "agree".to_string(),
2384 value: None,
2385 label: "I agree".to_string(),
2386 description: None,
2387 checked: None,
2388 data_path: None,
2389 required: Some(true),
2390 disabled: None,
2391 error: Some("You must agree".to_string()),
2392 });
2393 let json = serde_json::to_value(&checkbox).unwrap();
2394 assert_eq!(json["error"], "You must agree");
2395 assert!(json.get("description").is_none());
2396 assert!(json.get("checked").is_none());
2397 let parsed: Component = serde_json::from_value(json).unwrap();
2398 assert_eq!(parsed, checkbox);
2399 }
2400
2401 #[test]
2402 fn tabs_round_trips() {
2403 let tabs = Component::Tabs(TabsProps {
2404 default_tab: "general".to_string(),
2405 tabs: vec![
2406 Tab {
2407 value: "general".to_string(),
2408 label: "General".to_string(),
2409 children: vec![ComponentNode {
2410 key: "name-input".to_string(),
2411 component: Component::Input(InputProps {
2412 field: "name".to_string(),
2413 label: "Name".to_string(),
2414 input_type: InputType::Text,
2415 placeholder: None,
2416 required: None,
2417 disabled: None,
2418 error: None,
2419 description: None,
2420 default_value: None,
2421 data_path: None,
2422 step: None,
2423 list: None,
2424 }),
2425 action: None,
2426 visibility: None,
2427 }],
2428 },
2429 Tab {
2430 value: "security".to_string(),
2431 label: "Security".to_string(),
2432 children: vec![ComponentNode {
2433 key: "password-input".to_string(),
2434 component: Component::Input(InputProps {
2435 field: "password".to_string(),
2436 label: "Password".to_string(),
2437 input_type: InputType::Password,
2438 placeholder: None,
2439 required: None,
2440 disabled: None,
2441 error: None,
2442 description: None,
2443 default_value: None,
2444 data_path: None,
2445 step: None,
2446 list: None,
2447 }),
2448 action: None,
2449 visibility: None,
2450 }],
2451 },
2452 ],
2453 });
2454 let json = serde_json::to_string(&tabs).unwrap();
2455 let parsed: Component = serde_json::from_str(&json).unwrap();
2456 assert_eq!(parsed, tabs);
2457 }
2458
2459 #[test]
2460 fn breadcrumb_round_trips() {
2461 let breadcrumb = Component::Breadcrumb(BreadcrumbProps {
2462 items: vec![
2463 BreadcrumbItem {
2464 label: "Home".to_string(),
2465 url: Some("/".to_string()),
2466 },
2467 BreadcrumbItem {
2468 label: "Users".to_string(),
2469 url: Some("/users".to_string()),
2470 },
2471 BreadcrumbItem {
2472 label: "Edit User".to_string(),
2473 url: None,
2474 },
2475 ],
2476 });
2477 let json = serde_json::to_string(&breadcrumb).unwrap();
2478 let parsed: Component = serde_json::from_str(&json).unwrap();
2479 assert_eq!(parsed, breadcrumb);
2480
2481 let value = serde_json::to_value(&breadcrumb).unwrap();
2483 assert!(value["items"][2].get("url").is_none());
2484 }
2485
2486 #[test]
2487 fn pagination_round_trips() {
2488 let pagination = Component::Pagination(PaginationProps {
2489 current_page: 3,
2490 per_page: 25,
2491 total: 150,
2492 base_url: None,
2493 });
2494 let json = serde_json::to_string(&pagination).unwrap();
2495 let parsed: Component = serde_json::from_str(&json).unwrap();
2496 assert_eq!(parsed, pagination);
2497 }
2498
2499 #[test]
2500 fn progress_round_trips() {
2501 let progress = Component::Progress(ProgressProps {
2502 value: 75,
2503 max: Some(100),
2504 label: Some("Uploading...".to_string()),
2505 });
2506 let json = serde_json::to_string(&progress).unwrap();
2507 let parsed: Component = serde_json::from_str(&json).unwrap();
2508 assert_eq!(parsed, progress);
2509
2510 let value = serde_json::to_value(&progress).unwrap();
2511 assert_eq!(value["value"], 75);
2512 assert_eq!(value["max"], 100);
2513 assert_eq!(value["label"], "Uploading...");
2514 }
2515
2516 #[test]
2517 fn avatar_with_fallback() {
2518 let avatar = Component::Avatar(AvatarProps {
2519 src: None,
2520 alt: "John Doe".to_string(),
2521 fallback: Some("JD".to_string()),
2522 size: Some(Size::Lg),
2523 });
2524 let json = serde_json::to_string(&avatar).unwrap();
2525 let parsed: Component = serde_json::from_str(&json).unwrap();
2526 assert_eq!(parsed, avatar);
2527
2528 let value = serde_json::to_value(&avatar).unwrap();
2529 assert!(value.get("src").is_none());
2530 assert_eq!(value["fallback"], "JD");
2531 assert_eq!(value["size"], "lg");
2532 }
2533
2534 #[test]
2535 fn skeleton_round_trips() {
2536 let skeleton = Component::Skeleton(SkeletonProps {
2537 width: Some("100%".to_string()),
2538 height: Some("40px".to_string()),
2539 rounded: Some(true),
2540 });
2541 let json = serde_json::to_string(&skeleton).unwrap();
2542 let parsed: Component = serde_json::from_str(&json).unwrap();
2543 assert_eq!(parsed, skeleton);
2544
2545 let value = serde_json::to_value(&skeleton).unwrap();
2546 assert_eq!(value["width"], "100%");
2547 assert_eq!(value["height"], "40px");
2548 assert_eq!(value["rounded"], true);
2549 }
2550
2551 #[test]
2552 fn tabs_deserializes_from_json() {
2553 let json = r#"{
2554 "type": "Tabs",
2555 "default_tab": "general",
2556 "tabs": [
2557 {
2558 "value": "general",
2559 "label": "General",
2560 "children": [
2561 {
2562 "key": "name-input",
2563 "type": "Input",
2564 "field": "name",
2565 "label": "Name"
2566 }
2567 ]
2568 },
2569 {
2570 "value": "security",
2571 "label": "Security"
2572 }
2573 ]
2574 }"#;
2575 let component: Component = serde_json::from_str(json).unwrap();
2576 match component {
2577 Component::Tabs(props) => {
2578 assert_eq!(props.default_tab, "general");
2579 assert_eq!(props.tabs.len(), 2);
2580 assert_eq!(props.tabs[0].value, "general");
2581 assert_eq!(props.tabs[0].children.len(), 1);
2582 assert_eq!(props.tabs[1].value, "security");
2583 assert!(props.tabs[1].children.is_empty());
2584 }
2585 _ => panic!("expected Tabs"),
2586 }
2587 }
2588
2589 #[test]
2590 fn input_data_path_round_trips() {
2591 let input = Component::Input(InputProps {
2592 field: "name".to_string(),
2593 label: "Name".to_string(),
2594 input_type: InputType::Text,
2595 placeholder: None,
2596 required: None,
2597 disabled: None,
2598 error: None,
2599 description: None,
2600 default_value: None,
2601 data_path: Some("/data/user/name".to_string()),
2602 step: None,
2603 list: None,
2604 });
2605 let json = serde_json::to_value(&input).unwrap();
2606 assert_eq!(json["data_path"], "/data/user/name");
2607 let parsed: Component = serde_json::from_value(json).unwrap();
2608 assert_eq!(parsed, input);
2609 }
2610
2611 #[test]
2612 fn select_data_path_round_trips() {
2613 let select = Component::Select(SelectProps {
2614 field: "role".to_string(),
2615 label: "Role".to_string(),
2616 options: vec![SelectOption {
2617 value: "admin".to_string(),
2618 label: "Admin".to_string(),
2619 }],
2620 placeholder: None,
2621 required: None,
2622 disabled: None,
2623 error: None,
2624 description: None,
2625 default_value: None,
2626 data_path: Some("/data/user/role".to_string()),
2627 });
2628 let json = serde_json::to_value(&select).unwrap();
2629 assert_eq!(json["data_path"], "/data/user/role");
2630 let parsed: Component = serde_json::from_value(json).unwrap();
2631 assert_eq!(parsed, select);
2632 }
2633
2634 #[test]
2635 fn checkbox_data_path_round_trips() {
2636 let checkbox = Component::Checkbox(CheckboxProps {
2637 field: "terms".to_string(),
2638 value: None,
2639 label: "Accept Terms".to_string(),
2640 description: None,
2641 checked: None,
2642 data_path: Some("/data/user/accepted_terms".to_string()),
2643 required: None,
2644 disabled: None,
2645 error: None,
2646 });
2647 let json = serde_json::to_value(&checkbox).unwrap();
2648 assert_eq!(json["data_path"], "/data/user/accepted_terms");
2649 let parsed: Component = serde_json::from_value(json).unwrap();
2650 assert_eq!(parsed, checkbox);
2651 }
2652
2653 #[test]
2654 fn switch_data_path_round_trips() {
2655 let switch = Component::Switch(SwitchProps {
2656 field: "notifications".to_string(),
2657 label: "Enable Notifications".to_string(),
2658 description: None,
2659 checked: None,
2660 data_path: Some("/data/user/notifications_enabled".to_string()),
2661 required: None,
2662 disabled: None,
2663 error: None,
2664 action: None,
2665 });
2666 let json = serde_json::to_value(&switch).unwrap();
2667 assert_eq!(json["data_path"], "/data/user/notifications_enabled");
2668 let parsed: Component = serde_json::from_value(json).unwrap();
2669 assert_eq!(parsed, switch);
2670 }
2671
2672 #[test]
2675 fn unknown_type_deserializes_as_plugin() {
2676 let json = r#"{"type": "Map", "center": [40.7, -74.0], "zoom": 12}"#;
2677 let component: Component = serde_json::from_str(json).unwrap();
2678 match component {
2679 Component::Plugin(props) => {
2680 assert_eq!(props.plugin_type, "Map");
2681 assert_eq!(props.props["center"][0], 40.7);
2682 assert_eq!(props.props["center"][1], -74.0);
2683 assert_eq!(props.props["zoom"], 12);
2684 assert!(props.props.get("type").is_none());
2686 }
2687 _ => panic!("expected Plugin"),
2688 }
2689 }
2690
2691 #[test]
2692 fn plugin_round_trips() {
2693 let plugin = Component::Plugin(PluginProps {
2694 plugin_type: "Chart".to_string(),
2695 props: serde_json::json!({"data": [1, 2, 3], "style": "bar"}),
2696 });
2697 let json = serde_json::to_value(&plugin).unwrap();
2698 assert_eq!(json["type"], "Chart");
2699 assert_eq!(json["data"], serde_json::json!([1, 2, 3]));
2700 assert_eq!(json["style"], "bar");
2701
2702 let parsed: Component = serde_json::from_value(json).unwrap();
2703 assert_eq!(parsed, plugin);
2704 }
2705
2706 #[test]
2707 fn plugin_serializes_with_type_field() {
2708 let plugin = Component::Plugin(PluginProps {
2709 plugin_type: "Map".to_string(),
2710 props: serde_json::json!({"lat": 51.5, "lng": -0.1}),
2711 });
2712 let json = serde_json::to_value(&plugin).unwrap();
2713 assert_eq!(json["type"], "Map");
2714 assert_eq!(json["lat"], 51.5);
2715 assert_eq!(json["lng"], -0.1);
2716 }
2717
2718 #[test]
2719 fn plugin_with_empty_props() {
2720 let json = r#"{"type": "CustomWidget"}"#;
2721 let component: Component = serde_json::from_str(json).unwrap();
2722 match component {
2723 Component::Plugin(props) => {
2724 assert_eq!(props.plugin_type, "CustomWidget");
2725 assert!(props.props.as_object().unwrap().is_empty());
2726 }
2727 _ => panic!("expected Plugin"),
2728 }
2729 }
2730
2731 #[test]
2732 fn plugin_in_component_node() {
2733 let node = ComponentNode {
2734 key: "map-1".to_string(),
2735 component: Component::Plugin(PluginProps {
2736 plugin_type: "Map".to_string(),
2737 props: serde_json::json!({"center": [0.0, 0.0]}),
2738 }),
2739 action: None,
2740 visibility: None,
2741 };
2742 let json = serde_json::to_string(&node).unwrap();
2743 let parsed: ComponentNode = serde_json::from_str(&json).unwrap();
2744 assert_eq!(parsed, node);
2745
2746 let value = serde_json::to_value(&node).unwrap();
2747 assert_eq!(value["type"], "Map");
2748 assert_eq!(value["key"], "map-1");
2749 }
2750
2751 #[test]
2752 fn known_types_not_treated_as_plugin() {
2753 let known_types = [
2755 "Card",
2756 "Table",
2757 "Form",
2758 "Button",
2759 "Input",
2760 "Select",
2761 "Alert",
2762 "Badge",
2763 "Modal",
2764 "Text",
2765 "Checkbox",
2766 "Switch",
2767 "Separator",
2768 "DescriptionList",
2769 "Tabs",
2770 "Breadcrumb",
2771 "Pagination",
2772 "Progress",
2773 "Avatar",
2774 "Skeleton",
2775 ];
2776 for type_name in &known_types {
2777 let json_str = match *type_name {
2780 "Card" => r#"{"type":"Card","title":"t"}"#,
2781 "Table" => r#"{"type":"Table","columns":[],"data_path":"/d"}"#,
2782 "Form" => r#"{"type":"Form","action":{"handler":"h","method":"POST"},"fields":[]}"#,
2783 "Button" => r#"{"type":"Button","label":"b"}"#,
2784 "Input" => r#"{"type":"Input","field":"f","label":"l"}"#,
2785 "Select" => r#"{"type":"Select","field":"f","label":"l","options":[]}"#,
2786 "Alert" => r#"{"type":"Alert","message":"m"}"#,
2787 "Badge" => r#"{"type":"Badge","label":"b"}"#,
2788 "Modal" => r#"{"type":"Modal","id":"modal-t","title":"t"}"#,
2789 "Text" => r#"{"type":"Text","content":"c"}"#,
2790 "Checkbox" => r#"{"type":"Checkbox","field":"f","label":"l"}"#,
2791 "Switch" => r#"{"type":"Switch","field":"f","label":"l"}"#,
2792 "Separator" => r#"{"type":"Separator"}"#,
2793 "DescriptionList" => r#"{"type":"DescriptionList","items":[]}"#,
2794 "Tabs" => r#"{"type":"Tabs","default_tab":"t","tabs":[]}"#,
2795 "Breadcrumb" => r#"{"type":"Breadcrumb","items":[]}"#,
2796 "Pagination" => r#"{"type":"Pagination","current_page":1,"per_page":10,"total":0}"#,
2797 "Progress" => r#"{"type":"Progress","value":0}"#,
2798 "Avatar" => r#"{"type":"Avatar","alt":"a"}"#,
2799 "Skeleton" => r#"{"type":"Skeleton"}"#,
2800 _ => unreachable!(),
2801 };
2802 let component: Component = serde_json::from_str(json_str).unwrap();
2803 assert!(
2804 !matches!(component, Component::Plugin(_)),
2805 "type {type_name} should not deserialize as Plugin"
2806 );
2807 }
2808 }
2809
2810 #[test]
2813 fn test_stat_card_serde_round_trip() {
2814 let component = Component::StatCard(StatCardProps {
2815 label: "Orders".into(),
2816 value: "42".into(),
2817 icon: Some("package".into()),
2818 subtitle: Some("today".into()),
2819 sse_target: Some("orders_today".into()),
2820 });
2821 let json = serde_json::to_string(&component).unwrap();
2822 assert!(json.contains("\"type\":\"StatCard\""));
2823 assert!(json.contains("\"sse_target\":\"orders_today\""));
2824 let deserialized: Component = serde_json::from_str(&json).unwrap();
2825 assert_eq!(component, deserialized);
2826 }
2827
2828 #[test]
2829 fn test_checklist_serde_round_trip() {
2830 let component = Component::Checklist(ChecklistProps {
2831 title: "Getting Started".into(),
2832 items: vec![
2833 ChecklistItem {
2834 label: "Install dependencies".into(),
2835 checked: true,
2836 href: None,
2837 },
2838 ChecklistItem {
2839 label: "Read the docs".into(),
2840 checked: false,
2841 href: Some("/docs".into()),
2842 },
2843 ],
2844 dismissible: true,
2845 dismiss_label: Some("Dismiss".into()),
2846 data_key: Some("onboarding".into()),
2847 });
2848 let json = serde_json::to_string(&component).unwrap();
2849 assert!(json.contains("\"type\":\"Checklist\""));
2850 assert!(json.contains("\"data_key\":\"onboarding\""));
2851 let deserialized: Component = serde_json::from_str(&json).unwrap();
2852 assert_eq!(component, deserialized);
2853 }
2854
2855 #[test]
2856 fn test_toast_serde_round_trip() {
2857 let component = Component::Toast(ToastProps {
2858 message: "Operation completed".into(),
2859 variant: ToastVariant::Success,
2860 timeout: Some(10),
2861 dismissible: true,
2862 });
2863 let json = serde_json::to_string(&component).unwrap();
2864 assert!(json.contains("\"type\":\"Toast\""));
2865 assert!(json.contains("\"timeout\":10"));
2866 let deserialized: Component = serde_json::from_str(&json).unwrap();
2867 assert_eq!(component, deserialized);
2868 }
2869
2870 #[test]
2871 fn test_notification_dropdown_serde_round_trip() {
2872 let component = Component::NotificationDropdown(NotificationDropdownProps {
2873 notifications: vec![
2874 NotificationItem {
2875 icon: Some("bell".into()),
2876 text: "New message".into(),
2877 timestamp: Some("2m ago".into()),
2878 read: false,
2879 action_url: Some("/messages/1".into()),
2880 },
2881 NotificationItem {
2882 icon: None,
2883 text: "Old notification".into(),
2884 timestamp: None,
2885 read: true,
2886 action_url: None,
2887 },
2888 ],
2889 empty_text: Some("No notifications".into()),
2890 });
2891 let json = serde_json::to_string(&component).unwrap();
2892 assert!(json.contains("\"type\":\"NotificationDropdown\""));
2893 assert!(json.contains("\"empty_text\":\"No notifications\""));
2894 let deserialized: Component = serde_json::from_str(&json).unwrap();
2895 assert_eq!(component, deserialized);
2896 }
2897
2898 #[test]
2899 fn test_sidebar_serde_round_trip() {
2900 let component = Component::Sidebar(SidebarProps {
2901 fixed_top: vec![SidebarNavItem {
2902 label: "Dashboard".into(),
2903 href: "/dashboard".into(),
2904 icon: Some("home".into()),
2905 active: true,
2906 }],
2907 groups: vec![SidebarGroup {
2908 label: "Management".into(),
2909 collapsed: false,
2910 items: vec![SidebarNavItem {
2911 label: "Users".into(),
2912 href: "/users".into(),
2913 icon: None,
2914 active: false,
2915 }],
2916 }],
2917 fixed_bottom: vec![SidebarNavItem {
2918 label: "Settings".into(),
2919 href: "/settings".into(),
2920 icon: Some("gear".into()),
2921 active: false,
2922 }],
2923 });
2924 let json = serde_json::to_string(&component).unwrap();
2925 assert!(json.contains("\"type\":\"Sidebar\""));
2926 assert!(json.contains("\"fixed_top\""));
2927 let deserialized: Component = serde_json::from_str(&json).unwrap();
2928 assert_eq!(component, deserialized);
2929 }
2930
2931 #[test]
2932 fn test_header_serde_round_trip() {
2933 let component = Component::Header(HeaderProps {
2934 business_name: "Acme Corp".into(),
2935 notification_count: Some(5),
2936 user_name: Some("Jane Doe".into()),
2937 user_avatar: Some("/avatar.jpg".into()),
2938 logout_url: Some("/logout".into()),
2939 });
2940 let json = serde_json::to_string(&component).unwrap();
2941 assert!(json.contains("\"type\":\"Header\""));
2942 assert!(json.contains("\"business_name\":\"Acme Corp\""));
2943 assert!(json.contains("\"notification_count\":5"));
2944 let deserialized: Component = serde_json::from_str(&json).unwrap();
2945 assert_eq!(component, deserialized);
2946 }
2947
2948 #[test]
2951 fn test_stat_card_constructor() {
2952 let props = StatCardProps {
2953 label: "Revenue".into(),
2954 value: "$1,000".into(),
2955 icon: None,
2956 subtitle: None,
2957 sse_target: None,
2958 };
2959 let node = ComponentNode::stat_card("revenue-card", props.clone());
2960 assert_eq!(node.key, "revenue-card");
2961 assert!(node.action.is_none());
2962 assert!(node.visibility.is_none());
2963 assert_eq!(node.component, Component::StatCard(props));
2964 }
2965
2966 #[test]
2967 fn test_checklist_constructor() {
2968 let props = ChecklistProps {
2969 title: "Tasks".into(),
2970 items: vec![],
2971 dismissible: true,
2972 dismiss_label: None,
2973 data_key: None,
2974 };
2975 let node = ComponentNode::checklist("task-list", props.clone());
2976 assert_eq!(node.key, "task-list");
2977 assert!(node.action.is_none());
2978 assert!(node.visibility.is_none());
2979 assert_eq!(node.component, Component::Checklist(props));
2980 }
2981
2982 #[test]
2983 fn test_toast_constructor() {
2984 let props = ToastProps {
2985 message: "Done!".into(),
2986 variant: ToastVariant::Success,
2987 timeout: None,
2988 dismissible: true,
2989 };
2990 let node = ComponentNode::toast("success-toast", props.clone());
2991 assert_eq!(node.key, "success-toast");
2992 assert!(node.action.is_none());
2993 assert!(node.visibility.is_none());
2994 assert_eq!(node.component, Component::Toast(props));
2995 }
2996
2997 #[test]
2998 fn test_notification_dropdown_constructor() {
2999 let props = NotificationDropdownProps {
3000 notifications: vec![],
3001 empty_text: Some("All caught up!".into()),
3002 };
3003 let node = ComponentNode::notification_dropdown("notifs", props.clone());
3004 assert_eq!(node.key, "notifs");
3005 assert!(node.action.is_none());
3006 assert!(node.visibility.is_none());
3007 assert_eq!(node.component, Component::NotificationDropdown(props));
3008 }
3009
3010 #[test]
3011 fn test_sidebar_constructor() {
3012 let props = SidebarProps {
3013 fixed_top: vec![],
3014 groups: vec![],
3015 fixed_bottom: vec![],
3016 };
3017 let node = ComponentNode::sidebar("main-nav", props.clone());
3018 assert_eq!(node.key, "main-nav");
3019 assert!(node.action.is_none());
3020 assert!(node.visibility.is_none());
3021 assert_eq!(node.component, Component::Sidebar(props));
3022 }
3023
3024 #[test]
3025 fn test_header_constructor() {
3026 let props = HeaderProps {
3027 business_name: "MyApp".into(),
3028 notification_count: None,
3029 user_name: None,
3030 user_avatar: None,
3031 logout_url: None,
3032 };
3033 let node = ComponentNode::header("page-header", props.clone());
3034 assert_eq!(node.key, "page-header");
3035 assert!(node.action.is_none());
3036 assert!(node.visibility.is_none());
3037 assert_eq!(node.component, Component::Header(props));
3038 }
3039
3040 #[test]
3043 fn test_checklist_item_round_trip() {
3044 let checked_item = ChecklistItem {
3045 label: "Completed task".into(),
3046 checked: true,
3047 href: Some("/task/1".into()),
3048 };
3049 let json = serde_json::to_string(&checked_item).unwrap();
3050 let parsed: ChecklistItem = serde_json::from_str(&json).unwrap();
3051 assert_eq!(parsed, checked_item);
3052
3053 let unchecked_item = ChecklistItem {
3054 label: "Pending task".into(),
3055 checked: false,
3056 href: None,
3057 };
3058 let json = serde_json::to_string(&unchecked_item).unwrap();
3059 let parsed: ChecklistItem = serde_json::from_str(&json).unwrap();
3060 assert_eq!(parsed, unchecked_item);
3061 assert!(!json.contains("href"));
3063 }
3064
3065 #[test]
3066 fn test_sidebar_group_round_trip() {
3067 let expanded = SidebarGroup {
3068 label: "Main".into(),
3069 collapsed: false,
3070 items: vec![
3071 SidebarNavItem {
3072 label: "Home".into(),
3073 href: "/".into(),
3074 icon: Some("home".into()),
3075 active: true,
3076 },
3077 SidebarNavItem {
3078 label: "About".into(),
3079 href: "/about".into(),
3080 icon: None,
3081 active: false,
3082 },
3083 ],
3084 };
3085 let json = serde_json::to_string(&expanded).unwrap();
3086 let parsed: SidebarGroup = serde_json::from_str(&json).unwrap();
3087 assert_eq!(parsed, expanded);
3088 assert_eq!(parsed.items.len(), 2);
3089
3090 let collapsed = SidebarGroup {
3091 label: "Advanced".into(),
3092 collapsed: true,
3093 items: vec![],
3094 };
3095 let json = serde_json::to_string(&collapsed).unwrap();
3096 let parsed: SidebarGroup = serde_json::from_str(&json).unwrap();
3097 assert_eq!(parsed, collapsed);
3098 assert!(parsed.collapsed);
3099 }
3100
3101 #[test]
3102 fn test_notification_item_round_trip() {
3103 let unread = NotificationItem {
3104 icon: Some("mail".into()),
3105 text: "You have a new message".into(),
3106 timestamp: Some("5m ago".into()),
3107 read: false,
3108 action_url: Some("/messages/42".into()),
3109 };
3110 let json = serde_json::to_string(&unread).unwrap();
3111 let parsed: NotificationItem = serde_json::from_str(&json).unwrap();
3112 assert_eq!(parsed, unread);
3113 assert!(!parsed.read);
3114
3115 let read_notif = NotificationItem {
3116 icon: None,
3117 text: "Welcome to the platform".into(),
3118 timestamp: None,
3119 read: true,
3120 action_url: None,
3121 };
3122 let json = serde_json::to_string(&read_notif).unwrap();
3123 let parsed: NotificationItem = serde_json::from_str(&json).unwrap();
3124 assert_eq!(parsed, read_notif);
3125 assert!(parsed.read);
3126 assert!(!json.contains("\"icon\""));
3128 assert!(!json.contains("\"action_url\""));
3129 }
3130
3131 #[test]
3134 fn test_stat_card_all_optionals_none() {
3135 let component = Component::StatCard(StatCardProps {
3136 label: "Count".into(),
3137 value: "0".into(),
3138 icon: None,
3139 subtitle: None,
3140 sse_target: None,
3141 });
3142 let json = serde_json::to_string(&component).unwrap();
3143 assert!(json.contains("\"type\":\"StatCard\""));
3144 assert!(!json.contains("\"icon\""));
3145 assert!(!json.contains("\"subtitle\""));
3146 assert!(!json.contains("\"sse_target\""));
3147 let deserialized: Component = serde_json::from_str(&json).unwrap();
3148 assert_eq!(component, deserialized);
3149 }
3150
3151 #[test]
3152 fn test_checklist_empty_items() {
3153 let component = Component::Checklist(ChecklistProps {
3154 title: "Empty List".into(),
3155 items: vec![],
3156 dismissible: true,
3157 dismiss_label: None,
3158 data_key: None,
3159 });
3160 let json = serde_json::to_string(&component).unwrap();
3161 assert!(json.contains("\"type\":\"Checklist\""));
3162 let deserialized: Component = serde_json::from_str(&json).unwrap();
3163 assert_eq!(component, deserialized);
3164 match &deserialized {
3165 Component::Checklist(props) => assert!(props.items.is_empty()),
3166 _ => panic!("expected Checklist"),
3167 }
3168 }
3169
3170 #[test]
3171 fn test_sidebar_empty_groups_and_fixed() {
3172 let component = Component::Sidebar(SidebarProps {
3173 fixed_top: vec![],
3174 groups: vec![],
3175 fixed_bottom: vec![],
3176 });
3177 let json = serde_json::to_string(&component).unwrap();
3178 assert!(json.contains("\"type\":\"Sidebar\""));
3179 assert!(!json.contains("\"fixed_top\""));
3181 assert!(!json.contains("\"groups\""));
3182 assert!(!json.contains("\"fixed_bottom\""));
3183 let deserialized: Component = serde_json::from_str(&json).unwrap();
3184 assert_eq!(component, deserialized);
3185 }
3186
3187 #[test]
3188 fn test_notification_dropdown_empty_uses_empty_text() {
3189 let component = Component::NotificationDropdown(NotificationDropdownProps {
3190 notifications: vec![],
3191 empty_text: Some("Nothing here!".into()),
3192 });
3193 let json = serde_json::to_string(&component).unwrap();
3194 assert!(json.contains("\"type\":\"NotificationDropdown\""));
3195 assert!(json.contains("\"empty_text\":\"Nothing here!\""));
3196 let deserialized: Component = serde_json::from_str(&json).unwrap();
3197 assert_eq!(component, deserialized);
3198 }
3199
3200 #[test]
3203 fn test_stat_card_omits_sse_target_when_none() {
3204 let component = Component::StatCard(StatCardProps {
3205 label: "Revenue".into(),
3206 value: "$500".into(),
3207 icon: None,
3208 subtitle: None,
3209 sse_target: None,
3210 });
3211 let json = serde_json::to_string(&component).unwrap();
3212 assert!(
3213 !json.contains("sse_target"),
3214 "sse_target must be omitted when None"
3215 );
3216 }
3217
3218 #[test]
3221 fn grid_round_trips() {
3222 let grid = Component::Grid(GridProps {
3223 columns: 3,
3224 md_columns: None,
3225 lg_columns: None,
3226 gap: GapSize::Lg,
3227 scrollable: None,
3228 children: vec![ComponentNode::text(
3229 "t",
3230 TextProps {
3231 content: "cell".into(),
3232 element: TextElement::P,
3233 },
3234 )],
3235 });
3236 let json = serde_json::to_value(&grid).unwrap();
3237 assert_eq!(json["type"], "Grid");
3238 assert_eq!(json["columns"], 3);
3239 assert_eq!(json["gap"], "lg");
3240 let parsed: Component = serde_json::from_value(json).unwrap();
3241 assert_eq!(parsed, grid);
3242 }
3243
3244 #[test]
3245 fn grid_defaults() {
3246 let json = serde_json::json!({"type": "Grid"});
3247 let parsed: Component = serde_json::from_value(json).unwrap();
3248 match parsed {
3249 Component::Grid(props) => {
3250 assert_eq!(props.columns, 2);
3251 assert_eq!(props.gap, GapSize::Md);
3252 assert!(props.children.is_empty());
3253 }
3254 _ => panic!("expected Grid"),
3255 }
3256 }
3257
3258 #[test]
3261 fn collapsible_round_trips() {
3262 let c = Component::Collapsible(CollapsibleProps {
3263 title: "Details".into(),
3264 expanded: true,
3265 children: vec![],
3266 });
3267 let json = serde_json::to_value(&c).unwrap();
3268 assert_eq!(json["type"], "Collapsible");
3269 assert_eq!(json["title"], "Details");
3270 assert_eq!(json["expanded"], true);
3271 let parsed: Component = serde_json::from_value(json).unwrap();
3272 assert_eq!(parsed, c);
3273 }
3274
3275 #[test]
3278 fn empty_state_round_trips() {
3279 let es = Component::EmptyState(EmptyStateProps {
3280 title: "No items".into(),
3281 description: Some("Create one".into()),
3282 action: Some(Action::get("items.create")),
3283 action_label: Some("New item".into()),
3284 });
3285 let json = serde_json::to_value(&es).unwrap();
3286 assert_eq!(json["type"], "EmptyState");
3287 assert_eq!(json["title"], "No items");
3288 let parsed: Component = serde_json::from_value(json).unwrap();
3289 assert_eq!(parsed, es);
3290 }
3291
3292 #[test]
3293 fn empty_state_minimal() {
3294 let json = serde_json::json!({"type": "EmptyState", "title": "Nothing"});
3295 let parsed: Component = serde_json::from_value(json).unwrap();
3296 match parsed {
3297 Component::EmptyState(props) => {
3298 assert_eq!(props.title, "Nothing");
3299 assert!(props.description.is_none());
3300 assert!(props.action.is_none());
3301 assert!(props.action_label.is_none());
3302 }
3303 _ => panic!("expected EmptyState"),
3304 }
3305 }
3306
3307 #[test]
3310 fn form_section_round_trips() {
3311 let fs = Component::FormSection(FormSectionProps {
3312 title: "Contact".into(),
3313 description: Some("Your details".into()),
3314 children: vec![],
3315 layout: None,
3316 });
3317 let json = serde_json::to_value(&fs).unwrap();
3318 assert_eq!(json["type"], "FormSection");
3319 assert_eq!(json["title"], "Contact");
3320 let parsed: Component = serde_json::from_value(json).unwrap();
3321 assert_eq!(parsed, fs);
3322 }
3323
3324 #[test]
3327 fn switch_with_action_round_trips() {
3328 let sw = Component::Switch(SwitchProps {
3329 field: "active".into(),
3330 label: "Active".into(),
3331 description: None,
3332 checked: Some(true),
3333 data_path: None,
3334 required: None,
3335 disabled: None,
3336 error: None,
3337 action: Some(Action::new("settings.toggle")),
3338 });
3339 let json = serde_json::to_value(&sw).unwrap();
3340 assert!(json["action"].is_object());
3341 assert_eq!(json["action"]["handler"], "settings.toggle");
3342 let parsed: Component = serde_json::from_value(json).unwrap();
3343 assert_eq!(parsed, sw);
3344 }
3345
3346 #[test]
3347 fn switch_without_action_omits_field() {
3348 let sw = Component::Switch(SwitchProps {
3349 field: "f".into(),
3350 label: "l".into(),
3351 description: None,
3352 checked: None,
3353 data_path: None,
3354 required: None,
3355 disabled: None,
3356 error: None,
3357 action: None,
3358 });
3359 let json = serde_json::to_string(&sw).unwrap();
3360 assert!(!json.contains("\"action\""));
3361 }
3362
3363 #[test]
3364 fn test_toast_omits_timeout_when_none() {
3365 let component = Component::Toast(ToastProps {
3366 message: "Hello".into(),
3367 variant: ToastVariant::Info,
3368 timeout: None,
3369 dismissible: false,
3370 });
3371 let json = serde_json::to_string(&component).unwrap();
3372 assert!(
3373 !json.contains("\"timeout\""),
3374 "timeout must be omitted when None"
3375 );
3376 }
3377
3378 #[test]
3379 fn page_header_round_trip_title_only() {
3380 let component = Component::PageHeader(PageHeaderProps {
3381 title: "Test Title".to_string(),
3382 breadcrumb: vec![],
3383 actions: vec![],
3384 });
3385 let json = serde_json::to_value(&component).unwrap();
3386 assert_eq!(json["type"], "PageHeader");
3387 assert_eq!(json["title"], "Test Title");
3388 assert!(json.get("breadcrumb").is_none());
3390 assert!(json.get("actions").is_none());
3391 let parsed: Component = serde_json::from_value(json).unwrap();
3392 assert_eq!(parsed, component);
3393 }
3394
3395 #[test]
3396 fn page_header_round_trip_with_breadcrumb_and_actions() {
3397 let component = Component::PageHeader(PageHeaderProps {
3398 title: "Users".to_string(),
3399 breadcrumb: vec![
3400 BreadcrumbItem {
3401 label: "Home".to_string(),
3402 url: Some("/".to_string()),
3403 },
3404 BreadcrumbItem {
3405 label: "Users".to_string(),
3406 url: None,
3407 },
3408 ],
3409 actions: vec![ComponentNode {
3410 key: "add-btn".to_string(),
3411 component: Component::Button(ButtonProps {
3412 label: "Add User".to_string(),
3413 variant: ButtonVariant::Default,
3414 size: Size::Default,
3415 disabled: None,
3416 icon: None,
3417 icon_position: None,
3418 button_type: None,
3419 }),
3420 action: None,
3421 visibility: None,
3422 }],
3423 });
3424 let json = serde_json::to_string(&component).unwrap();
3425 let parsed: Component = serde_json::from_str(&json).unwrap();
3426 assert_eq!(parsed, component);
3427 let value = serde_json::to_value(&component).unwrap();
3429 assert_eq!(value["type"], "PageHeader");
3430 assert_eq!(value["title"], "Users");
3431 assert!(value["breadcrumb"].is_array());
3432 assert!(value["actions"].is_array());
3433 }
3434
3435 #[test]
3436 fn page_header_deserialize_from_json() {
3437 let json = r#"{"type":"PageHeader","title":"Test"}"#;
3438 let component: Component = serde_json::from_str(json).unwrap();
3439 match component {
3440 Component::PageHeader(props) => {
3441 assert_eq!(props.title, "Test");
3442 assert!(props.breadcrumb.is_empty());
3443 assert!(props.actions.is_empty());
3444 }
3445 _ => panic!("expected PageHeader"),
3446 }
3447 }
3448
3449 #[test]
3450 fn button_group_round_trip_empty() {
3451 let component = Component::ButtonGroup(ButtonGroupProps { buttons: vec![] });
3452 let json = serde_json::to_value(&component).unwrap();
3453 assert_eq!(json["type"], "ButtonGroup");
3454 assert!(json.get("buttons").is_none());
3456 let parsed: Component = serde_json::from_value(json).unwrap();
3457 assert_eq!(parsed, component);
3458 }
3459
3460 #[test]
3461 fn button_group_round_trip_with_buttons() {
3462 let component = Component::ButtonGroup(ButtonGroupProps {
3463 buttons: vec![
3464 ComponentNode {
3465 key: "save".to_string(),
3466 component: Component::Button(ButtonProps {
3467 label: "Save".to_string(),
3468 variant: ButtonVariant::Default,
3469 size: Size::Default,
3470 disabled: None,
3471 icon: None,
3472 icon_position: None,
3473 button_type: None,
3474 }),
3475 action: None,
3476 visibility: None,
3477 },
3478 ComponentNode {
3479 key: "cancel".to_string(),
3480 component: Component::Button(ButtonProps {
3481 label: "Cancel".to_string(),
3482 variant: ButtonVariant::Outline,
3483 size: Size::Default,
3484 disabled: None,
3485 icon: None,
3486 icon_position: None,
3487 button_type: None,
3488 }),
3489 action: None,
3490 visibility: None,
3491 },
3492 ],
3493 });
3494 let json = serde_json::to_string(&component).unwrap();
3495 let parsed: Component = serde_json::from_str(&json).unwrap();
3496 assert_eq!(parsed, component);
3497 let value = serde_json::to_value(&component).unwrap();
3498 assert_eq!(value["type"], "ButtonGroup");
3499 assert!(value["buttons"].is_array());
3500 assert_eq!(value["buttons"].as_array().unwrap().len(), 2);
3501 }
3502
3503 #[test]
3504 fn button_group_deserialize_from_json() {
3505 let json = r#"{"type":"ButtonGroup","buttons":[]}"#;
3506 let component: Component = serde_json::from_str(json).unwrap();
3507 match component {
3508 Component::ButtonGroup(props) => {
3509 assert!(props.buttons.is_empty());
3510 }
3511 _ => panic!("expected ButtonGroup"),
3512 }
3513 }
3514
3515 #[test]
3516 fn image_round_trips() {
3517 let json = r#"{"type": "Image", "src": "/img/s.png", "alt": "Screenshot"}"#;
3518 let component: Component = serde_json::from_str(json).unwrap();
3519 match component {
3520 Component::Image(props) => {
3521 assert_eq!(props.src, "/img/s.png");
3522 assert_eq!(props.alt, "Screenshot");
3523 assert!(props.aspect_ratio.is_none());
3524 }
3525 _ => panic!("expected Image"),
3526 }
3527 }
3528
3529 #[test]
3530 fn all_known_types_round_trip() {
3531 let known_types: &[(&str, &str)] = &[
3532 ("Alert", r#"{"type":"Alert","message":"m"}"#),
3533 ("Avatar", r#"{"type":"Avatar","alt":"a"}"#),
3534 ("Badge", r#"{"type":"Badge","label":"b"}"#),
3535 ("Breadcrumb", r#"{"type":"Breadcrumb","items":[]}"#),
3536 ("Button", r#"{"type":"Button","label":"b"}"#),
3537 ("CalendarCell", r#"{"type":"CalendarCell","day":1}"#),
3538 ("Checkbox", r#"{"type":"Checkbox","field":"f","label":"l"}"#),
3539 ("Image", r#"{"type":"Image","src":"/img/s.png","alt":"a"}"#),
3540 ("Input", r#"{"type":"Input","field":"f","label":"l"}"#),
3541 (
3542 "Pagination",
3543 r#"{"type":"Pagination","current_page":1,"per_page":10,"total":0}"#,
3544 ),
3545 ("Progress", r#"{"type":"Progress","value":50}"#),
3546 (
3547 "Select",
3548 r#"{"type":"Select","field":"f","label":"l","options":[]}"#,
3549 ),
3550 ("Separator", r#"{"type":"Separator"}"#),
3551 ("Skeleton", r#"{"type":"Skeleton"}"#),
3552 ("Text", r#"{"type":"Text","content":"c"}"#),
3553 ];
3554 for (type_name, json_str) in known_types {
3555 let component: Component = serde_json::from_str(json_str)
3556 .unwrap_or_else(|e| panic!("failed to parse {type_name}: {e}"));
3557 let serialized = serde_json::to_value(&component).unwrap();
3558 assert_eq!(
3559 serialized["type"], *type_name,
3560 "type mismatch for {type_name}"
3561 );
3562 let reparsed: Component = serde_json::from_value(serialized)
3563 .unwrap_or_else(|e| panic!("failed to reparse {type_name}: {e}"));
3564 assert_eq!(
3565 serde_json::to_value(&reparsed).unwrap()["type"],
3566 *type_name,
3567 "round-trip type mismatch for {type_name}"
3568 );
3569 }
3570 }
3571}