mod core;
mod dialog;
mod empty;
mod filter_menu;
mod head;
mod pager;
mod rows;
mod skeleton;
mod toolbar;
use topcoat::{
context::Cx,
runtime::Event,
view::{Attributes, StaticClass, attributes, class},
};
use super::{TableSignals, WiredTable};
use crate::{
panel::url::with_return,
schema::Schema,
table::{ActionLayout, Alignment, ColumnWidth, TableState, query_of},
};
pub(super) struct Frame<'t> {
columns: Vec<ColumnHead<'t>>,
picker: Option<ColumnPicker<'t>>,
search: bool,
filter_menu: bool,
page_sizes: Vec<usize>,
delete_prefix: Option<&'t str>,
edit_prefix: Option<&'t str>,
view_prefix: Option<&'t str>,
actions_prefix: Option<&'t str>,
bulk_delete: bool,
bulk_actions: Vec<BulkAction<'t>>,
inputs: Vec<InputAction<'t>>,
row_actions: Vec<&'t str>,
action_menu: bool,
return_to: Option<&'t str>,
}
struct ColumnHead<'t> {
name: &'t str,
label: crate::Text,
sortable: bool,
width: ColumnWidth,
alignment: Alignment,
}
struct ColumnPicker<'t> {
url: &'t str,
columns: Vec<PickerColumn<'t>>,
}
struct PickerColumn<'t> {
name: &'t str,
label: crate::Text,
shown: bool,
}
struct BulkAction<'t> {
name: &'static str,
label: &'t str,
confirm: bool,
input: bool,
}
struct InputAction<'t> {
name: &'static str,
label: &'t str,
confirm: bool,
input: fn() -> Schema,
}
impl<M> WiredTable<M> {
pub(super) fn frame(&self) -> Frame<'_>
where
M: toasty::schema::Model,
{
Frame {
columns: self
.visible_columns()
.map(|col| ColumnHead {
name: col.name(),
label: col.label(),
sortable: col.is_sortable(),
width: col.column_width(),
alignment: col.alignment(),
})
.collect(),
picker: self.columns_url().map(|url| ColumnPicker {
url,
columns: self
.declared_columns()
.iter()
.filter(|col| col.toggle().is_toggleable())
.map(|col| PickerColumn {
name: col.name(),
label: col.label(),
shown: self.shows(col),
})
.collect(),
}),
search: self.search_enabled(),
filter_menu: self.filter_menu_enabled(),
page_sizes: self.page_sizes(),
delete_prefix: self.delete_prefix(),
edit_prefix: self.edit_prefix(),
view_prefix: self.view_prefix(),
actions_prefix: self.actions_prefix(),
bulk_delete: self.bulk_delete_enabled(),
bulk_actions: self
.bulk_custom_actions()
.map(|action| BulkAction {
name: action.name,
label: &action.label,
confirm: action.confirm && action.input.is_none(),
input: action.input.is_some(),
})
.collect(),
inputs: self
.row_custom_actions()
.chain(self.bulk_custom_actions().filter(|action| !action.row))
.filter_map(|action| {
Some(InputAction {
name: action.name,
label: &action.label,
confirm: action.confirm,
input: action.input?,
})
})
.collect(),
row_actions: self
.row_custom_actions()
.map(|action| action.label.as_str())
.collect(),
action_menu: self.row_action_layout() == ActionLayout::Menu,
return_to: self.return_to(),
}
}
}
impl Frame<'_> {
fn with_actions(&self) -> bool {
self.delete_prefix.is_some()
|| self.edit_prefix.is_some()
|| self.view_prefix.is_some()
|| !self.row_actions.is_empty()
}
fn slack_column(&self) -> Option<usize> {
self.columns
.iter()
.position(|col| col.width == ColumnWidth::Grow)
}
fn bulk_enabled(&self) -> bool {
self.bulk_delete || !self.bulk_actions.is_empty()
}
fn action_url(&self, url: String) -> String {
match self.return_to {
Some(target) => with_return(&url, target),
None => url,
}
}
}
const CHROME_CELL_CLASS: &str = "w-px whitespace-nowrap";
const BAR_CLASS: StaticClass =
class!("flex flex-wrap items-center gap-2 border-b border-border px-4 py-3 sm:px-6");
const ROW_ICON_CLASS: StaticClass = class!(
"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none",
"transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2",
"focus-visible:ring-ring [&_svg]:size-4",
);
const ROW_DELETE_CLASS: StaticClass = class!(
"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none",
"transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:ring-2",
"focus-visible:ring-ring [&_svg]:size-4",
);
const SEARCH_FIELD_CLASS: StaticClass = class!("relative w-full sm:w-72");
const SEARCH_ICON_CLASS: StaticClass = class!(
"pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
);
pub(crate) const TABLE_CARD_CLASS: StaticClass = class!(
"overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-sm"
);
const QUIET_LINK_CLASS: StaticClass = class!("text-sm text-muted-foreground hover:text-foreground");
const EMPTY_LINK_CLASS: StaticClass =
class!("text-sm font-medium text-foreground underline underline-offset-4");
pub(super) fn table_dom_id(state: &TableState, suffix: &str) -> String {
format!("{}-{suffix}", state.prefix.as_deref().unwrap_or("table"))
}
pub(super) fn live_link(cx: &Cx, url: String, signals: &TableSignals) -> Attributes {
let query = signals.query.clone();
let next = query_of(&url).to_string();
attributes! {
cx =>
href=(url)
@click=$(|e: Event| {
if e.ctrl_key {
return;
}
if e.meta_key {
return;
}
if e.shift_key {
return;
}
if e.alt_key {
return;
}
e.prevent_default();
query.set(next.clone());
})
}
}