tablo-core 0.9.0

The core toolkit types for Tablo, a server-rendered admin toolkit on Topcoat and Toasty.
Documentation
//! [`BooleanColumn`]: a `bool` field rendered as an icon.

use derive_where::derive_where;
use toasty::stmt::OrderByExpr;
use topcoat::{context::Cx, icon::icon, view::*};

use super::{Column, ColumnBase, ColumnWidth, sort_order};
use crate::Lens;

/// A column of a `bool` field rendered as an icon.
///
/// ```rust
/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
/// # struct Post { #[key] #[auto] id: uuid::Uuid, featured: bool }
/// tablo_core::BooleanColumn::new(tablo_core::lens!(Post.featured)).sortable();
/// ```
#[derive_where(Clone, Debug)]
pub struct BooleanColumn<M> {
    lens: Lens<M, bool>,
    base: ColumnBase,
    sortable: bool,
    labels: (crate::Text, crate::Text),
}

impl<M> BooleanColumn<M>
where
    M: toasty::schema::Model,
{
    /// Bind the column to the `bool` field `lens` reads.
    pub fn new(lens: Lens<M, bool>) -> Self {
        Self {
            base: ColumnBase::new(lens.path(), ColumnWidth::Auto),
            lens,
            sortable: false,
            labels: (
                crate::Text::message("tablo-yes"),
                crate::Text::message("tablo-no"),
            ),
        }
    }

    base_builders!(sortable);

    /// The words for `true` and `false`.
    pub fn labels(mut self, yes: impl Into<crate::Text>, no: impl Into<crate::Text>) -> Self {
        self.labels = (yes.into(), no.into());
        self
    }
}

impl<M> Column<M> for BooleanColumn<M>
where
    M: toasty::schema::Model + Send + Sync + 'static,
{
    base_column_methods!();

    fn text(&self, cx: &Cx, row: &M) -> String {
        if *self.lens.read(row) {
            self.labels.0.localize(cx)
        } else {
            self.labels.1.localize(cx)
        }
    }

    fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
        boolean_cell(cx, *self.lens.read(row), self.text(cx, row))
    }

    fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
        self.sortable
            .then(|| sort_order(self.lens.path(), descending))
    }
}

impl<M> crate::column::ColumnInternals<M> for BooleanColumn<M>
where
    M: toasty::schema::Model + Send + Sync + 'static,
{
    base_internals!(binding);
}

/// A `bool` as its icon, with `text` for a screen reader.
pub(super) fn boolean_cell(cx: &Cx, value: bool, text: String) -> BoxView<'_> {
    // A no is the common case in most columns, so it stays quiet and the yeses stand out.
    let (data, class) = if value {
        (tablo_ui::icons::CIRCLE_CHECK, "size-4 text-success")
    } else {
        (tablo_ui::icons::MINUS, "size-4 text-muted-foreground/60")
    };
    view! {
        <span class="inline-flex items-center" data-boolean=(value.to_string())>
            icon(data: data, attrs: attributes! { class=(class) aria-hidden="true" })
            <span class="sr-only">(text)</span>
        </span>
    }
    .boxed()
}