320 lines
12 KiB
Rust
320 lines
12 KiB
Rust
// src/services/ui_service.rs
|
|
|
|
use crate::services::grpc_client::GrpcClient;
|
|
use crate::state::app::state::AppState;
|
|
use crate::state::pages::add_logic::AddLogicState;
|
|
use crate::state::pages::form::{FieldDefinition, FormState};
|
|
use crate::tui::functions::common::form::SaveOutcome;
|
|
use crate::utils::columns::filter_user_columns;
|
|
use anyhow::{anyhow, Context, Result};
|
|
use std::sync::Arc;
|
|
|
|
pub struct UiService;
|
|
|
|
impl UiService {
|
|
pub async fn load_table_view(
|
|
grpc_client: &mut GrpcClient,
|
|
app_state: &mut AppState,
|
|
profile_name: &str,
|
|
table_name: &str,
|
|
) -> Result<FormState> {
|
|
// 1. & 2. Fetch and Cache Schema - UNCHANGED
|
|
let table_structure = grpc_client
|
|
.get_table_structure(profile_name.to_string(), table_name.to_string())
|
|
.await
|
|
.context(format!(
|
|
"Failed to get table structure for {}.{}",
|
|
profile_name, table_name
|
|
))?;
|
|
let cache_key = format!("{}.{}", profile_name, table_name);
|
|
app_state
|
|
.schema_cache
|
|
.insert(cache_key, Arc::new(table_structure.clone()));
|
|
tracing::info!("Schema for '{}.{}' cached.", profile_name, table_name);
|
|
|
|
// --- START: FINAL, SIMPLIFIED, CORRECT LOGIC ---
|
|
|
|
// 3a. Create definitions for REGULAR fields first.
|
|
let mut fields: Vec<FieldDefinition> = table_structure
|
|
.columns
|
|
.iter()
|
|
.filter(|col| {
|
|
!col.is_primary_key
|
|
&& col.name != "deleted"
|
|
&& col.name != "created_at"
|
|
&& !col.name.ends_with("_id") // Filter out ALL potential links
|
|
})
|
|
.map(|col| FieldDefinition {
|
|
display_name: col.name.clone(),
|
|
data_key: col.name.clone(),
|
|
is_link: false,
|
|
link_target_table: None,
|
|
})
|
|
.collect();
|
|
|
|
// 3b. Now, find and APPEND definitions for LINK fields based on the `_id` convention.
|
|
let link_fields: Vec<FieldDefinition> = table_structure
|
|
.columns
|
|
.iter()
|
|
.filter(|col| col.name.ends_with("_id")) // Find all foreign key columns
|
|
.map(|col| {
|
|
// The table we link to is derived from the column name.
|
|
// e.g., "test_diacritics_id" -> "test_diacritics"
|
|
let target_table_base = col
|
|
.name
|
|
.strip_suffix("_id")
|
|
.unwrap_or(&col.name);
|
|
|
|
// Find the full table name from the profile tree for display.
|
|
// e.g., "test_diacritics" -> "2025_test_diacritics"
|
|
let full_target_table_name = app_state
|
|
.profile_tree
|
|
.profiles
|
|
.iter()
|
|
.find(|p| p.name == profile_name)
|
|
.and_then(|p| p.tables.iter().find(|t| t.name.ends_with(target_table_base)))
|
|
.map_or(target_table_base.to_string(), |t| t.name.clone());
|
|
|
|
FieldDefinition {
|
|
display_name: full_target_table_name.clone(),
|
|
data_key: col.name.clone(), // The actual FK column name
|
|
is_link: true,
|
|
link_target_table: Some(full_target_table_name),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
fields.extend(link_fields); // Append the link fields to the end
|
|
|
|
// --- END: FINAL, SIMPLIFIED, CORRECT LOGIC ---
|
|
|
|
Ok(FormState::new(
|
|
profile_name.to_string(),
|
|
table_name.to_string(),
|
|
fields,
|
|
))
|
|
}
|
|
|
|
pub async fn initialize_add_logic_table_data(
|
|
grpc_client: &mut GrpcClient,
|
|
add_logic_state: &mut AddLogicState,
|
|
profile_tree: &common::proto::komp_ac::table_definition::ProfileTreeResponse,
|
|
) -> Result<String> {
|
|
let profile_name_clone_opt = Some(add_logic_state.profile_name.clone());
|
|
let table_name_opt_clone = add_logic_state.selected_table_name.clone();
|
|
|
|
// Collect table names from SAME profile only
|
|
let same_profile_table_names: Vec<String> = profile_tree.profiles
|
|
.iter()
|
|
.find(|profile| profile.name == add_logic_state.profile_name)
|
|
.map(|profile| profile.tables.iter().map(|table| table.name.clone()).collect())
|
|
.unwrap_or_default();
|
|
|
|
// Set same profile table names for autocomplete
|
|
add_logic_state.set_same_profile_table_names(same_profile_table_names.clone());
|
|
|
|
if let (Some(profile_name_clone), Some(table_name_clone)) = (profile_name_clone_opt, table_name_opt_clone) {
|
|
match grpc_client.get_table_structure(profile_name_clone.clone(), table_name_clone.clone()).await {
|
|
Ok(response) => {
|
|
let column_names: Vec<String> = response.columns
|
|
.into_iter()
|
|
.map(|col| col.name)
|
|
.collect();
|
|
|
|
add_logic_state.set_table_columns(column_names.clone());
|
|
|
|
Ok(format!(
|
|
"Loaded {} columns for table '{}' and {} tables from profile '{}'",
|
|
column_names.len(),
|
|
table_name_clone,
|
|
same_profile_table_names.len(),
|
|
add_logic_state.profile_name
|
|
))
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(
|
|
"Failed to fetch table structure for {}.{}: {}",
|
|
profile_name_clone,
|
|
table_name_clone,
|
|
e
|
|
);
|
|
Ok(format!(
|
|
"Warning: Could not load table structure for '{}'. Autocomplete will use {} tables from profile '{}'.",
|
|
table_name_clone,
|
|
same_profile_table_names.len(),
|
|
add_logic_state.profile_name
|
|
))
|
|
}
|
|
}
|
|
} else {
|
|
Ok(format!(
|
|
"No table selected for Add Logic. Loaded {} tables from profile '{}' for autocomplete.",
|
|
same_profile_table_names.len(),
|
|
add_logic_state.profile_name
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Fetches columns for a specific table (used for table.column autocomplete)
|
|
pub async fn fetch_columns_for_table(
|
|
grpc_client: &mut GrpcClient,
|
|
profile_name: &str,
|
|
table_name: &str,
|
|
) -> Result<Vec<String>> {
|
|
match grpc_client.get_table_structure(profile_name.to_string(), table_name.to_string()).await {
|
|
Ok(response) => {
|
|
let column_names: Vec<String> = response.columns
|
|
.into_iter()
|
|
.map(|col| col.name)
|
|
.collect();
|
|
Ok(filter_user_columns(column_names))
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
|
|
Err(e.into())
|
|
}
|
|
}
|
|
}
|
|
|
|
// REFACTOR THIS FUNCTION
|
|
pub async fn initialize_app_state_and_form(
|
|
grpc_client: &mut GrpcClient,
|
|
app_state: &mut AppState,
|
|
) -> Result<(String, String, Vec<String>)> {
|
|
let profile_tree = grpc_client
|
|
.get_profile_tree()
|
|
.await
|
|
.context("Failed to get profile tree")?;
|
|
app_state.profile_tree = profile_tree;
|
|
|
|
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());
|
|
|
|
app_state.set_current_view_table(
|
|
initial_profile_name.clone(),
|
|
initial_table_name.clone(),
|
|
);
|
|
|
|
// NOW, just call our new central function. This avoids code duplication.
|
|
let form_state = Self::load_table_view(
|
|
grpc_client,
|
|
app_state,
|
|
&initial_profile_name,
|
|
&initial_table_name,
|
|
)
|
|
.await?;
|
|
|
|
// The field names for the UI are derived from the new form_state
|
|
let field_names = form_state.fields.iter().map(|f| f.display_name.clone()).collect();
|
|
|
|
Ok((initial_profile_name, initial_table_name, field_names))
|
|
}
|
|
|
|
pub async fn fetch_and_set_table_count(
|
|
grpc_client: &mut GrpcClient,
|
|
form_state: &mut FormState,
|
|
) -> 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;
|
|
|
|
if total_count > 0 {
|
|
form_state.current_position = total_count;
|
|
} else {
|
|
form_state.current_position = 1;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn load_table_data_by_position(
|
|
grpc_client: &mut GrpcClient,
|
|
form_state: &mut FormState,
|
|
) -> Result<String> {
|
|
if form_state.current_position == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) {
|
|
form_state.reset_to_empty();
|
|
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 {
|
|
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) => {
|
|
// FIX: Pass the current position as the second argument
|
|
form_state.update_from_response(&response.data, form_state.current_position);
|
|
Ok(format!(
|
|
"Loaded entry {}/{} for table {}.{}",
|
|
form_state.current_position,
|
|
form_state.total_count,
|
|
form_state.profile_name,
|
|
form_state.table_name
|
|
))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(
|
|
"Error loading entry {} for table {}.{}: {}",
|
|
form_state.current_position,
|
|
form_state.profile_name,
|
|
form_state.table_name,
|
|
e
|
|
);
|
|
Err(anyhow::anyhow!(
|
|
"Error loading entry {}: {}",
|
|
form_state.current_position,
|
|
e
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn handle_save_outcome(
|
|
save_outcome: SaveOutcome,
|
|
_grpc_client: &mut GrpcClient,
|
|
_app_state: &mut AppState,
|
|
form_state: &mut FormState,
|
|
) -> Result<()> {
|
|
match save_outcome {
|
|
SaveOutcome::CreatedNew(new_id) => {
|
|
form_state.id = new_id;
|
|
}
|
|
SaveOutcome::UpdatedExisting | SaveOutcome::NoChange => {
|
|
// No action needed
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|