Compare commits

..

12 Commits

23 changed files with 652 additions and 883 deletions

View File

@@ -18,3 +18,8 @@ Client with tracing:
``` ```
ENABLE_TRACING=1 RUST_LOG=client=debug cargo watch -x 'run --package client -- client' ENABLE_TRACING=1 RUST_LOG=client=debug cargo watch -x 'run --package client -- client'
``` ```
Client with debug that cant be traced
```
cargo run --package client --features ui-debug -- client
```

View File

@@ -26,3 +26,7 @@ tracing-subscriber = "0.3.19"
tui-textarea = { version = "0.7.0", features = ["crossterm", "ratatui", "search"] } tui-textarea = { version = "0.7.0", features = ["crossterm", "ratatui", "search"] }
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
unicode-width = "0.2.0" unicode-width = "0.2.0"
[features]
default = []
ui-debug = []

View File

@@ -1,14 +1,15 @@
// src/components/common/status_line.rs // src/components/common/status_line.rs
use crate::config::colors::themes::Theme;
use crate::state::app::state::AppState;
use ratatui::{ use ratatui::{
style::Style,
layout::Rect, layout::Rect,
Frame, style::Style,
text::{Line, Span}, text::{Line, Span},
widgets::Paragraph, widgets::Paragraph,
Frame,
}; };
use unicode_width::UnicodeWidthStr;
use crate::config::colors::themes::Theme;
use std::path::Path; use std::path::Path;
use unicode_width::UnicodeWidthStr;
pub fn render_status_line( pub fn render_status_line(
f: &mut Frame, f: &mut Frame,
@@ -17,11 +18,24 @@ pub fn render_status_line(
theme: &Theme, theme: &Theme,
is_edit_mode: bool, is_edit_mode: bool,
current_fps: f64, current_fps: f64,
app_state: &AppState,
) { ) {
// --- START FIX ---
// Ensure debug_text is always a &str, which implements UnicodeWidthStr.
#[cfg(feature = "ui-debug")]
let debug_text = app_state.debug_info.as_str();
#[cfg(not(feature = "ui-debug"))]
let debug_text = "";
// --- END FIX ---
let debug_width = UnicodeWidthStr::width(debug_text);
let debug_separator_width = if !debug_text.is_empty() { UnicodeWidthStr::width(" | ") } else { 0 };
let program_info = format!("multieko2 v{}", env!("CARGO_PKG_VERSION")); let program_info = format!("multieko2 v{}", env!("CARGO_PKG_VERSION"));
let mode_text = if is_edit_mode { "[EDIT]" } else { "[READ-ONLY]" }; let mode_text = if is_edit_mode { "[EDIT]" } else { "[READ-ONLY]" };
let home_dir = dirs::home_dir().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(); let home_dir =
dirs::home_dir().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
let display_dir = if current_dir.starts_with(&home_dir) { let display_dir = if current_dir.starts_with(&home_dir) {
current_dir.replacen(&home_dir, "~", 1) current_dir.replacen(&home_dir, "~", 1)
} else { } else {
@@ -37,23 +51,24 @@ pub fn render_status_line(
let separator_width = UnicodeWidthStr::width(separator); let separator_width = UnicodeWidthStr::width(separator);
let fixed_width_with_fps = mode_width + separator_width + separator_width + let fixed_width_with_fps = mode_width + separator_width + separator_width +
program_info_width + separator_width + fps_width; program_info_width + separator_width + fps_width +
let show_fps = fixed_width_with_fps <= available_width; // Use <= to show if it fits exactly debug_separator_width + debug_width;
let show_fps = fixed_width_with_fps <= available_width;
let remaining_width_for_dir = available_width.saturating_sub( let remaining_width_for_dir = available_width.saturating_sub(
mode_width + separator_width + // after mode mode_width + separator_width +
separator_width + program_info_width + // after program_info separator_width + program_info_width +
if show_fps { separator_width + fps_width } else { 0 } // after fps (if show_fps { separator_width + fps_width } else { 0 }) +
debug_separator_width + debug_width,
); );
// Original directory display logic
let dir_display_text_str = if UnicodeWidthStr::width(display_dir.as_str()) <= remaining_width_for_dir { let dir_display_text_str = if UnicodeWidthStr::width(display_dir.as_str()) <= remaining_width_for_dir {
display_dir // display_dir is already a String here display_dir
} else { } else {
let dir_name = Path::new(current_dir) // Use original current_dir for path logic let dir_name = Path::new(current_dir)
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or(current_dir); // Fallback to current_dir if no filename .unwrap_or(current_dir);
if UnicodeWidthStr::width(dir_name) <= remaining_width_for_dir { if UnicodeWidthStr::width(dir_name) <= remaining_width_for_dir {
dir_name.to_string() dir_name.to_string()
} else { } else {
@@ -61,10 +76,10 @@ pub fn render_status_line(
} }
}; };
// Calculate current content width based on what will be displayed
let mut current_content_width = mode_width + separator_width + let mut current_content_width = mode_width + separator_width +
UnicodeWidthStr::width(dir_display_text_str.as_str()) + UnicodeWidthStr::width(dir_display_text_str.as_str()) +
separator_width + program_info_width; separator_width + program_info_width +
debug_separator_width + debug_width;
if show_fps { if show_fps {
current_content_width += separator_width + fps_width; current_content_width += separator_width + fps_width;
} }
@@ -82,12 +97,17 @@ pub fn render_status_line(
line_spans.push(Span::styled(fps_text.as_str(), Style::default().fg(theme.secondary))); line_spans.push(Span::styled(fps_text.as_str(), Style::default().fg(theme.secondary)));
} }
// Calculate padding #[cfg(feature = "ui-debug")]
{
line_spans.push(Span::styled(separator, Style::default().fg(theme.border)));
line_spans.push(Span::styled(debug_text, Style::default().fg(theme.accent)));
}
let padding_needed = available_width.saturating_sub(current_content_width); let padding_needed = available_width.saturating_sub(current_content_width);
if padding_needed > 0 { if padding_needed > 0 {
line_spans.push(Span::styled( line_spans.push(Span::styled(
" ".repeat(padding_needed), " ".repeat(padding_needed),
Style::default().bg(theme.bg), // Ensure padding uses background color Style::default().bg(theme.bg),
)); ));
} }

View File

@@ -17,28 +17,29 @@ pub fn render_form(
fields: &[&str], fields: &[&str],
current_field_idx: &usize, current_field_idx: &usize,
inputs: &[&String], inputs: &[&String],
table_name: &str, // This parameter receives the correct table name
theme: &Theme, theme: &Theme,
is_edit_mode: bool, is_edit_mode: bool,
highlight_state: &HighlightState, highlight_state: &HighlightState,
total_count: u64, total_count: u64,
current_position: u64, current_position: u64,
) { ) {
// Create Adresar card // Use the dynamic `table_name` parameter for the title instead of a hardcoded string.
let card_title = format!(" {} ", table_name);
let adresar_card = Block::default() let adresar_card = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(theme.border)) .border_style(Style::default().fg(theme.border))
.title(" Adresar ") .title(card_title) // Use the dynamic title
.style(Style::default().bg(theme.bg).fg(theme.fg)); .style(Style::default().bg(theme.bg).fg(theme.fg));
f.render_widget(adresar_card, area); f.render_widget(adresar_card, area);
// Define inner area
let inner_area = area.inner(Margin { let inner_area = area.inner(Margin {
horizontal: 1, horizontal: 1,
vertical: 1, vertical: 1,
}); });
// Create main layout
let main_layout = Layout::default() let main_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
@@ -47,12 +48,11 @@ pub fn render_form(
]) ])
.split(inner_area); .split(inner_area);
// Render count/position
let count_position_text = if total_count == 0 && current_position == 1 { let count_position_text = if total_count == 0 && current_position == 1 {
"Total: 0 | New Entry".to_string() "Total: 0 | New Entry".to_string()
} else if current_position > total_count && total_count > 0 { } else if current_position > total_count && total_count > 0 {
format!("Total: {} | New Entry ({})", total_count, current_position) format!("Total: {} | New Entry ({})", total_count, current_position)
} else if total_count == 0 && current_position > 1 { // Should not happen if logic is correct } else if total_count == 0 && current_position > 1 {
format!("Total: 0 | New Entry ({})", current_position) format!("Total: 0 | New Entry ({})", current_position)
} }
else { else {
@@ -63,7 +63,6 @@ pub fn render_form(
.alignment(Alignment::Left); .alignment(Alignment::Left);
f.render_widget(count_para, main_layout[0]); f.render_widget(count_para, main_layout[0]);
// Delegate input handling to canvas
render_canvas( render_canvas(
f, f,
main_layout[1], main_layout[1],

View File

@@ -18,7 +18,7 @@ pub fn render_buffer_list(
area: Rect, area: Rect,
theme: &Theme, theme: &Theme,
buffer_state: &BufferState, buffer_state: &BufferState,
app_state: &AppState, // Add this parameter app_state: &AppState,
) { ) {
// --- Style Definitions --- // --- Style Definitions ---
let active_style = Style::default() let active_style = Style::default()
@@ -39,8 +39,7 @@ pub fn render_buffer_list(
let mut spans = Vec::new(); let mut spans = Vec::new();
let mut current_width = 0; let mut current_width = 0;
// TODO: Replace with actual table name from server response let current_table_name = app_state.current_view_table_name.as_deref();
let current_table_name = Some("2025_customer");
for (original_index, view) in buffer_state.history.iter().enumerate() { for (original_index, view) in buffer_state.history.iter().enumerate() {
// Filter: Only process views matching the active layer // Filter: Only process views matching the active layer

View File

@@ -7,6 +7,7 @@ pub mod components;
pub mod modes; pub mod modes;
pub mod functions; pub mod functions;
pub mod services; pub mod services;
pub mod utils;
pub use ui::run_ui; pub use ui::run_ui;

View File

@@ -23,8 +23,6 @@ pub async fn handle_read_only_event(
add_table_state: &mut AddTableState, add_table_state: &mut AddTableState,
add_logic_state: &mut AddLogicState, add_logic_state: &mut AddLogicState,
key_sequence_tracker: &mut KeySequenceTracker, key_sequence_tracker: &mut KeySequenceTracker,
current_position: &mut u64,
total_count: u64,
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
command_message: &mut String, command_message: &mut String,
edit_mode_cooldown: &mut bool, edit_mode_cooldown: &mut bool,
@@ -74,12 +72,10 @@ pub async fn handle_read_only_event(
action, action,
form_state, form_state,
grpc_client, grpc_client,
current_position,
total_count,
ideal_cursor_column, ideal_cursor_column,
) )
.await? .await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
crate::tui::functions::login::handle_action(action).await? crate::tui::functions::login::handle_action(action).await?
} else if app_state.ui.show_add_table { } else if app_state.ui.show_add_table {
add_table_ro::execute_action( add_table_ro::execute_action(
@@ -143,12 +139,10 @@ pub async fn handle_read_only_event(
action, action,
form_state, form_state,
grpc_client, grpc_client,
current_position,
total_count,
ideal_cursor_column, ideal_cursor_column,
) )
.await? .await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
crate::tui::functions::login::handle_action(action).await? crate::tui::functions::login::handle_action(action).await?
} else if app_state.ui.show_add_table { } else if app_state.ui.show_add_table {
add_table_ro::execute_action( add_table_ro::execute_action(
@@ -177,7 +171,7 @@ pub async fn handle_read_only_event(
key_sequence_tracker, key_sequence_tracker,
command_message, command_message,
).await? ).await?
} else if app_state.ui.show_login { // Handle login general actions } else if app_state.ui.show_login {
auth_ro::execute_action( auth_ro::execute_action(
action, action,
app_state, app_state,
@@ -211,8 +205,6 @@ pub async fn handle_read_only_event(
action, action,
form_state, form_state,
grpc_client, grpc_client,
current_position,
total_count,
ideal_cursor_column, ideal_cursor_column,
) )
.await? .await?
@@ -245,7 +237,7 @@ pub async fn handle_read_only_event(
key_sequence_tracker, key_sequence_tracker,
command_message, command_message,
).await? ).await?
} else if app_state.ui.show_login { // Handle login general actions } else if app_state.ui.show_login {
auth_ro::execute_action( auth_ro::execute_action(
action, action,
app_state, app_state,

View File

@@ -82,6 +82,8 @@ impl TableDependencyGraph {
} }
} }
// ... (NavigationState struct and its new(), activate_*, deactivate(), add_char(), remove_char(), move_*, autocomplete_selected(), get_display_input() methods are unchanged) ...
pub struct NavigationState { pub struct NavigationState {
pub active: bool, pub active: bool,
pub input: String, pub input: String,
@@ -114,7 +116,7 @@ impl NavigationState {
self.input.clear(); self.input.clear();
self.current_path.clear(); self.current_path.clear();
self.graph = None; self.graph = None;
self.update_filtered_options(); // Initial filter with empty input self.update_filtered_options();
} }
pub fn activate_table_tree(&mut self, graph: TableDependencyGraph) { pub fn activate_table_tree(&mut self, graph: TableDependencyGraph) {
@@ -123,7 +125,7 @@ impl NavigationState {
self.graph = Some(graph); self.graph = Some(graph);
self.input.clear(); self.input.clear();
self.current_path.clear(); self.current_path.clear();
self.update_options_for_path(); // Initial options are root tables self.update_options_for_path();
} }
pub fn deactivate(&mut self) { pub fn deactivate(&mut self) {
@@ -145,7 +147,6 @@ impl NavigationState {
NavigationType::TableTree => { NavigationType::TableTree => {
if c == '/' { if c == '/' {
if !self.input.is_empty() { if !self.input.is_empty() {
// Append current input to path
if self.current_path.is_empty() { if self.current_path.is_empty() {
self.current_path = self.input.clone(); self.current_path = self.input.clone();
} else { } else {
@@ -155,10 +156,9 @@ impl NavigationState {
self.input.clear(); self.input.clear();
self.update_options_for_path(); self.update_options_for_path();
} }
// If input is empty and char is '/', do nothing or define behavior
} else { } else {
self.input.push(c); self.input.push(c);
self.update_filtered_options(); // Filter current level options based on input self.update_filtered_options();
} }
} }
} }
@@ -172,24 +172,15 @@ impl NavigationState {
} }
NavigationType::TableTree => { NavigationType::TableTree => {
if self.input.is_empty() { if self.input.is_empty() {
// If input is empty, try to go up in path
if !self.current_path.is_empty() { if !self.current_path.is_empty() {
if let Some(last_slash_idx) = if let Some(last_slash_idx) = self.current_path.rfind('/') {
self.current_path.rfind('/') self.input = self.current_path[last_slash_idx + 1..].to_string();
{ self.current_path = self.current_path[..last_slash_idx].to_string();
// Set input to the segment being removed from path
self.input = self.current_path
[last_slash_idx + 1..]
.to_string();
self.current_path =
self.current_path[..last_slash_idx].to_string();
} else { } else {
// Path was a single segment
self.input = self.current_path.clone(); self.input = self.current_path.clone();
self.current_path.clear(); self.current_path.clear();
} }
self.update_options_for_path(); self.update_options_for_path();
// After path change, current input might match some options, so filter
self.update_filtered_options(); self.update_filtered_options();
} }
} else { } else {
@@ -218,9 +209,7 @@ impl NavigationState {
return; return;
} }
self.selected_index = match self.selected_index { self.selected_index = match self.selected_index {
Some(current) if current >= self.filtered_options.len() - 1 => { Some(current) if current >= self.filtered_options.len() - 1 => Some(0),
Some(0)
}
Some(current) => Some(current + 1), Some(current) => Some(current + 1),
None => Some(0), None => Some(0),
}; };
@@ -234,18 +223,11 @@ impl NavigationState {
pub fn autocomplete_selected(&mut self) { pub fn autocomplete_selected(&mut self) {
if let Some(selected_option_str) = self.get_selected_option_str() { if let Some(selected_option_str) = self.get_selected_option_str() {
// The current `self.input` is the text being typed for the current segment/filter.
// We replace it with the full string of the selected option.
self.input = selected_option_str.to_string(); self.input = selected_option_str.to_string();
// After updating the input, we need to re-filter the options.
// This will typically result in the filtered_options containing only the
// autocompleted item (or items that start with it, if any).
self.update_filtered_options(); self.update_filtered_options();
} }
} }
// Returns the string to display in the input line of the palette
pub fn get_display_input(&self) -> String { pub fn get_display_input(&self) -> String {
match self.navigation_type { match self.navigation_type {
NavigationType::FindFile => self.input.clone(), NavigationType::FindFile => self.input.clone(),
@@ -259,11 +241,12 @@ impl NavigationState {
} }
} }
// Gets the full path of the currently selected item for TableTree, or input for FindFile // --- START FIX ---
pub fn get_selected_value(&self) -> Option<String> { pub fn get_selected_value(&self) -> Option<String> {
match self.navigation_type { match self.navigation_type {
NavigationType::FindFile => { NavigationType::FindFile => {
if self.input.is_empty() { None } else { Some(self.input.clone()) } // Return the highlighted option, not the raw input buffer.
self.get_selected_option_str().map(|s| s.to_string())
} }
NavigationType::TableTree => { NavigationType::TableTree => {
self.get_selected_option_str().map(|selected_name| { self.get_selected_option_str().map(|selected_name| {
@@ -276,26 +259,23 @@ impl NavigationState {
} }
} }
} }
// --- END FIX ---
// Update self.all_options based on current_path (for TableTree)
fn update_options_for_path(&mut self) { fn update_options_for_path(&mut self) {
if let NavigationType::TableTree = self.navigation_type { if let NavigationType::TableTree = self.navigation_type {
if let Some(graph) = &self.graph { if let Some(graph) = &self.graph {
self.all_options = self.all_options = graph.get_dependent_children(&self.current_path);
graph.get_dependent_children(&self.current_path);
} else { } else {
self.all_options.clear(); self.all_options.clear();
} }
} }
// For FindFile, all_options is set once at activation.
self.update_filtered_options(); self.update_filtered_options();
} }
// Update self.filtered_options based on self.all_options and self.input
fn update_filtered_options(&mut self) { fn update_filtered_options(&mut self) {
let filter_text = match self.navigation_type { let filter_text = match self.navigation_type {
NavigationType::FindFile => &self.input, NavigationType::FindFile => &self.input,
NavigationType::TableTree => &self.input, // For TableTree, input is the current segment being typed NavigationType::TableTree => &self.input,
} }
.to_lowercase(); .to_lowercase();
@@ -319,11 +299,12 @@ impl NavigationState {
if self.filtered_options.is_empty() { if self.filtered_options.is_empty() {
self.selected_index = None; self.selected_index = None;
} else { } else {
self.selected_index = Some(0); // Default to selecting the first item self.selected_index = Some(0);
} }
} }
} }
pub async fn handle_command_navigation_event( pub async fn handle_command_navigation_event(
navigation_state: &mut NavigationState, navigation_state: &mut NavigationState,
key: KeyEvent, key: KeyEvent,
@@ -338,51 +319,15 @@ pub async fn handle_command_navigation_event(
navigation_state.deactivate(); navigation_state.deactivate();
Ok(EventOutcome::Ok("Navigation cancelled".to_string())) Ok(EventOutcome::Ok("Navigation cancelled".to_string()))
} }
KeyCode::Enter => {
if let Some(selected_value) = navigation_state.get_selected_value() {
let message = match navigation_state.navigation_type {
NavigationType::FindFile => format!("Selected file: {}", selected_value),
NavigationType::TableTree => format!("Selected table: {}", selected_value),
};
navigation_state.deactivate();
Ok(EventOutcome::Ok(message))
} else {
// Enhanced Enter behavior for TableTree: if input is a valid partial path, try to navigate
if navigation_state.navigation_type == NavigationType::TableTree && !navigation_state.input.is_empty() {
// Check if current input is a prefix of any option or a full option name
if let Some(selected_opt_str) = navigation_state.get_selected_option_str() {
if navigation_state.input == selected_opt_str {
// Input exactly matches the selected option, try to navigate
let input_before_slash = navigation_state.input.clone();
navigation_state.add_char('/');
if navigation_state.input.is_empty() {
return Ok(EventOutcome::Ok(format!("Navigated to: {}/", input_before_slash)));
} else {
return Ok(EventOutcome::Ok(format!("Selected leaf: {}", input_before_slash)));
}
}
}
}
Ok(EventOutcome::Ok("No valid selection to confirm or navigate".to_string()))
}
}
KeyCode::Tab => { KeyCode::Tab => {
if let Some(selected_opt_str) = navigation_state.get_selected_option_str() { if let Some(selected_opt_str) = navigation_state.get_selected_option_str() {
// Scenario 1: Input already exactly matches the selected option
if navigation_state.input == selected_opt_str { if navigation_state.input == selected_opt_str {
// Only attempt to navigate deeper for TableTree mode
if navigation_state.navigation_type == NavigationType::TableTree { if navigation_state.navigation_type == NavigationType::TableTree {
let path_before_nav = navigation_state.current_path.clone(); let path_before_nav = navigation_state.current_path.clone();
let input_before_nav = navigation_state.input.clone(); let input_before_nav = navigation_state.input.clone();
navigation_state.add_char('/'); navigation_state.add_char('/');
if !(navigation_state.input.is_empty() &&
if navigation_state.input.is_empty() && (navigation_state.current_path != path_before_nav || !navigation_state.all_options.is_empty())) {
(navigation_state.current_path != path_before_nav || !navigation_state.all_options.is_empty()) {
// Navigation successful
} else {
// Revert if navigation didn't happen
if !navigation_state.input.is_empty() && navigation_state.input != input_before_nav { if !navigation_state.input.is_empty() && navigation_state.input != input_before_nav {
navigation_state.input = input_before_nav; navigation_state.input = input_before_nav;
if navigation_state.current_path != path_before_nav { if navigation_state.current_path != path_before_nav {
@@ -393,20 +338,11 @@ pub async fn handle_command_navigation_event(
} }
} }
} else { } else {
// Scenario 2: Input is a partial match - autocomplete
navigation_state.autocomplete_selected(); navigation_state.autocomplete_selected();
} }
} }
Ok(EventOutcome::Ok(String::new())) Ok(EventOutcome::Ok(String::new()))
} }
KeyCode::Up => {
navigation_state.move_up();
Ok(EventOutcome::Ok(String::new()))
}
KeyCode::Down => {
navigation_state.move_down();
Ok(EventOutcome::Ok(String::new()))
}
KeyCode::Backspace => { KeyCode::Backspace => {
navigation_state.remove_char(); navigation_state.remove_char();
Ok(EventOutcome::Ok(String::new())) Ok(EventOutcome::Ok(String::new()))
@@ -428,12 +364,24 @@ pub async fn handle_command_navigation_event(
} }
"select" => { "select" => {
if let Some(selected_value) = navigation_state.get_selected_value() { if let Some(selected_value) = navigation_state.get_selected_value() {
let message = match navigation_state.navigation_type { let outcome = match navigation_state.navigation_type {
NavigationType::FindFile => format!("Selected file: {}", selected_value), // --- START FIX ---
NavigationType::TableTree => format!("Selected table: {}", selected_value), NavigationType::FindFile => {
// The purpose of this palette is to select a table.
// Emit a TableSelected event instead of a generic Ok message.
EventOutcome::TableSelected {
path: selected_value,
}
}
// --- END FIX ---
NavigationType::TableTree => {
EventOutcome::TableSelected {
path: selected_value,
}
}
}; };
navigation_state.deactivate(); navigation_state.deactivate();
Ok(EventOutcome::Ok(message)) Ok(outcome)
} else { } else {
Ok(EventOutcome::Ok("No selection".to_string())) Ok(EventOutcome::Ok("No selection".to_string()))
} }

View File

@@ -7,7 +7,7 @@ use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender;
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
use crate::functions::modes::navigation::{add_table_nav, admin_nav}; use crate::functions::modes::navigation::{add_table_nav, admin_nav};
use crate::modes::general::command_navigation::{ use crate::modes::general::command_navigation::{
handle_command_navigation_event, NavigationState, TableDependencyGraph, handle_command_navigation_event, NavigationState,
}; };
use crate::modes::{ use crate::modes::{
canvas::{common_mode, edit, read_only}, canvas::{common_mode, edit, read_only},
@@ -52,6 +52,7 @@ pub enum EventOutcome {
Exit(String), Exit(String),
DataSaved(SaveOutcome, String), DataSaved(SaveOutcome, String),
ButtonSelected { context: UiContext, index: usize }, ButtonSelected { context: UiContext, index: usize },
TableSelected { path: String },
} }
impl EventOutcome { impl EventOutcome {
@@ -132,7 +133,6 @@ impl EventHandler {
) -> 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);
// Handle active command navigation first
if current_mode == AppMode::General && self.navigation_state.active { if current_mode == AppMode::General && self.navigation_state.active {
if let Event::Key(key_event) = event { if let Event::Key(key_event) = event {
let outcome = let outcome =
@@ -154,43 +154,26 @@ impl EventHandler {
let current_view = { let current_view = {
let ui = &app_state.ui; let ui = &app_state.ui;
if ui.show_intro { if ui.show_intro { AppView::Intro }
AppView::Intro else if ui.show_login { AppView::Login }
} else if ui.show_login { else if ui.show_register { AppView::Register }
AppView::Login else if ui.show_admin { AppView::Admin }
} else if ui.show_register { else if ui.show_add_logic { AppView::AddLogic }
AppView::Register else if ui.show_add_table { AppView::AddTable }
} else if ui.show_admin { else if ui.show_form { AppView::Form }
AppView::Admin else { AppView::Scratch }
} else if ui.show_add_logic {
AppView::AddLogic
} else if ui.show_add_table {
AppView::AddTable
} else if ui.show_form {
AppView::Form
} else {
AppView::Scratch
}
}; };
buffer_state.update_history(current_view); buffer_state.update_history(current_view);
if app_state.ui.dialog.dialog_show { if app_state.ui.dialog.dialog_show {
if let Event::Key(key_event) = event { if let Event::Key(key_event) = event {
if let Some(dialog_result) = dialog::handle_dialog_event( if let Some(dialog_result) = dialog::handle_dialog_event(
&Event::Key(key_event), &Event::Key(key_event), config, app_state, login_state,
config, register_state, buffer_state, admin_state,
app_state, ).await {
login_state,
register_state,
buffer_state,
admin_state,
)
.await
{
return dialog_result; return dialog_result;
} }
} else if let Event::Resize(_, _) = event { } else if let Event::Resize(_, _) = event {
// Handle resize if needed
} }
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
@@ -200,34 +183,16 @@ impl EventHandler {
let modifiers = key_event.modifiers; let modifiers = key_event.modifiers;
if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) { if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) {
let message = format!( let message = format!("Sidebar {}", if app_state.ui.show_sidebar { "shown" } else { "hidden" });
"Sidebar {}",
if app_state.ui.show_sidebar {
"shown"
} else {
"hidden"
}
);
return Ok(EventOutcome::Ok(message)); return Ok(EventOutcome::Ok(message));
} }
if UiStateHandler::toggle_buffer_list(&mut app_state.ui, config, key_code, modifiers) { if UiStateHandler::toggle_buffer_list(&mut app_state.ui, config, key_code, modifiers) {
let message = format!( let message = format!("Buffer {}", if app_state.ui.show_buffer_list { "shown" } else { "hidden" });
"Buffer {}",
if app_state.ui.show_buffer_list {
"shown"
} else {
"hidden"
}
);
return Ok(EventOutcome::Ok(message)); return Ok(EventOutcome::Ok(message));
} }
if !matches!(current_mode, AppMode::Edit | AppMode::Command) { if !matches!(current_mode, AppMode::Edit | AppMode::Command) {
if let Some(action) = config.get_action_for_key_in_mode( if let Some(action) = config.get_action_for_key_in_mode(&config.keybindings.global, key_code, modifiers) {
&config.keybindings.global,
key_code,
modifiers,
) {
match action { match action {
"next_buffer" => { "next_buffer" => {
if buffer::switch_buffer(buffer_state, true) { if buffer::switch_buffer(buffer_state, true) {
@@ -236,15 +201,12 @@ impl EventHandler {
} }
"previous_buffer" => { "previous_buffer" => {
if buffer::switch_buffer(buffer_state, false) { if buffer::switch_buffer(buffer_state, false) {
return Ok(EventOutcome::Ok( return Ok(EventOutcome::Ok("Switched to previous buffer".to_string()));
"Switched to previous buffer".to_string(),
));
} }
} }
"close_buffer" => { "close_buffer" => {
let current_table_name = Some("2025_customer"); let current_table_name = app_state.current_view_table_name.as_deref();
let message = let message = buffer_state.close_buffer_with_intro_fallback(current_table_name);
buffer_state.close_buffer_with_intro_fallback(current_table_name);
return Ok(EventOutcome::Ok(message)); return Ok(EventOutcome::Ok(message));
} }
_ => {} _ => {}
@@ -255,14 +217,7 @@ impl EventHandler {
match current_mode { match current_mode {
AppMode::General => { AppMode::General => {
if app_state.ui.show_admin && auth_state.role.as_deref() == Some("admin") { if app_state.ui.show_admin && auth_state.role.as_deref() == Some("admin") {
if admin_nav::handle_admin_navigation( if admin_nav::handle_admin_navigation(key_event, config, app_state, admin_state, buffer_state, &mut self.command_message) {
key_event,
config,
app_state,
admin_state,
buffer_state,
&mut self.command_message,
) {
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
} }
@@ -270,17 +225,9 @@ impl EventHandler {
if app_state.ui.show_add_logic { if app_state.ui.show_add_logic {
let client_clone = grpc_client.clone(); let client_clone = grpc_client.clone();
let sender_clone = self.save_logic_result_sender.clone(); let sender_clone = self.save_logic_result_sender.clone();
if add_logic_nav::handle_add_logic_navigation( if add_logic_nav::handle_add_logic_navigation(
key_event, key_event, config, app_state, &mut admin_state.add_logic_state,
config, &mut self.is_edit_mode, buffer_state, client_clone, sender_clone, &mut self.command_message,
app_state,
&mut admin_state.add_logic_state,
&mut self.is_edit_mode,
buffer_state,
client_clone,
sender_clone,
&mut self.command_message,
) { ) {
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
@@ -289,85 +236,45 @@ impl EventHandler {
if app_state.ui.show_add_table { if app_state.ui.show_add_table {
let client_clone = grpc_client.clone(); let client_clone = grpc_client.clone();
let sender_clone = self.save_table_result_sender.clone(); let sender_clone = self.save_table_result_sender.clone();
if add_table_nav::handle_add_table_navigation( if add_table_nav::handle_add_table_navigation(
key_event, key_event, config, app_state, &mut admin_state.add_table_state,
config, client_clone, sender_clone, &mut self.command_message,
app_state,
&mut admin_state.add_table_state,
client_clone,
sender_clone,
&mut self.command_message,
) { ) {
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
} }
let nav_outcome = navigation::handle_navigation_event( let nav_outcome = navigation::handle_navigation_event(
key_event, key_event, config, form_state, app_state, login_state, register_state,
config, intro_state, admin_state, &mut self.command_mode, &mut self.command_input,
form_state, &mut self.command_message, &mut self.navigation_state,
app_state, ).await;
login_state,
register_state,
intro_state,
admin_state,
&mut self.command_mode,
&mut self.command_input,
&mut self.command_message,
&mut self.navigation_state,
)
.await;
match nav_outcome { match nav_outcome {
Ok(EventOutcome::ButtonSelected { context, index }) => { Ok(EventOutcome::ButtonSelected { context, index }) => {
let message = match context { let message = match context {
UiContext::Intro => { UiContext::Intro => {
intro::handle_intro_selection(app_state, buffer_state, index); intro::handle_intro_selection(app_state, buffer_state, index);
if app_state.ui.show_admin { if app_state.ui.show_admin && !app_state.profile_tree.profiles.is_empty() {
if !app_state.profile_tree.profiles.is_empty() { admin_state.profile_list_state.select(Some(0));
admin_state.profile_list_state.select(Some(0));
}
} }
format!("Intro Option {} selected", index) format!("Intro Option {} selected", index)
} }
UiContext::Login => match index { UiContext::Login => match index {
0 => login::initiate_login( 0 => login::initiate_login(login_state, app_state, self.auth_client.clone(), self.login_result_sender.clone()),
login_state, 1 => login::back_to_main(login_state, app_state, buffer_state).await,
app_state,
self.auth_client.clone(),
self.login_result_sender.clone(),
),
1 => {
login::back_to_main(login_state, app_state, buffer_state)
.await
}
_ => "Invalid Login Option".to_string(), _ => "Invalid Login Option".to_string(),
}, },
UiContext::Register => match index { UiContext::Register => match index {
0 => register::initiate_registration( 0 => register::initiate_registration(register_state, app_state, self.auth_client.clone(), self.register_result_sender.clone()),
register_state, 1 => register::back_to_login(register_state, app_state, buffer_state).await,
app_state,
self.auth_client.clone(),
self.register_result_sender.clone(),
),
1 => {
register::back_to_login(
register_state,
app_state,
buffer_state,
)
.await
}
_ => "Invalid Login Option".to_string(), _ => "Invalid Login Option".to_string(),
}, },
UiContext::Admin => { UiContext::Admin => {
admin::handle_admin_selection(app_state, admin_state); admin::handle_admin_selection(app_state, admin_state);
format!("Admin Option {} selected", index) format!("Admin Option {} selected", index)
} }
UiContext::Dialog => { UiContext::Dialog => "Internal error: Unexpected dialog state".to_string(),
"Internal error: Unexpected dialog state".to_string()
}
}; };
return Ok(EventOutcome::Ok(message)); return Ok(EventOutcome::Ok(message));
} }
@@ -376,74 +283,27 @@ impl EventHandler {
} }
AppMode::ReadOnly => { AppMode::ReadOnly => {
if config.get_read_only_action_for_key(key_code, modifiers) if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise") && ModeManager::can_enter_highlight_mode(current_mode) {
== Some("enter_highlight_mode_linewise") let current_field_index = if app_state.ui.show_login { login_state.current_field() } else if app_state.ui.show_register { register_state.current_field() } else { form_state.current_field() };
&& ModeManager::can_enter_highlight_mode(current_mode) self.highlight_state = HighlightState::Linewise { anchor_line: current_field_index };
{
let current_field_index = if app_state.ui.show_login {
login_state.current_field()
} else if app_state.ui.show_register {
register_state.current_field()
} else {
form_state.current_field()
};
self.highlight_state = HighlightState::Linewise {
anchor_line: current_field_index,
};
self.command_message = "-- LINE HIGHLIGHT --".to_string(); self.command_message = "-- LINE HIGHLIGHT --".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} else if config.get_read_only_action_for_key(key_code, modifiers) } else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode") && ModeManager::can_enter_highlight_mode(current_mode) {
== Some("enter_highlight_mode") let current_field_index = if app_state.ui.show_login { login_state.current_field() } else if app_state.ui.show_register { register_state.current_field() } else { form_state.current_field() };
&& ModeManager::can_enter_highlight_mode(current_mode) let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() } else if app_state.ui.show_register { register_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
{
let current_field_index = if app_state.ui.show_login {
login_state.current_field()
} else if app_state.ui.show_register {
register_state.current_field()
} else {
form_state.current_field()
};
let current_cursor_pos = if app_state.ui.show_login {
login_state.current_cursor_pos()
} else if app_state.ui.show_register {
register_state.current_cursor_pos()
} else {
form_state.current_cursor_pos()
};
let anchor = (current_field_index, current_cursor_pos); let anchor = (current_field_index, current_cursor_pos);
self.highlight_state = HighlightState::Characterwise { anchor }; self.highlight_state = HighlightState::Characterwise { anchor };
self.command_message = "-- HIGHLIGHT --".to_string(); self.command_message = "-- HIGHLIGHT --".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} else if config } else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_before") && ModeManager::can_enter_edit_mode(current_mode) {
.get_read_only_action_for_key(key_code, modifiers)
.as_deref()
== Some("enter_edit_mode_before")
&& ModeManager::can_enter_edit_mode(current_mode)
{
self.is_edit_mode = true; self.is_edit_mode = true;
self.edit_mode_cooldown = true; self.edit_mode_cooldown = true;
self.command_message = "Edit mode".to_string(); self.command_message = "Edit mode".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} else if config } else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after") && ModeManager::can_enter_edit_mode(current_mode) {
.get_read_only_action_for_key(key_code, modifiers) let current_input = if app_state.ui.show_login || app_state.ui.show_register { login_state.get_current_input() } else { form_state.get_current_input() };
.as_deref() let current_cursor_pos = if app_state.ui.show_login || app_state.ui.show_register { login_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
== Some("enter_edit_mode_after")
&& ModeManager::can_enter_edit_mode(current_mode)
{
let current_input = if app_state.ui.show_login || app_state.ui.show_register
{
login_state.get_current_input()
} else {
form_state.get_current_input()
};
let current_cursor_pos =
if app_state.ui.show_login || app_state.ui.show_register {
login_state.current_cursor_pos()
} else {
form_state.current_cursor_pos()
};
if !current_input.is_empty() && current_cursor_pos < current_input.len() { if !current_input.is_empty() && current_cursor_pos < current_input.len() {
if app_state.ui.show_login || app_state.ui.show_register { if app_state.ui.show_login || app_state.ui.show_register {
login_state.set_current_cursor_pos(current_cursor_pos + 1); login_state.set_current_cursor_pos(current_cursor_pos + 1);
@@ -459,10 +319,7 @@ impl EventHandler {
self.command_message = "Edit mode (after cursor)".to_string(); self.command_message = "Edit mode (after cursor)".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} else if config.get_read_only_action_for_key(key_code, modifiers) } else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_command_mode") && ModeManager::can_enter_command_mode(current_mode) {
== Some("enter_command_mode")
&& ModeManager::can_enter_command_mode(current_mode)
{
self.command_mode = true; self.command_mode = true;
self.command_input.clear(); self.command_input.clear();
self.command_message.clear(); self.command_message.clear();
@@ -473,26 +330,14 @@ impl EventHandler {
match action { match action {
"save" | "force_quit" | "save_and_quit" | "revert" => { "save" | "force_quit" | "save_and_quit" | "revert" => {
return common_mode::handle_core_action( return common_mode::handle_core_action(
action, action, form_state, auth_state, login_state, register_state,
form_state, grpc_client, &mut self.auth_client, terminal, app_state,
auth_state, ).await;
login_state,
register_state,
grpc_client,
&mut self.auth_client,
terminal,
app_state,
)
.await;
} }
_ => {} _ => {}
} }
} }
// Extracting values to avoid borrow conflicts
let mut current_position = form_state.current_position;
let total_count = form_state.total_count;
let (_should_exit, message) = read_only::handle_read_only_event( let (_should_exit, message) = read_only::handle_read_only_event(
app_state, app_state,
key_event, key_event,
@@ -503,8 +348,7 @@ 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,
&mut current_position, // No more current_position or total_count arguments
total_count,
grpc_client, grpc_client,
&mut self.command_message, &mut self.command_message,
&mut self.edit_mode_cooldown, &mut self.edit_mode_cooldown,
@@ -515,30 +359,20 @@ impl EventHandler {
} }
AppMode::Highlight => { AppMode::Highlight => {
if config.get_highlight_action_for_key(key_code, modifiers) if config.get_highlight_action_for_key(key_code, modifiers) == Some("exit_highlight_mode") {
== Some("exit_highlight_mode")
{
self.highlight_state = HighlightState::Off; self.highlight_state = HighlightState::Off;
self.command_message = "Exited highlight mode".to_string(); self.command_message = "Exited highlight mode".to_string();
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} else if config.get_highlight_action_for_key(key_code, modifiers) } else if config.get_highlight_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise") {
== Some("enter_highlight_mode_linewise")
{
if let HighlightState::Characterwise { anchor } = self.highlight_state { if let HighlightState::Characterwise { anchor } = self.highlight_state {
self.highlight_state = HighlightState::Linewise { self.highlight_state = HighlightState::Linewise { anchor_line: anchor.0 };
anchor_line: anchor.0,
};
self.command_message = "-- LINE HIGHLIGHT --".to_string(); self.command_message = "-- LINE HIGHLIGHT --".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
return Ok(EventOutcome::Ok("".to_string())); return Ok(EventOutcome::Ok("".to_string()));
} }
// Extracting values to avoid borrow conflicts
let mut current_position = form_state.current_position;
let total_count = form_state.total_count;
let (_should_exit, message) = read_only::handle_read_only_event( let (_should_exit, message) = read_only::handle_read_only_event(
app_state, app_state,
key_event, key_event,
@@ -549,8 +383,6 @@ 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,
&mut current_position,
total_count,
grpc_client, grpc_client,
&mut self.command_message, &mut self.command_message,
&mut self.edit_mode_cooldown, &mut self.edit_mode_cooldown,
@@ -565,99 +397,45 @@ impl EventHandler {
match action { match action {
"save" | "force_quit" | "save_and_quit" | "revert" => { "save" | "force_quit" | "save_and_quit" | "revert" => {
return common_mode::handle_core_action( return common_mode::handle_core_action(
action, action, form_state, auth_state, login_state, register_state,
form_state, grpc_client, &mut self.auth_client, terminal, app_state,
auth_state, ).await;
login_state,
register_state,
grpc_client,
&mut self.auth_client,
terminal,
app_state,
)
.await;
} }
_ => {} _ => {}
} }
} }
// Extracting values to avoid borrow conflicts
let mut current_position = form_state.current_position; let mut current_position = form_state.current_position;
let total_count = form_state.total_count; let total_count = form_state.total_count;
let edit_result = edit::handle_edit_event( let edit_result = edit::handle_edit_event(
key_event, key_event, config, form_state, login_state, register_state, admin_state,
config, &mut self.ideal_cursor_column, &mut current_position, total_count,
form_state, grpc_client, app_state,
login_state, ).await;
register_state,
admin_state,
&mut self.ideal_cursor_column,
&mut current_position,
total_count,
grpc_client,
app_state,
)
.await;
match edit_result { match edit_result {
Ok(edit::EditEventOutcome::ExitEditMode) => { Ok(edit::EditEventOutcome::ExitEditMode) => {
self.is_edit_mode = false; self.is_edit_mode = false;
self.edit_mode_cooldown = true; self.edit_mode_cooldown = true;
let has_changes = if app_state.ui.show_login { let has_changes = if app_state.ui.show_login { login_state.has_unsaved_changes() } else if app_state.ui.show_register { register_state.has_unsaved_changes() } else { form_state.has_unsaved_changes() };
login_state.has_unsaved_changes() self.command_message = if has_changes { "Exited edit mode (unsaved changes remain)".to_string() } else { "Read-only mode".to_string() };
} else if app_state.ui.show_register {
register_state.has_unsaved_changes()
} else {
form_state.has_unsaved_changes()
};
self.command_message = if has_changes {
"Exited edit mode (unsaved changes remain)".to_string()
} else {
"Read-only mode".to_string()
};
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
let current_input = if app_state.ui.show_login { let current_input = if app_state.ui.show_login { login_state.get_current_input() } else if app_state.ui.show_register { register_state.get_current_input() } else { form_state.get_current_input() };
login_state.get_current_input() let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() } else if app_state.ui.show_register { register_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
} else if app_state.ui.show_register { if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
register_state.get_current_input()
} else {
form_state.get_current_input()
};
let current_cursor_pos = if app_state.ui.show_login {
login_state.current_cursor_pos()
} else if app_state.ui.show_register {
register_state.current_cursor_pos()
} else {
form_state.current_cursor_pos()
};
if !current_input.is_empty()
&& current_cursor_pos >= current_input.len()
{
let new_pos = current_input.len() - 1; let new_pos = current_input.len() - 1;
let target_state: &mut dyn CanvasState = if app_state.ui.show_login let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state } else if app_state.ui.show_register { register_state } else { form_state };
{
login_state
} else if app_state.ui.show_register {
register_state
} else {
form_state
};
target_state.set_current_cursor_pos(new_pos); target_state.set_current_cursor_pos(new_pos);
self.ideal_cursor_column = new_pos; self.ideal_cursor_column = new_pos;
} }
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
Ok(edit::EditEventOutcome::Message(msg)) => { Ok(edit::EditEventOutcome::Message(msg)) => {
if !msg.is_empty() { if !msg.is_empty() { self.command_message = msg; }
self.command_message = msg;
}
self.key_sequence_tracker.reset(); self.key_sequence_tracker.reset();
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
Err(e) => { Err(e) => { return Err(e.into()); }
return Err(e.into());
}
} }
} }
@@ -671,31 +449,14 @@ impl EventHandler {
} }
if config.is_command_execute(key_code, modifiers) { if config.is_command_execute(key_code, modifiers) {
// Extracting values to avoid borrow conflicts
let mut current_position = form_state.current_position; let mut current_position = form_state.current_position;
let total_count = form_state.total_count; let total_count = form_state.total_count;
let outcome = command_mode::handle_command_event( let outcome = command_mode::handle_command_event(
key_event, key_event, config, app_state, login_state, register_state, form_state,
config, &mut self.command_input, &mut self.command_message, grpc_client,
app_state, command_handler, terminal, &mut current_position, total_count,
login_state, ).await?;
register_state,
form_state,
&mut self.command_input,
&mut self.command_message,
grpc_client,
command_handler,
terminal,
&mut current_position,
total_count,
)
.await?;
// Update form_state with potentially changed position
form_state.current_position = current_position; form_state.current_position = current_position;
self.command_mode = false; self.command_mode = false;
self.key_sequence_tracker.reset(); self.key_sequence_tracker.reset();
let new_mode = ModeManager::derive_mode(app_state, self, admin_state); let new_mode = ModeManager::derive_mode(app_state, self, admin_state);
@@ -711,40 +472,39 @@ impl EventHandler {
if let KeyCode::Char(c) = key_code { if let KeyCode::Char(c) = key_code {
if c == 'f' { if c == 'f' {
// Assuming 'f' is part of the sequence, e.g. ":f" or " f"
self.key_sequence_tracker.add_key(key_code); self.key_sequence_tracker.add_key(key_code);
let sequence = self.key_sequence_tracker.get_sequence(); let sequence = self.key_sequence_tracker.get_sequence();
if config.matches_key_sequence_generalized(&sequence) if config.matches_key_sequence_generalized(&sequence) == Some("find_file_palette_toggle") {
== Some("find_file_palette_toggle")
{
if app_state.ui.show_form || app_state.ui.show_intro { if app_state.ui.show_form || app_state.ui.show_intro {
// Build table graph from profile data // --- START FIX ---
let graph = TableDependencyGraph::from_profile_tree( let mut all_table_paths: Vec<String> = app_state
&app_state.profile_tree, .profile_tree
); .profiles
.iter()
.flat_map(|profile| {
profile.tables.iter().map(move |table| {
format!("{}/{}", profile.name, table.name)
})
})
.collect();
all_table_paths.sort();
// Activate navigation with graph self.navigation_state.activate_find_file(all_table_paths);
self.navigation_state.activate_table_tree(graph); // --- END FIX ---
self.command_mode = false; // Exit command mode self.command_mode = false;
self.command_input.clear(); self.command_input.clear();
// Message is set by render_find_file_palette's prompt_prefix self.command_message.clear();
self.command_message.clear(); // Clear old command message
self.key_sequence_tracker.reset(); self.key_sequence_tracker.reset();
// ModeManager will derive AppMode::General due to navigation_state.active return Ok(EventOutcome::Ok("Table selection palette activated".to_string()));
// app_state.update_mode(AppMode::General); // This will be handled by ModeManager
return Ok(EventOutcome::Ok(
"Table tree palette activated".to_string(),
));
} else { } else {
self.key_sequence_tracker.reset(); self.key_sequence_tracker.reset();
self.command_input.push('f'); self.command_input.push('f');
if sequence.len() > 1 && sequence[0] == KeyCode::Char('f') { if sequence.len() > 1 && sequence[0] == KeyCode::Char('f') {
self.command_input.push('f'); self.command_input.push('f');
} }
self.command_message = self.command_message = "Find File not available in this view.".to_string();
"Find File not available in this view.".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
} }

View File

@@ -44,8 +44,6 @@ pub async fn handle_highlight_event(
&mut admin_state.add_table_state, &mut admin_state.add_table_state,
&mut admin_state.add_logic_state, &mut admin_state.add_logic_state,
key_sequence_tracker, key_sequence_tracker,
current_position,
total_count,
grpc_client, grpc_client,
command_message, // Pass the message buffer command_message, // Pass the message buffer
edit_mode_cooldown, edit_mode_cooldown,

View File

@@ -5,6 +5,7 @@ use crate::state::pages::form::FormState;
use crate::tui::functions::common::form::SaveOutcome; use crate::tui::functions::common::form::SaveOutcome;
use crate::state::pages::add_logic::AddLogicState; use crate::state::pages::add_logic::AddLogicState;
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crate::utils::columns::filter_user_columns;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
pub struct UiService; pub struct UiService;
@@ -82,7 +83,7 @@ impl UiService {
.into_iter() .into_iter()
.map(|col| col.name) .map(|col| col.name)
.collect(); .collect();
Ok(column_names) Ok(filter_user_columns(column_names))
} }
Err(e) => { Err(e) => {
tracing::warn!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e); tracing::warn!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
@@ -90,12 +91,10 @@ impl UiService {
} }
} }
} }
// MODIFIED: To set initial view table in AppState and return initial column names
pub async fn initialize_app_state_and_form( pub async fn initialize_app_state_and_form(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &mut AppState, app_state: &mut AppState,
// Returns (initial_profile, initial_table, initial_columns)
) -> Result<(String, String, Vec<String>)> { ) -> Result<(String, String, Vec<String>)> {
let profile_tree = grpc_client let profile_tree = grpc_client
.get_profile_tree() .get_profile_tree()
@@ -104,7 +103,6 @@ impl UiService {
app_state.profile_tree = profile_tree; app_state.profile_tree = profile_tree;
// Determine initial table to load (e.g., first table of first profile, or a default) // Determine initial table to load (e.g., first table of first profile, or a default)
// For now, let's hardcode a default for simplicity, but this should be more dynamic
let initial_profile_name = app_state let initial_profile_name = app_state
.profile_tree .profile_tree
.profiles .profiles
@@ -141,10 +139,11 @@ impl UiService {
.map(|col| col.name.clone()) .map(|col| col.name.clone())
.collect(); .collect();
Ok((initial_profile_name, initial_table_name, column_names)) let filtered_columns = filter_user_columns(column_names);
Ok((initial_profile_name, initial_table_name, filtered_columns))
} }
// NEW: Fetches and sets count for the current table in FormState
pub async fn fetch_and_set_table_count( pub async fn fetch_and_set_table_count(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
form_state: &mut FormState, form_state: &mut FormState,
@@ -161,35 +160,26 @@ impl UiService {
))?; ))?;
form_state.total_count = total_count; 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 { if total_count > 0 {
form_state.current_position = 1; form_state.current_position = total_count;
} else { } else {
form_state.current_position = 1; // For a new entry in an empty table form_state.current_position = 1;
} }
Ok(()) Ok(())
} }
// MODIFIED: Generic table data loading
pub async fn load_table_data_by_position( pub async fn load_table_data_by_position(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
form_state: &mut FormState, // Takes &mut FormState to update it form_state: &mut FormState,
// position is now read from form_state.current_position
) -> Result<String> { ) -> Result<String> {
// 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) { 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. form_state.reset_to_empty();
// 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!( return Ok(format!(
"New entry mode for table {}.{}", "New entry mode for table {}.{}",
form_state.profile_name, form_state.table_name form_state.profile_name, form_state.table_name
)); ));
} }
if form_state.total_count == 0 && form_state.current_position == 1 { 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(); form_state.reset_to_empty();
return Ok(format!( return Ok(format!(
"New entry mode for empty table {}.{}", "New entry mode for empty table {}.{}",
@@ -197,7 +187,6 @@ impl UiService {
)); ));
} }
match grpc_client match grpc_client
.get_table_data_by_position( .get_table_data_by_position(
form_state.profile_name.clone(), form_state.profile_name.clone(),
@@ -207,8 +196,8 @@ impl UiService {
.await .await
{ {
Ok(response) => { Ok(response) => {
form_state.update_from_response(&response.data); // FIX: Pass the current position as the second argument
// ID, values, current_field, current_cursor_pos, has_unsaved_changes are set by update_from_response form_state.update_from_response(&response.data, form_state.current_position);
Ok(format!( Ok(format!(
"Loaded entry {}/{} for table {}.{}", "Loaded entry {}/{} for table {}.{}",
form_state.current_position, form_state.current_position,
@@ -218,9 +207,6 @@ impl UiService {
)) ))
} }
Err(e) => { Err(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!( tracing::error!(
"Error loading entry {} for table {}.{}: {}", "Error loading entry {} for table {}.{}: {}",
form_state.current_position, form_state.current_position,
@@ -228,8 +214,6 @@ impl UiService {
form_state.table_name, form_state.table_name,
e e
); );
// Potentially clear form or revert to a safe state
// form_state.reset_to_empty();
Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"Error loading entry {}: {}", "Error loading entry {}: {}",
form_state.current_position, form_state.current_position,
@@ -239,27 +223,20 @@ impl UiService {
} }
} }
// 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, // May not be needed if count is fetched separately _grpc_client: &mut GrpcClient,
_app_state: &mut AppState, // May not be needed directly _app_state: &mut AppState,
form_state: &mut FormState, form_state: &mut FormState,
) -> Result<()> { ) -> Result<()> {
match save_outcome { match save_outcome {
SaveOutcome::CreatedNew(new_id) => { SaveOutcome::CreatedNew(new_id) => {
// form_state.total_count and form_state.current_position should have been updated
// by the `save` function itself.
// Ensure form_state.id is set.
form_state.id = new_id; form_state.id = new_id;
// 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 changes to total_count or current_position needed from here. // No action needed
} }
} }
Ok(()) Ok(())
} }
} }

View File

@@ -44,6 +44,9 @@ pub struct AppState {
// UI preferences // UI preferences
pub ui: UiState, pub ui: UiState,
#[cfg(feature = "ui-debug")]
pub debug_info: String,
} }
impl AppState { impl AppState {
@@ -61,6 +64,9 @@ impl AppState {
focused_button_index: 0, focused_button_index: 0,
pending_table_structure_fetch: None, pending_table_structure_fetch: None,
ui: UiState::default(), ui: UiState::default(),
#[cfg(feature = "ui-debug")]
debug_info: String::new(),
}) })
} }
@@ -133,6 +139,7 @@ impl AppState {
self.ui.dialog.dialog_active_button_index = 0; self.ui.dialog.dialog_active_button_index = 0;
self.ui.dialog.purpose = None; self.ui.dialog.purpose = None;
self.ui.focus_outside_canvas = false; self.ui.focus_outside_canvas = false;
self.ui.dialog.is_loading = false;
} }
/// Sets the active button index, wrapping around if necessary. /// Sets the active button index, wrapping around if necessary.

View File

@@ -23,7 +23,9 @@ pub struct FormState {
} }
impl FormState { impl FormState {
// MODIFIED constructor /// Creates a new, empty FormState for a given table.
/// The position defaults to 1, representing either the first record
/// or the position for a new entry if the table is empty.
pub fn new( pub fn new(
profile_name: String, profile_name: String,
table_name: String, table_name: String,
@@ -34,8 +36,9 @@ impl FormState {
id: 0, // Default to 0, indicating a new or unloaded record id: 0, // Default to 0, indicating a new or unloaded record
profile_name, profile_name,
table_name, table_name,
total_count: 0, // Will be fetched after initialization 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) // FIX: Default to 1. A position of 0 is an invalid state.
current_position: 1,
fields, fields,
values, values,
current_field: 0, current_field: 0,
@@ -51,7 +54,6 @@ impl FormState {
theme: &Theme, theme: &Theme,
is_edit_mode: bool, is_edit_mode: bool,
highlight_state: &HighlightState, highlight_state: &HighlightState,
// total_count and current_position are now part of self
) { ) {
let fields_str_slice: Vec<&str> = let fields_str_slice: Vec<&str> =
self.fields.iter().map(|s| s.as_str()).collect(); self.fields.iter().map(|s| s.as_str()).collect();
@@ -64,24 +66,24 @@ impl FormState {
&fields_str_slice, &fields_str_slice,
&self.current_field, &self.current_field,
&values_str_slice, &values_str_slice,
&self.table_name,
theme, theme,
is_edit_mode, is_edit_mode,
highlight_state, highlight_state,
self.total_count, // MODIFIED: Use self.total_count self.total_count,
self.current_position, // MODIFIED: Use self.current_position self.current_position,
); );
} }
// MODIFIED: Reset now also considers table context for counts /// Resets the form to a state for creating a new entry.
/// It clears all values and sets the position to be one after the last record.
pub fn reset_to_empty(&mut self) { pub fn reset_to_empty(&mut self) {
self.id = 0; self.id = 0;
self.values.iter_mut().for_each(|v| v.clear()); self.values.iter_mut().for_each(|v| v.clear());
self.current_field = 0; self.current_field = 0;
self.current_cursor_pos = 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 // Set the position 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 { if self.total_count > 0 {
self.current_position = self.total_count + 1; self.current_position = self.total_count + 1;
} else { } else {
@@ -102,41 +104,45 @@ impl FormState {
.expect("Invalid current_field index") .expect("Invalid current_field index")
} }
// MODIFIED: Update from a generic HashMap response /// Updates the form's values from a data response and sets its position.
/// This is the single source of truth for populating the form after a data fetch.
pub fn update_from_response( pub fn update_from_response(
&mut self, &mut self,
response_data: &HashMap<String, String>, response_data: &HashMap<String, String>,
// FIX: Add new_position to make this method authoritative.
new_position: u64,
) { ) {
self.values = self.fields // Create a new vector for the values, ensuring they are in the correct order.
.iter() self.values = self.fields.iter().map(|field_from_schema| {
.map(|field_name| { // For each field from our schema, find the corresponding key in the
response_data.get(field_name).cloned().unwrap_or_default() // response data by doing a case-insensitive comparison.
}) response_data
.collect(); .iter()
.find(|(key_from_data, _)| key_from_data.eq_ignore_ascii_case(field_from_schema))
.map(|(_, value)| value.clone()) // If found, clone its value.
.unwrap_or_default() // If not found, use an empty string.
}).collect();
if let Some(id_str) = response_data.get("id") { // Now, do the same case-insensitive lookup for the 'id' field.
match id_str.parse::<i64>() { let id_str_opt = response_data
Ok(parsed_id) => self.id = parsed_id, .iter()
Err(e) => { .find(|(k, _)| k.eq_ignore_ascii_case("id"))
tracing::error!( .map(|(_, v)| v);
"Failed to parse 'id' field '{}' for table {}.{}: {}",
id_str, if let Some(id_str) = id_str_opt {
self.profile_name, if let Ok(parsed_id) = id_str.parse::<i64>() {
self.table_name, self.id = parsed_id;
e } else {
); tracing::error!( "Failed to parse 'id' field '{}' for table {}.{}", id_str, self.profile_name, self.table_name);
self.id = 0; // Default to 0 if parsing fails self.id = 0;
}
} }
} else { } 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.id = 0;
} }
// FIX: Set the position from the provided parameter.
self.current_position = new_position;
self.has_unsaved_changes = false; 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_field = 0;
self.current_cursor_pos = 0; self.current_cursor_pos = 0;
} }

View File

@@ -21,9 +21,7 @@ pub async fn save(
return Ok(SaveOutcome::NoChange); return Ok(SaveOutcome::NoChange);
} }
let data_map: HashMap<String, String> = form_state let data_map: HashMap<String, String> = form_state.fields.iter()
.fields
.iter()
.zip(form_state.values.iter()) .zip(form_state.values.iter())
.map(|(field, value)| (field.clone(), value.clone())) .map(|(field, value)| (field.clone(), value.clone()))
.collect(); .collect();
@@ -126,6 +124,8 @@ pub async fn revert(
form_state.table_name form_state.table_name
))?; ))?;
form_state.update_from_response(&response.data); // 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()) Ok("Changes discarded, reloaded last saved version".to_string())
} }

View File

@@ -1,19 +1,15 @@
// src/tui/functions/form.rs // src/tui/functions/form.rs
use crate::state::pages::canvas_state::CanvasState;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::state::pages::canvas_state::CanvasState;
use crate::services::ui_service::UiService;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
pub async fn handle_action( pub async fn handle_action(
action: &str, action: &str,
form_state: &mut FormState, form_state: &mut FormState,
grpc_client: &mut GrpcClient, _grpc_client: &mut GrpcClient,
current_position: &mut u64,
total_count: u64,
ideal_cursor_column: &mut usize, ideal_cursor_column: &mut usize,
) -> Result<String> { ) -> Result<String> {
// Check for unsaved changes in both cases
if form_state.has_unsaved_changes() { if form_state.has_unsaved_changes() {
return Ok( return Ok(
"Unsaved changes. Save (Ctrl+S) or Revert (Ctrl+R) before navigating." "Unsaved changes. Save (Ctrl+S) or Revert (Ctrl+R) before navigating."
@@ -21,56 +17,29 @@ pub async fn handle_action(
); );
} }
let total_count = form_state.total_count;
match action { match action {
"previous_entry" => { "previous_entry" => {
let new_position = form_state.current_position.saturating_sub(1); // Only decrement if the current position is greater than the first record.
if new_position >= 1 { // This prevents wrapping from 1 to total_count.
form_state.current_position = new_position; // It also correctly handles moving from "New Entry" (total_count + 1) to the last record.
*current_position = new_position; if form_state.current_position > 1 {
form_state.current_position -= 1;
if new_position <= form_state.total_count { *ideal_cursor_column = 0;
let load_message = UiService::load_table_data_by_position(grpc_client, form_state).await?;
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else { 0 };
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok(load_message)
} else {
Ok(format!("Moved to position {}", new_position))
}
} else {
Ok("Already at first position".into())
} }
} }
"next_entry" => { "next_entry" => {
if form_state.current_position <= form_state.total_count { // Only increment if the current position is not yet at the "New Entry" stage.
// The "New Entry" position is total_count + 1.
// This allows moving from the last record to "New Entry", but stops there.
if form_state.current_position <= total_count {
form_state.current_position += 1; form_state.current_position += 1;
*current_position = form_state.current_position; *ideal_cursor_column = 0;
if form_state.current_position <= form_state.total_count {
let load_message = UiService::load_table_data_by_position(grpc_client, form_state).await?;
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else { 0 };
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok(load_message)
} else {
form_state.reset_to_empty();
form_state.current_field = 0;
form_state.current_cursor_pos = 0;
*ideal_cursor_column = 0;
Ok("New form entry mode".into())
}
} else {
Ok("Already at last entry".into())
} }
} }
_ => Err(anyhow!("Unknown form action: {}", action)) _ => return Err(anyhow!("Unknown form action: {}", action)),
} }
Ok(String::new())
} }

View File

@@ -137,6 +137,7 @@ pub fn render_ui(
f, app_state, auth_state, admin_state, main_content_area, theme, f, app_state, auth_state, admin_state, main_content_area, theme,
&app_state.profile_tree, &app_state.selected_profile, &app_state.profile_tree, &app_state.selected_profile,
); );
} else if app_state.ui.show_form { } else if app_state.ui.show_form {
let (sidebar_area, form_actual_area) = calculate_sidebar_layout( let (sidebar_area, form_actual_area) = calculate_sidebar_layout(
app_state.ui.show_sidebar, main_content_area app_state.ui.show_sidebar, main_content_area
@@ -158,19 +159,39 @@ pub fn render_ui(
}; };
let fields_vec: Vec<&str> = form_state.fields.iter().map(AsRef::as_ref).collect(); let fields_vec: Vec<&str> = form_state.fields.iter().map(AsRef::as_ref).collect();
let values_vec: Vec<&String> = form_state.values.iter().collect(); let values_vec: Vec<&String> = form_state.values.iter().collect();
// --- START FIX ---
// Add the missing `&form_state.table_name` argument to this function call.
render_form( render_form(
f, form_render_area, form_state, &fields_vec, &form_state.current_field, f,
&values_vec, theme, is_event_handler_edit_mode, highlight_state, form_render_area,
form_state,
&fields_vec,
&form_state.current_field,
&values_vec,
&form_state.table_name, // <-- THIS ARGUMENT WAS MISSING
theme,
is_event_handler_edit_mode,
highlight_state,
form_state.total_count, form_state.total_count,
form_state.current_position, form_state.current_position,
); );
// --- END FIX ---
} }
if let Some(area) = buffer_list_area { if let Some(area) = buffer_list_area {
render_buffer_list(f, area, theme, buffer_state, app_state); render_buffer_list(f, area, theme, buffer_state, app_state);
} }
render_status_line(f, status_line_area, current_dir, theme, is_event_handler_edit_mode, current_fps); render_status_line(
f,
status_line_area,
current_dir,
theme,
is_event_handler_edit_mode,
current_fps,
app_state,
);
if let Some(palette_or_command_area) = command_render_area { // Use the calculated area if let Some(palette_or_command_area) = command_render_area { // Use the calculated area
if navigation_state.active { if navigation_state.active {

View File

@@ -26,6 +26,7 @@ use crate::tui::functions::common::register::RegisterResult;
use crate::ui::handlers::context::DialogPurpose; 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 crate::utils::columns::filter_user_columns;
use std::time::Instant; use std::time::Instant;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::SetCursorStyle;
@@ -81,16 +82,17 @@ pub async fn run_ui() -> Result<()> {
} }
} }
// Initialize AppState and FormState with table data let (initial_profile, initial_table, initial_columns_from_service) =
let (initial_profile, initial_table, initial_columns) =
UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state) UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state)
.await .await
.context("Failed to initialize app state and form")?; .context("Failed to initialize app state and form")?;
let filtered_columns = filter_user_columns(initial_columns_from_service);
let mut form_state = FormState::new( let mut form_state = FormState::new(
initial_profile.clone(), initial_profile.clone(),
initial_table.clone(), initial_table.clone(),
initial_columns, filtered_columns,
); );
UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state) UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state)
@@ -100,7 +102,6 @@ pub async fn run_ui() -> Result<()> {
initial_profile, initial_table initial_profile, initial_table
))?; ))?;
// Load initial data for the form
if form_state.total_count > 0 { if form_state.total_count > 0 {
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { 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); event_handler.command_message = format!("Error loading initial data: {}", e);
@@ -120,210 +121,15 @@ pub async fn run_ui() -> Result<()> {
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_profile_name = app_state.current_view_profile_name.clone();
let mut prev_view_table_name = app_state.current_view_table_name.clone(); let mut prev_view_table_name = app_state.current_view_table_name.clone();
let mut table_just_switched = false;
loop { loop {
if let Some(active_view) = buffer_state.get_active_view() {
app_state.ui.show_intro = false;
app_state.ui.show_login = false;
app_state.ui.show_register = false;
app_state.ui.show_admin = false;
app_state.ui.show_add_table = false;
app_state.ui.show_add_logic = false;
app_state.ui.show_form = false;
match active_view {
AppView::Intro => app_state.ui.show_intro = true,
AppView::Login => app_state.ui.show_login = true,
AppView::Register => app_state.ui.show_register = true,
AppView::Admin => {
info!("Active view is Admin, refreshing profile tree...");
match grpc_client.get_profile_tree().await {
Ok(refreshed_tree) => {
app_state.profile_tree = refreshed_tree;
}
Err(e) => {
error!("Failed to refresh profile tree for Admin panel: {}", e);
event_handler.command_message = format!("Error refreshing admin data: {}", e);
}
}
app_state.ui.show_admin = true;
let profile_names = app_state.profile_tree.profiles.iter()
.map(|p| p.name.clone())
.collect();
admin_state.set_profiles(profile_names);
if admin_state.current_focus == AdminFocus::default() ||
!matches!(admin_state.current_focus,
AdminFocus::InsideProfilesList |
AdminFocus::Tables | AdminFocus::InsideTablesList |
AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) {
admin_state.current_focus = AdminFocus::ProfilesPane;
}
if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() {
admin_state.profile_list_state.select(Some(0));
}
}
AppView::AddTable => app_state.ui.show_add_table = true,
AppView::AddLogic => app_state.ui.show_add_logic = true,
AppView::Form => app_state.ui.show_form = true,
AppView::Scratch => {}
}
}
// 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 app_state.ui.show_add_logic {
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()) {
info!("Fetching table structure for {}.{}", profile_name, table_name);
let fetch_message = UiService::initialize_add_logic_table_data(
&mut grpc_client,
&mut admin_state.add_logic_state,
&app_state.profile_tree,
).await.unwrap_or_else(|e| {
error!("Error initializing add_logic_table_data: {}", e);
format!("Error fetching table structure: {}", e)
});
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
info!("{}", fetch_message);
} else {
event_handler.command_message = fetch_message;
}
needs_redraw = true;
} else {
error!(
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but add_logic_state is for {}.{:?}",
profile_name, table_name,
admin_state.add_logic_state.profile_name,
admin_state.add_logic_state.selected_table_name
);
}
} else {
warn!(
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
profile_name, table_name
);
}
}
if needs_redraw {
terminal.draw(|f| {
render_ui(
f,
&mut form_state,
&mut auth_state,
&login_state,
&register_state,
&intro_state,
&mut admin_state,
&buffer_state,
&theme,
event_handler.is_edit_mode,
&event_handler.highlight_state,
&event_handler.command_input,
event_handler.command_mode,
&event_handler.command_message,
&event_handler.navigation_state,
&app_state.current_dir,
current_fps,
&app_state,
);
}).context("Terminal draw call failed")?;
needs_redraw = false;
}
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
if app_state.ui.show_add_logic {
let profile_name = admin_state.add_logic_state.profile_name.clone();
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
Ok(columns) => {
admin_state.add_logic_state.set_columns_for_table_autocomplete(columns.clone());
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
}
Err(e) => {
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
admin_state.add_logic_state.script_editor_awaiting_column_autocomplete = None;
admin_state.add_logic_state.deactivate_script_editor_autocomplete();
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
}
}
needs_redraw = true;
}
}
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
match current_mode {
AppMode::Edit => { terminal.show_cursor()?; }
AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; }
AppMode::ReadOnly => {
if !app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; }
else { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; }
terminal.show_cursor().context("Failed to show cursor in ReadOnly mode")?;
}
AppMode::General => {
if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; }
else { terminal.hide_cursor()?; }
}
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 position_before_event = form_state.current_position;
if app_state.ui.dialog.is_loading {
needs_redraw = true;
}
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))? {
let event = event_reader.read_event().context("Failed to read terminal event")?; let event = event_reader.read_event().context("Failed to read terminal event")?;
event_processed = true; event_processed = true;
event_outcome_result = event_handler.handle_event( let event_outcome_result = event_handler.handle_event(
event, event,
&config, &config,
&mut terminal, &mut terminal,
@@ -338,10 +144,53 @@ pub async fn run_ui() -> Result<()> {
&mut buffer_state, &mut buffer_state,
&mut app_state, &mut app_state,
).await; ).await;
}
if event_processed { let mut should_exit = false;
needs_redraw = true; match event_outcome_result {
Ok(outcome) => match outcome {
EventOutcome::Ok(message) => {
if !message.is_empty() {
event_handler.command_message = message;
}
}
EventOutcome::Exit(message) => {
event_handler.command_message = message;
should_exit = true;
}
EventOutcome::DataSaved(save_outcome, message) => {
event_handler.command_message = message;
if let Err(e) = UiService::handle_save_outcome(
save_outcome,
&mut grpc_client,
&mut app_state,
&mut form_state,
).await {
event_handler.command_message =
format!("Error handling save outcome: {}", e);
}
}
EventOutcome::ButtonSelected { .. } => {}
EventOutcome::TableSelected { path } => {
let parts: Vec<&str> = path.split('/').collect();
if parts.len() == 2 {
let profile_name = parts[0].to_string();
let table_name = parts[1].to_string();
app_state.set_current_view_table(profile_name, table_name);
buffer_state.update_history(AppView::Form);
event_handler.command_message = format!("Loading table: {}", path);
} else {
event_handler.command_message = format!("Invalid table path: {}", path);
}
}
},
Err(e) => {
event_handler.command_message = format!("Error: {}", e);
}
}
if should_exit {
return Ok(());
}
} }
match login_result_receiver.try_recv() { match login_result_receiver.try_recv() {
@@ -393,62 +242,208 @@ pub async fn run_ui() -> Result<()> {
} }
} }
let mut should_exit = false; if let Some(active_view) = buffer_state.get_active_view() {
match event_outcome_result { app_state.ui.show_intro = false;
Ok(outcome) => match outcome { app_state.ui.show_login = false;
EventOutcome::Ok(_message) => {} app_state.ui.show_register = false;
EventOutcome::Exit(message) => { app_state.ui.show_admin = false;
event_handler.command_message = message; app_state.ui.show_add_table = false;
should_exit = true; app_state.ui.show_add_logic = false;
} app_state.ui.show_form = false;
EventOutcome::DataSaved(save_outcome, message) => { match active_view {
event_handler.command_message = message; AppView::Intro => app_state.ui.show_intro = true,
if let Err(e) = UiService::handle_save_outcome( AppView::Login => app_state.ui.show_login = true,
save_outcome, AppView::Register => app_state.ui.show_register = true,
&mut grpc_client, AppView::Admin => {
&mut app_state, info!("Active view is Admin, refreshing profile tree...");
&mut form_state, match grpc_client.get_profile_tree().await {
) Ok(refreshed_tree) => {
.await app_state.profile_tree = refreshed_tree;
{ }
event_handler.command_message = Err(e) => {
format!("Error handling save outcome: {}", e); error!("Failed to refresh profile tree for Admin panel: {}", e);
event_handler.command_message = format!("Error refreshing admin data: {}", e);
}
}
app_state.ui.show_admin = true;
let profile_names = app_state.profile_tree.profiles.iter()
.map(|p| p.name.clone())
.collect();
admin_state.set_profiles(profile_names);
if admin_state.current_focus == AdminFocus::default() ||
!matches!(admin_state.current_focus,
AdminFocus::InsideProfilesList |
AdminFocus::Tables | AdminFocus::InsideTablesList |
AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) {
admin_state.current_focus = AdminFocus::ProfilesPane;
}
if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() {
admin_state.profile_list_state.select(Some(0));
} }
} }
EventOutcome::ButtonSelected { context: _, index: _ } => {} AppView::AddTable => app_state.ui.show_add_table = true,
}, AppView::AddLogic => app_state.ui.show_add_logic = true,
Err(e) => { AppView::Form => app_state.ui.show_form = true,
event_handler.command_message = format!("Error: {}", e); AppView::Scratch => {}
}
}
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();
let filtered_columns = filter_user_columns(new_columns);
form_state = FormState::new(
prof_name.clone(),
tbl_name.clone(),
filtered_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();
}
prev_view_profile_name = current_view_profile;
prev_view_table_name = current_view_table;
table_just_switched = true;
}
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();
}
}
}
needs_redraw = true;
}
}
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
if app_state.ui.show_add_logic {
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()) {
info!("Fetching table structure for {}.{}", profile_name, table_name);
let fetch_message = UiService::initialize_add_logic_table_data(
&mut grpc_client,
&mut admin_state.add_logic_state,
&app_state.profile_tree,
).await.unwrap_or_else(|e| {
error!("Error initializing add_logic_table_data: {}", e);
format!("Error fetching table structure: {}", e)
});
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
info!("{}", fetch_message);
} else {
event_handler.command_message = fetch_message;
}
needs_redraw = true;
} else {
error!(
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but add_logic_state is for {}.{:?}",
profile_name, table_name,
admin_state.add_logic_state.profile_name,
admin_state.add_logic_state.selected_table_name
);
}
} else {
warn!(
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
profile_name, table_name
);
}
}
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
if app_state.ui.show_add_logic {
let profile_name = admin_state.add_logic_state.profile_name.clone();
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
Ok(columns) => {
admin_state.add_logic_state.set_columns_for_table_autocomplete(columns.clone());
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
}
Err(e) => {
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
admin_state.add_logic_state.script_editor_awaiting_column_autocomplete = None;
admin_state.add_logic_state.deactivate_script_editor_autocomplete();
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
}
}
needs_redraw = true;
} }
} }
// --- MODIFIED: Position Change Handling (operates on form_state) ---
let position_changed = form_state.current_position != position_before_event; let position_changed = form_state.current_position != position_before_event;
let mut position_logic_needs_redraw = false; let mut position_logic_needs_redraw = false;
if app_state.ui.show_form { // Only if the form is active if app_state.ui.show_form && !table_just_switched {
if position_changed && !event_handler.is_edit_mode { if position_changed && !event_handler.is_edit_mode {
// This part is okay: update cursor for the current field BEFORE loading new data
let current_input_before_load = form_state.get_current_input();
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;
// Validate new form_state.current_position if form_state.current_position > form_state.total_count {
if form_state.total_count > 0 && form_state.current_position > form_state.total_count + 1 { form_state.reset_to_empty();
form_state.current_position = form_state.total_count + 1; // Cap at new entry event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name);
} else if form_state.total_count == 0 && form_state.current_position > 1 { } else {
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
}
// Load data for the new position OR reset for new entry
if (form_state.total_count > 0 && form_state.current_position <= form_state.total_count && form_state.current_position > 0)
{
// It's an existing record position
match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
Ok(load_message) => { Ok(load_message) => {
if event_handler.command_message.is_empty() || !load_message.starts_with("Error") { if event_handler.command_message.is_empty() || !load_message.starts_with("Error") {
@@ -457,34 +452,20 @@ pub async fn run_ui() -> Result<()> {
} }
Err(e) => { Err(e) => {
event_handler.command_message = format!("Error loading data: {}", 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 {
// Position indicates a new entry (or table is empty and position is 1)
form_state.reset_to_empty(); // This sets id=0, clears values, and sets current_position correctly
event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name);
} }
// 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_after_load_str = form_state.get_current_input();
let current_input_len_after_load = current_input_after_load_str.chars().count(); let current_input_len_after_load = current_input_after_load_str.chars().count();
let max_cursor_pos = if current_input_len_after_load > 0 {
let max_cursor_pos_for_readonly_after_load = if current_input_len_after_load > 0 {
current_input_len_after_load.saturating_sub(1) current_input_len_after_load.saturating_sub(1)
} else { } else {
0 0
}; };
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
if event_handler.is_edit_mode { } else if !position_changed && !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 && app_state.ui.show_form {
// 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_str = form_state.get_current_input();
let current_input_len = current_input_str.chars().count(); let current_input_len = current_input_str.chars().count();
let max_cursor_pos = if current_input_len > 0 { let max_cursor_pos = if current_input_len > 0 {
@@ -512,8 +493,58 @@ pub async fn run_ui() -> Result<()> {
needs_redraw = true; needs_redraw = true;
} }
if should_exit { if app_state.ui.dialog.is_loading {
return Ok(()); needs_redraw = true;
}
#[cfg(feature = "ui-debug")]
{
app_state.debug_info = format!(
"Redraw -> event: {}, needs_redraw: {}, pos_changed: {}",
event_processed, needs_redraw, position_changed
);
}
if event_processed || needs_redraw || position_changed {
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
match current_mode {
AppMode::Edit => { terminal.show_cursor()?; }
AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; }
AppMode::ReadOnly => {
if !app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; }
else { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; }
terminal.show_cursor().context("Failed to show cursor in ReadOnly mode")?;
}
AppMode::General => {
if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; }
else { terminal.hide_cursor()?; }
}
AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; }
}
terminal.draw(|f| {
render_ui(
f,
&mut form_state,
&mut auth_state,
&login_state,
&register_state,
&intro_state,
&mut admin_state,
&buffer_state,
&theme,
event_handler.is_edit_mode,
&event_handler.highlight_state,
&event_handler.command_input,
event_handler.command_mode,
&event_handler.command_message,
&event_handler.navigation_state,
&app_state.current_dir,
current_fps,
&app_state,
);
}).context("Terminal draw call failed")?;
needs_redraw = false;
} }
let now = Instant::now(); let now = Instant::now();
@@ -522,5 +553,7 @@ pub async fn run_ui() -> Result<()> {
if frame_duration.as_secs_f64() > 1e-6 { if frame_duration.as_secs_f64() > 1e-6 {
current_fps = 1.0 / frame_duration.as_secs_f64(); current_fps = 1.0 / frame_duration.as_secs_f64();
} }
table_just_switched = false;
} }
} }

View File

@@ -0,0 +1,14 @@
// src/utils/columns.rs
pub fn is_system_column(column_name: &str) -> bool {
match column_name {
"id" | "deleted" | "created_at" => true,
name if name.ends_with("_id") => true,
_ => false,
}
}
pub fn filter_user_columns(all_columns: Vec<String>) -> Vec<String> {
all_columns.into_iter()
.filter(|col| !is_system_column(col))
.collect()
}

4
client/src/utils/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
// src/utils/mod.rs
pub mod columns;
pub use columns::*;

View File

@@ -45,6 +45,13 @@ fn map_field_type(field_type: &str) -> Result<&str, Status> {
.ok_or_else(|| Status::invalid_argument(format!("Invalid field type: {}", field_type))) .ok_or_else(|| Status::invalid_argument(format!("Invalid field type: {}", field_type)))
} }
fn is_invalid_table_name(table_name: &str) -> bool {
table_name.ends_with("_id") ||
table_name == "id" ||
table_name == "deleted" ||
table_name == "created_at"
}
pub async fn post_table_definition( pub async fn post_table_definition(
db_pool: &PgPool, db_pool: &PgPool,
request: PostTableDefinitionRequest, request: PostTableDefinitionRequest,
@@ -55,6 +62,13 @@ pub async fn post_table_definition(
.trim_matches('_') .trim_matches('_')
.to_lowercase(); .to_lowercase();
// New validation check
if is_invalid_table_name(&user_part_cleaned) {
return Err(Status::invalid_argument(
"Table name cannot be 'id', 'deleted', 'created_at' or end with '_id'"
));
}
if !user_part_cleaned.is_empty() && !is_valid_identifier(&user_part_cleaned) { if !user_part_cleaned.is_empty() && !is_valid_identifier(&user_part_cleaned) {
return Err(Status::invalid_argument("Invalid table name")); return Err(Status::invalid_argument("Invalid table name"));
} else if user_part_cleaned.is_empty() { } else if user_part_cleaned.is_empty() {
@@ -142,8 +156,8 @@ async fn execute_table_definition(
RETURNING id"#, RETURNING id"#,
profile.id, profile.id,
&table_name, &table_name,
json!(request.columns.iter().map(|c| c.name.clone()).collect::<Vec<_>>()), json!(columns),
json!(request.indexes.iter().map(|i| i.clone()).collect::<Vec<_>>()) json!(indexes)
) )
.fetch_one(&mut **tx) .fetch_one(&mut **tx)
.await .await

View File

@@ -56,7 +56,6 @@ pub async fn get_table_data(
let system_columns = vec![ let system_columns = vec![
("id".to_string(), "BIGINT".to_string()), ("id".to_string(), "BIGINT".to_string()),
("deleted".to_string(), "BOOLEAN".to_string()), ("deleted".to_string(), "BOOLEAN".to_string()),
("firma".to_string(), "TEXT".to_string()),
]; ];
let all_columns: Vec<(String, String)> = system_columns let all_columns: Vec<(String, String)> = system_columns
.into_iter() .into_iter()

View File

@@ -19,19 +19,11 @@ pub async fn post_table_data(
let table_name = request.table_name; let table_name = request.table_name;
let mut data = HashMap::new(); let mut data = HashMap::new();
// Process and validate all data values // CORRECTED: Process and trim all incoming data values.
// We remove the hardcoded validation. We will let the database's
// NOT NULL constraints or Steel validation scripts handle required fields.
for (key, value) in request.data { for (key, value) in request.data {
let trimmed = value.trim().to_string(); data.insert(key, value.trim().to_string());
// Handle specially - it cannot be empty
if trimmed.is_empty() {
return Err(Status::invalid_argument("Firma cannot be empty"));
}
// Add trimmed non-empty values to data map
if !trimmed.is_empty() {
data.insert(key, trimmed);
}
} }
// Lookup profile // Lookup profile

View File

@@ -19,15 +19,12 @@ pub async fn put_table_data(
let mut processed_data = HashMap::new(); let mut processed_data = HashMap::new();
let mut null_fields = Vec::new(); let mut null_fields = Vec::new();
// CORRECTED: Generic handling for all fields.
// Any field with an empty string will be added to the null_fields list.
// The special, hardcoded logic for "firma" has been removed.
for (key, value) in request.data { for (key, value) in request.data {
let trimmed = value.trim().to_string(); let trimmed = value.trim().to_string();
if trimmed.is_empty() {
if key == "firma" && trimmed.is_empty() {
return Err(Status::invalid_argument("Firma cannot be empty"));
}
// Store fields that should be set to NULL
if key != "firma" && trimmed.is_empty() {
null_fields.push(key); null_fields.push(key);
} else { } else {
processed_data.insert(key, trimmed); processed_data.insert(key, trimmed);
@@ -73,8 +70,9 @@ pub async fn put_table_data(
columns.push((name, sql_type)); columns.push((name, sql_type));
} }
// Validate system columns // CORRECTED: "firma" is not a system column.
let system_columns = ["firma", "deleted"]; // It should be treated as a user-defined column.
let system_columns = ["deleted"];
let user_columns: Vec<&String> = columns.iter().map(|(name, _)| name).collect(); let user_columns: Vec<&String> = columns.iter().map(|(name, _)| name).collect();
// Validate input columns // Validate input columns
@@ -91,9 +89,11 @@ pub async fn put_table_data(
// Add data parameters for non-empty fields // Add data parameters for non-empty fields
for (col, value) in &processed_data { for (col, value) in &processed_data {
// CORRECTED: The logic for "firma" is removed from this match.
// It will now fall through to the `else` block and have its type
// correctly looked up from the `columns` vector.
let sql_type = if system_columns.contains(&col.as_str()) { let sql_type = if system_columns.contains(&col.as_str()) {
match col.as_str() { match col.as_str() {
"firma" => "TEXT",
"deleted" => "BOOLEAN", "deleted" => "BOOLEAN",
_ => return Err(Status::invalid_argument("Invalid system column")), _ => return Err(Status::invalid_argument("Invalid system column")),
} }
@@ -121,7 +121,7 @@ pub async fn put_table_data(
let val = value.parse::<bool>() let val = value.parse::<bool>()
.map_err(|_| Status::invalid_argument(format!("Invalid boolean for {}", col)))?; .map_err(|_| Status::invalid_argument(format!("Invalid boolean for {}", col)))?;
params.add(val) params.add(val)
.map_err(|e| Status::internal(format!("Failed to add boolean parameter for {} {}", col, e)))?; .map_err(|e| Status::internal(format!("Failed to add boolean parameter for {}: {}", col, e)))?;
}, },
"TIMESTAMPTZ" => { "TIMESTAMPTZ" => {
let dt = DateTime::parse_from_rfc3339(value) let dt = DateTime::parse_from_rfc3339(value)
@@ -129,6 +129,13 @@ pub async fn put_table_data(
params.add(dt.with_timezone(&Utc)) params.add(dt.with_timezone(&Utc))
.map_err(|e| Status::internal(format!("Failed to add timestamp parameter for {}: {}", col, e)))?; .map_err(|e| Status::internal(format!("Failed to add timestamp parameter for {}: {}", col, e)))?;
}, },
// ADDED: BIGINT handling for completeness, if needed for other columns.
"BIGINT" => {
let val = value.parse::<i64>()
.map_err(|_| Status::invalid_argument(format!("Invalid integer for {}", col)))?;
params.add(val)
.map_err(|e| Status::internal(format!("Failed to add integer parameter for {}: {}", col, e)))?;
},
_ => return Err(Status::invalid_argument(format!("Unsupported type {}", sql_type))), _ => return Err(Status::invalid_argument(format!("Unsupported type {}", sql_type))),
} }