1use std::{marker::PhantomData, num::NonZeroUsize, sync::Arc};
9
10use derive_where::derive_where;
11use toasty::stmt::{Expr, List};
12use topcoat::context::Cx;
13
14pub(crate) use self::choice::ColumnChoice;
15use self::filter::BoxFilter;
16pub(crate) use crate::column::{BoxColumn, Column, include_relations, restrict_relations};
17use crate::{DeclarationErrorKind, Lens, dom::ignored, schema::Binding, values::FormScalar};
18
19mod choice;
20mod export;
21pub(crate) mod filter;
22mod mask;
23pub(crate) mod model;
24mod page;
25mod search;
26mod sort;
27mod state;
28
29pub use self::{
30 filter::{DateFilter, FilterValue, IntoFilters, QueryFilter, SelectFilter, TernaryFilter},
31 model::ModelColumns,
32 page::{DEFAULT_PAGE_SIZE, DEFAULT_PER_PAGE_OPTIONS, PagePosition, Pagination, TablePage},
33 state::{Cursor, Sort, TableState},
34};
35pub(crate) use self::{
36 page::{Past, row_exists_past},
37 state::{bulk_key, bulk_token, group_header_dom_id, query_of, request_query, row_dom_id},
38};
39pub(crate) use crate::{
40 column::{ColumnToggle, ColumnWidth, IntoColumns, contains_expr},
41 display::Alignment,
42};
43
44pub(crate) type GroupKey<M> = Arc<dyn Fn(&M) -> crate::Text + Send + Sync>;
46
47#[derive_where(Clone)]
49pub(crate) struct GroupDef<M> {
50 binding: Binding,
51 key: GroupKey<M>,
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub enum ActionLayout {
65 #[default]
68 Inline,
69 Menu,
71}
72
73#[derive_where(Clone)]
116pub struct Table<M> {
117 columns: Vec<BoxColumn<M>>,
118 misdeclared: Vec<DeclarationErrorKind>,
120 filters: Vec<BoxFilter<M>>,
121 group_by: Option<GroupDef<M>>,
122 key: fn(&M) -> String,
124 addressable: bool,
127 page_size: NonZeroUsize,
128 pagination: Pagination,
129 action_layout: ActionLayout,
130 per_page_options: Vec<usize>,
132 default_sort: Option<Sort>,
134 hide_search: bool,
135 hide_filter_menu: bool,
136 _marker: PhantomData<M>,
137}
138
139impl<M> std::fmt::Debug for Table<M> {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 f.debug_struct("Table")
142 .field(
143 "columns",
144 &self.columns.iter().map(|c| c.name()).collect::<Vec<_>>(),
145 )
146 .field("filters", &self.filters.len())
147 .field("group_by", &self.group_by.is_some())
148 .field("page_size", &self.page_size)
149 .field("pagination", &self.pagination)
150 .field("default_sort", &self.default_sort)
151 .field("hide_search", &self.hide_search)
152 .field("hide_filter_menu", &self.hide_filter_menu)
153 .finish()
154 }
155}
156
157impl<M> Table<M> {
158 pub fn new(cols: impl IntoColumns<M>) -> Self
162 where
163 M: toasty::schema::Model + toasty::stmt::IntoExpr<M>,
164 {
165 Self {
166 columns: cols.into_columns(),
167 misdeclared: Vec::new(),
168 filters: Vec::new(),
169 group_by: None,
170 key: crate::toasty_compat::pk::pk_text::<M>,
171 addressable: !crate::toasty_compat::pk::pk_is_composite::<M>(),
172 page_size: DEFAULT_PAGE_SIZE,
173 pagination: Pagination::default(),
174 action_layout: ActionLayout::default(),
175 per_page_options: DEFAULT_PER_PAGE_OPTIONS.to_vec(),
176 default_sort: None,
177 hide_search: false,
178 hide_filter_menu: false,
179 _marker: PhantomData,
180 }
181 }
182
183 pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
185 self.columns.push(Arc::new(column));
186 self
187 }
188
189 pub fn filters(mut self, filters: impl IntoFilters<M>) -> Self
191 where
192 M: toasty::schema::Model,
193 {
194 let filters = filters.into_filters();
195 self.filters = filters;
196 self
197 }
198
199 pub(crate) fn has_toggleable_columns(&self) -> bool {
201 self.columns.iter().any(|c| c.toggle().is_toggleable())
202 }
203
204 pub(crate) fn declared_columns(&self) -> &[BoxColumn<M>] {
206 &self.columns
207 }
208
209 pub(crate) fn push_filter(&mut self, filter: BoxFilter<M>) {
211 self.filters.push(filter);
212 }
213
214 pub(crate) fn declared_filters(&self) -> &[BoxFilter<M>] {
216 &self.filters
217 }
218
219 pub(crate) fn include_relations(
221 &self,
222 query: toasty::stmt::Query<List<M>>,
223 ) -> toasty::stmt::Query<List<M>>
224 where
225 M: toasty::schema::Model,
226 {
227 include_relations(query, &self.columns)
228 }
229
230 pub(crate) async fn restrict_relations(
233 &self,
234 cx: &topcoat::context::Cx,
235 rows: &[M],
236 ex: &mut dyn toasty::Executor,
237 ) -> topcoat::Result<()> {
238 restrict_relations(cx, &self.columns, rows, ex).await
239 }
240
241 pub(crate) async fn refused_filters(
249 &self,
250 cx: &topcoat::context::Cx,
251 state: &TableState,
252 ) -> topcoat::Result<Vec<String>> {
253 let mut refused = Vec::new();
254 for f in &self.filters {
255 if let Some(value) = state.filters.get(f.name())
256 && !f.admits(cx, value).await?
257 {
258 refused.push(f.name().to_string());
259 }
260 }
261 Ok(refused)
262 }
263
264 pub(crate) fn filter_expr(
266 &self,
267 cx: &Cx,
268 state: &TableState,
269 refused: &[String],
270 ) -> Option<Expr<bool>>
271 where
272 M: toasty::schema::Model,
273 {
274 let mut exprs = Vec::new();
275 for f in &self.filters {
276 if let Some(v) = state.filters.get(f.name())
277 && !refused.iter().any(|name| name == f.name())
278 && let Some(e) = f.to_expr(cx, v)
279 {
280 exprs.push(e);
281 }
282 }
283 if exprs.is_empty() {
284 None
285 } else {
286 let mut iter = exprs.into_iter();
287 let first = iter.next().unwrap();
288 Some(iter.fold(first, |acc, e| acc.and(e)))
289 }
290 }
291
292 pub(crate) fn unapplied_filters(
298 &self,
299 cx: &Cx,
300 state: &TableState,
301 refused: &[String],
302 ) -> Vec<(String, &'static str)>
303 where
304 M: toasty::schema::Model,
305 {
306 let mut out = Vec::new();
307 for (key, value) in &state.filters {
308 match self.filters.iter().find(|f| f.name() == key) {
309 None => out.push((state.spell_filter(key, value), ignored::UNKNOWN)),
310 Some(f)
311 if refused.iter().any(|name| name == key)
312 || (f.to_expr(cx, value).is_none() && !f.is_noop_value(value)) =>
313 {
314 out.push((state.spell_filter(key, value), ignored::INVALID))
315 }
316 Some(_) => {}
317 }
318 }
319 if state.filters_dropped {
320 out.push(("dropped filters".to_string(), ignored::DROPPED));
321 }
322 out.sort();
323 out
324 }
325
326 pub fn group_by<T>(mut self, lens: Lens<M, T>) -> Self
328 where
329 M: toasty::schema::Model + Send + Sync + 'static,
330 T: FormScalar + Send + Sync + 'static,
331 {
332 self.group_by = Some(GroupDef {
333 binding: Binding::of(lens.path()),
334 key: Arc::new(move |record| lens.read(record).to_label()),
335 });
336 self
337 }
338
339 pub(crate) fn effective_group_key(&self, state: &TableState) -> Option<GroupKey<M>> {
341 match (&self.group_by, &state.group_by) {
342 (Some(def), Some(want)) if def.binding.name() == want => Some(def.key.clone()),
343 _ => None,
344 }
345 }
346
347 pub(crate) fn normalize_state(&self, state: &TableState) -> TableState {
349 let mut out = state.clone();
350 if self.group_by.as_ref().map(|def| def.binding.name()) != out.group_by.as_deref() {
351 out.group_by = None;
352 }
353 out
354 }
355
356 pub fn pagination(mut self, pagination: Pagination) -> Self {
358 self.pagination = pagination;
359 self
360 }
361
362 pub fn action_layout(mut self, layout: ActionLayout) -> Self {
364 self.action_layout = layout;
365 self
366 }
367
368 pub fn per_page_options(mut self, options: impl IntoIterator<Item = usize>) -> Self {
371 self.per_page_options = options.into_iter().filter(|size| *size > 0).collect();
372 self
373 }
374
375 pub fn paginate(mut self, per_page: usize) -> Self {
377 match NonZeroUsize::new(per_page) {
378 Some(size) => self.page_size = size,
379 None => self.misdeclared.push(DeclarationErrorKind::ZeroPageSize),
380 }
381 self
382 }
383
384 pub(crate) fn unavailable_sources(
386 &self,
387 cx: &topcoat::context::Cx,
388 ) -> Vec<DeclarationErrorKind> {
389 let mut errors = crate::column::unavailable_sources(cx, self.columns.iter().map(|c| &**c));
390 errors.extend(self.filters.iter().filter_map(|filter| {
391 filter.unavailable_source(cx).map(|source| {
392 DeclarationErrorKind::UnregisteredOptionSource {
393 field: filter.name().to_string(),
394 source,
395 }
396 })
397 }));
398 errors
399 }
400
401 pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
403 let mut errors = self.misdeclared.clone();
404 errors.extend(
405 self.group_by
406 .as_ref()
407 .and_then(|def| def.binding.misdeclared()),
408 );
409 if self.columns.is_empty() {
410 errors.push(DeclarationErrorKind::NoColumns);
411 }
412 if let Some(sort) = &self.default_sort
413 && self.sortable_column(&sort.column).is_none()
414 {
415 errors.push(DeclarationErrorKind::UnsortableDefaultSort {
416 column: sort.column.clone(),
417 });
418 }
419 let mut seen = std::collections::HashSet::with_capacity(self.columns.len());
420 for column in &self.columns {
421 match column.misdeclared() {
422 Some(error) => errors.push(error),
423 None if !seen.insert(column.name()) => {
424 errors.push(DeclarationErrorKind::DuplicateColumn {
425 name: column.name().to_string(),
426 });
427 }
428 None => {}
429 }
430 }
431 let mut seen = std::collections::HashSet::with_capacity(self.filters.len());
432 for filter in &self.filters {
433 match filter.misdeclared() {
434 Some(error) => errors.push(error),
435 None if filter.name().contains('.') => {
436 errors.push(DeclarationErrorKind::DottedFilterName {
437 name: filter.name().to_string(),
438 });
439 }
440 None if !seen.insert(filter.name()) => {
441 errors.push(DeclarationErrorKind::DuplicateFilter {
442 name: filter.name().to_string(),
443 });
444 }
445 None => {}
446 }
447 }
448 errors
449 }
450
451 pub fn page_size(&self) -> usize {
453 self.page_size.get()
454 }
455
456 pub fn pagination_mode(&self) -> Pagination {
458 self.pagination
459 }
460
461 pub fn row_action_layout(&self) -> ActionLayout {
463 self.action_layout
464 }
465
466 pub(crate) fn page_sizes(&self) -> Vec<usize> {
469 if self.per_page_options.is_empty() || self.pagination == Pagination::Cursor {
470 return Vec::new();
471 }
472 let mut sizes = self.per_page_options.clone();
473 sizes.push(self.page_size());
474 sizes.sort_unstable();
475 sizes.dedup();
476 sizes
477 }
478
479 pub(crate) fn page_size_for(&self, state: &TableState) -> usize {
482 state
483 .per_page
484 .and_then(|asked| usize::try_from(asked).ok())
485 .filter(|asked| self.page_sizes().contains(asked))
486 .unwrap_or_else(|| self.page_size())
487 }
488
489 pub(crate) fn key_of(&self, record: &M) -> String {
491 (self.key)(record)
492 }
493
494 pub(crate) fn is_addressable(&self) -> bool {
496 self.addressable
497 }
498
499 pub fn hide_search(mut self) -> Self {
501 self.hide_search = true;
502 self
503 }
504
505 pub fn hide_filter_menu(mut self) -> Self {
507 self.hide_filter_menu = true;
508 self
509 }
510
511 pub(crate) fn apply_declaration(
513 &self,
514 cx: &Cx,
515 db: &toasty::Db,
516 query: toasty::stmt::Query<List<M>>,
517 state: &TableState,
518 refused: &[String],
519 ) -> toasty::stmt::Query<List<M>>
520 where
521 M: toasty::schema::Model,
522 {
523 self.order(self.narrow(cx, db, query, state, refused), state)
524 }
525
526 pub(crate) fn narrow(
529 &self,
530 cx: &Cx,
531 db: &toasty::Db,
532 mut query: toasty::stmt::Query<List<M>>,
533 state: &TableState,
534 refused: &[String],
535 ) -> toasty::stmt::Query<List<M>>
536 where
537 M: toasty::schema::Model,
538 {
539 if let Some(term) = &state.search
540 && let Some(expr) = self.search_expr(term)
541 {
542 query = query.filter(crate::toasty_compat::case_insensitive_search(db, expr));
543 }
544 if let Some(expr) = self.filter_expr(cx, state, refused) {
545 query = query.filter(expr);
546 }
547 query
548 }
549
550 pub(crate) fn filter_menu_enabled(&self) -> bool {
552 !self.hide_filter_menu && !self.filters.is_empty()
553 }
554}
555
556pub(crate) fn ignored_reason(kind: &str) -> crate::Text {
559 match kind {
560 ignored::UNKNOWN => crate::text!("tablo-filter-ignored-unknown"),
561 ignored::DROPPED => crate::text!(
562 "tablo-filter-ignored-dropped",
563 filters = state::MAX_FILTERS,
564 values = state::MAX_FILTER_VALUES,
565 bytes = state::MAX_FILTER_LEN,
566 ),
567 _ => crate::text!("tablo-filter-ignored-invalid"),
568 }
569}
570
571#[cfg(test)]
572mod tests;