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
116 let ColumnWidths {
117 cells: cell_widths,
118 actions_min,
119 table_min_width,
120 ..
121 } = self.column_widths();
122 let mut pager_views: Vec<BoxView<'_>> = Vec::new();
123 let body: BoxView<'_> = if page.rows.is_empty() {
124 let empty_cell = self
125 .render_empty_cell(cx, state, path, with_actions, with_bulk, signals.as_ref())
126 .await?;
127 view! {
128 cx =>
129 table(
130 attrs: attributes! { class="table-fixed" style=(table_min_width.as_deref()) },
131 (head)
132 (empty_cell)
133 )
134 }
135 .boxed()
136 } else {
137 pager_views = pager;
138 let chrome = RowChrome {
139 with_bulk,
140 with_actions,
141 header_colspan: self.columns.len()
142 + usize::from(with_bulk)
143 + usize::from(with_actions),
144 cell_widths,
145 actions_min,
146 delete_dialog_id: delete_prefix
147 .as_deref()
148 .map(Self::delete_dialog_dom_id)
149 .unwrap_or_default(),
150 };
151 let rows = render_rows(cx, row_data, &chrome);
152 view! {
153 cx =>
154 table(
155 attrs: attributes! { class="table-fixed" style=(table_min_width.as_deref()) },
156 (head)
157 table_body(
158 #[key(row.key.as_str())]
159 for row in rows {
160 (row.view)
161 }
162 )
163 )
164 }
165 .boxed()
166 };
167
168 let revision_attrs = signals.as_ref().map(|_| {
169 let revision = topcoat::runtime::signal(&cx.keyed(path), || "0".to_string());
170 attributes! {
171 cx =>
172 type="hidden"
173 value=(revision.get())
174 :value=$(revision.get())
175 @change=$(|e: Event| revision.set(e.target.value))
176 data-table-revision=""
177 }
178 });
179
180 let toolbar_row = show_search || with_bulk;
181 let content = view! {
182 cx =>
183 if toolbar_row {
184 <div class=(BAR_CLASS)>
185 if show_search {
186 (search_bar.expect("search bar built when enabled"))
187 }
188 (bulk_bar_view)
189 </div>
190 } else {
191 (bulk_bar_view)
192 }
193 if show_filters {
194 (filter_bar.expect("filter bar built when enabled"))
195 }
196 if let Some(attrs) = revision_attrs {
197 <input (attrs)>
198 }
199 if let Some(warning) = filter_warning {
200 (warning)
201 }
202 (body)
203 for p in pager_views {
204 (p)
205 }
206 if let Some(dialog) = delete_dialog {
207 (dialog)
208 }
209 };
210 Ok(table_frame(
211 cx,
212 false,
213 self.framed,
214 self.delete_prefix.as_deref(),
215 content.boxed(),
216 ))
217 }
218}
219
220pub(super) fn table_frame<'a>(
223 cx: &'a Cx,
224 busy: bool,
225 framed: bool,
226 name: Option<&str>,
227 content: BoxView<'a>,
228) -> BoxView<'a> {
229 let busy = busy.then_some("true");
230 let name = name.map(str::to_string);
231 let root_class = if framed {
232 TABLE_CARD_CLASS
233 } else {
234 TABLE_BARE_CLASS
235 };
236 view! {
237 cx =>
238 <div data-boundary="table" data-table=(name) aria-busy=(busy)>
239 <div class=(root_class) data-table-root="" aria-busy=(busy)>(content)</div>
240 </div>
241 }
242 .boxed()
243}
244
245#[cfg(test)]
246pub(super) mod tests;