dropdown is being triggered
This commit is contained in:
@@ -70,10 +70,11 @@ prev_field = ["shift+enter"]
|
|||||||
exit = ["esc", "ctrl+e"]
|
exit = ["esc", "ctrl+e"]
|
||||||
delete_char_forward = ["delete"]
|
delete_char_forward = ["delete"]
|
||||||
delete_char_backward = ["backspace"]
|
delete_char_backward = ["backspace"]
|
||||||
move_left = ["left"]
|
move_left = [""]
|
||||||
move_right = ["right"]
|
move_right = ["right"]
|
||||||
suggestion_down = ["ctrl+n", "tab"]
|
suggestion_down = ["ctrl+n", "tab"]
|
||||||
suggestion_up = ["ctrl+p", "shift+tab"]
|
suggestion_up = ["ctrl+p", "shift+tab"]
|
||||||
|
trigger_autocomplete = ["left"]
|
||||||
|
|
||||||
[keybindings.command]
|
[keybindings.command]
|
||||||
exit_command_mode = ["ctrl+g", "esc"]
|
exit_command_mode = ["ctrl+g", "esc"]
|
||||||
|
|||||||
@@ -1,36 +1,37 @@
|
|||||||
// src/components/form/form.rs
|
// 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::{
|
use ratatui::{
|
||||||
widgets::{Paragraph, Block, Borders},
|
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||||
layout::{Layout, Constraint, Direction, Rect, Margin, Alignment},
|
|
||||||
style::Style,
|
style::Style,
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
Frame,
|
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(
|
pub fn render_form(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
form_state_param: &impl CanvasState,
|
form_state: &FormState, // <--- CHANGE THIS to the concrete type
|
||||||
fields: &[&str],
|
fields: &[&str],
|
||||||
current_field_idx: &usize,
|
current_field_idx: &usize,
|
||||||
inputs: &[&String],
|
inputs: &[&String],
|
||||||
table_name: &str, // This parameter receives the correct table name
|
table_name: &str,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
is_edit_mode: bool,
|
is_edit_mode: bool,
|
||||||
highlight_state: &HighlightState,
|
highlight_state: &HighlightState,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
current_position: 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 card_title = format!(" {} ", table_name);
|
||||||
|
|
||||||
let adresar_card = Block::default()
|
let adresar_card = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(theme.border))
|
.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));
|
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
||||||
|
|
||||||
f.render_widget(adresar_card, area);
|
f.render_widget(adresar_card, area);
|
||||||
@@ -42,10 +43,7 @@ pub fn render_form(
|
|||||||
|
|
||||||
let main_layout = Layout::default()
|
let main_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([Constraint::Length(1), Constraint::Min(1)])
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(1),
|
|
||||||
])
|
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
let count_position_text = if total_count == 0 && current_position == 1 {
|
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)
|
format!("Total: {} | New Entry ({})", total_count, current_position)
|
||||||
} else if total_count == 0 && current_position > 1 {
|
} else if total_count == 0 && current_position > 1 {
|
||||||
format!("Total: 0 | New Entry ({})", current_position)
|
format!("Total: 0 | New Entry ({})", current_position)
|
||||||
}
|
} else {
|
||||||
else {
|
format!(
|
||||||
format!("Total: {} | Position: {}/{}", total_count, current_position, total_count)
|
"Total: {} | Position: {}/{}",
|
||||||
|
total_count, current_position, total_count
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let count_para = Paragraph::new(count_position_text)
|
let count_para = Paragraph::new(count_position_text)
|
||||||
.style(Style::default().fg(theme.fg))
|
.style(Style::default().fg(theme.fg))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
f.render_widget(count_para, main_layout[0]);
|
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,
|
f,
|
||||||
main_layout[1],
|
main_layout[1],
|
||||||
form_state_param,
|
form_state,
|
||||||
fields,
|
fields,
|
||||||
current_field_idx,
|
current_field_idx,
|
||||||
inputs,
|
inputs,
|
||||||
@@ -74,4 +75,22 @@ pub fn render_form(
|
|||||||
is_edit_mode,
|
is_edit_mode,
|
||||||
highlight_state,
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ use crate::state::pages::{
|
|||||||
auth::{LoginState, RegisterState},
|
auth::{LoginState, RegisterState},
|
||||||
canvas_state::CanvasState,
|
canvas_state::CanvasState,
|
||||||
};
|
};
|
||||||
use crate::state::pages::form::FormState; // <<< ADD THIS LINE
|
use crate::state::pages::form::FormState;
|
||||||
// AddLogicState is already imported
|
|
||||||
// AddTableState is already imported
|
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::state::pages::admin::AdminState;
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e};
|
use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e};
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::KeyEvent; // Removed KeyCode, KeyModifiers as they were unused
|
use crossterm::event::KeyEvent;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -25,7 +23,7 @@ pub enum EditEventOutcome {
|
|||||||
pub async fn handle_edit_event(
|
pub async fn handle_edit_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
form_state: &mut FormState, // Now FormState is in scope
|
form_state: &mut FormState,
|
||||||
login_state: &mut LoginState,
|
login_state: &mut LoginState,
|
||||||
register_state: &mut RegisterState,
|
register_state: &mut RegisterState,
|
||||||
admin_state: &mut AdminState,
|
admin_state: &mut AdminState,
|
||||||
@@ -35,20 +33,16 @@ pub async fn handle_edit_event(
|
|||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
) -> Result<EditEventOutcome> {
|
) -> Result<EditEventOutcome> {
|
||||||
// --- Global command mode check ---
|
|
||||||
if let Some("enter_command_mode") = config.get_action_for_key_in_mode(
|
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.code,
|
||||||
key.modifiers,
|
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(
|
return Ok(EditEventOutcome::Message(
|
||||||
"Cannot enter command mode from edit mode here.".to_string(),
|
"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(
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
&config.keybindings.common,
|
&config.keybindings.common,
|
||||||
key.code,
|
key.code,
|
||||||
@@ -60,12 +54,10 @@ pub async fn handle_edit_event(
|
|||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
auth_e::execute_common_action(action, register_state, grpc_client, current_position, total_count).await?
|
auth_e::execute_common_action(action, register_state, grpc_client, current_position, total_count).await?
|
||||||
} else if app_state.ui.show_add_table {
|
} 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)
|
format!("Action '{}' not implemented for Add Table in edit mode.", action)
|
||||||
} else if app_state.ui.show_add_logic {
|
} 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)
|
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?;
|
let outcome = form_e::execute_common_action(action, form_state, grpc_client).await?;
|
||||||
match outcome {
|
match outcome {
|
||||||
EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg,
|
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() {
|
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" {
|
if action_str == "enter_decider" {
|
||||||
let effective_action = if app_state.ui.show_register
|
let effective_action = if app_state.ui.show_register
|
||||||
&& register_state.in_suggestion_mode
|
&& register_state.in_suggestion_mode
|
||||||
&& register_state.current_field() == 4 { // Role field
|
&& register_state.current_field() == 4 {
|
||||||
"select_suggestion"
|
"select_suggestion"
|
||||||
} else if app_state.ui.show_add_logic
|
} else if app_state.ui.show_add_logic
|
||||||
&& admin_state.add_logic_state.in_target_column_suggestion_mode
|
&& 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"
|
"select_suggestion"
|
||||||
} else {
|
} else {
|
||||||
"next_field" // Default action for Enter
|
"next_field"
|
||||||
};
|
};
|
||||||
|
|
||||||
let msg = if app_state.ui.show_login {
|
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?
|
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 {
|
} else if app_state.ui.show_register {
|
||||||
auth_e::execute_edit_action(effective_action, key, register_state, ideal_cursor_column).await?
|
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?
|
form_e::execute_edit_action(effective_action, key, form_state, ideal_cursor_column).await?
|
||||||
};
|
};
|
||||||
return Ok(EditEventOutcome::Message(msg));
|
return Ok(EditEventOutcome::Message(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Handle "exit" (Escape key) ---
|
|
||||||
if action_str == "exit" {
|
if action_str == "exit" {
|
||||||
if app_state.ui.show_register && register_state.in_suggestion_mode {
|
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?;
|
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 {
|
||||||
if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 { // Target Column field
|
if action_str == "suggestion_down" {
|
||||||
if action_str == "suggestion_down" { // "Tab" is mapped to suggestion_down
|
|
||||||
if !admin_state.add_logic_state.in_target_column_suggestion_mode {
|
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(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() {
|
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);
|
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();
|
admin_state.add_logic_state.update_target_column_suggestions();
|
||||||
if !admin_state.add_logic_state.target_column_suggestions.is_empty() {
|
if !admin_state.add_logic_state.target_column_suggestions.is_empty() {
|
||||||
admin_state.add_logic_state.in_target_column_suggestion_mode = true;
|
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()));
|
return Ok(EditEventOutcome::Message("Column suggestions shown".to_string()));
|
||||||
} else {
|
} else {
|
||||||
return Ok(EditEventOutcome::Message("No column suggestions for current input".to_string()));
|
return Ok(EditEventOutcome::Message("No column suggestions for current input".to_string()));
|
||||||
@@ -144,7 +159,7 @@ pub async fn handle_edit_event(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("Error fetching table structure: {}", 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();
|
admin_state.add_logic_state.update_target_column_suggestions();
|
||||||
return Ok(EditEventOutcome::Message(format!("Error fetching columns: {}", e)));
|
return Ok(EditEventOutcome::Message(format!("Error fetching columns: {}", e)));
|
||||||
}
|
}
|
||||||
@@ -152,10 +167,10 @@ pub async fn handle_edit_event(
|
|||||||
} else {
|
} else {
|
||||||
return Ok(EditEventOutcome::Message("No table selected for column suggestions".to_string()));
|
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()));
|
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?;
|
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));
|
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 {
|
||||||
if app_state.ui.show_register && register_state.current_field() == 4 { // Role field
|
if !register_state.in_suggestion_mode && action_str == "suggestion_down" {
|
||||||
if !register_state.in_suggestion_mode && action_str == "suggestion_down" { // Tab
|
|
||||||
register_state.update_role_suggestions();
|
register_state.update_role_suggestions();
|
||||||
if !register_state.role_suggestions.is_empty() {
|
if !register_state.role_suggestions.is_empty() {
|
||||||
register_state.in_suggestion_mode = true;
|
register_state.in_suggestion_mode = true;
|
||||||
// update_role_suggestions should handle initial selection
|
|
||||||
return Ok(EditEventOutcome::Message("Role suggestions shown".to_string()));
|
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") {
|
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 {
|
let msg = if app_state.ui.show_login {
|
||||||
auth_e::execute_edit_action(action_str, key, login_state, ideal_cursor_column).await?
|
auth_e::execute_edit_action(action_str, key, login_state, ideal_cursor_column).await?
|
||||||
} else if app_state.ui.show_add_table {
|
} 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?
|
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 {
|
} 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")) {
|
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?
|
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 {
|
} else if app_state.ui.show_register {
|
||||||
if !(register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) {
|
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?
|
auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await?
|
||||||
} else { String::new() /* Already handled */ }
|
} else { String::new() }
|
||||||
} else { // Form view
|
} else {
|
||||||
form_e::execute_edit_action(action_str, key, form_state, ideal_cursor_column).await?
|
form_e::execute_edit_action(action_str, key, form_state, ideal_cursor_column).await?
|
||||||
};
|
};
|
||||||
return Ok(EditEventOutcome::Message(msg));
|
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;
|
let mut exited_suggestion_mode_for_typing = false;
|
||||||
if app_state.ui.show_register && register_state.in_suggestion_mode {
|
if app_state.ui.show_register && register_state.in_suggestion_mode {
|
||||||
register_state.in_suggestion_mode = false;
|
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?
|
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 {
|
} else if app_state.ui.show_register {
|
||||||
auth_e::execute_edit_action("insert_char", key, register_state, ideal_cursor_column).await?
|
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?
|
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 {
|
if app_state.ui.show_register && register_state.current_field() == 4 {
|
||||||
register_state.update_role_suggestions();
|
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 {
|
if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 {
|
||||||
admin_state.add_logic_state.update_target_column_suggestions();
|
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();
|
char_insert_msg = "Suggestions hidden".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(EditEventOutcome::Message(char_insert_msg))
|
Ok(EditEventOutcome::Message(char_insert_msg))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,44 @@
|
|||||||
// src/state/pages/form.rs
|
// src/state/pages/form.rs
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::layout::Rect;
|
|
||||||
use ratatui::Frame;
|
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
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 struct FormState {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub profile_name: String,
|
pub profile_name: String,
|
||||||
pub table_name: String,
|
pub table_name: String,
|
||||||
pub total_count: u64,
|
pub total_count: u64,
|
||||||
pub current_position: u64,
|
pub current_position: u64,
|
||||||
pub fields: Vec<String>,
|
pub fields: Vec<FieldDefinition>,
|
||||||
pub values: Vec<String>,
|
pub values: Vec<String>,
|
||||||
pub current_field: usize,
|
pub current_field: usize,
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
pub current_cursor_pos: usize,
|
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 {
|
impl FormState {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
table_name: String,
|
table_name: String,
|
||||||
fields: Vec<String>,
|
fields: Vec<FieldDefinition>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let values = vec![String::new(); fields.len()];
|
let values = vec![String::new(); fields.len()];
|
||||||
FormState {
|
FormState {
|
||||||
@@ -38,10 +52,13 @@ impl FormState {
|
|||||||
current_field: 0,
|
current_field: 0,
|
||||||
has_unsaved_changes: false,
|
has_unsaved_changes: false,
|
||||||
current_cursor_pos: 0,
|
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(
|
pub fn render(
|
||||||
&self,
|
&self,
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -51,13 +68,13 @@ impl FormState {
|
|||||||
highlight_state: &HighlightState,
|
highlight_state: &HighlightState,
|
||||||
) {
|
) {
|
||||||
let fields_str_slice: Vec<&str> =
|
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();
|
let values_str_slice: Vec<&String> = self.values.iter().collect();
|
||||||
|
|
||||||
crate::components::form::form::render_form(
|
crate::components::form::form::render_form(
|
||||||
f,
|
f,
|
||||||
area,
|
area,
|
||||||
self,
|
self, // <--- This now correctly passes the concrete &FormState
|
||||||
&fields_str_slice,
|
&fields_str_slice,
|
||||||
&self.current_field,
|
&self.current_field,
|
||||||
&values_str_slice,
|
&values_str_slice,
|
||||||
@@ -70,7 +87,6 @@ impl FormState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... other methods are unchanged ...
|
|
||||||
pub fn reset_to_empty(&mut self) {
|
pub fn reset_to_empty(&mut self) {
|
||||||
self.id = 0;
|
self.id = 0;
|
||||||
self.values.iter_mut().for_each(|v| v.clear());
|
self.values.iter_mut().for_each(|v| v.clear());
|
||||||
@@ -82,6 +98,7 @@ impl FormState {
|
|||||||
} else {
|
} else {
|
||||||
self.current_position = 1;
|
self.current_position = 1;
|
||||||
}
|
}
|
||||||
|
self.deactivate_autocomplete(); // Deactivate on reset
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_input(&self) -> &str {
|
pub fn get_current_input(&self) -> &str {
|
||||||
@@ -102,13 +119,16 @@ impl FormState {
|
|||||||
response_data: &HashMap<String, String>,
|
response_data: &HashMap<String, String>,
|
||||||
new_position: u64,
|
new_position: u64,
|
||||||
) {
|
) {
|
||||||
self.values = self.fields.iter().map(|field_from_schema| {
|
self.values = self
|
||||||
response_data
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(key_from_data, _)| key_from_data.eq_ignore_ascii_case(field_from_schema))
|
.map(|field_def| {
|
||||||
.map(|(_, value)| value.clone())
|
response_data
|
||||||
.unwrap_or_default()
|
.get(&field_def.data_key)
|
||||||
}).collect();
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let id_str_opt = response_data
|
let id_str_opt = response_data
|
||||||
.iter()
|
.iter()
|
||||||
@@ -119,7 +139,12 @@ impl FormState {
|
|||||||
if let Ok(parsed_id) = id_str.parse::<i64>() {
|
if let Ok(parsed_id) = id_str.parse::<i64>() {
|
||||||
self.id = parsed_id;
|
self.id = parsed_id;
|
||||||
} else {
|
} 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;
|
self.id = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -130,6 +155,15 @@ impl FormState {
|
|||||||
self.has_unsaved_changes = false;
|
self.has_unsaved_changes = false;
|
||||||
self.current_field = 0;
|
self.current_field = 0;
|
||||||
self.current_cursor_pos = 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> {
|
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) {
|
fn set_current_field(&mut self, index: usize) {
|
||||||
if index < self.fields.len() {
|
if index < self.fields.len() {
|
||||||
self.current_field = index;
|
self.current_field = index;
|
||||||
}
|
}
|
||||||
|
// Deactivate autocomplete when changing fields
|
||||||
|
self.deactivate_autocomplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
@@ -176,11 +215,20 @@ impl CanvasState for FormState {
|
|||||||
self.has_unsaved_changes = changed;
|
self.has_unsaved_changes = changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- MODIFIED: Implement autocomplete trait methods ---
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
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> {
|
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||||
None
|
if self.autocomplete_active {
|
||||||
|
self.selected_suggestion_index
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pub async fn save(
|
|||||||
|
|
||||||
let data_map: HashMap<String, String> = form_state.fields.iter()
|
let data_map: HashMap<String, String> = form_state.fields.iter()
|
||||||
.zip(form_state.values.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();
|
.collect();
|
||||||
|
|
||||||
let outcome: SaveOutcome;
|
let outcome: SaveOutcome;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::modes::common::commands::CommandHandler;
|
|||||||
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
||||||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
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::AuthState;
|
||||||
use crate::state::pages::auth::LoginState;
|
use crate::state::pages::auth::LoginState;
|
||||||
use crate::state::pages::auth::RegisterState;
|
use crate::state::pages::auth::RegisterState;
|
||||||
@@ -92,13 +92,19 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to initialize app state and form")?;
|
.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(
|
let mut form_state = FormState::new(
|
||||||
initial_profile.clone(),
|
initial_profile.clone(),
|
||||||
initial_table.clone(),
|
initial_table.clone(),
|
||||||
filtered_columns,
|
initial_field_defs,
|
||||||
vec![], // FIX 1: Provide the missing 4th argument
|
|
||||||
);
|
);
|
||||||
|
|
||||||
UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state)
|
UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state)
|
||||||
@@ -334,36 +340,54 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(structure_response) => {
|
Ok(structure_response) => {
|
||||||
let new_columns: Vec<String> = structure_response
|
// --- START OF MODIFIED LOGIC ---
|
||||||
|
let all_columns: Vec<String> = structure_response
|
||||||
.columns
|
.columns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.name.clone())
|
.map(|c| c.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// FIX 2: Look up links in the profile_tree
|
// 1. Process regular columns first, filtering out FKs
|
||||||
let new_links: Vec<String> = app_state
|
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
|
.profile_tree
|
||||||
.profiles
|
.profiles
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.name == *prof_name)
|
.find(|p| p.name == *prof_name)
|
||||||
.and_then(|profile| {
|
.and_then(|profile| {
|
||||||
profile
|
profile.tables.iter().find(|t| t.name == *tbl_name)
|
||||||
.tables
|
|
||||||
.iter()
|
|
||||||
.find(|t| t.name == *tbl_name)
|
|
||||||
})
|
})
|
||||||
.map_or(vec![], |table| {
|
.map_or(vec![], |table| table.depends_on.clone());
|
||||||
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 =
|
// --- END OF MODIFIED LOGIC ---
|
||||||
filter_user_columns(new_columns);
|
|
||||||
|
|
||||||
form_state = FormState::new(
|
form_state = FormState::new(
|
||||||
prof_name.clone(),
|
prof_name.clone(),
|
||||||
tbl_name.clone(),
|
tbl_name.clone(),
|
||||||
filtered_columns,
|
field_definitions,
|
||||||
new_links,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = UiService::fetch_and_set_table_count(
|
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;
|
prev_view_table_name = current_view_table;
|
||||||
table_just_switched = true;
|
table_just_switched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app_state.update_dialog_content(
|
app_state.update_dialog_content(
|
||||||
&format!("Error fetching table structure: {}", e),
|
&format!("Error fetching table structure: {}", e),
|
||||||
|
|||||||
Reference in New Issue
Block a user