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}