1use 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 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 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
76struct 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 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 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 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 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 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
261pub(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;