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>, pub display_order: usize, pub is_locked: bool, pub is_to_be_locked: bool, pub is_visible: bool, }
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 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 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 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 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 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 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 self.columns[real_idx].display_order = new_display_order;
166
167 if !was_locked && !was_to_be_locked {
169 if target_col_locked || target_col_to_be_locked {
170 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 if (was_locked || was_to_be_locked)
178 && current_display_order == last_locked_or_to_be_order
179 {
180 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 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 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 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 self.columns[real_idx].display_order = new_display_order;
236
237 if was_locked || was_to_be_locked {
242 for (idx, col) in self.columns.iter_mut().enumerate() {
243 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 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 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 let current_locked_count = self.columns.iter().filter(|c| c.is_locked).count();
279
280 if target_display_order < current_locked_count {
282 for col in &mut self.columns {
284 col.is_locked = col.display_order < target_display_order;
285 col.is_to_be_locked = false; }
287 } else {
288 for col in &mut self.columns {
290 col.is_locked = col.display_order <= target_display_order;
291 col.is_to_be_locked = false; }
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 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 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 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 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 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; col.is_visible = true; }
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 let max_order = if self.columns[real_idx].is_visible {
428 0 } 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 col.is_visible = false;
443 col.display_order = 9999; col.is_locked = false; col.is_to_be_locked = false; } else {
447 col.is_visible = true;
449 col.display_order = max_order + 1;
450 }
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 for col in &mut self.columns {
475 if col.name == selected_column_name {
476 continue; }
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 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 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 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 self.columns[real_idx].sort_order = None;
519 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 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)); 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)); 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)); 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)); 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)); 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}