use std::sync::Arc;
use derive_where::derive_where;
use toasty::stmt::{Expr, List, OrderByExpr, Path, Query};
use topcoat::{context::Cx, view::*};
use crate::{display::Alignment, schema::Binding};
macro_rules! base_builders {
() => {
pub fn label(mut self, label: impl Into<$crate::Text>) -> Self {
self.base.label = Some(label.into());
self
}
pub fn width(mut self, width: $crate::column::ColumnWidth) -> Self {
self.base.width = width;
self
}
toggle_builders!(base.toggle);
};
(sortable) => {
base_builders!();
pub fn sortable(mut self) -> Self {
self.sortable = true;
self
}
};
}
macro_rules! toggle_builders {
($($field:ident).+) => {
pub fn toggleable(mut self) -> Self {
self.$($field).+ = $crate::column::ColumnToggle::ShownByDefault;
self
}
pub fn hidden_by_default(mut self) -> Self {
self.$($field).+ = $crate::column::ColumnToggle::HiddenByDefault;
self
}
};
}
macro_rules! base_column_methods {
() => {
base_column_methods!(reads);
fn reads(&self) -> Option<$crate::lens::Reads<M>> {
Some(self.base.reads())
}
};
(reads) => {
fn name(&self) -> &str {
self.base.name()
}
fn label(&self) -> $crate::Text {
self.base.label()
}
fn column_width(&self) -> $crate::column::ColumnWidth {
self.base.width
}
fn toggle(&self) -> $crate::column::ColumnToggle {
self.base.toggle
}
fn internals(&self) -> &dyn $crate::column::ColumnInternals<M> {
self
}
};
}
macro_rules! base_internals {
() => {
fn related(&self) -> $crate::column::Related {
$crate::column::Related::None
}
};
(binding) => {
base_internals!();
base_internals!(binding, related);
};
(binding, related) => {
fn misdeclared(&self) -> Option<$crate::DeclarationErrorKind> {
self.base.binding.misdeclared()
}
};
}
pub(crate) type ReadFn<T, V> = Arc<dyn Fn(&T) -> V + Send + Sync>;
fn sort_order<M, T>(path: &Path<M, T>, descending: bool) -> OrderByExpr {
if descending {
path.clone().desc()
} else {
path.clone().asc()
}
}
mod related;
pub use related::Related;
mod bare;
mod boolean;
mod cell;
mod color;
mod computed;
mod date;
mod editable;
mod embedded;
mod file;
mod icon;
mod image;
mod into_columns;
mod list;
mod relation;
mod repeater;
#[cfg(feature = "rich-text")]
mod rich_text;
mod text;
pub(crate) use bare::{bare_column, reads_own_fields};
pub use boolean::BooleanColumn;
pub(crate) use cell::link_href;
pub use color::ColorColumn;
pub(crate) use color::is_css_color;
pub use computed::ComputedColumn;
pub use date::{DateColumn, DateValue};
pub(crate) use editable::Editor;
pub use editable::{SelectColumn, TextInputColumn, ToggleColumn};
pub use embedded::EmbeddedColumn;
pub use file::FileColumn;
pub use icon::IconColumn;
pub use image::ImageColumn;
pub use into_columns::IntoColumns;
pub use list::ListColumn;
pub use relation::{CountColumn, RelationColumn, RelationLens, ToOneRelation};
pub use repeater::RepeaterColumn;
#[cfg(feature = "rich-text")]
pub use rich_text::RichTextColumn;
pub(crate) use sealed::ColumnInternals;
#[cfg(test)]
use text::escape_like_pattern;
pub(crate) use text::{LIKE_ESCAPE, escape_like};
pub use text::{TextColumn, contains_expr};
mod sealed {
use topcoat::{context::Cx, view::BoxView};
use super::{Editor, Related};
pub trait ColumnInternals<M>: Send + Sync {
fn restrict<'a>(
&'a self,
_cx: &'a Cx,
_rows: &'a [M],
_ex: &'a mut dyn toasty::Executor,
) -> crate::BoxFuture<'a, topcoat::Result<()>> {
Box::pin(async { Ok(()) })
}
fn masked(&self, _cx: &Cx) -> bool {
false
}
fn related(&self) -> Related {
Related::Row
}
fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
None
}
fn unavailable_source(&self, _cx: &Cx) -> Option<&'static str> {
None
}
fn edits(&self) -> Option<Editor> {
None
}
fn edit_options(&self) -> Vec<(String, crate::Text)> {
Vec::new()
}
fn editor<'a>(&self, _cx: &'a Cx, _row: &M) -> Option<BoxView<'a>> {
None
}
}
pub struct Defaults;
impl<M> ColumnInternals<M> for Defaults {}
}
pub trait Column<M>: Send + Sync {
fn name(&self) -> &str;
fn label(&self) -> crate::Text;
fn text(&self, cx: &Cx, row: &M) -> String;
fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
crate::schema::value_cell(cx, &self.text(cx, row))
}
fn entry<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
crate::schema::read_only(cx, &self.label(), self.cell(cx, row))
}
fn column_width(&self) -> ColumnWidth {
ColumnWidth::Auto
}
fn toggle(&self) -> ColumnToggle {
ColumnToggle::Fixed
}
fn alignment(&self) -> Alignment {
Alignment::Start
}
fn search_expr(&self, _term: &str) -> Option<Expr<bool>> {
None
}
fn order_by(&self, _descending: bool) -> Option<OrderByExpr> {
None
}
fn includes(&self) -> Includes<M> {
Includes::new()
}
fn reads(&self) -> Option<crate::lens::Reads<M>> {
None
}
#[doc(hidden)]
fn internals(&self) -> &dyn ColumnInternals<M> {
&sealed::Defaults
}
}
#[derive_where(Clone, Debug, Default)]
pub struct Includes<M>(
Vec<crate::toasty_compat::UntypedInclude>,
#[derive_where(skip(Debug))] std::marker::PhantomData<fn() -> M>,
);
impl<M> Includes<M> {
pub fn new() -> Self {
Self::default()
}
pub fn with<T>(mut self, relation: impl Into<toasty::stmt::Include<M, T>>) -> Self {
let include: crate::toasty_compat::UntypedInclude = relation.into().into();
if !self.0.contains(&include) {
self.0.push(include);
}
self
}
pub(crate) fn into_vec(self) -> Vec<crate::toasty_compat::UntypedInclude> {
self.0
}
}
pub(crate) fn unavailable_sources<'c, M: 'c>(
cx: &Cx,
columns: impl IntoIterator<Item = &'c dyn Column<M>>,
) -> Vec<crate::DeclarationErrorKind> {
columns
.into_iter()
.filter_map(|column| {
column.internals().unavailable_source(cx).map(|source| {
crate::DeclarationErrorKind::UnregisteredLabelSource {
column: column.name().to_string(),
source,
}
})
})
.collect()
}
pub(crate) fn include_relations<'c, M>(
mut query: Query<List<M>>,
columns: impl IntoIterator<Item = &'c BoxColumn<M>>,
) -> Query<List<M>>
where
M: toasty::schema::Model + 'c,
{
let mut seen: Vec<crate::toasty_compat::UntypedInclude> = Vec::new();
for include in columns.into_iter().flat_map(|c| c.includes().into_vec()) {
if !seen.contains(&include) {
query = query.include(include.clone());
seen.push(include);
}
}
query
}
pub(crate) async fn restrict_relations<'c, M>(
cx: &Cx,
columns: impl IntoIterator<Item = &'c BoxColumn<M>>,
rows: &[M],
ex: &mut dyn toasty::Executor,
) -> topcoat::Result<()>
where
M: 'c,
{
for column in columns {
column.internals().restrict(cx, rows, &mut *ex).await?;
}
Ok(())
}
#[derive(Clone, Debug)]
struct ColumnBase {
binding: Binding,
label: Option<crate::Text>,
width: ColumnWidth,
toggle: ColumnToggle,
}
impl ColumnBase {
fn new<M, T>(path: &Path<M, T>, width: ColumnWidth) -> Self
where
M: toasty::schema::Model,
{
Self {
binding: Binding::of(path),
label: None,
width,
toggle: ColumnToggle::Fixed,
}
}
fn name(&self) -> &str {
self.binding.name()
}
fn label(&self) -> crate::Text {
self.label.clone().unwrap_or_else(|| self.binding.label())
}
fn reads<M>(&self) -> crate::lens::Reads<M> {
self.binding.reads()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ColumnWidth {
#[default]
Auto,
Grow,
Wrap,
Rem(u8),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ColumnToggle {
#[default]
Fixed,
ShownByDefault,
HiddenByDefault,
}
impl ColumnToggle {
pub(crate) fn is_toggleable(self) -> bool {
self != Self::Fixed
}
}
impl ColumnWidth {
pub(crate) fn content_class(self) -> Option<&'static str> {
match self {
Self::Auto => None,
Self::Grow => Some("max-w-xs truncate"),
Self::Wrap => Some("min-w-48 max-w-md whitespace-normal"),
Self::Rem(_) => Some("truncate"),
}
}
pub(crate) fn style(self) -> Option<String> {
match self {
Self::Rem(rem) => Some(format!("width: {rem}rem; max-width: {rem}rem")),
Self::Auto | Self::Grow | Self::Wrap => None,
}
}
}
pub(crate) fn searches<M>(column: &dyn Column<M>) -> bool {
column.search_expr("a").is_some()
}
pub(crate) fn sorts<M>(column: &dyn Column<M>) -> bool {
column.order_by(false).is_some()
}
pub(crate) type BoxColumn<M> = Arc<dyn Column<M>>;
pub(crate) fn shows<M>(cx: &Cx, column: &dyn Column<M>, hidden: &[String]) -> bool {
crate::lens::shows(column.reads(), hidden) && !column.internals().masked(cx)
}
#[cfg(test)]
mod tests;