diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index f947785..fd077ea 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -321,6 +321,18 @@ impl EventHandler { if !outcome.get_message_if_ok().is_empty() { return Ok(outcome); } + } else if let Page::AddLogic(add_logic_page) = &mut router.current { + let outcome = add_logic::event::handle_add_logic_event( + event, + config, + app_state, + add_logic_page, + self.grpc_client.clone(), + self.save_logic_result_sender.clone(), + )?; + if !outcome.get_message_if_ok().is_empty() { + return Ok(outcome); + } } else if let Page::Form(path) = &router.current { let outcome = forms::event::handle_form_event( event, @@ -469,24 +481,6 @@ impl EventHandler { } } - // Optional page-specific handlers (non-movement or rich actions) - let client_clone = self.grpc_client.clone(); - let sender_clone = self.save_logic_result_sender.clone(); - if add_logic::nav::handle_add_logic_navigation( - key_event, - config, - app_state, - buffer_state, - client_clone, - sender_clone, - &mut self.command_message, - router, - ) { - return Ok(EventOutcome::Ok( - self.command_message.clone(), - )); - } - if let Page::AddTable(add_table_state) = &mut router.current { let client_clone = self.grpc_client.clone(); let sender_clone = self.save_table_result_sender.clone(); diff --git a/client/src/pages/admin_panel/add_logic/event.rs b/client/src/pages/admin_panel/add_logic/event.rs new file mode 100644 index 0000000..f102dea --- /dev/null +++ b/client/src/pages/admin_panel/add_logic/event.rs @@ -0,0 +1,199 @@ +// src/pages/admin_panel/add_logic/event.rs +use anyhow::Result; +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use crate::config::binds::config::Config; +use crate::modes::handlers::event::EventOutcome; +use crate::pages::admin_panel::add_logic::state::{AddLogicFormState, AddLogicFocus}; +use crate::components::common::text_editor::TextEditor; +use crate::services::grpc_client::GrpcClient; +use crate::pages::admin_panel::add_logic::nav::SaveLogicResultSender; // keep type alias +use crate::state::app::state::AppState; +use canvas::DataProvider; + +pub fn handle_add_logic_event( + event: Event, + config: &Config, + app_state: &mut AppState, + add_logic_page: &mut AddLogicFormState, + grpc_client: GrpcClient, + save_logic_sender: SaveLogicResultSender, +) -> Result { + if let Event::Key(key_event) = event { + let st = &mut add_logic_page.state; + let key_code = key_event.code; + let modifiers = key_event.modifiers; + + // 1) Fullscreen Script Editor mode + if st.current_focus == AddLogicFocus::InsideScriptContent { + match key_code { + KeyCode::Esc if modifiers == KeyModifiers::NONE => { + st.current_focus = AddLogicFocus::ScriptContentPreview; + app_state.ui.focus_outside_canvas = true; + return Ok(EventOutcome::Ok("Exited script editing.".into())); + } + _ => { + let changed = { + let mut editor_borrow = st.script_content_editor.borrow_mut(); + TextEditor::handle_input( + &mut editor_borrow, + key_event, + &st.editor_keybinding_mode, + &mut st.vim_state, + ) + }; + if changed { + st.has_unsaved_changes = true; + return Ok(EventOutcome::Ok("Script updated".into())); + } else { + return Ok(EventOutcome::Ok(String::new())); + } + } + } + } + + // 2) Canvas inputs (three fields) – forward to FormEditor + let inside_canvas_inputs = matches!( + st.current_focus, + AddLogicFocus::InputLogicName + | AddLogicFocus::InputTargetColumn + | AddLogicFocus::InputDescription + ); + + if inside_canvas_inputs { + // Handoff from last field to Script Preview on "down"/"next" + let last_idx = add_logic_page + .editor + .data_provider() + .field_count() + .saturating_sub(1); + let at_last = add_logic_page.editor.current_field() >= last_idx; + + // Map to generic "down/next" via config or raw keys + let action_opt = config.get_general_action(key_code, modifiers); + let is_down_or_next = matches!( + action_opt, + Some("down") | Some("next") | Some("move_down") | Some("next_field") + ) + || matches!(key_code, KeyCode::Down) + || matches!(key_code, KeyCode::Char('j') if modifiers.is_empty()); + + if at_last && is_down_or_next { + st.last_canvas_field = last_idx; + st.current_focus = AddLogicFocus::ScriptContentPreview; + add_logic_page.focus_outside_canvas = true; + app_state.ui.focus_outside_canvas = true; + return Ok(EventOutcome::Ok( + "Moved to Script Preview".to_string(), + )); + } + + // Normal canvas input + match add_logic_page.editor.handle_key_event(key_event) { + canvas::keymap::KeyEventOutcome::Consumed(Some(msg)) => { + add_logic_page.sync_from_editor(); + return Ok(EventOutcome::Ok(msg)); + } + canvas::keymap::KeyEventOutcome::Consumed(None) => { + add_logic_page.sync_from_editor(); + return Ok(EventOutcome::Ok("Input updated".into())); + } + canvas::keymap::KeyEventOutcome::Pending => { + return Ok(EventOutcome::Ok(String::new())); + } + canvas::keymap::KeyEventOutcome::NotMatched => { + // fall through to outside handling + } + } + } + + // 3) Outside-canvas focus (Script Preview, Save, Cancel) + let action_opt = config.get_general_action(key_code, modifiers); + + match action_opt { + Some("up") | Some("move_up") | Some("previous") | Some("previous_option") | Some("prev_field") => { + match st.current_focus { + AddLogicFocus::ScriptContentPreview => { + st.current_focus = AddLogicFocus::InputDescription; + add_logic_page.focus_outside_canvas = false; + app_state.ui.focus_outside_canvas = false; + return Ok(EventOutcome::Ok( + "Back to Description".to_string(), + )); + } + AddLogicFocus::SaveButton => { + st.current_focus = AddLogicFocus::ScriptContentPreview; + return Ok(EventOutcome::Ok( + "Back to Script Preview".to_string(), + )); + } + AddLogicFocus::CancelButton => { + st.current_focus = AddLogicFocus::SaveButton; + return Ok(EventOutcome::Ok("Back to Save".to_string())); + } + _ => {} + } + } + Some("down") | Some("move_down") | Some("next") | Some("next_option") | Some("next_field") => { + match st.current_focus { + AddLogicFocus::ScriptContentPreview => { + st.current_focus = AddLogicFocus::SaveButton; + return Ok(EventOutcome::Ok("Focus: Save".to_string())); + } + AddLogicFocus::SaveButton => { + st.current_focus = AddLogicFocus::CancelButton; + return Ok(EventOutcome::Ok("Focus: Cancel".to_string())); + } + _ => {} + } + } + Some("select") => { + match st.current_focus { + AddLogicFocus::ScriptContentPreview => { + st.current_focus = AddLogicFocus::InsideScriptContent; + add_logic_page.focus_outside_canvas = false; + app_state.ui.focus_outside_canvas = false; + return Ok(EventOutcome::Ok( + "Fullscreen script editing. Esc to exit.".into(), + )); + } + AddLogicFocus::SaveButton => { + if let Some(msg) = st.save_logic() { + return Ok(EventOutcome::Ok(msg)); + } + return Ok(EventOutcome::Ok( + "Saved (no changes)".to_string(), + )); + } + AddLogicFocus::CancelButton => { + // Keep this simple: you can wire buffer/view navigation where needed + return Ok(EventOutcome::Ok( + "Cancelled Add Logic".to_string(), + )); + } + AddLogicFocus::InputLogicName + | AddLogicFocus::InputTargetColumn + | AddLogicFocus::InputDescription => { + add_logic_page.focus_outside_canvas = false; + app_state.ui.focus_outside_canvas = false; + return Ok(EventOutcome::Ok(String::new())); + } + _ => {} + } + } + Some("esc") => { + if st.current_focus == AddLogicFocus::ScriptContentPreview { + // Go back to Description (last canvas field) + st.current_focus = AddLogicFocus::InputDescription; + add_logic_page.focus_outside_canvas = false; + app_state.ui.focus_outside_canvas = false; + return Ok(EventOutcome::Ok( + "Back to Description".to_string(), + )); + } + } + _ => {} + } + } + + Ok(EventOutcome::Ok(String::new())) +} diff --git a/client/src/pages/admin_panel/add_logic/loader.rs b/client/src/pages/admin_panel/add_logic/loader.rs index c3b48bc..142f424 100644 --- a/client/src/pages/admin_panel/add_logic/loader.rs +++ b/client/src/pages/admin_panel/add_logic/loader.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use tracing::{error, info, warn}; -use crate::pages::admin_panel::add_logic::state::AddLogicState; +use crate::pages::admin_panel::add_logic::state::AddLogicFormState; use crate::pages::routing::{Page, Router}; use crate::services::grpc_client::GrpcClient; use crate::services::ui_service::UiService; @@ -19,9 +19,9 @@ pub async fn process_pending_table_structure_fetch( let mut needs_redraw = false; if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() { - if let Page::AddLogic(state) = &mut router.current { - if state.profile_name == profile_name - && state.selected_table_name.as_deref() == Some(table_name.as_str()) + if let Page::AddLogic(page) = &mut router.current { + if page.profile_name() == profile_name + && page.selected_table_name().map(|s| s.as_str()) == Some(table_name.as_str()) { info!( "Fetching table structure for {}.{}", @@ -30,7 +30,7 @@ pub async fn process_pending_table_structure_fetch( let fetch_message = UiService::initialize_add_logic_table_data( grpc_client, - state, + &mut page.state, // keep state here, UiService expects AddLogicState &app_state.profile_tree, ) .await @@ -53,7 +53,10 @@ pub async fn process_pending_table_structure_fetch( error!( "Mismatch in pending_table_structure_fetch: app_state wants {}.{}, \ but AddLogic state is for {}.{:?}", - profile_name, table_name, state.profile_name, state.selected_table_name + profile_name, + table_name, + page.profile_name(), + page.selected_table_name() ); } } else { @@ -71,14 +74,15 @@ pub async fn process_pending_table_structure_fetch( /// fetch them and update the state. Returns true if UI needs a redraw. pub async fn maybe_fetch_columns_for_awaiting_table( grpc_client: &mut GrpcClient, - state: &mut AddLogicState, + page: &mut AddLogicFormState, command_message: &mut String, ) -> Result { - if let Some(table_name) = state + if let Some(table_name) = page + .state .script_editor_awaiting_column_autocomplete .clone() { - let profile_name = state.profile_name.clone(); + let profile_name = page.state.profile_name.clone(); info!( "Fetching columns for table selection: {}.{}", @@ -86,7 +90,7 @@ pub async fn maybe_fetch_columns_for_awaiting_table( ); match UiService::fetch_columns_for_table(grpc_client, &profile_name, &table_name).await { Ok(columns) => { - state.set_columns_for_table_autocomplete(columns.clone()); + page.state.set_columns_for_table_autocomplete(columns.clone()); info!("Loaded {} columns for table '{}'", columns.len(), table_name); *command_message = format!("Columns for '{}' loaded. Select a column.", table_name); @@ -96,8 +100,8 @@ pub async fn maybe_fetch_columns_for_awaiting_table( "Failed to fetch columns for {}.{}: {}", profile_name, table_name, e ); - state.script_editor_awaiting_column_autocomplete = None; - state.deactivate_script_editor_autocomplete(); + page.state.script_editor_awaiting_column_autocomplete = None; + page.state.deactivate_script_editor_autocomplete(); *command_message = format!("Error loading columns for '{}': {}", table_name, e); } } diff --git a/client/src/pages/admin_panel/add_logic/mod.rs b/client/src/pages/admin_panel/add_logic/mod.rs index 64fadcc..26660b4 100644 --- a/client/src/pages/admin_panel/add_logic/mod.rs +++ b/client/src/pages/admin_panel/add_logic/mod.rs @@ -4,3 +4,4 @@ pub mod ui; pub mod nav; pub mod state; pub mod loader; +pub mod event; diff --git a/client/src/pages/admin_panel/add_logic/nav.rs b/client/src/pages/admin_panel/add_logic/nav.rs index 2470902..c5c805e 100644 --- a/client/src/pages/admin_panel/add_logic/nav.rs +++ b/client/src/pages/admin_panel/add_logic/nav.rs @@ -1,531 +1,6 @@ // src/pages/admin_panel/add_logic/nav.rs -use crate::config::binds::config::{Config, EditorKeybindingMode}; -use crate::state::app::state::AppState; -use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState}; -use crate::buffer::{AppView, BufferState}; -use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; -use crate::services::GrpcClient; -use tokio::sync::mpsc; use anyhow::Result; -use crate::components::common::text_editor::TextEditor; -use crate::services::ui_service::UiService; -use tui_textarea::CursorMove; -use crate::pages::admin::AdminState; -use crate::pages::routing::{Router, Page}; +use tokio::sync::mpsc; pub type SaveLogicResultSender = mpsc::Sender>; - -pub fn handle_add_logic_navigation( - key_event: KeyEvent, - config: &Config, - app_state: &mut AppState, - buffer_state: &mut BufferState, - grpc_client: GrpcClient, - save_logic_sender: SaveLogicResultSender, - command_message: &mut String, - router: &mut Router, -) -> bool { - if let Page::AddLogic(add_logic_state) = &mut router.current { - // === FULLSCREEN SCRIPT EDITING === - if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent { - // === AUTOCOMPLETE HANDLING === - if add_logic_state.script_editor_autocomplete_active { - match key_event.code { - KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => { - add_logic_state.script_editor_filter_text.push(c); - add_logic_state.update_script_editor_suggestions(); - { - let mut editor_borrow = - add_logic_state.script_content_editor.borrow_mut(); - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); - } - *command_message = - format!("Filtering: @{}", add_logic_state.script_editor_filter_text); - return true; - } - KeyCode::Backspace => { - if !add_logic_state.script_editor_filter_text.is_empty() { - add_logic_state.script_editor_filter_text.pop(); - add_logic_state.update_script_editor_suggestions(); - { - let mut editor_borrow = - add_logic_state.script_content_editor.borrow_mut(); - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); - } - *command_message = - if add_logic_state.script_editor_filter_text.is_empty() { - "Autocomplete: @".to_string() - } else { - format!( - "Filtering: @{}", - add_logic_state.script_editor_filter_text - ) - }; - } else { - let should_deactivate = - if let Some((trigger_line, trigger_col)) = - add_logic_state.script_editor_trigger_position - { - let current_cursor = { - let editor_borrow = - add_logic_state.script_content_editor.borrow(); - editor_borrow.cursor() - }; - current_cursor.0 == trigger_line - && current_cursor.1 == trigger_col + 1 - } else { - false - }; - if should_deactivate { - add_logic_state.deactivate_script_editor_autocomplete(); - *command_message = "Autocomplete cancelled".to_string(); - } - { - let mut editor_borrow = - add_logic_state.script_content_editor.borrow_mut(); - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); - } - } - return true; - } - KeyCode::Tab | KeyCode::Down => { - if !add_logic_state.script_editor_suggestions.is_empty() { - let current = add_logic_state - .script_editor_selected_suggestion_index - .unwrap_or(0); - let next = - (current + 1) % add_logic_state.script_editor_suggestions.len(); - add_logic_state.script_editor_selected_suggestion_index = Some(next); - *command_message = format!( - "Selected: {}", - add_logic_state.script_editor_suggestions[next] - ); - } - return true; - } - KeyCode::Up => { - if !add_logic_state.script_editor_suggestions.is_empty() { - let current = add_logic_state - .script_editor_selected_suggestion_index - .unwrap_or(0); - let prev = if current == 0 { - add_logic_state.script_editor_suggestions.len() - 1 - } else { - current - 1 - }; - add_logic_state.script_editor_selected_suggestion_index = Some(prev); - *command_message = format!( - "Selected: {}", - add_logic_state.script_editor_suggestions[prev] - ); - } - return true; - } - KeyCode::Enter => { - if let Some(selected_idx) = - add_logic_state.script_editor_selected_suggestion_index - { - if let Some(suggestion) = add_logic_state - .script_editor_suggestions - .get(selected_idx) - .cloned() - { - let trigger_pos = - add_logic_state.script_editor_trigger_position; - let filter_len = - add_logic_state.script_editor_filter_text.len(); - - add_logic_state.deactivate_script_editor_autocomplete(); - add_logic_state.has_unsaved_changes = true; - - if let Some(pos) = trigger_pos { - let mut editor_borrow = - add_logic_state.script_content_editor.borrow_mut(); - - if suggestion == "sql" { - replace_autocomplete_text( - &mut editor_borrow, - pos, - filter_len, - "sql", - ); - editor_borrow.insert_str("('')"); - editor_borrow.move_cursor(CursorMove::Back); - editor_borrow.move_cursor(CursorMove::Back); - *command_message = "Inserted: @sql('')".to_string(); - } else { - let is_table_selection = - add_logic_state.is_table_name_suggestion(&suggestion); - replace_autocomplete_text( - &mut editor_borrow, - pos, - filter_len, - &suggestion, - ); - - if is_table_selection { - editor_borrow.insert_str("."); - let new_cursor = editor_borrow.cursor(); - drop(editor_borrow); - - add_logic_state.script_editor_trigger_position = - Some(new_cursor); - add_logic_state.script_editor_autocomplete_active = true; - add_logic_state.script_editor_filter_text.clear(); - add_logic_state - .trigger_column_autocomplete_for_table( - suggestion.clone(), - ); - - let profile_name = - add_logic_state.profile_name.clone(); - let table_name_for_fetch = suggestion.clone(); - let mut client_clone = grpc_client.clone(); - tokio::spawn(async move { - if let Err(e) = UiService::fetch_columns_for_table( - &mut client_clone, - &profile_name, - &table_name_for_fetch, - ) - .await - { - tracing::error!( - "Failed to fetch columns for {}.{}: {}", - profile_name, - table_name_for_fetch, - e - ); - } - }); - *command_message = format!( - "Selected table '{}', fetching columns...", - suggestion - ); - } else { - *command_message = - format!("Inserted: {}", suggestion); - } - } - } - return true; - } - } - add_logic_state.deactivate_script_editor_autocomplete(); - { - let mut editor_borrow = - add_logic_state.script_content_editor.borrow_mut(); - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); - } - return true; - } - KeyCode::Esc => { - add_logic_state.deactivate_script_editor_autocomplete(); - *command_message = "Autocomplete cancelled".to_string(); - } - _ => { - add_logic_state.deactivate_script_editor_autocomplete(); - *command_message = "Autocomplete cancelled".to_string(); - { - let mut editor_borrow = - add_logic_state.script_content_editor.borrow_mut(); - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); - } - return true; - } - } - } - - // Trigger autocomplete with '@' - if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE { - let should_trigger = match add_logic_state.editor_keybinding_mode { - EditorKeybindingMode::Vim => { - TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) - } - _ => true, - }; - if should_trigger { - let cursor_before = { - let editor_borrow = add_logic_state.script_content_editor.borrow(); - editor_borrow.cursor() - }; - { - let mut editor_borrow = - add_logic_state.script_content_editor.borrow_mut(); - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); - } - add_logic_state.script_editor_trigger_position = Some(cursor_before); - add_logic_state.script_editor_autocomplete_active = true; - add_logic_state.script_editor_filter_text.clear(); - add_logic_state.update_script_editor_suggestions(); - add_logic_state.has_unsaved_changes = true; - *command_message = "Autocomplete: @ (Tab/↑↓ to navigate, Enter to select, Esc to cancel)".to_string(); - return true; - } - } - - // Esc handling - if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE { - match add_logic_state.editor_keybinding_mode { - EditorKeybindingMode::Vim => { - let was_insert = - TextEditor::is_vim_insert_mode(&add_logic_state.vim_state); - { - let mut editor_borrow = - add_logic_state.script_content_editor.borrow_mut(); - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); - } - if was_insert { - *command_message = - "VIM: Normal Mode. Esc again to exit script.".to_string(); - } else { - add_logic_state.current_focus = - AddLogicFocus::ScriptContentPreview; - app_state.ui.focus_outside_canvas = true; - *command_message = "Exited script editing.".to_string(); - } - } - _ => { - add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview; - app_state.ui.focus_outside_canvas = true; - *command_message = "Exited script editing.".to_string(); - } - } - return true; - } - - // Normal text input - let changed = { - let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut(); - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ) - }; - if changed { - add_logic_state.has_unsaved_changes = true; - } - return true; - } - - // === NON-FULLSCREEN NAVIGATION === - let action = config.get_general_action(key_event.code, key_event.modifiers); - let current_focus = add_logic_state.current_focus; - let mut handled = true; - let mut new_focus = current_focus; - - match action.as_deref() { - Some("exit_table_scroll") => { - handled = false; - } - Some("move_up") => { - match current_focus { - AddLogicFocus::InputLogicName => {} - AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName, - AddLogicFocus::InputDescription => { - new_focus = AddLogicFocus::InputTargetColumn - } - AddLogicFocus::ScriptContentPreview => { - new_focus = AddLogicFocus::InputDescription - } - AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview, - AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton, - _ => handled = false, - } - } - Some("move_down") => { - match current_focus { - AddLogicFocus::InputLogicName => { - new_focus = AddLogicFocus::InputTargetColumn - } - AddLogicFocus::InputTargetColumn => { - new_focus = AddLogicFocus::InputDescription - } - AddLogicFocus::InputDescription => { - add_logic_state.last_canvas_field = 2; - new_focus = AddLogicFocus::ScriptContentPreview; - } - AddLogicFocus::ScriptContentPreview => { - new_focus = AddLogicFocus::SaveButton - } - AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton, - AddLogicFocus::CancelButton => {} - _ => handled = false, - } - } - Some("next_option") => { - match current_focus { - AddLogicFocus::InputLogicName - | AddLogicFocus::InputTargetColumn - | AddLogicFocus::InputDescription => { - new_focus = AddLogicFocus::ScriptContentPreview - } - AddLogicFocus::ScriptContentPreview => { - new_focus = AddLogicFocus::SaveButton - } - AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton, - AddLogicFocus::CancelButton => {} - _ => handled = false, - } - } - Some("previous_option") => { - match current_focus { - AddLogicFocus::InputLogicName - | AddLogicFocus::InputTargetColumn - | AddLogicFocus::InputDescription => {} - AddLogicFocus::ScriptContentPreview => { - new_focus = AddLogicFocus::InputDescription - } - AddLogicFocus::SaveButton => { - new_focus = AddLogicFocus::ScriptContentPreview - } - AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton, - _ => handled = false, - } - } - Some("next_field") => { - new_focus = match current_focus { - AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn, - AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription, - AddLogicFocus::InputDescription => AddLogicFocus::ScriptContentPreview, - AddLogicFocus::ScriptContentPreview => AddLogicFocus::SaveButton, - AddLogicFocus::SaveButton => AddLogicFocus::CancelButton, - AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName, - _ => current_focus, - }; - } - Some("prev_field") => { - new_focus = match current_focus { - AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton, - AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName, - AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn, - AddLogicFocus::ScriptContentPreview => AddLogicFocus::InputDescription, - AddLogicFocus::SaveButton => AddLogicFocus::ScriptContentPreview, - AddLogicFocus::CancelButton => AddLogicFocus::SaveButton, - _ => current_focus, - }; - } - Some("select") => { - match current_focus { - AddLogicFocus::ScriptContentPreview => { - new_focus = AddLogicFocus::InsideScriptContent; - app_state.ui.focus_outside_canvas = false; - let mode_hint = match add_logic_state.editor_keybinding_mode { - EditorKeybindingMode::Vim => { - "VIM mode - 'i'/'a'/'o' to edit" - } - _ => "Enter/Ctrl+E to edit", - }; - *command_message = format!( - "Fullscreen script editing. {} or Esc to exit.", - mode_hint - ); - handled = true; - } - AddLogicFocus::SaveButton => { - *command_message = "Save logic action".to_string(); - handled = true; - } - AddLogicFocus::CancelButton => { - buffer_state.update_history(AppView::Admin); - *command_message = "Cancelled Add Logic".to_string(); - handled = true; - } - AddLogicFocus::InputLogicName - | AddLogicFocus::InputTargetColumn - | AddLogicFocus::InputDescription => { - // Focus canvas inputs; let canvas keymap handle editing - app_state.ui.focus_outside_canvas = false; - handled = false; // forward to canvas - } - _ => handled = false, - } - } - Some("toggle_edit_mode") => { - match current_focus { - AddLogicFocus::InputLogicName - | AddLogicFocus::InputTargetColumn - | AddLogicFocus::InputDescription => { - app_state.ui.focus_outside_canvas = false; - *command_message = - "Focus moved to input. Use i/a (Vim) or type to edit.".to_string(); - handled = true; - } - _ => { - *command_message = "Cannot toggle edit mode here.".to_string(); - } - } - } - _ => handled = false, - } - - if handled && current_focus != new_focus { - add_logic_state.current_focus = new_focus; - let new_is_canvas_input_focus = matches!( - new_focus, - AddLogicFocus::InputLogicName - | AddLogicFocus::InputTargetColumn - | AddLogicFocus::InputDescription - ); - if new_is_canvas_input_focus { - app_state.ui.focus_outside_canvas = false; - } else { - app_state.ui.focus_outside_canvas = true; - } - } - handled - } else { - false - } -} - -fn replace_autocomplete_text( - editor: &mut tui_textarea::TextArea, - trigger_pos: (usize, usize), - filter_len: usize, - replacement: &str, -) { - let filter_start_pos = (trigger_pos.0, trigger_pos.1 + 1); - editor.move_cursor(CursorMove::Jump(filter_start_pos.0 as u16, filter_start_pos.1 as u16)); - for _ in 0..filter_len { - editor.delete_next_char(); - } - editor.insert_str(replacement); -} diff --git a/client/src/pages/admin_panel/add_logic/state.rs b/client/src/pages/admin_panel/add_logic/state.rs index fdf8332..70da1b3 100644 --- a/client/src/pages/admin_panel/add_logic/state.rs +++ b/client/src/pages/admin_panel/add_logic/state.rs @@ -1,7 +1,7 @@ // 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}; +use canvas::{DataProvider, AppMode, FormEditor}; use std::cell::RefCell; use std::rc::Rc; use tui_textarea::TextArea; @@ -315,3 +315,141 @@ impl DataProvider for AddLogicState { 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, +} + +// 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) + .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, + } + } + + pub fn from_state(state: AddLogicState) -> Self { + let editor = FormEditor::new(state.clone()); + Self { + state, + editor, + focus_outside_canvas: false, + } + } + + /// 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 { + self.editor.handle_key_event(key_event) + } +} diff --git a/client/src/pages/admin_panel/add_logic/ui.rs b/client/src/pages/admin_panel/add_logic/ui.rs index 5e3824c..640fb7c 100644 --- a/client/src/pages/admin_panel/add_logic/ui.rs +++ b/client/src/pages/admin_panel/add_logic/ui.rs @@ -1,7 +1,7 @@ // src/pages/admin_panel/add_logic/ui.rs use crate::config::colors::themes::Theme; use crate::state::app::state::AppState; -use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState}; +use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState, AddLogicFormState}; use canvas::{render_canvas, FormEditor}; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -19,7 +19,7 @@ pub fn render_add_logic( area: Rect, theme: &Theme, app_state: &AppState, - add_logic_state: &mut AddLogicState, + add_logic_state: &mut AddLogicFormState, ) { let main_block = Block::default() .title(" Add New Logic Script ") @@ -32,9 +32,13 @@ pub fn render_add_logic( f.render_widget(main_block, area); // Handle full-screen script editing - if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent { - let mut editor_ref = add_logic_state.script_content_editor.borrow_mut(); - let border_style_color = if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) { + if add_logic_state.current_focus() == AddLogicFocus::InsideScriptContent { + let mut editor_ref = add_logic_state + .state + .script_content_editor + .borrow_mut(); + + let border_style_color = if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(add_logic_state.vim_state()) { theme.highlight } else { theme.secondary @@ -44,13 +48,13 @@ pub fn render_add_logic( editor_ref.set_cursor_line_style(Style::default()); editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); - let script_title_hint = match add_logic_state.editor_keybinding_mode { + let script_title_hint = match add_logic_state.editor_keybinding_mode() { EditorKeybindingMode::Vim => { - let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(&add_logic_state.vim_state); + let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(add_logic_state.vim_state()); format!("Script {}", vim_mode_status) } EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => { - if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) { + if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(add_logic_state.vim_state()) { "Script (Editing)".to_string() } else { "Script".to_string() @@ -72,10 +76,10 @@ pub fn render_add_logic( drop(editor_ref); // === SCRIPT EDITOR AUTOCOMPLETE RENDERING === - if add_logic_state.script_editor_autocomplete_active && !add_logic_state.script_editor_suggestions.is_empty() { + if add_logic_state.script_editor_autocomplete_active() && !add_logic_state.script_editor_suggestions().is_empty() { // Get the current cursor position from textarea let current_cursor = { - let editor_borrow = add_logic_state.script_content_editor.borrow(); + let editor_borrow = add_logic_state.script_content_editor().borrow(); editor_borrow.cursor() // Returns (row, col) as (usize, usize) }; @@ -103,8 +107,8 @@ pub fn render_add_logic( input_rect, f.area(), // Full frame area for clamping theme, - &add_logic_state.script_editor_suggestions, - add_logic_state.script_editor_selected_suggestion_index, + add_logic_state.script_editor_suggestions(), + add_logic_state.script_editor_selected_suggestion_index(), ); } @@ -128,54 +132,54 @@ pub fn render_add_logic( let buttons_area = main_chunks[3]; // Top info + let table_label = if let Some(name) = add_logic_state.selected_table_name() { + name.clone() + } else if let Some(id) = add_logic_state.selected_table_id() { + format!("ID {}", id) + } else { + "Global (Not Selected)".to_string() + }; + let profile_text = Paragraph::new(vec![ Line::from(Span::styled( - format!("Profile: {}", add_logic_state.profile_name), - Style::default().fg(theme.fg), + format!("Profile: {}", add_logic_state.profile_name()), + Style::default().fg(theme.fg), )), Line::from(Span::styled( - format!( - "Table: {}", - add_logic_state - .selected_table_name - .clone() - .unwrap_or_else(|| add_logic_state.selected_table_id - .map(|id| format!("ID {}", id)) - .unwrap_or_else(|| "Global (Not Selected)".to_string())) - ), - Style::default().fg(theme.fg), + format!("Table: {}", table_label), + Style::default().fg(theme.fg), )), ]) - .block( - Block::default() + .block( + Block::default() .borders(Borders::BOTTOM) .border_style(Style::default().fg(theme.secondary)), - ); + ); f.render_widget(profile_text, top_info_area); // Canvas - USING CANVAS LIBRARY let focus_on_canvas_inputs = matches!( - add_logic_state.current_focus, + add_logic_state.current_focus(), AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription ); - let editor = FormEditor::new(add_logic_state.clone()); - let active_field_rect = render_canvas(f, canvas_area, &editor, theme); + let editor = &add_logic_state.editor; + let active_field_rect = render_canvas(f, canvas_area, editor, theme); // --- Render Autocomplete for Target Column --- if editor.mode() == canvas::AppMode::Edit && editor.current_field() == 1 { // Target Column field - if add_logic_state.in_target_column_suggestion_mode && add_logic_state.show_target_column_suggestions { - if !add_logic_state.target_column_suggestions.is_empty() { + if add_logic_state.in_target_column_suggestion_mode() && add_logic_state.show_target_column_suggestions() { + if !add_logic_state.target_column_suggestions().is_empty() { if let Some(input_rect) = active_field_rect { autocomplete::render_autocomplete_dropdown( f, input_rect, f.area(), // Full frame area for clamping theme, - &add_logic_state.target_column_suggestions, - add_logic_state.selected_target_column_suggestion_index, + add_logic_state.target_column_suggestions(), + add_logic_state.selected_target_column_suggestion_index(), ); } } @@ -184,10 +188,10 @@ pub fn render_add_logic( // Script content preview { - let mut editor_ref = add_logic_state.script_content_editor.borrow_mut(); + let mut editor_ref = add_logic_state.script_content_editor().borrow_mut(); editor_ref.set_cursor_line_style(Style::default()); - let is_script_preview_focused = add_logic_state.current_focus == AddLogicFocus::ScriptContentPreview; + let is_script_preview_focused = add_logic_state.current_focus() == AddLogicFocus::ScriptContentPreview; if is_script_preview_focused { editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); @@ -256,7 +260,7 @@ pub fn render_add_logic( let save_button = Paragraph::new(" Save Logic ") .style(get_button_style( AddLogicFocus::SaveButton, - add_logic_state.current_focus, + add_logic_state.current_focus(), )) .alignment(Alignment::Center) .block( @@ -264,7 +268,7 @@ pub fn render_add_logic( .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(get_button_border_style( - add_logic_state.current_focus == AddLogicFocus::SaveButton, + add_logic_state.current_focus() == AddLogicFocus::SaveButton, theme, )), ); @@ -273,7 +277,7 @@ pub fn render_add_logic( let cancel_button = Paragraph::new(" Cancel ") .style(get_button_style( AddLogicFocus::CancelButton, - add_logic_state.current_focus, + add_logic_state.current_focus(), )) .alignment(Alignment::Center) .block( @@ -281,7 +285,7 @@ pub fn render_add_logic( .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(get_button_border_style( - add_logic_state.current_focus == AddLogicFocus::CancelButton, + add_logic_state.current_focus() == AddLogicFocus::CancelButton, theme, )), ); diff --git a/client/src/pages/routing/router.rs b/client/src/pages/routing/router.rs index 70e12e5..3099554 100644 --- a/client/src/pages/routing/router.rs +++ b/client/src/pages/routing/router.rs @@ -1,6 +1,6 @@ // src/pages/routing/router.rs use crate::state::pages::auth::AuthState; -use crate::pages::admin_panel::add_logic::state::AddLogicState; +use crate::pages::admin_panel::add_logic::state::AddLogicFormState; use crate::pages::admin_panel::add_table::state::AddTableState; use crate::pages::admin::AdminState; use crate::pages::forms::FormState; @@ -14,7 +14,7 @@ pub enum Page { Login(LoginFormState), Register(RegisterFormState), Admin(AdminState), - AddLogic(AddLogicState), + AddLogic(AddLogicFormState), AddTable(AddTableState), Form(String), } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 2a33b5d..e37ec25 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -425,7 +425,18 @@ pub async fn run_ui() -> Result<()> { router.navigate(Page::Admin(admin_state.clone())); } AppView::AddTable => router.navigate(Page::AddTable(admin_state.add_table_state.clone())), - AppView::AddLogic => router.navigate(Page::AddLogic(admin_state.add_logic_state.clone())), + AppView::AddLogic => { + // Create once, like Login/Register + if let Page::AddLogic(_) = &router.current { + // already on page + } else { + let mut page = add_logic::state::AddLogicFormState::from_state( + admin_state.add_logic_state.clone(), + ); + page.editor.set_keymap(config.build_canvas_keymap()); + router.navigate(Page::AddLogic(page)); + } + } AppView::Form(path) => { // Keep current_view_* consistent with the active buffer path if let Some((profile, table)) = path.split_once('/') {