1use tablo_ui::{table, table_body};
4use topcoat::{Result, context::Cx, runtime::Event, view::*};
5
6use super::{
7 super::Table,
8 BAR_CLASS, TABLE_BARE_CLASS, TABLE_CARD_CLASS,
9 rows::{RowChrome, render_rows},
10 widths::ColumnWidths,
11};
12use crate::table::{
13 page::TablePage,
14 state::{TableSignals, TableState},
15};
16
17impl<M> Table<M> {
18 pub async fn render<'a>(&self, cx: &'a Cx, page: TablePage<M>) -> Result<BoxView<'a>>
25 where
26 M: toasty::schema::Model + Send + Sync + 'static,
27 {
28 let state = TableState::from_cx(cx);
29 let path = topcoat::context::try_request_context::<http::request::Parts>(cx)
30 .map(|parts| parts.uri.path().to_string())
31 .unwrap_or_default();
32 self.render_with_state(cx, page, &state, &path).await
33 }
34
35 pub async fn render_with_state<'a>(
37 &self,
38 cx: &'a Cx,
39 page: TablePage<M>,
40 state: &TableState,
41 path: &str,
42 ) -> Result<BoxView<'a>>
43 where
44 M: toasty::schema::Model + Send + Sync + 'static,
45 {
46 self.render_inner(cx, page, &self.normalize_state(state), path, None)
47 .await
48 }
49
50 pub(crate) async fn render_live<'a>(
53 &self,
54 cx: &'a Cx,
55 page: TablePage<M>,
56 state: &TableState,
57 path: &str,
58 signals: TableSignals,
59 ) -> Result<BoxView<'a>>
60 where
61 M: toasty::schema::Model + Send + Sync + 'static,
62 {
63 self.render_inner(cx, page, state, path, Some(signals))
64 .await
65 }
66
67 async fn render_inner<'a>(
68 &self,
69 cx: &'a Cx,
70 page: TablePage<M>,
71 state: &TableState,
72 path: &str,
73 signals: Option<TableSignals>,
74 ) -> Result<BoxView<'a>>
75 where
76 M: toasty::schema::Model + Send + Sync + 'static,
77 {
78 let errors = self.declaration_errors();
79 if !errors.is_empty() {
80 return Err(crate::error::misdeclared(&errors));
81 }
82 let delete_prefix = self.delete_prefix.clone();
83 let with_actions = self.with_actions();
84 let with_bulk = self.bulk_enabled();
85 let head = self
86 .render_thead(cx, state, path, with_actions, with_bulk, signals.as_ref())
87 .await?;
88 let show_search = self.search_enabled();
89 let search_bar = if show_search {
90 Some(self.render_search_bar(cx, state, path).await?)
91 } else {
92 None
93 };
94 let show_filters = self.filter_bar_enabled();
95 let filter_bar = if show_filters {
96 Some(
97 self.render_filter_bar(cx, state, path, signals.as_ref())
98 .await?,
99 )
100 } else {
101 None
102 };
103 let bulk_bar_view = self.render_bulk_bar(cx, signals.as_ref());
104 let pager = self
105 .render_pager(cx, state, path, &page, signals.as_ref())
106 .await?;
107 let filter_warning = self.render_filter_warning(cx, state, path);
108 let group_key = self.effective_group_key(state);
109 let row_data = self.row_views(cx, state, path, &page, group_key.as_ref());
110 let delete_dialog = if signals.is_none() {
111 self.render_delete_dialog(cx, state).await?
112 } else {
113 None
114 };
115 let action_dialog = if signals.is_none() {
116 self.render_action_confirm_dialog(cx)
117 } else {
118 None
119 };
120
121 let ColumnWidths {
122 cells: cell_widths,
123 actions_min,
124 table_min_width,
125 ..
126 } = self.column_widths();
127 let mut pager_views: Vec<BoxView<'_>> = Vec::new();
128 let body: BoxView<'_> = if page.rows.is_empty() {
129 let empty_cell = self
130 .render_empty_cell(cx, state, path, with_actions, with_bulk, signals.as_ref())
131 .await?;
132 view! {
133 cx =>
134 table(
135 attrs: attributes! { class="table-fixed" style=(table_min_width.as_deref()) },
136 (head)
137 (empty_cell)
138 )
139 }
140 .boxed()
141 } else {
142 pager_views = pager;
143 let chrome = RowChrome {
144 with_bulk,
145 with_actions,
146 header_colspan: self.columns.len()
147 + usize::from(with_bulk)
148 + usize::from(with_actions),
149 cell_widths,
150 actions_min,
151 delete_dialog_id: delete_prefix
152 .as_deref()
153 .map(Self::delete_dialog_dom_id)
154 .unwrap_or_default(),
155 action_dialog_id: self
156 .actions_prefix
157 .as_deref()
158 .map(Self::action_confirm_dialog_dom_id)
159 .unwrap_or_default(),
160 };
161 let rows = render_rows(cx, row_data, &chrome);
162 view! {
163 cx =>
164 table(
165 attrs: attributes! { class="table-fixed" style=(table_min_width.as_deref()) },
166 (head)
167 table_body(
168 #[key(row.key.as_str())]
169 for row in rows {
170 (row.view)
171 }
172 )
173 )
174 }
175 .boxed()
176 };
177
178 let revision_attrs = signals.as_ref().map(|_| {
179 let revision = topcoat::runtime::signal(&cx.keyed(path), || "0".to_string());
180 attributes! {
181 cx =>
182 type="hidden"
183 value=(revision.get())
184 :value=$(revision.get())
185 @change=$(|e: Event| revision.set(e.target.value))
186 data-table-revision=""
187 }
188 });
189
190 let toolbar_row = show_search || with_bulk;
191 let content = view! {
192 cx =>
193 if toolbar_row {
194 <div class=(BAR_CLASS)>
195 if show_search {
196 (search_bar.expect("search bar built when enabled"))
197 }
198 (bulk_bar_view)
199 </div>
200 } else {
201 (bulk_bar_view)
202 }
203 if show_filters {
204 (filter_bar.expect("filter bar built when enabled"))
205 }
206 if let Some(attrs) = revision_attrs {
207 <input (attrs)>
208 }
209 if let Some(warning) = filter_warning {
210 (warning)
211 }
212 (body)
213 for p in pager_views {
214 (p)
215 }
216 if let Some(dialog) = delete_dialog {
217 (dialog)
218 }
219 if let Some(dialog) = action_dialog {
220 (dialog)
221 }
222 };
223 Ok(table_frame(
224 cx,
225 false,
226 self.framed,
227 self.delete_prefix.as_deref(),
228 content.boxed(),
229 ))
230 }
231}
232
233pub(super) fn table_frame<'a>(
236 cx: &'a Cx,
237 busy: bool,
238 framed: bool,
239 name: Option<&str>,
240 content: BoxView<'a>,
241) -> BoxView<'a> {
242 let busy = busy.then_some("true");
243 let name = name.map(str::to_string);
244 let root_class = if framed {
245 TABLE_CARD_CLASS
246 } else {
247 TABLE_BARE_CLASS
248 };
249 view! {
250 cx =>
251 <div data-boundary="table" data-table=(name) aria-busy=(busy)>
252 <div class=(root_class) data-table-root="" aria-busy=(busy)>(content)</div>
253 </div>
254 }
255 .boxed()
256}
257
258#[cfg(test)]
259pub(super) mod tests;