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