From 58fdaa8298f570681ae42f3ac432d828ea17b4ca Mon Sep 17 00:00:00 2001 From: filipriec Date: Fri, 23 May 2025 13:34:49 +0200 Subject: [PATCH] add logic, not working tho --- client/src/components/admin/add_logic.rs | 194 ++++++++++++++++ client/src/functions/common/buffer.rs | 2 +- client/src/functions/modes/navigation.rs | 1 + .../modes/navigation/add_logic_nav.rs | 215 ++++++++++++++++++ .../functions/modes/navigation/admin_nav.rs | 141 ++++++------ client/src/modes/general/dialog.rs | 10 + client/src/modes/handlers/event.rs | 4 + client/src/services/grpc_client.rs | 14 ++ client/src/state/app/buffer.rs | 2 + client/src/state/app/state.rs | 2 + client/src/state/pages.rs | 1 + client/src/state/pages/add_logic.rs | 145 ++++++++++++ client/src/state/pages/admin.rs | 2 + client/src/ui/handlers/context.rs | 5 +- client/src/ui/handlers/ui.rs | 5 + 15 files changed, 671 insertions(+), 72 deletions(-) create mode 100644 client/src/components/admin/add_logic.rs create mode 100644 client/src/functions/modes/navigation/add_logic_nav.rs create mode 100644 client/src/state/pages/add_logic.rs diff --git a/client/src/components/admin/add_logic.rs b/client/src/components/admin/add_logic.rs new file mode 100644 index 0000000..d9cb673 --- /dev/null +++ b/client/src/components/admin/add_logic.rs @@ -0,0 +1,194 @@ +// src/components/admin/add_logic.rs +use crate::config::colors::themes::Theme; +use crate::state::app::highlight::HighlightState; +use crate::state::app::state::AppState; +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}, + Frame, +}; +use crate::components::handlers::canvas::render_canvas; +use crate::components::common::dialog; + +pub fn render_add_logic( + f: &mut Frame, + area: Rect, + theme: &Theme, + app_state: &AppState, + add_logic_state: &mut AddLogicState, + is_edit_mode: bool, + highlight_state: &HighlightState, +) { + let main_block = Block::default() + .title(" Add New Logic Script ") + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(theme.border)) + .style(Style::default().bg(theme.bg)); + let inner_area = main_block.inner(area); + f.render_widget(main_block, area); + + // Calculate areas dynamically like add_table + 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 + ]) + .split(inner_area); + + let top_info_area = main_chunks[0]; + let canvas_area = main_chunks[1]; + let script_content_area = main_chunks[2]; + let buttons_area = main_chunks[3]; + + // Top Info Rendering (like add_table) + let profile_text = Paragraph::new(vec![ + Line::from(Span::styled( + format!("Profile: {}", add_logic_state.profile_name), + 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()) + ), + theme.fg, + )), + ]) + .block( + Block::default() + .borders(Borders::BOTTOM) + .border_style(Style::default().fg(theme.secondary)), + ); + f.render_widget(profile_text, top_info_area); + + // Canvas rendering for input fields (like add_table) + let focus_on_canvas_inputs = matches!( + add_logic_state.current_focus, + AddLogicFocus::InputLogicName + | AddLogicFocus::InputTargetColumn + | AddLogicFocus::InputDescription + ); + + render_canvas( + f, + canvas_area, + add_logic_state, + &add_logic_state.fields(), + &add_logic_state.current_field(), + &add_logic_state.inputs(), + theme, + is_edit_mode && focus_on_canvas_inputs, + highlight_state, + ); + + // Script Content Area + 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 script_block = Block::default() + .title(" Steel Script Content ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(script_block_border_style); + + 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 (same as add_table) + 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 { + theme.highlight + } else { + theme.secondary + }); + if is_focused { + base_style.add_modifier(Modifier::BOLD) + } else { + base_style + } + }; + + let get_button_border_style = |is_focused: bool, theme: &Theme| { + if is_focused { + Style::default().fg(theme.highlight) + } else { + Style::default().fg(theme.secondary) + } + }; + + // Bottom Buttons (same style as add_table) + let button_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(50), // Save Button + Constraint::Percentage(50), // Cancel Button + ]) + .split(buttons_area); + + let save_button = Paragraph::new(" Save Logic ") + .style(get_button_style( + AddLogicFocus::SaveButton, + add_logic_state.current_focus, + )) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(get_button_border_style( + add_logic_state.current_focus == AddLogicFocus::SaveButton, + theme, + )), + ); + f.render_widget(save_button, button_chunks[0]); + + let cancel_button = Paragraph::new(" Cancel ") + .style(get_button_style( + AddLogicFocus::CancelButton, + add_logic_state.current_focus, + )) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(get_button_border_style( + add_logic_state.current_focus == AddLogicFocus::CancelButton, + theme, + )), + ); + f.render_widget(cancel_button, button_chunks[1]); + + // Dialog rendering (same as add_table) + if app_state.ui.dialog.dialog_show { + dialog::render_dialog( + f, + f.area(), + theme, + &app_state.ui.dialog.dialog_title, + &app_state.ui.dialog.dialog_message, + &app_state.ui.dialog.dialog_buttons, + app_state.ui.dialog.dialog_active_button_index, + app_state.ui.dialog.is_loading, + ); + } +} + diff --git a/client/src/functions/common/buffer.rs b/client/src/functions/common/buffer.rs index 0e3f7e8..a1d8ba4 100644 --- a/client/src/functions/common/buffer.rs +++ b/client/src/functions/common/buffer.rs @@ -6,7 +6,7 @@ use crate::state::app::buffer::AppView; pub fn get_view_layer(view: &AppView) -> u8 { match view { AppView::Intro => 1, - AppView::Login | AppView::Register | AppView::Admin | AppView::AddTable => 2, + AppView::Login | AppView::Register | AppView::Admin | AppView::AddTable | AppView::AddLogic => 2, AppView::Form(_) | AppView::Scratch => 3, } } diff --git a/client/src/functions/modes/navigation.rs b/client/src/functions/modes/navigation.rs index cfa5e47..d963419 100644 --- a/client/src/functions/modes/navigation.rs +++ b/client/src/functions/modes/navigation.rs @@ -2,3 +2,4 @@ pub mod admin_nav; pub mod add_table_nav; +pub mod add_logic_nav; diff --git a/client/src/functions/modes/navigation/add_logic_nav.rs b/client/src/functions/modes/navigation/add_logic_nav.rs new file mode 100644 index 0000000..a24e672 --- /dev/null +++ b/client/src/functions/modes/navigation/add_logic_nav.rs @@ -0,0 +1,215 @@ +// client/src/functions/modes/navigation/add_logic_nav.rs +use crate::config::binds::config::Config; +use crate::state::{ + app::state::AppState, + pages::add_logic::{AddLogicFocus, AddLogicState}, + app::buffer::AppView, + app::buffer::BufferState, +}; +use crate::state::pages::canvas_state::CanvasState; +use crossterm::event::{KeyEvent}; +use crate::services::GrpcClient; +use tokio::sync::mpsc; +use anyhow::Result; +use common::proto::multieko2::table_script::{PostTableScriptRequest}; + +pub type SaveLogicResultSender = mpsc::Sender>; + +pub fn handle_add_logic_navigation( + key: KeyEvent, + config: &Config, + app_state: &mut AppState, + add_logic_state: &mut AddLogicState, + is_edit_mode: &mut bool, + buffer_state: &mut BufferState, + grpc_client: GrpcClient, + save_logic_sender: SaveLogicResultSender, + command_message: &mut String, +) -> bool { + let action = config.get_general_action(key.code, key.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 + ); + + // 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; + } + crossterm::event::KeyCode::Enter => { + add_logic_state.script_content_input.push('\n'); + 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; + } + } + _ => {} + } + } + + 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); + } + } + + // 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("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; + } + } + } + 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 +} + diff --git a/client/src/functions/modes/navigation/admin_nav.rs b/client/src/functions/modes/navigation/admin_nav.rs index 2fa029a..0612ce3 100644 --- a/client/src/functions/modes/navigation/admin_nav.rs +++ b/client/src/functions/modes/navigation/admin_nav.rs @@ -9,6 +9,7 @@ use crossterm::event::KeyEvent; use crate::state::app::buffer::AppView; use crate::state::app::buffer::BufferState; use crate::state::pages::add_table::{AddTableState, LinkDefinition}; +use crate::state::pages::add_logic::AddLogicState; use ratatui::widgets::ListState; // --- Helper functions for ListState navigation (similar to TableState) --- @@ -68,11 +69,10 @@ pub fn handle_admin_navigation( } } AdminFocus::Tables => { - // Do nothing when focus is on the Tables pane itself *command_message = "Press Enter to select and scroll tables".to_string(); } - AdminFocus::InsideTablesList => { // Scroll inside - if let Some(p_idx) = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) { // Use nav or persistent selection + AdminFocus::InsideTablesList => { + if let Some(p_idx) = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) { if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { list_select_previous(&mut admin_state.table_list_state, profile.tables.len()); } @@ -85,17 +85,15 @@ pub fn handle_admin_navigation( match current_focus { AdminFocus::Profiles => { if profile_count > 0 { - // Updates navigation state, resets table state admin_state.next_profile(profile_count); *command_message = "Navigated profiles list".to_string(); } } AdminFocus::Tables => { - // Do nothing when focus is on the Tables pane itself *command_message = "Press Enter to select and scroll tables".to_string(); } - AdminFocus::InsideTablesList => { // Scroll inside - if let Some(p_idx) = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) { // Use nav or persistent selection + AdminFocus::InsideTablesList => { + if let Some(p_idx) = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) { if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { list_select_next(&mut admin_state.table_list_state, profile.tables.len()); } @@ -110,18 +108,16 @@ pub fn handle_admin_navigation( let is_next = action.as_deref() == Some("next_option"); admin_state.current_focus = match old_focus { - AdminFocus::Profiles => if is_next { AdminFocus::Tables } else { AdminFocus::Button3 }, // P -> T (l) or P -> B3 (h) - AdminFocus::Tables => if is_next { AdminFocus::Button1 } else { AdminFocus::Profiles }, // T -> B1 (l) or T -> P (h) - AdminFocus::Button1 => if is_next { AdminFocus::Button2 } else { AdminFocus::Tables }, // B1 -> B2 (l) or B1 -> T (h) - AdminFocus::Button2 => if is_next { AdminFocus::Button3 } else { AdminFocus::Button1 }, // B2 -> B3 (l) or B2 -> B1 (h) - AdminFocus::Button3 => if is_next { AdminFocus::Profiles } else { AdminFocus::Button2 }, // B3 -> P (l) or B3 -> B2 (h) - // Prevent horizontal nav when inside lists + AdminFocus::Profiles => if is_next { AdminFocus::Tables } else { AdminFocus::Button3 }, + AdminFocus::Tables => if is_next { AdminFocus::Button1 } else { AdminFocus::Profiles }, + AdminFocus::Button1 => if is_next { AdminFocus::Button2 } else { AdminFocus::Tables }, + AdminFocus::Button2 => if is_next { AdminFocus::Button3 } else { AdminFocus::Button1 }, + AdminFocus::Button3 => if is_next { AdminFocus::Profiles } else { AdminFocus::Button2 }, AdminFocus::InsideTablesList => old_focus, }; - let new_focus = admin_state.current_focus; + new_focus = admin_state.current_focus; // Update new_focus after changing admin_state.current_focus *command_message = format!("Focus set to {:?}", new_focus); - // Auto-select first item only when moving from Profiles to Tables via 'l' if old_focus == AdminFocus::Profiles && new_focus == AdminFocus::Tables && is_next { if let Some(profile_idx) = admin_state.profile_list_state.selected() { if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) { @@ -137,37 +133,24 @@ pub fn handle_admin_navigation( admin_state.table_list_state.select(None); } } - // Clear table nav selection if moving away from Tables - if old_focus == AdminFocus::Tables && new_focus != AdminFocus::Tables { + if old_focus == AdminFocus::Tables && new_focus != AdminFocus::Tables && old_focus != AdminFocus::InsideTablesList { admin_state.table_list_state.select(None); } - // Clear profile nav selection if moving away from Profiles - if old_focus == AdminFocus::Profiles && new_focus != AdminFocus::Profiles { - // Maybe keep profile nav highlight? Let's try clearing it. - // admin_state.profile_list_state.select(None); // Optional: clear profile nav highlight - } - + // No change needed for profile_list_state clearing here based on current logic } // --- Selection --- Some("select") => { match current_focus { AdminFocus::Profiles => { - // --- Perform persistent selection --- - // Set the persistent selection to the currently navigated item if let Some(nav_idx) = admin_state.profile_list_state.selected() { admin_state.selected_profile_index = Some(nav_idx); - - // Move focus to Tables (like pressing 'l') new_focus = AdminFocus::Tables; - - // Select the first table for navigation highlight - admin_state.table_list_state.select(None); // Clear table nav first - admin_state.selected_table_index = None; // Clear persistent table selection + admin_state.table_list_state.select(None); + admin_state.selected_table_index = None; if let Some(profile) = app_state.profile_tree.profiles.get(nav_idx) { if !profile.tables.is_empty() { admin_state.table_list_state.select(Some(0)); } - *command_message = format!("Selected profile: {}", app_state.profile_tree.profiles[nav_idx].name); } } else { @@ -175,9 +158,7 @@ pub fn handle_admin_navigation( } } AdminFocus::Tables => { - // --- Enter InsideTablesList focus --- new_focus = AdminFocus::InsideTablesList; - // Select first item if none selected when entering if let Some(p_idx) = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) { if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { if admin_state.table_list_state.selected().is_none() && !profile.tables.is_empty() { @@ -188,11 +169,8 @@ pub fn handle_admin_navigation( *command_message = "Entered Tables List (Select item with Enter, Exit with Esc)".to_string(); } AdminFocus::InsideTablesList => { - // --- Perform persistent selection --- - // Set the persistent selection to the currently navigated item if let Some(nav_idx) = admin_state.table_list_state.selected() { - admin_state.selected_table_index = Some(nav_idx); // Set persistent selection - // Get table name for message + admin_state.selected_table_index = Some(nav_idx); let table_name = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) .and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx)) .and_then(|p| p.tables.get(nav_idx).map(|t| t.name.clone())) @@ -201,91 +179,116 @@ pub fn handle_admin_navigation( } else { *command_message = "No table highlighted".to_string(); } - // Stay inside } - AdminFocus::Button1 => { - *command_message = "Action: Add Logic (Not Implemented)".to_string(); - // TODO: Trigger action for Button 1 + AdminFocus::Button1 => { // Add Logic + let mut logic_state_profile_name = "None (Global)".to_string(); + let mut selected_table_id: Option = None; + let mut selected_table_name_for_logic: Option = None; + if let Some(p_idx) = admin_state.selected_profile_index { + if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { + logic_state_profile_name = profile.name.clone(); + // Check for persistently selected table within this profile + if let Some(t_idx) = admin_state.selected_table_index { + if let Some(table) = profile.tables.get(t_idx) { + selected_table_id = None; + selected_table_name_for_logic = Some(table.name.clone()); + *command_message = format!("Adding logic for table: {}. CRITICAL: Table ID not found in profile tree response!", table.name); + } else { + *command_message = format!("Selected table index {} out of bounds for profile '{}'. Logic will not be table-specific.", t_idx, profile.name); + }} else { + *command_message = format!("No table selected in profile '{}'. Logic will not be table-specific.", profile.name); + } + } else { + *command_message = "Error: Selected profile index out of bounds, associating with 'None'.".to_string(); + } + } else { + *command_message = "No profile selected ([*]), associating Logic with 'None (Global)'.".to_string(); + // Keep logic_state_profile_name as "None (Global)" + } + + admin_state.add_logic_state = AddLogicState { + profile_name: logic_state_profile_name.clone(), + ..AddLogicState::default() + }; + buffer_state.update_history(AppView::AddLogic); + app_state.ui.focus_outside_canvas = false; + // Command message might be overwritten if profile selection had an issue, + // so set the navigation message last if no error. + if !command_message.starts_with("Error:") && !command_message.contains("associating Logic with 'None (Global)'") { + *command_message = format!( + "Navigating to Add Logic for profile '{}'...", + logic_state_profile_name + ); + } else if command_message.contains("associating Logic with 'None (Global)'") { + // Append to existing message + let existing_msg = command_message.clone(); + *command_message = format!( + "{} Navigating to Add Logic...", + existing_msg + ); + } } AdminFocus::Button2 => { - // --- Prepare AddTableState based on persistent selections --- if let Some(p_idx) = admin_state.selected_profile_index { if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { let selected_profile_name = profile.name.clone(); - // Populate links from the selected profile's tables let available_links: Vec = profile .tables .iter() .map(|table| LinkDefinition { linked_table_name: table.name.clone(), - is_required: false, // Default - selected: false, // Default + is_required: false, + selected: false, }) .collect(); - // Create and populate the new AddTableState let new_add_table_state = AddTableState { profile_name: selected_profile_name, - links: available_links, // Assign populated links + links: available_links, ..AddTableState::default() }; - - // Assign the prepared state admin_state.add_table_state = new_add_table_state; - - // Switch view buffer_state.update_history(AppView::AddTable); app_state.ui.focus_outside_canvas = false; *command_message = format!( "Navigating to Add Table for profile '{}'...", admin_state.add_table_state.profile_name ); - } else { *command_message = "Error: Selected profile index out of bounds.".to_string(); } } else { *command_message = "Please select a profile ([*]) first.".to_string(); } - // --- End preparation --- } AdminFocus::Button3 => { *command_message = "Action: Change Table (Not Implemented)".to_string(); - // TODO: Trigger action for Button 3 } } } - // --- Handle Exiting Inside Mode --- - Some("exit_table_scroll") => { // Assuming you have this action bound (e.g., to Esc) + Some("exit_table_scroll") => { match current_focus { AdminFocus::InsideTablesList => { new_focus = AdminFocus::Tables; - admin_state.table_list_state.select(None); // Clear nav highlight on exit + admin_state.table_list_state.select(None); *command_message = "Exited Tables List".to_string(); } - _ => handled = false, // Not applicable + _ => handled = false, } } - // --- Other General Keys (Ignore for admin nav) --- Some("toggle_sidebar") | Some("toggle_buffer_list") | Some("next_field") | Some("prev_field") => { - // These are handled globally or not applicable here. handled = false; } - // --- No matching action --- - _ => handled = false, // Event not handled by admin navigation + _ => handled = false, } - // Update focus state if it changed and was handled - if handled && current_focus != new_focus { + if handled && admin_state.current_focus != new_focus { // Check admin_state.current_focus admin_state.current_focus = new_focus; - // Avoid overwriting specific messages set during 'select' or 'exit' handling if command_message.is_empty() || command_message.starts_with("Focus set to") { *command_message = format!("Focus set to {:?}", admin_state.current_focus); } - } else if !handled { - // command_message.clear(); // Optional: Clear message if not handled here } - handled // Return whether the event was handled by this function + handled } diff --git a/client/src/modes/general/dialog.rs b/client/src/modes/general/dialog.rs index b8fa2b7..e390b6e 100644 --- a/client/src/modes/general/dialog.rs +++ b/client/src/modes/general/dialog.rs @@ -139,6 +139,16 @@ pub async fn handle_dialog_event( _ => { /* Handle unexpected index */ } } } + DialogPurpose::SaveLogicSuccess => { + match selected_index { + 0 => { // "OK" button selected + app_state.hide_dialog(); + buffer_state.update_history(AppView::Admin); + return Some(Ok(EventOutcome::Ok("Save success dialog dismissed.".to_string()))); + } + _ => { /* Handle unexpected index */ } + } + } } } _ => {} // Ignore other general actions when dialog is shown diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 5f48d34..8904827 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -41,6 +41,7 @@ use tokio::sync::mpsc; use crate::tui::functions::common::login::LoginResult; use crate::tui::functions::common::register::RegisterResult; use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; +use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EventOutcome { @@ -63,6 +64,7 @@ pub struct EventHandler { pub login_result_sender: mpsc::Sender, pub register_result_sender: mpsc::Sender, pub save_table_result_sender: SaveTableResultSender, + pub save_logic_result_sender: SaveLogicResultSender, } impl EventHandler { @@ -70,6 +72,7 @@ impl EventHandler { login_result_sender: mpsc::Sender, register_result_sender: mpsc::Sender, save_table_result_sender: SaveTableResultSender, + save_logic_result_sender: SaveLogicResultSender, ) -> Result { Ok(EventHandler { command_mode: false, @@ -84,6 +87,7 @@ impl EventHandler { login_result_sender, register_result_sender, save_table_result_sender, + save_logic_result_sender, }) } diff --git a/client/src/services/grpc_client.rs b/client/src/services/grpc_client.rs index e56b8a2..953afc9 100644 --- a/client/src/services/grpc_client.rs +++ b/client/src/services/grpc_client.rs @@ -10,6 +10,10 @@ use common::proto::multieko2::table_definition::{ table_definition_client::TableDefinitionClient, ProfileTreeResponse, PostTableDefinitionRequest, TableDefinitionResponse, }; +use common::proto::multieko2::table_script::{ + table_script_client::TableScriptClient, + PostTableScriptRequest, TableScriptResponse, +}; use anyhow::Result; #[derive(Clone)] @@ -17,6 +21,7 @@ pub struct GrpcClient { adresar_client: AdresarClient, table_structure_client: TableStructureServiceClient, table_definition_client: TableDefinitionClient, + table_script_client: TableScriptClient, } impl GrpcClient { @@ -24,11 +29,13 @@ impl GrpcClient { let adresar_client = AdresarClient::connect("http://[::1]:50051").await?; let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?; let table_definition_client = TableDefinitionClient::connect("http://[::1]:50051").await?; + let table_script_client = TableScriptClient::connect("http://[::1]:50051").await?; Ok(Self { adresar_client, table_structure_client, table_definition_client, + table_script_client, }) } @@ -73,4 +80,11 @@ impl GrpcClient { let response = self.table_definition_client.post_table_definition(tonic_request).await?; Ok(response.into_inner()) } + + pub async fn post_table_script(&mut self, request: PostTableScriptRequest) -> Result { + let tonic_request = tonic::Request::new(request); + let response = self.table_script_client.post_table_script(tonic_request).await?; + Ok(response.into_inner()) + } } + diff --git a/client/src/state/app/buffer.rs b/client/src/state/app/buffer.rs index 83d24df..07d3134 100644 --- a/client/src/state/app/buffer.rs +++ b/client/src/state/app/buffer.rs @@ -8,6 +8,7 @@ pub enum AppView { Register, Admin, AddTable, + AddLogic, Form(String), Scratch, } @@ -21,6 +22,7 @@ impl AppView { AppView::Register => "Register", AppView::Admin => "Admin_Panel", AppView::AddTable => "Add_Table", + AppView::AddLogic => "Add_Logic", AppView::Form(name) => name.as_str(), AppView::Scratch => "*scratch*", } diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index d1d40c4..95cafcc 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -22,6 +22,7 @@ pub struct UiState { pub show_intro: bool, pub show_admin: bool, pub show_add_table: bool, + pub show_add_logic: bool, pub show_form: bool, pub show_login: bool, pub show_register: bool, @@ -170,6 +171,7 @@ impl Default for UiState { show_intro: true, show_admin: false, show_add_table: false, + show_add_logic: false, show_form: false, show_login: false, show_register: false, diff --git a/client/src/state/pages.rs b/client/src/state/pages.rs index c22fb10..9883701 100644 --- a/client/src/state/pages.rs +++ b/client/src/state/pages.rs @@ -5,4 +5,5 @@ pub mod auth; pub mod admin; pub mod intro; pub mod add_table; +pub mod add_logic; pub mod canvas_state; diff --git a/client/src/state/pages/add_logic.rs b/client/src/state/pages/add_logic.rs new file mode 100644 index 0000000..2d2d88c --- /dev/null +++ b/client/src/state/pages/add_logic.rs @@ -0,0 +1,145 @@ +// src/state/pages/add_logic.rs +use crate::state::pages::canvas_state::CanvasState; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum AddLogicFocus { + #[default] + InputLogicName, + InputTargetColumn, + InputScriptContent, + InputDescription, + SaveButton, + CancelButton, +} + +#[derive(Debug, Clone)] +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 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 { + 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(), + 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, + } + } +} + +impl AddLogicState { + // Number of canvas-editable fields + pub const INPUT_FIELD_COUNT: usize = 3; // Logic Name, Target Column, Description +} + +impl CanvasState for AddLogicState { + fn current_field(&self) -> usize { + match self.current_focus { + AddLogicFocus::InputLogicName => 0, + AddLogicFocus::InputTargetColumn => 1, + AddLogicFocus::InputDescription => 2, + _ => 0, // Default or non-input focus + } + } + + fn current_cursor_pos(&self) -> usize { + match self.current_focus { + AddLogicFocus::InputLogicName => self.logic_name_cursor_pos, + AddLogicFocus::InputTargetColumn => self.target_column_cursor_pos, + AddLogicFocus::InputDescription => self.description_cursor_pos, + _ => 0, + } + } + + fn has_unsaved_changes(&self) -> bool { + self.has_unsaved_changes + } + + fn inputs(&self) -> Vec<&String> { + vec![ + &self.logic_name_input, + &self.target_column_input, + &self.description_input, + ] + } + + fn get_current_input(&self) -> &str { + match self.current_focus { + AddLogicFocus::InputLogicName => &self.logic_name_input, + AddLogicFocus::InputTargetColumn => &self.target_column_input, + AddLogicFocus::InputDescription => &self.description_input, + _ => "", + } + } + + fn get_current_input_mut(&mut self) -> &mut String { + match self.current_focus { + 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 + } + } + + fn fields(&self) -> Vec<&str> { + vec!["Logic Name", "Target Column", "Description"] + } + + fn set_current_field(&mut self, index: usize) { + self.current_focus = match index { + 0 => AddLogicFocus::InputLogicName, + 1 => AddLogicFocus::InputTargetColumn, + 2 => AddLogicFocus::InputDescription, + _ => self.current_focus, // Stay if out of bounds + }; + } + + fn set_current_cursor_pos(&mut self, pos: usize) { + match self.current_focus { + AddLogicFocus::InputLogicName => { + 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()); + } + AddLogicFocus::InputDescription => { + self.description_cursor_pos = pos.min(self.description_input.len()); + } + _ => {} + } + } + + fn set_has_unsaved_changes(&mut self, changed: bool) { + self.has_unsaved_changes = changed; + } + + fn get_suggestions(&self) -> Option<&[String]> { + None + } + + fn get_selected_suggestion_index(&self) -> Option { + None + } +} diff --git a/client/src/state/pages/admin.rs b/client/src/state/pages/admin.rs index 6f16b16..b8d6888 100644 --- a/client/src/state/pages/admin.rs +++ b/client/src/state/pages/admin.rs @@ -2,6 +2,7 @@ use ratatui::widgets::ListState; use crate::state::pages::add_table::AddTableState; +use crate::state::pages::add_logic::AddLogicState; // Define the focus states for the admin panel panes #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -24,6 +25,7 @@ pub struct AdminState { pub selected_table_index: Option, // Index with [*] in tables (persistent) pub current_focus: AdminFocus, // Tracks which pane is focused pub add_table_state: AddTableState, + pub add_logic_state: AddLogicState, } impl AdminState { diff --git a/client/src/ui/handlers/context.rs b/client/src/ui/handlers/context.rs index cfbc303..8a755b1 100644 --- a/client/src/ui/handlers/context.rs +++ b/client/src/ui/handlers/context.rs @@ -15,8 +15,9 @@ pub enum DialogPurpose { LoginFailed, RegisterSuccess, RegisterFailed, - ConfirmDeleteColumns, // add_table delete selected Columns - SaveTableSuccess, // add_table save table + ConfirmDeleteColumns, + SaveTableSuccess, + SaveLogicSuccess, // TODO in the future: // ConfirmQuit, } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 8247c27..e2e0745 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -48,11 +48,14 @@ pub async fn run_ui() -> Result<()> { mpsc::channel::(1); let (save_table_result_sender, mut save_table_result_receiver) = mpsc::channel::>(1); + let (save_logic_result_sender, mut save_logic_result_receiver) = + mpsc::channel::>(1); let mut event_handler = EventHandler::new( login_result_sender.clone(), register_result_sender.clone(), save_table_result_sender.clone(), + save_logic_result_sender.clone(), ).await.context("Failed to create event handler")?; let event_reader = EventReader::new(); @@ -88,6 +91,7 @@ pub async fn run_ui() -> Result<()> { app_state.ui.show_register = false; app_state.ui.show_admin = false; app_state.ui.show_add_table = false; + app_state.ui.show_add_logic = false; app_state.ui.show_form = false; match active_view { AppView::Intro => app_state.ui.show_intro = true, @@ -111,6 +115,7 @@ pub async fn run_ui() -> Result<()> { admin_state.set_profiles(profile_names); } AppView::AddTable => app_state.ui.show_add_table = true, + AppView::AddLogic => app_state.ui.show_add_logic = true, AppView::Form(_) => app_state.ui.show_form = true, AppView::Scratch => {} // Or show a scratchpad component }