1use std::{marker::PhantomData, num::NonZeroUsize, sync::Arc};
8
9use toasty::stmt::{Expr, List, OrderByExpr};
10
11pub(crate) use self::column::{BoxColumn, Column, include_relations};
12use self::filter::BoxFilter;
13use crate::{
14 DeclarationErrorKind, Lens,
15 form::FormScalar,
16 schema::{Binding, FieldResolver, Schema},
17};
18
19pub(crate) mod column;
20mod export;
21pub(crate) mod filter;
22mod input;
23mod page;
24mod render;
25mod state;
26mod wiring;
27
28pub use self::{
29 column::{
30 BooleanColumn, ColumnWidth, ComputedColumn, CountColumn, EmbeddedColumn, FileColumn,
31 IntoColumns, RelationColumn, RelationLens, RepeaterColumn, TextColumn, ToOneRelation,
32 contains_expr,
33 },
34 filter::{DateFilter, IntoFilters, QueryFilter, SelectFilter, TernaryFilter},
35 page::TablePage,
36 state::{Cursor, Sort, TableState},
37 wiring::WiredTable,
38};
39pub(crate) use self::{
40 input::{InputDialog, Posts, TableTarget, open_dialog, selected_count},
41 page::{Past, row_exists_past},
42 state::{
43 ACTION_ROUTE_PARAM, ACTIONS_ROUTE_SEGMENT, BULK_DELETE_ROUTE_SEGMENT, CREATE_ROUTE_SEGMENT,
44 DASH_ROUTE_SEGMENT, DELETE_ROUTE_SEGMENT, EDIT_ROUTE_SEGMENT, RECORD_ROUTE_PARAM,
45 RELATED_ROUTE_PARAM, RELATION_ROUTE_PARAM, RELATIONS_ROUTE_SEGMENT, RETURN_PARAM,
46 action_options_url, bulk_action_url, create_page_url, delete_action_url, relation_url,
47 row_action_url, with_return,
48 },
49};
50
51pub(crate) type GroupKey<M> = Arc<dyn Fn(&M) -> String + Send + Sync>;
53
54pub(crate) type RowPolicy<M> = Arc<dyn Fn(&M) -> RowActions + Send + Sync>;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(crate) struct RowActions {
62 pub view: bool,
64 pub edit: bool,
66 pub delete: bool,
68}
69
70impl RowActions {
71 pub const ALL: Self = Self {
73 view: true,
74 edit: true,
75 delete: true,
76 };
77}
78
79pub(crate) struct GroupDef<M> {
81 binding: Binding,
82 key: GroupKey<M>,
83}
84
85impl<M> Clone for GroupDef<M> {
86 fn clone(&self) -> Self {
87 Self {
88 binding: self.binding.clone(),
89 key: Arc::clone(&self.key),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
96pub(crate) struct TableChrome {
97 pub(crate) delete: bool,
99 pub(crate) edit: bool,
101 pub(crate) view: bool,
103 pub(crate) actions: bool,
105}
106
107pub(crate) struct TableAction<M> {
109 pub(crate) name: &'static str,
110 pub(crate) label: String,
111 pub(crate) row: bool,
112 pub(crate) bulk: bool,
113 pub(crate) confirm: bool,
115 pub(crate) input: Option<fn() -> Schema>,
117 pub(crate) allowed: Arc<dyn Fn(&M) -> bool + Send + Sync>,
118}
119
120pub const DEFAULT_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(25).unwrap();
122
123pub struct Table<M> {
125 columns: Vec<BoxColumn<M>>,
126 misdeclared: Vec<DeclarationErrorKind>,
128 filters: Vec<BoxFilter<M>>,
129 group_by: Option<GroupDef<M>>,
130 key: fn(&M) -> String,
132 addressable: bool,
135 page_size: NonZeroUsize,
136 hide_search: bool,
137 hide_filter_bar: bool,
138 _marker: PhantomData<M>,
139}
140
141impl<M> Clone for Table<M> {
143 fn clone(&self) -> Self {
144 Self {
145 columns: self.columns.clone(),
146 misdeclared: self.misdeclared.clone(),
147 filters: self.filters.clone(),
148 group_by: self.group_by.clone(),
149 key: self.key,
150 addressable: self.addressable,
151 page_size: self.page_size,
152 hide_search: self.hide_search,
153 hide_filter_bar: self.hide_filter_bar,
154 _marker: PhantomData,
155 }
156 }
157}
158
159impl<M> std::fmt::Debug for Table<M> {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 f.debug_struct("Table")
162 .field(
163 "columns",
164 &self.columns.iter().map(|c| c.name()).collect::<Vec<_>>(),
165 )
166 .field("filters", &self.filters.len())
167 .field("group_by", &self.group_by.is_some())
168 .field("page_size", &self.page_size)
169 .field("hide_search", &self.hide_search)
170 .field("hide_filter_bar", &self.hide_filter_bar)
171 .finish()
172 }
173}
174
175impl<M> Table<M> {
176 pub fn new(cols: impl IntoColumns<M>) -> Self
180 where
181 M: toasty::schema::Model + toasty::stmt::IntoExpr<M>,
182 {
183 Self {
184 columns: cols.into_columns(),
185 misdeclared: Vec::new(),
186 filters: Vec::new(),
187 group_by: None,
188 key: crate::toasty_compat::pk::pk_text::<M>,
189 addressable: !crate::toasty_compat::pk::pk_is_composite::<M>(),
190 page_size: DEFAULT_PAGE_SIZE,
191 hide_search: false,
192 hide_filter_bar: false,
193 _marker: PhantomData,
194 }
195 }
196
197 pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
199 self.columns.push(Arc::new(column));
200 self
201 }
202
203 pub fn filters(mut self, filters: impl IntoFilters<M>) -> Self
205 where
206 M: toasty::schema::Model,
207 {
208 let filters = filters.into_filters();
209 self.filters = filters;
210 self
211 }
212
213 pub(crate) fn include_relations(
215 &self,
216 query: toasty::stmt::Query<List<M>>,
217 ) -> toasty::stmt::Query<List<M>>
218 where
219 M: toasty::schema::Model,
220 {
221 column::include_relations(query, &self.columns)
222 }
223
224 pub(crate) fn filter_expr(&self, state: &TableState) -> Option<Expr<bool>>
226 where
227 M: toasty::schema::Model,
228 {
229 let mut exprs = Vec::new();
230 for f in &self.filters {
231 if let Some(v) = state.filters.get(f.name())
232 && let Some(e) = f.to_expr(v)
233 {
234 exprs.push(e);
235 }
236 }
237 if exprs.is_empty() {
238 None
239 } else {
240 let mut iter = exprs.into_iter();
241 let first = iter.next().unwrap();
242 Some(iter.fold(first, |acc, e| acc.and(e)))
243 }
244 }
245
246 pub(crate) fn unapplied_filters(&self, state: &TableState) -> Vec<(String, String)>
248 where
249 M: toasty::schema::Model,
250 {
251 let mut out = Vec::new();
252 for (key, value) in &state.filters {
253 match self.filters.iter().find(|f| f.name() == key) {
254 None => out.push((format!("{key}:{value}"), "unknown filter".to_string())),
255 Some(f) if f.to_expr(value).is_none() && !f.is_noop_value(value) => {
256 out.push((format!("{key}:{value}"), "invalid value".to_string()))
257 }
258 Some(_) => {}
259 }
260 }
261 if state.filters_dropped {
262 out.push((
263 "dropped filters".to_string(),
264 format!(
265 "more than {}, over {} bytes, or the retired filters= form",
266 state::MAX_FILTERS,
267 state::MAX_FILTER_LEN
268 ),
269 ));
270 }
271 out.sort();
272 out
273 }
274
275 pub fn group_by<T>(mut self, lens: Lens<M, T>) -> Self
277 where
278 M: toasty::schema::Model + Send + Sync + 'static,
279 T: FormScalar + Send + Sync + 'static,
280 {
281 self.group_by = Some(GroupDef {
282 binding: Binding::of(lens.path()),
283 key: Arc::new(move |record| lens.read(record).to_label()),
284 });
285 self
286 }
287
288 fn effective_group_key(&self, state: &TableState) -> Option<GroupKey<M>> {
290 match (&self.group_by, &state.group_by) {
291 (Some(def), Some(want)) if def.binding.name() == want => Some(def.key.clone()),
292 _ => None,
293 }
294 }
295
296 pub(crate) fn normalize_state(&self, state: &TableState) -> TableState {
298 let mut out = state.clone();
299 if self.group_by.as_ref().map(|def| def.binding.name()) != out.group_by.as_deref() {
300 out.group_by = None;
301 }
302 out
303 }
304
305 pub fn paginate(mut self, per_page: usize) -> Self {
307 match NonZeroUsize::new(per_page) {
308 Some(size) => self.page_size = size,
309 None => self.misdeclared.push(DeclarationErrorKind::ZeroPageSize),
310 }
311 self
312 }
313
314 pub(crate) fn unavailable_sources(
316 &self,
317 cx: &topcoat::context::Cx,
318 ) -> Vec<DeclarationErrorKind> {
319 crate::table::column::unavailable_sources(cx, self.columns.iter().map(|c| &**c))
320 }
321
322 pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
324 let mut errors = self.misdeclared.clone();
325 errors.extend(
326 self.group_by
327 .as_ref()
328 .and_then(|def| def.binding.misdeclared()),
329 );
330 if self.columns.is_empty() {
331 errors.push(DeclarationErrorKind::NoColumns);
332 }
333 let mut seen = std::collections::HashSet::with_capacity(self.columns.len());
334 for column in &self.columns {
335 match column.misdeclared() {
336 Some(error) => errors.push(error),
337 None if !seen.insert(column.name()) => {
338 errors.push(DeclarationErrorKind::DuplicateColumn {
339 name: column.name().to_string(),
340 });
341 }
342 None => {}
343 }
344 }
345 let mut seen = std::collections::HashSet::with_capacity(self.filters.len());
346 for filter in &self.filters {
347 match filter.misdeclared() {
348 Some(error) => errors.push(error),
349 None if !seen.insert(filter.name()) => {
350 errors.push(DeclarationErrorKind::DuplicateFilter {
351 name: filter.name().to_string(),
352 });
353 }
354 None => {}
355 }
356 }
357 errors
358 }
359
360 pub fn bind(self, db: &toasty::Db) -> Self {
366 self.bind_with(&FieldResolver::of_db(db));
367 self
368 }
369
370 pub(crate) fn bind_with(&self, resolver: &FieldResolver) {
372 for column in &self.columns {
373 column.bind(resolver);
374 }
375 for filter in &self.filters {
376 filter.bind(resolver);
377 }
378 if let Some(def) = &self.group_by {
379 def.binding.bind(resolver);
380 }
381 }
382
383 pub fn page_size(&self) -> usize {
385 self.page_size.get()
386 }
387
388 pub(crate) fn key_of(&self, record: &M) -> String {
390 (self.key)(record)
391 }
392
393 pub(crate) fn is_addressable(&self) -> bool {
395 self.addressable
396 }
397
398 pub fn hide_search(mut self) -> Self {
400 self.hide_search = true;
401 self
402 }
403
404 pub fn hide_filter_bar(mut self) -> Self {
406 self.hide_filter_bar = true;
407 self
408 }
409
410 pub(crate) fn search_expr(&self, term: &str) -> Option<Expr<bool>>
412 where
413 M: toasty::schema::Model,
414 {
415 let t = term.trim();
416 if t.is_empty() {
417 return None;
418 }
419 let mut exprs = self.columns.iter().filter_map(|c| c.search_expr(t));
420 let first = exprs.next()?;
421 Some(exprs.fold(first, |acc, e| acc.or(e)))
422 }
423
424 pub(crate) fn order_by(&self, descending: bool) -> Option<OrderByExpr>
426 where
427 M: toasty::schema::Model,
428 {
429 self.columns.iter().find_map(|c| c.order_by(descending))
430 }
431
432 pub(crate) fn order_bys_for(&self, state: &TableState) -> Vec<OrderByExpr>
434 where
435 M: toasty::schema::Model,
436 {
437 if let Some(sort) = &state.sort
438 && let Some(col) = self
439 .columns
440 .iter()
441 .find(|c| c.is_sortable() && c.name() == sort.column)
442 && let Some(ord) = col.order_by(sort.descending)
443 {
444 return vec![ord];
445 }
446 let out: Vec<OrderByExpr> = self.order_by(false).into_iter().collect();
447 if out.is_empty() {
448 return crate::toasty_compat::pk::pk_order_bys::<M>();
449 }
450 out
451 }
452
453 pub(crate) fn apply_declaration(
455 &self,
456 db: &toasty::Db,
457 mut query: toasty::stmt::Query<List<M>>,
458 state: &TableState,
459 ) -> toasty::stmt::Query<List<M>>
460 where
461 M: toasty::schema::Model,
462 {
463 if let Some(term) = &state.search
464 && let Some(expr) = self.search_expr(term)
465 {
466 query = query.filter(crate::toasty_compat::case_insensitive_search(db, expr));
467 }
468 if let Some(expr) = self.filter_expr(state) {
469 query = query.filter(expr);
470 }
471 for ord in self.order_bys_for(state) {
472 query = query.order_by(ord);
473 }
474 query
475 }
476
477 pub(crate) fn search_enabled(&self) -> bool
479 where
480 M: toasty::schema::Model,
481 {
482 !self.hide_search && self.columns.iter().any(|c| c.is_searchable())
483 }
484
485 pub(crate) fn filter_bar_enabled(&self) -> bool {
487 !self.hide_filter_bar && !self.filters.is_empty()
488 }
489}
490
491#[cfg(test)]
492mod tests;