1use tablo_core::dom::row;
4
5use super::{Attrs, attr_value, elements, is_selected, open_tags, strip_text};
6use crate::protocol::controls::{DisabledAction, disabled_actions};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Row {
11 pub select_value: Option<String>,
13 pub cells: Vec<String>,
15 pub actions: RowActions,
17 pub links: Vec<String>,
19 pub copies: Vec<String>,
21 pub color: Option<String>,
23 pub edits: Vec<CellEdit>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct CellEdit {
31 pub column: String,
33 pub action: String,
35 pub value: Option<String>,
38 pub version: Option<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct RowActions {
45 pub view: Option<String>,
47 pub edit: Option<String>,
49 pub delete_action: Option<String>,
52 pub links: Vec<String>,
54 pub disabled: Vec<DisabledAction>,
56}
57
58pub fn rows(html: &str) -> Vec<Row> {
60 let mut out = Vec::new();
61 for (_, attrs, inner) in elements(html, "tr") {
62 let Some(id) = attr_value(&attrs, "id") else {
63 continue;
64 };
65 if !id.starts_with("row-") {
66 continue;
67 }
68 out.push(row(&attrs, &inner));
69 }
70 out
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Group {
76 pub label: String,
78 pub rows: Vec<Row>,
80}
81
82pub fn groups(html: &str) -> Vec<Group> {
85 let mut out: Vec<Group> = Vec::new();
86 for (_, attrs, inner) in elements(html, "tr") {
87 if let Some(label) = attr_value(&attrs, "data-group") {
88 out.push(Group {
89 label,
90 rows: Vec::new(),
91 });
92 } else if attr_value(&attrs, "id").is_some_and(|id| id.starts_with("row-"))
93 && let Some(group) = out.last_mut()
94 {
95 group.rows.push(row(&attrs, &inner));
96 }
97 }
98 out
99}
100
101pub fn row_keys(html: &str) -> Vec<String> {
104 rows(html)
105 .into_iter()
106 .filter_map(|row| row.select_value)
107 .collect()
108}
109
110pub fn row_actions(html: &str, key: &str) -> Option<RowActions> {
113 rows(html).into_iter().find_map(|row| {
114 let matches = row.select_value.as_deref() == Some(key)
115 || row
116 .actions
117 .view
118 .as_deref()
119 .is_some_and(|href| href_contains_key(href, key))
120 || row
121 .actions
122 .edit
123 .as_deref()
124 .is_some_and(|href| href_contains_key(href, key))
125 || row
126 .actions
127 .delete_action
128 .as_deref()
129 .is_some_and(|action| href_contains_key(action, key));
130 matches.then_some(row.actions)
131 })
132}
133
134fn row(row_attrs: &Attrs, row_html: &str) -> Row {
135 let mut select_value = None;
136 for (tag, attrs, _) in open_tags(row_html) {
137 if tag == "input" && attr_value(&attrs, "data-row-select").is_some() {
138 select_value = attr_value(&attrs, "value");
139 break;
140 }
141 }
142 let mut cells = Vec::new();
143 for (_, _, inner) in elements(row_html, "td") {
144 cells.push(strip_text(&inner));
145 }
146 let mut view_href = None;
147 let mut edit_href = None;
148 let mut delete_action = None;
149 let mut row_links = Vec::new();
150 let mut links = Vec::new();
151 let mut copies = Vec::new();
152 for (_, attrs, _) in open_tags(row_html) {
153 if attr_value(&attrs, "data-cell-link").is_some() {
154 links.extend(attr_value(&attrs, "href"));
155 }
156 if let Some(value) = attr_value(&attrs, "data-copy") {
157 copies.push(value);
158 }
159 match attr_value(&attrs, "data-row-action").as_deref() {
160 Some(row::VIEW) => view_href = attr_value(&attrs, "href"),
161 Some(row::EDIT) => edit_href = attr_value(&attrs, "href"),
162 Some(row::DELETE) => delete_action = attr_value(&attrs, "formaction"),
163 Some(row::LINK) => row_links.extend(attr_value(&attrs, "href")),
164 _ => {}
165 }
166 }
167 Row {
168 select_value,
169 cells,
170 actions: RowActions {
171 view: view_href,
172 edit: edit_href,
173 delete_action,
174 links: row_links,
175 disabled: disabled_actions(row_html),
176 },
177 links,
178 copies,
179 color: attr_value(row_attrs, "data-row-color"),
180 edits: cell_edits(row_html),
181 }
182}
183
184fn cell_edits(row_html: &str) -> Vec<CellEdit> {
185 elements(row_html, "form")
186 .into_iter()
187 .filter_map(|(_, attrs, inner)| {
188 let column = attr_value(&attrs, "data-cell-edit")?;
189 let action = attr_value(&attrs, "action").unwrap_or_default();
190 let mut value = None;
191 let mut version = None;
192 for (tag, attrs, _) in open_tags(&inner) {
193 let name = attr_value(&attrs, "name");
194 let kind = attr_value(&attrs, "type");
195 match (tag.as_str(), name.as_deref(), kind.as_deref()) {
196 ("input", Some("record_version"), _) => version = attr_value(&attrs, "value"),
197 ("input", Some(name), Some("checkbox")) if name == column => {
198 let checked = attrs.iter().any(|(attr, _)| attr == "checked");
199 value = Some(checked.to_string());
200 }
201 ("input", Some(name), kind) if name == column && kind != Some("hidden") => {
202 value = attr_value(&attrs, "value");
203 }
204 _ => {}
205 }
206 }
207 for (_, attrs, _) in open_tags(&inner) {
208 if is_selected(&attrs) {
209 value = attr_value(&attrs, "value");
210 }
211 }
212 Some(CellEdit {
213 column,
214 action,
215 value,
216 version,
217 })
218 })
219 .collect()
220}
221
222fn href_contains_key(href: &str, key: &str) -> bool {
223 href.split(['?', '#', '/', '&'])
224 .any(|segment| segment == key)
225}