Skip to main content

datui_lib/
filter_modal.rs

1use ratatui::widgets::ListState;
2
3#[derive(Debug, Clone, PartialEq, Eq, Copy, serde::Serialize, serde::Deserialize)]
4pub enum FilterOperator {
5    Eq,
6    NotEq,
7    Gt,
8    Lt,
9    GtEq,
10    LtEq,
11    Contains,
12    NotContains,
13}
14
15impl FilterOperator {
16    pub fn as_str(&self) -> &'static str {
17        match self {
18            FilterOperator::Eq => "=",
19            FilterOperator::NotEq => "!=",
20            FilterOperator::Gt => ">",
21            FilterOperator::Lt => "<",
22            FilterOperator::GtEq => ">=",
23            FilterOperator::LtEq => "<=",
24            FilterOperator::Contains => "contains",
25            FilterOperator::NotContains => "!contains",
26        }
27    }
28
29    pub fn iterator() -> impl Iterator<Item = FilterOperator> {
30        [
31            FilterOperator::Eq,
32            FilterOperator::NotEq,
33            FilterOperator::Gt,
34            FilterOperator::Lt,
35            FilterOperator::GtEq,
36            FilterOperator::LtEq,
37            FilterOperator::Contains,
38            FilterOperator::NotContains,
39        ]
40        .iter()
41        .copied()
42    }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Copy, serde::Serialize, serde::Deserialize)]
46pub enum LogicalOperator {
47    And,
48    Or,
49}
50
51impl LogicalOperator {
52    pub fn as_str(&self) -> &'static str {
53        match self {
54            LogicalOperator::And => "AND",
55            LogicalOperator::Or => "OR",
56        }
57    }
58
59    pub fn iterator() -> impl Iterator<Item = LogicalOperator> {
60        [LogicalOperator::And, LogicalOperator::Or].iter().copied()
61    }
62}
63
64#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
65pub struct FilterStatement {
66    pub column: String,
67    pub operator: FilterOperator,
68    pub value: String,
69    pub logical_op: LogicalOperator,
70}
71
72#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
73pub enum FilterFocus {
74    #[default]
75    Column,
76    Operator,
77    Value,
78    Logical,
79    Add,
80    Statements,
81    Confirm,
82    Clear,
83}
84
85#[derive(Default)]
86pub struct FilterModal {
87    pub active: bool,
88    pub statements: Vec<FilterStatement>,
89    pub available_columns: Vec<String>,
90
91    pub new_column_idx: usize,
92    pub new_operator_idx: usize,
93    pub new_value: String,
94    pub new_logical_idx: usize,
95
96    pub focus: FilterFocus,
97    pub list_state: ListState,
98}
99
100impl FilterModal {
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    pub fn add_statement(&mut self) {
106        if self.available_columns.is_empty() {
107            return;
108        }
109        let op = FilterOperator::iterator()
110            .nth(self.new_operator_idx)
111            .unwrap();
112        let log = LogicalOperator::iterator()
113            .nth(self.new_logical_idx)
114            .unwrap();
115        let col = self.available_columns[self.new_column_idx].clone();
116
117        self.statements.push(FilterStatement {
118            column: col,
119            operator: op,
120            value: self.new_value.clone(),
121            logical_op: log,
122        });
123
124        self.new_value.clear();
125        self.focus = FilterFocus::Column;
126    }
127
128    /// Advance focus within body only (Column → ... → Statements). Returns true if we were on
129    /// Statements and caller should move to footer (Apply).
130    pub fn next_body_focus(&mut self) -> bool {
131        match self.focus {
132            FilterFocus::Statements => return true,
133            FilterFocus::Column => self.focus = FilterFocus::Operator,
134            FilterFocus::Operator => self.focus = FilterFocus::Value,
135            FilterFocus::Value => self.focus = FilterFocus::Logical,
136            FilterFocus::Logical => self.focus = FilterFocus::Add,
137            FilterFocus::Add => self.focus = FilterFocus::Statements,
138            FilterFocus::Confirm | FilterFocus::Clear => {}
139        }
140        false
141    }
142
143    /// Retreat focus within body only. Returns true if we were on Column and caller should move to TabBar.
144    pub fn prev_body_focus(&mut self) -> bool {
145        match self.focus {
146            FilterFocus::Column => return true,
147            FilterFocus::Operator => self.focus = FilterFocus::Column,
148            FilterFocus::Value => self.focus = FilterFocus::Operator,
149            FilterFocus::Logical => self.focus = FilterFocus::Value,
150            FilterFocus::Add => self.focus = FilterFocus::Logical,
151            FilterFocus::Statements => self.focus = FilterFocus::Add,
152            FilterFocus::Confirm | FilterFocus::Clear => {}
153        }
154        false
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_filter_modal_new() {
164        let modal = FilterModal::new();
165        assert!(!modal.active);
166        assert!(modal.statements.is_empty());
167        assert!(modal.available_columns.is_empty());
168        assert_eq!(modal.new_column_idx, 0);
169        assert_eq!(modal.new_operator_idx, 0);
170        assert_eq!(modal.new_value, "");
171        assert_eq!(modal.new_logical_idx, 0);
172        assert_eq!(modal.focus, FilterFocus::Column);
173    }
174
175    #[test]
176    fn test_filter_modal_add_statement() {
177        let mut modal = FilterModal::new();
178        modal.available_columns = vec!["a".to_string(), "b".to_string()];
179        modal.new_column_idx = 1;
180        modal.new_operator_idx = 2; // Gt
181        modal.new_value = "10".to_string();
182        modal.new_logical_idx = 1; // Or
183        modal.add_statement();
184
185        assert_eq!(modal.statements.len(), 1);
186        let statement = &modal.statements[0];
187        assert_eq!(statement.column, "b");
188        assert_eq!(statement.operator, FilterOperator::Gt);
189        assert_eq!(statement.value, "10");
190        assert_eq!(statement.logical_op, LogicalOperator::Or);
191
192        assert_eq!(modal.new_value, "");
193        assert_eq!(modal.focus, FilterFocus::Column);
194    }
195
196    #[test]
197    fn test_add_statement_no_columns() {
198        let mut modal = FilterModal::new();
199        modal.new_value = "test".to_string();
200        modal.add_statement();
201        assert!(modal.statements.is_empty());
202    }
203}