Skip to main content

rustio_admin/view_layer/
compose.rs

1//! Cell composition — merging several schema fields into one visual unit
2//! (e.g. "name over email", or "title with a trailing status badge").
3
4use serde::{Deserialize, Serialize};
5
6// public:
7/// How several fields are visually merged into a single cell.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ComposeStyle {
11    /// Fields stacked vertically, primary on top.
12    Stacked,
13    /// Leading icon followed by inline text.
14    InlineIcon,
15    /// Primary text with a trailing badge.
16    BadgeInline,
17}
18
19impl ComposeStyle {
20    // public:
21    /// The stable slug used in forms and serde (matches the snake_case
22    /// representation), e.g. `badge_inline`.
23    pub fn slug(self) -> &'static str {
24        match self {
25            ComposeStyle::Stacked => "stacked",
26            ComposeStyle::InlineIcon => "inline_icon",
27            ComposeStyle::BadgeInline => "badge_inline",
28        }
29    }
30
31    // public:
32    /// Parse a style slug from the designer form. Unknown values return `None`.
33    pub fn from_slug(slug: &str) -> Option<Self> {
34        match slug {
35            "stacked" => Some(ComposeStyle::Stacked),
36            "inline_icon" => Some(ComposeStyle::InlineIcon),
37            "badge_inline" => Some(ComposeStyle::BadgeInline),
38            _ => None,
39        }
40    }
41
42    // public:
43    /// Every style, in display order — for building the style `<select>`.
44    pub fn all() -> &'static [ComposeStyle] {
45        &[
46            ComposeStyle::Stacked,
47            ComposeStyle::InlineIcon,
48            ComposeStyle::BadgeInline,
49        ]
50    }
51}
52
53// public:
54/// A composed cell merges multiple schema fields into one visual unit.
55///
56/// Example: an "avatar + name + email" cell, or "status badge next to title".
57/// The renderer pulls `primary_field` first, then the `secondary_fields`, and
58/// lays them out according to `style`.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct CellComposition {
61    /// Stable id so a ViewSpec editor can reference this composition.
62    pub id: String,
63    /// Optional column header / label for the composed cell.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub label: Option<String>,
66    /// The layout the renderer applies to the merged fields.
67    pub style: ComposeStyle,
68    /// The field that carries the strongest visual weight in the cell.
69    pub primary_field: String,
70    /// Supporting fields, rendered in order after the primary.
71    #[serde(default)]
72    pub secondary_fields: Vec<String>,
73}
74
75impl CellComposition {
76    // public:
77    /// Every field this composition touches, primary first. Used by the
78    /// renderer to know which raw fields a cell already consumes so they
79    /// aren't also rendered standalone.
80    pub fn all_fields(&self) -> Vec<&str> {
81        let mut names = Vec::with_capacity(1 + self.secondary_fields.len());
82        names.push(self.primary_field.as_str());
83        names.extend(self.secondary_fields.iter().map(String::as_str));
84        names
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn style_slug_roundtrips_and_matches_serde() {
94        for style in ComposeStyle::all() {
95            assert_eq!(ComposeStyle::from_slug(style.slug()), Some(*style));
96            let json = serde_json::to_string(style).unwrap();
97            assert_eq!(json, format!("\"{}\"", style.slug()));
98        }
99        assert_eq!(ComposeStyle::from_slug("nope"), None);
100    }
101}