adui_dioxus/components/
result.rs

1use crate::components::icon::{Icon, IconKind};
2use dioxus::prelude::*;
3
4/// Status of a Result view.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum ResultStatus {
7    Success,
8    Info,
9    Warning,
10    Error,
11    NotFound,
12    Forbidden,
13    ServerError,
14}
15
16impl ResultStatus {
17    fn as_class(&self) -> &'static str {
18        match self {
19            ResultStatus::Success => "adui-result-success",
20            ResultStatus::Info => "adui-result-info",
21            ResultStatus::Warning => "adui-result-warning",
22            ResultStatus::Error => "adui-result-error",
23            ResultStatus::NotFound => "adui-result-404",
24            ResultStatus::Forbidden => "adui-result-403",
25            ResultStatus::ServerError => "adui-result-500",
26        }
27    }
28
29    fn icon_kind(&self) -> IconKind {
30        match self {
31            ResultStatus::Success => IconKind::Check,
32            ResultStatus::Error | ResultStatus::ServerError => IconKind::Close,
33            ResultStatus::Warning | ResultStatus::Forbidden | ResultStatus::NotFound => {
34                IconKind::Info
35            }
36            ResultStatus::Info => IconKind::Info,
37        }
38    }
39}
40
41/// Props for the Result component (MVP subset).
42#[derive(Props, Clone, PartialEq)]
43pub struct ResultProps {
44    /// Overall status of the result.
45    #[props(optional)]
46    pub status: Option<ResultStatus>,
47    /// Optional custom icon.
48    #[props(optional)]
49    pub icon: Option<Element>,
50    /// Title of the result page.
51    #[props(optional)]
52    pub title: Option<Element>,
53    /// Optional subtitle/description text.
54    #[props(optional)]
55    pub sub_title: Option<Element>,
56    /// Extra action area, typically buttons.
57    #[props(optional)]
58    pub extra: Option<Element>,
59    /// Extra class on the root element.
60    #[props(optional)]
61    pub class: Option<String>,
62    /// Inline style on the root element.
63    #[props(optional)]
64    pub style: Option<String>,
65    /// Optional content section rendered below extra.
66    pub children: Option<Element>,
67}
68
69/// Ant Design flavored Result (MVP: status + icon + title/subtitle/extra/content).
70#[component]
71pub fn Result(props: ResultProps) -> Element {
72    let ResultProps {
73        status,
74        icon,
75        title,
76        sub_title,
77        extra,
78        class,
79        style,
80        children,
81    } = props;
82
83    let status_value = status.unwrap_or(ResultStatus::Info);
84
85    let mut class_list = vec![
86        "adui-result".to_string(),
87        status_value.as_class().to_string(),
88    ];
89    if let Some(extra_class) = class {
90        class_list.push(extra_class);
91    }
92    let class_attr = class_list.join(" ");
93    let style_attr = style.unwrap_or_default();
94
95    let icon_node = icon.map(Some).unwrap_or_else(|| {
96        Some(rsx!(Icon {
97            kind: status_value.icon_kind(),
98            size: 40.0,
99        }))
100    });
101
102    rsx! {
103        div { class: "{class_attr}", style: "{style_attr}",
104            if let Some(node) = icon_node {
105                div { class: "adui-result-icon", {node} }
106            }
107            if let Some(t) = title {
108                div { class: "adui-result-title", {t} }
109            }
110            if let Some(st) = sub_title {
111                div { class: "adui-result-subtitle", {st} }
112            }
113            if let Some(extra_node) = extra {
114                div { class: "adui-result-extra", {extra_node} }
115            }
116            if let Some(content) = children {
117                div { class: "adui-result-content", {content} }
118            }
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn result_status_class_mapping_is_stable() {
129        assert_eq!(ResultStatus::Success.as_class(), "adui-result-success");
130        assert_eq!(ResultStatus::Info.as_class(), "adui-result-info");
131        assert_eq!(ResultStatus::Warning.as_class(), "adui-result-warning");
132        assert_eq!(ResultStatus::Error.as_class(), "adui-result-error");
133        assert_eq!(ResultStatus::NotFound.as_class(), "adui-result-404");
134        assert_eq!(ResultStatus::Forbidden.as_class(), "adui-result-403");
135        assert_eq!(ResultStatus::ServerError.as_class(), "adui-result-500");
136    }
137
138    #[test]
139    fn result_status_icon_mapping() {
140        assert_eq!(ResultStatus::Success.icon_kind(), IconKind::Check);
141        assert_eq!(ResultStatus::Info.icon_kind(), IconKind::Info);
142        assert_eq!(ResultStatus::Warning.icon_kind(), IconKind::Info);
143        assert_eq!(ResultStatus::Error.icon_kind(), IconKind::Close);
144        assert_eq!(ResultStatus::NotFound.icon_kind(), IconKind::Info);
145        assert_eq!(ResultStatus::Forbidden.icon_kind(), IconKind::Info);
146        assert_eq!(ResultStatus::ServerError.icon_kind(), IconKind::Close);
147    }
148
149    #[test]
150    fn result_status_all_variants() {
151        let variants = [
152            ResultStatus::Success,
153            ResultStatus::Info,
154            ResultStatus::Warning,
155            ResultStatus::Error,
156            ResultStatus::NotFound,
157            ResultStatus::Forbidden,
158            ResultStatus::ServerError,
159        ];
160        for variant in variants.iter() {
161            let class = variant.as_class();
162            assert!(!class.is_empty());
163            assert!(class.starts_with("adui-result-"));
164            let icon = variant.icon_kind();
165            // Just verify icon_kind doesn't panic
166            let _ = format!("{:?}", icon);
167        }
168    }
169
170    #[test]
171    fn result_status_equality() {
172        assert_eq!(ResultStatus::Success, ResultStatus::Success);
173        assert_eq!(ResultStatus::Info, ResultStatus::Info);
174        assert_ne!(ResultStatus::Success, ResultStatus::Error);
175        assert_ne!(ResultStatus::NotFound, ResultStatus::Forbidden);
176    }
177
178    #[test]
179    fn result_status_clone() {
180        let original = ResultStatus::Warning;
181        let cloned = original;
182        assert_eq!(original, cloned);
183        assert_eq!(original.as_class(), cloned.as_class());
184        assert_eq!(original.icon_kind(), cloned.icon_kind());
185    }
186
187    #[test]
188    fn result_props_defaults() {
189        // ResultProps doesn't require any fields, but status defaults to Info when None
190        // All other fields are optional
191    }
192
193    #[test]
194    fn result_status_debug() {
195        let status = ResultStatus::ServerError;
196        let debug_str = format!("{:?}", status);
197        assert!(debug_str.contains("ServerError"));
198    }
199}