443 lines
16 KiB
Rust
443 lines
16 KiB
Rust
// src/modes/canvas/edit.rs
|
|
use crate::config::binds::config::Config;
|
|
use crate::functions::modes::edit::{
|
|
add_logic_e, add_table_e, form_e,
|
|
};
|
|
use crate::modes::handlers::event::EventHandler;
|
|
use crate::services::grpc_client::GrpcClient;
|
|
use crate::state::app::state::AppState;
|
|
use crate::state::pages::admin::AdminState;
|
|
use crate::state::pages::{
|
|
auth::{LoginState, RegisterState},
|
|
form::FormState,
|
|
};
|
|
use canvas::canvas::CanvasState;
|
|
use canvas::{canvas::CanvasAction, dispatcher::ActionDispatcher, canvas::ActionResult};
|
|
use anyhow::Result;
|
|
use common::proto::komp_ac::search::search_response::Hit;
|
|
use crossterm::event::{KeyCode, KeyEvent};
|
|
use tokio::sync::mpsc;
|
|
use tracing::info;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum EditEventOutcome {
|
|
Message(String),
|
|
ExitEditMode,
|
|
}
|
|
|
|
/// Helper function to spawn a non-blocking search task for autocomplete.
|
|
async fn trigger_form_autocomplete_search(
|
|
form_state: &mut FormState,
|
|
grpc_client: &mut GrpcClient,
|
|
sender: mpsc::UnboundedSender<Vec<Hit>>,
|
|
) {
|
|
if let Some(field_def) = form_state.fields.get(form_state.current_field) {
|
|
if field_def.is_link {
|
|
if let Some(target_table) = &field_def.link_target_table {
|
|
// 1. Update state for immediate UI feedback
|
|
form_state.autocomplete_loading = true;
|
|
form_state.autocomplete_active = true;
|
|
form_state.autocomplete_suggestions.clear();
|
|
form_state.selected_suggestion_index = None;
|
|
|
|
// 2. Clone everything needed for the background task
|
|
let query = form_state.get_current_input().to_string();
|
|
let table_to_search = target_table.clone();
|
|
let mut grpc_client_clone = grpc_client.clone();
|
|
|
|
info!(
|
|
"[Autocomplete] Spawning search in '{}' for query: '{}'",
|
|
table_to_search, query
|
|
);
|
|
|
|
// 3. Spawn the non-blocking task
|
|
tokio::spawn(async move {
|
|
match grpc_client_clone
|
|
.search_table(table_to_search, query)
|
|
.await
|
|
{
|
|
Ok(response) => {
|
|
// Send results back through the channel
|
|
let _ = sender.send(response.hits);
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(
|
|
"[Autocomplete] Search failed: {:?}",
|
|
e
|
|
);
|
|
// Send an empty vec on error so the UI can stop loading
|
|
let _ = sender.send(vec![]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn handle_form_edit_with_canvas(
|
|
key_event: KeyEvent,
|
|
config: &Config,
|
|
form_state: &mut FormState,
|
|
ideal_cursor_column: &mut usize,
|
|
) -> Result<String> {
|
|
// Try canvas action from key first
|
|
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
|
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
|
Ok(ActionResult::Success(msg)) => {
|
|
return Ok(msg.unwrap_or_default());
|
|
}
|
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
|
return Ok(msg);
|
|
}
|
|
Ok(ActionResult::Error(msg)) => {
|
|
return Ok(format!("Error: {}", msg));
|
|
}
|
|
Ok(ActionResult::RequiresContext(msg)) => {
|
|
return Ok(format!("Context needed: {}", msg));
|
|
}
|
|
Err(_) => {
|
|
// Fall through to try config mapping
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try config-mapped action
|
|
if let Some(action_str) = config.get_edit_action_for_key(key_event.code, key_event.modifiers) {
|
|
let canvas_action = CanvasAction::from_string(&action_str);
|
|
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
|
Ok(ActionResult::Success(msg)) => {
|
|
return Ok(msg.unwrap_or_default());
|
|
}
|
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
|
return Ok(msg);
|
|
}
|
|
Ok(ActionResult::Error(msg)) => {
|
|
return Ok(format!("Error: {}", msg));
|
|
}
|
|
Ok(ActionResult::RequiresContext(msg)) => {
|
|
return Ok(format!("Context needed: {}", msg));
|
|
}
|
|
Err(e) => {
|
|
return Ok(format!("Action failed: {}", e));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(String::new())
|
|
}
|
|
|
|
/// NEW: Unified canvas action handler for any CanvasState (LoginState, RegisterState, etc.)
|
|
/// This replaces the old auth_e::execute_edit_action calls with the new canvas library
|
|
async fn handle_canvas_state_edit<S: CanvasState>(
|
|
key: KeyEvent,
|
|
config: &Config,
|
|
state: &mut S,
|
|
ideal_cursor_column: &mut usize,
|
|
) -> Result<String> {
|
|
// Try direct key mapping first (same pattern as FormState)
|
|
if let Some(canvas_action) = CanvasAction::from_key(key.code) {
|
|
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
|
Ok(ActionResult::Success(msg)) => {
|
|
return Ok(msg.unwrap_or_default());
|
|
}
|
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
|
return Ok(msg);
|
|
}
|
|
Ok(ActionResult::Error(msg)) => {
|
|
return Ok(format!("Error: {}", msg));
|
|
}
|
|
Ok(ActionResult::RequiresContext(msg)) => {
|
|
return Ok(format!("Context needed: {}", msg));
|
|
}
|
|
Err(_) => {
|
|
// Fall through to try config mapping
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try config-mapped action (same pattern as FormState)
|
|
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
|
let canvas_action = CanvasAction::from_string(&action_str);
|
|
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
|
Ok(ActionResult::Success(msg)) => {
|
|
return Ok(msg.unwrap_or_default());
|
|
}
|
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
|
return Ok(msg);
|
|
}
|
|
Ok(ActionResult::Error(msg)) => {
|
|
return Ok(format!("Error: {}", msg));
|
|
}
|
|
Ok(ActionResult::RequiresContext(msg)) => {
|
|
return Ok(format!("Context needed: {}", msg));
|
|
}
|
|
Err(e) => {
|
|
return Ok(format!("Action failed: {}", e));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(String::new())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn handle_edit_event(
|
|
key: KeyEvent,
|
|
config: &Config,
|
|
form_state: &mut FormState,
|
|
login_state: &mut LoginState,
|
|
register_state: &mut RegisterState,
|
|
admin_state: &mut AdminState,
|
|
current_position: &mut u64,
|
|
total_count: u64,
|
|
event_handler: &mut EventHandler,
|
|
app_state: &AppState,
|
|
) -> Result<EditEventOutcome> {
|
|
// --- AUTOCOMPLETE-SPECIFIC KEY HANDLING ---
|
|
if app_state.ui.show_form && form_state.autocomplete_active {
|
|
if let Some(action) =
|
|
config.get_edit_action_for_key(key.code, key.modifiers)
|
|
{
|
|
match action {
|
|
"suggestion_down" => {
|
|
if !form_state.autocomplete_suggestions.is_empty() {
|
|
let current =
|
|
form_state.selected_suggestion_index.unwrap_or(0);
|
|
let next = (current + 1)
|
|
% form_state.autocomplete_suggestions.len();
|
|
form_state.selected_suggestion_index = Some(next);
|
|
}
|
|
return Ok(EditEventOutcome::Message(String::new()));
|
|
}
|
|
"suggestion_up" => {
|
|
if !form_state.autocomplete_suggestions.is_empty() {
|
|
let current =
|
|
form_state.selected_suggestion_index.unwrap_or(0);
|
|
let prev = if current == 0 {
|
|
form_state.autocomplete_suggestions.len() - 1
|
|
} else {
|
|
current - 1
|
|
};
|
|
form_state.selected_suggestion_index = Some(prev);
|
|
}
|
|
return Ok(EditEventOutcome::Message(String::new()));
|
|
}
|
|
"exit" => {
|
|
form_state.deactivate_autocomplete();
|
|
return Ok(EditEventOutcome::Message(
|
|
"Autocomplete cancelled".to_string(),
|
|
));
|
|
}
|
|
"enter_decider" => {
|
|
if let Some(selected_idx) =
|
|
form_state.selected_suggestion_index
|
|
{
|
|
if let Some(selection) = form_state
|
|
.autocomplete_suggestions
|
|
.get(selected_idx)
|
|
.cloned()
|
|
{
|
|
// --- THIS IS THE CORE LOGIC CHANGE ---
|
|
|
|
// 1. Get the friendly display name for the UI
|
|
let display_name =
|
|
form_state.get_display_name_for_hit(&selection);
|
|
|
|
// 2. Store the REAL ID in the form's values
|
|
let current_input =
|
|
form_state.get_current_input_mut();
|
|
*current_input = selection.id.to_string();
|
|
|
|
// 3. Set the persistent display override in the map
|
|
form_state.link_display_map.insert(
|
|
form_state.current_field,
|
|
display_name,
|
|
);
|
|
|
|
// 4. Finalize state
|
|
form_state.deactivate_autocomplete();
|
|
form_state.set_has_unsaved_changes(true);
|
|
return Ok(EditEventOutcome::Message(
|
|
"Selection made".to_string(),
|
|
));
|
|
}
|
|
}
|
|
form_state.deactivate_autocomplete();
|
|
// Fall through to default 'enter' behavior
|
|
}
|
|
_ => {} // Let other keys fall through to the live search logic
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- LIVE AUTOCOMPLETE TRIGGER LOGIC ---
|
|
let mut trigger_search = false;
|
|
|
|
if app_state.ui.show_form {
|
|
// Manual trigger
|
|
if let Some("trigger_autocomplete") =
|
|
config.get_edit_action_for_key(key.code, key.modifiers)
|
|
{
|
|
if !form_state.autocomplete_active {
|
|
trigger_search = true;
|
|
}
|
|
}
|
|
// Live search trigger while typing
|
|
else if form_state.autocomplete_active {
|
|
if let KeyCode::Char(_) | KeyCode::Backspace = key.code {
|
|
let action = if let KeyCode::Backspace = key.code {
|
|
"delete_char_backward"
|
|
} else {
|
|
"insert_char"
|
|
};
|
|
// FIX: Pass &mut event_handler.ideal_cursor_column
|
|
form_e::execute_edit_action(
|
|
action,
|
|
key,
|
|
form_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?;
|
|
trigger_search = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if trigger_search {
|
|
trigger_form_autocomplete_search(
|
|
form_state,
|
|
&mut event_handler.grpc_client,
|
|
event_handler.autocomplete_result_sender.clone(),
|
|
)
|
|
.await;
|
|
return Ok(EditEventOutcome::Message("Searching...".to_string()));
|
|
}
|
|
|
|
// --- GENERAL EDIT MODE EVENT HANDLING (IF NOT AUTOCOMPLETE) ---
|
|
|
|
if let Some(action_str) =
|
|
config.get_edit_action_for_key(key.code, key.modifiers)
|
|
{
|
|
// Handle Enter key (next field)
|
|
if action_str == "enter_decider" {
|
|
// FIX: Pass &mut event_handler.ideal_cursor_column
|
|
let msg = form_e::execute_edit_action(
|
|
"next_field",
|
|
key,
|
|
form_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?;
|
|
return Ok(EditEventOutcome::Message(msg));
|
|
}
|
|
|
|
// Handle exiting edit mode
|
|
if action_str == "exit" {
|
|
return Ok(EditEventOutcome::ExitEditMode);
|
|
}
|
|
|
|
// Handle all other edit actions - NOW USING CANVAS LIBRARY
|
|
let msg = if app_state.ui.show_login {
|
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
|
handle_canvas_state_edit(
|
|
key,
|
|
config,
|
|
login_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
} else if app_state.ui.show_add_table {
|
|
// FIX: Pass &mut event_handler.ideal_cursor_column
|
|
add_table_e::execute_edit_action(
|
|
action_str,
|
|
key,
|
|
&mut admin_state.add_table_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
} else if app_state.ui.show_add_logic {
|
|
// FIX: Pass &mut event_handler.ideal_cursor_column
|
|
add_logic_e::execute_edit_action(
|
|
action_str,
|
|
key,
|
|
&mut admin_state.add_logic_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
} else if app_state.ui.show_register {
|
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
|
handle_canvas_state_edit(
|
|
key,
|
|
config,
|
|
register_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
} else {
|
|
// FIX: Pass &mut event_handler.ideal_cursor_column
|
|
form_e::execute_edit_action(
|
|
action_str,
|
|
key,
|
|
form_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
};
|
|
return Ok(EditEventOutcome::Message(msg));
|
|
}
|
|
|
|
// --- FALLBACK FOR CHARACTER INSERTION (IF NO OTHER BINDING MATCHED) ---
|
|
if let KeyCode::Char(_) = key.code {
|
|
let msg = if app_state.ui.show_login {
|
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
|
handle_canvas_state_edit(
|
|
key,
|
|
config,
|
|
login_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
} else if app_state.ui.show_add_table {
|
|
// FIX: Pass &mut event_handler.ideal_cursor_column
|
|
add_table_e::execute_edit_action(
|
|
"insert_char",
|
|
key,
|
|
&mut admin_state.add_table_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
} else if app_state.ui.show_add_logic {
|
|
// FIX: Pass &mut event_handler.ideal_cursor_column
|
|
add_logic_e::execute_edit_action(
|
|
"insert_char",
|
|
key,
|
|
&mut admin_state.add_logic_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
} else if app_state.ui.show_register {
|
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
|
handle_canvas_state_edit(
|
|
key,
|
|
config,
|
|
register_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
} else {
|
|
// FIX: Pass &mut event_handler.ideal_cursor_column
|
|
form_e::execute_edit_action(
|
|
"insert_char",
|
|
key,
|
|
form_state,
|
|
&mut event_handler.ideal_cursor_column,
|
|
)
|
|
.await?
|
|
};
|
|
return Ok(EditEventOutcome::Message(msg));
|
|
}
|
|
|
|
Ok(EditEventOutcome::Message(String::new())) // No action taken
|
|
}
|