use std::{num::NonZeroUsize, sync::Arc};
use derive_where::derive_where;
use toasty::stmt::List;
use topcoat::context::Cx;
use self::filter::BoxFilter;
pub(crate) use self::{applied::ignored_reason, choice::ColumnChoice};
use crate::DeclarationErrorKind;
pub(crate) use crate::column::{BoxColumn, Column, include_relations, restrict_relations};
mod applied;
mod choice;
mod export;
pub(crate) mod filter;
mod group;
mod mask;
pub(crate) mod model;
mod page;
mod row_color;
mod row_link;
mod search;
mod sort;
mod state;
mod summary;
mod tab;
use self::sort::FieldOrder;
pub use self::{
filter::{
DateFilter, FilterValue, IntoFilters, NumberFilter, PresenceFilter, QueryFilter,
SelectFilter, TernaryFilter, TextFilter, TextMatch,
},
model::ModelColumns,
page::{DEFAULT_PAGE_SIZE, DEFAULT_PER_PAGE_OPTIONS, PagePosition, Pagination, TablePage},
row_link::RowLink,
state::{Cursor, Sort, TableState},
summary::Summary,
tab::ListTab,
};
pub(crate) use self::{
group::GroupDef,
state::{bulk_key, bulk_token, group_header_dom_id, query_of, request_query, row_dom_id},
};
pub(crate) use crate::{
column::{ColumnToggle, ColumnWidth, IntoColumns, contains_expr},
display::Alignment,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ActionLayout {
#[default]
Inline,
Menu,
}
#[derive_where(Clone)]
pub struct Table<M> {
columns: Vec<BoxColumn<M>>,
misdeclared: Vec<DeclarationErrorKind>,
filters: Vec<BoxFilter<M>>,
groupings: Vec<GroupDef<M>>,
row_color: row_color::RowColor<M>,
row_links: Vec<RowLink<M>>,
tabs: Vec<ListTab<M>>,
summaries: Vec<Summary<M>>,
key: fn(&M) -> String,
addressable: bool,
page_size: NonZeroUsize,
pagination: Pagination,
action_layout: ActionLayout,
per_page_options: Vec<usize>,
default_sort: Option<Sort>,
default_field: Option<FieldOrder>,
hide_search: bool,
hide_filter_menu: bool,
deferred_filters: bool,
}
impl<M> std::fmt::Debug for Table<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Table")
.field(
"columns",
&self.columns.iter().map(|c| c.name()).collect::<Vec<_>>(),
)
.field("filters", &self.filters.len())
.field(
"groupings",
&self
.groupings
.iter()
.map(GroupDef::name)
.collect::<Vec<_>>(),
)
.field(
"tabs",
&self.tabs.iter().map(ListTab::key).collect::<Vec<_>>(),
)
.field("page_size", &self.page_size)
.field("pagination", &self.pagination)
.field("default_sort", &self.default_sort)
.field("hide_search", &self.hide_search)
.field("hide_filter_menu", &self.hide_filter_menu)
.field("deferred_filters", &self.deferred_filters)
.finish()
}
}
impl<M> Table<M> {
pub fn new(cols: impl IntoColumns<M>) -> Self
where
M: toasty::schema::Model + toasty::stmt::IntoExpr<M>,
{
Self {
columns: cols.into_columns(),
misdeclared: Vec::new(),
filters: Vec::new(),
groupings: Vec::new(),
row_color: row_color::RowColor::default(),
row_links: Vec::new(),
tabs: Vec::new(),
summaries: Vec::new(),
key: crate::toasty_compat::pk::pk_text::<M>,
addressable: !crate::toasty_compat::pk::pk_is_composite::<M>(),
page_size: DEFAULT_PAGE_SIZE,
pagination: Pagination::default(),
action_layout: ActionLayout::default(),
per_page_options: DEFAULT_PER_PAGE_OPTIONS.to_vec(),
default_sort: None,
default_field: None,
hide_search: false,
hide_filter_menu: false,
deferred_filters: false,
}
}
pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
self.columns.push(Arc::new(column));
self
}
pub fn filters(mut self, filters: impl IntoFilters<M>) -> Self
where
M: toasty::schema::Model,
{
self.filters.extend(filters.into_filters());
self
}
pub(crate) fn has_toggleable_columns(&self) -> bool {
self.columns.iter().any(|c| c.toggle().is_toggleable())
}
pub(crate) fn declared_columns(&self) -> &[BoxColumn<M>] {
&self.columns
}
pub(crate) fn only(&self, names: &[String]) -> Self {
let mut table = self.clone();
table.columns = names
.iter()
.filter_map(|name| self.columns.iter().find(|column| column.name() == name))
.cloned()
.collect();
if table
.default_sort
.as_ref()
.is_some_and(|sort| !table.orders_by(sort))
{
table.default_sort = None;
}
table
}
pub(crate) fn push_filter(&mut self, filter: BoxFilter<M>) {
self.filters.push(filter);
}
pub(crate) fn declared_filters(&self) -> &[BoxFilter<M>] {
&self.filters
}
pub(crate) fn include_relations(
&self,
query: toasty::stmt::Query<List<M>>,
) -> toasty::stmt::Query<List<M>>
where
M: toasty::schema::Model,
{
include_relations(query, &self.columns)
}
pub(crate) fn includes_relations(&self) -> bool {
self.columns
.iter()
.any(|column| !column.includes().into_vec().is_empty())
}
pub(crate) fn read_bare(&mut self, bare: fn(&M) -> M)
where
M: toasty::schema::Model + 'static,
{
if !self.includes_relations() {
return;
}
self.bare_row_color(bare);
self.bare_row_links(bare);
self.columns = std::mem::take(&mut self.columns)
.into_iter()
.map(|column| crate::column::bare_column(column, bare))
.collect();
}
pub(crate) async fn restrict_relations(
&self,
cx: &topcoat::context::Cx,
rows: &[M],
ex: &mut dyn toasty::Executor,
) -> topcoat::Result<()> {
restrict_relations(cx, &self.columns, rows, ex).await
}
pub fn pagination(mut self, pagination: Pagination) -> Self {
self.pagination = pagination;
self
}
pub fn action_layout(mut self, layout: ActionLayout) -> Self {
self.action_layout = layout;
self
}
pub fn per_page_options(mut self, options: impl IntoIterator<Item = usize>) -> Self {
self.per_page_options = options.into_iter().filter(|size| *size > 0).collect();
self
}
pub fn page_size(mut self, size: usize) -> Self {
match NonZeroUsize::new(size) {
Some(size) => self.page_size = size,
None => self.misdeclared.push(DeclarationErrorKind::ZeroPageSize),
}
self
}
pub(crate) fn unavailable_sources(
&self,
cx: &topcoat::context::Cx,
) -> Vec<DeclarationErrorKind> {
let mut errors = crate::column::unavailable_sources(cx, self.columns.iter().map(|c| &**c));
errors.extend(self.filters.iter().filter_map(|filter| {
filter.internals().unavailable_source(cx).map(|source| {
DeclarationErrorKind::UnregisteredOptionSource {
field: filter.name().to_string(),
source,
}
})
}));
errors
}
pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
let mut errors = self.misdeclared.clone();
errors.extend(self.grouping_errors());
errors.extend(self.tab_errors());
if self.columns.is_empty() {
errors.push(DeclarationErrorKind::NoColumns);
}
if let Some(sort) = &self.default_sort
&& !self.orders_by(sort)
{
errors.push(DeclarationErrorKind::UnsortableDefaultSort {
column: sort.column.clone(),
});
}
let mut seen = std::collections::HashSet::with_capacity(self.columns.len());
for column in &self.columns {
match column.internals().misdeclared() {
Some(error) => errors.push(error),
None if !seen.insert(column.name()) => {
errors.push(DeclarationErrorKind::DuplicateColumn {
name: column.name().to_string(),
});
}
None => {}
}
}
let mut seen = std::collections::HashSet::with_capacity(self.filters.len());
for filter in &self.filters {
match filter.internals().misdeclared() {
Some(error) => errors.push(error),
None if filter.name().contains('.') => {
errors.push(DeclarationErrorKind::DottedFilterName {
name: filter.name().to_string(),
});
}
None if !seen.insert(filter.name()) => {
errors.push(DeclarationErrorKind::DuplicateFilter {
name: filter.name().to_string(),
});
}
None => {}
}
}
errors
}
pub fn per_page(&self) -> usize {
self.page_size.get()
}
pub(crate) fn pagination_mode(&self) -> Pagination {
self.pagination
}
pub(crate) fn row_action_layout(&self) -> ActionLayout {
self.action_layout
}
pub(crate) fn page_sizes(&self) -> Vec<usize> {
if self.per_page_options.is_empty() || self.pagination == Pagination::Cursor {
return Vec::new();
}
let mut sizes = self.per_page_options.clone();
sizes.push(self.per_page());
sizes.sort_unstable();
sizes.dedup();
sizes
}
pub(crate) fn page_size_for(&self, state: &TableState) -> usize {
state
.per_page
.and_then(|asked| usize::try_from(asked).ok())
.filter(|asked| self.page_sizes().contains(asked))
.unwrap_or_else(|| self.per_page())
}
pub(crate) fn key_of(&self, record: &M) -> String {
(self.key)(record)
}
pub(crate) fn spell(&self, cx: &Cx, record: &M) -> Vec<(String, crate::Text, String)> {
self.columns
.iter()
.map(|column| {
(
column.name().to_string(),
column.label(),
column.text(cx, record),
)
})
.collect()
}
pub(crate) fn is_addressable(&self) -> bool {
self.addressable
}
pub fn hide_search(mut self) -> Self {
self.hide_search = true;
self
}
pub fn hide_filter_menu(mut self) -> Self {
self.hide_filter_menu = true;
self
}
pub fn deferred_filters(mut self) -> Self {
self.deferred_filters = true;
self
}
pub(crate) fn filters_deferred(&self) -> bool {
self.deferred_filters
}
pub(crate) fn apply_declaration(
&self,
cx: &Cx,
db: &toasty::Db,
query: toasty::stmt::Query<List<M>>,
state: &TableState,
refused: &[String],
) -> toasty::stmt::Query<List<M>>
where
M: toasty::schema::Model,
{
self.order(self.narrow(cx, db, query, state, refused), state)
}
pub(crate) fn narrow(
&self,
cx: &Cx,
db: &toasty::Db,
query: toasty::stmt::Query<List<M>>,
state: &TableState,
refused: &[String],
) -> toasty::stmt::Query<List<M>>
where
M: toasty::schema::Model,
{
self.narrow_tab(self.narrow_untabbed(cx, db, query, state, refused), state)
}
fn narrow_untabbed(
&self,
cx: &Cx,
db: &toasty::Db,
mut query: toasty::stmt::Query<List<M>>,
state: &TableState,
refused: &[String],
) -> toasty::stmt::Query<List<M>>
where
M: toasty::schema::Model,
{
if let Some(term) = &state.search
&& let Some(expr) = self.search_expr(term)
{
query = query.filter(crate::toasty_compat::case_insensitive_search(db, expr));
}
if let Some(expr) = self.filter_expr(cx, state, refused) {
query = query.filter(expr);
}
query
}
pub(crate) fn filter_menu_enabled(&self) -> bool {
!self.hide_filter_menu && !self.filters.is_empty()
}
}
#[cfg(test)]
mod tests;