Skip to main content

tablo_core/table/render/
core.rs

1//! The table's entry points and its card.
2
3use tablo_ui::{table, table_body};
4use toasty::stmt::{List, Query};
5use topcoat::{Result, context::Cx, view::*};
6
7use super::{
8    super::WiredTable,
9    Frame, TABLE_CARD_CLASS,
10    filterbar::FilterViews,
11    rows::{RowChrome, RowView, render_rows, selectable_keys},
12    table_dom_id,
13    widths::ColumnWidths,
14};
15use crate::table::{
16    page::TablePage,
17    state::{TableSignals, TableState},
18};
19
20impl<M> WiredTable<M> {
21    /// Load the page of `query` the table's browser state selects, and render it.
22    ///
23    /// Call it while a page body runs: the table declares its signals here, and the page reads
24    /// the list state from them, so a sort, search, filter or page change in the browser reruns
25    /// the page in place. Links keep their `href`, which spells the same state. A page rendering
26    /// several tables gives each its own [`prefix`](Self::prefixed).
27    ///
28    /// # Panics
29    ///
30    /// Without a request context in `cx`.
31    ///
32    /// # Errors
33    ///
34    /// A misdeclared table ([`Table::declaration_errors`](crate::table::Table::declaration_errors))
35    /// fails with its errors rather than render, and a failed load fails with the database's
36    /// error.
37    pub async fn render<'a>(&self, cx: &'a Cx, query: Query<List<M>>) -> Result<BoxView<'a>>
38    where
39        M: toasty::schema::Model + Send + Sync + 'static,
40    {
41        let path = topcoat::router::request::uri(cx).path().to_string();
42        let (signals, state) = self.browser_state(cx);
43        let page = TablePage::load(cx, self, query, &state).await?;
44        self.render_page(cx, page, &state, &path, &signals).await
45    }
46
47    /// Render a loaded page for `state`, its links pointing at `path` and writing `signals`.
48    pub(crate) async fn render_page<'a>(
49        &self,
50        cx: &'a Cx,
51        page: TablePage<M>,
52        state: &TableState,
53        path: &str,
54        signals: &TableSignals,
55    ) -> Result<BoxView<'a>>
56    where
57        M: toasty::schema::Model + Send + Sync + 'static,
58    {
59        let errors = self.declaration_errors();
60        if !errors.is_empty() {
61            return Err(crate::error::misdeclared(&errors));
62        }
63        let frame = self.frame();
64        let group_key = self.effective_group_key(state);
65        let rows = self.row_views(cx, &frame, &page, group_key.as_ref());
66        let loaded = Loaded {
67            rows,
68            next_cursor: page.next_cursor,
69            prev_cursor: page.prev_cursor,
70            filters: self.filter_views(cx, state),
71        };
72        frame.render_page(cx, loaded, state, path, signals).await
73    }
74}
75
76/// One loaded page, projected for rendering.
77struct Loaded<'a> {
78    rows: Vec<RowView<'a>>,
79    next_cursor: Option<String>,
80    prev_cursor: Option<String>,
81    filters: FilterViews<'a>,
82}
83
84impl Frame<'_> {
85    /// Render `loaded` for `state`, its links pointing at `path` and writing `signals`.
86    async fn render_page<'a>(
87        &self,
88        cx: &'a Cx,
89        loaded: Loaded<'a>,
90        state: &TableState,
91        path: &str,
92        signals: &TableSignals,
93    ) -> Result<BoxView<'a>> {
94        let Loaded {
95            rows: row_data,
96            next_cursor,
97            prev_cursor,
98            mut filters,
99        } = loaded;
100        let with_actions = self.with_actions();
101        let with_bulk = self.bulk_enabled();
102        let head = self
103            .render_thead(
104                cx,
105                state,
106                path,
107                with_actions,
108                with_bulk,
109                Some((signals, selectable_keys(&row_data))),
110            )
111            .await?;
112        let filter_warning = self.render_filter_warning(cx, state, path, signals, &filters);
113        let controls = std::mem::take(&mut filters.controls);
114        let toolbar = self
115            .render_toolbar(cx, state, path, signals, controls)
116            .await?;
117        let pager = self
118            .render_pager(
119                cx,
120                state,
121                path,
122                next_cursor.as_deref(),
123                prev_cursor.as_deref(),
124                signals,
125            )
126            .await?;
127        let write_form = self.render_write_form(cx, state, signals);
128
129        let ColumnWidths {
130            cells: cell_widths,
131            actions_min,
132            table_min_width,
133            ..
134        } = self.column_widths();
135        let mut pager_views: Vec<BoxView<'_>> = Vec::new();
136        let body: BoxView<'_> = if row_data.is_empty() {
137            let empty_cell = self
138                .render_empty_cell(cx, state, path, with_actions, with_bulk, signals)
139                .await?;
140            view! {
141                cx =>
142                table(
143                    attrs: attributes! { class="table-fixed" style=(table_min_width.as_deref()) },
144                    (head)
145                    (empty_cell)
146                )
147            }
148            .boxed()
149        } else {
150            pager_views = pager;
151            let chrome = RowChrome {
152                with_bulk,
153                with_actions,
154                header_colspan: self.columns.len()
155                    + usize::from(with_bulk)
156                    + usize::from(with_actions),
157                cell_widths,
158                actions_min,
159                signals: signals.clone(),
160                form: table_dom_id(state, "writes"),
161            };
162            let rows = render_rows(cx, row_data, &chrome);
163            view! {
164                cx =>
165                table(
166                    attrs: attributes! { class="table-fixed" style=(table_min_width.as_deref()) },
167                    (head)
168                    table_body(
169                        #[key(row.key.as_str())]
170                        for row in rows {
171                            (row.view)
172                        }
173                    )
174                )
175            }
176            .boxed()
177        };
178
179        let content = view! {
180            cx =>
181            (toolbar)
182            if let Some(warning) = filter_warning {
183                (warning)
184            }
185            (body)
186            for p in pager_views {
187                (p)
188            }
189            if let Some(form) = write_form {
190                (form)
191            }
192        };
193        Ok(table_frame(cx, false, content.boxed()))
194    }
195}
196
197#[cfg(test)]
198impl<M> crate::table::Table<M>
199where
200    M: toasty::schema::Model + Send + Sync + 'static,
201{
202    /// This table, wired with nothing, rendering `page` as [`WiredTable::render_loaded`] does.
203    pub(crate) async fn render<'a>(&self, cx: &'a Cx, page: TablePage<M>) -> Result<BoxView<'a>> {
204        self.clone()
205            .wired()
206            .render_loaded_with(cx, page, &TableState::default(), "")
207            .await
208    }
209
210    /// [`Self::render`] for `state`, its links pointing at `path`.
211    pub(crate) async fn render_with_state<'a>(
212        &self,
213        cx: &'a Cx,
214        page: TablePage<M>,
215        state: &TableState,
216        path: &str,
217    ) -> Result<BoxView<'a>> {
218        self.clone()
219            .wired()
220            .render_loaded_with(cx, page, state, path)
221            .await
222    }
223}
224
225#[cfg(test)]
226impl<M> WiredTable<M>
227where
228    M: toasty::schema::Model + Send + Sync + 'static,
229{
230    /// Render a loaded `page` with neutral state inside a page body, as a test's view resolves it.
231    pub(crate) async fn render_loaded<'a>(
232        self,
233        cx: &'a Cx,
234        page: TablePage<M>,
235    ) -> Result<BoxView<'a>> {
236        self.render_loaded_with(cx, page, &TableState::default(), "")
237            .await
238    }
239
240    /// [`Self::render_loaded`] for `state`, its links pointing at `path`.
241    pub(crate) async fn render_loaded_with<'a>(
242        self,
243        cx: &'a Cx,
244        page: TablePage<M>,
245        state: &TableState,
246        path: &str,
247    ) -> Result<BoxView<'a>> {
248        let errors = self.declaration_errors();
249        if !errors.is_empty() {
250            return Err(crate::error::misdeclared(&errors));
251        }
252        let (state, path) = (state.clone(), path.to_string());
253        Ok(crate::topcoat_compat::async_page(async move {
254            let signals = TableSignals::new(cx, state.prefix.as_deref());
255            let state = self.normalize_state(&state);
256            self.render_page(cx, page, &state, &path, &signals).await
257        }))
258    }
259}
260
261/// Render the table's card, shared by the loaded table and its skeleton.
262pub(super) fn table_frame<'a>(cx: &'a Cx, busy: bool, content: BoxView<'a>) -> BoxView<'a> {
263    let busy = busy.then_some("true");
264    view! { cx => <div class=(TABLE_CARD_CLASS) aria-busy=(busy)>(content)</div> }.boxed()
265}
266
267#[cfg(test)]
268pub(super) mod tests;