dropdown is being triggered

This commit is contained in:
filipriec
2025-06-15 12:15:25 +02:00
parent 8ace9bc4d1
commit b49dce3334
6 changed files with 202 additions and 108 deletions

View File

@@ -70,10 +70,11 @@ prev_field = ["shift+enter"]
exit = ["esc", "ctrl+e"]
delete_char_forward = ["delete"]
delete_char_backward = ["backspace"]
move_left = ["left"]
move_left = [""]
move_right = ["right"]
suggestion_down = ["ctrl+n", "tab"]
suggestion_up = ["ctrl+p", "shift+tab"]
trigger_autocomplete = ["left"]
[keybindings.command]
exit_command_mode = ["ctrl+g", "esc"]

View File

@@ -1,36 +1,37 @@
// src/components/form/form.rs
use crate::components::common::autocomplete; // <--- ADD THIS IMPORT
use crate::components::handlers::canvas::render_canvas;
use crate::config::colors::themes::Theme;
use crate::state::app::highlight::HighlightState;
use crate::state::pages::canvas_state::CanvasState;
use crate::state::pages::form::FormState; // <--- CHANGE THIS IMPORT
use ratatui::{
widgets::{Paragraph, Block, Borders},
layout::{Layout, Constraint, Direction, Rect, Margin, Alignment},
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
style::Style,
widgets::{Block, Borders, Paragraph},
Frame,
};
use crate::config::colors::themes::Theme;
use crate::state::pages::canvas_state::CanvasState;
use crate::state::app::highlight::HighlightState;
use crate::components::handlers::canvas::render_canvas;
pub fn render_form(
f: &mut Frame,
area: Rect,
form_state_param: &impl CanvasState,
form_state: &FormState, // <--- CHANGE THIS to the concrete type
fields: &[&str],
current_field_idx: &usize,
inputs: &[&String],
table_name: &str, // This parameter receives the correct table name
table_name: &str,
theme: &Theme,
is_edit_mode: bool,
highlight_state: &HighlightState,
total_count: u64,
current_position: u64,
) {
// Use the dynamic `table_name` parameter for the title instead of a hardcoded string.
let card_title = format!(" {} ", table_name);
let adresar_card = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border))
.title(card_title) // Use the dynamic title
.title(card_title)
.style(Style::default().bg(theme.bg).fg(theme.fg));
f.render_widget(adresar_card, area);
@@ -42,10 +43,7 @@ pub fn render_form(
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Min(1),
])
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(inner_area);
let count_position_text = if total_count == 0 && current_position == 1 {
@@ -54,19 +52,22 @@ pub fn render_form(
format!("Total: {} | New Entry ({})", total_count, current_position)
} else if total_count == 0 && current_position > 1 {
format!("Total: 0 | New Entry ({})", current_position)
}
else {
format!("Total: {} | Position: {}/{}", total_count, current_position, total_count)
} else {
format!(
"Total: {} | Position: {}/{}",
total_count, current_position, total_count
)
};
let count_para = Paragraph::new(count_position_text)
.style(Style::default().fg(theme.fg))
.alignment(Alignment::Left);
f.render_widget(count_para, main_layout[0]);
render_canvas(
// Get the active field's rect from render_canvas
let active_field_rect = render_canvas(
f,
main_layout[1],
form_state_param,
form_state,
fields,
current_field_idx,
inputs,
@@ -74,4 +75,22 @@ pub fn render_form(
is_edit_mode,
highlight_state,
);
// --- NEW: RENDER AUTOCOMPLETE ---
if form_state.autocomplete_active {
if let Some(suggestions) = form_state.get_suggestions() {
if let Some(active_rect) = active_field_rect {
if !suggestions.is_empty() {
autocomplete::render_autocomplete_dropdown(
f,
active_rect,
f.area(),
theme,
suggestions,
form_state.get_selected_suggestion_index(),
);
}
}
}
}
}

View File

@@ -5,15 +5,13 @@ use crate::state::pages::{
auth::{LoginState, RegisterState},
canvas_state::CanvasState,
};
use crate::state::pages::form::FormState; // <<< ADD THIS LINE
// AddLogicState is already imported
// AddTableState is already imported
use crate::state::pages::form::FormState;
use crate::state::pages::admin::AdminState;
use crate::modes::handlers::event::EventOutcome;
use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e};
use crate::state::app::state::AppState;
use anyhow::Result;
use crossterm::event::KeyEvent; // Removed KeyCode, KeyModifiers as they were unused
use crossterm::event::KeyEvent;
use tracing::debug;
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -25,7 +23,7 @@ pub enum EditEventOutcome {
pub async fn handle_edit_event(
key: KeyEvent,
config: &Config,
form_state: &mut FormState, // Now FormState is in scope
form_state: &mut FormState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
admin_state: &mut AdminState,
@@ -35,20 +33,16 @@ pub async fn handle_edit_event(
grpc_client: &mut GrpcClient,
app_state: &AppState,
) -> Result<EditEventOutcome> {
// --- Global command mode check ---
if let Some("enter_command_mode") = config.get_action_for_key_in_mode(
&config.keybindings.global, // Assuming command mode can be entered globally
&config.keybindings.global,
key.code,
key.modifiers,
) {
// This check might be redundant if EventHandler already prevents entering Edit mode
// when command_mode is true. However, it's a safeguard.
return Ok(EditEventOutcome::Message(
"Cannot enter command mode from edit mode here.".to_string(),
));
}
// --- Common actions (save, revert) ---
if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.common,
key.code,
@@ -60,12 +54,10 @@ pub async fn handle_edit_event(
} else if app_state.ui.show_register {
auth_e::execute_common_action(action, register_state, grpc_client, current_position, total_count).await?
} else if app_state.ui.show_add_table {
// TODO: Implement common actions for AddTable if needed
format!("Action '{}' not implemented for Add Table in edit mode.", action)
} else if app_state.ui.show_add_logic {
// TODO: Implement common actions for AddLogic if needed
format!("Action '{}' not implemented for Add Logic in edit mode.", action)
} else { // Assuming Form view
} else {
let outcome = form_e::execute_common_action(action, form_state, grpc_client).await?;
match outcome {
EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg,
@@ -76,20 +68,47 @@ pub async fn handle_edit_event(
}
}
// --- Edit-specific actions ---
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers).as_deref() {
// --- Handle "enter_decider" (Enter key) ---
tracing::info!("[Handler] `handle_edit_event` received action: '{}'", action_str);
// --- MANUAL AUTOCOMPLETE TRIGGER ---
if action_str == "trigger_autocomplete" {
tracing::info!("[Handler] Action is 'trigger_autocomplete'. Checking conditions..."); // <-- ADD THIS
if app_state.ui.show_form {
tracing::info!("[Handler] In form view. Checking field..."); // <-- ADD THIS
if let Some(field_def) = form_state.fields.get(form_state.current_field) {
if field_def.is_link {
tracing::info!("[Handler] Field '{}' is a link. Activating autocomplete.", field_def.display_name); // <-- ADD THIS
form_state.autocomplete_active = true;
form_state.selected_suggestion_index = Some(0);
form_state.autocomplete_suggestions = vec![
"Hardcoded Supplier A".to_string(),
"Hardcoded Supplier B".to_string(),
"Hardcoded Company C".to_string(),
];
return Ok(EditEventOutcome::Message("Autocomplete triggered".to_string()));
} else {
tracing::error!("[Handler] Field '{}' is NOT a link.", field_def.display_name); // <-- ADD THIS
}
}
} else {
tracing::error!("[Handler] Not in form view. Cannot trigger autocomplete."); // <-- ADD THIS
}
return Ok(EditEventOutcome::Message("Not a linkable field".to_string()));
}
// --- END OF NEW LOGIC ---
if action_str == "enter_decider" {
let effective_action = if app_state.ui.show_register
&& register_state.in_suggestion_mode
&& register_state.current_field() == 4 { // Role field
&& register_state.current_field() == 4 {
"select_suggestion"
} else if app_state.ui.show_add_logic
&& admin_state.add_logic_state.in_target_column_suggestion_mode
&& admin_state.add_logic_state.current_field() == 1 { // Target Column field
&& admin_state.add_logic_state.current_field() == 1 {
"select_suggestion"
} else {
"next_field" // Default action for Enter
"next_field"
};
let msg = if app_state.ui.show_login {
@@ -100,13 +119,12 @@ pub async fn handle_edit_event(
add_logic_e::execute_edit_action(effective_action, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
} else if app_state.ui.show_register {
auth_e::execute_edit_action(effective_action, key, register_state, ideal_cursor_column).await?
} else { // Form view
} else {
form_e::execute_edit_action(effective_action, key, form_state, ideal_cursor_column).await?
};
return Ok(EditEventOutcome::Message(msg));
}
// --- Handle "exit" (Escape key) ---
if action_str == "exit" {
if app_state.ui.show_register && register_state.in_suggestion_mode {
let msg = auth_e::execute_edit_action("exit_suggestion_mode", key, register_state, ideal_cursor_column).await?;
@@ -121,11 +139,9 @@ pub async fn handle_edit_event(
}
}
// --- Autocomplete for AddLogicState Target Column ---
if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 { // Target Column field
if action_str == "suggestion_down" { // "Tab" is mapped to suggestion_down
if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 {
if action_str == "suggestion_down" {
if !admin_state.add_logic_state.in_target_column_suggestion_mode {
// Attempt to open suggestions
if let Some(profile_name) = admin_state.add_logic_state.profile_name.clone().into() {
if let Some(table_name) = admin_state.add_logic_state.selected_table_name.clone() {
debug!("Fetching table structure for autocomplete: Profile='{}', Table='{}'", profile_name, table_name);
@@ -136,7 +152,6 @@ pub async fn handle_edit_event(
admin_state.add_logic_state.update_target_column_suggestions();
if !admin_state.add_logic_state.target_column_suggestions.is_empty() {
admin_state.add_logic_state.in_target_column_suggestion_mode = true;
// update_target_column_suggestions handles initial selection
return Ok(EditEventOutcome::Message("Column suggestions shown".to_string()));
} else {
return Ok(EditEventOutcome::Message("No column suggestions for current input".to_string()));
@@ -144,7 +159,7 @@ pub async fn handle_edit_event(
}
Err(e) => {
debug!("Error fetching table structure: {}", e);
admin_state.add_logic_state.table_columns_for_suggestions.clear(); // Clear old data on error
admin_state.add_logic_state.table_columns_for_suggestions.clear();
admin_state.add_logic_state.update_target_column_suggestions();
return Ok(EditEventOutcome::Message(format!("Error fetching columns: {}", e)));
}
@@ -152,10 +167,10 @@ pub async fn handle_edit_event(
} else {
return Ok(EditEventOutcome::Message("No table selected for column suggestions".to_string()));
}
} else { // Should not happen if AddLogic is properly initialized
} else {
return Ok(EditEventOutcome::Message("Profile name missing for column suggestions".to_string()));
}
} else { // Already in suggestion mode, navigate down
} else {
let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?;
return Ok(EditEventOutcome::Message(msg));
}
@@ -165,17 +180,12 @@ pub async fn handle_edit_event(
}
}
// --- Autocomplete for RegisterState Role Field ---
if app_state.ui.show_register && register_state.current_field() == 4 { // Role field
if !register_state.in_suggestion_mode && action_str == "suggestion_down" { // Tab
if app_state.ui.show_register && register_state.current_field() == 4 {
if !register_state.in_suggestion_mode && action_str == "suggestion_down" {
register_state.update_role_suggestions();
if !register_state.role_suggestions.is_empty() {
register_state.in_suggestion_mode = true;
// update_role_suggestions should handle initial selection
return Ok(EditEventOutcome::Message("Role suggestions shown".to_string()));
} else {
// If Tab doesn't open suggestions, it might fall through to "next_field"
// or you might want specific behavior. For now, let it fall through.
}
}
if register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up") {
@@ -184,28 +194,24 @@ pub async fn handle_edit_event(
}
}
// --- Dispatch other edit actions ---
let msg = if app_state.ui.show_login {
auth_e::execute_edit_action(action_str, key, login_state, ideal_cursor_column).await?
} else if app_state.ui.show_add_table {
add_table_e::execute_edit_action(action_str, key, &mut admin_state.add_table_state, ideal_cursor_column).await?
} else if app_state.ui.show_add_logic {
// If not a suggestion action handled above for AddLogic
if !(admin_state.add_logic_state.in_target_column_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) {
add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
} else { String::new() /* Already handled */ }
} else { String::new() }
} else if app_state.ui.show_register {
if !(register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) {
auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await?
} else { String::new() /* Already handled */ }
} else { // Form view
} else { String::new() }
} else {
form_e::execute_edit_action(action_str, key, form_state, ideal_cursor_column).await?
};
return Ok(EditEventOutcome::Message(msg));
}
// --- Character insertion ---
// If character insertion happens while in suggestion mode, exit suggestion mode first.
let mut exited_suggestion_mode_for_typing = false;
if app_state.ui.show_register && register_state.in_suggestion_mode {
register_state.in_suggestion_mode = false;
@@ -228,16 +234,12 @@ pub async fn handle_edit_event(
add_logic_e::execute_edit_action("insert_char", key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
} else if app_state.ui.show_register {
auth_e::execute_edit_action("insert_char", key, register_state, ideal_cursor_column).await?
} else { // Form view
} else {
form_e::execute_edit_action("insert_char", key, form_state, ideal_cursor_column).await?
};
// After character insertion, update suggestions if applicable
if app_state.ui.show_register && register_state.current_field() == 4 {
register_state.update_role_suggestions();
// If we just exited suggestion mode by typing, don't immediately show them again unless Tab is pressed.
// However, update_role_suggestions will set show_role_suggestions if matches are found.
// This is fine, as the render logic checks in_suggestion_mode.
}
if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 {
admin_state.add_logic_state.update_target_column_suggestions();
@@ -247,6 +249,5 @@ pub async fn handle_edit_event(
char_insert_msg = "Suggestions hidden".to_string();
}
Ok(EditEventOutcome::Message(char_insert_msg))
}

View File

@@ -1,30 +1,44 @@
// src/state/pages/form.rs
use std::collections::HashMap;
use crate::config::colors::themes::Theme;
use ratatui::layout::Rect;
use ratatui::Frame;
use crate::state::app::highlight::HighlightState;
use crate::state::pages::canvas_state::CanvasState;
use ratatui::layout::Rect;
use ratatui::Frame;
use std::collections::HashMap;
// A struct to bridge the display name (label) to the data key from the server.
#[derive(Debug, Clone)]
pub struct FieldDefinition {
pub display_name: String,
pub data_key: String,
pub is_link: bool, // --- NEW --- To identify FK fields
}
#[derive(Clone)]
pub struct FormState {
pub id: i64,
pub profile_name: String,
pub table_name: String,
pub total_count: u64,
pub current_position: u64,
pub fields: Vec<String>,
pub fields: Vec<FieldDefinition>,
pub values: Vec<String>,
pub current_field: usize,
pub has_unsaved_changes: bool,
pub current_cursor_pos: usize,
// --- NEW AUTOCOMPLETE STATE ---
pub autocomplete_active: bool,
pub autocomplete_suggestions: Vec<String>,
pub selected_suggestion_index: Option<usize>,
}
impl FormState {
pub fn new(
profile_name: String,
table_name: String,
fields: Vec<String>,
fields: Vec<FieldDefinition>,
) -> Self {
let values = vec![String::new(); fields.len()];
FormState {
@@ -38,10 +52,13 @@ impl FormState {
current_field: 0,
has_unsaved_changes: false,
current_cursor_pos: 0,
// --- INITIALIZE NEW STATE ---
autocomplete_active: false,
autocomplete_suggestions: Vec::new(),
selected_suggestion_index: None,
}
}
// This signature is now correct and only deals with form-related state.
pub fn render(
&self,
f: &mut Frame,
@@ -51,13 +68,13 @@ impl FormState {
highlight_state: &HighlightState,
) {
let fields_str_slice: Vec<&str> =
self.fields.iter().map(|s| s.as_str()).collect();
self.fields().iter().map(|s| *s).collect();
let values_str_slice: Vec<&String> = self.values.iter().collect();
crate::components::form::form::render_form(
f,
area,
self,
self, // <--- This now correctly passes the concrete &FormState
&fields_str_slice,
&self.current_field,
&values_str_slice,
@@ -70,7 +87,6 @@ impl FormState {
);
}
// ... other methods are unchanged ...
pub fn reset_to_empty(&mut self) {
self.id = 0;
self.values.iter_mut().for_each(|v| v.clear());
@@ -82,6 +98,7 @@ impl FormState {
} else {
self.current_position = 1;
}
self.deactivate_autocomplete(); // Deactivate on reset
}
pub fn get_current_input(&self) -> &str {
@@ -102,13 +119,16 @@ impl FormState {
response_data: &HashMap<String, String>,
new_position: u64,
) {
self.values = self.fields.iter().map(|field_from_schema| {
response_data
.iter()
.find(|(key_from_data, _)| key_from_data.eq_ignore_ascii_case(field_from_schema))
.map(|(_, value)| value.clone())
.unwrap_or_default()
}).collect();
self.values = self
.fields
.iter()
.map(|field_def| {
response_data
.get(&field_def.data_key)
.cloned()
.unwrap_or_default()
})
.collect();
let id_str_opt = response_data
.iter()
@@ -119,7 +139,12 @@ impl FormState {
if let Ok(parsed_id) = id_str.parse::<i64>() {
self.id = parsed_id;
} else {
tracing::error!( "Failed to parse 'id' field '{}' for table {}.{}", id_str, self.profile_name, self.table_name);
tracing::error!(
"Failed to parse 'id' field '{}' for table {}.{}",
id_str,
self.profile_name,
self.table_name
);
self.id = 0;
}
} else {
@@ -130,6 +155,15 @@ impl FormState {
self.has_unsaved_changes = false;
self.current_field = 0;
self.current_cursor_pos = 0;
self.deactivate_autocomplete(); // Deactivate on update
}
// --- NEW HELPER METHOD ---
/// Deactivates autocomplete and clears its state.
pub fn deactivate_autocomplete(&mut self) {
self.autocomplete_active = false;
self.autocomplete_suggestions.clear();
self.selected_suggestion_index = None;
}
}
@@ -159,13 +193,18 @@ impl CanvasState for FormState {
}
fn fields(&self) -> Vec<&str> {
self.fields.iter().map(|s| s.as_str()).collect()
self.fields
.iter()
.map(|f| f.display_name.as_str())
.collect()
}
fn set_current_field(&mut self, index: usize) {
if index < self.fields.len() {
self.current_field = index;
}
// Deactivate autocomplete when changing fields
self.deactivate_autocomplete();
}
fn set_current_cursor_pos(&mut self, pos: usize) {
@@ -176,11 +215,20 @@ impl CanvasState for FormState {
self.has_unsaved_changes = changed;
}
// --- MODIFIED: Implement autocomplete trait methods ---
fn get_suggestions(&self) -> Option<&[String]> {
None
if self.autocomplete_active {
Some(&self.autocomplete_suggestions)
} else {
None
}
}
fn get_selected_suggestion_index(&self) -> Option<usize> {
None
if self.autocomplete_active {
self.selected_suggestion_index
} else {
None
}
}
}

View File

@@ -23,7 +23,7 @@ pub async fn save(
let data_map: HashMap<String, String> = form_state.fields.iter()
.zip(form_state.values.iter())
.map(|(field, value)| (field.clone(), value.clone()))
.map(|(field_def, value)| (field_def.data_key.clone(), value.clone()))
.collect();
let outcome: SaveOutcome;

View File

@@ -9,7 +9,7 @@ use crate::modes::common::commands::CommandHandler;
use crate::modes::handlers::event::{EventHandler, EventOutcome};
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
use crate::state::pages::canvas_state::CanvasState;
use crate::state::pages::form::FormState;
use crate::state::pages::form::{FormState, FieldDefinition}; // Import FieldDefinition
use crate::state::pages::auth::AuthState;
use crate::state::pages::auth::LoginState;
use crate::state::pages::auth::RegisterState;
@@ -92,13 +92,19 @@ pub async fn run_ui() -> Result<()> {
.await
.context("Failed to initialize app state and form")?;
let filtered_columns = filter_user_columns(initial_columns_from_service);
let initial_field_defs: Vec<FieldDefinition> = filter_user_columns(initial_columns_from_service)
.into_iter()
.map(|col_name| FieldDefinition {
display_name: col_name.clone(),
data_key: col_name,
is_link: false,
})
.collect();
let mut form_state = FormState::new(
initial_profile.clone(),
initial_table.clone(),
filtered_columns,
vec![], // FIX 1: Provide the missing 4th argument
initial_field_defs,
);
UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state)
@@ -334,36 +340,54 @@ pub async fn run_ui() -> Result<()> {
.await
{
Ok(structure_response) => {
let new_columns: Vec<String> = structure_response
// --- START OF MODIFIED LOGIC ---
let all_columns: Vec<String> = structure_response
.columns
.iter()
.map(|c| c.name.clone())
.collect();
// FIX 2: Look up links in the profile_tree
let new_links: Vec<String> = app_state
// 1. Process regular columns first, filtering out FKs
let mut field_definitions: Vec<FieldDefinition> =
filter_user_columns(all_columns)
.into_iter()
.filter(|col_name| !col_name.ends_with("_id")) // Exclude FKs
.map(|col_name| FieldDefinition {
display_name: col_name.clone(),
data_key: col_name,
is_link: false, // Regular fields are not links
})
.collect();
// 2. Process linked tables to create the correct labels
let linked_tables: Vec<String> = app_state
.profile_tree
.profiles
.iter()
.find(|p| p.name == *prof_name)
.and_then(|profile| {
profile
.tables
.iter()
.find(|t| t.name == *tbl_name)
profile.tables.iter().find(|t| t.name == *tbl_name)
})
.map_or(vec![], |table| {
table.depends_on.clone()
.map_or(vec![], |table| table.depends_on.clone());
for linked_table_name in linked_tables {
let base_name = linked_table_name
.split_once('_')
.map_or(linked_table_name.as_str(), |(_, rest)| rest);
let data_key = format!("{}_id", base_name);
let display_name = linked_table_name;
field_definitions.push(FieldDefinition {
display_name,
data_key,
is_link: true, // These fields ARE links
});
let filtered_columns =
filter_user_columns(new_columns);
}
// --- END OF MODIFIED LOGIC ---
form_state = FormState::new(
prof_name.clone(),
tbl_name.clone(),
filtered_columns,
new_links,
field_definitions,
);
if let Err(e) = UiService::fetch_and_set_table_count(
@@ -401,6 +425,7 @@ pub async fn run_ui() -> Result<()> {
prev_view_table_name = current_view_table;
table_just_switched = true;
}
Err(e) => {
app_state.update_dialog_content(
&format!("Error fetching table structure: {}", e),