Skip to main content

datui_lib/
sort_modal.rs

1use crate::widgets::text_input::TextInput;
2use ratatui::widgets::TableState;
3
4#[derive(Debug, Clone)]
5pub struct SortColumn {
6    pub name: String,
7    pub sort_order: Option<usize>, // For sorting (which columns to sort by and in what order)
8    pub display_order: usize,      // For column display order
9    pub is_locked: bool,           // Whether this column is locked (and all columns before it)
10    pub is_to_be_locked: bool, // Whether this column is to-be-locked (pending, shown as dim lock)
11    pub is_visible: bool,      // Whether this column is visible in the table
12}
13
14#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
15pub enum SortFocus {
16    #[default]
17    Filter,
18    ColumnList,
19    Order,
20    Apply,
21    Cancel,
22    Clear,
23}
24
25pub struct SortModal {
26    pub active: bool,
27    pub filter_input: TextInput,
28    pub columns: Vec<SortColumn>,
29    pub table_state: TableState,
30    pub ascending: bool,
31    pub focus: SortFocus,
32    pub has_unapplied_changes: bool,
33    pub history_limit: usize,
34}
35
36impl Default for SortModal {
37    fn default() -> Self {
38        Self {
39            active: false,
40            filter_input: TextInput::new(),
41            columns: Vec::new(),
42            table_state: TableState::default(),
43            ascending: true,
44            focus: SortFocus::default(),
45            has_unapplied_changes: false,
46            history_limit: 1000,
47        }
48    }
49}
50
51impl SortModal {
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    pub fn filtered_columns(&self) -> Vec<(usize, &SortColumn)> {
57        let filter_text = self.filter_input.value.to_lowercase();
58        let mut filtered: Vec<_> = self
59            .columns
60            .iter()
61            .enumerate()
62            .filter(|(_, c)| c.name.to_lowercase().contains(&filter_text))
63            .collect();
64        // Sort by display_order to show columns in their current order
65        filtered.sort_by_key(|(_, c)| c.display_order);
66        filtered
67    }
68
69    pub fn get_column_order(&self) -> Vec<String> {
70        let mut cols: Vec<_> = self.columns.iter().filter(|c| c.is_visible).collect();
71        cols.sort_by_key(|c| c.display_order);
72        cols.into_iter().map(|c| c.name.clone()).collect()
73    }
74
75    pub fn get_locked_columns_count(&self) -> usize {
76        // Count locked columns by checking display_order
77        let mut locked_count = 0;
78        for col in &self.columns {
79            if col.is_locked {
80                locked_count = locked_count.max(col.display_order + 1);
81            }
82        }
83        locked_count
84    }
85
86    pub fn get_sorted_columns(&self) -> Vec<String> {
87        let mut sorted: Vec<_> = self
88            .columns
89            .iter()
90            .filter_map(|c| c.sort_order.map(|o| (o, c.name.clone())))
91            .collect();
92        sorted.sort_by_key(|(order, _)| *order);
93        sorted.into_iter().map(|(_, name)| name).collect()
94    }
95
96    pub fn toggle_selection(&mut self) {
97        if let Some(idx) = self.table_state.selected() {
98            let filtered = self.filtered_columns();
99            if let Some((real_idx, _)) = filtered.get(idx) {
100                let real_idx = *real_idx;
101                if let Some(old_order) = self.columns[real_idx].sort_order {
102                    self.columns[real_idx].sort_order = None;
103                    for col in &mut self.columns {
104                        if let Some(order) = col.sort_order {
105                            if order > old_order {
106                                col.sort_order = Some(order - 1);
107                            }
108                        }
109                    }
110                } else {
111                    let max_order = self
112                        .columns
113                        .iter()
114                        .filter_map(|c| c.sort_order)
115                        .max()
116                        .unwrap_or(0);
117                    self.columns[real_idx].sort_order = Some(max_order + 1);
118                }
119                self.has_unapplied_changes = true;
120            }
121        }
122    }
123
124    // Move column up in display order (left)
125    pub fn move_column_display_up(&mut self) {
126        if let Some(idx) = self.table_state.selected() {
127            let filtered = self.filtered_columns();
128            if let Some((real_idx, _)) = filtered.get(idx) {
129                let real_idx = *real_idx;
130                let current_display_order = self.columns[real_idx].display_order;
131                let was_locked = self.columns[real_idx].is_locked;
132                if current_display_order > 0 {
133                    // Find the column with display_order one less (the column we're moving above)
134                    let mut target_col_locked = false;
135                    let mut target_col_to_be_locked = false;
136                    for col in &self.columns {
137                        if col.display_order == current_display_order - 1 {
138                            target_col_locked = col.is_locked;
139                            target_col_to_be_locked = col.is_to_be_locked;
140                            break;
141                        }
142                    }
143
144                    // Swap display orders
145                    for col in &mut self.columns {
146                        if col.display_order == current_display_order - 1 {
147                            col.display_order = current_display_order;
148                            break;
149                        }
150                    }
151                    let new_display_order = current_display_order - 1;
152                    let was_to_be_locked = self.columns[real_idx].is_to_be_locked;
153
154                    // Find the last column in the lock/to-be-locked section BEFORE the move
155                    // (needed to check if this is the last one)
156                    let last_locked_or_to_be_order = self
157                        .columns
158                        .iter()
159                        .filter(|c| c.is_locked || c.is_to_be_locked)
160                        .map(|c| c.display_order)
161                        .max()
162                        .unwrap_or(0);
163
164                    // Swap display orders
165                    self.columns[real_idx].display_order = new_display_order;
166
167                    // If moving an unlocked column into a locked region, inherit the lock status
168                    if !was_locked && !was_to_be_locked {
169                        if target_col_locked || target_col_to_be_locked {
170                            // The column we moved above is locked or to-be-locked, so this column should match
171                            self.columns[real_idx].is_locked = target_col_locked;
172                            self.columns[real_idx].is_to_be_locked = target_col_to_be_locked;
173                        }
174                    } else {
175                        // When moving a locked or to-be-locked column up, check if it's the last one in the lock/to-be-locked section
176                        // Only clear to-be-locked if this is the last column in the lock/to-be-locked section
177                        if (was_locked || was_to_be_locked)
178                            && current_display_order == last_locked_or_to_be_order
179                        {
180                            // Clear to-be-locked for columns that are now at positions between new and old (exclusive of new, inclusive of old)
181                            // After swap: the column that was at new_display_order is now at current_display_order
182                            // We want to clear to-be-locked for columns at positions > new_display_order and <= current_display_order
183                            // but don't remove real locks
184                            for col in &mut self.columns {
185                                if col.display_order > new_display_order
186                                    && col.display_order <= current_display_order
187                                    && col.is_to_be_locked
188                                {
189                                    col.is_to_be_locked = false;
190                                }
191                            }
192                        }
193                    }
194
195                    self.has_unapplied_changes = true;
196                    // Update selection to follow the moved item
197                    if let Some(new_selected_idx) = self
198                        .filtered_columns()
199                        .iter()
200                        .position(|&(idx, _)| idx == real_idx)
201                    {
202                        self.table_state.select(Some(new_selected_idx));
203                    }
204                }
205            }
206        }
207    }
208
209    // Move column down in display order (right)
210    pub fn move_column_display_down(&mut self) {
211        if let Some(idx) = self.table_state.selected() {
212            let filtered = self.filtered_columns();
213            if let Some((real_idx, _)) = filtered.get(idx) {
214                let real_idx = *real_idx;
215                let max_display_order = self
216                    .columns
217                    .iter()
218                    .map(|c| c.display_order)
219                    .max()
220                    .unwrap_or(0);
221                let current_display_order = self.columns[real_idx].display_order;
222                let was_locked = self.columns[real_idx].is_locked;
223                if current_display_order < max_display_order {
224                    // Find the column with display_order one more
225                    for col in &mut self.columns {
226                        if col.display_order == current_display_order + 1 {
227                            col.display_order = current_display_order;
228                            break;
229                        }
230                    }
231                    let new_display_order = current_display_order + 1;
232                    let was_to_be_locked = self.columns[real_idx].is_to_be_locked;
233
234                    // Swap display orders first
235                    self.columns[real_idx].display_order = new_display_order;
236
237                    // If a locked or to-be-locked column is moved down, mark any unlocked columns it crosses as to-be-locked
238                    // After swap: the column that was at new_display_order is now at current_display_order
239                    // We need to mark columns that are now at positions from old position (inclusive) to new position (exclusive)
240                    // Excluding the moved column itself (which is now at new_display_order)
241                    if was_locked || was_to_be_locked {
242                        for (idx, col) in self.columns.iter_mut().enumerate() {
243                            // Mark columns that are now at positions from old position (inclusive) to new position (exclusive)
244                            // Exclude the moved column itself
245                            if idx != real_idx
246                                && col.display_order >= current_display_order
247                                && col.display_order < new_display_order
248                                && !col.is_locked
249                            {
250                                col.is_to_be_locked = true;
251                            }
252                        }
253                    }
254
255                    self.has_unapplied_changes = true;
256                    // Update selection to follow the moved item
257                    if let Some(new_selected_idx) = self
258                        .filtered_columns()
259                        .iter()
260                        .position(|&(idx, _)| idx == real_idx)
261                    {
262                        self.table_state.select(Some(new_selected_idx));
263                    }
264                }
265            }
266        }
267    }
268
269    // Toggle lock at this column (lock all columns up to and including this one)
270    pub fn toggle_lock_at_column(&mut self) {
271        if let Some(idx) = self.table_state.selected() {
272            let filtered = self.filtered_columns();
273            if let Some((real_idx, _)) = filtered.get(idx) {
274                let real_idx = *real_idx;
275                let target_display_order = self.columns[real_idx].display_order;
276
277                // Count how many columns are currently locked
278                let current_locked_count = self.columns.iter().filter(|c| c.is_locked).count();
279
280                // If clicking on a locked column or the first unlocked column, toggle lock boundary
281                if target_display_order < current_locked_count {
282                    // Unlock: set locked count to target_display_order
283                    for col in &mut self.columns {
284                        col.is_locked = col.display_order < target_display_order;
285                        col.is_to_be_locked = false; // Clear to-be-locked when unlocking
286                    }
287                } else {
288                    // Lock: set locked count to target_display_order + 1
289                    for col in &mut self.columns {
290                        col.is_locked = col.display_order <= target_display_order;
291                        col.is_to_be_locked = false; // Clear to-be-locked when applying locks
292                    }
293                }
294                self.has_unapplied_changes = true;
295            }
296        }
297    }
298
299    pub fn move_selection_up(&mut self) {
300        if let Some(idx) = self.table_state.selected() {
301            let filtered = self.filtered_columns();
302            if let Some((real_idx, _)) = filtered.get(idx) {
303                let real_idx = *real_idx;
304                if let Some(current_order) = self.columns[real_idx].sort_order {
305                    if current_order > 1 {
306                        for col in &mut self.columns {
307                            if col.sort_order == Some(current_order - 1) {
308                                col.sort_order = Some(current_order);
309                                break;
310                            }
311                        }
312                        self.columns[real_idx].sort_order = Some(current_order - 1);
313                        self.has_unapplied_changes = true;
314                        // Update selection to follow the moved item
315                        if let Some(new_selected_idx) = self
316                            .filtered_columns()
317                            .iter()
318                            .position(|&(idx, _)| idx == real_idx)
319                        {
320                            self.table_state.select(Some(new_selected_idx));
321                        }
322                    }
323                }
324            }
325        }
326    }
327
328    pub fn move_selection_down(&mut self) {
329        if let Some(idx) = self.table_state.selected() {
330            let filtered = self.filtered_columns();
331            if let Some((real_idx, _)) = filtered.get(idx) {
332                let real_idx = *real_idx;
333                let max_order = self
334                    .columns
335                    .iter()
336                    .filter_map(|c| c.sort_order)
337                    .max()
338                    .unwrap_or(0);
339                if let Some(current_order) = self.columns[real_idx].sort_order {
340                    if current_order < max_order {
341                        for col in &mut self.columns {
342                            if col.sort_order == Some(current_order + 1) {
343                                col.sort_order = Some(current_order);
344                                break;
345                            }
346                        }
347                        self.columns[real_idx].sort_order = Some(current_order + 1);
348                        // Update selection to follow the moved item
349                        if let Some(new_selected_idx) = self
350                            .filtered_columns()
351                            .iter()
352                            .position(|&(idx, _)| idx == real_idx)
353                        {
354                            self.table_state.select(Some(new_selected_idx));
355                        }
356                        self.has_unapplied_changes = true;
357                    }
358                }
359            }
360        }
361    }
362
363    pub fn next_focus(&mut self) {
364        self.focus = match self.focus {
365            SortFocus::Filter => SortFocus::ColumnList,
366            SortFocus::ColumnList => SortFocus::Order,
367            SortFocus::Order => SortFocus::Apply,
368            SortFocus::Apply => SortFocus::Cancel,
369            SortFocus::Cancel => SortFocus::Clear,
370            SortFocus::Clear => SortFocus::Filter,
371        };
372    }
373
374    pub fn prev_focus(&mut self) {
375        self.focus = match self.focus {
376            SortFocus::Filter => SortFocus::Clear,
377            SortFocus::ColumnList => SortFocus::Filter,
378            SortFocus::Order => SortFocus::ColumnList,
379            SortFocus::Apply => SortFocus::Order,
380            SortFocus::Cancel => SortFocus::Apply,
381            SortFocus::Clear => SortFocus::Cancel,
382        };
383    }
384
385    /// Advance focus within body only (Filter → ColumnList → Order). Returns true if we were on
386    /// Order and caller should move to footer (Apply).
387    pub fn next_body_focus(&mut self) -> bool {
388        match self.focus {
389            SortFocus::Order => return true,
390            SortFocus::Filter => self.focus = SortFocus::ColumnList,
391            SortFocus::ColumnList => self.focus = SortFocus::Order,
392            SortFocus::Apply | SortFocus::Cancel | SortFocus::Clear => {}
393        }
394        false
395    }
396
397    /// Retreat focus within body only. Returns true if we were on Filter and caller should move to TabBar.
398    pub fn prev_body_focus(&mut self) -> bool {
399        match self.focus {
400            SortFocus::Filter => return true,
401            SortFocus::ColumnList => self.focus = SortFocus::Filter,
402            SortFocus::Order => self.focus = SortFocus::ColumnList,
403            SortFocus::Apply | SortFocus::Cancel | SortFocus::Clear => {}
404        }
405        false
406    }
407
408    pub fn clear_selection(&mut self) {
409        // Reset all column state: clear sorting, unlock all, reset display order
410        for (idx, col) in self.columns.iter_mut().enumerate() {
411            col.sort_order = None;
412            col.is_locked = false;
413            col.is_to_be_locked = false;
414            col.display_order = idx; // Reset to natural order (0, 1, 2, ...)
415            col.is_visible = true; // Make all columns visible
416        }
417        self.has_unapplied_changes = true;
418    }
419
420    pub fn toggle_visibility(&mut self) {
421        if let Some(idx) = self.table_state.selected() {
422            let filtered = self.filtered_columns();
423            if let Some((real_idx, _)) = filtered.get(idx) {
424                let real_idx = *real_idx;
425
426                // Calculate max order before mutating
427                let max_order = if self.columns[real_idx].is_visible {
428                    0 // Will be recalculated if showing
429                } else {
430                    self.columns
431                        .iter()
432                        .filter(|c| c.is_visible)
433                        .map(|c| c.display_order)
434                        .max()
435                        .unwrap_or(0)
436                };
437
438                let col = &mut self.columns[real_idx];
439
440                if col.is_visible {
441                    // Hiding: clear display order (set to a high value to push to end) and remove locked status
442                    col.is_visible = false;
443                    col.display_order = 9999; // High value to push hidden columns to end
444                    col.is_locked = false; // Remove locked status when hiding
445                    col.is_to_be_locked = false; // Remove to-be-locked status when hiding
446                } else {
447                    // Showing: assign next available display order (don't restore locked status)
448                    col.is_visible = true;
449                    col.display_order = max_order + 1;
450                    // Don't restore locked status - it stays false
451                }
452                self.has_unapplied_changes = true;
453            }
454        }
455    }
456
457    pub fn jump_selection_to_order(&mut self, new_order: usize) {
458        if let Some(idx) = self.table_state.selected() {
459            let filtered = self.filtered_columns();
460            if let Some((real_idx, _)) = filtered.get(idx) {
461                let real_idx = *real_idx;
462                let max_order = self
463                    .columns
464                    .iter()
465                    .filter_map(|c| c.sort_order)
466                    .max()
467                    .unwrap_or(0);
468
469                if new_order > 0 && new_order <= max_order + 1 {
470                    let old_order = self.columns[real_idx].sort_order;
471                    let selected_column_name = self.columns[real_idx].name.clone();
472
473                    // Adjust existing orders
474                    for col in &mut self.columns {
475                        if col.name == selected_column_name {
476                            continue; // Skip the selected column for now
477                        }
478                        if let Some(order) = col.sort_order {
479                            if let Some(old) = old_order {
480                                if new_order < old && order >= new_order && order < old {
481                                    col.sort_order = Some(order + 1);
482                                } else if new_order > old && order <= new_order && order > old {
483                                    col.sort_order = Some(order - 1);
484                                }
485                            } else {
486                                // If the selected column was not sorted before
487                                if order >= new_order {
488                                    col.sort_order = Some(order + 1);
489                                }
490                            }
491                        }
492                    }
493                    self.columns[real_idx].sort_order = Some(new_order);
494
495                    // Re-number to ensure continuous sequence if a gap was created or an item was removed
496                    let mut current_sorted_cols: Vec<(&mut SortColumn, usize)> = self
497                        .columns
498                        .iter_mut()
499                        .filter_map(|c| c.sort_order.map(|o| (c, o)))
500                        .collect();
501                    current_sorted_cols.sort_by_key(|(_, o)| *o);
502
503                    for (i, (col, _)) in current_sorted_cols.into_iter().enumerate() {
504                        col.sort_order = Some(i + 1);
505                    }
506
507                    // Update selection to follow the moved item
508                    if let Some(new_selected_idx) = self
509                        .filtered_columns()
510                        .iter()
511                        .position(|&(r_idx, _)| r_idx == real_idx)
512                    {
513                        self.table_state.select(Some(new_selected_idx));
514                    }
515                    self.has_unapplied_changes = true;
516                } else if new_order == 0 {
517                    // User wants to unset sort order
518                    self.columns[real_idx].sort_order = None;
519                    // Re-number to ensure continuous sequence
520                    let mut current_sorted_cols: Vec<(&mut SortColumn, usize)> = self
521                        .columns
522                        .iter_mut()
523                        .filter_map(|c| c.sort_order.map(|o| (c, o)))
524                        .collect();
525                    current_sorted_cols.sort_by_key(|(_, o)| *o);
526
527                    for (i, (col, _)) in current_sorted_cols.into_iter().enumerate() {
528                        col.sort_order = Some(i + 1);
529                    }
530                    // Selection should remain on the same column even if its sort order is removed
531                    if let Some(new_selected_idx) = self
532                        .filtered_columns()
533                        .iter()
534                        .position(|&(r_idx, _)| r_idx == real_idx)
535                    {
536                        self.table_state.select(Some(new_selected_idx));
537                    }
538                }
539            }
540        }
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn test_sort_modal_new() {
550        let modal = SortModal::new();
551        assert!(!modal.active);
552        assert_eq!(modal.filter_input.value, "");
553        assert!(modal.columns.is_empty());
554        assert!(modal.table_state.selected().is_none());
555        assert!(modal.ascending);
556        assert_eq!(modal.focus, SortFocus::Filter);
557    }
558
559    #[test]
560    fn test_filtered_columns() {
561        let mut modal = SortModal::new();
562        modal.columns = vec![
563            SortColumn {
564                name: "Apple".to_string(),
565                sort_order: None,
566                display_order: 0,
567                is_locked: false,
568                is_to_be_locked: false,
569                is_visible: true,
570            },
571            SortColumn {
572                name: "Banana".to_string(),
573                sort_order: None,
574                display_order: 1,
575                is_locked: false,
576                is_to_be_locked: false,
577                is_visible: true,
578            },
579            SortColumn {
580                name: "Orange".to_string(),
581                sort_order: None,
582                display_order: 2,
583                is_locked: false,
584                is_to_be_locked: false,
585                is_visible: true,
586            },
587        ];
588        modal.filter_input.value = "an".to_string();
589        let filtered = modal.filtered_columns();
590        assert_eq!(filtered.len(), 2);
591        assert_eq!(filtered[0].1.name, "Banana");
592        assert_eq!(filtered[1].1.name, "Orange");
593    }
594
595    #[test]
596    fn test_toggle_selection() {
597        let mut modal = SortModal::new();
598        modal.columns = vec![
599            SortColumn {
600                name: "A".to_string(),
601                sort_order: None,
602                display_order: 0,
603                is_locked: false,
604                is_to_be_locked: false,
605                is_visible: true,
606            },
607            SortColumn {
608                name: "B".to_string(),
609                sort_order: None,
610                display_order: 1,
611                is_locked: false,
612                is_to_be_locked: false,
613                is_visible: true,
614            },
615            SortColumn {
616                name: "C".to_string(),
617                sort_order: None,
618                display_order: 2,
619                is_locked: false,
620                is_to_be_locked: false,
621                is_visible: true,
622            },
623        ];
624        modal.table_state.select(Some(1)); // Select "B"
625        modal.toggle_selection();
626        assert_eq!(modal.columns[0].sort_order, None);
627        assert_eq!(modal.columns[1].sort_order, Some(1));
628        assert_eq!(modal.columns[2].sort_order, None);
629
630        modal.table_state.select(Some(0)); // Select "A"
631        modal.toggle_selection();
632        assert_eq!(modal.columns[0].sort_order, Some(2));
633        assert_eq!(modal.columns[1].sort_order, Some(1));
634        assert_eq!(modal.columns[2].sort_order, None);
635
636        modal.table_state.select(Some(1)); // Deselect "B"
637        modal.toggle_selection();
638        assert_eq!(modal.columns[0].sort_order, Some(1));
639        assert_eq!(modal.columns[1].sort_order, None);
640        assert_eq!(modal.columns[2].sort_order, None);
641    }
642
643    #[test]
644    fn test_get_sorted_columns() {
645        let mut modal = SortModal::new();
646        modal.columns = vec![
647            SortColumn {
648                name: "A".to_string(),
649                sort_order: Some(2),
650                display_order: 0,
651                is_locked: false,
652                is_to_be_locked: false,
653                is_visible: true,
654            },
655            SortColumn {
656                name: "B".to_string(),
657                sort_order: Some(1),
658                display_order: 1,
659                is_locked: false,
660                is_to_be_locked: false,
661                is_visible: true,
662            },
663            SortColumn {
664                name: "C".to_string(),
665                sort_order: None,
666                display_order: 2,
667                is_locked: false,
668                is_to_be_locked: false,
669                is_visible: true,
670            },
671        ];
672        assert_eq!(modal.get_sorted_columns(), vec!["B", "A"]);
673    }
674
675    #[test]
676    fn test_move_selection_up() {
677        let mut modal = SortModal::new();
678        modal.columns = vec![
679            SortColumn {
680                name: "A".to_string(),
681                sort_order: Some(2),
682                display_order: 0,
683                is_locked: false,
684                is_to_be_locked: false,
685                is_visible: true,
686            },
687            SortColumn {
688                name: "B".to_string(),
689                sort_order: Some(1),
690                display_order: 1,
691                is_locked: false,
692                is_to_be_locked: false,
693                is_visible: true,
694            },
695        ];
696        modal.table_state.select(Some(0)); // Select "A"
697        modal.move_selection_up();
698        assert_eq!(modal.columns[0].sort_order, Some(1));
699        assert_eq!(modal.columns[1].sort_order, Some(2));
700    }
701
702    #[test]
703    fn test_move_selection_down() {
704        let mut modal = SortModal::new();
705        modal.columns = vec![
706            SortColumn {
707                name: "A".to_string(),
708                sort_order: Some(2),
709                display_order: 0,
710                is_locked: false,
711                is_to_be_locked: false,
712                is_visible: true,
713            },
714            SortColumn {
715                name: "B".to_string(),
716                sort_order: Some(1),
717                display_order: 1,
718                is_locked: false,
719                is_to_be_locked: false,
720                is_visible: true,
721            },
722        ];
723        modal.table_state.select(Some(1)); // Select "B"
724        modal.move_selection_down();
725        assert_eq!(modal.columns[0].sort_order, Some(1));
726        assert_eq!(modal.columns[1].sort_order, Some(2));
727    }
728
729    #[test]
730    fn test_next_focus() {
731        let mut modal = SortModal::new();
732        assert_eq!(modal.focus, SortFocus::Filter);
733        modal.next_focus();
734        assert_eq!(modal.focus, SortFocus::ColumnList);
735        modal.next_focus();
736        assert_eq!(modal.focus, SortFocus::Order);
737        modal.next_focus();
738        assert_eq!(modal.focus, SortFocus::Apply);
739        modal.next_focus();
740        assert_eq!(modal.focus, SortFocus::Cancel);
741        modal.next_focus();
742        assert_eq!(modal.focus, SortFocus::Clear);
743        modal.next_focus();
744        assert_eq!(modal.focus, SortFocus::Filter);
745    }
746
747    #[test]
748    fn test_prev_focus() {
749        let mut modal = SortModal::new();
750        assert_eq!(modal.focus, SortFocus::Filter);
751        modal.prev_focus();
752        assert_eq!(modal.focus, SortFocus::Clear);
753        modal.prev_focus();
754        assert_eq!(modal.focus, SortFocus::Cancel);
755        modal.prev_focus();
756        assert_eq!(modal.focus, SortFocus::Apply);
757        modal.prev_focus();
758        assert_eq!(modal.focus, SortFocus::Order);
759        modal.prev_focus();
760        assert_eq!(modal.focus, SortFocus::ColumnList);
761        modal.prev_focus();
762        assert_eq!(modal.focus, SortFocus::Filter);
763    }
764
765    #[test]
766    fn test_clear_selection() {
767        let mut modal = SortModal::new();
768        modal.columns = vec![
769            SortColumn {
770                name: "A".to_string(),
771                sort_order: Some(1),
772                display_order: 0,
773                is_locked: false,
774                is_to_be_locked: false,
775                is_visible: true,
776            },
777            SortColumn {
778                name: "B".to_string(),
779                sort_order: Some(2),
780                display_order: 1,
781                is_locked: false,
782                is_to_be_locked: false,
783                is_visible: true,
784            },
785        ];
786        modal.clear_selection();
787        assert!(modal.columns[0].sort_order.is_none());
788        assert!(modal.columns[1].sort_order.is_none());
789    }
790}