// src/pages/admin_panel/add_logic/state.rs use crate::config::binds::config::{EditorConfig, EditorKeybindingMode}; use crate::components::common::text_editor::{TextEditor, VimState}; use canvas::{DataProvider, AppMode, FormEditor, SuggestionItem}; use crossterm::event::KeyCode; use std::cell::RefCell; use std::rc::Rc; use tui_textarea::TextArea; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AddLogicFocus { #[default] InputLogicName, InputTargetColumn, InputDescription, ScriptContentPreview, InsideScriptContent, SaveButton, CancelButton, } #[derive(Clone, Debug)] pub struct AddLogicState { pub profile_name: String, pub selected_table_id: Option, pub selected_table_name: Option, pub logic_name_input: String, pub target_column_input: String, pub script_content_editor: Rc>>, pub description_input: String, pub current_focus: AddLogicFocus, pub last_canvas_field: usize, pub logic_name_cursor_pos: usize, pub target_column_cursor_pos: usize, pub description_cursor_pos: usize, pub has_unsaved_changes: bool, pub editor_keybinding_mode: EditorKeybindingMode, pub vim_state: VimState, // New fields for Target Column Autocomplete pub table_columns_for_suggestions: Vec, // All columns for the table pub target_column_suggestions: Vec, // Filtered suggestions pub show_target_column_suggestions: bool, pub selected_target_column_suggestion_index: Option, pub in_target_column_suggestion_mode: bool, // Script Editor Autocomplete pub script_editor_autocomplete_active: bool, pub script_editor_suggestions: Vec, pub script_editor_selected_suggestion_index: Option, pub script_editor_trigger_position: Option<(usize, usize)>, // (line, column) pub all_table_names: Vec, pub script_editor_filter_text: String, // New fields for same-profile table names and column autocomplete pub same_profile_table_names: Vec, // Tables from same profile only pub script_editor_awaiting_column_autocomplete: Option, // Table name waiting for column fetch pub app_mode: canvas::AppMode, } impl AddLogicState { pub fn new(editor_config: &EditorConfig) -> Self { let editor = TextEditor::new_textarea(editor_config); AddLogicState { profile_name: "default".to_string(), selected_table_id: None, selected_table_name: None, logic_name_input: String::new(), target_column_input: String::new(), script_content_editor: Rc::new(RefCell::new(editor)), description_input: String::new(), current_focus: AddLogicFocus::InputLogicName, last_canvas_field: 2, logic_name_cursor_pos: 0, target_column_cursor_pos: 0, description_cursor_pos: 0, has_unsaved_changes: false, editor_keybinding_mode: editor_config.keybinding_mode.clone(), vim_state: VimState::default(), table_columns_for_suggestions: Vec::new(), target_column_suggestions: Vec::new(), show_target_column_suggestions: false, selected_target_column_suggestion_index: None, in_target_column_suggestion_mode: false, script_editor_autocomplete_active: false, script_editor_suggestions: Vec::new(), script_editor_selected_suggestion_index: None, script_editor_trigger_position: None, all_table_names: Vec::new(), script_editor_filter_text: String::new(), same_profile_table_names: Vec::new(), script_editor_awaiting_column_autocomplete: None, app_mode: canvas::AppMode::Edit, } } pub const INPUT_FIELD_COUNT: usize = 3; /// Build canvas SuggestionItem list for target column pub fn column_suggestions_sync(&self, query: &str) -> Vec { let q = query.to_lowercase(); self.table_columns_for_suggestions .iter() .filter(|c| q.is_empty() || c.to_lowercase().contains(&q)) .map(|c| SuggestionItem { display_text: c.clone(), value_to_store: c.clone(), }) .collect() } /// Updates the target_column_suggestions based on current input. pub fn update_target_column_suggestions(&mut self) { let current_input = self.target_column_input.to_lowercase(); if self.table_columns_for_suggestions.is_empty() { self.target_column_suggestions.clear(); self.show_target_column_suggestions = false; self.selected_target_column_suggestion_index = None; return; } if current_input.is_empty() { self.target_column_suggestions = self.table_columns_for_suggestions.clone(); } else { self.target_column_suggestions = self .table_columns_for_suggestions .iter() .filter(|name| name.to_lowercase().contains(¤t_input)) .cloned() .collect(); } self.show_target_column_suggestions = !self.target_column_suggestions.is_empty(); if self.show_target_column_suggestions { if let Some(selected_idx) = self.selected_target_column_suggestion_index { if selected_idx >= self.target_column_suggestions.len() { self.selected_target_column_suggestion_index = Some(0); } } else { self.selected_target_column_suggestion_index = Some(0); } } else { self.selected_target_column_suggestion_index = None; } } /// Updates script editor suggestions based on current filter text pub fn update_script_editor_suggestions(&mut self) { let mut suggestions = vec!["sql".to_string()]; if self.selected_table_name.is_some() { suggestions.extend(self.table_columns_for_suggestions.clone()); } let current_selected_table_name = self.selected_table_name.as_deref(); suggestions.extend( self.same_profile_table_names .iter() .filter(|tn| Some(tn.as_str()) != current_selected_table_name) .cloned() ); if self.script_editor_filter_text.is_empty() { self.script_editor_suggestions = suggestions; } else { let filter_lower = self.script_editor_filter_text.to_lowercase(); self.script_editor_suggestions = suggestions .into_iter() .filter(|suggestion| suggestion.to_lowercase().contains(&filter_lower)) .collect(); } // Update selection index if self.script_editor_suggestions.is_empty() { self.script_editor_selected_suggestion_index = None; self.script_editor_autocomplete_active = false; } else if let Some(selected_idx) = self.script_editor_selected_suggestion_index { if selected_idx >= self.script_editor_suggestions.len() { self.script_editor_selected_suggestion_index = Some(0); } } else { self.script_editor_selected_suggestion_index = Some(0); } } /// Checks if a suggestion is a table name (for triggering column autocomplete) pub fn is_table_name_suggestion(&self, suggestion: &str) -> bool { // Not "sql" if suggestion == "sql" { return false; } if self.table_columns_for_suggestions.contains(&suggestion.to_string()) { return false; } self.same_profile_table_names.contains(&suggestion.to_string()) } /// Sets table columns for autocomplete suggestions pub fn set_table_columns(&mut self, columns: Vec) { self.table_columns_for_suggestions = columns.clone(); if !columns.is_empty() { self.update_target_column_suggestions(); } } /// Sets all available table names for autocomplete suggestions pub fn set_all_table_names(&mut self, table_names: Vec) { self.all_table_names = table_names; } /// Sets table names from the same profile for autocomplete suggestions pub fn set_same_profile_table_names(&mut self, table_names: Vec) { self.same_profile_table_names = table_names; } /// Triggers waiting for column autocomplete for a specific table pub fn trigger_column_autocomplete_for_table(&mut self, table_name: String) { self.script_editor_awaiting_column_autocomplete = Some(table_name); } /// Updates autocomplete with columns for a specific table pub fn set_columns_for_table_autocomplete(&mut self, columns: Vec) { self.script_editor_suggestions = columns; self.script_editor_selected_suggestion_index = if self.script_editor_suggestions.is_empty() { None } else { Some(0) }; self.script_editor_autocomplete_active = !self.script_editor_suggestions.is_empty(); self.script_editor_awaiting_column_autocomplete = None; } /// Deactivates script editor autocomplete and clears related state pub fn deactivate_script_editor_autocomplete(&mut self) { self.script_editor_autocomplete_active = false; self.script_editor_suggestions.clear(); self.script_editor_selected_suggestion_index = None; self.script_editor_trigger_position = None; self.script_editor_filter_text.clear(); } /// Helper method to validate and save logic pub fn save_logic(&mut self) -> Option { if self.logic_name_input.trim().is_empty() { return Some("Logic name is required".to_string()); } if self.target_column_input.trim().is_empty() { return Some("Target column is required".to_string()); } let script_content = { let editor_borrow = self.script_content_editor.borrow(); editor_borrow.lines().join("\n") }; if script_content.trim().is_empty() { return Some("Script content is required".to_string()); } // Here you would typically save to database/storage // For now, just clear the form and mark as saved self.has_unsaved_changes = false; Some(format!("Logic '{}' saved successfully", self.logic_name_input.trim())) } /// Helper method to clear the form pub fn clear_form(&mut self) -> Option { let profile = self.profile_name.clone(); let table_id = self.selected_table_id; let table_name = self.selected_table_name.clone(); let editor_config = EditorConfig::default(); // You might want to preserve the actual config *self = Self::new(&editor_config); self.profile_name = profile; self.selected_table_id = table_id; self.selected_table_name = table_name; Some("Form cleared".to_string()) } } impl Default for AddLogicState { fn default() -> Self { let mut state = Self::new(&EditorConfig::default()); state.app_mode = canvas::AppMode::Edit; state } } impl DataProvider for AddLogicState { fn field_count(&self) -> usize { 3 // Logic Name, Target Column, Description } fn field_name(&self, index: usize) -> &str { match index { 0 => "Logic Name", 1 => "Target Column", 2 => "Description", _ => "", } } fn field_value(&self, index: usize) -> &str { match index { 0 => &self.logic_name_input, 1 => &self.target_column_input, 2 => &self.description_input, _ => "", } } fn set_field_value(&mut self, index: usize, value: String) { match index { 0 => self.logic_name_input = value, 1 => self.target_column_input = value, 2 => self.description_input = value, _ => {} } self.has_unsaved_changes = true; } fn supports_suggestions(&self, field_index: usize) -> bool { // Only Target Column supports suggestions field_index == 1 } } // Wrapper that owns both the raw state and its FormEditor (like LoginFormState) pub struct AddLogicFormState { pub state: AddLogicState, pub editor: FormEditor, pub focus_outside_canvas: bool, pub focused_button_index: usize, } // manual Debug because FormEditor may not implement Debug impl std::fmt::Debug for AddLogicFormState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AddLogicFormState") .field("state", &self.state) .field("focus_outside_canvas", &self.focus_outside_canvas) .field("focused_button_index", &self.focused_button_index) .finish() } } impl AddLogicFormState { pub fn new(editor_config: &EditorConfig) -> Self { let state = AddLogicState::new(editor_config); let editor = FormEditor::new(state.clone()); Self { state, editor, focus_outside_canvas: false, focused_button_index: 0, } } pub fn new_with_table( editor_config: &EditorConfig, profile_name: String, table_id: Option, table_name: String, ) -> Self { let mut state = AddLogicState::new(editor_config); state.profile_name = profile_name; state.selected_table_id = table_id; state.selected_table_name = Some(table_name); let editor = FormEditor::new(state.clone()); Self { state, editor, focus_outside_canvas: false, focused_button_index: 0, } } pub fn from_state(state: AddLogicState) -> Self { let editor = FormEditor::new(state.clone()); Self { state, editor, focus_outside_canvas: false, focused_button_index: 0, } } /// Sync state from editor's data provider snapshot pub fn sync_from_editor(&mut self) { self.state = self.editor.data_provider().clone(); } // === Delegates to AddLogicState fields === pub fn current_focus(&self) -> AddLogicFocus { self.state.current_focus } pub fn set_current_focus(&mut self, focus: AddLogicFocus) { self.state.current_focus = focus; } pub fn has_unsaved_changes(&self) -> bool { self.state.has_unsaved_changes } pub fn set_has_unsaved_changes(&mut self, changed: bool) { self.state.has_unsaved_changes = changed; } pub fn profile_name(&self) -> &str { &self.state.profile_name } pub fn selected_table_name(&self) -> Option<&String> { self.state.selected_table_name.as_ref() } pub fn selected_table_id(&self) -> Option { self.state.selected_table_id } pub fn script_content_editor(&self) -> &Rc>> { &self.state.script_content_editor } pub fn script_content_editor_mut(&mut self) -> &mut Rc>> { &mut self.state.script_content_editor } pub fn vim_state(&self) -> &VimState { &self.state.vim_state } pub fn vim_state_mut(&mut self) -> &mut VimState { &mut self.state.vim_state } pub fn editor_keybinding_mode(&self) -> &EditorKeybindingMode { &self.state.editor_keybinding_mode } pub fn script_editor_autocomplete_active(&self) -> bool { self.state.script_editor_autocomplete_active } pub fn script_editor_suggestions(&self) -> &Vec { &self.state.script_editor_suggestions } pub fn script_editor_selected_suggestion_index(&self) -> Option { self.state.script_editor_selected_suggestion_index } pub fn target_column_suggestions(&self) -> &Vec { &self.state.target_column_suggestions } pub fn selected_target_column_suggestion_index(&self) -> Option { self.state.selected_target_column_suggestion_index } pub fn in_target_column_suggestion_mode(&self) -> bool { self.state.in_target_column_suggestion_mode } pub fn show_target_column_suggestions(&self) -> bool { self.state.show_target_column_suggestions } // === Delegates to FormEditor === pub fn mode(&self) -> AppMode { self.editor.mode() } pub fn cursor_position(&self) -> usize { self.editor.cursor_position() } pub fn handle_key_event( &mut self, key_event: crossterm::event::KeyEvent, ) -> canvas::keymap::KeyEventOutcome { // Customize behavior for Target Column (field index 1) in Edit mode, // mirroring how Register page does suggestions for Role. let in_target_col_field = self.editor.current_field() == 1; let in_edit_mode = self.editor.mode() == canvas::AppMode::Edit; if in_target_col_field && in_edit_mode { match key_event.code { // Tab: open suggestions if inactive; otherwise cycle next KeyCode::Tab => { if !self.editor.is_suggestions_active() { if let Some(query) = self.editor.start_suggestions(1) { let items = self.state.column_suggestions_sync(&query); let applied = self.editor.apply_suggestions_result(1, &query, items); if applied { self.editor.update_inline_completion(); } } } else { self.editor.suggestions_next(); } return canvas::keymap::KeyEventOutcome::Consumed(None); } // Shift+Tab: cycle suggestions too (fallback to next) KeyCode::BackTab => { if self.editor.is_suggestions_active() { self.editor.suggestions_next(); return canvas::keymap::KeyEventOutcome::Consumed(None); } } // Enter: apply selected suggestion (if active) KeyCode::Enter => { if self.editor.is_suggestions_active() { let _ = self.editor.apply_suggestion(); return canvas::keymap::KeyEventOutcome::Consumed(None); } } // Esc: close suggestions if active KeyCode::Esc => { if self.editor.is_suggestions_active() { self.editor.close_suggestions(); return canvas::keymap::KeyEventOutcome::Consumed(None); } } // Character input: mutate then refresh suggestions if active KeyCode::Char(_) => { let outcome = self.editor.handle_key_event(key_event); if self.editor.is_suggestions_active() { if let Some(query) = self.editor.start_suggestions(1) { let items = self.state.column_suggestions_sync(&query); let applied = self.editor.apply_suggestions_result(1, &query, items); if applied { self.editor.update_inline_completion(); } } } return outcome; } // Backspace/Delete: mutate then refresh suggestions if active KeyCode::Backspace | KeyCode::Delete => { let outcome = self.editor.handle_key_event(key_event); if self.editor.is_suggestions_active() { if let Some(query) = self.editor.start_suggestions(1) { let items = self.state.column_suggestions_sync(&query); let applied = self.editor.apply_suggestions_result(1, &query, items); if applied { self.editor.update_inline_completion(); } } } return outcome; } _ => { /* fall through */ } } } // Default: let canvas handle it self.editor.handle_key_event(key_event) } }