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