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).await,
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 signals = &signals
101            .clone()
102            .with_inputs(cx, self.inputs.iter().map(|action| action.name));
103        let with_actions = self.with_actions();
104        let with_bulk = self.bulk_enabled();
105        let head = self
106            .render_thead(
107                cx,
108                state,
109                path,
110                with_actions,
111                with_bulk,
112                Some((signals, selectable_keys(&row_data))),
113            )
114            .await?;
115        let filter_warning = self.render_filter_warning(cx, state, path, signals, &filters);
116        let controls = std::mem::take(&mut filters.controls);
117        let toolbar = self
118            .render_toolbar(cx, state, path, signals, controls)
119            .await?;
120        let pager = self
121            .render_pager(
122                cx,
123                state,
124                path,
125                next_cursor.as_deref(),
126                prev_cursor.as_deref(),
127                signals,
128            )
129            .await?;
130        let write_form = self.render_write_form(cx, state, signals);
131        let input_dialogs = self.render_input_dialogs(cx, state, signals);
132
133        let ColumnWidths {
134            cells: cell_widths,
135            actions,
136            table_min_width,
137            ..
138        } = self.column_widths();
139        let mut pager_views: Vec<BoxView<'_>> = Vec::new();
140        let body: BoxView<'_> = if row_data.is_empty() {
141            let empty_cell = self
142                .render_empty_cell(cx, state, path, with_actions, with_bulk, signals)
143                .await?;
144            view! {
145                cx =>
146                table(
147                    attrs: attributes! { class="table-fixed" style=(table_min_width.as_deref()) },
148                    (head)
149                    (empty_cell)
150                )
151            }
152            .boxed()
153        } else {
154            pager_views = pager;
155            let chrome = RowChrome {
156                with_bulk,
157                with_actions,
158                header_colspan: self.columns.len()
159                    + usize::from(with_bulk)
160                    + usize::from(with_actions),
161                cell_widths,
162                actions,
163                signals: signals.clone(),
164                form: table_dom_id(state, "writes"),
165            };
166            let rows = render_rows(cx, row_data, &chrome);
167            view! {
168                cx =>
169                table(
170                    attrs: attributes! { class="table-fixed" style=(table_min_width.as_deref()) },
171                    (head)
172                    table_body(
173                        #[key(row.key.as_str())]
174                        for row in rows {
175                            (row.view)
176                        }
177                    )
178                )
179            }
180            .boxed()
181        };
182
183        let content = view! {
184            cx =>
185            (toolbar)
186            if let Some(warning) = filter_warning {
187                (warning)
188            }
189            (body)
190            for p in pager_views {
191                (p)
192            }
193            if let Some(form) = write_form {
194                (form)
195            }
196            for dialog in input_dialogs {
197                (dialog)
198            }
199        };
200        Ok(table_frame(cx, false, content.boxed()))
201    }
202}
203
204#[cfg(test)]
205impl<M> crate::table::Table<M>
206where
207    M: toasty::schema::Model + Send + Sync + 'static,
208{
209    /// This table, wired with nothing, rendering `page` as [`WiredTable::render_loaded`] does.
210    pub(crate) async fn render<'a>(&self, cx: &'a Cx, page: TablePage<M>) -> Result<BoxView<'a>> {
211        self.clone()
212            .wired()
213            .render_loaded_with(cx, page, &TableState::default(), "")
214            .await
215    }
216
217    /// [`Self::render`] for `state`, its links pointing at `path`.
218    pub(crate) async fn render_with_state<'a>(
219        &self,
220        cx: &'a Cx,
221        page: TablePage<M>,
222        state: &TableState,
223        path: &str,
224    ) -> Result<BoxView<'a>> {
225        self.clone()
226            .wired()
227            .render_loaded_with(cx, page, state, path)
228            .await
229    }
230}
231
232#[cfg(test)]
233impl<M> WiredTable<M>
234where
235    M: toasty::schema::Model + Send + Sync + 'static,
236{
237    /// Render a loaded `page` with neutral state inside a page body, as a test's view resolves it.
238    pub(crate) async fn render_loaded<'a>(
239        self,
240        cx: &'a Cx,
241        page: TablePage<M>,
242    ) -> Result<BoxView<'a>> {
243        self.render_loaded_with(cx, page, &TableState::default(), "")
244            .await
245    }
246
247    /// [`Self::render_loaded`] for `state`, its links pointing at `path`.
248    pub(crate) async fn render_loaded_with<'a>(
249        self,
250        cx: &'a Cx,
251        page: TablePage<M>,
252        state: &TableState,
253        path: &str,
254    ) -> Result<BoxView<'a>> {
255        let errors = self.declaration_errors();
256        if !errors.is_empty() {
257            return Err(crate::error::misdeclared(&errors));
258        }
259        let (state, path) = (state.clone(), path.to_string());
260        Ok(crate::topcoat_compat::async_page(async move {
261            let signals = TableSignals::new(cx, state.prefix.as_deref());
262            let state = self.normalize_state(&state);
263            self.render_page(cx, page, &state, &path, &signals).await
264        }))
265    }
266}
267
268/// Render the table's card, shared by the loaded table and its skeleton.
269pub(super) fn table_frame<'a>(cx: &'a Cx, busy: bool, content: BoxView<'a>) -> BoxView<'a> {
270    let busy = busy.then_some("true");
271    view! { cx => <div class=(TABLE_CARD_CLASS) aria-busy=(busy)>(content)</div> }.boxed()
272}
273
274#[cfg(test)]
275pub(super) mod tests;