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}
771
772#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
775pub struct KanbanColumnProps {
776 pub id: String,
777 pub title: String,
778 pub count: u32,
779 #[serde(default, skip_serializing_if = "Vec::is_empty")]
780 pub children: Vec<ComponentNode>,
781}
782
783#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
786pub struct KanbanBoardProps {
787 pub columns: Vec<KanbanColumnProps>,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub mobile_default_column: Option<String>,
790}
791
792#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
797pub struct CalendarCellProps {
798 pub day: u8,
799 #[serde(default)]
800 pub is_today: bool,
801 #[serde(default)]
802 pub is_current_month: bool,
803 #[serde(default)]
804 pub event_count: u32,
805 #[serde(default, skip_serializing_if = "Vec::is_empty")]
808 pub dot_colors: Vec<String>,
809}
810
811#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
813#[serde(rename_all = "snake_case")]
814pub enum ActionCardVariant {
815 #[default]
816 Default,
817 Setup,
818 Danger,
819}
820
821#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
826pub struct ActionCardProps {
827 pub title: String,
828 pub description: String,
829 #[serde(default, skip_serializing_if = "Option::is_none")]
830 pub icon: Option<String>,
831 #[serde(default)]
832 pub variant: ActionCardVariant,
833 #[serde(default, skip_serializing_if = "Option::is_none")]
835 pub href: Option<String>,
836}
837
838#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
843pub struct ProductTileProps {
844 pub product_id: String,
845 pub name: String,
846 pub price: String,
847 pub field: String,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
849 pub default_quantity: Option<u32>,
850}
851
852#[derive(Debug, Clone, PartialEq)]
859pub struct PluginProps {
860 pub plugin_type: String,
862 pub props: serde_json::Value,
864}
865
866impl Serialize for PluginProps {
867 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
868 let obj = self.props.as_object();
870 let extra_len = obj.map_or(0, |m| m.len());
871 let mut map = serializer.serialize_map(Some(1 + extra_len))?;
872 map.serialize_entry("type", &self.plugin_type)?;
873 if let Some(obj) = obj {
874 for (k, v) in obj {
875 if k != "type" {
876 map.serialize_entry(k, v)?;
877 }
878 }
879 }
880 map.end()
881 }
882}
883
884impl<'de> Deserialize<'de> for PluginProps {
885 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
886 let mut value = serde_json::Value::deserialize(deserializer)?;
887 let plugin_type = value
888 .get("type")
889 .and_then(|v| v.as_str())
890 .map(|s| s.to_string())
891 .ok_or_else(|| de::Error::missing_field("type"))?;
892 if let Some(obj) = value.as_object_mut() {
894 obj.remove("type");
895 }
896 Ok(PluginProps {
897 plugin_type,
898 props: value,
899 })
900 }
901}
902
903#[derive(Debug, Clone, PartialEq)]
910pub enum Component {
911 Card(CardProps),
912 Table(TableProps),
913 Form(FormProps),
914 Button(ButtonProps),
915 Input(InputProps),
916 Select(SelectProps),
917 Alert(AlertProps),
918 Badge(BadgeProps),
919 Modal(ModalProps),
920 Text(TextProps),
921 Checkbox(CheckboxProps),
922 Switch(SwitchProps),
923 Separator(SeparatorProps),
924 DescriptionList(DescriptionListProps),
925 Tabs(TabsProps),
926 Breadcrumb(BreadcrumbProps),
927 Pagination(PaginationProps),
928 Progress(ProgressProps),
929 Avatar(AvatarProps),
930 Skeleton(SkeletonProps),
931 StatCard(StatCardProps),
932 Checklist(ChecklistProps),
933 Toast(ToastProps),
934 NotificationDropdown(NotificationDropdownProps),
935 Sidebar(SidebarProps),
936 Header(HeaderProps),
937 Grid(GridProps),
938 Collapsible(CollapsibleProps),
939 EmptyState(EmptyStateProps),
940 FormSection(FormSectionProps),
941 PageHeader(PageHeaderProps),
942 ButtonGroup(ButtonGroupProps),
943 DropdownMenu(DropdownMenuProps),
944 KanbanBoard(KanbanBoardProps),
945 CalendarCell(CalendarCellProps),
946 ActionCard(ActionCardProps),
947 ProductTile(ProductTileProps),
948 DataTable(DataTableProps),
949 Image(ImageProps),
950 Plugin(PluginProps),
951}
952
953fn serialize_tagged<S: Serializer, T: Serialize>(
958 serializer: S,
959 type_name: &str,
960 props: &T,
961) -> Result<S::Ok, S::Error> {
962 let mut value = serde_json::to_value(props).map_err(serde::ser::Error::custom)?;
963 if let Some(obj) = value.as_object_mut() {
964 obj.insert(
965 "type".to_string(),
966 serde_json::Value::String(type_name.to_string()),
967 );
968 }
969 value.serialize(serializer)
970}
971
972impl Serialize for Component {
973 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
974 match self {
975 Component::Card(p) => serialize_tagged(serializer, "Card", p),
976 Component::Table(p) => serialize_tagged(serializer, "Table", p),
977 Component::Form(p) => serialize_tagged(serializer, "Form", p),
978 Component::Button(p) => serialize_tagged(serializer, "Button", p),
979 Component::Input(p) => serialize_tagged(serializer, "Input", p),
980 Component::Select(p) => serialize_tagged(serializer, "Select", p),
981 Component::Alert(p) => serialize_tagged(serializer, "Alert", p),
982 Component::Badge(p) => serialize_tagged(serializer, "Badge", p),
983 Component::Modal(p) => serialize_tagged(serializer, "Modal", p),
984 Component::Text(p) => serialize_tagged(serializer, "Text", p),
985 Component::Checkbox(p) => serialize_tagged(serializer, "Checkbox", p),
986 Component::Switch(p) => serialize_tagged(serializer, "Switch", p),
987 Component::Separator(p) => serialize_tagged(serializer, "Separator", p),
988 Component::DescriptionList(p) => serialize_tagged(serializer, "DescriptionList", p),
989 Component::Tabs(p) => serialize_tagged(serializer, "Tabs", p),
990 Component::Breadcrumb(p) => serialize_tagged(serializer, "Breadcrumb", p),
991 Component::Pagination(p) => serialize_tagged(serializer, "Pagination", p),
992 Component::Progress(p) => serialize_tagged(serializer, "Progress", p),
993 Component::Avatar(p) => serialize_tagged(serializer, "Avatar", p),
994 Component::Skeleton(p) => serialize_tagged(serializer, "Skeleton", p),
995 Component::StatCard(p) => serialize_tagged(serializer, "StatCard", p),
996 Component::Checklist(p) => serialize_tagged(serializer, "Checklist", p),
997 Component::Toast(p) => serialize_tagged(serializer, "Toast", p),
998 Component::NotificationDropdown(p) => {
999 serialize_tagged(serializer, "NotificationDropdown", p)
1000 }
1001 Component::Sidebar(p) => serialize_tagged(serializer, "Sidebar", p),
1002 Component::Header(p) => serialize_tagged(serializer, "Header", p),
1003 Component::Grid(p) => serialize_tagged(serializer, "Grid", p),
1004 Component::Collapsible(p) => serialize_tagged(serializer, "Collapsible", p),
1005 Component::EmptyState(p) => serialize_tagged(serializer, "EmptyState", p),
1006 Component::FormSection(p) => serialize_tagged(serializer, "FormSection", p),
1007 Component::PageHeader(p) => serialize_tagged(serializer, "PageHeader", p),
1008 Component::ButtonGroup(p) => serialize_tagged(serializer, "ButtonGroup", p),
1009 Component::DropdownMenu(p) => serialize_tagged(serializer, "DropdownMenu", p),
1010 Component::KanbanBoard(p) => serialize_tagged(serializer, "KanbanBoard", p),
1011 Component::CalendarCell(p) => serialize_tagged(serializer, "CalendarCell", p),
1012 Component::ActionCard(p) => serialize_tagged(serializer, "ActionCard", p),
1013 Component::ProductTile(p) => serialize_tagged(serializer, "ProductTile", p),
1014 Component::DataTable(p) => serialize_tagged(serializer, "DataTable", p),
1015 Component::Image(p) => serialize_tagged(serializer, "Image", p),
1016 Component::Plugin(p) => p.serialize(serializer),
1017 }
1018 }
1019}
1020
1021impl<'de> Deserialize<'de> for Component {
1024 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1025 let value = serde_json::Value::deserialize(deserializer)?;
1026 let type_str = value
1027 .get("type")
1028 .and_then(|v| v.as_str())
1029 .ok_or_else(|| de::Error::missing_field("type"))?;
1030
1031 match type_str {
1032 "Card" => serde_json::from_value::<CardProps>(value)
1033 .map(Component::Card)
1034 .map_err(de::Error::custom),
1035 "Table" => serde_json::from_value::<TableProps>(value)
1036 .map(Component::Table)
1037 .map_err(de::Error::custom),
1038 "Form" => serde_json::from_value::<FormProps>(value)
1039 .map(Component::Form)
1040 .map_err(de::Error::custom),
1041 "Button" => serde_json::from_value::<ButtonProps>(value)
1042 .map(Component::Button)
1043 .map_err(de::Error::custom),
1044 "Input" => serde_json::from_value::<InputProps>(value)
1045 .map(Component::Input)
1046 .map_err(de::Error::custom),
1047 "Select" => serde_json::from_value::<SelectProps>(value)
1048 .map(Component::Select)
1049 .map_err(de::Error::custom),
1050 "Alert" => serde_json::from_value::<AlertProps>(value)
1051 .map(Component::Alert)
1052 .map_err(de::Error::custom),
1053 "Badge" => serde_json::from_value::<BadgeProps>(value)
1054 .map(Component::Badge)
1055 .map_err(de::Error::custom),
1056 "Modal" => serde_json::from_value::<ModalProps>(value)
1057 .map(Component::Modal)
1058 .map_err(de::Error::custom),
1059 "Text" => serde_json::from_value::<TextProps>(value)
1060 .map(Component::Text)
1061 .map_err(de::Error::custom),
1062 "Checkbox" => serde_json::from_value::<CheckboxProps>(value)
1063 .map(Component::Checkbox)
1064 .map_err(de::Error::custom),
1065 "Switch" => serde_json::from_value::<SwitchProps>(value)
1066 .map(Component::Switch)
1067 .map_err(de::Error::custom),
1068 "Separator" => serde_json::from_value::<SeparatorProps>(value)
1069 .map(Component::Separator)
1070 .map_err(de::Error::custom),
1071 "DescriptionList" => serde_json::from_value::<DescriptionListProps>(value)
1072 .map(Component::DescriptionList)
1073 .map_err(de::Error::custom),
1074 "Tabs" => serde_json::from_value::<TabsProps>(value)
1075 .map(Component::Tabs)
1076 .map_err(de::Error::custom),
1077 "Breadcrumb" => serde_json::from_value::<BreadcrumbProps>(value)
1078 .map(Component::Breadcrumb)
1079 .map_err(de::Error::custom),
1080 "Pagination" => serde_json::from_value::<PaginationProps>(value)
1081 .map(Component::Pagination)
1082 .map_err(de::Error::custom),
1083 "Progress" => serde_json::from_value::<ProgressProps>(value)
1084 .map(Component::Progress)
1085 .map_err(de::Error::custom),
1086 "Avatar" => serde_json::from_value::<AvatarProps>(value)
1087 .map(Component::Avatar)
1088 .map_err(de::Error::custom),
1089 "Skeleton" => serde_json::from_value::<SkeletonProps>(value)
1090 .map(Component::Skeleton)
1091 .map_err(de::Error::custom),
1092 "StatCard" => serde_json::from_value::<StatCardProps>(value)
1093 .map(Component::StatCard)
1094 .map_err(de::Error::custom),
1095 "Checklist" => serde_json::from_value::<ChecklistProps>(value)
1096 .map(Component::Checklist)
1097 .map_err(de::Error::custom),
1098 "Toast" => serde_json::from_value::<ToastProps>(value)
1099 .map(Component::Toast)
1100 .map_err(de::Error::custom),
1101 "NotificationDropdown" => serde_json::from_value::<NotificationDropdownProps>(value)
1102 .map(Component::NotificationDropdown)
1103 .map_err(de::Error::custom),
1104 "Sidebar" => serde_json::from_value::<SidebarProps>(value)
1105 .map(Component::Sidebar)
1106 .map_err(de::Error::custom),
1107 "Header" => serde_json::from_value::<HeaderProps>(value)
1108 .map(Component::Header)
1109 .map_err(de::Error::custom),
1110 "Grid" => serde_json::from_value::<GridProps>(value)
1111 .map(Component::Grid)
1112 .map_err(de::Error::custom),
1113 "Collapsible" => serde_json::from_value::<CollapsibleProps>(value)
1114 .map(Component::Collapsible)
1115 .map_err(de::Error::custom),
1116 "EmptyState" => serde_json::from_value::<EmptyStateProps>(value)
1117 .map(Component::EmptyState)
1118 .map_err(de::Error::custom),
1119 "FormSection" => serde_json::from_value::<FormSectionProps>(value)
1120 .map(Component::FormSection)
1121 .map_err(de::Error::custom),
1122 "PageHeader" => serde_json::from_value::<PageHeaderProps>(value)
1123 .map(Component::PageHeader)
1124 .map_err(de::Error::custom),
1125 "ButtonGroup" => serde_json::from_value::<ButtonGroupProps>(value)
1126 .map(Component::ButtonGroup)
1127 .map_err(de::Error::custom),
1128 "DropdownMenu" => serde_json::from_value::<DropdownMenuProps>(value)
1129 .map(Component::DropdownMenu)
1130 .map_err(de::Error::custom),
1131 "KanbanBoard" => serde_json::from_value::<KanbanBoardProps>(value)
1132 .map(Component::KanbanBoard)
1133 .map_err(de::Error::custom),
1134 "CalendarCell" => serde_json::from_value::<CalendarCellProps>(value)
1135 .map(Component::CalendarCell)
1136 .map_err(de::Error::custom),
1137 "ActionCard" => serde_json::from_value::<ActionCardProps>(value)
1138 .map(Component::ActionCard)
1139 .map_err(de::Error::custom),
1140 "ProductTile" => serde_json::from_value::<ProductTileProps>(value)
1141 .map(Component::ProductTile)
1142 .map_err(de::Error::custom),
1143 "DataTable" => serde_json::from_value::<DataTableProps>(value)
1144 .map(Component::DataTable)
1145 .map_err(de::Error::custom),
1146 "Image" => serde_json::from_value::<ImageProps>(value)
1147 .map(Component::Image)
1148 .map_err(de::Error::custom),
1149 _ => {
1150 let plugin_type = type_str.to_string();
1152 let mut props = value;
1153 if let Some(obj) = props.as_object_mut() {
1154 obj.remove("type");
1155 }
1156 Ok(Component::Plugin(PluginProps { plugin_type, props }))
1157 }
1158 }
1159 }
1160}
1161
1162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1169pub struct ComponentNode {
1170 pub key: String,
1171 #[serde(flatten)]
1172 pub component: Component,
1173 #[serde(default, skip_serializing_if = "Option::is_none")]
1174 pub action: Option<Action>,
1175 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub visibility: Option<Visibility>,
1177}
1178
1179impl ComponentNode {
1180 pub fn card(key: impl Into<String>, props: CardProps) -> Self {
1182 Self {
1183 key: key.into(),
1184 component: Component::Card(props),
1185 action: None,
1186 visibility: None,
1187 }
1188 }
1189
1190 pub fn table(key: impl Into<String>, props: TableProps) -> Self {
1192 Self {
1193 key: key.into(),
1194 component: Component::Table(props),
1195 action: None,
1196 visibility: None,
1197 }
1198 }
1199
1200 pub fn form(key: impl Into<String>, props: FormProps) -> Self {
1202 Self {
1203 key: key.into(),
1204 component: Component::Form(props),
1205 action: None,
1206 visibility: None,
1207 }
1208 }
1209
1210 pub fn button(key: impl Into<String>, props: ButtonProps) -> Self {
1212 Self {
1213 key: key.into(),
1214 component: Component::Button(props),
1215 action: None,
1216 visibility: None,
1217 }
1218 }
1219
1220 pub fn input(key: impl Into<String>, props: InputProps) -> Self {
1222 Self {
1223 key: key.into(),
1224 component: Component::Input(props),
1225 action: None,
1226 visibility: None,
1227 }
1228 }
1229
1230 pub fn select(key: impl Into<String>, props: SelectProps) -> Self {
1232 Self {
1233 key: key.into(),
1234 component: Component::Select(props),
1235 action: None,
1236 visibility: None,
1237 }
1238 }
1239
1240 pub fn alert(key: impl Into<String>, props: AlertProps) -> Self {
1242 Self {
1243 key: key.into(),
1244 component: Component::Alert(props),
1245 action: None,
1246 visibility: None,
1247 }
1248 }
1249
1250 pub fn badge(key: impl Into<String>, props: BadgeProps) -> Self {
1252 Self {
1253 key: key.into(),
1254 component: Component::Badge(props),
1255 action: None,
1256 visibility: None,
1257 }
1258 }
1259
1260 pub fn modal(key: impl Into<String>, props: ModalProps) -> Self {
1262 Self {
1263 key: key.into(),
1264 component: Component::Modal(props),
1265 action: None,
1266 visibility: None,
1267 }
1268 }
1269
1270 pub fn text(key: impl Into<String>, props: TextProps) -> Self {
1272 Self {
1273 key: key.into(),
1274 component: Component::Text(props),
1275 action: None,
1276 visibility: None,
1277 }
1278 }
1279
1280 pub fn checkbox(key: impl Into<String>, props: CheckboxProps) -> Self {
1282 Self {
1283 key: key.into(),
1284 component: Component::Checkbox(props),
1285 action: None,
1286 visibility: None,
1287 }
1288 }
1289
1290 pub fn switch(key: impl Into<String>, props: SwitchProps) -> Self {
1292 Self {
1293 key: key.into(),
1294 component: Component::Switch(props),
1295 action: None,
1296 visibility: None,
1297 }
1298 }
1299
1300 pub fn separator(key: impl Into<String>, props: SeparatorProps) -> Self {
1302 Self {
1303 key: key.into(),
1304 component: Component::Separator(props),
1305 action: None,
1306 visibility: None,
1307 }
1308 }
1309
1310 pub fn description_list(key: impl Into<String>, props: DescriptionListProps) -> Self {
1312 Self {
1313 key: key.into(),
1314 component: Component::DescriptionList(props),
1315 action: None,
1316 visibility: None,
1317 }
1318 }
1319
1320 pub fn tabs(key: impl Into<String>, props: TabsProps) -> Self {
1322 Self {
1323 key: key.into(),
1324 component: Component::Tabs(props),
1325 action: None,
1326 visibility: None,
1327 }
1328 }
1329
1330 pub fn breadcrumb(key: impl Into<String>, props: BreadcrumbProps) -> Self {
1332 Self {
1333 key: key.into(),
1334 component: Component::Breadcrumb(props),
1335 action: None,
1336 visibility: None,
1337 }
1338 }
1339
1340 pub fn pagination(key: impl Into<String>, props: PaginationProps) -> Self {
1342 Self {
1343 key: key.into(),
1344 component: Component::Pagination(props),
1345 action: None,
1346 visibility: None,
1347 }
1348 }
1349
1350 pub fn progress(key: impl Into<String>, props: ProgressProps) -> Self {
1352 Self {
1353 key: key.into(),
1354 component: Component::Progress(props),
1355 action: None,
1356 visibility: None,
1357 }
1358 }
1359
1360 pub fn avatar(key: impl Into<String>, props: AvatarProps) -> Self {
1362 Self {
1363 key: key.into(),
1364 component: Component::Avatar(props),
1365 action: None,
1366 visibility: None,
1367 }
1368 }
1369
1370 pub fn skeleton(key: impl Into<String>, props: SkeletonProps) -> Self {
1372 Self {
1373 key: key.into(),
1374 component: Component::Skeleton(props),
1375 action: None,
1376 visibility: None,
1377 }
1378 }
1379
1380 pub fn stat_card(key: impl Into<String>, props: StatCardProps) -> Self {
1382 Self {
1383 key: key.into(),
1384 component: Component::StatCard(props),
1385 action: None,
1386 visibility: None,
1387 }
1388 }
1389
1390 pub fn checklist(key: impl Into<String>, props: ChecklistProps) -> Self {
1392 Self {
1393 key: key.into(),
1394 component: Component::Checklist(props),
1395 action: None,
1396 visibility: None,
1397 }
1398 }
1399
1400 pub fn toast(key: impl Into<String>, props: ToastProps) -> Self {
1402 Self {
1403 key: key.into(),
1404 component: Component::Toast(props),
1405 action: None,
1406 visibility: None,
1407 }
1408 }
1409
1410 pub fn notification_dropdown(key: impl Into<String>, props: NotificationDropdownProps) -> Self {
1412 Self {
1413 key: key.into(),
1414 component: Component::NotificationDropdown(props),
1415 action: None,
1416 visibility: None,
1417 }
1418 }
1419
1420 pub fn sidebar(key: impl Into<String>, props: SidebarProps) -> Self {
1422 Self {
1423 key: key.into(),
1424 component: Component::Sidebar(props),
1425 action: None,
1426 visibility: None,
1427 }
1428 }
1429
1430 pub fn header(key: impl Into<String>, props: HeaderProps) -> Self {
1432 Self {
1433 key: key.into(),
1434 component: Component::Header(props),
1435 action: None,
1436 visibility: None,
1437 }
1438 }
1439
1440 pub fn grid(key: impl Into<String>, props: GridProps) -> Self {
1442 Self {
1443 key: key.into(),
1444 component: Component::Grid(props),
1445 action: None,
1446 visibility: None,
1447 }
1448 }
1449
1450 pub fn collapsible(key: impl Into<String>, props: CollapsibleProps) -> Self {
1452 Self {
1453 key: key.into(),
1454 component: Component::Collapsible(props),
1455 action: None,
1456 visibility: None,
1457 }
1458 }
1459
1460 pub fn empty_state(key: impl Into<String>, props: EmptyStateProps) -> Self {
1462 Self {
1463 key: key.into(),
1464 component: Component::EmptyState(props),
1465 action: None,
1466 visibility: None,
1467 }
1468 }
1469
1470 pub fn form_section(key: impl Into<String>, props: FormSectionProps) -> Self {
1472 Self {
1473 key: key.into(),
1474 component: Component::FormSection(props),
1475 action: None,
1476 visibility: None,
1477 }
1478 }
1479
1480 pub fn dropdown_menu(key: impl Into<String>, props: DropdownMenuProps) -> Self {
1482 Self {
1483 key: key.into(),
1484 component: Component::DropdownMenu(props),
1485 action: None,
1486 visibility: None,
1487 }
1488 }
1489
1490 pub fn kanban_board(key: impl Into<String>, props: KanbanBoardProps) -> Self {
1492 Self {
1493 key: key.into(),
1494 component: Component::KanbanBoard(props),
1495 action: None,
1496 visibility: None,
1497 }
1498 }
1499
1500 pub fn calendar_cell(key: impl Into<String>, props: CalendarCellProps) -> Self {
1502 Self {
1503 key: key.into(),
1504 component: Component::CalendarCell(props),
1505 action: None,
1506 visibility: None,
1507 }
1508 }
1509
1510 pub fn action_card(key: impl Into<String>, props: ActionCardProps) -> Self {
1512 Self {
1513 key: key.into(),
1514 component: Component::ActionCard(props),
1515 action: None,
1516 visibility: None,
1517 }
1518 }
1519
1520 pub fn product_tile(key: impl Into<String>, props: ProductTileProps) -> Self {
1522 Self {
1523 key: key.into(),
1524 component: Component::ProductTile(props),
1525 action: None,
1526 visibility: None,
1527 }
1528 }
1529
1530 pub fn data_table(key: impl Into<String>, props: DataTableProps) -> Self {
1532 Self {
1533 key: key.into(),
1534 component: Component::DataTable(props),
1535 action: None,
1536 visibility: None,
1537 }
1538 }
1539
1540 pub fn image(key: impl Into<String>, props: ImageProps) -> Self {
1542 Self {
1543 key: key.into(),
1544 component: Component::Image(props),
1545 action: None,
1546 visibility: None,
1547 }
1548 }
1549
1550 pub fn plugin_component(key: impl Into<String>, props: PluginProps) -> Self {
1554 Self {
1555 key: key.into(),
1556 component: Component::Plugin(props),
1557 action: None,
1558 visibility: None,
1559 }
1560 }
1561}
1562
1563#[cfg(test)]
1564mod tests {
1565 use super::*;
1566 use crate::action::HttpMethod;
1567 use crate::visibility::{VisibilityCondition, VisibilityOperator};
1568
1569 #[test]
1570 fn card_component_tagged_serialization() {
1571 let card = Component::Card(CardProps {
1572 title: "Test Card".to_string(),
1573 description: Some("A description".to_string()),
1574 children: vec![],
1575 footer: vec![],
1576 max_width: None,
1577 });
1578 let json = serde_json::to_value(&card).unwrap();
1579 assert_eq!(json["type"], "Card");
1580 assert_eq!(json["title"], "Test Card");
1581 assert_eq!(json["description"], "A description");
1582 }
1583
1584 #[test]
1585 fn button_variant_defaults_to_default() {
1586 let json = r#"{"type": "Button", "label": "Click me"}"#;
1587 let component: Component = serde_json::from_str(json).unwrap();
1588 match component {
1589 Component::Button(props) => {
1590 assert_eq!(props.variant, ButtonVariant::Default);
1591 assert_eq!(props.label, "Click me");
1592 }
1593 _ => panic!("expected Button"),
1594 }
1595 }
1596
1597 #[test]
1598 fn input_type_defaults_to_text() {
1599 let json = r#"{"type": "Input", "field": "email", "label": "Email"}"#;
1600 let component: Component = serde_json::from_str(json).unwrap();
1601 match component {
1602 Component::Input(props) => {
1603 assert_eq!(props.input_type, InputType::Text);
1604 assert_eq!(props.field, "email");
1605 }
1606 _ => panic!("expected Input"),
1607 }
1608 }
1609
1610 #[test]
1611 fn alert_variant_defaults_to_info() {
1612 let json = r#"{"type": "Alert", "message": "Hello"}"#;
1613 let component: Component = serde_json::from_str(json).unwrap();
1614 match component {
1615 Component::Alert(props) => assert_eq!(props.variant, AlertVariant::Info),
1616 _ => panic!("expected Alert"),
1617 }
1618 }
1619
1620 #[test]
1621 fn badge_variant_defaults_to_default() {
1622 let json = r#"{"type": "Badge", "label": "New"}"#;
1623 let component: Component = serde_json::from_str(json).unwrap();
1624 match component {
1625 Component::Badge(props) => assert_eq!(props.variant, BadgeVariant::Default),
1626 _ => panic!("expected Badge"),
1627 }
1628 }
1629
1630 #[test]
1631 fn text_element_defaults_to_p() {
1632 let json = r#"{"type": "Text", "content": "Hello world"}"#;
1633 let component: Component = serde_json::from_str(json).unwrap();
1634 match component {
1635 Component::Text(props) => {
1636 assert_eq!(props.element, TextElement::P);
1637 assert_eq!(props.content, "Hello world");
1638 }
1639 _ => panic!("expected Text"),
1640 }
1641 }
1642
1643 #[test]
1644 fn table_component_round_trips() {
1645 let table = Component::Table(TableProps {
1646 columns: vec![
1647 Column {
1648 key: "name".to_string(),
1649 label: "Name".to_string(),
1650 format: None,
1651 },
1652 Column {
1653 key: "created_at".to_string(),
1654 label: "Created".to_string(),
1655 format: Some(ColumnFormat::Date),
1656 },
1657 ],
1658 data_path: "/data/users".to_string(),
1659 row_actions: None,
1660 empty_message: Some("No users found".to_string()),
1661 sortable: None,
1662 sort_column: None,
1663 sort_direction: None,
1664 });
1665 let json = serde_json::to_string(&table).unwrap();
1666 let parsed: Component = serde_json::from_str(&json).unwrap();
1667 assert_eq!(parsed, table);
1668 }
1669
1670 #[test]
1671 fn select_component_round_trips() {
1672 let select = Component::Select(SelectProps {
1673 field: "role".to_string(),
1674 label: "Role".to_string(),
1675 options: vec![
1676 SelectOption {
1677 value: "admin".to_string(),
1678 label: "Administrator".to_string(),
1679 },
1680 SelectOption {
1681 value: "user".to_string(),
1682 label: "User".to_string(),
1683 },
1684 ],
1685 placeholder: Some("Select a role".to_string()),
1686 required: Some(true),
1687 disabled: None,
1688 error: None,
1689 description: None,
1690 default_value: None,
1691 data_path: None,
1692 });
1693 let json = serde_json::to_string(&select).unwrap();
1694 let parsed: Component = serde_json::from_str(&json).unwrap();
1695 assert_eq!(parsed, select);
1696 }
1697
1698 #[test]
1699 fn modal_component_round_trips() {
1700 let modal = Component::Modal(ModalProps {
1701 id: "modal-confirm".to_string(),
1702 title: "Confirm".to_string(),
1703 description: None,
1704 children: vec![ComponentNode {
1705 key: "msg".to_string(),
1706 component: Component::Text(TextProps {
1707 content: "Are you sure?".to_string(),
1708 element: TextElement::P,
1709 }),
1710 action: None,
1711 visibility: None,
1712 }],
1713 footer: vec![],
1714 trigger_label: Some("Open".to_string()),
1715 });
1716 let json = serde_json::to_string(&modal).unwrap();
1717 let parsed: Component = serde_json::from_str(&json).unwrap();
1718 assert_eq!(parsed, modal);
1719 }
1720
1721 #[test]
1722 fn form_component_round_trips() {
1723 let form = Component::Form(FormProps {
1724 action: Action {
1725 handler: "users.store".to_string(),
1726 url: None,
1727 method: HttpMethod::Post,
1728 confirm: None,
1729 on_success: None,
1730 on_error: None,
1731 target: None,
1732 },
1733 fields: vec![ComponentNode {
1734 key: "email-input".to_string(),
1735 component: Component::Input(InputProps {
1736 field: "email".to_string(),
1737 label: "Email".to_string(),
1738 input_type: InputType::Email,
1739 placeholder: Some("user@example.com".to_string()),
1740 required: Some(true),
1741 disabled: None,
1742 error: None,
1743 description: None,
1744 default_value: None,
1745 data_path: None,
1746 step: None,
1747 list: None,
1748 }),
1749 action: None,
1750 visibility: None,
1751 }],
1752 method: None,
1753 guard: None,
1754 max_width: None,
1755 });
1756 let json = serde_json::to_string(&form).unwrap();
1757 let parsed: Component = serde_json::from_str(&json).unwrap();
1758 assert_eq!(parsed, form);
1759 }
1760
1761 #[test]
1762 fn component_node_with_action_and_visibility() {
1763 let node = ComponentNode {
1764 key: "create-btn".to_string(),
1765 component: Component::Button(ButtonProps {
1766 label: "Create User".to_string(),
1767 variant: ButtonVariant::Default,
1768 size: Size::Default,
1769 disabled: None,
1770 icon: None,
1771 icon_position: None,
1772 button_type: None,
1773 }),
1774 action: Some(Action {
1775 handler: "users.create".to_string(),
1776 url: None,
1777 method: HttpMethod::Post,
1778 confirm: None,
1779 on_success: None,
1780 on_error: None,
1781 target: None,
1782 }),
1783 visibility: Some(Visibility::Condition(VisibilityCondition {
1784 path: "/auth/user/role".to_string(),
1785 operator: VisibilityOperator::Eq,
1786 value: Some(serde_json::Value::String("admin".to_string())),
1787 })),
1788 };
1789 let json = serde_json::to_string(&node).unwrap();
1790 let parsed: ComponentNode = serde_json::from_str(&json).unwrap();
1791 assert_eq!(parsed, node);
1792
1793 let value = serde_json::to_value(&node).unwrap();
1795 assert_eq!(value["type"], "Button");
1796 assert_eq!(value["key"], "create-btn");
1797 assert!(value.get("action").is_some());
1798 assert!(value.get("visibility").is_some());
1799 }
1800
1801 #[test]
1802 fn all_component_variants_serialize() {
1803 let components: Vec<Component> = vec![
1804 Component::Card(CardProps {
1805 title: "t".to_string(),
1806 description: None,
1807 children: vec![],
1808 footer: vec![],
1809 max_width: None,
1810 }),
1811 Component::Table(TableProps {
1812 columns: vec![],
1813 data_path: "/d".to_string(),
1814 row_actions: None,
1815 empty_message: None,
1816 sortable: None,
1817 sort_column: None,
1818 sort_direction: None,
1819 }),
1820 Component::Form(FormProps {
1821 action: Action {
1822 handler: "h.m".to_string(),
1823 url: None,
1824 method: HttpMethod::Post,
1825 confirm: None,
1826 on_success: None,
1827 on_error: None,
1828 target: None,
1829 },
1830 fields: vec![],
1831 method: None,
1832 guard: None,
1833 max_width: None,
1834 }),
1835 Component::Button(ButtonProps {
1836 label: "b".to_string(),
1837 variant: ButtonVariant::Default,
1838 size: Size::Default,
1839 disabled: None,
1840 icon: None,
1841 icon_position: None,
1842 button_type: None,
1843 }),
1844 Component::Input(InputProps {
1845 field: "f".to_string(),
1846 label: "l".to_string(),
1847 input_type: InputType::Text,
1848 placeholder: None,
1849 required: None,
1850 disabled: None,
1851 error: None,
1852 description: None,
1853 default_value: None,
1854 data_path: None,
1855 step: None,
1856 list: None,
1857 }),
1858 Component::Select(SelectProps {
1859 field: "f".to_string(),
1860 label: "l".to_string(),
1861 options: vec![],
1862 placeholder: None,
1863 required: None,
1864 disabled: None,
1865 error: None,
1866 description: None,
1867 default_value: None,
1868 data_path: None,
1869 }),
1870 Component::Alert(AlertProps {
1871 message: "m".to_string(),
1872 variant: AlertVariant::Info,
1873 title: None,
1874 }),
1875 Component::Badge(BadgeProps {
1876 label: "b".to_string(),
1877 variant: BadgeVariant::Default,
1878 }),
1879 Component::Modal(ModalProps {
1880 id: "modal-t".to_string(),
1881 title: "t".to_string(),
1882 description: None,
1883 children: vec![],
1884 footer: vec![],
1885 trigger_label: None,
1886 }),
1887 Component::Text(TextProps {
1888 content: "c".to_string(),
1889 element: TextElement::P,
1890 }),
1891 Component::Checkbox(CheckboxProps {
1892 field: "f".to_string(),
1893 value: None,
1894 label: "l".to_string(),
1895 description: None,
1896 checked: None,
1897 data_path: None,
1898 required: None,
1899 disabled: None,
1900 error: None,
1901 }),
1902 Component::Switch(SwitchProps {
1903 field: "f".to_string(),
1904 label: "l".to_string(),
1905 description: None,
1906 checked: None,
1907 data_path: None,
1908 required: None,
1909 disabled: None,
1910 error: None,
1911 action: None,
1912 }),
1913 Component::Separator(SeparatorProps { orientation: None }),
1914 Component::DescriptionList(DescriptionListProps {
1915 items: vec![DescriptionItem {
1916 label: "k".to_string(),
1917 value: "v".to_string(),
1918 format: None,
1919 }],
1920 columns: None,
1921 }),
1922 Component::Tabs(TabsProps {
1923 default_tab: "t1".to_string(),
1924 tabs: vec![Tab {
1925 value: "t1".to_string(),
1926 label: "Tab 1".to_string(),
1927 children: vec![],
1928 }],
1929 }),
1930 Component::Breadcrumb(BreadcrumbProps {
1931 items: vec![BreadcrumbItem {
1932 label: "Home".to_string(),
1933 url: Some("/".to_string()),
1934 }],
1935 }),
1936 Component::Pagination(PaginationProps {
1937 current_page: 1,
1938 per_page: 10,
1939 total: 100,
1940 base_url: None,
1941 }),
1942 Component::Progress(ProgressProps {
1943 value: 50,
1944 max: None,
1945 label: None,
1946 }),
1947 Component::Avatar(AvatarProps {
1948 src: None,
1949 alt: "User".to_string(),
1950 fallback: Some("U".to_string()),
1951 size: None,
1952 }),
1953 Component::Skeleton(SkeletonProps {
1954 width: None,
1955 height: None,
1956 rounded: None,
1957 }),
1958 Component::StatCard(StatCardProps {
1959 label: "Revenue".to_string(),
1960 value: "$1,234".to_string(),
1961 icon: None,
1962 subtitle: None,
1963 sse_target: None,
1964 }),
1965 Component::Checklist(ChecklistProps {
1966 title: "Tasks".to_string(),
1967 items: vec![],
1968 dismissible: true,
1969 dismiss_label: None,
1970 data_key: None,
1971 }),
1972 Component::Toast(ToastProps {
1973 message: "Saved!".to_string(),
1974 variant: ToastVariant::Success,
1975 timeout: None,
1976 dismissible: true,
1977 }),
1978 Component::NotificationDropdown(NotificationDropdownProps {
1979 notifications: vec![],
1980 empty_text: None,
1981 }),
1982 Component::Sidebar(SidebarProps {
1983 fixed_top: vec![],
1984 groups: vec![],
1985 fixed_bottom: vec![],
1986 }),
1987 Component::Header(HeaderProps {
1988 business_name: "Acme".to_string(),
1989 notification_count: None,
1990 user_name: None,
1991 user_avatar: None,
1992 logout_url: None,
1993 }),
1994 Component::Image(ImageProps {
1995 src: "/img/screenshot.png".to_string(),
1996 alt: "Page screenshot".to_string(),
1997 aspect_ratio: None,
1998 placeholder_label: None,
1999 }),
2000 ];
2001 assert_eq!(components.len(), 27, "should have 27 component variants");
2002 let expected_types = [
2003 "Card",
2004 "Table",
2005 "Form",
2006 "Button",
2007 "Input",
2008 "Select",
2009 "Alert",
2010 "Badge",
2011 "Modal",
2012 "Text",
2013 "Checkbox",
2014 "Switch",
2015 "Separator",
2016 "DescriptionList",
2017 "Tabs",
2018 "Breadcrumb",
2019 "Pagination",
2020 "Progress",
2021 "Avatar",
2022 "Skeleton",
2023 "StatCard",
2024 "Checklist",
2025 "Toast",
2026 "NotificationDropdown",
2027 "Sidebar",
2028 "Header",
2029 "Image",
2030 ];
2031 for (component, expected_type) in components.iter().zip(expected_types.iter()) {
2032 let json = serde_json::to_value(component).unwrap();
2033 assert_eq!(
2034 json["type"], *expected_type,
2035 "component should serialize with type={expected_type}"
2036 );
2037 let roundtripped: Component = serde_json::from_value(json).unwrap();
2038 assert_eq!(&roundtripped, component);
2039 }
2040 }
2041
2042 #[test]
2043 fn size_enum_serialization() {
2044 let cases = [
2045 (Size::Xs, "xs"),
2046 (Size::Sm, "sm"),
2047 (Size::Default, "default"),
2048 (Size::Lg, "lg"),
2049 ];
2050 for (size, expected) in &cases {
2051 let json = serde_json::to_value(size).unwrap();
2052 assert_eq!(json, *expected);
2053 let parsed: Size = serde_json::from_value(json).unwrap();
2054 assert_eq!(&parsed, size);
2055 }
2056 }
2057
2058 #[test]
2059 fn icon_position_serialization() {
2060 let cases = [(IconPosition::Left, "left"), (IconPosition::Right, "right")];
2061 for (pos, expected) in &cases {
2062 let json = serde_json::to_value(pos).unwrap();
2063 assert_eq!(json, *expected);
2064 let parsed: IconPosition = serde_json::from_value(json).unwrap();
2065 assert_eq!(&parsed, pos);
2066 }
2067 }
2068
2069 #[test]
2070 fn sort_direction_serialization() {
2071 let cases = [(SortDirection::Asc, "asc"), (SortDirection::Desc, "desc")];
2072 for (dir, expected) in &cases {
2073 let json = serde_json::to_value(dir).unwrap();
2074 assert_eq!(json, *expected);
2075 let parsed: SortDirection = serde_json::from_value(json).unwrap();
2076 assert_eq!(&parsed, dir);
2077 }
2078 }
2079
2080 #[test]
2081 fn button_with_size_and_icon() {
2082 let button = Component::Button(ButtonProps {
2083 label: "Save".to_string(),
2084 variant: ButtonVariant::Default,
2085 size: Size::Lg,
2086 disabled: None,
2087 icon: Some("save".to_string()),
2088 icon_position: Some(IconPosition::Left),
2089 button_type: None,
2090 });
2091 let json = serde_json::to_value(&button).unwrap();
2092 assert_eq!(json["size"], "lg");
2093 assert_eq!(json["icon"], "save");
2094 assert_eq!(json["icon_position"], "left");
2095 let parsed: Component = serde_json::from_value(json).unwrap();
2096 assert_eq!(parsed, button);
2097 }
2098
2099 #[test]
2100 fn card_with_footer() {
2101 let card = Component::Card(CardProps {
2102 title: "Actions".to_string(),
2103 description: None,
2104 children: vec![],
2105 max_width: None,
2106 footer: vec![ComponentNode {
2107 key: "cancel".to_string(),
2108 component: Component::Button(ButtonProps {
2109 label: "Cancel".to_string(),
2110 variant: ButtonVariant::Outline,
2111 size: Size::Default,
2112 disabled: None,
2113 icon: None,
2114 icon_position: None,
2115 button_type: None,
2116 }),
2117 action: None,
2118 visibility: None,
2119 }],
2120 });
2121 let json = serde_json::to_value(&card).unwrap();
2122 assert!(json["footer"].is_array());
2123 assert_eq!(json["footer"][0]["label"], "Cancel");
2124 let parsed: Component = serde_json::from_value(json).unwrap();
2125 assert_eq!(parsed, card);
2126 }
2127
2128 #[test]
2129 fn input_with_error_and_description() {
2130 let input = Component::Input(InputProps {
2131 field: "email".to_string(),
2132 label: "Email".to_string(),
2133 input_type: InputType::Email,
2134 placeholder: None,
2135 required: Some(true),
2136 disabled: Some(false),
2137 error: Some("Invalid email".to_string()),
2138 description: Some("Your work email".to_string()),
2139 default_value: Some("user@example.com".to_string()),
2140 data_path: None,
2141 step: None,
2142 list: None,
2143 });
2144 let json = serde_json::to_value(&input).unwrap();
2145 assert_eq!(json["error"], "Invalid email");
2146 assert_eq!(json["description"], "Your work email");
2147 assert_eq!(json["default_value"], "user@example.com");
2148 assert_eq!(json["disabled"], false);
2149 let parsed: Component = serde_json::from_value(json).unwrap();
2150 assert_eq!(parsed, input);
2151 }
2152
2153 #[test]
2154 fn select_with_default_value() {
2155 let select = Component::Select(SelectProps {
2156 field: "role".to_string(),
2157 label: "Role".to_string(),
2158 options: vec![SelectOption {
2159 value: "admin".to_string(),
2160 label: "Admin".to_string(),
2161 }],
2162 placeholder: None,
2163 required: None,
2164 disabled: Some(true),
2165 error: Some("Required field".to_string()),
2166 description: Some("User role".to_string()),
2167 default_value: Some("admin".to_string()),
2168 data_path: None,
2169 });
2170 let json = serde_json::to_value(&select).unwrap();
2171 assert_eq!(json["default_value"], "admin");
2172 assert_eq!(json["error"], "Required field");
2173 assert_eq!(json["description"], "User role");
2174 assert_eq!(json["disabled"], true);
2175 let parsed: Component = serde_json::from_value(json).unwrap();
2176 assert_eq!(parsed, select);
2177 }
2178
2179 #[test]
2180 fn alert_with_title() {
2181 let alert = Component::Alert(AlertProps {
2182 message: "Something happened".to_string(),
2183 variant: AlertVariant::Warning,
2184 title: Some("Warning".to_string()),
2185 });
2186 let json = serde_json::to_value(&alert).unwrap();
2187 assert_eq!(json["title"], "Warning");
2188 assert_eq!(json["message"], "Something happened");
2189 let parsed: Component = serde_json::from_value(json).unwrap();
2190 assert_eq!(parsed, alert);
2191 }
2192
2193 #[test]
2194 fn modal_with_footer_and_description() {
2195 let modal = Component::Modal(ModalProps {
2196 id: "modal-delete-item".to_string(),
2197 title: "Delete Item".to_string(),
2198 description: Some("This action cannot be undone.".to_string()),
2199 children: vec![],
2200 footer: vec![ComponentNode {
2201 key: "confirm".to_string(),
2202 component: Component::Button(ButtonProps {
2203 label: "Delete".to_string(),
2204 variant: ButtonVariant::Destructive,
2205 size: Size::Default,
2206 disabled: None,
2207 icon: None,
2208 icon_position: None,
2209 button_type: None,
2210 }),
2211 action: None,
2212 visibility: None,
2213 }],
2214 trigger_label: Some("Delete".to_string()),
2215 });
2216 let json = serde_json::to_value(&modal).unwrap();
2217 assert_eq!(json["description"], "This action cannot be undone.");
2218 assert!(json["footer"].is_array());
2219 assert_eq!(json["footer"][0]["label"], "Delete");
2220 let parsed: Component = serde_json::from_value(json).unwrap();
2221 assert_eq!(parsed, modal);
2222 }
2223
2224 #[test]
2225 fn table_with_sort_props() {
2226 let table = Component::Table(TableProps {
2227 columns: vec![Column {
2228 key: "name".to_string(),
2229 label: "Name".to_string(),
2230 format: None,
2231 }],
2232 data_path: "/data/users".to_string(),
2233 row_actions: None,
2234 empty_message: None,
2235 sortable: Some(true),
2236 sort_column: Some("name".to_string()),
2237 sort_direction: Some(SortDirection::Desc),
2238 });
2239 let json = serde_json::to_value(&table).unwrap();
2240 assert_eq!(json["sortable"], true);
2241 assert_eq!(json["sort_column"], "name");
2242 assert_eq!(json["sort_direction"], "desc");
2243 let parsed: Component = serde_json::from_value(json).unwrap();
2244 assert_eq!(parsed, table);
2245 }
2246
2247 #[test]
2248 fn aligned_button_variants_serialize() {
2249 let cases = [
2250 (ButtonVariant::Default, "default"),
2251 (ButtonVariant::Secondary, "secondary"),
2252 (ButtonVariant::Destructive, "destructive"),
2253 (ButtonVariant::Outline, "outline"),
2254 (ButtonVariant::Ghost, "ghost"),
2255 (ButtonVariant::Link, "link"),
2256 ];
2257 for (variant, expected) in &cases {
2258 let json = serde_json::to_value(variant).unwrap();
2259 assert_eq!(
2260 json, *expected,
2261 "ButtonVariant::{variant:?} should serialize as {expected}"
2262 );
2263 let parsed: ButtonVariant = serde_json::from_value(json).unwrap();
2264 assert_eq!(&parsed, variant);
2265 }
2266 }
2267
2268 #[test]
2269 fn aligned_badge_variants_serialize() {
2270 let cases = [
2271 (BadgeVariant::Default, "default"),
2272 (BadgeVariant::Secondary, "secondary"),
2273 (BadgeVariant::Destructive, "destructive"),
2274 (BadgeVariant::Outline, "outline"),
2275 ];
2276 for (variant, expected) in &cases {
2277 let json = serde_json::to_value(variant).unwrap();
2278 assert_eq!(
2279 json, *expected,
2280 "BadgeVariant::{variant:?} should serialize as {expected}"
2281 );
2282 let parsed: BadgeVariant = serde_json::from_value(json).unwrap();
2283 assert_eq!(&parsed, variant);
2284 }
2285 }
2286
2287 #[test]
2288 fn checkbox_round_trips() {
2289 let checkbox = Component::Checkbox(CheckboxProps {
2290 field: "terms".to_string(),
2291 value: None,
2292 label: "Accept Terms".to_string(),
2293 description: Some("You must accept the terms".to_string()),
2294 checked: Some(true),
2295 data_path: None,
2296 required: Some(true),
2297 disabled: Some(false),
2298 error: None,
2299 });
2300 let json = serde_json::to_value(&checkbox).unwrap();
2301 assert_eq!(json["type"], "Checkbox");
2302 assert_eq!(json["field"], "terms");
2303 assert_eq!(json["checked"], true);
2304 assert_eq!(json["description"], "You must accept the terms");
2305 let parsed: Component = serde_json::from_value(json).unwrap();
2306 assert_eq!(parsed, checkbox);
2307 }
2308
2309 #[test]
2310 fn switch_round_trips() {
2311 let switch = Component::Switch(SwitchProps {
2312 field: "notifications".to_string(),
2313 label: "Enable Notifications".to_string(),
2314 description: Some("Receive email notifications".to_string()),
2315 checked: Some(false),
2316 data_path: None,
2317 required: None,
2318 disabled: Some(false),
2319 error: None,
2320 action: None,
2321 });
2322 let json = serde_json::to_value(&switch).unwrap();
2323 assert_eq!(json["type"], "Switch");
2324 assert_eq!(json["field"], "notifications");
2325 assert_eq!(json["checked"], false);
2326 let parsed: Component = serde_json::from_value(json).unwrap();
2327 assert_eq!(parsed, switch);
2328 }
2329
2330 #[test]
2331 fn separator_defaults_to_horizontal() {
2332 let json = r#"{"type": "Separator"}"#;
2333 let component: Component = serde_json::from_str(json).unwrap();
2334 match component {
2335 Component::Separator(props) => {
2336 assert_eq!(props.orientation, None);
2337 let explicit = Component::Separator(SeparatorProps {
2340 orientation: Some(Orientation::Horizontal),
2341 });
2342 let v = serde_json::to_value(&explicit).unwrap();
2343 assert_eq!(v["orientation"], "horizontal");
2344 let parsed: Component = serde_json::from_value(v).unwrap();
2345 assert_eq!(parsed, explicit);
2346 }
2347 _ => panic!("expected Separator"),
2348 }
2349 }
2350
2351 #[test]
2352 fn description_list_with_format() {
2353 let dl = Component::DescriptionList(DescriptionListProps {
2354 items: vec![
2355 DescriptionItem {
2356 label: "Created".to_string(),
2357 value: "2026-01-15".to_string(),
2358 format: Some(ColumnFormat::Date),
2359 },
2360 DescriptionItem {
2361 label: "Name".to_string(),
2362 value: "Alice".to_string(),
2363 format: None,
2364 },
2365 ],
2366 columns: Some(2),
2367 });
2368 let json = serde_json::to_value(&dl).unwrap();
2369 assert_eq!(json["type"], "DescriptionList");
2370 assert_eq!(json["columns"], 2);
2371 assert_eq!(json["items"][0]["format"], "date");
2372 assert!(json["items"][1].get("format").is_none());
2373 let parsed: Component = serde_json::from_value(json).unwrap();
2374 assert_eq!(parsed, dl);
2375 }
2376
2377 #[test]
2378 fn checkbox_with_error() {
2379 let checkbox = Component::Checkbox(CheckboxProps {
2380 field: "agree".to_string(),
2381 value: None,
2382 label: "I agree".to_string(),
2383 description: None,
2384 checked: None,
2385 data_path: None,
2386 required: Some(true),
2387 disabled: None,
2388 error: Some("You must agree".to_string()),
2389 });
2390 let json = serde_json::to_value(&checkbox).unwrap();
2391 assert_eq!(json["error"], "You must agree");
2392 assert!(json.get("description").is_none());
2393 assert!(json.get("checked").is_none());
2394 let parsed: Component = serde_json::from_value(json).unwrap();
2395 assert_eq!(parsed, checkbox);
2396 }
2397
2398 #[test]
2399 fn tabs_round_trips() {
2400 let tabs = Component::Tabs(TabsProps {
2401 default_tab: "general".to_string(),
2402 tabs: vec![
2403 Tab {
2404 value: "general".to_string(),
2405 label: "General".to_string(),
2406 children: vec![ComponentNode {
2407 key: "name-input".to_string(),
2408 component: Component::Input(InputProps {
2409 field: "name".to_string(),
2410 label: "Name".to_string(),
2411 input_type: InputType::Text,
2412 placeholder: None,
2413 required: None,
2414 disabled: None,
2415 error: None,
2416 description: None,
2417 default_value: None,
2418 data_path: None,
2419 step: None,
2420 list: None,
2421 }),
2422 action: None,
2423 visibility: None,
2424 }],
2425 },
2426 Tab {
2427 value: "security".to_string(),
2428 label: "Security".to_string(),
2429 children: vec![ComponentNode {
2430 key: "password-input".to_string(),
2431 component: Component::Input(InputProps {
2432 field: "password".to_string(),
2433 label: "Password".to_string(),
2434 input_type: InputType::Password,
2435 placeholder: None,
2436 required: None,
2437 disabled: None,
2438 error: None,
2439 description: None,
2440 default_value: None,
2441 data_path: None,
2442 step: None,
2443 list: None,
2444 }),
2445 action: None,
2446 visibility: None,
2447 }],
2448 },
2449 ],
2450 });
2451 let json = serde_json::to_string(&tabs).unwrap();
2452 let parsed: Component = serde_json::from_str(&json).unwrap();
2453 assert_eq!(parsed, tabs);
2454 }
2455
2456 #[test]
2457 fn breadcrumb_round_trips() {
2458 let breadcrumb = Component::Breadcrumb(BreadcrumbProps {
2459 items: vec![
2460 BreadcrumbItem {
2461 label: "Home".to_string(),
2462 url: Some("/".to_string()),
2463 },
2464 BreadcrumbItem {
2465 label: "Users".to_string(),
2466 url: Some("/users".to_string()),
2467 },
2468 BreadcrumbItem {
2469 label: "Edit User".to_string(),
2470 url: None,
2471 },
2472 ],
2473 });
2474 let json = serde_json::to_string(&breadcrumb).unwrap();
2475 let parsed: Component = serde_json::from_str(&json).unwrap();
2476 assert_eq!(parsed, breadcrumb);
2477
2478 let value = serde_json::to_value(&breadcrumb).unwrap();
2480 assert!(value["items"][2].get("url").is_none());
2481 }
2482
2483 #[test]
2484 fn pagination_round_trips() {
2485 let pagination = Component::Pagination(PaginationProps {
2486 current_page: 3,
2487 per_page: 25,
2488 total: 150,
2489 base_url: None,
2490 });
2491 let json = serde_json::to_string(&pagination).unwrap();
2492 let parsed: Component = serde_json::from_str(&json).unwrap();
2493 assert_eq!(parsed, pagination);
2494 }
2495
2496 #[test]
2497 fn progress_round_trips() {
2498 let progress = Component::Progress(ProgressProps {
2499 value: 75,
2500 max: Some(100),
2501 label: Some("Uploading...".to_string()),
2502 });
2503 let json = serde_json::to_string(&progress).unwrap();
2504 let parsed: Component = serde_json::from_str(&json).unwrap();
2505 assert_eq!(parsed, progress);
2506
2507 let value = serde_json::to_value(&progress).unwrap();
2508 assert_eq!(value["value"], 75);
2509 assert_eq!(value["max"], 100);
2510 assert_eq!(value["label"], "Uploading...");
2511 }
2512
2513 #[test]
2514 fn avatar_with_fallback() {
2515 let avatar = Component::Avatar(AvatarProps {
2516 src: None,
2517 alt: "John Doe".to_string(),
2518 fallback: Some("JD".to_string()),
2519 size: Some(Size::Lg),
2520 });
2521 let json = serde_json::to_string(&avatar).unwrap();
2522 let parsed: Component = serde_json::from_str(&json).unwrap();
2523 assert_eq!(parsed, avatar);
2524
2525 let value = serde_json::to_value(&avatar).unwrap();
2526 assert!(value.get("src").is_none());
2527 assert_eq!(value["fallback"], "JD");
2528 assert_eq!(value["size"], "lg");
2529 }
2530
2531 #[test]
2532 fn skeleton_round_trips() {
2533 let skeleton = Component::Skeleton(SkeletonProps {
2534 width: Some("100%".to_string()),
2535 height: Some("40px".to_string()),
2536 rounded: Some(true),
2537 });
2538 let json = serde_json::to_string(&skeleton).unwrap();
2539 let parsed: Component = serde_json::from_str(&json).unwrap();
2540 assert_eq!(parsed, skeleton);
2541
2542 let value = serde_json::to_value(&skeleton).unwrap();
2543 assert_eq!(value["width"], "100%");
2544 assert_eq!(value["height"], "40px");
2545 assert_eq!(value["rounded"], true);
2546 }
2547
2548 #[test]
2549 fn tabs_deserializes_from_json() {
2550 let json = r#"{
2551 "type": "Tabs",
2552 "default_tab": "general",
2553 "tabs": [
2554 {
2555 "value": "general",
2556 "label": "General",
2557 "children": [
2558 {
2559 "key": "name-input",
2560 "type": "Input",
2561 "field": "name",
2562 "label": "Name"
2563 }
2564 ]
2565 },
2566 {
2567 "value": "security",
2568 "label": "Security"
2569 }
2570 ]
2571 }"#;
2572 let component: Component = serde_json::from_str(json).unwrap();
2573 match component {
2574 Component::Tabs(props) => {
2575 assert_eq!(props.default_tab, "general");
2576 assert_eq!(props.tabs.len(), 2);
2577 assert_eq!(props.tabs[0].value, "general");
2578 assert_eq!(props.tabs[0].children.len(), 1);
2579 assert_eq!(props.tabs[1].value, "security");
2580 assert!(props.tabs[1].children.is_empty());
2581 }
2582 _ => panic!("expected Tabs"),
2583 }
2584 }
2585
2586 #[test]
2587 fn input_data_path_round_trips() {
2588 let input = Component::Input(InputProps {
2589 field: "name".to_string(),
2590 label: "Name".to_string(),
2591 input_type: InputType::Text,
2592 placeholder: None,
2593 required: None,
2594 disabled: None,
2595 error: None,
2596 description: None,
2597 default_value: None,
2598 data_path: Some("/data/user/name".to_string()),
2599 step: None,
2600 list: None,
2601 });
2602 let json = serde_json::to_value(&input).unwrap();
2603 assert_eq!(json["data_path"], "/data/user/name");
2604 let parsed: Component = serde_json::from_value(json).unwrap();
2605 assert_eq!(parsed, input);
2606 }
2607
2608 #[test]
2609 fn select_data_path_round_trips() {
2610 let select = Component::Select(SelectProps {
2611 field: "role".to_string(),
2612 label: "Role".to_string(),
2613 options: vec![SelectOption {
2614 value: "admin".to_string(),
2615 label: "Admin".to_string(),
2616 }],
2617 placeholder: None,
2618 required: None,
2619 disabled: None,
2620 error: None,
2621 description: None,
2622 default_value: None,
2623 data_path: Some("/data/user/role".to_string()),
2624 });
2625 let json = serde_json::to_value(&select).unwrap();
2626 assert_eq!(json["data_path"], "/data/user/role");
2627 let parsed: Component = serde_json::from_value(json).unwrap();
2628 assert_eq!(parsed, select);
2629 }
2630
2631 #[test]
2632 fn checkbox_data_path_round_trips() {
2633 let checkbox = Component::Checkbox(CheckboxProps {
2634 field: "terms".to_string(),
2635 value: None,
2636 label: "Accept Terms".to_string(),
2637 description: None,
2638 checked: None,
2639 data_path: Some("/data/user/accepted_terms".to_string()),
2640 required: None,
2641 disabled: None,
2642 error: None,
2643 });
2644 let json = serde_json::to_value(&checkbox).unwrap();
2645 assert_eq!(json["data_path"], "/data/user/accepted_terms");
2646 let parsed: Component = serde_json::from_value(json).unwrap();
2647 assert_eq!(parsed, checkbox);
2648 }
2649
2650 #[test]
2651 fn switch_data_path_round_trips() {
2652 let switch = Component::Switch(SwitchProps {
2653 field: "notifications".to_string(),
2654 label: "Enable Notifications".to_string(),
2655 description: None,
2656 checked: None,
2657 data_path: Some("/data/user/notifications_enabled".to_string()),
2658 required: None,
2659 disabled: None,
2660 error: None,
2661 action: None,
2662 });
2663 let json = serde_json::to_value(&switch).unwrap();
2664 assert_eq!(json["data_path"], "/data/user/notifications_enabled");
2665 let parsed: Component = serde_json::from_value(json).unwrap();
2666 assert_eq!(parsed, switch);
2667 }
2668
2669 #[test]
2672 fn unknown_type_deserializes_as_plugin() {
2673 let json = r#"{"type": "Map", "center": [40.7, -74.0], "zoom": 12}"#;
2674 let component: Component = serde_json::from_str(json).unwrap();
2675 match component {
2676 Component::Plugin(props) => {
2677 assert_eq!(props.plugin_type, "Map");
2678 assert_eq!(props.props["center"][0], 40.7);
2679 assert_eq!(props.props["center"][1], -74.0);
2680 assert_eq!(props.props["zoom"], 12);
2681 assert!(props.props.get("type").is_none());
2683 }
2684 _ => panic!("expected Plugin"),
2685 }
2686 }
2687
2688 #[test]
2689 fn plugin_round_trips() {
2690 let plugin = Component::Plugin(PluginProps {
2691 plugin_type: "Chart".to_string(),
2692 props: serde_json::json!({"data": [1, 2, 3], "style": "bar"}),
2693 });
2694 let json = serde_json::to_value(&plugin).unwrap();
2695 assert_eq!(json["type"], "Chart");
2696 assert_eq!(json["data"], serde_json::json!([1, 2, 3]));
2697 assert_eq!(json["style"], "bar");
2698
2699 let parsed: Component = serde_json::from_value(json).unwrap();
2700 assert_eq!(parsed, plugin);
2701 }
2702
2703 #[test]
2704 fn plugin_serializes_with_type_field() {
2705 let plugin = Component::Plugin(PluginProps {
2706 plugin_type: "Map".to_string(),
2707 props: serde_json::json!({"lat": 51.5, "lng": -0.1}),
2708 });
2709 let json = serde_json::to_value(&plugin).unwrap();
2710 assert_eq!(json["type"], "Map");
2711 assert_eq!(json["lat"], 51.5);
2712 assert_eq!(json["lng"], -0.1);
2713 }
2714
2715 #[test]
2716 fn plugin_with_empty_props() {
2717 let json = r#"{"type": "CustomWidget"}"#;
2718 let component: Component = serde_json::from_str(json).unwrap();
2719 match component {
2720 Component::Plugin(props) => {
2721 assert_eq!(props.plugin_type, "CustomWidget");
2722 assert!(props.props.as_object().unwrap().is_empty());
2723 }
2724 _ => panic!("expected Plugin"),
2725 }
2726 }
2727
2728 #[test]
2729 fn plugin_in_component_node() {
2730 let node = ComponentNode {
2731 key: "map-1".to_string(),
2732 component: Component::Plugin(PluginProps {
2733 plugin_type: "Map".to_string(),
2734 props: serde_json::json!({"center": [0.0, 0.0]}),
2735 }),
2736 action: None,
2737 visibility: None,
2738 };
2739 let json = serde_json::to_string(&node).unwrap();
2740 let parsed: ComponentNode = serde_json::from_str(&json).unwrap();
2741 assert_eq!(parsed, node);
2742
2743 let value = serde_json::to_value(&node).unwrap();
2744 assert_eq!(value["type"], "Map");
2745 assert_eq!(value["key"], "map-1");
2746 }
2747
2748 #[test]
2749 fn known_types_not_treated_as_plugin() {
2750 let known_types = [
2752 "Card",
2753 "Table",
2754 "Form",
2755 "Button",
2756 "Input",
2757 "Select",
2758 "Alert",
2759 "Badge",
2760 "Modal",
2761 "Text",
2762 "Checkbox",
2763 "Switch",
2764 "Separator",
2765 "DescriptionList",
2766 "Tabs",
2767 "Breadcrumb",
2768 "Pagination",
2769 "Progress",
2770 "Avatar",
2771 "Skeleton",
2772 ];
2773 for type_name in &known_types {
2774 let json_str = match *type_name {
2777 "Card" => r#"{"type":"Card","title":"t"}"#,
2778 "Table" => r#"{"type":"Table","columns":[],"data_path":"/d"}"#,
2779 "Form" => r#"{"type":"Form","action":{"handler":"h","method":"POST"},"fields":[]}"#,
2780 "Button" => r#"{"type":"Button","label":"b"}"#,
2781 "Input" => r#"{"type":"Input","field":"f","label":"l"}"#,
2782 "Select" => r#"{"type":"Select","field":"f","label":"l","options":[]}"#,
2783 "Alert" => r#"{"type":"Alert","message":"m"}"#,
2784 "Badge" => r#"{"type":"Badge","label":"b"}"#,
2785 "Modal" => r#"{"type":"Modal","id":"modal-t","title":"t"}"#,
2786 "Text" => r#"{"type":"Text","content":"c"}"#,
2787 "Checkbox" => r#"{"type":"Checkbox","field":"f","label":"l"}"#,
2788 "Switch" => r#"{"type":"Switch","field":"f","label":"l"}"#,
2789 "Separator" => r#"{"type":"Separator"}"#,
2790 "DescriptionList" => r#"{"type":"DescriptionList","items":[]}"#,
2791 "Tabs" => r#"{"type":"Tabs","default_tab":"t","tabs":[]}"#,
2792 "Breadcrumb" => r#"{"type":"Breadcrumb","items":[]}"#,
2793 "Pagination" => r#"{"type":"Pagination","current_page":1,"per_page":10,"total":0}"#,
2794 "Progress" => r#"{"type":"Progress","value":0}"#,
2795 "Avatar" => r#"{"type":"Avatar","alt":"a"}"#,
2796 "Skeleton" => r#"{"type":"Skeleton"}"#,
2797 _ => unreachable!(),
2798 };
2799 let component: Component = serde_json::from_str(json_str).unwrap();
2800 assert!(
2801 !matches!(component, Component::Plugin(_)),
2802 "type {type_name} should not deserialize as Plugin"
2803 );
2804 }
2805 }
2806
2807 #[test]
2810 fn test_stat_card_serde_round_trip() {
2811 let component = Component::StatCard(StatCardProps {
2812 label: "Orders".into(),
2813 value: "42".into(),
2814 icon: Some("package".into()),
2815 subtitle: Some("today".into()),
2816 sse_target: Some("orders_today".into()),
2817 });
2818 let json = serde_json::to_string(&component).unwrap();
2819 assert!(json.contains("\"type\":\"StatCard\""));
2820 assert!(json.contains("\"sse_target\":\"orders_today\""));
2821 let deserialized: Component = serde_json::from_str(&json).unwrap();
2822 assert_eq!(component, deserialized);
2823 }
2824
2825 #[test]
2826 fn test_checklist_serde_round_trip() {
2827 let component = Component::Checklist(ChecklistProps {
2828 title: "Getting Started".into(),
2829 items: vec![
2830 ChecklistItem {
2831 label: "Install dependencies".into(),
2832 checked: true,
2833 href: None,
2834 },
2835 ChecklistItem {
2836 label: "Read the docs".into(),
2837 checked: false,
2838 href: Some("/docs".into()),
2839 },
2840 ],
2841 dismissible: true,
2842 dismiss_label: Some("Dismiss".into()),
2843 data_key: Some("onboarding".into()),
2844 });
2845 let json = serde_json::to_string(&component).unwrap();
2846 assert!(json.contains("\"type\":\"Checklist\""));
2847 assert!(json.contains("\"data_key\":\"onboarding\""));
2848 let deserialized: Component = serde_json::from_str(&json).unwrap();
2849 assert_eq!(component, deserialized);
2850 }
2851
2852 #[test]
2853 fn test_toast_serde_round_trip() {
2854 let component = Component::Toast(ToastProps {
2855 message: "Operation completed".into(),
2856 variant: ToastVariant::Success,
2857 timeout: Some(10),
2858 dismissible: true,
2859 });
2860 let json = serde_json::to_string(&component).unwrap();
2861 assert!(json.contains("\"type\":\"Toast\""));
2862 assert!(json.contains("\"timeout\":10"));
2863 let deserialized: Component = serde_json::from_str(&json).unwrap();
2864 assert_eq!(component, deserialized);
2865 }
2866
2867 #[test]
2868 fn test_notification_dropdown_serde_round_trip() {
2869 let component = Component::NotificationDropdown(NotificationDropdownProps {
2870 notifications: vec![
2871 NotificationItem {
2872 icon: Some("bell".into()),
2873 text: "New message".into(),
2874 timestamp: Some("2m ago".into()),
2875 read: false,
2876 action_url: Some("/messages/1".into()),
2877 },
2878 NotificationItem {
2879 icon: None,
2880 text: "Old notification".into(),
2881 timestamp: None,
2882 read: true,
2883 action_url: None,
2884 },
2885 ],
2886 empty_text: Some("No notifications".into()),
2887 });
2888 let json = serde_json::to_string(&component).unwrap();
2889 assert!(json.contains("\"type\":\"NotificationDropdown\""));
2890 assert!(json.contains("\"empty_text\":\"No notifications\""));
2891 let deserialized: Component = serde_json::from_str(&json).unwrap();
2892 assert_eq!(component, deserialized);
2893 }
2894
2895 #[test]
2896 fn test_sidebar_serde_round_trip() {
2897 let component = Component::Sidebar(SidebarProps {
2898 fixed_top: vec![SidebarNavItem {
2899 label: "Dashboard".into(),
2900 href: "/dashboard".into(),
2901 icon: Some("home".into()),
2902 active: true,
2903 }],
2904 groups: vec![SidebarGroup {
2905 label: "Management".into(),
2906 collapsed: false,
2907 items: vec![SidebarNavItem {
2908 label: "Users".into(),
2909 href: "/users".into(),
2910 icon: None,
2911 active: false,
2912 }],
2913 }],
2914 fixed_bottom: vec![SidebarNavItem {
2915 label: "Settings".into(),
2916 href: "/settings".into(),
2917 icon: Some("gear".into()),
2918 active: false,
2919 }],
2920 });
2921 let json = serde_json::to_string(&component).unwrap();
2922 assert!(json.contains("\"type\":\"Sidebar\""));
2923 assert!(json.contains("\"fixed_top\""));
2924 let deserialized: Component = serde_json::from_str(&json).unwrap();
2925 assert_eq!(component, deserialized);
2926 }
2927
2928 #[test]
2929 fn test_header_serde_round_trip() {
2930 let component = Component::Header(HeaderProps {
2931 business_name: "Acme Corp".into(),
2932 notification_count: Some(5),
2933 user_name: Some("Jane Doe".into()),
2934 user_avatar: Some("/avatar.jpg".into()),
2935 logout_url: Some("/logout".into()),
2936 });
2937 let json = serde_json::to_string(&component).unwrap();
2938 assert!(json.contains("\"type\":\"Header\""));
2939 assert!(json.contains("\"business_name\":\"Acme Corp\""));
2940 assert!(json.contains("\"notification_count\":5"));
2941 let deserialized: Component = serde_json::from_str(&json).unwrap();
2942 assert_eq!(component, deserialized);
2943 }
2944
2945 #[test]
2948 fn test_stat_card_constructor() {
2949 let props = StatCardProps {
2950 label: "Revenue".into(),
2951 value: "$1,000".into(),
2952 icon: None,
2953 subtitle: None,
2954 sse_target: None,
2955 };
2956 let node = ComponentNode::stat_card("revenue-card", props.clone());
2957 assert_eq!(node.key, "revenue-card");
2958 assert!(node.action.is_none());
2959 assert!(node.visibility.is_none());
2960 assert_eq!(node.component, Component::StatCard(props));
2961 }
2962
2963 #[test]
2964 fn test_checklist_constructor() {
2965 let props = ChecklistProps {
2966 title: "Tasks".into(),
2967 items: vec![],
2968 dismissible: true,
2969 dismiss_label: None,
2970 data_key: None,
2971 };
2972 let node = ComponentNode::checklist("task-list", props.clone());
2973 assert_eq!(node.key, "task-list");
2974 assert!(node.action.is_none());
2975 assert!(node.visibility.is_none());
2976 assert_eq!(node.component, Component::Checklist(props));
2977 }
2978
2979 #[test]
2980 fn test_toast_constructor() {
2981 let props = ToastProps {
2982 message: "Done!".into(),
2983 variant: ToastVariant::Success,
2984 timeout: None,
2985 dismissible: true,
2986 };
2987 let node = ComponentNode::toast("success-toast", props.clone());
2988 assert_eq!(node.key, "success-toast");
2989 assert!(node.action.is_none());
2990 assert!(node.visibility.is_none());
2991 assert_eq!(node.component, Component::Toast(props));
2992 }
2993
2994 #[test]
2995 fn test_notification_dropdown_constructor() {
2996 let props = NotificationDropdownProps {
2997 notifications: vec![],
2998 empty_text: Some("All caught up!".into()),
2999 };
3000 let node = ComponentNode::notification_dropdown("notifs", props.clone());
3001 assert_eq!(node.key, "notifs");
3002 assert!(node.action.is_none());
3003 assert!(node.visibility.is_none());
3004 assert_eq!(node.component, Component::NotificationDropdown(props));
3005 }
3006
3007 #[test]
3008 fn test_sidebar_constructor() {
3009 let props = SidebarProps {
3010 fixed_top: vec![],
3011 groups: vec![],
3012 fixed_bottom: vec![],
3013 };
3014 let node = ComponentNode::sidebar("main-nav", props.clone());
3015 assert_eq!(node.key, "main-nav");
3016 assert!(node.action.is_none());
3017 assert!(node.visibility.is_none());
3018 assert_eq!(node.component, Component::Sidebar(props));
3019 }
3020
3021 #[test]
3022 fn test_header_constructor() {
3023 let props = HeaderProps {
3024 business_name: "MyApp".into(),
3025 notification_count: None,
3026 user_name: None,
3027 user_avatar: None,
3028 logout_url: None,
3029 };
3030 let node = ComponentNode::header("page-header", props.clone());
3031 assert_eq!(node.key, "page-header");
3032 assert!(node.action.is_none());
3033 assert!(node.visibility.is_none());
3034 assert_eq!(node.component, Component::Header(props));
3035 }
3036
3037 #[test]
3040 fn test_checklist_item_round_trip() {
3041 let checked_item = ChecklistItem {
3042 label: "Completed task".into(),
3043 checked: true,
3044 href: Some("/task/1".into()),
3045 };
3046 let json = serde_json::to_string(&checked_item).unwrap();
3047 let parsed: ChecklistItem = serde_json::from_str(&json).unwrap();
3048 assert_eq!(parsed, checked_item);
3049
3050 let unchecked_item = ChecklistItem {
3051 label: "Pending task".into(),
3052 checked: false,
3053 href: None,
3054 };
3055 let json = serde_json::to_string(&unchecked_item).unwrap();
3056 let parsed: ChecklistItem = serde_json::from_str(&json).unwrap();
3057 assert_eq!(parsed, unchecked_item);
3058 assert!(!json.contains("href"));
3060 }
3061
3062 #[test]
3063 fn test_sidebar_group_round_trip() {
3064 let expanded = SidebarGroup {
3065 label: "Main".into(),
3066 collapsed: false,
3067 items: vec![
3068 SidebarNavItem {
3069 label: "Home".into(),
3070 href: "/".into(),
3071 icon: Some("home".into()),
3072 active: true,
3073 },
3074 SidebarNavItem {
3075 label: "About".into(),
3076 href: "/about".into(),
3077 icon: None,
3078 active: false,
3079 },
3080 ],
3081 };
3082 let json = serde_json::to_string(&expanded).unwrap();
3083 let parsed: SidebarGroup = serde_json::from_str(&json).unwrap();
3084 assert_eq!(parsed, expanded);
3085 assert_eq!(parsed.items.len(), 2);
3086
3087 let collapsed = SidebarGroup {
3088 label: "Advanced".into(),
3089 collapsed: true,
3090 items: vec![],
3091 };
3092 let json = serde_json::to_string(&collapsed).unwrap();
3093 let parsed: SidebarGroup = serde_json::from_str(&json).unwrap();
3094 assert_eq!(parsed, collapsed);
3095 assert!(parsed.collapsed);
3096 }
3097
3098 #[test]
3099 fn test_notification_item_round_trip() {
3100 let unread = NotificationItem {
3101 icon: Some("mail".into()),
3102 text: "You have a new message".into(),
3103 timestamp: Some("5m ago".into()),
3104 read: false,
3105 action_url: Some("/messages/42".into()),
3106 };
3107 let json = serde_json::to_string(&unread).unwrap();
3108 let parsed: NotificationItem = serde_json::from_str(&json).unwrap();
3109 assert_eq!(parsed, unread);
3110 assert!(!parsed.read);
3111
3112 let read_notif = NotificationItem {
3113 icon: None,
3114 text: "Welcome to the platform".into(),
3115 timestamp: None,
3116 read: true,
3117 action_url: None,
3118 };
3119 let json = serde_json::to_string(&read_notif).unwrap();
3120 let parsed: NotificationItem = serde_json::from_str(&json).unwrap();
3121 assert_eq!(parsed, read_notif);
3122 assert!(parsed.read);
3123 assert!(!json.contains("\"icon\""));
3125 assert!(!json.contains("\"action_url\""));
3126 }
3127
3128 #[test]
3131 fn test_stat_card_all_optionals_none() {
3132 let component = Component::StatCard(StatCardProps {
3133 label: "Count".into(),
3134 value: "0".into(),
3135 icon: None,
3136 subtitle: None,
3137 sse_target: None,
3138 });
3139 let json = serde_json::to_string(&component).unwrap();
3140 assert!(json.contains("\"type\":\"StatCard\""));
3141 assert!(!json.contains("\"icon\""));
3142 assert!(!json.contains("\"subtitle\""));
3143 assert!(!json.contains("\"sse_target\""));
3144 let deserialized: Component = serde_json::from_str(&json).unwrap();
3145 assert_eq!(component, deserialized);
3146 }
3147
3148 #[test]
3149 fn test_checklist_empty_items() {
3150 let component = Component::Checklist(ChecklistProps {
3151 title: "Empty List".into(),
3152 items: vec![],
3153 dismissible: true,
3154 dismiss_label: None,
3155 data_key: None,
3156 });
3157 let json = serde_json::to_string(&component).unwrap();
3158 assert!(json.contains("\"type\":\"Checklist\""));
3159 let deserialized: Component = serde_json::from_str(&json).unwrap();
3160 assert_eq!(component, deserialized);
3161 match &deserialized {
3162 Component::Checklist(props) => assert!(props.items.is_empty()),
3163 _ => panic!("expected Checklist"),
3164 }
3165 }
3166
3167 #[test]
3168 fn test_sidebar_empty_groups_and_fixed() {
3169 let component = Component::Sidebar(SidebarProps {
3170 fixed_top: vec![],
3171 groups: vec![],
3172 fixed_bottom: vec![],
3173 });
3174 let json = serde_json::to_string(&component).unwrap();
3175 assert!(json.contains("\"type\":\"Sidebar\""));
3176 assert!(!json.contains("\"fixed_top\""));
3178 assert!(!json.contains("\"groups\""));
3179 assert!(!json.contains("\"fixed_bottom\""));
3180 let deserialized: Component = serde_json::from_str(&json).unwrap();
3181 assert_eq!(component, deserialized);
3182 }
3183
3184 #[test]
3185 fn test_notification_dropdown_empty_uses_empty_text() {
3186 let component = Component::NotificationDropdown(NotificationDropdownProps {
3187 notifications: vec![],
3188 empty_text: Some("Nothing here!".into()),
3189 });
3190 let json = serde_json::to_string(&component).unwrap();
3191 assert!(json.contains("\"type\":\"NotificationDropdown\""));
3192 assert!(json.contains("\"empty_text\":\"Nothing here!\""));
3193 let deserialized: Component = serde_json::from_str(&json).unwrap();
3194 assert_eq!(component, deserialized);
3195 }
3196
3197 #[test]
3200 fn test_stat_card_omits_sse_target_when_none() {
3201 let component = Component::StatCard(StatCardProps {
3202 label: "Revenue".into(),
3203 value: "$500".into(),
3204 icon: None,
3205 subtitle: None,
3206 sse_target: None,
3207 });
3208 let json = serde_json::to_string(&component).unwrap();
3209 assert!(
3210 !json.contains("sse_target"),
3211 "sse_target must be omitted when None"
3212 );
3213 }
3214
3215 #[test]
3218 fn grid_round_trips() {
3219 let grid = Component::Grid(GridProps {
3220 columns: 3,
3221 md_columns: None,
3222 lg_columns: None,
3223 gap: GapSize::Lg,
3224 scrollable: None,
3225 children: vec![ComponentNode::text(
3226 "t",
3227 TextProps {
3228 content: "cell".into(),
3229 element: TextElement::P,
3230 },
3231 )],
3232 });
3233 let json = serde_json::to_value(&grid).unwrap();
3234 assert_eq!(json["type"], "Grid");
3235 assert_eq!(json["columns"], 3);
3236 assert_eq!(json["gap"], "lg");
3237 let parsed: Component = serde_json::from_value(json).unwrap();
3238 assert_eq!(parsed, grid);
3239 }
3240
3241 #[test]
3242 fn grid_defaults() {
3243 let json = serde_json::json!({"type": "Grid"});
3244 let parsed: Component = serde_json::from_value(json).unwrap();
3245 match parsed {
3246 Component::Grid(props) => {
3247 assert_eq!(props.columns, 2);
3248 assert_eq!(props.gap, GapSize::Md);
3249 assert!(props.children.is_empty());
3250 }
3251 _ => panic!("expected Grid"),
3252 }
3253 }
3254
3255 #[test]
3258 fn collapsible_round_trips() {
3259 let c = Component::Collapsible(CollapsibleProps {
3260 title: "Details".into(),
3261 expanded: true,
3262 children: vec![],
3263 });
3264 let json = serde_json::to_value(&c).unwrap();
3265 assert_eq!(json["type"], "Collapsible");
3266 assert_eq!(json["title"], "Details");
3267 assert_eq!(json["expanded"], true);
3268 let parsed: Component = serde_json::from_value(json).unwrap();
3269 assert_eq!(parsed, c);
3270 }
3271
3272 #[test]
3275 fn empty_state_round_trips() {
3276 let es = Component::EmptyState(EmptyStateProps {
3277 title: "No items".into(),
3278 description: Some("Create one".into()),
3279 action: Some(Action::get("items.create")),
3280 action_label: Some("New item".into()),
3281 });
3282 let json = serde_json::to_value(&es).unwrap();
3283 assert_eq!(json["type"], "EmptyState");
3284 assert_eq!(json["title"], "No items");
3285 let parsed: Component = serde_json::from_value(json).unwrap();
3286 assert_eq!(parsed, es);
3287 }
3288
3289 #[test]
3290 fn empty_state_minimal() {
3291 let json = serde_json::json!({"type": "EmptyState", "title": "Nothing"});
3292 let parsed: Component = serde_json::from_value(json).unwrap();
3293 match parsed {
3294 Component::EmptyState(props) => {
3295 assert_eq!(props.title, "Nothing");
3296 assert!(props.description.is_none());
3297 assert!(props.action.is_none());
3298 assert!(props.action_label.is_none());
3299 }
3300 _ => panic!("expected EmptyState"),
3301 }
3302 }
3303
3304 #[test]
3307 fn form_section_round_trips() {
3308 let fs = Component::FormSection(FormSectionProps {
3309 title: "Contact".into(),
3310 description: Some("Your details".into()),
3311 children: vec![],
3312 layout: None,
3313 });
3314 let json = serde_json::to_value(&fs).unwrap();
3315 assert_eq!(json["type"], "FormSection");
3316 assert_eq!(json["title"], "Contact");
3317 let parsed: Component = serde_json::from_value(json).unwrap();
3318 assert_eq!(parsed, fs);
3319 }
3320
3321 #[test]
3324 fn switch_with_action_round_trips() {
3325 let sw = Component::Switch(SwitchProps {
3326 field: "active".into(),
3327 label: "Active".into(),
3328 description: None,
3329 checked: Some(true),
3330 data_path: None,
3331 required: None,
3332 disabled: None,
3333 error: None,
3334 action: Some(Action::new("settings.toggle")),
3335 });
3336 let json = serde_json::to_value(&sw).unwrap();
3337 assert!(json["action"].is_object());
3338 assert_eq!(json["action"]["handler"], "settings.toggle");
3339 let parsed: Component = serde_json::from_value(json).unwrap();
3340 assert_eq!(parsed, sw);
3341 }
3342
3343 #[test]
3344 fn switch_without_action_omits_field() {
3345 let sw = Component::Switch(SwitchProps {
3346 field: "f".into(),
3347 label: "l".into(),
3348 description: None,
3349 checked: None,
3350 data_path: None,
3351 required: None,
3352 disabled: None,
3353 error: None,
3354 action: None,
3355 });
3356 let json = serde_json::to_string(&sw).unwrap();
3357 assert!(!json.contains("\"action\""));
3358 }
3359
3360 #[test]
3361 fn test_toast_omits_timeout_when_none() {
3362 let component = Component::Toast(ToastProps {
3363 message: "Hello".into(),
3364 variant: ToastVariant::Info,
3365 timeout: None,
3366 dismissible: false,
3367 });
3368 let json = serde_json::to_string(&component).unwrap();
3369 assert!(
3370 !json.contains("\"timeout\""),
3371 "timeout must be omitted when None"
3372 );
3373 }
3374
3375 #[test]
3376 fn page_header_round_trip_title_only() {
3377 let component = Component::PageHeader(PageHeaderProps {
3378 title: "Test Title".to_string(),
3379 breadcrumb: vec![],
3380 actions: vec![],
3381 });
3382 let json = serde_json::to_value(&component).unwrap();
3383 assert_eq!(json["type"], "PageHeader");
3384 assert_eq!(json["title"], "Test Title");
3385 assert!(json.get("breadcrumb").is_none());
3387 assert!(json.get("actions").is_none());
3388 let parsed: Component = serde_json::from_value(json).unwrap();
3389 assert_eq!(parsed, component);
3390 }
3391
3392 #[test]
3393 fn page_header_round_trip_with_breadcrumb_and_actions() {
3394 let component = Component::PageHeader(PageHeaderProps {
3395 title: "Users".to_string(),
3396 breadcrumb: vec![
3397 BreadcrumbItem {
3398 label: "Home".to_string(),
3399 url: Some("/".to_string()),
3400 },
3401 BreadcrumbItem {
3402 label: "Users".to_string(),
3403 url: None,
3404 },
3405 ],
3406 actions: vec![ComponentNode {
3407 key: "add-btn".to_string(),
3408 component: Component::Button(ButtonProps {
3409 label: "Add User".to_string(),
3410 variant: ButtonVariant::Default,
3411 size: Size::Default,
3412 disabled: None,
3413 icon: None,
3414 icon_position: None,
3415 button_type: None,
3416 }),
3417 action: None,
3418 visibility: None,
3419 }],
3420 });
3421 let json = serde_json::to_string(&component).unwrap();
3422 let parsed: Component = serde_json::from_str(&json).unwrap();
3423 assert_eq!(parsed, component);
3424 let value = serde_json::to_value(&component).unwrap();
3426 assert_eq!(value["type"], "PageHeader");
3427 assert_eq!(value["title"], "Users");
3428 assert!(value["breadcrumb"].is_array());
3429 assert!(value["actions"].is_array());
3430 }
3431
3432 #[test]
3433 fn page_header_deserialize_from_json() {
3434 let json = r#"{"type":"PageHeader","title":"Test"}"#;
3435 let component: Component = serde_json::from_str(json).unwrap();
3436 match component {
3437 Component::PageHeader(props) => {
3438 assert_eq!(props.title, "Test");
3439 assert!(props.breadcrumb.is_empty());
3440 assert!(props.actions.is_empty());
3441 }
3442 _ => panic!("expected PageHeader"),
3443 }
3444 }
3445
3446 #[test]
3447 fn button_group_round_trip_empty() {
3448 let component = Component::ButtonGroup(ButtonGroupProps { buttons: vec![] });
3449 let json = serde_json::to_value(&component).unwrap();
3450 assert_eq!(json["type"], "ButtonGroup");
3451 assert!(json.get("buttons").is_none());
3453 let parsed: Component = serde_json::from_value(json).unwrap();
3454 assert_eq!(parsed, component);
3455 }
3456
3457 #[test]
3458 fn button_group_round_trip_with_buttons() {
3459 let component = Component::ButtonGroup(ButtonGroupProps {
3460 buttons: vec![
3461 ComponentNode {
3462 key: "save".to_string(),
3463 component: Component::Button(ButtonProps {
3464 label: "Save".to_string(),
3465 variant: ButtonVariant::Default,
3466 size: Size::Default,
3467 disabled: None,
3468 icon: None,
3469 icon_position: None,
3470 button_type: None,
3471 }),
3472 action: None,
3473 visibility: None,
3474 },
3475 ComponentNode {
3476 key: "cancel".to_string(),
3477 component: Component::Button(ButtonProps {
3478 label: "Cancel".to_string(),
3479 variant: ButtonVariant::Outline,
3480 size: Size::Default,
3481 disabled: None,
3482 icon: None,
3483 icon_position: None,
3484 button_type: None,
3485 }),
3486 action: None,
3487 visibility: None,
3488 },
3489 ],
3490 });
3491 let json = serde_json::to_string(&component).unwrap();
3492 let parsed: Component = serde_json::from_str(&json).unwrap();
3493 assert_eq!(parsed, component);
3494 let value = serde_json::to_value(&component).unwrap();
3495 assert_eq!(value["type"], "ButtonGroup");
3496 assert!(value["buttons"].is_array());
3497 assert_eq!(value["buttons"].as_array().unwrap().len(), 2);
3498 }
3499
3500 #[test]
3501 fn button_group_deserialize_from_json() {
3502 let json = r#"{"type":"ButtonGroup","buttons":[]}"#;
3503 let component: Component = serde_json::from_str(json).unwrap();
3504 match component {
3505 Component::ButtonGroup(props) => {
3506 assert!(props.buttons.is_empty());
3507 }
3508 _ => panic!("expected ButtonGroup"),
3509 }
3510 }
3511
3512 #[test]
3513 fn image_round_trips() {
3514 let json = r#"{"type": "Image", "src": "/img/s.png", "alt": "Screenshot"}"#;
3515 let component: Component = serde_json::from_str(json).unwrap();
3516 match component {
3517 Component::Image(props) => {
3518 assert_eq!(props.src, "/img/s.png");
3519 assert_eq!(props.alt, "Screenshot");
3520 assert!(props.aspect_ratio.is_none());
3521 }
3522 _ => panic!("expected Image"),
3523 }
3524 }
3525
3526 #[test]
3527 fn all_known_types_round_trip() {
3528 let known_types: &[(&str, &str)] = &[
3529 ("Alert", r#"{"type":"Alert","message":"m"}"#),
3530 ("Avatar", r#"{"type":"Avatar","alt":"a"}"#),
3531 ("Badge", r#"{"type":"Badge","label":"b"}"#),
3532 ("Breadcrumb", r#"{"type":"Breadcrumb","items":[]}"#),
3533 ("Button", r#"{"type":"Button","label":"b"}"#),
3534 ("CalendarCell", r#"{"type":"CalendarCell","day":1}"#),
3535 ("Checkbox", r#"{"type":"Checkbox","field":"f","label":"l"}"#),
3536 ("Image", r#"{"type":"Image","src":"/img/s.png","alt":"a"}"#),
3537 ("Input", r#"{"type":"Input","field":"f","label":"l"}"#),
3538 (
3539 "Pagination",
3540 r#"{"type":"Pagination","current_page":1,"per_page":10,"total":0}"#,
3541 ),
3542 ("Progress", r#"{"type":"Progress","value":50}"#),
3543 (
3544 "Select",
3545 r#"{"type":"Select","field":"f","label":"l","options":[]}"#,
3546 ),
3547 ("Separator", r#"{"type":"Separator"}"#),
3548 ("Skeleton", r#"{"type":"Skeleton"}"#),
3549 ("Text", r#"{"type":"Text","content":"c"}"#),
3550 ];
3551 for (type_name, json_str) in known_types {
3552 let component: Component = serde_json::from_str(json_str)
3553 .unwrap_or_else(|e| panic!("failed to parse {type_name}: {e}"));
3554 let serialized = serde_json::to_value(&component).unwrap();
3555 assert_eq!(
3556 serialized["type"], *type_name,
3557 "type mismatch for {type_name}"
3558 );
3559 let reparsed: Component = serde_json::from_value(serialized)
3560 .unwrap_or_else(|e| panic!("failed to reparse {type_name}: {e}"));
3561 assert_eq!(
3562 serde_json::to_value(&reparsed).unwrap()["type"],
3563 *type_name,
3564 "round-trip type mismatch for {type_name}"
3565 );
3566 }
3567 }
3568}