dropdown is being triggered
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user