use std::sync::Arc;
use toasty::stmt::{List, Query};
use topcoat::{context::Cx, view::*};
#[cfg(feature = "rich-text")]
use crate::column::RichTextColumn;
use crate::{
DeclarationErrorKind,
column::{
BooleanColumn, BoxColumn, ColorColumn, Column, ComputedColumn, CountColumn, DateColumn,
EmbeddedColumn, FileColumn, IconColumn, ImageColumn, ListColumn, RelationColumn,
RepeaterColumn, SelectColumn, TextColumn, TextInputColumn, ToggleColumn, include_relations,
restrict_relations, shows, unavailable_sources,
},
schema::{Grid, Group, HoldsTabs, Section, Tab, TabView, Tabs, tab_id, tab_view},
};
pub struct Detail<M> {
nodes: Vec<Node<M>>,
}
fn entry<'a>(cx: &'a Cx, name: &str, entry: BoxView<'a>) -> BoxView<'a> {
let name = name.to_string();
view! { <div class="contents" data-entry=(name)>(entry)</div> }.boxed()
}
enum Node<M> {
Column(BoxColumn<M>),
Section(Box<Section<Detail<M>>>),
Group(Box<Group<Detail<M>>>),
Grid(Box<Grid<Detail<M>>>),
Tabs(Box<Tabs<Detail<M>>>),
Tab(Box<Tab<Detail<M>>>),
}
impl<M> Detail<M> {
pub fn new(children: impl IntoDetail<M>) -> Self {
children.into_detail()
}
pub fn empty() -> Self {
Self { nodes: Vec::new() }
}
pub(crate) fn of_columns(columns: &[BoxColumn<M>]) -> Self {
Self {
nodes: columns.iter().cloned().map(Node::Column).collect(),
}
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
self.nodes.push(Node::Column(Arc::new(column)));
self
}
fn append(&mut self, other: Self) {
self.nodes.extend(other.nodes);
}
pub(crate) fn read_bare(&mut self, bare: fn(&M) -> M)
where
M: toasty::schema::Model + 'static,
{
fn walk<M: toasty::schema::Model + 'static>(nodes: &mut [Node<M>], bare: fn(&M) -> M) {
for node in nodes {
match node {
Node::Column(column) => {
*column = crate::column::bare_column(column.clone(), bare);
}
Node::Section(section) => walk(&mut section.children.nodes, bare),
Node::Group(group) => walk(&mut group.children.nodes, bare),
Node::Grid(grid) => walk(&mut grid.children.nodes, bare),
Node::Tabs(tabs) => walk(&mut tabs.children.nodes, bare),
Node::Tab(tab) => walk(&mut tab.children.nodes, bare),
}
}
}
let loads = self
.columns()
.iter()
.any(|column| !column.includes().into_vec().is_empty());
if loads {
walk(&mut self.nodes, bare);
}
}
pub(crate) fn columns(&self) -> Vec<&BoxColumn<M>> {
fn walk<'d, M>(nodes: &'d [Node<M>], out: &mut Vec<&'d BoxColumn<M>>) {
for node in nodes {
match node {
Node::Column(column) => out.push(column),
Node::Section(section) => walk(§ion.children.nodes, out),
Node::Group(group) => walk(&group.children.nodes, out),
Node::Grid(grid) => walk(&grid.children.nodes, out),
Node::Tabs(tabs) => walk(&tabs.children.nodes, out),
Node::Tab(tab) => walk(&tab.children.nodes, out),
}
}
}
let mut out = Vec::new();
walk(&self.nodes, &mut out);
out
}
pub async fn load(&self, cx: &Cx, query: Query<List<M>>) -> topcoat::Result<Vec<M>>
where
M: toasty::schema::Model + Send + Sync + 'static,
{
let mut db = crate::db::db(cx)?;
let records = self
.include_relations(query)
.exec(&mut db)
.await
.map_err(crate::error::unavailable)?;
self.restrict_relations(cx, &records, &mut db).await?;
Ok(records)
}
pub(crate) fn include_relations(&self, query: Query<List<M>>) -> Query<List<M>>
where
M: toasty::schema::Model,
{
include_relations(query, self.columns())
}
pub(crate) async fn restrict_relations(
&self,
cx: &Cx,
records: &[M],
ex: &mut dyn toasty::Executor,
) -> topcoat::Result<()> {
restrict_relations(cx, self.columns(), records, ex).await
}
pub(crate) fn unavailable_sources(&self, cx: &Cx) -> Vec<DeclarationErrorKind> {
unavailable_sources(cx, self.columns().into_iter().map(|c| &**c))
}
pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
self.columns()
.into_iter()
.filter_map(|column| column.internals().misdeclared())
.collect()
}
pub fn render<'a>(&self, cx: &'a Cx, record: &M) -> BoxView<'a> {
self.render_without(cx, record, &[])
}
pub(crate) fn render_without<'a>(
&self,
cx: &'a Cx,
record: &M,
hidden: &[String],
) -> BoxView<'a> {
self.render_in(cx, record, hidden, 0)
}
fn render_in<'a>(
&self,
cx: &'a Cx,
record: &M,
hidden: &[String],
depth: usize,
) -> BoxView<'a> {
let views: Vec<BoxView<'a>> = self
.nodes
.iter()
.filter(|node| match node {
Node::Column(column) => shows(cx, &**column, hidden),
Node::Section(section) => {
section.children.is_empty() || section.children.shows_any(cx, hidden)
}
Node::Group(group) => group.children.shows_any(cx, hidden),
Node::Grid(grid) => grid.children.shows_any(cx, hidden),
Node::Tabs(tabs) => tabs.children.shows_any(cx, hidden),
Node::Tab(tab) => tab.children.shows_any(cx, hidden),
})
.map(|node| match node {
Node::Column(column) => entry(cx, column.name(), column.entry(cx, record)),
Node::Section(section) => {
let body = (!section.children.is_empty())
.then(|| section.children.render_in(cx, record, hidden, depth));
section.chrome(cx, body)
}
Node::Group(group) => {
group.chrome(cx, group.children.render_in(cx, record, hidden, depth))
}
Node::Grid(grid) => {
grid.chrome(cx, grid.children.render_in(cx, record, hidden, depth))
}
Node::Tabs(tabs) => {
tabs.chrome(cx, tabs.children.render_tabs(cx, record, hidden, depth))
}
Node::Tab(tab) => tab.children.render_in(cx, record, hidden, depth),
})
.collect();
stack(cx, views)
}
fn render_tabs<'a>(
&self,
cx: &'a Cx,
record: &M,
hidden: &[String],
depth: usize,
) -> Vec<TabView<'a>> {
self.nodes
.iter()
.filter_map(|node| match node {
Node::Tab(tab) if tab.children.shows_any(cx, hidden) => Some(tab),
_ => None,
})
.enumerate()
.map(|(index, tab)| {
let id = tab
.children
.columns()
.first()
.map(|column| tab_id(&format!("detail-{}", column.name()), depth))
.unwrap_or_default();
let body = tab.children.render_in(cx, record, hidden, depth + 1);
tab_view(cx, &id, tab.face(), index == 0, false, body)
})
.collect()
}
fn shows_any(&self, cx: &Cx, hidden: &[String]) -> bool {
self.columns()
.into_iter()
.any(|column| shows(cx, &**column, hidden))
}
pub(crate) fn undeclared_reads(&self) -> Vec<&str> {
self.columns()
.into_iter()
.filter(|column| column.reads().is_none())
.map(|column| column.name())
.collect()
}
}
fn stack<'a>(cx: &'a Cx, views: Vec<BoxView<'a>>) -> BoxView<'a> {
view! {
for v in views {
(v)
}
}
.boxed()
}
impl<M> Default for Detail<M> {
fn default() -> Self {
Self::empty()
}
}
impl<M> std::fmt::Debug for Detail<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Detail")
.field(
"columns",
&self
.columns()
.into_iter()
.map(|column| column.name())
.collect::<Vec<_>>(),
)
.finish()
}
}
pub trait IntoDetail<M> {
fn into_detail(self) -> Detail<M>;
}
impl<M> IntoDetail<M> for Detail<M> {
fn into_detail(self) -> Detail<M> {
self
}
}
impl<M> IntoDetail<M> for () {
fn into_detail(self) -> Detail<M> {
Detail::empty()
}
}
impl<M> IntoDetail<M> for BoxColumn<M> {
fn into_detail(self) -> Detail<M> {
Detail {
nodes: vec![Node::Column(self)],
}
}
}
macro_rules! column_details {
($($ty:ident<$($g:ident),+>),+ $(,)?) => {
$(
impl<$($g),+> IntoDetail<M> for $ty<$($g),+>
where
Self: Column<M> + 'static,
{
fn into_detail(self) -> Detail<M> {
Detail::empty().column(self)
}
}
)+
};
}
column_details!(
TextColumn<M, T>,
DateColumn<M, T>,
ComputedColumn<M>,
RelationColumn<M>,
CountColumn<M>,
BooleanColumn<M>,
FileColumn<M>,
ColorColumn<M, T>,
IconColumn<M, T>,
ImageColumn<M, T>,
ToggleColumn<M>,
SelectColumn<M, T>,
TextInputColumn<M, T>,
EmbeddedColumn<M, T>,
ListColumn<M, T>,
RepeaterColumn<M, T>,
);
#[cfg(feature = "rich-text")]
column_details!(RichTextColumn<M, T>);
macro_rules! block_columns {
($($ty:ident),+ $(,)?) => {
$(
impl $ty<()> {
pub fn columns<M>(self, children: impl IntoDetail<M>) -> $ty<Detail<M>> {
self.holding(children.into_detail())
}
}
)+
};
}
block_columns!(Section, Group, Grid, Tab);
impl<M> HoldsTabs for Detail<M> {
fn push_tab(&mut self, tab: Tab<Self>) {
self.nodes.push(Node::Tab(Box::new(tab)));
}
}
impl<M> IntoDetail<M> for Tabs<Detail<M>> {
fn into_detail(self) -> Detail<M> {
Detail {
nodes: vec![Node::Tabs(Box::new(self))],
}
}
}
macro_rules! block_details {
($($ty:ident),+ $(,)?) => {
$(
impl<M> IntoDetail<M> for $ty<Detail<M>> {
fn into_detail(self) -> Detail<M> {
Detail {
nodes: vec![Node::$ty(Box::new(self))],
}
}
}
impl<M> IntoDetail<M> for $ty<()> {
fn into_detail(self) -> Detail<M> {
self.holding(Detail::empty()).into_detail()
}
}
)+
};
}
block_details!(Section, Group, Grid);
macro_rules! into_detail_tuples {
($($T:ident => $v:ident),+ $(,)?) => {
impl<M, $($T),+> IntoDetail<M> for ($($T,)+)
where
$($T: IntoDetail<M>,)+
{
fn into_detail(self) -> Detail<M> {
let ($($v,)+) = self;
let mut detail = Detail::empty();
$( detail.append($v.into_detail()); )+
detail
}
}
};
}
into_detail_tuples!(A => a);
into_detail_tuples!(A => a, B => b);
into_detail_tuples!(A => a, B => b, C => c);
into_detail_tuples!(A => a, B => b, C => c, D => d);
into_detail_tuples!(A => a, B => b, C => c, D => d, E => e);
into_detail_tuples!(A => a, B => b, C => c, D => d, E => e, F => f);
into_detail_tuples!(A => a, B => b, C => c, D => d, E => e, F => f, G => g);
into_detail_tuples!(
A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h
);
into_detail_tuples!(
A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i
);
into_detail_tuples!(
A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j
);
into_detail_tuples!(
A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k
);
into_detail_tuples!(
A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k, L => l
);
#[cfg(test)]
mod tests;