diff --git a/Cargo.lock b/Cargo.lock index c943c24..21cc28d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,7 +402,7 @@ dependencies = [ "anyhow", "async-trait", "common", - "crossterm 0.29.0", + "crossterm", "dirs 6.0.0", "dotenvy", "lazy_static", @@ -485,15 +485,6 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" -[[package]] -name = "convert_case" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -621,24 +612,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix 1.0.5", - "signal-hook", - "signal-hook-mio", - "winapi", -] - [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -727,27 +700,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "digest" version = "0.10.7" @@ -813,15 +765,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "document-features" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" -dependencies = [ - "litrs", -] - [[package]] name = "dotenvy" version = "0.15.7" @@ -1682,12 +1625,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" -[[package]] -name = "litrs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" - [[package]] name = "lock_api" version = "0.4.12" @@ -2355,7 +2292,7 @@ dependencies = [ "bitflags", "cassowary", "compact_str", - "crossterm 0.28.1", + "crossterm", "indoc", "instability", "itertools 0.13.0", @@ -3613,7 +3550,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" dependencies = [ - "crossterm 0.28.1", + "crossterm", "ratatui", "unicode-width 0.2.0", ] diff --git a/client/Cargo.toml b/client/Cargo.toml index e060a02..285ca7d 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,12 +9,12 @@ anyhow = "1.0.98" async-trait = "0.1.88" common = { path = "../common" } -crossterm = "0.29.0" +crossterm = "0.28.1" dirs = "6.0.0" dotenvy = "0.15.7" lazy_static = "1.5.0" prost = "0.13.5" -ratatui = "0.29.0" +ratatui = { version = "0.29.0", features = ["crossterm"] } serde = { version = "1.0.219", features = ["derive"] } time = "0.3.41" tokio = { version = "1.44.2", features = ["full", "macros"] } @@ -22,6 +22,6 @@ toml = "0.8.20" tonic = "0.13.0" tracing = "0.1.41" tracing-subscriber = "0.3.19" -tui-textarea = "0.7.0" +tui-textarea = { version = "0.7.0", features = ["crossterm"] } unicode-segmentation = "1.12.0" unicode-width = "0.2.0" diff --git a/client/src/components/admin/add_logic.rs b/client/src/components/admin/add_logic.rs index 410992e..e7d1230 100644 --- a/client/src/components/admin/add_logic.rs +++ b/client/src/components/admin/add_logic.rs @@ -6,9 +6,9 @@ use crate::state::pages::add_logic::{AddLogicFocus, AddLogicState}; use crate::state::pages::canvas_state::CanvasState; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, BorderType, Borders, Paragraph}, + style::{Modifier, Style, Stylize}, // Added Stylize for .dim() + text::{Line, Span}, + widgets::{Block, BorderType, Borders, Paragraph}, // Removed unused Widget Frame, }; use crate::components::handlers::canvas::render_canvas; @@ -33,39 +33,43 @@ pub fn render_add_logic( let inner_area = main_block.inner(area); f.render_widget(main_block, area); - // --- Fullscreen Script Content Check --- if add_logic_state.current_focus == AddLogicFocus::InputScriptContent { - let script_block_border_style = Style::default().fg(theme.highlight); // Always highlighted + let mut editor = add_logic_state.script_content_editor.borrow_mut(); + let border_style = if is_edit_mode { + Style::default().fg(theme.highlight) + } else { + Style::default().fg(theme.highlight) + }; - let script_block = Block::default() - .title(Span::styled( - " Steel Script Content (Fullscreen) ", - theme.fg, - )) // Indicate fullscreen - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(script_block_border_style); + editor.set_cursor_line_style( + Style::default().bg(theme.secondary), + ); + editor.set_line_number_style( + Style::default().fg(theme.secondary), + ); - let script_text = - Text::from(add_logic_state.script_content_input.as_str()); - let script_paragraph = Paragraph::new(script_text) - .block(script_block) - .scroll(add_logic_state.script_content_scroll) - .style(Style::default().fg(theme.fg)); - f.render_widget(script_paragraph, inner_area); // Use inner_area for fullscreen - return; // IMPORTANT: Stop rendering here for fullscreen mode + editor.set_block( + Block::default() + .title(Span::styled( + " Steel Script Content (Ctrl+E or Enter to edit, Esc to unfocus/exit edit) ", + Style::default().fg(theme.fg), + )) + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(border_style), + ); + f.render_widget(&*editor, inner_area); + return; } - // --- Normal Layout --- - // Calculate areas dynamically let main_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // Top Info (Profile/Table) - Constraint::Length(9), // Canvas Area (3 input fields × 3 lines each) - Constraint::Min(5), // Script Content Area - Constraint::Length(3), // Bottom Buttons + Constraint::Length(3), + Constraint::Length(9), + Constraint::Min(5), + Constraint::Length(3), ]) .split(inner_area); @@ -74,21 +78,22 @@ pub fn render_add_logic( let script_content_area = main_chunks[2]; let buttons_area = main_chunks[3]; - // Top Info Rendering let profile_text = Paragraph::new(vec![ Line::from(Span::styled( format!("Profile: {}", add_logic_state.profile_name), - theme.fg, + Style::default().fg(theme.fg), )), Line::from(Span::styled( format!( "Table: {}", add_logic_state - .selected_table_id - .map(|id| format!("ID {}", id)) - .unwrap_or_else(|| "Global".to_string()) + .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())) ), - theme.fg, + Style::default().fg(theme.fg), )), ]) .block( @@ -98,7 +103,6 @@ pub fn render_add_logic( ); f.render_widget(profile_text, top_info_area); - // Canvas rendering for input fields let focus_on_canvas_inputs = matches!( add_logic_state.current_focus, AddLogicFocus::InputLogicName @@ -118,29 +122,23 @@ pub fn render_add_logic( highlight_state, ); - // Script Content Area (Normal Mode) - let script_block_border_style = - if add_logic_state.current_focus == AddLogicFocus::InputScriptContent { - Style::default().fg(theme.highlight) - } else { - Style::default().fg(theme.secondary) - }; + { + let mut editor = add_logic_state.script_content_editor.borrow_mut(); + editor.set_cursor_line_style(Style::default()); + editor.set_line_number_style( + Style::default().fg(theme.secondary).dim(), // Fixed: apply .dim() to Style, not Color + ); - let script_block = Block::default() - .title(" Steel Script Content ") - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(script_block_border_style); + editor.set_block( + Block::default() + .title(" Steel Script Content ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(theme.secondary)), + ); + f.render_widget(&*editor, script_content_area); + } - let script_text = - Text::from(add_logic_state.script_content_input.as_str()); - let script_paragraph = Paragraph::new(script_text) - .block(script_block) - .scroll(add_logic_state.script_content_scroll) - .style(Style::default().fg(theme.fg)); - f.render_widget(script_paragraph, script_content_area); - - // Button Style Helpers let get_button_style = |button_focus: AddLogicFocus, current_focus| { let is_focused = current_focus == button_focus; let base_style = Style::default().fg(if is_focused { @@ -163,12 +161,11 @@ pub fn render_add_logic( } }; - // Bottom Buttons let button_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Percentage(50), // Save Button - Constraint::Percentage(50), // Cancel Button + Constraint::Percentage(50), + Constraint::Percentage(50), ]) .split(buttons_area); @@ -206,7 +203,6 @@ pub fn render_add_logic( ); f.render_widget(cancel_button, button_chunks[1]); - // Dialog rendering if app_state.ui.dialog.dialog_show { dialog::render_dialog( f, diff --git a/client/src/functions/modes/navigation/add_logic_nav.rs b/client/src/functions/modes/navigation/add_logic_nav.rs index c0584d0..0c85204 100644 --- a/client/src/functions/modes/navigation/add_logic_nav.rs +++ b/client/src/functions/modes/navigation/add_logic_nav.rs @@ -1,4 +1,4 @@ -// client/src/functions/modes/navigation/add_logic_nav.rs +// src/functions/modes/navigation/add_logic_nav.rs use crate::config::binds::config::Config; use crate::state::{ app::state::AppState, @@ -6,17 +6,19 @@ use crate::state::{ app::buffer::AppView, app::buffer::BufferState, }; -use crate::state::pages::canvas_state::CanvasState; -use crossterm::event::{KeyEvent}; +// Now that client/Cargo.toml uses crossterm 0.28.1, +// this KeyEvent will match what ratatui and tui-textarea expect. +use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; use crate::services::GrpcClient; use tokio::sync::mpsc; use anyhow::Result; -use common::proto::multieko2::table_script::{PostTableScriptRequest}; +use common::proto::multieko2::table_script::PostTableScriptRequest; +use tui_textarea::Input as TextAreaInput; // Import with an alias pub type SaveLogicResultSender = mpsc::Sender>; pub fn handle_add_logic_navigation( - key: KeyEvent, + key_event: KeyEvent, // This is crossterm::event::KeyEvent v0.28.1 config: &Config, app_state: &mut AppState, add_logic_state: &mut AddLogicState, @@ -26,250 +28,180 @@ pub fn handle_add_logic_navigation( save_logic_sender: SaveLogicResultSender, command_message: &mut String, ) -> bool { - let action = config.get_general_action(key.code, key.modifiers).map(String::from); + let action = config.get_general_action(key_event.code, key_event.modifiers).map(String::from); let mut handled = false; - // Check if focus is on canvas input fields - let focus_on_canvas_inputs = matches!( - add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription - ); + if add_logic_state.current_focus == AddLogicFocus::InputScriptContent { + // Add explicit type annotation for .into() + let textarea_input: TextAreaInput = key_event.into(); - // Handle script content editing separately (multiline) - if *is_edit_mode && add_logic_state.current_focus == AddLogicFocus::InputScriptContent { - match key.code { - crossterm::event::KeyCode::Char(c) => { - add_logic_state.script_content_input.push(c); - add_logic_state.has_unsaved_changes = true; - handled = true; + if *is_edit_mode { + if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE { + *is_edit_mode = false; + *command_message = "Exited script edit mode. Press Ctrl+E/Enter to re-enter.".to_string(); + return true; } - crossterm::event::KeyCode::Enter => { - add_logic_state.script_content_input.push('\n'); + + let changed = add_logic_state.script_content_editor.borrow_mut().input(textarea_input); + if changed { add_logic_state.has_unsaved_changes = true; - add_logic_state.script_content_scroll.0 = add_logic_state.script_content_scroll.0.saturating_add(1); - handled = true; } - crossterm::event::KeyCode::Backspace => { - if !add_logic_state.script_content_input.is_empty() { - add_logic_state.script_content_input.pop(); - add_logic_state.has_unsaved_changes = true; + handled = true; + } else { + match key_event.code { + KeyCode::Enter if key_event.modifiers == KeyModifiers::NONE => { + *is_edit_mode = true; + *command_message = "Entered script edit mode.".to_string(); + handled = true; + } + KeyCode::Up | KeyCode::Down | KeyCode::PageUp | KeyCode::PageDown | + KeyCode::Left | KeyCode::Right | KeyCode::Home | KeyCode::End => { + let changed = add_logic_state.script_content_editor.borrow_mut().input(textarea_input); + if changed { + add_logic_state.has_unsaved_changes = true; + } + handled = true; + } + KeyCode::Esc if key_event.modifiers == KeyModifiers::NONE => { + add_logic_state.current_focus = AddLogicFocus::InputDescription; + app_state.ui.focus_outside_canvas = false; + *command_message = "Script content unfocused.".to_string(); + *is_edit_mode = matches!(add_logic_state.current_focus, + AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription); + handled = true; + } + _ => {} + } + } + if handled { return true; } + } + + // ... (rest of the function remains the same) + match action.as_deref() { + Some("exit_view") | Some("cancel_action") => { + buffer_state.update_history(AppView::Admin); + app_state.ui.show_add_logic = false; + *command_message = "Exited Add Logic".to_string(); + handled = true; + } + Some("next_field") => { + let previous_focus = add_logic_state.current_focus; + add_logic_state.current_focus = match add_logic_state.current_focus { + AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn, + AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription, + AddLogicFocus::InputDescription => AddLogicFocus::InputScriptContent, + AddLogicFocus::InputScriptContent => AddLogicFocus::SaveButton, + AddLogicFocus::SaveButton => AddLogicFocus::CancelButton, + AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName, + }; + if previous_focus == AddLogicFocus::InputScriptContent && + add_logic_state.current_focus != AddLogicFocus::InputScriptContent { + *is_edit_mode = false; + } + *is_edit_mode = matches!(add_logic_state.current_focus, + AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription); + + app_state.ui.focus_outside_canvas = !matches!( + add_logic_state.current_focus, + AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent + ); + *command_message = format!("Focus: {:?}", add_logic_state.current_focus); + handled = true; + } + Some("prev_field") => { + let previous_focus = add_logic_state.current_focus; + add_logic_state.current_focus = match add_logic_state.current_focus { + AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton, + AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName, + AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn, + AddLogicFocus::InputScriptContent => AddLogicFocus::InputDescription, + AddLogicFocus::SaveButton => AddLogicFocus::InputScriptContent, + AddLogicFocus::CancelButton => AddLogicFocus::SaveButton, + }; + if previous_focus == AddLogicFocus::InputScriptContent && + add_logic_state.current_focus != AddLogicFocus::InputScriptContent { + *is_edit_mode = false; + } + *is_edit_mode = matches!(add_logic_state.current_focus, + AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription); + + app_state.ui.focus_outside_canvas = !matches!( + add_logic_state.current_focus, + AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent + ); + *command_message = format!("Focus: {:?}", add_logic_state.current_focus); + handled = true; + } + Some("select") => { + match add_logic_state.current_focus { + AddLogicFocus::SaveButton => { + if let Some(table_def_id) = add_logic_state.selected_table_id { + let script_lines = add_logic_state.script_content_editor.borrow().lines().to_vec(); + let script_content = script_lines.join("\n"); + + if add_logic_state.target_column_input.trim().is_empty() { + *command_message = "Cannot save: Target Column cannot be empty.".to_string(); + } else if script_content.trim().is_empty() { + *command_message = "Cannot save: Script Content cannot be empty.".to_string(); + } else { + *command_message = "Saving logic script...".to_string(); + app_state.show_loading_dialog("Saving Script", "Please wait..."); + + let request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: add_logic_state.target_column_input.trim().to_string(), + script: script_content.trim().to_string(), + description: add_logic_state.description_input.trim().to_string(), + }; + let mut client_clone = grpc_client.clone(); + let sender_clone = save_logic_sender.clone(); + tokio::spawn(async move { + let result = client_clone.post_table_script(request).await + .map(|res| format!("Script saved with ID: {}", res.id)) + .map_err(|e| anyhow::anyhow!("gRPC call failed: {}", e)); + if sender_clone.send(result).await.is_err() { + // Log error or handle if receiver dropped + } + }); + } + } else { + *command_message = "Cannot save: Table Definition ID is missing.".to_string(); + } + handled = true; + } + AddLogicFocus::CancelButton => { + buffer_state.update_history(AppView::Admin); + app_state.ui.show_add_logic = false; + *command_message = "Cancelled Add Logic".to_string(); + handled = true; + } + AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => { + *is_edit_mode = !*is_edit_mode; + *command_message = format!("Field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" }); + handled = true; + } + AddLogicFocus::InputScriptContent => { + if !*is_edit_mode { + *is_edit_mode = true; + *command_message = "Entered script edit mode.".to_string(); + } handled = true; } } - _ => {} } - } - - if !handled { - match action.as_deref() { - Some("exit_view") | Some("cancel_action") => { - buffer_state.update_history(AppView::Admin); // Fixed: was AdminPanel - app_state.ui.focus_outside_canvas = true; - *command_message = "Exited Add Logic".to_string(); - handled = true; - } - Some("next_field") => { - let previous_focus = add_logic_state.current_focus; - add_logic_state.current_focus = match add_logic_state.current_focus { - AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn, - AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription, - AddLogicFocus::InputDescription => AddLogicFocus::InputScriptContent, - AddLogicFocus::InputScriptContent => AddLogicFocus::SaveButton, - AddLogicFocus::SaveButton => AddLogicFocus::CancelButton, - AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName, - }; - - // Update canvas field index only when moving between canvas inputs - if matches!(previous_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn) { - if matches!(add_logic_state.current_focus, AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) { - let new_field = match add_logic_state.current_focus { - AddLogicFocus::InputTargetColumn => 1, - AddLogicFocus::InputDescription => 2, - _ => 0, - }; - add_logic_state.set_current_field(new_field); - } + Some("toggle_edit_mode") => { + match add_logic_state.current_focus { + AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent => { + *is_edit_mode = !*is_edit_mode; + *command_message = format!("Edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" }); } - - // Update focus outside canvas flag - app_state.ui.focus_outside_canvas = !matches!( - add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription - ); - - *command_message = format!("Focus: {:?}", add_logic_state.current_focus); - *is_edit_mode = matches!(add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | - AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent); - handled = true; - } - Some("prev_field") => { - let previous_focus = add_logic_state.current_focus; - add_logic_state.current_focus = match add_logic_state.current_focus { - AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton, - AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName, - AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn, - AddLogicFocus::InputScriptContent => AddLogicFocus::InputDescription, - AddLogicFocus::SaveButton => AddLogicFocus::InputScriptContent, - AddLogicFocus::CancelButton => AddLogicFocus::SaveButton, - }; - - // Update canvas field index only when moving between canvas inputs - if matches!(previous_focus, AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) { - if matches!(add_logic_state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn) { - let new_field = match add_logic_state.current_focus { - AddLogicFocus::InputLogicName => 0, - AddLogicFocus::InputTargetColumn => 1, - _ => 0, - }; - add_logic_state.set_current_field(new_field); - } - } - - // Update focus outside canvas flag - app_state.ui.focus_outside_canvas = !matches!( - add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription - ); - - *command_message = format!("Focus: {:?}", add_logic_state.current_focus); - *is_edit_mode = matches!(add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | - AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent); - handled = true; - } - Some("next_option") => { // Horizontal next - let previous_focus = add_logic_state.current_focus; - add_logic_state.current_focus = match add_logic_state.current_focus { - AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn, - AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription, - AddLogicFocus::InputDescription => AddLogicFocus::InputScriptContent, - AddLogicFocus::InputScriptContent => AddLogicFocus::SaveButton, - AddLogicFocus::SaveButton => AddLogicFocus::CancelButton, - AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName, // Cycle back - }; - // Update canvas field index if moving within canvas inputs - if matches!(add_logic_state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) { - let new_field = match add_logic_state.current_focus { - AddLogicFocus::InputLogicName => 0, - AddLogicFocus::InputTargetColumn => 1, - AddLogicFocus::InputDescription => 2, - _ => add_logic_state.current_field(), // Should not happen - }; - add_logic_state.set_current_field(new_field); - } - app_state.ui.focus_outside_canvas = !matches!( - add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription - ); - *command_message = format!("Focus: {:?}", add_logic_state.current_focus); - *is_edit_mode = matches!(add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | - AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent); - handled = true; - } - Some("previous_option") => { // Horizontal previous - let previous_focus = add_logic_state.current_focus; - add_logic_state.current_focus = match add_logic_state.current_focus { - AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton, // Cycle back - AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName, - AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn, - AddLogicFocus::InputScriptContent => AddLogicFocus::InputDescription, - AddLogicFocus::SaveButton => AddLogicFocus::InputScriptContent, - AddLogicFocus::CancelButton => AddLogicFocus::SaveButton, - }; - // Update canvas field index if moving within canvas inputs - if matches!(add_logic_state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) { - let new_field = match add_logic_state.current_focus { - AddLogicFocus::InputLogicName => 0, - AddLogicFocus::InputTargetColumn => 1, - AddLogicFocus::InputDescription => 2, - _ => add_logic_state.current_field(), // Should not happen - }; - add_logic_state.set_current_field(new_field); - } - app_state.ui.focus_outside_canvas = !matches!( - add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription - ); - *command_message = format!("Focus: {:?}", add_logic_state.current_focus); - *is_edit_mode = matches!(add_logic_state.current_focus, - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | - AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent); - handled = true; - } - Some("select") => { - match add_logic_state.current_focus { - AddLogicFocus::SaveButton => { - if let Some(table_def_id) = add_logic_state.selected_table_id { - if add_logic_state.target_column_input.trim().is_empty() { - *command_message = "Cannot save: Target Column cannot be empty.".to_string(); - } else if add_logic_state.script_content_input.trim().is_empty() { - *command_message = "Cannot save: Script Content cannot be empty.".to_string(); - } else { - *command_message = "Saving logic script...".to_string(); - app_state.show_loading_dialog("Saving Script", "Please wait..."); - - let request = PostTableScriptRequest { - table_definition_id: table_def_id, - target_column: add_logic_state.target_column_input.trim().to_string(), - script: add_logic_state.script_content_input.trim().to_string(), - description: add_logic_state.description_input.trim().to_string(), - }; - - let mut client_clone = grpc_client.clone(); - let sender_clone = save_logic_sender.clone(); - - tokio::spawn(async move { - let result = client_clone.post_table_script(request).await - .map(|res| format!("Script saved with ID: {}", res.id)) - .map_err(|e| anyhow::anyhow!("gRPC call failed: {}", e)); - let _ = sender_clone.send(result).await; - }); - } - } else { - *command_message = "Cannot save: Table Definition ID is missing.".to_string(); - } - handled = true; - } - AddLogicFocus::CancelButton => { - buffer_state.update_history(AppView::Admin); // Fixed: was AdminPanel - app_state.ui.focus_outside_canvas = true; - *command_message = "Cancelled Add Logic".to_string(); - handled = true; - } - AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | - AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent => { - if !*is_edit_mode { - *is_edit_mode = true; - *command_message = "Edit mode: ON".to_string(); - } - handled = true; - } + _ => { + *command_message = "Cannot toggle edit mode here.".to_string(); } } - Some("toggle_edit_mode") => { - *is_edit_mode = !*is_edit_mode; - *command_message = format!("Edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" }); - handled = true; - } - // Handle script content scrolling when not in edit mode - _ if !*is_edit_mode && add_logic_state.current_focus == AddLogicFocus::InputScriptContent => { - match action.as_deref() { - Some("move_up") => { - add_logic_state.script_content_scroll.0 = add_logic_state.script_content_scroll.0.saturating_sub(1); - handled = true; - } - Some("move_down") => { - add_logic_state.script_content_scroll.0 = add_logic_state.script_content_scroll.0.saturating_add(1); - handled = true; - } - _ => {} - } - } - _ => {} + handled = true; } + _ => {} } handled } - diff --git a/client/src/state/pages/add_logic.rs b/client/src/state/pages/add_logic.rs index 2d2d88c..690b130 100644 --- a/client/src/state/pages/add_logic.rs +++ b/client/src/state/pages/add_logic.rs @@ -1,5 +1,10 @@ // src/state/pages/add_logic.rs use crate::state::pages::canvas_state::CanvasState; +use std::cell::RefCell; +use std::rc::Rc; +use tui_textarea::TextArea; +// Removed unused Style, Color imports if not used in Default for TextArea styling +// use ratatui::style::{Color, Style}; // Keep if you add custom styling in default() #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AddLogicFocus { @@ -12,46 +17,47 @@ pub enum AddLogicFocus { CancelButton, } -#[derive(Debug, Clone)] +#[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_input: String, + pub script_content_editor: Rc>>, pub description_input: String, pub current_focus: AddLogicFocus, pub logic_name_cursor_pos: usize, pub target_column_cursor_pos: usize, - pub script_content_scroll: (u16, u16), // (vertical, horizontal) pub description_cursor_pos: usize, pub has_unsaved_changes: bool, } impl Default for AddLogicState { fn default() -> Self { + let editor = TextArea::default(); // No 'mut' needed if not modified further here + // Example: editor.set_placeholder_text("Enter script..."); + // Example: editor.set_line_number_style(Style::default().fg(Color::DarkGray)); 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_input: String::new(), + script_content_editor: Rc::new(RefCell::new(editor)), description_input: String::new(), current_focus: AddLogicFocus::InputLogicName, logic_name_cursor_pos: 0, target_column_cursor_pos: 0, - script_content_scroll: (0, 0), description_cursor_pos: 0, has_unsaved_changes: false, } } } +// ... rest of the CanvasState impl remains the same impl AddLogicState { - // Number of canvas-editable fields - pub const INPUT_FIELD_COUNT: usize = 3; // Logic Name, Target Column, Description + pub const INPUT_FIELD_COUNT: usize = 3; } impl CanvasState for AddLogicState { @@ -60,7 +66,7 @@ impl CanvasState for AddLogicState { AddLogicFocus::InputLogicName => 0, AddLogicFocus::InputTargetColumn => 1, AddLogicFocus::InputDescription => 2, - _ => 0, // Default or non-input focus + _ => 0, } } @@ -99,7 +105,7 @@ impl CanvasState for AddLogicState { AddLogicFocus::InputLogicName => &mut self.logic_name_input, AddLogicFocus::InputTargetColumn => &mut self.target_column_input, AddLogicFocus::InputDescription => &mut self.description_input, - _ => &mut self.logic_name_input, // Placeholder, should not be hit if focus is correct + _ => &mut self.logic_name_input, } } @@ -112,7 +118,7 @@ impl CanvasState for AddLogicState { 0 => AddLogicFocus::InputLogicName, 1 => AddLogicFocus::InputTargetColumn, 2 => AddLogicFocus::InputDescription, - _ => self.current_focus, // Stay if out of bounds + _ => self.current_focus, }; } @@ -122,10 +128,12 @@ impl CanvasState for AddLogicState { self.logic_name_cursor_pos = pos.min(self.logic_name_input.len()); } AddLogicFocus::InputTargetColumn => { - self.target_column_cursor_pos = pos.min(self.target_column_input.len()); + self.target_column_cursor_pos = + pos.min(self.target_column_input.len()); } AddLogicFocus::InputDescription => { - self.description_cursor_pos = pos.min(self.description_input.len()); + self.description_cursor_pos = + pos.min(self.description_input.len()); } _ => {} }