fixed form state removed, but not won, aint working yet

This commit is contained in:
Priec
2025-08-22 00:27:23 +02:00
parent 3dd6808ea2
commit 4ed8e7b421
7 changed files with 275 additions and 252 deletions

View File

@@ -50,7 +50,7 @@ move_right = ["l", "Right"]
move_down = ["j", "Down"]
# Optional
move_line_end = ["$"]
# move_word_next = ["w"]
move_word_next = ["w"]
next_field = ["Tab"]
move_word_prev = ["b"]
move_word_end = ["e"]

View File

@@ -18,7 +18,6 @@ pub async fn handle_command_event(
app_state: &mut AppState,
login_state: &LoginState,
register_state: &RegisterState,
form_state: &mut FormState,
command_input: &mut String,
command_message: &mut String,
grpc_client: &mut GrpcClient,
@@ -38,7 +37,6 @@ pub async fn handle_command_event(
if config.is_command_execute(key.code, key.modifiers) {
return process_command(
config,
form_state,
app_state,
login_state,
register_state,
@@ -73,7 +71,6 @@ pub async fn handle_command_event(
async fn process_command(
config: &Config,
form_state: &mut FormState,
app_state: &mut AppState,
login_state: &LoginState,
register_state: &RegisterState,
@@ -103,7 +100,6 @@ async fn process_command(
action,
terminal,
app_state,
form_state,
login_state,
register_state,
)
@@ -118,7 +114,6 @@ async fn process_command(
"save" => {
let outcome = save(
app_state,
form_state,
grpc_client,
).await?;
let message = match outcome {
@@ -131,7 +126,7 @@ async fn process_command(
},
"revert" => {
let message = revert(
form_state,
app_state,
grpc_client,
).await?;
command_input.clear();

View File

@@ -15,13 +15,12 @@ impl CommandHandler {
&mut self,
action: &str,
terminal: &mut TerminalCore,
app_state: &AppState,
form_state: &FormState,
app_state: &mut AppState,
login_state: &LoginState,
register_state: &RegisterState,
) -> Result<(bool, String)> {
match action {
"quit" => self.handle_quit(terminal, app_state, form_state, login_state, register_state).await,
"quit" => self.handle_quit(terminal, app_state, login_state, register_state).await,
"force_quit" => self.handle_force_quit(terminal).await,
"save_and_quit" => self.handle_save_quit(terminal).await,
_ => Ok((false, format!("Unknown command: {}", action))),
@@ -31,8 +30,7 @@ impl CommandHandler {
async fn handle_quit(
&self,
terminal: &mut TerminalCore,
app_state: &AppState,
form_state: &FormState,
app_state: &mut AppState,
login_state: &LoginState,
register_state: &RegisterState,
) -> Result<(bool, String)> {
@@ -41,8 +39,10 @@ impl CommandHandler {
login_state.has_unsaved_changes()
} else if app_state.ui.show_register {
register_state.has_unsaved_changes()
} else if let Some(fs) = app_state.form_state_mut() {
fs.has_unsaved_changes
} else {
form_state.has_unsaved_changes
false
};
if !has_unsaved {

View File

@@ -17,7 +17,6 @@ use anyhow::Result;
pub async fn handle_navigation_event(
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
app_state: &mut AppState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
@@ -52,11 +51,15 @@ pub async fn handle_navigation_event(
return Ok(EventOutcome::Ok(String::new()));
}
"next_field" => {
next_field(form_state);
if let Some(fs) = app_state.form_state_mut() {
next_field(fs);
}
return Ok(EventOutcome::Ok(String::new()));
}
"prev_field" => {
prev_field(form_state);
if let Some(fs) = app_state.form_state_mut() {
prev_field(fs);
}
return Ok(EventOutcome::Ok(String::new()));
}
"enter_command_mode" => {

View File

@@ -220,66 +220,90 @@ impl EventHandler {
async fn handle_search_palette_event(
&mut self,
key_event: KeyEvent,
form_state: &mut FormState,
app_state: &mut AppState,
) -> Result<EventOutcome> {
let mut should_close = false;
let mut outcome_message = String::new();
let mut trigger_search = false;
if let Some(search_state) = app_state.search_state.as_mut() {
match key_event.code {
KeyCode::Esc => {
should_close = true;
outcome_message = "Search cancelled".to_string();
}
KeyCode::Enter => {
if let Some(selected_hit) =
search_state.results.get(search_state.selected_index)
{
if let Ok(data) = serde_json::from_str::<
std::collections::HashMap<String, String>,
>(&selected_hit.content_json)
{
let detached_pos = form_state.total_count + 2;
form_state
.update_from_response(&data, detached_pos);
}
// Step 1: Handle search_state logic in a short scope
let (maybe_data, maybe_id) = {
if let Some(search_state) = app_state.search_state.as_mut() {
match key_event.code {
KeyCode::Esc => {
should_close = true;
outcome_message =
format!("Loaded record ID {}", selected_hit.id);
outcome_message = "Search cancelled".to_string();
(None, None)
}
}
KeyCode::Up => search_state.previous_result(),
KeyCode::Down => search_state.next_result(),
KeyCode::Char(c) => {
search_state
.input
.insert(search_state.cursor_position, c);
search_state.cursor_position += 1;
trigger_search = true;
}
KeyCode::Backspace => {
if search_state.cursor_position > 0 {
search_state.cursor_position -= 1;
search_state.input.remove(search_state.cursor_position);
trigger_search = true;
KeyCode::Enter => {
if let Some(selected_hit) =
search_state.results.get(search_state.selected_index)
{
if let Ok(data) = serde_json::from_str::<
std::collections::HashMap<String, String>,
>(&selected_hit.content_json)
{
(Some(data), Some(selected_hit.id))
} else {
(None, None)
}
} else {
(None, None)
}
}
}
KeyCode::Left => {
search_state.cursor_position =
search_state.cursor_position.saturating_sub(1);
}
KeyCode::Right => {
if search_state.cursor_position < search_state.input.len()
{
KeyCode::Up => {
search_state.previous_result();
(None, None)
}
KeyCode::Down => {
search_state.next_result();
(None, None)
}
KeyCode::Char(c) => {
search_state.input.insert(search_state.cursor_position, c);
search_state.cursor_position += 1;
trigger_search = true;
(None, None)
}
KeyCode::Backspace => {
if search_state.cursor_position > 0 {
search_state.cursor_position -= 1;
search_state.input.remove(search_state.cursor_position);
trigger_search = true;
}
(None, None)
}
KeyCode::Left => {
search_state.cursor_position =
search_state.cursor_position.saturating_sub(1);
(None, None)
}
KeyCode::Right => {
if search_state.cursor_position < search_state.input.len() {
search_state.cursor_position += 1;
}
(None, None)
}
_ => (None, None),
}
_ => {}
} else {
(None, None)
}
};
if trigger_search {
// Step 2: Now safe to borrow form_state
if let (Some(data), Some(id)) = (maybe_data, maybe_id) {
if let Some(fs) = app_state.form_state_mut() {
let detached_pos = fs.total_count + 2;
fs.update_from_response(&data, detached_pos);
}
should_close = true;
outcome_message = format!("Loaded record ID {}", id);
}
// Step 3: Trigger async search if needed
if trigger_search {
if let Some(search_state) = app_state.search_state.as_mut() {
search_state.is_loading = true;
search_state.results.clear();
search_state.selected_index = 0;
@@ -289,10 +313,7 @@ impl EventHandler {
let sender = self.search_result_sender.clone();
let mut grpc_client = self.grpc_client.clone();
info!(
"--- 1. Spawning search task for query: '{}' ---",
query
);
info!("--- 1. Spawning search task for query: '{}' ---", query);
tokio::spawn(async move {
info!("--- 2. Background task started. ---");
match grpc_client.search_table(table_name, query).await {
@@ -328,7 +349,6 @@ impl EventHandler {
config: &Config,
terminal: &mut TerminalCore,
command_handler: &mut CommandHandler,
form_state: &mut FormState,
auth_state: &mut AuthState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
@@ -339,13 +359,7 @@ impl EventHandler {
) -> Result<EventOutcome> {
if app_state.ui.show_search_palette {
if let Event::Key(key_event) = event {
return self
.handle_search_palette_event(
key_event,
form_state,
app_state,
)
.await;
return self.handle_search_palette_event(key_event, app_state).await;
}
return Ok(EventOutcome::Ok(String::new()));
}
@@ -570,7 +584,6 @@ impl EventHandler {
let nav_outcome = navigation::handle_navigation_event(
key_event,
config,
form_state,
app_state,
login_state,
register_state,
@@ -580,9 +593,7 @@ impl EventHandler {
&mut self.command_input,
&mut self.command_message,
&mut self.navigation_state,
)
.await;
).await;
match nav_outcome {
Ok(EventOutcome::ButtonSelected { context, index }) => {
let message = match context {
@@ -692,7 +703,6 @@ impl EventHandler {
return self
.handle_core_action(
action,
form_state,
auth_state,
login_state,
register_state,
@@ -739,7 +749,6 @@ impl EventHandler {
return self
.handle_core_action(
action,
form_state,
auth_state,
login_state,
register_state,
@@ -792,15 +801,18 @@ impl EventHandler {
}
if config.is_command_execute(key_code, modifiers) {
let mut current_position = form_state.current_position;
let total_count = form_state.total_count;
let (mut current_position, total_count) = if let Some(fs) = app_state.form_state() {
(fs.current_position, fs.total_count)
} else {
(1, 0)
};
let outcome = command_mode::handle_command_event(
key_event,
config,
app_state,
login_state,
register_state,
form_state,
&mut self.command_input,
&mut self.command_message,
&mut self.grpc_client,
@@ -808,9 +820,10 @@ impl EventHandler {
terminal,
&mut current_position,
total_count,
)
.await?;
form_state.current_position = current_position;
).await?;
if let Some(fs) = app_state.form_state_mut() {
fs.current_position = current_position;
}
self.command_mode = false;
self.key_sequence_tracker.reset();
let new_mode = ModeManager::derive_mode(
@@ -921,7 +934,6 @@ impl EventHandler {
async fn handle_core_action(
&mut self,
action: &str,
form_state: &mut FormState,
auth_state: &mut AuthState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
@@ -940,12 +952,15 @@ impl EventHandler {
.await?;
Ok(EventOutcome::Ok(message))
} else {
let save_outcome = crate::tui::functions::common::form::save(
app_state,
form_state,
&mut self.grpc_client,
)
.await?;
let save_outcome = if let Some(fs) = app_state.form_state_mut() {
crate::tui::functions::common::form::save(
app_state,
&mut self.grpc_client,
)
.await?
} else {
SaveOutcome::NoChange
};
let message = match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
@@ -975,10 +990,8 @@ impl EventHandler {
} else {
let save_outcome = crate::tui::functions::common::form::save(
app_state,
form_state,
&mut self.grpc_client,
)
.await?;
).await?;
match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
@@ -1003,13 +1016,17 @@ impl EventHandler {
register_state,
app_state,
)
.await
.await
} else {
crate::tui::functions::common::form::revert(
form_state,
&mut self.grpc_client,
)
.await?
if let Some(fs) = app_state.form_state_mut() {
crate::tui::functions::common::form::revert(
app_state,
&mut self.grpc_client,
)
.await?
} else {
"Nothing to revert".to_string()
}
};
Ok(EventOutcome::Ok(message))
}

View File

@@ -1,9 +1,7 @@
// src/tui/functions/common/form.rs
use crate::services::grpc_client::GrpcClient;
use crate::state::app::state::AppState; // NEW: Import AppState
use crate::state::pages::form::FormState;
use crate::utils::data_converter; // NEW: Import our translator
use crate::state::app::state::AppState;
use crate::utils::data_converter;
use anyhow::{anyhow, Context, Result};
use std::collections::HashMap;
@@ -14,143 +12,137 @@ pub enum SaveOutcome {
CreatedNew(i64),
}
// MODIFIED save function signature and logic
pub async fn save(
app_state: &AppState, // NEW: Pass in AppState
form_state: &mut FormState,
app_state: &mut AppState,
grpc_client: &mut GrpcClient,
) -> Result<SaveOutcome> {
if !form_state.has_unsaved_changes {
return Ok(SaveOutcome::NoChange);
}
// --- NEW: VALIDATION & CONVERSION STEP ---
let cache_key =
format!("{}.{}", form_state.profile_name, form_state.table_name);
let schema = match app_state.schema_cache.get(&cache_key) {
Some(s) => s,
None => {
return Err(anyhow!(
"Schema for table '{}' not found in cache. Cannot save.",
form_state.table_name
));
if let Some(fs) = app_state.form_state_mut() {
if !fs.has_unsaved_changes {
return Ok(SaveOutcome::NoChange);
}
};
let data_map: HashMap<String, String> = form_state
.fields
.iter()
.zip(form_state.values.iter())
.map(|(field_def, value)| (field_def.data_key.clone(), value.clone()))
.collect();
// Copy out what we need before dropping the mutable borrow
let profile_name = fs.profile_name.clone();
let table_name = fs.table_name.clone();
let fields = fs.fields.clone();
let values = fs.values.clone();
let id = fs.id;
let total_count = fs.total_count;
let current_position = fs.current_position;
// Use our new translator. It returns a user-friendly error on failure.
let converted_data =
match data_converter::convert_and_validate_data(&data_map, schema) {
Ok(data) => data,
Err(user_error) => return Err(anyhow!(user_error)),
let cache_key = format!("{}.{}", profile_name, table_name);
let schema = app_state
.schema_cache
.get(&cache_key)
.ok_or_else(|| {
anyhow!(
"Schema for table '{}' not found in cache. Cannot save.",
table_name
)
})?;
let data_map: HashMap<String, String> = fields
.iter()
.zip(values.iter())
.map(|(field_def, value)| (field_def.data_key.clone(), value.clone()))
.collect();
let converted_data =
data_converter::convert_and_validate_data(&data_map, schema)
.map_err(|user_error| anyhow!(user_error))?;
let is_new_entry = id == 0
|| (total_count > 0 && current_position > total_count)
|| (total_count == 0 && current_position == 1);
let outcome = if is_new_entry {
let response = grpc_client
.post_table_data(profile_name.clone(), table_name.clone(), converted_data)
.await
.context("Failed to post new table data")?;
if response.success {
if let Some(fs) = app_state.form_state_mut() {
fs.id = response.inserted_id;
fs.total_count += 1;
fs.current_position = fs.total_count;
fs.has_unsaved_changes = false;
}
SaveOutcome::CreatedNew(response.inserted_id)
} else {
return Err(anyhow!("Server failed to insert data: {}", response.message));
}
} else {
if id == 0 {
return Err(anyhow!(
"Cannot update record: ID is 0, but not classified as new entry."
));
}
let response = grpc_client
.put_table_data(profile_name.clone(), table_name.clone(), id, converted_data)
.await
.context("Failed to put (update) table data")?;
if response.success {
if let Some(fs) = app_state.form_state_mut() {
fs.has_unsaved_changes = false;
}
SaveOutcome::UpdatedExisting
} else {
return Err(anyhow!("Server failed to update data: {}", response.message));
}
};
// --- END OF NEW STEP ---
let outcome: SaveOutcome;
let is_new_entry = form_state.id == 0
|| (form_state.total_count > 0
&& form_state.current_position > form_state.total_count)
|| (form_state.total_count == 0 && form_state.current_position == 1);
if is_new_entry {
let response = grpc_client
.post_table_data(
form_state.profile_name.clone(),
form_state.table_name.clone(),
converted_data, // Use the validated & converted data
)
.await
.context("Failed to post new table data")?;
if response.success {
form_state.id = response.inserted_id;
form_state.total_count += 1;
form_state.current_position = form_state.total_count;
outcome = SaveOutcome::CreatedNew(response.inserted_id);
} else {
return Err(anyhow!(
"Server failed to insert data: {}",
response.message
));
}
Ok(outcome)
} else {
if form_state.id == 0 {
return Err(anyhow!(
"Cannot update record: ID is 0, but not classified as new entry."
));
}
let response = grpc_client
.put_table_data(
form_state.profile_name.clone(),
form_state.table_name.clone(),
form_state.id,
converted_data, // Use the validated & converted data
)
.await
.context("Failed to put (update) table data")?;
if response.success {
outcome = SaveOutcome::UpdatedExisting;
} else {
return Err(anyhow!(
"Server failed to update data: {}",
response.message
));
}
Ok(SaveOutcome::NoChange)
}
form_state.has_unsaved_changes = false;
Ok(outcome)
}
pub async fn revert(
form_state: &mut FormState, // Takes &mut FormState to update it
app_state: &mut AppState,
grpc_client: &mut GrpcClient,
) -> Result<String> {
if form_state.id == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) || (form_state.total_count == 0 && form_state.current_position == 1) {
let old_total_count = form_state.total_count; // Preserve for correct new position
form_state.reset_to_empty(); // reset_to_empty will clear values and set id=0
form_state.total_count = old_total_count; // Restore total_count
if form_state.total_count > 0 { // Correctly set current_position for new
form_state.current_position = form_state.total_count + 1;
} else {
form_state.current_position = 1;
if let Some(fs) = app_state.form_state_mut() {
if fs.id == 0
|| (fs.total_count > 0 && fs.current_position > fs.total_count)
|| (fs.total_count == 0 && fs.current_position == 1)
{
let old_total_count = fs.total_count;
fs.reset_to_empty();
fs.total_count = old_total_count;
if fs.total_count > 0 {
fs.current_position = fs.total_count + 1;
} else {
fs.current_position = 1;
}
return Ok("New entry cleared".to_string());
}
return Ok("New entry cleared".to_string());
}
if form_state.current_position == 0 || form_state.current_position > form_state.total_count {
if form_state.total_count > 0 {
form_state.current_position = 1;
} else {
// No records to revert to, effectively a new entry state.
form_state.reset_to_empty();
return Ok("No saved data to revert to; form cleared.".to_string());
if fs.current_position == 0 || fs.current_position > fs.total_count {
if fs.total_count > 0 {
fs.current_position = 1;
} else {
fs.reset_to_empty();
return Ok("No saved data to revert to; form cleared.".to_string());
}
}
let response = grpc_client
.get_table_data_by_position(
fs.profile_name.clone(),
fs.table_name.clone(),
fs.current_position as i32,
)
.await
.context(format!(
"Failed to get table data by position {} for table {}.{}",
fs.current_position, fs.profile_name, fs.table_name
))?;
fs.update_from_response(&response.data, fs.current_position);
Ok("Changes discarded, reloaded last saved version".to_string())
} else {
Ok("Nothing to revert".to_string())
}
let response = grpc_client
.get_table_data_by_position(
form_state.profile_name.clone(),
form_state.table_name.clone(),
form_state.current_position as i32,
)
.await
.context(format!(
"Failed to get table data by position {} for table {}.{}",
form_state.current_position,
form_state.profile_name,
form_state.table_name
))?;
// FIX: Pass the current position as the second argument
form_state.update_from_response(&response.data, form_state.current_position);
Ok("Changes discarded, reloaded last saved version".to_string())
}

View File

@@ -26,6 +26,7 @@ use crate::ui::handlers::context::DialogPurpose;
use crate::tui::functions::common::login;
use crate::tui::functions::common::register;
use crate::utils::columns::filter_user_columns;
use canvas::keymap::KeyEventOutcome;
use anyhow::{Context, Result};
use crossterm::cursor::SetCursorStyle;
use crossterm::event as crossterm_event;
@@ -193,33 +194,48 @@ pub async fn run_ui() -> Result<()> {
if crossterm_event::poll(std::time::Duration::from_millis(1))? {
let event = event_reader.read_event().context("Failed to read terminal event")?;
event_processed = true;
let event_outcome_result = {
// We need to avoid borrowing app_state twice, so we'll need to modify the handle_event call
// For now, let's create a temporary approach
let mut temp_form_state = app_state.form_state_mut().unwrap().clone();
let result = event_handler.handle_event(
event,
&config,
&mut terminal,
&mut command_handler,
&mut temp_form_state,
&mut auth_state,
&mut login_state,
&mut register_state,
&mut intro_state,
&mut admin_state,
&mut buffer_state,
&mut app_state,
).await;
// Update the app_state with any changes from temp_form_state
if let Some(form_state) = app_state.form_state_mut() {
*form_state = temp_form_state;
if let crossterm_event::Event::Key(key_event) = &event {
if app_state.ui.show_form {
if let Some(editor) = app_state.form_editor.as_mut() {
match editor.handle_key_event(*key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
event_handler.command_message = msg;
needs_redraw = true;
continue;
}
KeyEventOutcome::Consumed(None) => {
needs_redraw = true;
continue;
}
KeyEventOutcome::Pending => {
needs_redraw = true;
continue;
}
KeyEventOutcome::NotMatched => {
// fall through to client-level handling
}
}
}
}
}
result
};
// Get form state from app_state and pass to handle_event
let form_state = app_state.form_state_mut().unwrap();
let event_outcome_result = event_handler.handle_event(
event,
&config,
&mut terminal,
&mut command_handler,
&mut auth_state,
&mut login_state,
&mut register_state,
&mut intro_state,
&mut admin_state,
&mut buffer_state,
&mut app_state,
).await;
let mut should_exit = false;
match event_outcome_result {
Ok(outcome) => match outcome {