1use crate::view::{BrokenView, MatchReason, SavedView};
5use crate::widgets::text_input::TextInput;
6use ratatui::widgets::TableState;
7
8#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
9pub enum ViewModalMode {
10 #[default]
11 List,
12 Create,
13 Edit,
14}
15
16pub struct ViewRow {
19 pub view: SavedView,
20 pub score: f64,
21 pub reason: Option<MatchReason>,
22}
23
24#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
27pub enum FormFocus {
28 #[default]
29 Name,
30 Description,
31 Matching,
32 ExactPath,
33 RelativePath,
34 PathPattern,
35 FilenamePattern,
36 SchemaMatch,
37}
38
39#[derive(Default)]
40pub struct ViewModal {
41 pub active: bool,
42 pub mode: ViewModalMode,
43 pub table_state: TableState,
44 pub rows: Vec<ViewRow>,
45 pub broken_views: Vec<BrokenView>, pub form_focus: FormFocus,
48 pub matching_expanded: bool,
51 pub name_input: TextInput,
52 pub description_input: TextInput,
53 pub exact_path_input: TextInput,
54 pub relative_path_input: TextInput,
55 pub path_pattern_input: TextInput,
56 pub filename_pattern_input: TextInput,
57 pub schema_match_enabled: bool,
58 pub table: Option<String>,
61 pub editing_view_id: Option<String>, pub name_error: Option<String>,
63 pub status: Option<String>,
66 pub history_limit: usize,
67 pub score_details: Option<(String, String)>,
69}
70
71impl ViewModal {
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn selected_view(&self) -> Option<&SavedView> {
77 self.table_state
78 .selected()
79 .and_then(|i| self.rows.get(i))
80 .map(|row| &row.view)
81 }
82
83 pub fn select_next(&mut self) {
86 if self.rows.is_empty() {
87 return;
88 }
89 let i = match self.table_state.selected() {
90 Some(i) if i + 1 < self.rows.len() => i + 1,
91 Some(_) => 0,
92 None => 0,
93 };
94 self.table_state.select(Some(i));
95 }
96
97 pub fn select_prev(&mut self) {
98 if self.rows.is_empty() {
99 return;
100 }
101 let i = match self.table_state.selected() {
102 Some(0) | None => self.rows.len() - 1,
103 Some(i) => i - 1,
104 };
105 self.table_state.select(Some(i));
106 }
107
108 pub fn focus_order(&self) -> &'static [FormFocus] {
110 if self.matching_expanded {
111 &[
112 FormFocus::Name,
113 FormFocus::Description,
114 FormFocus::Matching,
115 FormFocus::ExactPath,
116 FormFocus::RelativePath,
117 FormFocus::PathPattern,
118 FormFocus::FilenamePattern,
119 FormFocus::SchemaMatch,
120 ]
121 } else {
122 &[FormFocus::Name, FormFocus::Description, FormFocus::Matching]
123 }
124 }
125
126 pub fn toggle_matching(&mut self) {
129 self.matching_expanded = !self.matching_expanded;
130 if !self.matching_expanded && !self.focus_order().contains(&self.form_focus) {
131 self.form_focus = FormFocus::Matching;
132 }
133 }
134
135 pub fn focused_input_mut(&mut self) -> Option<&mut TextInput> {
137 match self.form_focus {
138 FormFocus::Name => Some(&mut self.name_input),
139 FormFocus::Description => Some(&mut self.description_input),
140 FormFocus::ExactPath => Some(&mut self.exact_path_input),
141 FormFocus::RelativePath => Some(&mut self.relative_path_input),
142 FormFocus::PathPattern => Some(&mut self.path_pattern_input),
143 FormFocus::FilenamePattern => Some(&mut self.filename_pattern_input),
144 FormFocus::Matching | FormFocus::SchemaMatch => None,
145 }
146 }
147
148 pub fn criteria_count(&self) -> usize {
150 [
151 &self.exact_path_input,
152 &self.relative_path_input,
153 &self.path_pattern_input,
154 &self.filename_pattern_input,
155 ]
156 .iter()
157 .filter(|input| !input.value().trim().is_empty())
158 .count()
159 + usize::from(self.schema_match_enabled)
160 + usize::from(self.table.is_some())
161 }
162
163 fn reset_form(&mut self, history_limit: usize, theme: &crate::config::Theme) {
164 self.form_focus = FormFocus::Name;
165 self.matching_expanded = false;
166 self.name_error = None;
167 self.history_limit = history_limit;
168 self.name_input = TextInput::new()
169 .with_history_limit(history_limit)
170 .with_theme(theme);
171 self.description_input = TextInput::multiline()
172 .with_history_limit(history_limit)
173 .with_theme(theme);
174 self.exact_path_input = TextInput::new()
175 .with_history_limit(history_limit)
176 .with_theme(theme);
177 self.relative_path_input = TextInput::new()
178 .with_history_limit(history_limit)
179 .with_theme(theme);
180 self.path_pattern_input = TextInput::new()
181 .with_history_limit(history_limit)
182 .with_theme(theme);
183 self.filename_pattern_input = TextInput::new()
184 .with_history_limit(history_limit)
185 .with_theme(theme);
186 self.schema_match_enabled = false;
187 self.table = None;
188 }
189
190 pub fn enter_create_mode(&mut self, history_limit: usize, theme: &crate::config::Theme) {
191 self.mode = ViewModalMode::Create;
192 self.editing_view_id = None;
193 self.reset_form(history_limit, theme);
194 }
195
196 pub fn enter_edit_mode(
197 &mut self,
198 view: &SavedView,
199 history_limit: usize,
200 theme: &crate::config::Theme,
201 ) {
202 self.mode = ViewModalMode::Edit;
203 self.reset_form(history_limit, theme);
204 self.editing_view_id = Some(view.id.clone());
205 self.name_input.set_value(&view.name);
206 self.description_input
207 .set_value(view.description.clone().unwrap_or_default());
208 self.exact_path_input.set_value(
209 view.match_criteria
210 .exact_path
211 .as_ref()
212 .map(|p| p.to_string_lossy().to_string())
213 .unwrap_or_default(),
214 );
215 self.relative_path_input.set_value(
216 view.match_criteria
217 .relative_path
218 .clone()
219 .unwrap_or_default(),
220 );
221 self.path_pattern_input
222 .set_value(view.match_criteria.path_pattern.clone().unwrap_or_default());
223 self.filename_pattern_input.set_value(
224 view.match_criteria
225 .filename_pattern
226 .clone()
227 .unwrap_or_default(),
228 );
229 self.schema_match_enabled = view.match_criteria.schema_columns.is_some();
230 self.table = view.match_criteria.table.clone();
231 }
232
233 pub fn exit_form(&mut self) {
234 self.mode = ViewModalMode::List;
235 self.editing_view_id = None;
236 self.name_error = None;
237 }
238
239 pub fn close(&mut self) {
241 self.exit_form();
242 self.active = false;
243 self.score_details = None;
244 }
245}
246
247impl crate::form::Form for ViewModal {
248 type Field = FormFocus;
249
250 fn fields(&self) -> Vec<(FormFocus, crate::form::FieldKind)> {
251 use crate::form::FieldKind;
252 self.focus_order()
253 .iter()
254 .map(|&row| {
255 let kind = match row {
256 FormFocus::Description => FieldKind::MultilineText,
257 FormFocus::Matching => FieldKind::Button,
258 FormFocus::SchemaMatch => FieldKind::Checkbox,
259 _ => FieldKind::Text,
260 };
261 (row, kind)
262 })
263 .collect()
264 }
265
266 fn focused(&self) -> FormFocus {
267 self.form_focus
268 }
269
270 fn set_focused(&mut self, field: FormFocus) {
271 self.form_focus = field;
272 }
273
274 fn text_edge(&self, _field: FormFocus) -> (bool, bool) {
275 let line = self.description_input.cursor_line();
276 let last = self.description_input.line_count().saturating_sub(1);
277 (line == 0, line >= last)
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::form::Form;
285
286 #[test]
289 fn tab_walks_the_criteria_only_when_expanded() {
290 let mut modal = ViewModal::new();
291 modal.form_focus = FormFocus::Name;
292 modal.focus_next();
293 modal.focus_next();
294 assert_eq!(modal.form_focus, FormFocus::Matching);
295 modal.focus_next();
296 assert_eq!(modal.form_focus, FormFocus::Name, "collapsed wraps early");
297
298 modal.form_focus = FormFocus::Matching;
299 modal.toggle_matching();
300 modal.focus_next();
301 assert_eq!(modal.form_focus, FormFocus::ExactPath);
302 modal.focus_prev();
303 modal.focus_prev();
304 assert_eq!(modal.form_focus, FormFocus::Description);
305 }
306
307 #[test]
310 fn collapsing_rescues_focus_from_a_hidden_row() {
311 let mut modal = ViewModal::new();
312 modal.matching_expanded = true;
313 modal.form_focus = FormFocus::PathPattern;
314 modal.toggle_matching();
315 assert!(!modal.matching_expanded);
316 assert_eq!(modal.form_focus, FormFocus::Matching);
317 }
318
319 #[test]
320 fn criteria_count_counts_set_fields_and_the_schema_toggle() {
321 let mut modal = ViewModal::new();
322 assert_eq!(modal.criteria_count(), 0);
323 modal.exact_path_input.set_value("/data/x.csv");
324 modal.filename_pattern_input.set_value("x_*.csv");
325 modal.schema_match_enabled = true;
326 assert_eq!(modal.criteria_count(), 3);
327 }
328}