1use std::{num::NonZeroUsize, sync::Arc};
9
10use derive_where::derive_where;
11use toasty::stmt::List;
12use topcoat::context::Cx;
13
14use self::filter::BoxFilter;
15pub(crate) use self::{applied::ignored_reason, choice::ColumnChoice};
16use crate::DeclarationErrorKind;
17pub(crate) use crate::column::{BoxColumn, Column, include_relations, restrict_relations};
18
19mod applied;
20mod choice;
21mod export;
22pub(crate) mod filter;
23mod group;
24mod mask;
25pub(crate) mod model;
26mod page;
27mod row_color;
28mod row_link;
29mod search;
30mod sort;
31mod state;
32mod summary;
33mod tab;
34
35use self::sort::FieldOrder;
36pub use self::{
37 filter::{
38 DateFilter, FilterValue, IntoFilters, NumberFilter, PresenceFilter, QueryFilter,
39 SelectFilter, TernaryFilter, TextFilter, TextMatch,
40 },
41 model::ModelColumns,
42 page::{DEFAULT_PAGE_SIZE, DEFAULT_PER_PAGE_OPTIONS, PagePosition, Pagination, TablePage},
43 row_link::RowLink,
44 state::{Cursor, Sort, TableState},
45 summary::Summary,
46 tab::ListTab,
47};
48pub(crate) use self::{
49 group::GroupDef,
50 state::{bulk_key, bulk_token, group_header_dom_id, query_of, request_query, row_dom_id},
51};
52pub(crate) use crate::{
53 column::{ColumnToggle, ColumnWidth, IntoColumns, contains_expr},
54 display::Alignment,
55};
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
67#[non_exhaustive]
68pub enum ActionLayout {
69 #[default]
72 Inline,
73 Menu,
75}
76
77#[derive_where(Clone)]
123pub struct Table<M> {
124 columns: Vec<BoxColumn<M>>,
125 misdeclared: Vec<DeclarationErrorKind>,
127 filters: Vec<BoxFilter<M>>,
128 groupings: Vec<GroupDef<M>>,
129 row_color: row_color::RowColor<M>,
130 row_links: Vec<RowLink<M>>,
131 tabs: Vec<ListTab<M>>,
132 summaries: Vec<Summary<M>>,
133 key: fn(&M) -> String,
135 addressable: bool,
138 page_size: NonZeroUsize,
139 pagination: Pagination,
140 action_layout: ActionLayout,
141 per_page_options: Vec<usize>,
143 default_sort: Option<Sort>,
145 default_field: Option<FieldOrder>,
148 hide_search: bool,
149 hide_filter_menu: bool,
150 deferred_filters: bool,
152}
153
154impl<M> std::fmt::Debug for Table<M> {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.debug_struct("Table")
157 .field(
158 "columns",
159 &self.columns.iter().map(|c| c.name()).collect::<Vec<_>>(),
160 )
161 .field("filters", &self.filters.len())
162 .field(
163 "groupings",
164 &self
165 .groupings
166 .iter()
167 .map(GroupDef::name)
168 .collect::<Vec<_>>(),
169 )
170 .field(
171 "tabs",
172 &self.tabs.iter().map(ListTab::key).collect::<Vec<_>>(),
173 )
174 .field("page_size", &self.page_size)
175 .field("pagination", &self.pagination)
176 .field("default_sort", &self.default_sort)
177 .field("hide_search", &self.hide_search)
178 .field("hide_filter_menu", &self.hide_filter_menu)
179 .field("deferred_filters", &self.deferred_filters)
180 .finish()
181 }
182}
183
184impl<M> Table<M> {
185 pub fn new(cols: impl IntoColumns<M>) -> Self
189 where
190 M: toasty::schema::Model + toasty::stmt::IntoExpr<M>,
191 {
192 Self {
193 columns: cols.into_columns(),
194 misdeclared: Vec::new(),
195 filters: Vec::new(),
196 groupings: Vec::new(),
197 row_color: row_color::RowColor::default(),
198 row_links: Vec::new(),
199 tabs: Vec::new(),
200 summaries: Vec::new(),
201 key: crate::toasty_compat::pk::pk_text::<M>,
202 addressable: !crate::toasty_compat::pk::pk_is_composite::<M>(),
203 page_size: DEFAULT_PAGE_SIZE,
204 pagination: Pagination::default(),
205 action_layout: ActionLayout::default(),
206 per_page_options: DEFAULT_PER_PAGE_OPTIONS.to_vec(),
207 default_sort: None,
208 default_field: None,
209 hide_search: false,
210 hide_filter_menu: false,
211 deferred_filters: false,
212 }
213 }
214
215 pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
217 self.columns.push(Arc::new(column));
218 self
219 }
220
221 pub fn filters(mut self, filters: impl IntoFilters<M>) -> Self
224 where
225 M: toasty::schema::Model,
226 {
227 self.filters.extend(filters.into_filters());
228 self
229 }
230
231 pub(crate) fn has_toggleable_columns(&self) -> bool {
233 self.columns.iter().any(|c| c.toggle().is_toggleable())
234 }
235
236 pub(crate) fn declared_columns(&self) -> &[BoxColumn<M>] {
238 &self.columns
239 }
240
241 pub(crate) fn only(&self, names: &[String]) -> Self {
244 let mut table = self.clone();
245 table.columns = names
246 .iter()
247 .filter_map(|name| self.columns.iter().find(|column| column.name() == name))
248 .cloned()
249 .collect();
250 if table
251 .default_sort
252 .as_ref()
253 .is_some_and(|sort| !table.orders_by(sort))
254 {
255 table.default_sort = None;
256 }
257 table
258 }
259
260 pub(crate) fn push_filter(&mut self, filter: BoxFilter<M>) {
262 self.filters.push(filter);
263 }
264
265 pub(crate) fn declared_filters(&self) -> &[BoxFilter<M>] {
267 &self.filters
268 }
269
270 pub(crate) fn include_relations(
272 &self,
273 query: toasty::stmt::Query<List<M>>,
274 ) -> toasty::stmt::Query<List<M>>
275 where
276 M: toasty::schema::Model,
277 {
278 include_relations(query, &self.columns)
279 }
280
281 pub(crate) fn includes_relations(&self) -> bool {
283 self.columns
284 .iter()
285 .any(|column| !column.includes().into_vec().is_empty())
286 }
287
288 pub(crate) fn read_bare(&mut self, bare: fn(&M) -> M)
292 where
293 M: toasty::schema::Model + 'static,
294 {
295 if !self.includes_relations() {
296 return;
297 }
298 self.bare_row_color(bare);
299 self.bare_row_links(bare);
300 self.columns = std::mem::take(&mut self.columns)
301 .into_iter()
302 .map(|column| crate::column::bare_column(column, bare))
303 .collect();
304 }
305
306 pub(crate) async fn restrict_relations(
309 &self,
310 cx: &topcoat::context::Cx,
311 rows: &[M],
312 ex: &mut dyn toasty::Executor,
313 ) -> topcoat::Result<()> {
314 restrict_relations(cx, &self.columns, rows, ex).await
315 }
316
317 pub fn pagination(mut self, pagination: Pagination) -> Self {
319 self.pagination = pagination;
320 self
321 }
322
323 pub fn action_layout(mut self, layout: ActionLayout) -> Self {
325 self.action_layout = layout;
326 self
327 }
328
329 pub fn per_page_options(mut self, options: impl IntoIterator<Item = usize>) -> Self {
332 self.per_page_options = options.into_iter().filter(|size| *size > 0).collect();
333 self
334 }
335
336 pub fn page_size(mut self, size: usize) -> Self {
338 match NonZeroUsize::new(size) {
339 Some(size) => self.page_size = size,
340 None => self.misdeclared.push(DeclarationErrorKind::ZeroPageSize),
341 }
342 self
343 }
344
345 pub(crate) fn unavailable_sources(
347 &self,
348 cx: &topcoat::context::Cx,
349 ) -> Vec<DeclarationErrorKind> {
350 let mut errors = crate::column::unavailable_sources(cx, self.columns.iter().map(|c| &**c));
351 errors.extend(self.filters.iter().filter_map(|filter| {
352 filter.internals().unavailable_source(cx).map(|source| {
353 DeclarationErrorKind::UnregisteredOptionSource {
354 field: filter.name().to_string(),
355 source,
356 }
357 })
358 }));
359 errors
360 }
361
362 pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
364 let mut errors = self.misdeclared.clone();
365 errors.extend(self.grouping_errors());
366 errors.extend(self.tab_errors());
367 if self.columns.is_empty() {
368 errors.push(DeclarationErrorKind::NoColumns);
369 }
370 if let Some(sort) = &self.default_sort
371 && !self.orders_by(sort)
372 {
373 errors.push(DeclarationErrorKind::UnsortableDefaultSort {
374 column: sort.column.clone(),
375 });
376 }
377 let mut seen = std::collections::HashSet::with_capacity(self.columns.len());
378 for column in &self.columns {
379 match column.internals().misdeclared() {
380 Some(error) => errors.push(error),
381 None if !seen.insert(column.name()) => {
382 errors.push(DeclarationErrorKind::DuplicateColumn {
383 name: column.name().to_string(),
384 });
385 }
386 None => {}
387 }
388 }
389 let mut seen = std::collections::HashSet::with_capacity(self.filters.len());
390 for filter in &self.filters {
391 match filter.internals().misdeclared() {
392 Some(error) => errors.push(error),
393 None if filter.name().contains('.') => {
394 errors.push(DeclarationErrorKind::DottedFilterName {
395 name: filter.name().to_string(),
396 });
397 }
398 None if !seen.insert(filter.name()) => {
399 errors.push(DeclarationErrorKind::DuplicateFilter {
400 name: filter.name().to_string(),
401 });
402 }
403 None => {}
404 }
405 }
406 errors
407 }
408
409 pub fn per_page(&self) -> usize {
411 self.page_size.get()
412 }
413
414 pub(crate) fn pagination_mode(&self) -> Pagination {
416 self.pagination
417 }
418
419 pub(crate) fn row_action_layout(&self) -> ActionLayout {
421 self.action_layout
422 }
423
424 pub(crate) fn page_sizes(&self) -> Vec<usize> {
427 if self.per_page_options.is_empty() || self.pagination == Pagination::Cursor {
428 return Vec::new();
429 }
430 let mut sizes = self.per_page_options.clone();
431 sizes.push(self.per_page());
432 sizes.sort_unstable();
433 sizes.dedup();
434 sizes
435 }
436
437 pub(crate) fn page_size_for(&self, state: &TableState) -> usize {
440 state
441 .per_page
442 .and_then(|asked| usize::try_from(asked).ok())
443 .filter(|asked| self.page_sizes().contains(asked))
444 .unwrap_or_else(|| self.per_page())
445 }
446
447 pub(crate) fn key_of(&self, record: &M) -> String {
449 (self.key)(record)
450 }
451
452 pub(crate) fn spell(&self, cx: &Cx, record: &M) -> Vec<(String, crate::Text, String)> {
454 self.columns
455 .iter()
456 .map(|column| {
457 (
458 column.name().to_string(),
459 column.label(),
460 column.text(cx, record),
461 )
462 })
463 .collect()
464 }
465
466 pub(crate) fn is_addressable(&self) -> bool {
468 self.addressable
469 }
470
471 pub fn hide_search(mut self) -> Self {
473 self.hide_search = true;
474 self
475 }
476
477 pub fn hide_filter_menu(mut self) -> Self {
479 self.hide_filter_menu = true;
480 self
481 }
482
483 pub fn deferred_filters(mut self) -> Self {
488 self.deferred_filters = true;
489 self
490 }
491
492 pub(crate) fn filters_deferred(&self) -> bool {
494 self.deferred_filters
495 }
496
497 pub(crate) fn apply_declaration(
499 &self,
500 cx: &Cx,
501 db: &toasty::Db,
502 query: toasty::stmt::Query<List<M>>,
503 state: &TableState,
504 refused: &[String],
505 ) -> toasty::stmt::Query<List<M>>
506 where
507 M: toasty::schema::Model,
508 {
509 self.order(self.narrow(cx, db, query, state, refused), state)
510 }
511
512 pub(crate) fn narrow(
515 &self,
516 cx: &Cx,
517 db: &toasty::Db,
518 query: toasty::stmt::Query<List<M>>,
519 state: &TableState,
520 refused: &[String],
521 ) -> toasty::stmt::Query<List<M>>
522 where
523 M: toasty::schema::Model,
524 {
525 self.narrow_tab(self.narrow_untabbed(cx, db, query, state, refused), state)
526 }
527
528 fn narrow_untabbed(
530 &self,
531 cx: &Cx,
532 db: &toasty::Db,
533 mut query: toasty::stmt::Query<List<M>>,
534 state: &TableState,
535 refused: &[String],
536 ) -> toasty::stmt::Query<List<M>>
537 where
538 M: toasty::schema::Model,
539 {
540 if let Some(term) = &state.search
541 && let Some(expr) = self.search_expr(term)
542 {
543 query = query.filter(crate::toasty_compat::case_insensitive_search(db, expr));
544 }
545 if let Some(expr) = self.filter_expr(cx, state, refused) {
546 query = query.filter(expr);
547 }
548 query
549 }
550
551 pub(crate) fn filter_menu_enabled(&self) -> bool {
553 !self.hide_filter_menu && !self.filters.is_empty()
554 }
555}
556
557#[cfg(test)]
558mod tests;