Skip to main content

tablo_core/table/
wiring.rs

1//! A declared [`Table`] together with what one request wires onto it.
2
3use std::{ops::Deref, sync::Arc};
4
5use topcoat::context::Cx;
6
7use super::{
8    RowActions, RowPolicy, Table, TableAction,
9    state::{TableSignals, TableState},
10};
11
12/// What a request wires onto a declared table: the action URLs its rows and bulk bar link to,
13/// and which actions each row allows.
14///
15/// The panel builds it per request from the mounted resource and the request's policy, so the
16/// declaration itself carries none of it.
17pub(crate) struct Wiring<M> {
18    row_policy: Option<RowPolicy<M>>,
19    delete_prefix: Option<String>,
20    edit_prefix: Option<String>,
21    view_prefix: Option<String>,
22    bulk_delete: bool,
23    /// The custom actions and the list URL their routes hang off.
24    custom_actions: Vec<TableAction<M>>,
25    actions_prefix: Option<String>,
26    /// Where a write this table's row and bulk actions start lands.
27    return_to: Option<String>,
28    /// The prefix the table's URL parameters carry.
29    prefix: Option<String>,
30}
31
32impl<M> Default for Wiring<M> {
33    fn default() -> Self {
34        Self {
35            row_policy: None,
36            delete_prefix: None,
37            edit_prefix: None,
38            view_prefix: None,
39            bulk_delete: false,
40            custom_actions: Vec::new(),
41            actions_prefix: None,
42            return_to: None,
43            prefix: None,
44        }
45    }
46}
47
48/// A declared [`Table`] with one request's actions and policy wired on: what the list page
49/// renders.
50///
51/// [`wired_table`](crate::panel::wired_table) returns the panel's for a resource. It reads as
52/// its [`Table`] for loading a [`TablePage`](crate::TablePage).
53pub struct WiredTable<M> {
54    table: Arc<Table<M>>,
55    pub(super) wiring: Wiring<M>,
56}
57
58impl<M> Deref for WiredTable<M> {
59    type Target = Table<M>;
60
61    fn deref(&self) -> &Table<M> {
62        &self.table
63    }
64}
65
66impl<M> std::fmt::Debug for WiredTable<M> {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        let wiring = &self.wiring;
69        f.debug_struct("WiredTable")
70            .field("table", &self.table)
71            .field("row_policy", &wiring.row_policy.is_some())
72            .field("delete_prefix", &wiring.delete_prefix)
73            .field("edit_prefix", &wiring.edit_prefix)
74            .field("view_prefix", &wiring.view_prefix)
75            .field("bulk_delete", &wiring.bulk_delete)
76            .field(
77                "custom_actions",
78                &wiring
79                    .custom_actions
80                    .iter()
81                    .map(|a| a.name)
82                    .collect::<Vec<_>>(),
83            )
84            .field("return_to", &wiring.return_to)
85            .field("prefix", &wiring.prefix)
86            .finish()
87    }
88}
89
90impl<M> WiredTable<M> {
91    /// `table` with nothing wired: no actions.
92    pub(crate) fn new(table: Arc<Table<M>>) -> Self {
93        Self {
94            table,
95            wiring: Wiring::default(),
96        }
97    }
98
99    /// Declare the per-record action policy.
100    pub(crate) fn row_actions(
101        mut self,
102        policy: impl Fn(&M) -> RowActions + Send + Sync + 'static,
103    ) -> Self {
104        self.wiring.row_policy = Some(Arc::new(policy));
105        self
106    }
107
108    /// Enable the row-level `Delete` action.
109    pub(crate) fn with_delete(mut self, prefix: String) -> Self {
110        if self.addressable {
111            self.wiring.delete_prefix = Some(prefix);
112        }
113        self
114    }
115
116    /// Enable the row-level `Edit` action.
117    pub(crate) fn with_edit(mut self, prefix: String) -> Self {
118        if self.addressable {
119            self.wiring.edit_prefix = Some(prefix);
120        }
121        self
122    }
123
124    /// Enable the row-level `View` action.
125    pub(crate) fn with_view(mut self, prefix: String) -> Self {
126        if self.addressable {
127            self.wiring.view_prefix = Some(prefix);
128        }
129        self
130    }
131
132    /// Enable bulk selection with the `BulkDelete` action.
133    pub(crate) fn with_bulk_delete(mut self, enabled: bool) -> Self {
134        self.wiring.bulk_delete = enabled;
135        self
136    }
137
138    /// Wire the resource's custom actions.
139    pub(crate) fn with_custom_actions(
140        mut self,
141        prefix: String,
142        actions: Vec<TableAction<M>>,
143    ) -> Self {
144        if self.addressable {
145            self.wiring.actions_prefix = Some(prefix);
146            self.wiring.custom_actions = actions;
147        }
148        self
149    }
150
151    /// Send the writes this table starts back to `url`.
152    pub(crate) fn returning_to(mut self, url: String) -> Self {
153        self.wiring.return_to = Some(url);
154        self
155    }
156
157    /// Spell the table's URL parameters `{prefix}.{name}`, so a page renders several tables and
158    /// each keeps its own state and browser controls.
159    #[must_use]
160    pub fn prefixed(mut self, prefix: impl Into<String>) -> Self {
161        self.wiring.prefix = Some(prefix.into());
162        self
163    }
164
165    /// The table's signals and the list state they hold. Creates the signals, so it carries
166    /// [`topcoat::runtime::signal`]'s contract: call it while a page body runs.
167    pub(crate) fn browser_state(&self, cx: &Cx) -> (TableSignals, TableState) {
168        let prefix = self.wiring.prefix.as_deref();
169        let signals = TableSignals::new(cx, prefix);
170        let state = self.normalize_state(&signals.state(prefix));
171        (signals, state)
172    }
173
174    /// Which row actions `record` allows.
175    pub(crate) fn actions_for(&self, record: &M) -> RowActions {
176        self.wiring
177            .row_policy
178            .as_ref()
179            .map_or(RowActions::ALL, |policy| policy(record))
180    }
181
182    pub(super) fn return_to(&self) -> Option<&str> {
183        self.wiring.return_to.as_deref()
184    }
185
186    pub(super) fn delete_prefix(&self) -> Option<&str> {
187        self.wiring.delete_prefix.as_deref()
188    }
189
190    pub(super) fn edit_prefix(&self) -> Option<&str> {
191        self.wiring.edit_prefix.as_deref()
192    }
193
194    pub(super) fn view_prefix(&self) -> Option<&str> {
195        self.wiring.view_prefix.as_deref()
196    }
197
198    pub(super) fn actions_prefix(&self) -> Option<&str> {
199        self.wiring.actions_prefix.as_deref()
200    }
201
202    pub(super) fn row_custom_actions(&self) -> impl Iterator<Item = &TableAction<M>> {
203        self.wiring
204            .custom_actions
205            .iter()
206            .filter(|a| a.row && self.wiring.actions_prefix.is_some())
207    }
208
209    pub(super) fn bulk_custom_actions(&self) -> impl Iterator<Item = &TableAction<M>> {
210        self.wiring
211            .custom_actions
212            .iter()
213            .filter(|a| a.bulk && self.wiring.actions_prefix.is_some())
214    }
215
216    pub(super) fn bulk_delete_enabled(&self) -> bool {
217        self.wiring.bulk_delete && self.wiring.delete_prefix.is_some()
218    }
219}
220
221#[cfg(test)]
222impl<M> Table<M> {
223    /// This table with nothing wired yet, for a test that wires it by hand.
224    pub(crate) fn wired(self) -> WiredTable<M> {
225        WiredTable::new(Arc::new(self))
226    }
227}