From a9c452731805ce4ca92cbf1918683814c80bd0d9 Mon Sep 17 00:00:00 2001 From: filipriec Date: Mon, 16 Jun 2025 16:10:24 +0200 Subject: [PATCH] complete redesign oh how client is displaying data --- client/src/functions/modes/edit/auth_e.rs | 3 ++ client/src/functions/modes/edit/form_e.rs | 3 ++ client/src/modes/canvas/common_mode.rs | 2 + client/src/modes/common/command_mode.rs | 5 ++- client/src/services/grpc_client.rs | 26 ++++------- client/src/services/ui_service.rs | 13 +++++- client/src/state/app/state.rs | 29 +++++++----- client/src/tui/functions/common/form.rs | 55 ++++++++++++++++------- client/src/utils/data_converter.rs | 50 +++++++++++++++++++++ client/src/utils/mod.rs | 3 ++ 10 files changed, 141 insertions(+), 48 deletions(-) create mode 100644 client/src/utils/data_converter.rs diff --git a/client/src/functions/modes/edit/auth_e.rs b/client/src/functions/modes/edit/auth_e.rs index 5e4692b..68b2301 100644 --- a/client/src/functions/modes/edit/auth_e.rs +++ b/client/src/functions/modes/edit/auth_e.rs @@ -4,6 +4,7 @@ use crate::services::grpc_client::GrpcClient; use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::form::FormState; use crate::state::pages::auth::RegisterState; +use crate::state::app::state::AppState; use crate::tui::functions::common::form::{revert, save}; use crossterm::event::{KeyCode, KeyEvent}; use std::any::Any; @@ -13,6 +14,7 @@ pub async fn execute_common_action( action: &str, state: &mut S, grpc_client: &mut GrpcClient, + app_state: &AppState, current_position: &mut u64, total_count: u64, ) -> Result { @@ -27,6 +29,7 @@ pub async fn execute_common_action( match action { "save" => { let outcome = save( + app_state, form_state, grpc_client, ) diff --git a/client/src/functions/modes/edit/form_e.rs b/client/src/functions/modes/edit/form_e.rs index 2d74d69..a9cf483 100644 --- a/client/src/functions/modes/edit/form_e.rs +++ b/client/src/functions/modes/edit/form_e.rs @@ -3,6 +3,7 @@ use crate::services::grpc_client::GrpcClient; use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::form::FormState; +use crate::state::app::state::AppState; use crate::tui::functions::common::form::{revert, save}; use crate::tui::functions::common::form::SaveOutcome; use crate::modes::handlers::event::EventOutcome; @@ -14,6 +15,7 @@ pub async fn execute_common_action( action: &str, state: &mut S, grpc_client: &mut GrpcClient, + app_state: &AppState, ) -> Result { match action { "save" | "revert" => { @@ -26,6 +28,7 @@ pub async fn execute_common_action( match action { "save" => { let save_result = save( + app_state, form_state, grpc_client, ).await; diff --git a/client/src/modes/canvas/common_mode.rs b/client/src/modes/canvas/common_mode.rs index 2774a26..cbdc846 100644 --- a/client/src/modes/canvas/common_mode.rs +++ b/client/src/modes/canvas/common_mode.rs @@ -32,6 +32,7 @@ pub async fn handle_core_action( Ok(EventOutcome::Ok(message)) } else { let save_outcome = form_save( + app_state, form_state, grpc_client, ).await.context("Register save action failed")?; @@ -52,6 +53,7 @@ pub async fn handle_core_action( login_save(auth_state, login_state, auth_client, app_state).await.context("Login save n quit action failed")? } else { let save_outcome = form_save( + app_state, form_state, grpc_client, ).await?; diff --git a/client/src/modes/common/command_mode.rs b/client/src/modes/common/command_mode.rs index 891d705..2fac602 100644 --- a/client/src/modes/common/command_mode.rs +++ b/client/src/modes/common/command_mode.rs @@ -15,7 +15,7 @@ use anyhow::Result; pub async fn handle_command_event( key: KeyEvent, config: &Config, - app_state: &AppState, + app_state: &mut AppState, login_state: &LoginState, register_state: &RegisterState, form_state: &mut FormState, @@ -74,7 +74,7 @@ pub async fn handle_command_event( async fn process_command( config: &Config, form_state: &mut FormState, - app_state: &AppState, + app_state: &mut AppState, login_state: &LoginState, register_state: &RegisterState, command_input: &mut String, @@ -117,6 +117,7 @@ async fn process_command( }, "save" => { let outcome = save( + app_state, form_state, grpc_client, ).await?; diff --git a/client/src/services/grpc_client.rs b/client/src/services/grpc_client.rs index 1f793d0..c4481e4 100644 --- a/client/src/services/grpc_client.rs +++ b/client/src/services/grpc_client.rs @@ -1,7 +1,6 @@ // src/services/grpc_client.rs -use tonic::transport::Channel; -use common::proto::multieko2::common::{CountResponse, Empty}; +use common::proto::multieko2::common::Empty; use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient; use common::proto::multieko2::table_structure::{GetTableStructureRequest, TableStructureResponse}; use common::proto::multieko2::table_definition::{ @@ -25,6 +24,7 @@ use common::proto::multieko2::search::{ }; use anyhow::{Context, Result}; use std::collections::HashMap; +use tonic::transport::Channel; use prost_types::Value; #[derive(Clone)] @@ -155,18 +155,14 @@ pub async fn get_table_data_by_position( Ok(response.into_inner()) } -pub async fn post_table_data( + pub async fn post_table_data( &mut self, profile_name: String, table_name: String, - data: HashMap, + // CHANGE THIS: Accept the pre-converted data + data: HashMap, ) -> Result { - // 2. CONVERT THE HASHMAP - let data: HashMap = data - .into_iter() - .map(|(k, v)| (k, Value::from(v))) - .collect(); - + // The conversion logic is now gone from here. let grpc_request = PostTableDataRequest { profile_name, table_name, @@ -186,14 +182,10 @@ pub async fn post_table_data( profile_name: String, table_name: String, id: i64, - data: HashMap, + // CHANGE THIS: Accept the pre-converted data + data: HashMap, ) -> Result { - // 2. CONVERT THE HASHMAP - let data: HashMap = data - .into_iter() - .map(|(k, v)| (k, Value::from(v))) - .collect(); - + // The conversion logic is now gone from here. let grpc_request = PutTableDataRequest { profile_name, table_name, diff --git a/client/src/services/ui_service.rs b/client/src/services/ui_service.rs index d0a3f05..bfb9f04 100644 --- a/client/src/services/ui_service.rs +++ b/client/src/services/ui_service.rs @@ -7,6 +7,7 @@ use crate::state::pages::add_logic::AddLogicState; use crate::state::app::state::AppState; use crate::utils::columns::filter_user_columns; use anyhow::{Context, Result}; +use std::sync::Arc; pub struct UiService; @@ -102,7 +103,6 @@ impl UiService { .context("Failed to get profile tree")?; app_state.profile_tree = profile_tree; - // Determine initial table to load (e.g., first table of first profile, or a default) let initial_profile_name = app_state .profile_tree .profiles @@ -115,7 +115,7 @@ impl UiService { .profiles .first() .and_then(|p| p.tables.first().map(|t| t.name.clone())) - .unwrap_or_else(|| "2025_company_data1".to_string()); // Fallback if no tables + .unwrap_or_else(|| "2025_company_data1".to_string()); app_state.set_current_view_table( initial_profile_name.clone(), @@ -133,6 +133,15 @@ impl UiService { initial_profile_name, initial_table_name ))?; + // NEW: Populate the "Rulebook" cache + let cache_key = format!( + "{}.{}", + initial_profile_name, initial_table_name + ); + app_state + .schema_cache + .insert(cache_key, Arc::new(table_structure.clone())); + let column_names: Vec = table_structure .columns .iter() diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index 9258672..e922227 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -1,15 +1,19 @@ // src/state/app/state.rs -use std::env; -use common::proto::multieko2::table_definition::ProfileTreeResponse; -use crate::modes::handlers::mode_manager::AppMode; -use crate::ui::handlers::context::DialogPurpose; -use crate::state::app::search::SearchState; // ADDED use anyhow::Result; +use common::proto::multieko2::table_definition::ProfileTreeResponse; +// NEW: Import the types we need for the cache +use common::proto::multieko2::table_structure::TableStructureResponse; +use crate::modes::handlers::mode_manager::AppMode; +use crate::state::app::search::SearchState; +use crate::ui::handlers::context::DialogPurpose; +use std::collections::HashMap; +use std::env; +use std::sync::Arc; #[cfg(feature = "ui-debug")] use std::time::Instant; -// --- YOUR EXISTING DIALOGSTATE IS UNTOUCHED --- +// --- DialogState and UiState are unchanged --- pub struct DialogState { pub dialog_show: bool, pub dialog_title: String, @@ -30,7 +34,7 @@ pub struct UiState { pub show_form: bool, pub show_login: bool, pub show_register: bool, - pub show_search_palette: bool, // ADDED + pub show_search_palette: bool, pub focus_outside_canvas: bool, pub dialog: DialogState, } @@ -52,10 +56,12 @@ pub struct AppState { pub current_view_profile_name: Option, pub current_view_table_name: Option, + // NEW: The "Rulebook" cache. We use Arc for efficient sharing. + pub schema_cache: HashMap>, + pub focused_button_index: usize, pub pending_table_structure_fetch: Option<(String, String)>, - // ADDED: State for the search palette pub search_state: Option, // UI preferences @@ -67,9 +73,7 @@ pub struct AppState { impl AppState { pub fn new() -> Result { - let current_dir = env::current_dir()? - .to_string_lossy() - .to_string(); + let current_dir = env::current_dir()?.to_string_lossy().to_string(); Ok(AppState { current_dir, profile_tree: ProfileTreeResponse::default(), @@ -77,9 +81,10 @@ impl AppState { current_view_profile_name: None, current_view_table_name: None, current_mode: AppMode::General, + schema_cache: HashMap::new(), // NEW: Initialize the cache focused_button_index: 0, pending_table_structure_fetch: None, - search_state: None, // ADDED + search_state: None, ui: UiState::default(), #[cfg(feature = "ui-debug")] diff --git a/client/src/tui/functions/common/form.rs b/client/src/tui/functions/common/form.rs index 952bb91..86c8bec 100644 --- a/client/src/tui/functions/common/form.rs +++ b/client/src/tui/functions/common/form.rs @@ -1,19 +1,22 @@ // 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 anyhow::{Context, Result}; // Added Context -use std::collections::HashMap; // NEW +use crate::utils::data_converter; // NEW: Import our translator +use anyhow::{anyhow, Context, Result}; +use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SaveOutcome { NoChange, UpdatedExisting, - CreatedNew(i64), // Keep the ID + CreatedNew(i64), } -// MODIFIED save function +// MODIFIED save function signature and logic pub async fn save( + app_state: &AppState, // NEW: Pass in AppState form_state: &mut FormState, grpc_client: &mut GrpcClient, ) -> Result { @@ -21,42 +24,64 @@ pub async fn save( return Ok(SaveOutcome::NoChange); } - let data_map: HashMap = form_state.fields.iter() + // --- 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 + )); + } + }; + + let data_map: HashMap = form_state + .fields + .iter() .zip(form_state.values.iter()) .map(|(field_def, value)| (field_def.data_key.clone(), value.clone())) .collect(); + // 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)), + }; + // --- 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) ; - + 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(), - data_map, + converted_data, // Use the validated & converted data ) .await .context("Failed to post new table data")?; if response.success { form_state.id = response.inserted_id; - // After creating a new entry, total_count increases, and current_position becomes this new total_count form_state.total_count += 1; form_state.current_position = form_state.total_count; outcome = SaveOutcome::CreatedNew(response.inserted_id); } else { - return Err(anyhow::anyhow!( + return Err(anyhow!( "Server failed to insert data: {}", response.message )); } } else { - // This assumes form_state.id is valid for an existing record if form_state.id == 0 { - return Err(anyhow::anyhow!( + return Err(anyhow!( "Cannot update record: ID is 0, but not classified as new entry." )); } @@ -65,7 +90,7 @@ pub async fn save( form_state.profile_name.clone(), form_state.table_name.clone(), form_state.id, - data_map, + converted_data, // Use the validated & converted data ) .await .context("Failed to put (update) table data")?; @@ -73,7 +98,7 @@ pub async fn save( if response.success { outcome = SaveOutcome::UpdatedExisting; } else { - return Err(anyhow::anyhow!( + return Err(anyhow!( "Server failed to update data: {}", response.message )); diff --git a/client/src/utils/data_converter.rs b/client/src/utils/data_converter.rs new file mode 100644 index 0000000..0293b85 --- /dev/null +++ b/client/src/utils/data_converter.rs @@ -0,0 +1,50 @@ +// src/utils/data_converter.rs + +use common::proto::multieko2::table_structure::TableStructureResponse; +use prost_types::{value::Kind, NullValue, Value}; // Removed unused anyhow +use std::collections::HashMap; + +pub fn convert_and_validate_data( + data: &HashMap, + schema: &TableStructureResponse, +) -> Result, String> { + let type_map: HashMap<_, _> = schema + .columns + .iter() + .map(|col| (col.name.as_str(), col.data_type.as_str())) + .collect(); + + data.iter() + .map(|(key, str_value)| { + let expected_type = type_map.get(key.as_str()).unwrap_or(&"TEXT"); + + let kind = if str_value.is_empty() { + // CHANGE THIS: Use the correct enum variant + Kind::NullValue(NullValue::NullValue.into()) + } else { + // Attempt to parse the string based on the expected type + match *expected_type { + "BOOL" => match str_value.to_lowercase().parse::() { + Ok(v) => Kind::BoolValue(v), + Err(_) => return Err(format!("Invalid boolean for '{}': must be 'true' or 'false'", key)), + }, + "INT8" | "INT4" | "INT2" | "SERIAL" | "BIGSERIAL" => { + match str_value.parse::() { + Ok(v) => Kind::NumberValue(v), + Err(_) => return Err(format!("Invalid number for '{}': must be a whole number", key)), + } + } + "NUMERIC" | "FLOAT4" | "FLOAT8" => match str_value.parse::() { + Ok(v) => Kind::NumberValue(v), + Err(_) => return Err(format!("Invalid decimal for '{}': must be a number", key)), + }, + "TIMESTAMPTZ" | "DATE" | "TIME" | "TEXT" | "VARCHAR" | "UUID" => { + Kind::StringValue(str_value.clone()) + } + _ => Kind::StringValue(str_value.clone()), + } + }; + Ok((key.clone(), Value { kind: Some(kind) })) + }) + .collect() +} diff --git a/client/src/utils/mod.rs b/client/src/utils/mod.rs index efc8001..9b22312 100644 --- a/client/src/utils/mod.rs +++ b/client/src/utils/mod.rs @@ -2,5 +2,8 @@ pub mod columns; pub mod debug_logger; +pub mod data_converter; + pub use columns::*; pub use debug_logger::*; +pub use data_converter::*;