Skip to main content

tablo_test/protocol/
controls.rs

1//! Reads a page's action buttons: whether one confirms before it posts, whether the bulk bar
2//! offers every matching record, and which custom actions show disabled with their reason.
3
4use super::dom::{Attrs, Element, attr_value, elements, open_tags, strip_text};
5
6/// A custom action whose button shows disabled, as the record's state refuses it.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct DisabledAction {
9    /// The action's `NAME`.
10    pub name: String,
11    /// Why the button is disabled, as the action's `disabled_reason` words it.
12    pub reason: String,
13}
14
15/// Whether the button posting to `action` asks first: a confirming trigger is a plain button that
16/// opens the dialog, a direct write is a submit button. `None` when no button posts there.
17pub fn confirms_first(html: &str, action: &str) -> Option<bool> {
18    open_tags(html)
19        .into_iter()
20        .find(|(tag, attrs, _)| {
21            tag == "button" && attr_value(attrs, "formaction").as_deref() == Some(action)
22        })
23        .map(|(_, attrs, _)| attr_value(&attrs, "type").as_deref() == Some("button"))
24}
25
26/// The button posting to `url`, a custom action's route, on a row or a record page, as its
27/// element. A row's button posts itself; a record page's button sits in the form posting to `url`.
28fn posting_button(html: &str, url: &str) -> Option<Element> {
29    let posts_to = |attrs: &Attrs, name: &str| {
30        attr_value(attrs, name).is_some_and(|posted| posted.split('?').next() == Some(url))
31    };
32    elements(html, "button")
33        .into_iter()
34        .find(|(_, attrs, _)| posts_to(attrs, "formaction"))
35        .or_else(|| {
36            elements(html, "form")
37                .into_iter()
38                .find(|(_, attrs, _)| posts_to(attrs, "action"))
39                .and_then(|(_, _, inner)| elements(&inner, "button").into_iter().next())
40        })
41}
42
43/// The text of the button posting to `url`, its icon aside. `None` when no button posts there.
44pub fn action_label(html: &str, url: &str) -> Option<String> {
45    posting_button(html, url).map(|(_, _, inner)| strip_text(&inner))
46}
47
48/// The hint the button posting to `url` shows on hover. `None` when no button posts there or it
49/// shows none.
50pub fn action_tooltip(html: &str, url: &str) -> Option<String> {
51    posting_button(html, url).and_then(|(_, attrs, _)| attr_value(&attrs, "title"))
52}
53
54/// The count a header action's button shows as its badge, posting to `url`. `None` when no button
55/// posts there or it shows no badge.
56pub fn action_badge(html: &str, url: &str) -> Option<String> {
57    let (_, _, inner) = posting_button(html, url)?;
58    elements(&inner, "span")
59        .into_iter()
60        .find(|(_, attrs, _)| attr_value(attrs, "data-action-badge").is_some())
61        .map(|(_, _, count)| strip_text(&count))
62}
63
64/// Whether the bulk bar offers to select every record the list's query selects, beyond its page.
65pub fn selects_matching(html: &str) -> bool {
66    open_tags(html).iter().any(|(tag, attrs, _)| {
67        tag == "button" && attr_value(attrs, "data-select-matching").is_some()
68    })
69}
70
71/// The custom actions `html` shows disabled, in document order: a row's, or a record page
72/// header's.
73pub fn disabled_actions(html: &str) -> Vec<DisabledAction> {
74    open_tags(html)
75        .into_iter()
76        .filter_map(|(_, attrs, _)| {
77            Some(DisabledAction {
78                name: attr_value(&attrs, "data-disabled-action")?,
79                reason: attr_value(&attrs, "title").unwrap_or_default(),
80            })
81        })
82        .collect()
83}