hardcoded adresar to general form

This commit is contained in:
filipriec
2025-06-02 10:32:39 +02:00
parent 6e2fc5349b
commit 3488ab4f6b
12 changed files with 636 additions and 375 deletions

View File

@@ -13,9 +13,9 @@ 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: &impl CanvasState, form_state_param: &impl CanvasState,
fields: &[&str], fields: &[&str],
current_field: &usize, current_field_idx: &usize,
inputs: &[&String], inputs: &[&String],
theme: &Theme, theme: &Theme,
is_edit_mode: bool, is_edit_mode: bool,
@@ -48,7 +48,16 @@ pub fn render_form(
.split(inner_area); .split(inner_area);
// Render count/position // Render count/position
let count_position_text = format!("Total: {} | Position: {}", total_count, current_position); let count_position_text = if total_count == 0 && current_position == 1 {
"Total: 0 | New Entry".to_string()
} else if current_position > total_count && total_count > 0 {
format!("Total: {} | New Entry ({})", total_count, current_position)
} else if total_count == 0 && current_position > 1 { // Should not happen if logic is correct
format!("Total: 0 | New Entry ({})", current_position)
}
else {
format!("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);
@@ -58,9 +67,9 @@ pub fn render_form(
render_canvas( render_canvas(
f, f,
main_layout[1], main_layout[1],
form_state, form_state_param,
fields, fields,
current_field, current_field_idx,
inputs, inputs,
theme, theme,
is_edit_mode, is_edit_mode,

View File

@@ -14,8 +14,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
action: &str, action: &str,
state: &mut S, state: &mut S,
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
current_position: &mut u64,
total_count: u64,
) -> Result<EventOutcome> { ) -> Result<EventOutcome> {
match action { match action {
"save" | "revert" => { "save" | "revert" => {
@@ -30,8 +28,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
let save_result = save( let save_result = save(
form_state, form_state,
grpc_client, grpc_client,
current_position,
total_count,
).await; ).await;
match save_result { match save_result {
@@ -50,8 +46,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
let revert_result = revert( let revert_result = revert(
form_state, form_state,
grpc_client, grpc_client,
current_position,
total_count,
).await; ).await;
match revert_result { match revert_result {

View File

@@ -24,8 +24,6 @@ pub async fn handle_core_action(
auth_client: &mut AuthClient, auth_client: &mut AuthClient,
terminal: &mut TerminalCore, terminal: &mut TerminalCore,
app_state: &mut AppState, app_state: &mut AppState,
current_position: &mut u64,
total_count: u64,
) -> Result<EventOutcome> { ) -> Result<EventOutcome> {
match action { match action {
"save" => { "save" => {
@@ -36,8 +34,6 @@ pub async fn handle_core_action(
let save_outcome = form_save( let save_outcome = form_save(
form_state, form_state,
grpc_client, grpc_client,
current_position,
total_count,
).await.context("Register save action failed")?; ).await.context("Register save action failed")?;
let message = match save_outcome { let message = match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(), SaveOutcome::NoChange => "No changes to save.".to_string(),
@@ -58,8 +54,6 @@ pub async fn handle_core_action(
let save_outcome = form_save( let save_outcome = form_save(
form_state, form_state,
grpc_client, grpc_client,
current_position,
total_count,
).await?; ).await?;
match save_outcome { match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(), SaveOutcome::NoChange => "No changes to save.".to_string(),
@@ -81,8 +75,6 @@ pub async fn handle_core_action(
let message = form_revert( let message = form_revert(
form_state, form_state,
grpc_client, grpc_client,
current_position,
total_count,
).await.context("Form revert x action failed")?; ).await.context("Form revert x action failed")?;
Ok(EventOutcome::Ok(message)) Ok(EventOutcome::Ok(message))
} }

View File

@@ -66,7 +66,7 @@ pub async fn handle_edit_event(
// TODO: Implement common actions for AddLogic if needed // 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 { // Assuming Form view
let outcome = form_e::execute_common_action(action, form_state, grpc_client, current_position, total_count).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,
_ => format!("Unexpected outcome from common action: {:?}", outcome), _ => format!("Unexpected outcome from common action: {:?}", outcome),

View File

@@ -129,8 +129,6 @@ impl EventHandler {
admin_state: &mut AdminState, admin_state: &mut AdminState,
buffer_state: &mut BufferState, buffer_state: &mut BufferState,
app_state: &mut AppState, app_state: &mut AppState,
total_count: u64,
current_position: &mut u64,
) -> Result<EventOutcome> { ) -> Result<EventOutcome> {
let mut current_mode = ModeManager::derive_mode(app_state, self, admin_state); let mut current_mode = ModeManager::derive_mode(app_state, self, admin_state);
@@ -484,8 +482,6 @@ impl EventHandler {
&mut self.auth_client, &mut self.auth_client,
terminal, terminal,
app_state, app_state,
current_position,
total_count,
) )
.await; .await;
} }
@@ -503,8 +499,8 @@ impl EventHandler {
&mut admin_state.add_table_state, &mut admin_state.add_table_state,
&mut admin_state.add_logic_state, &mut admin_state.add_logic_state,
&mut self.key_sequence_tracker, &mut self.key_sequence_tracker,
current_position, form_state.current_position,
total_count, form_state.total_count,
grpc_client, grpc_client,
&mut self.command_message, &mut self.command_message,
&mut self.edit_mode_cooldown, &mut self.edit_mode_cooldown,
@@ -545,8 +541,8 @@ impl EventHandler {
&mut admin_state.add_table_state, &mut admin_state.add_table_state,
&mut admin_state.add_logic_state, &mut admin_state.add_logic_state,
&mut self.key_sequence_tracker, &mut self.key_sequence_tracker,
current_position, form_state.current_position,
total_count, form_state.total_count,
grpc_client, grpc_client,
&mut self.command_message, &mut self.command_message,
&mut self.edit_mode_cooldown, &mut self.edit_mode_cooldown,
@@ -570,8 +566,6 @@ impl EventHandler {
&mut self.auth_client, &mut self.auth_client,
terminal, terminal,
app_state, app_state,
current_position,
total_count,
) )
.await; .await;
} }
@@ -587,8 +581,6 @@ impl EventHandler {
register_state, register_state,
admin_state, admin_state,
&mut self.ideal_cursor_column, &mut self.ideal_cursor_column,
current_position,
total_count,
grpc_client, grpc_client,
app_state, app_state,
) )
@@ -677,8 +669,6 @@ impl EventHandler {
grpc_client, grpc_client,
command_handler, command_handler,
terminal, terminal,
current_position,
total_count,
) )
.await?; .await?;
self.command_mode = false; self.command_mode = false;

View File

@@ -1,101 +1,200 @@
// src/services/grpc_client.rs // src/services/grpc_client.rs
use tonic::transport::Channel; use tonic::transport::Channel;
use common::proto::multieko2::adresar::adresar_client::AdresarClient; use common::proto::multieko2::common::{CountResponse, Empty};
use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, PutAdresarRequest};
use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty};
use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient; use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient;
// Import the new request type for table structure use common::proto::multieko2::table_structure::{GetTableStructureRequest, TableStructureResponse};
use common::proto::multieko2::table_structure::{TableStructureResponse, GetTableStructureRequest};
use common::proto::multieko2::table_definition::{ use common::proto::multieko2::table_definition::{
table_definition_client::TableDefinitionClient, table_definition_client::TableDefinitionClient,
ProfileTreeResponse, PostTableDefinitionRequest, TableDefinitionResponse, PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse,
}; };
use common::proto::multieko2::table_script::{ use common::proto::multieko2::table_script::{
table_script_client::TableScriptClient, table_script_client::TableScriptClient,
PostTableScriptRequest, TableScriptResponse, PostTableScriptRequest, TableScriptResponse,
}; };
use anyhow::Result; use common::proto::multieko2::tables_data::{
tables_data_client::TablesDataClient,
GetTableDataByPositionRequest,
GetTableDataResponse,
GetTableDataCountRequest,
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
PutTableDataResponse,
};
use anyhow::{Context, Result}; // Added Context
use std::collections::HashMap; // NEW
#[derive(Clone)] #[derive(Clone)]
pub struct GrpcClient { pub struct GrpcClient {
adresar_client: AdresarClient<Channel>,
table_structure_client: TableStructureServiceClient<Channel>, table_structure_client: TableStructureServiceClient<Channel>,
table_definition_client: TableDefinitionClient<Channel>, table_definition_client: TableDefinitionClient<Channel>,
table_script_client: TableScriptClient<Channel>, table_script_client: TableScriptClient<Channel>,
tables_data_client: TablesDataClient<Channel>, // NEW
} }
impl GrpcClient { impl GrpcClient {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
let adresar_client = AdresarClient::connect("http://[::1]:50051").await?; let table_structure_client = TableStructureServiceClient::connect(
let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?; "http://[::1]:50051",
let table_definition_client = TableDefinitionClient::connect("http://[::1]:50051").await?; )
let table_script_client = TableScriptClient::connect("http://[::1]:50051").await?; .await
.context("Failed to connect to TableStructureService")?;
let table_definition_client = TableDefinitionClient::connect(
"http://[::1]:50051",
)
.await
.context("Failed to connect to TableDefinitionService")?;
let table_script_client =
TableScriptClient::connect("http://[::1]:50051")
.await
.context("Failed to connect to TableScriptService")?;
let tables_data_client =
TablesDataClient::connect("http://[::1]:50051")
.await
.context("Failed to connect to TablesDataService")?; // NEW
Ok(Self { Ok(Self {
adresar_client, // adresar_client, // REMOVE
table_structure_client, table_structure_client,
table_definition_client, table_definition_client,
table_script_client, table_script_client,
tables_data_client, // NEW
}) })
} }
pub async fn get_adresar_count(&mut self) -> Result<u64> {
let request = tonic::Request::new(Empty::default());
let response: CountResponse = self.adresar_client.get_adresar_count(request).await?.into_inner();
Ok(response.count as u64)
}
pub async fn get_adresar_by_position(&mut self, position: u64) -> Result<AdresarResponse> {
let request = tonic::Request::new(PositionRequest { position: position as i64 });
let response: AdresarResponse = self.adresar_client.get_adresar_by_position(request).await?.into_inner();
Ok(response)
}
pub async fn post_adresar(&mut self, request: PostAdresarRequest) -> Result<tonic::Response<AdresarResponse>> {
let request = tonic::Request::new(request);
let response = self.adresar_client.post_adresar(request).await?;
Ok(response)
}
pub async fn put_adresar(&mut self, request: PutAdresarRequest) -> Result<tonic::Response<AdresarResponse>> {
let request = tonic::Request::new(request);
let response = self.adresar_client.put_adresar(request).await?;
Ok(response)
}
// Updated get_table_structure method
pub async fn get_table_structure( pub async fn get_table_structure(
&mut self, &mut self,
profile_name: String, profile_name: String,
table_name: String, table_name: String,
) -> Result<TableStructureResponse> { ) -> Result<TableStructureResponse> {
// Create the new request type
let grpc_request = GetTableStructureRequest { let grpc_request = GetTableStructureRequest {
profile_name, profile_name,
table_name, table_name,
}; };
let request = tonic::Request::new(grpc_request); let request = tonic::Request::new(grpc_request);
// Call the new gRPC method let response = self
let response = self.table_structure_client.get_table_structure(request).await?; .table_structure_client
.get_table_structure(request)
.await
.context("gRPC GetTableStructure call failed")?;
Ok(response.into_inner()) Ok(response.into_inner())
} }
pub async fn get_profile_tree(&mut self) -> Result<ProfileTreeResponse> { pub async fn get_profile_tree(
&mut self,
) -> Result<ProfileTreeResponse> {
let request = tonic::Request::new(Empty::default()); let request = tonic::Request::new(Empty::default());
let response = self.table_definition_client.get_profile_tree(request).await?; let response = self
.table_definition_client
.get_profile_tree(request)
.await
.context("gRPC GetProfileTree call failed")?;
Ok(response.into_inner()) Ok(response.into_inner())
} }
pub async fn post_table_definition(&mut self, request: PostTableDefinitionRequest) -> Result<TableDefinitionResponse> { pub async fn post_table_definition(
&mut self,
request: PostTableDefinitionRequest,
) -> Result<TableDefinitionResponse> {
let tonic_request = tonic::Request::new(request); let tonic_request = tonic::Request::new(request);
let response = self.table_definition_client.post_table_definition(tonic_request).await?; let response = self
.table_definition_client
.post_table_definition(tonic_request)
.await
.context("gRPC PostTableDefinition call failed")?;
Ok(response.into_inner()) Ok(response.into_inner())
} }
pub async fn post_table_script(&mut self, request: PostTableScriptRequest) -> Result<TableScriptResponse> { pub async fn post_table_script(
&mut self,
request: PostTableScriptRequest,
) -> Result<TableScriptResponse> {
let tonic_request = tonic::Request::new(request); let tonic_request = tonic::Request::new(request);
let response = self.table_script_client.post_table_script(tonic_request).await?; let response = self
.table_script_client
.post_table_script(tonic_request)
.await
.context("gRPC PostTableScript call failed")?;
Ok(response.into_inner())
}
// NEW Methods for TablesData service
pub async fn get_table_data_count(
&mut self,
profile_name: String,
table_name: String,
) -> Result<u64> {
let grpc_request = GetTableDataCountRequest {
profile_name,
table_name,
};
let request = tonic::Request::new(grpc_request);
let response = self
.tables_data_client
.get_table_data_count(request)
.await
.context("gRPC GetTableDataCount call failed")?;
Ok(response.into_inner().count as u64)
}
pub async fn get_table_data_by_position(
&mut self,
profile_name: String,
table_name: String,
position: i32,
) -> Result<GetTableDataResponse> {
let grpc_request = GetTableDataByPositionRequest {
profile_name,
table_name,
position,
};
let request = tonic::Request::new(grpc_request);
let response = self
.tables_data_client
.get_table_data_by_position(request)
.await
.context("gRPC GetTableDataByPosition call failed")?;
Ok(response.into_inner())
}
pub async fn post_table_data(
&mut self,
profile_name: String,
table_name: String,
data: HashMap<String, String>,
) -> Result<PostTableDataResponse> {
let grpc_request = PostTableDataRequest {
profile_name,
table_name,
data,
};
let request = tonic::Request::new(grpc_request);
let response = self
.tables_data_client
.post_table_data(request)
.await
.context("gRPC PostTableData call failed")?;
Ok(response.into_inner())
}
pub async fn put_table_data(
&mut self,
profile_name: String,
table_name: String,
id: i64,
data: HashMap<String, String>,
) -> Result<PutTableDataResponse> {
let grpc_request = PutTableDataRequest {
profile_name,
table_name,
id,
data,
};
let request = tonic::Request::new(grpc_request);
let response = self
.tables_data_client
.put_table_data(request)
.await
.context("gRPC PutTableData call failed")?;
Ok(response.into_inner()) Ok(response.into_inner())
} }
} }

View File

@@ -91,109 +91,172 @@ impl UiService {
} }
} }
pub async fn initialize_app_state( // MODIFIED: To set initial view table in AppState and return initial column names
pub async fn initialize_app_state_and_form(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &mut AppState, app_state: &mut AppState,
) -> Result<Vec<String>> { // Returns (initial_profile, initial_table, initial_columns)
// Fetch profile tree ) -> Result<(String, String, Vec<String>)> {
let profile_tree = grpc_client.get_profile_tree().await.context("Failed to get profile tree")?; let profile_tree = grpc_client
.get_profile_tree()
.await
.context("Failed to get profile tree")?;
app_state.profile_tree = profile_tree; app_state.profile_tree = profile_tree;
// TODO for general tables and not hardcoded // Determine initial table to load (e.g., first table of first profile, or a default)
let default_profile_name = "default".to_string(); // For now, let's hardcode a default for simplicity, but this should be more dynamic
let default_table_name = "2025_test_schema3".to_string(); let initial_profile_name = app_state
.profile_tree
.profiles
.first()
.map(|p| p.name.clone())
.unwrap_or_else(|| "default".to_string());
let initial_table_name = app_state
.profile_tree
.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
app_state.set_current_view_table(
initial_profile_name.clone(),
initial_table_name.clone(),
);
// Fetch table structure for the default table
let table_structure = grpc_client let table_structure = grpc_client
.get_table_structure(default_profile_name, default_table_name) .get_table_structure(
initial_profile_name.clone(),
initial_table_name.clone(),
)
.await .await
.context("Failed to get initial table structure")?; .context(format!(
"Failed to get initial table structure for {}.{}",
initial_profile_name, initial_table_name
))?;
// Extract the column names from the response
let column_names: Vec<String> = table_structure let column_names: Vec<String> = table_structure
.columns .columns
.iter() .iter()
.map(|col| col.name.clone()) .map(|col| col.name.clone())
.collect(); .collect();
Ok(column_names) Ok((initial_profile_name, initial_table_name, column_names))
} }
pub async fn initialize_adresar_count( // NEW: Fetches and sets count for the current table in FormState
pub async fn fetch_and_set_table_count(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &mut AppState,
) -> Result<()> {
let total_count = grpc_client.get_adresar_count().await.context("Failed to get adresar count")?;
app_state.update_total_count(total_count);
app_state.update_current_position(total_count.saturating_add(1)); // Start in new entry mode
Ok(())
}
pub async fn update_adresar_count(
grpc_client: &mut GrpcClient,
app_state: &mut AppState,
) -> Result<()> {
let total_count = grpc_client.get_adresar_count().await.context("Failed to get adresar by position")?;
app_state.update_total_count(total_count);
Ok(())
}
pub async fn load_adresar_by_position(
grpc_client: &mut GrpcClient,
_app_state: &mut AppState,
form_state: &mut FormState, form_state: &mut FormState,
position: u64, ) -> Result<()> {
let total_count = grpc_client
.get_table_data_count(
form_state.profile_name.clone(),
form_state.table_name.clone(),
)
.await
.context(format!(
"Failed to get count for table {}.{}",
form_state.profile_name, form_state.table_name
))?;
form_state.total_count = total_count;
// Set initial position: if table has items, point to first, else point to new entry
if total_count > 0 {
form_state.current_position = 1;
} else {
form_state.current_position = 1; // For a new entry in an empty table
}
Ok(())
}
// MODIFIED: Generic table data loading
pub async fn load_table_data_by_position(
grpc_client: &mut GrpcClient,
form_state: &mut FormState, // Takes &mut FormState to update it
// position is now read from form_state.current_position
) -> Result<String> { ) -> Result<String> {
match grpc_client.get_adresar_by_position(position).await { // Ensure current_position is valid before fetching
if form_state.current_position == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) {
// This indicates a "new entry" state, no data to load from server.
// The caller should handle this by calling form_state.reset_to_empty()
// or ensuring this function isn't called for a new entry position.
// For now, let's assume reset_to_empty was called if needed.
form_state.reset_to_empty(); // Ensure fields are clear for new entry
return Ok(format!(
"New entry mode for table {}.{}",
form_state.profile_name, form_state.table_name
));
}
if form_state.total_count == 0 && form_state.current_position == 1 {
// Table is empty, this is the position for a new entry
form_state.reset_to_empty();
return Ok(format!(
"New entry mode for empty table {}.{}",
form_state.profile_name, form_state.table_name
));
}
match grpc_client
.get_table_data_by_position(
form_state.profile_name.clone(),
form_state.table_name.clone(),
form_state.current_position as i32,
)
.await
{
Ok(response) => { Ok(response) => {
// Set the ID properly form_state.update_from_response(&response.data);
form_state.id = response.id; // ID, values, current_field, current_cursor_pos, has_unsaved_changes are set by update_from_response
Ok(format!(
// Update form values dynamically "Loaded entry {}/{} for table {}.{}",
form_state.values = vec![ form_state.current_position,
response.firma, form_state.total_count,
response.kz, form_state.profile_name,
response.drc, form_state.table_name
response.ulica, ))
response.psc,
response.mesto,
response.stat,
response.banka,
response.ucet,
response.skladm,
response.ico,
response.kontakt,
response.telefon,
response.skladu,
response.fax,
];
form_state.has_unsaved_changes = false;
Ok(format!("Loaded entry {}", position))
} }
Err(e) => { Err(e) => {
Ok(format!("Error loading entry: {}", e)) // If loading fails (e.g., record deleted, network error), what should happen?
// Maybe reset to a new entry state or show an error and keep current data.
// For now, log error and return error message.
tracing::error!(
"Error loading entry {} for table {}.{}: {}",
form_state.current_position,
form_state.profile_name,
form_state.table_name,
e
);
// Potentially clear form or revert to a safe state
// form_state.reset_to_empty();
Err(anyhow::anyhow!(
"Error loading entry {}: {}",
form_state.current_position,
e
))
} }
} }
} }
/// Handles the consequences of a save operation, like updating counts. // MODIFIED: To work with FormState's count and position
pub async fn handle_save_outcome( pub async fn handle_save_outcome(
save_outcome: SaveOutcome, save_outcome: SaveOutcome,
grpc_client: &mut GrpcClient, _grpc_client: &mut GrpcClient, // May not be needed if count is fetched separately
app_state: &mut AppState, _app_state: &mut AppState, // May not be needed directly
form_state: &mut FormState, form_state: &mut FormState,
) -> Result<()> { ) -> Result<()> {
match save_outcome { match save_outcome {
SaveOutcome::CreatedNew(new_id) => { SaveOutcome::CreatedNew(new_id) => {
// A new record was created, update the count! // form_state.total_count and form_state.current_position should have been updated
UiService::update_adresar_count(grpc_client, app_state).await?; // by the `save` function itself.
// Navigate to the new record (now that count is updated) // Ensure form_state.id is set.
app_state.update_current_position(app_state.total_count); form_state.id = new_id;
form_state.id = new_id; // Ensure ID is set (might be redundant if save already did it) // Potentially, re-fetch count to be absolutely sure, but save should be authoritative.
// UiService::fetch_and_set_table_count(grpc_client, form_state).await?;
} }
SaveOutcome::UpdatedExisting | SaveOutcome::NoChange => { SaveOutcome::UpdatedExisting | SaveOutcome::NoChange => {
// No count update needed for these outcomes // No changes to total_count or current_position needed from here.
} }
} }
Ok(()) Ok(())

View File

@@ -33,11 +33,12 @@ pub struct UiState {
pub struct AppState { pub struct AppState {
// Core editor state // Core editor state
pub current_dir: String, pub current_dir: String,
pub total_count: u64,
pub current_position: u64,
pub profile_tree: ProfileTreeResponse, pub profile_tree: ProfileTreeResponse,
pub selected_profile: Option<String>, pub selected_profile: Option<String>,
pub current_mode: AppMode, pub current_mode: AppMode,
pub current_view_profile_name: Option<String>,
pub current_view_table_name: Option<String>,
pub focused_button_index: usize, pub focused_button_index: usize,
pub pending_table_structure_fetch: Option<(String, String)>, pub pending_table_structure_fetch: Option<(String, String)>,
@@ -52,10 +53,10 @@ impl AppState {
.to_string(); .to_string();
Ok(AppState { Ok(AppState {
current_dir, current_dir,
total_count: 0,
current_position: 0,
profile_tree: ProfileTreeResponse::default(), profile_tree: ProfileTreeResponse::default(),
selected_profile: None, selected_profile: None,
current_view_profile_name: None,
current_view_table_name: None,
current_mode: AppMode::General, current_mode: AppMode::General,
focused_button_index: 0, focused_button_index: 0,
pending_table_structure_fetch: None, pending_table_structure_fetch: None,
@@ -63,19 +64,15 @@ impl AppState {
}) })
} }
// Existing methods remain unchanged
pub fn update_total_count(&mut self, total_count: u64) {
self.total_count = total_count;
}
pub fn update_current_position(&mut self, current_position: u64) {
self.current_position = current_position;
}
pub fn update_mode(&mut self, mode: AppMode) { pub fn update_mode(&mut self, mode: AppMode) {
self.current_mode = mode; self.current_mode = mode;
} }
pub fn set_current_view_table(&mut self, profile_name: String, table_name: String) {
self.current_view_profile_name = Some(profile_name);
self.current_view_table_name = Some(table_name);
}
// Add dialog helper methods // Add dialog helper methods
/// Shows a dialog with the given title, message, and buttons. /// Shows a dialog with the given title, message, and buttons.
/// The first button (index 0) is active by default. /// The first button (index 0) is active by default.

View File

@@ -1,4 +1,6 @@
// src/state/pages/form.rs // src/state/pages/form.rs
use std::collections::HashMap; // NEW
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::Frame; use ratatui::Frame;
@@ -7,7 +9,13 @@ use crate::state::pages::canvas_state::CanvasState;
pub struct FormState { pub struct FormState {
pub id: i64, pub id: i64,
pub fields: Vec<String>, // NEW fields for dynamic table context
pub profile_name: String,
pub table_name: String,
pub total_count: u64,
pub current_position: u64, // 1-based index, 0 or total_count + 1 for new entry
pub fields: Vec<String>, // Already dynamic, which is good
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,
@@ -15,11 +23,19 @@ pub struct FormState {
} }
impl FormState { impl FormState {
/// Create a new FormState with dynamic fields. // MODIFIED constructor
pub fn new(fields: Vec<String>) -> Self { pub fn new(
let values = vec![String::new(); fields.len()]; // Initialize values for each field profile_name: String,
table_name: String,
fields: Vec<String>,
) -> Self {
let values = vec![String::new(); fields.len()];
FormState { FormState {
id: 0, id: 0, // Default to 0, indicating a new or unloaded record
profile_name,
table_name,
total_count: 0, // Will be fetched after initialization
current_position: 0, // Will be set after count is fetched (e.g., 1 or total_count + 1)
fields, fields,
values, values,
current_field: 0, current_field: 0,
@@ -35,31 +51,42 @@ impl FormState {
theme: &Theme, theme: &Theme,
is_edit_mode: bool, is_edit_mode: bool,
highlight_state: &HighlightState, highlight_state: &HighlightState,
total_count: u64, // total_count and current_position are now part of self
current_position: u64,
) { ) {
let fields: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect(); let fields_str_slice: Vec<&str> =
let values: Vec<&String> = self.values.iter().collect(); self.fields.iter().map(|s| s.as_str()).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, // Pass self as CanvasState
&fields, &fields_str_slice,
&self.current_field, &self.current_field,
&values, &values_str_slice,
theme, theme,
is_edit_mode, is_edit_mode,
highlight_state, highlight_state,
total_count, self.total_count, // MODIFIED: Use self.total_count
current_position, self.current_position, // MODIFIED: Use self.current_position
); );
} }
// MODIFIED: Reset now also considers table context for counts
pub fn reset_to_empty(&mut self) { pub fn reset_to_empty(&mut self) {
self.id = 0; // Reset ID to 0 for new entries self.id = 0;
self.values.iter_mut().for_each(|v| v.clear()); // Clear all values self.values.iter_mut().for_each(|v| v.clear());
self.current_field = 0;
self.current_cursor_pos = 0;
self.has_unsaved_changes = false; self.has_unsaved_changes = false;
// current_position should be set to total_count + 1 for a new entry
// This might be better handled by the logic that calls reset_to_empty
// For now, let's ensure it's consistent with a "new" state.
if self.total_count > 0 {
self.current_position = self.total_count + 1;
} else {
self.current_position = 1; // If table is empty, new record is at position 1
}
} }
pub fn get_current_input(&self) -> &str { pub fn get_current_input(&self) -> &str {
@@ -75,15 +102,43 @@ impl FormState {
.expect("Invalid current_field index") .expect("Invalid current_field index")
} }
pub fn update_from_response(&mut self, response: common::proto::multieko2::adresar::AdresarResponse) { // MODIFIED: Update from a generic HashMap response
self.id = response.id; pub fn update_from_response(
self.values = vec![ &mut self,
response.firma, response.kz, response.drc, response_data: &HashMap<String, String>,
response.ulica, response.psc, response.mesto, ) {
response.stat, response.banka, response.ucet, self.values = self.fields
response.skladm, response.ico, response.kontakt, .iter()
response.telefon, response.skladu, response.fax, .map(|field_name| {
]; response_data.get(field_name).cloned().unwrap_or_default()
})
.collect();
if let Some(id_str) = response_data.get("id") {
match id_str.parse::<i64>() {
Ok(parsed_id) => self.id = parsed_id,
Err(e) => {
tracing::error!(
"Failed to parse 'id' field '{}' for table {}.{}: {}",
id_str,
self.profile_name,
self.table_name,
e
);
self.id = 0; // Default to 0 if parsing fails
}
}
} else {
// If no ID is present, it might be a new record structure or an error
// For now, assume it means the record doesn't have an ID from the server yet
self.id = 0;
}
self.has_unsaved_changes = false;
// current_field and current_cursor_pos might need resetting or adjusting
// depending on the desired behavior after loading data.
// For now, let's reset current_field to 0.
self.current_field = 0;
self.current_cursor_pos = 0;
} }
} }
@@ -105,31 +160,26 @@ impl CanvasState for FormState {
} }
fn get_current_input(&self) -> &str { fn get_current_input(&self) -> &str {
self.values // Re-use the struct's own method
.get(self.current_field) FormState::get_current_input(self)
.map(|s| s.as_str())
.unwrap_or("")
} }
fn get_current_input_mut(&mut self) -> &mut String { fn get_current_input_mut(&mut self) -> &mut String {
self.values // Re-use the struct's own method
.get_mut(self.current_field) FormState::get_current_input_mut(self)
.expect("Invalid current_field index")
} }
fn fields(&self) -> Vec<&str> { fn fields(&self) -> Vec<&str> {
self.fields.iter().map(|s| s.as_str()).collect() self.fields.iter().map(|s| s.as_str()).collect()
} }
// --- Implement the setter methods ---
fn set_current_field(&mut self, index: usize) { fn set_current_field(&mut self, index: usize) {
if index < self.fields.len() { // Basic bounds check if index < self.fields.len() {
self.current_field = index; self.current_field = index;
} }
} }
fn set_current_cursor_pos(&mut self, pos: usize) { fn set_current_cursor_pos(&mut self, pos: usize) {
// Optional: Add validation based on current input length if needed
self.current_cursor_pos = pos; self.current_cursor_pos = pos;
} }
@@ -137,12 +187,11 @@ impl CanvasState for FormState {
self.has_unsaved_changes = changed; self.has_unsaved_changes = changed;
} }
// --- Autocomplete Support (Not Used for FormState) ---
fn get_suggestions(&self) -> Option<&[String]> { fn get_suggestions(&self) -> Option<&[String]> {
None // FormState doesn't provide suggestions None
} }
fn get_selected_suggestion_index(&self) -> Option<usize> { fn get_selected_suggestion_index(&self) -> Option<usize> {
None // FormState doesn't have selected suggestions None
} }
} }

View File

@@ -2,114 +2,130 @@
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest}; use anyhow::{Context, Result}; // Added Context
use anyhow::Result; use std::collections::HashMap; // NEW
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SaveOutcome { pub enum SaveOutcome {
NoChange, // Nothing needed saving NoChange,
UpdatedExisting, // An existing record was updated UpdatedExisting,
CreatedNew(i64), // A new record was created (include its new ID) CreatedNew(i64), // Keep the ID
} }
/// Shared logic for saving the current form state // MODIFIED save function
pub async fn save( pub async fn save(
form_state: &mut FormState, form_state: &mut FormState,
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
current_position: &mut u64, ) -> Result<SaveOutcome> {
total_count: u64,
) -> Result<SaveOutcome> { // <-- Return SaveOutcome
if !form_state.has_unsaved_changes { if !form_state.has_unsaved_changes {
return Ok(SaveOutcome::NoChange); // Early exit if no changes return Ok(SaveOutcome::NoChange);
} }
let is_new = *current_position == total_count + 1;
let outcome = if is_new { let data_map: HashMap<String, String> = form_state
let post_request = PostAdresarRequest { .fields
firma: form_state.values[0].clone(), .iter()
kz: form_state.values[1].clone(), .zip(form_state.values.iter())
drc: form_state.values[2].clone(), .map(|(field, value)| (field.clone(), value.clone()))
ulica: form_state.values[3].clone(), .collect();
psc: form_state.values[4].clone(),
mesto: form_state.values[5].clone(), let outcome: SaveOutcome;
stat: form_state.values[6].clone(),
banka: form_state.values[7].clone(), 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) ;
ucet: form_state.values[8].clone(),
skladm: form_state.values[9].clone(),
ico: form_state.values[10].clone(), if is_new_entry {
kontakt: form_state.values[11].clone(), let response = grpc_client
telefon: form_state.values[12].clone(), .post_table_data(
skladu: form_state.values[13].clone(), form_state.profile_name.clone(),
fax: form_state.values[14].clone(), form_state.table_name.clone(),
}; data_map,
let response = grpc_client.post_adresar(post_request).await?; )
let new_id = response.into_inner().id; .await
form_state.id = new_id; .context("Failed to post new table data")?;
SaveOutcome::CreatedNew(new_id) // <-- Return CreatedNew with ID
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 { } else {
let put_request = PutAdresarRequest { return Err(anyhow::anyhow!(
id: form_state.id, "Server failed to insert data: {}",
firma: form_state.values[0].clone(), response.message
kz: form_state.values[1].clone(), ));
drc: form_state.values[2].clone(), }
ulica: form_state.values[3].clone(), } else {
psc: form_state.values[4].clone(), // This assumes form_state.id is valid for an existing record
mesto: form_state.values[5].clone(), if form_state.id == 0 {
stat: form_state.values[6].clone(), return Err(anyhow::anyhow!(
banka: form_state.values[7].clone(), "Cannot update record: ID is 0, but not classified as new entry."
ucet: form_state.values[8].clone(), ));
skladm: form_state.values[9].clone(), }
ico: form_state.values[10].clone(), let response = grpc_client
kontakt: form_state.values[11].clone(), .put_table_data(
telefon: form_state.values[12].clone(), form_state.profile_name.clone(),
skladu: form_state.values[13].clone(), form_state.table_name.clone(),
fax: form_state.values[14].clone(), form_state.id,
}; data_map,
let _ = grpc_client.put_adresar(put_request).await?; )
SaveOutcome::UpdatedExisting .await
}; .context("Failed to put (update) table data")?;
if response.success {
outcome = SaveOutcome::UpdatedExisting;
} else {
return Err(anyhow::anyhow!(
"Server failed to update data: {}",
response.message
));
}
}
form_state.has_unsaved_changes = false; form_state.has_unsaved_changes = false;
Ok(outcome) Ok(outcome)
} }
/// Discard changes since last save
pub async fn revert( pub async fn revert(
form_state: &mut FormState, form_state: &mut FormState, // Takes &mut FormState to update it
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
current_position: &mut u64,
total_count: u64,
) -> Result<String> { ) -> Result<String> {
let is_new = *current_position == total_count + 1; 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
if is_new { form_state.reset_to_empty(); // reset_to_empty will clear values and set id=0
// Clear all fields for new entries form_state.total_count = old_total_count; // Restore total_count
form_state.values.iter_mut().for_each(|v| *v = String::new()); if form_state.total_count > 0 { // Correctly set current_position for new
form_state.has_unsaved_changes = false; form_state.current_position = form_state.total_count + 1;
} else {
form_state.current_position = 1;
}
return Ok("New entry cleared".to_string()); return Ok("New entry cleared".to_string());
} }
let data = grpc_client.get_adresar_by_position(*current_position).await?; 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());
}
}
// Update form fields with saved values let response = grpc_client
form_state.values = vec![ .get_table_data_by_position(
data.firma, form_state.profile_name.clone(),
data.kz, form_state.table_name.clone(),
data.drc, form_state.current_position as i32,
data.ulica, )
data.psc, .await
data.mesto, .context(format!(
data.stat, "Failed to get table data by position {} for table {}.{}",
data.banka, form_state.current_position,
data.ucet, form_state.profile_name,
data.skladm, form_state.table_name
data.ico, ))?;
data.kontakt,
data.telefon,
data.skladu,
data.fax,
];
form_state.has_unsaved_changes = false; form_state.update_from_response(&response.data);
Ok("Changes discarded, reloaded last saved version".to_string()) Ok("Changes discarded, reloaded last saved version".to_string())
} }

View File

@@ -47,8 +47,6 @@ pub fn render_ui(
event_handler_command_mode_active: bool, event_handler_command_mode_active: bool,
event_handler_command_message: &str, event_handler_command_message: &str,
navigation_state: &NavigationState, navigation_state: &NavigationState,
total_count: u64,
current_position: u64,
current_dir: &str, current_dir: &str,
current_fps: f64, current_fps: f64,
app_state: &AppState, app_state: &AppState,
@@ -163,7 +161,6 @@ pub fn render_ui(
render_form( render_form(
f, form_render_area, form_state, &fields_vec, &form_state.current_field, f, form_render_area, form_state, &fields_vec, &form_state.current_field,
&values_vec, theme, is_event_handler_edit_mode, highlight_state, &values_vec, theme, is_event_handler_edit_mode, highlight_state,
total_count, current_position,
); );
} }

View File

@@ -1,4 +1,4 @@
// client/src/ui/handlers/ui.rs // src/ui/handlers/ui.rs
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
@@ -27,36 +27,32 @@ use crate::ui::handlers::context::DialogPurpose;
use crate::tui::functions::common::login; use crate::tui::functions::common::login;
use crate::tui::functions::common::register; use crate::tui::functions::common::register;
use std::time::Instant; use std::time::Instant;
use anyhow::{Context, Result}; use anyhow::{anyhow, Context, Result};
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::SetCursorStyle;
use crossterm::event as crossterm_event; use crossterm::event as crossterm_event;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use tokio::sync::mpsc; use tokio::sync::mpsc;
pub async fn run_ui() -> Result<()> { pub async fn run_ui() -> Result<()> {
let config = Config::load().context("Failed to load configuration")?; let config = Config::load().context("Failed to load configuration")?;
let theme = Theme::from_str(&config.colors.theme); let theme = Theme::from_str(&config.colors.theme);
let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?; let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?;
let mut grpc_client = GrpcClient::new().await?; let mut grpc_client = GrpcClient::new().await.context("Failed to create GrpcClient")?;
let mut command_handler = CommandHandler::new(); let mut command_handler = CommandHandler::new();
let (login_result_sender, mut login_result_receiver) = mpsc::channel::<LoginResult>(1);
let (login_result_sender, mut login_result_receiver) = let (register_result_sender, mut register_result_receiver) = mpsc::channel::<RegisterResult>(1);
mpsc::channel::<LoginResult>(1); let (save_table_result_sender, mut save_table_result_receiver) = mpsc::channel::<Result<String>>(1);
let (register_result_sender, mut register_result_receiver) = let (save_logic_result_sender, _save_logic_result_receiver) = mpsc::channel::<Result<String>>(1);
mpsc::channel::<RegisterResult>(1);
let (save_table_result_sender, mut save_table_result_receiver) =
mpsc::channel::<Result<String>>(1);
let (save_logic_result_sender, _save_logic_result_receiver) =
mpsc::channel::<Result<String>>(1);
let mut event_handler = EventHandler::new( let mut event_handler = EventHandler::new(
login_result_sender.clone(), login_result_sender.clone(),
register_result_sender.clone(), register_result_sender.clone(),
save_table_result_sender.clone(), save_table_result_sender.clone(),
save_logic_result_sender.clone(), save_logic_result_sender.clone(),
).await.context("Failed to create event handler")?; )
.await
.context("Failed to create event handler")?;
let event_reader = EventReader::new(); let event_reader = EventReader::new();
let mut auth_state = AuthState::default(); let mut auth_state = AuthState::default();
@@ -67,7 +63,6 @@ pub async fn run_ui() -> Result<()> {
let mut buffer_state = BufferState::default(); let mut buffer_state = BufferState::default();
let mut app_state = AppState::new().context("Failed to create initial app state")?; let mut app_state = AppState::new().context("Failed to create initial app state")?;
let mut auto_logged_in = false; let mut auto_logged_in = false;
match load_auth_data() { match load_auth_data() {
Ok(Some(stored_data)) => { Ok(Some(stored_data)) => {
@@ -86,14 +81,33 @@ pub async fn run_ui() -> Result<()> {
} }
} }
// Initialize AppState and FormState with table data
let (initial_profile, initial_table, initial_columns) =
UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state)
.await
.context("Failed to initialize app state and form")?;
let column_names = let mut form_state = FormState::new(
UiService::initialize_app_state(&mut grpc_client, &mut app_state) initial_profile.clone(),
.await.context("Failed to initialize app state from UI service")?; initial_table.clone(),
let mut form_state = FormState::new(column_names); initial_columns,
);
UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?; UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state)
.await
.context(format!(
"Failed to fetch initial count for table {}.{}",
initial_profile, initial_table
))?;
// Load initial data for the form
if form_state.total_count > 0 {
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
event_handler.command_message = format!("Error loading initial data: {}", e);
}
} else {
form_state.reset_to_empty(); form_state.reset_to_empty();
}
if auto_logged_in { if auto_logged_in {
buffer_state.history = vec![AppView::Form]; buffer_state.history = vec![AppView::Form];
@@ -104,9 +118,10 @@ pub async fn run_ui() -> Result<()> {
let mut last_frame_time = Instant::now(); let mut last_frame_time = Instant::now();
let mut current_fps = 0.0; let mut current_fps = 0.0;
let mut needs_redraw = true; let mut needs_redraw = true;
let mut prev_view_profile_name = app_state.current_view_profile_name.clone();
let mut prev_view_table_name = app_state.current_view_table_name.clone();
loop { loop {
if let Some(active_view) = buffer_state.get_active_view() { if let Some(active_view) = buffer_state.get_active_view() {
app_state.ui.show_intro = false; app_state.ui.show_intro = false;
app_state.ui.show_login = false; app_state.ui.show_login = false;
@@ -154,14 +169,53 @@ pub async fn run_ui() -> Result<()> {
} }
} }
// Handle table change for FormView
if app_state.ui.show_form {
let current_view_profile = app_state.current_view_profile_name.clone();
let current_view_table = app_state.current_view_table_name.clone();
if prev_view_profile_name != current_view_profile || prev_view_table_name != current_view_table {
if let (Some(prof_name), Some(tbl_name)) = (current_view_profile.as_ref(), current_view_table.as_ref()) {
app_state.show_loading_dialog("Loading Table", &format!("Fetching data for {}.{}...", prof_name, tbl_name));
needs_redraw = true;
match grpc_client.get_table_structure(prof_name.clone(), tbl_name.clone()).await {
Ok(structure_response) => {
let new_columns: Vec<String> = structure_response.columns.iter().map(|c| c.name.clone()).collect();
form_state = FormState::new(prof_name.clone(), tbl_name.clone(), new_columns);
if let Err(e) = UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state).await {
app_state.update_dialog_content(&format!("Error fetching count: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
} else {
if form_state.total_count > 0 {
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
app_state.update_dialog_content(&format!("Error loading data: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
} else {
app_state.hide_dialog();
}
} else {
form_state.reset_to_empty();
app_state.hide_dialog();
}
}
}
Err(e) => {
app_state.update_dialog_content(&format!("Error fetching table structure: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
app_state.current_view_profile_name = prev_view_profile_name.clone();
app_state.current_view_table_name = prev_view_table_name.clone();
}
}
}
prev_view_profile_name = current_view_profile;
prev_view_table_name = current_view_table;
needs_redraw = true;
}
}
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() { if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
if app_state.ui.show_add_logic { if app_state.ui.show_add_logic {
if admin_state.add_logic_state.profile_name == profile_name && if admin_state.add_logic_state.profile_name == profile_name &&
admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) { admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) {
info!("Fetching table structure for {}.{}", profile_name, table_name); info!("Fetching table structure for {}.{}", profile_name, table_name);
let fetch_message = UiService::initialize_add_logic_table_data( let fetch_message = UiService::initialize_add_logic_table_data(
&mut grpc_client, &mut grpc_client,
@@ -194,7 +248,6 @@ pub async fn run_ui() -> Result<()> {
} }
} }
if needs_redraw { if needs_redraw {
terminal.draw(|f| { terminal.draw(|f| {
render_ui( render_ui(
@@ -213,9 +266,6 @@ pub async fn run_ui() -> Result<()> {
event_handler.command_mode, event_handler.command_mode,
&event_handler.command_message, &event_handler.command_message,
&event_handler.navigation_state, &event_handler.navigation_state,
app_state.total_count,
app_state.current_position,
&app_state.current_dir, &app_state.current_dir,
current_fps, current_fps,
&app_state, &app_state,
@@ -224,7 +274,6 @@ pub async fn run_ui() -> Result<()> {
needs_redraw = false; needs_redraw = false;
} }
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() { if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
if app_state.ui.show_add_logic { if app_state.ui.show_add_logic {
let profile_name = admin_state.add_logic_state.profile_name.clone(); let profile_name = admin_state.add_logic_state.profile_name.clone();
@@ -247,7 +296,6 @@ pub async fn run_ui() -> Result<()> {
} }
} }
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state); let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
match current_mode { match current_mode {
AppMode::Edit => { terminal.show_cursor()?; } AppMode::Edit => { terminal.show_cursor()?; }
@@ -264,15 +312,12 @@ pub async fn run_ui() -> Result<()> {
AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; } AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; }
} }
let position_before_event = form_state.current_position;
let total_count = app_state.total_count;
let mut current_position = app_state.current_position;
let position_before_event = current_position;
if app_state.ui.dialog.is_loading { if app_state.ui.dialog.is_loading {
needs_redraw = true; needs_redraw = true;
} }
let mut event_outcome_result = Ok(EventOutcome::Ok(String::new())); let mut event_outcome_result = Ok(EventOutcome::Ok(String::new()));
let mut event_processed = false; let mut event_processed = false;
if crossterm_event::poll(std::time::Duration::from_millis(1))? { if crossterm_event::poll(std::time::Duration::from_millis(1))? {
@@ -292,16 +337,12 @@ pub async fn run_ui() -> Result<()> {
&mut admin_state, &mut admin_state,
&mut buffer_state, &mut buffer_state,
&mut app_state, &mut app_state,
total_count,
&mut current_position,
).await; ).await;
} }
if event_processed { if event_processed {
needs_redraw = true; needs_redraw = true;
} }
app_state.current_position = current_position;
match login_result_receiver.try_recv() { match login_result_receiver.try_recv() {
Ok(result) => { Ok(result) => {
@@ -315,7 +356,6 @@ pub async fn run_ui() -> Result<()> {
} }
} }
match register_result_receiver.try_recv() { match register_result_receiver.try_recv() {
Ok(result) => { Ok(result) => {
if register::handle_registration_result(result, &mut app_state, &mut register_state) { if register::handle_registration_result(result, &mut app_state, &mut register_state) {
@@ -353,7 +393,6 @@ pub async fn run_ui() -> Result<()> {
} }
} }
let mut should_exit = false; let mut should_exit = false;
match event_outcome_result { match event_outcome_result {
Ok(outcome) => match outcome { Ok(outcome) => match outcome {
@@ -383,58 +422,76 @@ pub async fn run_ui() -> Result<()> {
} }
} }
// --- MODIFIED: Position Change Handling (operates on form_state) ---
let position_changed = form_state.current_position != position_before_event;
let position_changed = app_state.current_position != position_before_event;
let current_total_count = app_state.total_count;
let mut position_logic_needs_redraw = false; let mut position_logic_needs_redraw = false;
if app_state.ui.show_form { if app_state.ui.show_form { // Only if the form is active
if position_changed && !event_handler.is_edit_mode { if position_changed && !event_handler.is_edit_mode {
let current_input = form_state.get_current_input(); // This part is okay: update cursor for the current field BEFORE loading new data
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; let current_input_before_load = form_state.get_current_input();
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); let max_cursor_pos_before_load = if !current_input_before_load.is_empty() { current_input_before_load.chars().count() } else { 0 };
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_before_load);
position_logic_needs_redraw = true; position_logic_needs_redraw = true;
if app_state.current_position > current_total_count + 1 { // Validate new form_state.current_position
app_state.current_position = current_total_count + 1; if form_state.total_count > 0 && form_state.current_position > form_state.total_count + 1 {
form_state.current_position = form_state.total_count + 1; // Cap at new entry
} else if form_state.total_count == 0 && form_state.current_position > 1 {
form_state.current_position = 1; // Cap at new entry for empty table
}
if form_state.current_position == 0 && form_state.total_count > 0 {
form_state.current_position = 1; // Don't allow 0 if there are records
} }
if app_state.current_position > current_total_count {
form_state.reset_to_empty();
form_state.current_field = 0;
} else if app_state.current_position >= 1 && app_state.current_position <= current_total_count {
let current_position_to_load = app_state.current_position;
let load_message = UiService::load_adresar_by_position(
&mut grpc_client,
&mut app_state,
&mut form_state,
current_position_to_load,
)
.await.with_context(|| format!("Failed to load adresar by position: {}", current_position_to_load))?;
let current_input_after_load = form_state.get_current_input(); // Load data for the new position OR reset for new entry
let max_cursor_pos_after_load = if !event_handler.is_edit_mode && !current_input_after_load.is_empty() { if (form_state.total_count > 0 && form_state.current_position <= form_state.total_count && form_state.current_position > 0)
current_input_after_load.len() - 1 {
} else { // It's an existing record position
current_input_after_load.len() match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
}; Ok(load_message) => {
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_after_load); if event_handler.command_message.is_empty() || !load_message.starts_with("Error") {
if !load_message.starts_with("Loaded entry") || event_handler.command_message.is_empty() {
event_handler.command_message = load_message; event_handler.command_message = load_message;
} }
}
Err(e) => {
event_handler.command_message = format!("Error loading data: {}", e);
// Consider what to do with form_state here - maybe revert position or clear form
}
}
} else { } else {
app_state.current_position = 1.min(current_total_count + 1); // Position indicates a new entry (or table is empty and position is 1)
if app_state.current_position > current_total_count { form_state.reset_to_empty(); // This sets id=0, clears values, and sets current_position correctly
form_state.reset_to_empty(); event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name);
form_state.current_field = 0;
} }
// NOW, after data is loaded or form is reset, get the current input string and its length
let current_input_after_load_str = form_state.get_current_input();
let current_input_len_after_load = current_input_after_load_str.chars().count();
let max_cursor_pos_for_readonly_after_load = if current_input_len_after_load > 0 {
current_input_len_after_load.saturating_sub(1)
} else {
0
};
if event_handler.is_edit_mode {
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(current_input_len_after_load);
} else {
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_for_readonly_after_load);
// The check for empty string is implicitly handled by max_cursor_pos_for_readonly_after_load being 0
} }
} else if !position_changed && !event_handler.is_edit_mode {
let current_input = form_state.get_current_input(); } else if !position_changed && !event_handler.is_edit_mode && app_state.ui.show_form {
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; // Update cursor if not editing and position didn't change (e.g. arrow keys within field)
let current_input_str = form_state.get_current_input();
let current_input_len = current_input_str.chars().count();
let max_cursor_pos = if current_input_len > 0 {
current_input_len.saturating_sub(1)
} else {
0
};
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
} }
} else if app_state.ui.show_register { } else if app_state.ui.show_register {
@@ -455,12 +512,10 @@ pub async fn run_ui() -> Result<()> {
needs_redraw = true; needs_redraw = true;
} }
if should_exit { if should_exit {
return Ok(()); return Ok(());
} }
let now = Instant::now(); let now = Instant::now();
let frame_duration = now.duration_since(last_frame_time); let frame_duration = now.duration_since(last_frame_time);
last_frame_time = now; last_frame_time = now;