tablo_core/table/row_link.rs
1//! [`Table::row_link`]: a link among a row's actions.
2
3use std::sync::Arc;
4
5use derive_where::derive_where;
6use topcoat::icon::IconData;
7
8use super::Table;
9use crate::{Reads, Text};
10
11/// A link among a row's actions, such as a product's storefront page: unlike an
12/// [`Action`](crate::Action), it posts nothing and runs no policy check, so it suits only a page
13/// the user may already open.
14///
15/// ```rust
16/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
17/// # struct Product { #[key] #[auto] id: uuid::Uuid, name: String, slug: String }
18/// use tablo_core::{Reads, RowLink, Table, TextColumn, lens};
19///
20/// Table::new(TextColumn::new(lens!(Product.name))).row_link(
21/// RowLink::new("Open product", |product: &Product| {
22/// Some(format!("/shop/{}", product.slug))
23/// })
24/// .new_tab()
25/// .reading(Reads::of(lens!(Product.slug))),
26/// );
27/// ```
28#[derive_where(Clone)]
29pub struct RowLink<M> {
30 pub(crate) label: Text,
31 url: crate::column::ReadFn<M, Option<String>>,
32 pub(crate) icon: Option<IconData>,
33 pub(crate) new_tab: bool,
34 /// The fields `url` reads, when declared.
35 pub(super) reads: Option<Reads<M>>,
36 /// Whether `url` reads a copy of the row without its relations.
37 bare: bool,
38}
39
40impl<M> RowLink<M> {
41 /// A link reading `label` to `url(row)`; a row it gives `None` shows no link. The URL is
42 /// relative, or absolute over `http`, `https`, `mailto` or `tel`: a row whose URL has any
43 /// other scheme, such as `javascript:`, shows no link either.
44 pub fn new(
45 label: impl Into<Text>,
46 url: impl Fn(&M) -> Option<String> + Send + Sync + 'static,
47 ) -> Self {
48 Self {
49 label: label.into(),
50 url: Arc::new(url),
51 icon: None,
52 new_tab: false,
53 reads: None,
54 bare: false,
55 }
56 }
57
58 /// Show `icon` before the label.
59 pub fn icon(mut self, icon: IconData) -> Self {
60 self.icon = Some(icon);
61 self
62 }
63
64 /// Open the link in a new tab.
65 pub fn new_tab(mut self) -> Self {
66 self.new_tab = true;
67 self
68 }
69
70 /// Declare the fields the URL reads. A resource that
71 /// [restricts a field](crate::ResourceDef::field_access) refuses the table until each of its
72 /// links declares them, and a user who may not view one of them sees no such link.
73 pub fn reading(mut self, reads: Reads<M>) -> Self {
74 self.reads = Some(reads);
75 self
76 }
77
78 /// Where the link leads from `row`, when it shows there.
79 pub(crate) fn url_of(&self, row: &M) -> Option<String> {
80 (self.url)(row).filter(|url| crate::column::link_href(url).is_some())
81 }
82}
83
84impl<M> Table<M> {
85 /// Add `link` to each row's actions, before its custom actions, after the links already
86 /// declared.
87 pub fn row_link(mut self, link: RowLink<M>) -> Self {
88 self.row_links.push(link);
89 self
90 }
91
92 /// The links each row shows.
93 pub(crate) fn row_links(&self) -> &[RowLink<M>] {
94 &self.row_links
95 }
96
97 /// Whether a row link reads the row's relations as loaded.
98 pub(crate) fn row_link_reads_row(&self) -> bool {
99 self.row_links.iter().any(|link| !link.bare)
100 }
101
102 /// Have each row link declaring it reads none of the row's relations read `bare(row)`.
103 pub(super) fn bare_row_links(&mut self, bare: fn(&M) -> M)
104 where
105 M: toasty::schema::Model + 'static,
106 {
107 for link in &mut self.row_links {
108 if crate::column::reads_own_fields(link.reads.as_ref()) {
109 let url = link.url.clone();
110 link.url = Arc::new(move |row: &M| url(&bare(row)));
111 link.bare = true;
112 }
113 }
114 }
115
116 /// Whether a row link does not declare what its URL reads.
117 pub(crate) fn row_link_reads_undeclared(&self) -> bool {
118 self.row_links.iter().any(|link| link.reads.is_none())
119 }
120}