Skip to main content

tablo_core/table/column/
repeater.rs

1//! [`RepeaterColumn`]: a `#[document]` list read off the record, one item at a time.
2
3use std::collections::HashMap;
4
5use toasty::stmt::List;
6use topcoat::{context::Cx, view::*};
7
8use super::{Column, ColumnWidth};
9use crate::{
10    Lens,
11    schema::{Binding, FieldResolver, RepeaterItem, read_only, value_cell},
12};
13
14/// A column of a list of [`RepeaterItem`] values.
15///
16/// A detail page shows each item as a block of its fields, each under its own label. A table cell
17/// lists the same fields as `label: value`, one item after another.
18///
19/// ```rust
20/// # #[derive(Debug, Clone, toasty::Embed, tablo_core::RepeaterItem)]
21/// # struct Link { label: String, url: String }
22/// # #[derive(Debug, Clone, toasty::Model)]
23/// # struct Post {
24/// #     #[key] #[auto] id: uuid::Uuid,
25/// #     #[document] links: Vec<Link>,
26/// # }
27/// tablo_core::RepeaterColumn::new(tablo_core::lens!(Post.links));
28/// ```
29pub struct RepeaterColumn<M, T> {
30    lens: Lens<M, Vec<T>, List<T>>,
31    binding: Binding,
32    /// The declared label, over the binding's.
33    label: Option<String>,
34    width: ColumnWidth,
35}
36
37impl<M, T> RepeaterColumn<M, T>
38where
39    M: toasty::schema::Model,
40    T: RepeaterItem,
41{
42    /// Bind the column to the list `lens` reads.
43    pub fn new(lens: Lens<M, Vec<T>, List<T>>) -> Self {
44        let binding = Binding::of(lens.path());
45        Self {
46            lens,
47            binding,
48            label: None,
49            width: ColumnWidth::Wide,
50        }
51    }
52
53    /// Replace the label the field's name gives it.
54    pub fn label(mut self, label: impl Into<String>) -> Self {
55        self.label = Some(label.into());
56        self
57    }
58
59    /// Declare this column's width.
60    pub fn width(mut self, width: ColumnWidth) -> Self {
61        self.width = width;
62        self
63    }
64
65    /// Each item's keys as its row posts them.
66    fn spelled(&self, row: &M) -> Vec<HashMap<String, String>> {
67        self.lens
68            .read(row)
69            .iter()
70            .map(|item| {
71                let mut values = HashMap::new();
72                item.write(&mut values);
73                values
74            })
75            .collect()
76    }
77}
78
79impl<M, T> Column<M> for RepeaterColumn<M, T>
80where
81    M: toasty::schema::Model + Send + Sync + 'static,
82    T: RepeaterItem,
83{
84    fn name(&self) -> &str {
85        self.binding.name()
86    }
87
88    fn label(&self) -> &str {
89        self.label.as_deref().unwrap_or(self.binding.label())
90    }
91
92    fn text(&self, _cx: &Cx, row: &M) -> String {
93        let schema = T::schema();
94        self.spelled(row)
95            .iter()
96            .map(|values| {
97                schema
98                    .fields()
99                    .filter_map(|field| {
100                        let value = values.get(field.name()).map_or("", String::as_str);
101                        let read = field.read(value)?;
102                        Some(format!("{}: {read}", field.label_str()))
103                    })
104                    .collect::<Vec<_>>()
105                    .join(", ")
106            })
107            .collect::<Vec<_>>()
108            .join("; ")
109    }
110
111    fn entry<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
112        let label = Column::label(self);
113        let items = self.spelled(row);
114        if items.is_empty() {
115            return read_only(cx, label, value_cell(cx, ""));
116        }
117        let schema = T::schema();
118        let items: Vec<BoxView<'a>> = items
119            .iter()
120            .map(|values| {
121                let fields: Vec<BoxView<'a>> = schema
122                    .fields()
123                    .map(|field| {
124                        field.display(cx, values.get(field.name()).map_or("", String::as_str))
125                    })
126                    .collect();
127                view! {
128                    cx =>
129                    <li class="flex flex-col gap-3 rounded-lg border border-border p-4">
130                        for field in fields {
131                            (field)
132                        }
133                    </li>
134                }
135                .boxed()
136            })
137            .collect();
138        read_only(
139            cx,
140            label,
141            view! {
142                cx =>
143                <ol class="flex flex-col gap-3 whitespace-normal">
144                    for item in items {
145                        (item)
146                    }
147                </ol>
148            }
149            .boxed(),
150        )
151    }
152
153    fn column_width(&self) -> ColumnWidth {
154        self.width
155    }
156
157    fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
158        self.binding.misdeclared()
159    }
160
161    fn bind(&self, resolver: &FieldResolver) {
162        self.binding.bind(resolver);
163    }
164}
165
166impl<M, T> Clone for RepeaterColumn<M, T> {
167    fn clone(&self) -> Self {
168        Self {
169            lens: self.lens.clone(),
170            binding: self.binding.clone(),
171            label: self.label.clone(),
172            width: self.width,
173        }
174    }
175}
176
177impl<M, T> std::fmt::Debug for RepeaterColumn<M, T> {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.debug_struct("RepeaterColumn")
180            .field("name", &self.binding.name())
181            .field(
182                "label",
183                &self.label.as_deref().unwrap_or(self.binding.label()),
184            )
185            .field("item", &std::any::type_name::<T>())
186            .finish_non_exhaustive()
187    }
188}