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 derive_where::derive_where;
4use toasty::stmt::List;
5use topcoat::{context::Cx, view::*};
6
7use super::{Column, ColumnBase, ColumnWidth};
8use crate::{
9    Lens,
10    form::FormValues,
11    schema::{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, tablo_core::ModelColumns)]
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/// ```
29#[derive_where(Clone, Debug)]
30pub struct RepeaterColumn<M, T> {
31    lens: Lens<M, Vec<T>, List<T>>,
32    base: ColumnBase,
33}
34
35impl<M, T> RepeaterColumn<M, T>
36where
37    M: toasty::schema::Model,
38    T: RepeaterItem,
39{
40    /// Bind the column to the list `lens` reads.
41    pub fn new(lens: Lens<M, Vec<T>, List<T>>) -> Self {
42        Self {
43            base: ColumnBase::new(lens.path(), ColumnWidth::Wide),
44            lens,
45        }
46    }
47
48    base_builders!();
49
50    /// Each item's keys as its row posts them.
51    fn spelled(&self, row: &M) -> Vec<FormValues> {
52        self.lens
53            .read(row)
54            .iter()
55            .map(|item| {
56                let mut values = FormValues::new();
57                item.write(&mut values);
58                values
59            })
60            .collect()
61    }
62}
63
64impl<M, T> Column<M> for RepeaterColumn<M, T>
65where
66    M: toasty::schema::Model + Send + Sync + 'static,
67    T: RepeaterItem,
68{
69    base_column_methods!(bind);
70
71    fn text(&self, _cx: &Cx, row: &M) -> String {
72        let schema = T::schema();
73        self.spelled(row)
74            .iter()
75            .map(|values| {
76                schema
77                    .fields()
78                    .filter_map(|field| {
79                        let value = values.text(field.name()).unwrap_or_default();
80                        let read = field.read(value)?;
81                        Some(format!("{}: {read}", field.label_str()))
82                    })
83                    .collect::<Vec<_>>()
84                    .join(", ")
85            })
86            .collect::<Vec<_>>()
87            .join("; ")
88    }
89
90    fn entry<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
91        let label = Column::label(self);
92        let items = self.spelled(row);
93        if items.is_empty() {
94            return read_only(cx, label, value_cell(cx, ""));
95        }
96        let schema = T::schema();
97        let items: Vec<BoxView<'a>> = items
98            .iter()
99            .map(|values| {
100                let fields: Vec<BoxView<'a>> = schema
101                    .fields()
102                    .map(|field| field.display(cx, values.text(field.name()).unwrap_or_default()))
103                    .collect();
104                view! {
105                    cx =>
106                    <li class="flex flex-col gap-3 rounded-lg border border-border p-4">
107                        for field in fields {
108                            (field)
109                        }
110                    </li>
111                }
112                .boxed()
113            })
114            .collect();
115        read_only(
116            cx,
117            label,
118            view! {
119                cx =>
120                <ol class="flex flex-col gap-3 whitespace-normal">
121                    for item in items {
122                        (item)
123                    }
124                </ol>
125            }
126            .boxed(),
127        )
128    }
129}