Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b5cbe854b | ||
|
|
59ed52814e | ||
|
|
3488ab4f6b | ||
|
|
6e2fc5349b | ||
|
|
ea88c2686d | ||
|
|
3df4baec92 | ||
|
|
ff74e1aaa1 | ||
|
|
b0c865ab76 | ||
|
|
3dbc086f10 | ||
|
|
e9b4b34fb4 | ||
|
|
668eeee197 | ||
|
|
799d8471c9 | ||
|
|
f77c16dec9 | ||
|
|
45026cac6a | ||
|
|
edf6ab5bca | ||
|
|
462b1f14e2 |
@@ -83,6 +83,7 @@ quit = ["q"]
|
|||||||
force_quit = ["q!"]
|
force_quit = ["q!"]
|
||||||
save_and_quit = ["wq"]
|
save_and_quit = ["wq"]
|
||||||
revert = ["r"]
|
revert = ["r"]
|
||||||
|
find_file_palette_toggle = ["ff"]
|
||||||
|
|
||||||
[editor]
|
[editor]
|
||||||
keybinding_mode = "vim" # Options: "default", "vim", "emacs"
|
keybinding_mode = "vim" # Options: "default", "vim", "emacs"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod text_editor;
|
|||||||
pub mod background;
|
pub mod background;
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
pub mod autocomplete;
|
pub mod autocomplete;
|
||||||
|
pub mod find_file_palette;
|
||||||
|
|
||||||
pub use command_line::*;
|
pub use command_line::*;
|
||||||
pub use status_line::*;
|
pub use status_line::*;
|
||||||
@@ -12,3 +13,4 @@ pub use text_editor::*;
|
|||||||
pub use background::*;
|
pub use background::*;
|
||||||
pub use dialog::*;
|
pub use dialog::*;
|
||||||
pub use autocomplete::*;
|
pub use autocomplete::*;
|
||||||
|
pub use find_file_palette::*;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// src/client/components/command_line.rs
|
// src/components/common/command_line.rs
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
style::Style,
|
style::Style,
|
||||||
@@ -6,30 +7,63 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
|
use unicode_width::UnicodeWidthStr; // Import for width calculation
|
||||||
|
|
||||||
pub fn render_command_line(f: &mut Frame, area: Rect, input: &str, active: bool, theme: &Theme, message: &str) {
|
pub fn render_command_line(
|
||||||
let prompt = if active {
|
f: &mut Frame,
|
||||||
":"
|
area: Rect,
|
||||||
} else {
|
input: &str, // This is event_handler.command_input
|
||||||
""
|
active: bool, // This is event_handler.command_mode
|
||||||
|
theme: &Theme,
|
||||||
|
message: &str, // This is event_handler.command_message
|
||||||
|
) {
|
||||||
|
// Original logic for determining display_text
|
||||||
|
let display_text = if !active {
|
||||||
|
// If not in normal command mode, but there's a message (e.g. from Find File palette closing)
|
||||||
|
// Or if command mode is off and message is empty (render minimally)
|
||||||
|
if message.is_empty() {
|
||||||
|
"".to_string() // Render an empty string, background will cover
|
||||||
|
} else {
|
||||||
|
message.to_string()
|
||||||
|
}
|
||||||
|
} else { // active is true (normal command mode)
|
||||||
|
let prompt = ":";
|
||||||
|
if message.is_empty() || message == ":" {
|
||||||
|
format!("{}{}", prompt, input)
|
||||||
|
} else {
|
||||||
|
if input.is_empty() { // If command was just executed, input is cleared, show message
|
||||||
|
message.to_string()
|
||||||
|
} else { // Show input and message
|
||||||
|
format!("{}{} | {}", prompt, input, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combine the prompt, input, and message
|
let content_width = UnicodeWidthStr::width(display_text.as_str());
|
||||||
let display_text = if message.is_empty() {
|
let available_width = area.width as usize;
|
||||||
format!("{}{}", prompt, input)
|
let padding_needed = available_width.saturating_sub(content_width);
|
||||||
|
|
||||||
|
let display_text_padded = if padding_needed > 0 {
|
||||||
|
format!("{}{}", display_text, " ".repeat(padding_needed))
|
||||||
} else {
|
} else {
|
||||||
format!("{}{} | {}", prompt, input, message)
|
// If text is too long, ratatui's Paragraph will handle truncation.
|
||||||
|
// We could also truncate here if specific behavior is needed:
|
||||||
|
// display_text.chars().take(available_width).collect::<String>()
|
||||||
|
display_text
|
||||||
};
|
};
|
||||||
|
|
||||||
let style = if active {
|
// Determine style based on active state, but apply to the whole paragraph
|
||||||
|
let text_style = if active {
|
||||||
Style::default().fg(theme.accent)
|
Style::default().fg(theme.accent)
|
||||||
} else {
|
} else {
|
||||||
|
// If not active, but there's a message, use default foreground.
|
||||||
|
// If message is also empty, this style won't matter much for empty text.
|
||||||
Style::default().fg(theme.fg)
|
Style::default().fg(theme.fg)
|
||||||
};
|
};
|
||||||
|
|
||||||
let paragraph = Paragraph::new(display_text)
|
let paragraph = Paragraph::new(display_text_padded)
|
||||||
.block(Block::default().style(Style::default().bg(theme.bg)))
|
.block(Block::default().style(Style::default().bg(theme.bg))) // Block ensures bg for whole area
|
||||||
.style(style);
|
.style(text_style); // Style for the text itself
|
||||||
|
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
}
|
}
|
||||||
|
|||||||
142
client/src/components/common/find_file_palette.rs
Normal file
142
client/src/components/common/find_file_palette.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
// src/components/common/find_file_palette.rs
|
||||||
|
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
|
use crate::modes::general::command_navigation::NavigationState; // Corrected path
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::Style,
|
||||||
|
widgets::{Block, List, ListItem, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
const PALETTE_MAX_VISIBLE_OPTIONS: usize = 15;
|
||||||
|
const PADDING_CHAR: &str = " ";
|
||||||
|
|
||||||
|
pub fn render_find_file_palette(
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
theme: &Theme,
|
||||||
|
navigation_state: &NavigationState,
|
||||||
|
) {
|
||||||
|
let palette_display_input = navigation_state.get_display_input(); // Use the new method
|
||||||
|
|
||||||
|
let num_total_filtered = navigation_state.filtered_options.len();
|
||||||
|
let current_selected_list_idx = navigation_state.selected_index;
|
||||||
|
|
||||||
|
let mut display_start_offset = 0;
|
||||||
|
if num_total_filtered > PALETTE_MAX_VISIBLE_OPTIONS {
|
||||||
|
if let Some(sel_idx) = current_selected_list_idx {
|
||||||
|
if sel_idx >= display_start_offset + PALETTE_MAX_VISIBLE_OPTIONS {
|
||||||
|
display_start_offset = sel_idx - PALETTE_MAX_VISIBLE_OPTIONS + 1;
|
||||||
|
} else if sel_idx < display_start_offset {
|
||||||
|
display_start_offset = sel_idx;
|
||||||
|
}
|
||||||
|
display_start_offset = display_start_offset
|
||||||
|
.min(num_total_filtered.saturating_sub(PALETTE_MAX_VISIBLE_OPTIONS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display_start_offset = display_start_offset.max(0);
|
||||||
|
|
||||||
|
let display_end_offset = (display_start_offset + PALETTE_MAX_VISIBLE_OPTIONS)
|
||||||
|
.min(num_total_filtered);
|
||||||
|
|
||||||
|
// navigation_state.filtered_options is Vec<(usize, String)>
|
||||||
|
// We only need the String part for display.
|
||||||
|
let visible_options_slice: Vec<&String> = if num_total_filtered > 0 {
|
||||||
|
navigation_state.filtered_options
|
||||||
|
[display_start_offset..display_end_offset]
|
||||||
|
.iter()
|
||||||
|
.map(|(_, opt_str)| opt_str)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(1), // For palette input line
|
||||||
|
Constraint::Min(0), // For options list, take remaining space
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
// Ensure list_area height does not exceed PALETTE_MAX_VISIBLE_OPTIONS
|
||||||
|
let list_area_height = std::cmp::min(chunks[1].height, PALETTE_MAX_VISIBLE_OPTIONS as u16);
|
||||||
|
let final_list_area = Rect::new(chunks[1].x, chunks[1].y, chunks[1].width, list_area_height);
|
||||||
|
|
||||||
|
|
||||||
|
let input_area = chunks[0];
|
||||||
|
// let list_area = chunks[1]; // Use final_list_area
|
||||||
|
|
||||||
|
let prompt_prefix = match navigation_state.navigation_type {
|
||||||
|
crate::modes::general::command_navigation::NavigationType::FindFile => "Find File: ",
|
||||||
|
crate::modes::general::command_navigation::NavigationType::TableTree => "Table Path: ",
|
||||||
|
};
|
||||||
|
let base_prompt_text = format!("{}{}", prompt_prefix, palette_display_input);
|
||||||
|
let prompt_text_width = UnicodeWidthStr::width(base_prompt_text.as_str());
|
||||||
|
let input_area_width = input_area.width as usize;
|
||||||
|
let input_padding_needed =
|
||||||
|
input_area_width.saturating_sub(prompt_text_width);
|
||||||
|
|
||||||
|
let padded_prompt_text = if input_padding_needed > 0 {
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
base_prompt_text,
|
||||||
|
PADDING_CHAR.repeat(input_padding_needed)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
base_prompt_text
|
||||||
|
};
|
||||||
|
|
||||||
|
let input_paragraph = Paragraph::new(padded_prompt_text)
|
||||||
|
.style(Style::default().fg(theme.accent).bg(theme.bg));
|
||||||
|
f.render_widget(input_paragraph, input_area);
|
||||||
|
|
||||||
|
let mut display_list_items: Vec<ListItem> =
|
||||||
|
Vec::with_capacity(PALETTE_MAX_VISIBLE_OPTIONS);
|
||||||
|
|
||||||
|
for (idx_in_visible_slice, opt_str) in
|
||||||
|
visible_options_slice.iter().enumerate()
|
||||||
|
{
|
||||||
|
// The selected_index in navigation_state is relative to the full filtered_options list.
|
||||||
|
// We need to check if the current item (from the visible slice) corresponds to the selected_index.
|
||||||
|
let original_filtered_idx = display_start_offset + idx_in_visible_slice;
|
||||||
|
let is_selected =
|
||||||
|
current_selected_list_idx == Some(original_filtered_idx);
|
||||||
|
|
||||||
|
let style = if is_selected {
|
||||||
|
Style::default().fg(theme.bg).bg(theme.accent)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(theme.fg).bg(theme.bg)
|
||||||
|
};
|
||||||
|
|
||||||
|
let opt_width = opt_str.width() as u16;
|
||||||
|
let list_item_width = final_list_area.width;
|
||||||
|
let padding_amount = list_item_width.saturating_sub(opt_width);
|
||||||
|
let padded_opt_str = format!(
|
||||||
|
"{}{}",
|
||||||
|
opt_str,
|
||||||
|
PADDING_CHAR.repeat(padding_amount as usize)
|
||||||
|
);
|
||||||
|
display_list_items.push(ListItem::new(padded_opt_str).style(style));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill remaining lines in the list area to maintain fixed height appearance
|
||||||
|
let num_rendered_options = display_list_items.len();
|
||||||
|
if num_rendered_options < PALETTE_MAX_VISIBLE_OPTIONS && (final_list_area.height as usize) > num_rendered_options {
|
||||||
|
for _ in num_rendered_options..(final_list_area.height as usize) {
|
||||||
|
let empty_padded_str =
|
||||||
|
PADDING_CHAR.repeat(final_list_area.width as usize);
|
||||||
|
display_list_items.push(
|
||||||
|
ListItem::new(empty_padded_str)
|
||||||
|
.style(Style::default().fg(theme.bg).bg(theme.bg)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let options_list_widget = List::new(display_list_items)
|
||||||
|
.block(Block::default().style(Style::default().bg(theme.bg)));
|
||||||
|
f.render_widget(options_list_widget, final_list_area);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// src/components/common/status_line.rs
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
style::Style,
|
style::Style,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
@@ -35,43 +36,62 @@ pub fn render_status_line(
|
|||||||
let separator = " | ";
|
let separator = " | ";
|
||||||
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;
|
let show_fps = fixed_width_with_fps <= available_width; // Use <= to show if it fits exactly
|
||||||
|
|
||||||
let remaining_width_for_dir = available_width.saturating_sub(
|
let remaining_width_for_dir = available_width.saturating_sub(
|
||||||
mode_width + separator_width + separator_width + program_info_width +
|
mode_width + separator_width + // after mode
|
||||||
if show_fps { separator_width + fps_width } else { 0 }
|
separator_width + program_info_width + // after program_info
|
||||||
|
if show_fps { separator_width + fps_width } else { 0 } // after fps
|
||||||
);
|
);
|
||||||
|
|
||||||
let dir_display_text = if UnicodeWidthStr::width(display_dir.as_str()) <= remaining_width_for_dir {
|
// Original directory display logic
|
||||||
display_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
|
||||||
} else {
|
} else {
|
||||||
let dir_name = Path::new(current_dir)
|
let dir_name = Path::new(current_dir) // Use original current_dir for path logic
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or(current_dir);
|
.unwrap_or(current_dir); // Fallback to current_dir if no filename
|
||||||
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 {
|
||||||
dir_name.chars().take(remaining_width_for_dir).collect()
|
dir_name.chars().take(remaining_width_for_dir).collect::<String>()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut spans = vec![
|
// Calculate current content width based on what will be displayed
|
||||||
|
let mut current_content_width = mode_width + separator_width +
|
||||||
|
UnicodeWidthStr::width(dir_display_text_str.as_str()) +
|
||||||
|
separator_width + program_info_width;
|
||||||
|
if show_fps {
|
||||||
|
current_content_width += separator_width + fps_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line_spans = vec![
|
||||||
Span::styled(mode_text, Style::default().fg(theme.accent)),
|
Span::styled(mode_text, Style::default().fg(theme.accent)),
|
||||||
Span::styled(" | ", Style::default().fg(theme.border)),
|
Span::styled(separator, Style::default().fg(theme.border)),
|
||||||
Span::styled(dir_display_text, Style::default().fg(theme.fg)),
|
Span::styled(dir_display_text_str.as_str(), Style::default().fg(theme.fg)),
|
||||||
Span::styled(" | ", Style::default().fg(theme.border)),
|
Span::styled(separator, Style::default().fg(theme.border)),
|
||||||
Span::styled(program_info, Style::default().fg(theme.secondary)),
|
Span::styled(program_info.as_str(), Style::default().fg(theme.secondary)),
|
||||||
];
|
];
|
||||||
|
|
||||||
if show_fps {
|
if show_fps {
|
||||||
spans.push(Span::styled(" | ", Style::default().fg(theme.border)));
|
line_spans.push(Span::styled(separator, Style::default().fg(theme.border)));
|
||||||
spans.push(Span::styled(fps_text, Style::default().fg(theme.secondary)));
|
line_spans.push(Span::styled(fps_text.as_str(), Style::default().fg(theme.secondary)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let paragraph = Paragraph::new(Line::from(spans))
|
// Calculate padding
|
||||||
|
let padding_needed = available_width.saturating_sub(current_content_width);
|
||||||
|
if padding_needed > 0 {
|
||||||
|
line_spans.push(Span::styled(
|
||||||
|
" ".repeat(padding_needed),
|
||||||
|
Style::default().bg(theme.bg), // Ensure padding uses background color
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(Line::from(line_spans))
|
||||||
.style(Style::default().bg(theme.bg));
|
.style(Style::default().bg(theme.bg));
|
||||||
|
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ use crate::components::handlers::canvas::render_canvas;
|
|||||||
pub fn render_form(
|
pub fn render_form(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
form_state: &impl CanvasState,
|
form_state_param: &impl CanvasState,
|
||||||
fields: &[&str],
|
fields: &[&str],
|
||||||
current_field: &usize,
|
current_field_idx: &usize,
|
||||||
inputs: &[&String],
|
inputs: &[&String],
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
is_edit_mode: bool,
|
is_edit_mode: bool,
|
||||||
@@ -48,7 +48,16 @@ pub fn render_form(
|
|||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// Render count/position
|
// Render count/position
|
||||||
let count_position_text = format!("Total: {} | Position: {}", total_count, current_position);
|
let count_position_text = if total_count == 0 && current_position == 1 {
|
||||||
|
"Total: 0 | New Entry".to_string()
|
||||||
|
} else if current_position > total_count && total_count > 0 {
|
||||||
|
format!("Total: {} | New Entry ({})", total_count, current_position)
|
||||||
|
} else if total_count == 0 && current_position > 1 { // Should not happen if logic is correct
|
||||||
|
format!("Total: 0 | New Entry ({})", current_position)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
format!("Total: {} | Position: {}/{}", total_count, current_position, total_count)
|
||||||
|
};
|
||||||
let count_para = Paragraph::new(count_position_text)
|
let count_para = Paragraph::new(count_position_text)
|
||||||
.style(Style::default().fg(theme.fg))
|
.style(Style::default().fg(theme.fg))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
@@ -58,9 +67,9 @@ pub fn render_form(
|
|||||||
render_canvas(
|
render_canvas(
|
||||||
f,
|
f,
|
||||||
main_layout[1],
|
main_layout[1],
|
||||||
form_state,
|
form_state_param,
|
||||||
fields,
|
fields,
|
||||||
current_field,
|
current_field_idx,
|
||||||
inputs,
|
inputs,
|
||||||
theme,
|
theme,
|
||||||
is_edit_mode,
|
is_edit_mode,
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
|
|||||||
let outcome = save(
|
let outcome = save(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let message = format!("Save successful: {:?}", outcome); // Simple message for now
|
let message = format!("Save successful: {:?}", outcome); // Simple message for now
|
||||||
@@ -40,8 +38,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
|
|||||||
revert(
|
revert(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
|
|||||||
action: &str,
|
action: &str,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
current_position: &mut u64,
|
|
||||||
total_count: u64,
|
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
match action {
|
match action {
|
||||||
"save" | "revert" => {
|
"save" | "revert" => {
|
||||||
@@ -30,8 +28,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
|
|||||||
let save_result = save(
|
let save_result = save(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await;
|
).await;
|
||||||
|
|
||||||
match save_result {
|
match save_result {
|
||||||
@@ -50,8 +46,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
|
|||||||
let revert_result = revert(
|
let revert_result = revert(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await;
|
).await;
|
||||||
|
|
||||||
match revert_result {
|
match revert_result {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ pub async fn handle_core_action(
|
|||||||
auth_client: &mut AuthClient,
|
auth_client: &mut AuthClient,
|
||||||
terminal: &mut TerminalCore,
|
terminal: &mut TerminalCore,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
current_position: &mut u64,
|
|
||||||
total_count: u64,
|
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
match action {
|
match action {
|
||||||
"save" => {
|
"save" => {
|
||||||
@@ -36,8 +34,6 @@ pub async fn handle_core_action(
|
|||||||
let save_outcome = form_save(
|
let save_outcome = form_save(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await.context("Register save action failed")?;
|
).await.context("Register save action failed")?;
|
||||||
let message = match save_outcome {
|
let message = match save_outcome {
|
||||||
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
||||||
@@ -58,8 +54,6 @@ pub async fn handle_core_action(
|
|||||||
let save_outcome = form_save(
|
let save_outcome = form_save(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
).await?;
|
||||||
match save_outcome {
|
match save_outcome {
|
||||||
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
||||||
@@ -81,8 +75,6 @@ pub async fn handle_core_action(
|
|||||||
let message = form_revert(
|
let message = form_revert(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await.context("Form revert x action failed")?;
|
).await.context("Form revert x action failed")?;
|
||||||
Ok(EventOutcome::Ok(message))
|
Ok(EventOutcome::Ok(message))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ pub async fn handle_edit_event(
|
|||||||
// TODO: Implement common actions for AddLogic if needed
|
// TODO: Implement common actions for AddLogic if needed
|
||||||
format!("Action '{}' not implemented for Add Logic in edit mode.", action)
|
format!("Action '{}' not implemented for Add Logic in edit mode.", action)
|
||||||
} else { // Assuming Form view
|
} else { // Assuming Form view
|
||||||
let outcome = form_e::execute_common_action(action, form_state, grpc_client, current_position, total_count).await?;
|
let outcome = form_e::execute_common_action(action, form_state, grpc_client).await?;
|
||||||
match outcome {
|
match outcome {
|
||||||
EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg,
|
EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg,
|
||||||
_ => format!("Unexpected outcome from common action: {:?}", outcome),
|
_ => format!("Unexpected outcome from common action: {:?}", outcome),
|
||||||
|
|||||||
@@ -119,8 +119,6 @@ async fn process_command(
|
|||||||
let outcome = save(
|
let outcome = save(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
).await?;
|
||||||
let message = match outcome {
|
let message = match outcome {
|
||||||
SaveOutcome::CreatedNew(_) => "New entry created".to_string(),
|
SaveOutcome::CreatedNew(_) => "New entry created".to_string(),
|
||||||
@@ -134,8 +132,6 @@ async fn process_command(
|
|||||||
let message = revert(
|
let message = revert(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
).await?;
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
Ok(EventOutcome::Ok(message))
|
Ok(EventOutcome::Ok(message))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
// src/client/modes/general.rs
|
// src/client/modes/general.rs
|
||||||
pub mod navigation;
|
pub mod navigation;
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
|
pub mod command_navigation;
|
||||||
|
|||||||
448
client/src/modes/general/command_navigation.rs
Normal file
448
client/src/modes/general/command_navigation.rs
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
// src/modes/general/command_navigation.rs
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
|
use anyhow::Result;
|
||||||
|
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum NavigationType {
|
||||||
|
FindFile,
|
||||||
|
TableTree,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TableDependencyGraph {
|
||||||
|
all_tables: HashSet<String>,
|
||||||
|
dependents_map: HashMap<String, Vec<String>>,
|
||||||
|
root_tables: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableDependencyGraph {
|
||||||
|
pub fn from_profile_tree(profile_tree: &ProfileTreeResponse) -> Self {
|
||||||
|
let mut dependents_map: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
let mut all_tables_set: HashSet<String> = HashSet::new();
|
||||||
|
let mut table_dependencies: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
|
||||||
|
for profile in &profile_tree.profiles {
|
||||||
|
for table in &profile.tables {
|
||||||
|
all_tables_set.insert(table.name.clone());
|
||||||
|
table_dependencies.insert(table.name.clone(), table.depends_on.clone());
|
||||||
|
|
||||||
|
for dependency_name in &table.depends_on {
|
||||||
|
dependents_map
|
||||||
|
.entry(dependency_name.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(table.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let root_tables: Vec<String> = all_tables_set
|
||||||
|
.iter()
|
||||||
|
.filter(|name| {
|
||||||
|
table_dependencies
|
||||||
|
.get(*name)
|
||||||
|
.map_or(true, |deps| deps.is_empty())
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut sorted_root_tables = root_tables;
|
||||||
|
sorted_root_tables.sort();
|
||||||
|
|
||||||
|
for dependents_list in dependents_map.values_mut() {
|
||||||
|
dependents_list.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
all_tables: all_tables_set,
|
||||||
|
dependents_map,
|
||||||
|
root_tables: sorted_root_tables,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dependent_children(&self, path: &str) -> Vec<String> {
|
||||||
|
if path.is_empty() {
|
||||||
|
return self.root_tables.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
||||||
|
if let Some(last_segment_name) = path_segments.last() {
|
||||||
|
if self.all_tables.contains(*last_segment_name) {
|
||||||
|
return self
|
||||||
|
.dependents_map
|
||||||
|
.get(*last_segment_name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NavigationState {
|
||||||
|
pub active: bool,
|
||||||
|
pub input: String,
|
||||||
|
pub selected_index: Option<usize>,
|
||||||
|
pub filtered_options: Vec<(usize, String)>,
|
||||||
|
pub navigation_type: NavigationType,
|
||||||
|
pub current_path: String,
|
||||||
|
pub graph: Option<TableDependencyGraph>,
|
||||||
|
pub all_options: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigationState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
active: false,
|
||||||
|
input: String::new(),
|
||||||
|
selected_index: None,
|
||||||
|
filtered_options: Vec::new(),
|
||||||
|
navigation_type: NavigationType::FindFile,
|
||||||
|
current_path: String::new(),
|
||||||
|
graph: None,
|
||||||
|
all_options: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_find_file(&mut self, options: Vec<String>) {
|
||||||
|
self.active = true;
|
||||||
|
self.navigation_type = NavigationType::FindFile;
|
||||||
|
self.all_options = options;
|
||||||
|
self.input.clear();
|
||||||
|
self.current_path.clear();
|
||||||
|
self.graph = None;
|
||||||
|
self.update_filtered_options(); // Initial filter with empty input
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_table_tree(&mut self, graph: TableDependencyGraph) {
|
||||||
|
self.active = true;
|
||||||
|
self.navigation_type = NavigationType::TableTree;
|
||||||
|
self.graph = Some(graph);
|
||||||
|
self.input.clear();
|
||||||
|
self.current_path.clear();
|
||||||
|
self.update_options_for_path(); // Initial options are root tables
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deactivate(&mut self) {
|
||||||
|
self.active = false;
|
||||||
|
self.input.clear();
|
||||||
|
self.all_options.clear();
|
||||||
|
self.filtered_options.clear();
|
||||||
|
self.selected_index = None;
|
||||||
|
self.current_path.clear();
|
||||||
|
self.graph = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_char(&mut self, c: char) {
|
||||||
|
match self.navigation_type {
|
||||||
|
NavigationType::FindFile => {
|
||||||
|
self.input.push(c);
|
||||||
|
self.update_filtered_options();
|
||||||
|
}
|
||||||
|
NavigationType::TableTree => {
|
||||||
|
if c == '/' {
|
||||||
|
if !self.input.is_empty() {
|
||||||
|
// Append current input to path
|
||||||
|
if self.current_path.is_empty() {
|
||||||
|
self.current_path = self.input.clone();
|
||||||
|
} else {
|
||||||
|
self.current_path.push('/');
|
||||||
|
self.current_path.push_str(&self.input);
|
||||||
|
}
|
||||||
|
self.input.clear();
|
||||||
|
self.update_options_for_path();
|
||||||
|
}
|
||||||
|
// If input is empty and char is '/', do nothing or define behavior
|
||||||
|
} else {
|
||||||
|
self.input.push(c);
|
||||||
|
self.update_filtered_options(); // Filter current level options based on input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_char(&mut self) {
|
||||||
|
match self.navigation_type {
|
||||||
|
NavigationType::FindFile => {
|
||||||
|
self.input.pop();
|
||||||
|
self.update_filtered_options();
|
||||||
|
}
|
||||||
|
NavigationType::TableTree => {
|
||||||
|
if self.input.is_empty() {
|
||||||
|
// If input is empty, try to go up in path
|
||||||
|
if !self.current_path.is_empty() {
|
||||||
|
if let Some(last_slash_idx) =
|
||||||
|
self.current_path.rfind('/')
|
||||||
|
{
|
||||||
|
// 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 {
|
||||||
|
// Path was a single segment
|
||||||
|
self.input = self.current_path.clone();
|
||||||
|
self.current_path.clear();
|
||||||
|
}
|
||||||
|
self.update_options_for_path();
|
||||||
|
// After path change, current input might match some options, so filter
|
||||||
|
self.update_filtered_options();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.input.pop();
|
||||||
|
self.update_filtered_options();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_up(&mut self) {
|
||||||
|
if self.filtered_options.is_empty() {
|
||||||
|
self.selected_index = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.selected_index = match self.selected_index {
|
||||||
|
Some(0) => Some(self.filtered_options.len() - 1),
|
||||||
|
Some(current) => Some(current - 1),
|
||||||
|
None => Some(self.filtered_options.len() - 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_down(&mut self) {
|
||||||
|
if self.filtered_options.is_empty() {
|
||||||
|
self.selected_index = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.selected_index = match self.selected_index {
|
||||||
|
Some(current) if current >= self.filtered_options.len() - 1 => {
|
||||||
|
Some(0)
|
||||||
|
}
|
||||||
|
Some(current) => Some(current + 1),
|
||||||
|
None => Some(0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_option_str(&self) -> Option<&str> {
|
||||||
|
self.selected_index
|
||||||
|
.and_then(|idx| self.filtered_options.get(idx))
|
||||||
|
.map(|(_, option_str)| option_str.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn autocomplete_selected(&mut self) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the string to display in the input line of the palette
|
||||||
|
pub fn get_display_input(&self) -> String {
|
||||||
|
match self.navigation_type {
|
||||||
|
NavigationType::FindFile => self.input.clone(),
|
||||||
|
NavigationType::TableTree => {
|
||||||
|
if self.current_path.is_empty() {
|
||||||
|
self.input.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", self.current_path, self.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the full path of the currently selected item for TableTree, or input for FindFile
|
||||||
|
pub fn get_selected_value(&self) -> Option<String> {
|
||||||
|
match self.navigation_type {
|
||||||
|
NavigationType::FindFile => {
|
||||||
|
if self.input.is_empty() { None } else { Some(self.input.clone()) }
|
||||||
|
}
|
||||||
|
NavigationType::TableTree => {
|
||||||
|
self.get_selected_option_str().map(|selected_name| {
|
||||||
|
if self.current_path.is_empty() {
|
||||||
|
selected_name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", self.current_path, selected_name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update self.all_options based on current_path (for TableTree)
|
||||||
|
fn update_options_for_path(&mut self) {
|
||||||
|
if let NavigationType::TableTree = self.navigation_type {
|
||||||
|
if let Some(graph) = &self.graph {
|
||||||
|
self.all_options =
|
||||||
|
graph.get_dependent_children(&self.current_path);
|
||||||
|
} else {
|
||||||
|
self.all_options.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For FindFile, all_options is set once at activation.
|
||||||
|
self.update_filtered_options();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update self.filtered_options based on self.all_options and self.input
|
||||||
|
fn update_filtered_options(&mut self) {
|
||||||
|
let filter_text = match self.navigation_type {
|
||||||
|
NavigationType::FindFile => &self.input,
|
||||||
|
NavigationType::TableTree => &self.input, // For TableTree, input is the current segment being typed
|
||||||
|
}
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
if filter_text.is_empty() {
|
||||||
|
self.filtered_options = self
|
||||||
|
.all_options
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, opt)| (i, opt.clone()))
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
self.filtered_options = self
|
||||||
|
.all_options
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, opt)| opt.to_lowercase().contains(&filter_text))
|
||||||
|
.map(|(i, opt)| (i, opt.clone()))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.filtered_options.is_empty() {
|
||||||
|
self.selected_index = None;
|
||||||
|
} else {
|
||||||
|
self.selected_index = Some(0); // Default to selecting the first item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_command_navigation_event(
|
||||||
|
navigation_state: &mut NavigationState,
|
||||||
|
key: KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<EventOutcome> {
|
||||||
|
if !navigation_state.active {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
navigation_state.deactivate();
|
||||||
|
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 => {
|
||||||
|
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 {
|
||||||
|
// Only attempt to navigate deeper for TableTree mode
|
||||||
|
if navigation_state.navigation_type == NavigationType::TableTree {
|
||||||
|
let path_before_nav = navigation_state.current_path.clone();
|
||||||
|
let input_before_nav = navigation_state.input.clone();
|
||||||
|
|
||||||
|
navigation_state.add_char('/');
|
||||||
|
|
||||||
|
if navigation_state.input.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 {
|
||||||
|
navigation_state.input = input_before_nav;
|
||||||
|
if navigation_state.current_path != path_before_nav {
|
||||||
|
navigation_state.current_path = path_before_nav;
|
||||||
|
}
|
||||||
|
navigation_state.update_options_for_path();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Scenario 2: Input is a partial match - autocomplete
|
||||||
|
navigation_state.autocomplete_selected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 => {
|
||||||
|
navigation_state.remove_char();
|
||||||
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
navigation_state.add_char(c);
|
||||||
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
||||||
|
match action {
|
||||||
|
"move_up" => {
|
||||||
|
navigation_state.move_up();
|
||||||
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
|
}
|
||||||
|
"move_down" => {
|
||||||
|
navigation_state.move_down();
|
||||||
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
|
}
|
||||||
|
"select" => {
|
||||||
|
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 {
|
||||||
|
Ok(EventOutcome::Ok("No selection".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(EventOutcome::Ok(String::new())),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ use crate::state::pages::admin::AdminState;
|
|||||||
use crate::state::pages::canvas_state::CanvasState;
|
use crate::state::pages::canvas_state::CanvasState;
|
||||||
use crate::ui::handlers::context::UiContext;
|
use crate::ui::handlers::context::UiContext;
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
|
use crate::modes::general::command_navigation::{handle_command_navigation_event, NavigationState};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub async fn handle_navigation_event(
|
pub async fn handle_navigation_event(
|
||||||
@@ -25,7 +26,13 @@ pub async fn handle_navigation_event(
|
|||||||
command_mode: &mut bool,
|
command_mode: &mut bool,
|
||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
|
navigation_state: &mut NavigationState,
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
|
// Handle command navigation first if active
|
||||||
|
if navigation_state.active {
|
||||||
|
return handle_command_navigation_event(navigation_state, key, config).await;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
||||||
match action {
|
match action {
|
||||||
"move_up" => {
|
"move_up" => {
|
||||||
|
|||||||
@@ -1,48 +1,50 @@
|
|||||||
// src/modes/handlers/event.rs
|
// src/modes/handlers/event.rs
|
||||||
use crossterm::event::Event;
|
|
||||||
use crossterm::cursor::SetCursorStyle;
|
|
||||||
use crate::services::grpc_client::GrpcClient;
|
|
||||||
use crate::services::auth::AuthClient;
|
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::rat_state::UiStateHandler;
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
use crate::ui::handlers::context::UiContext;
|
|
||||||
use crate::functions::common::buffer;
|
use crate::functions::common::buffer;
|
||||||
use anyhow::Result;
|
use crate::functions::modes::navigation::add_logic_nav;
|
||||||
use crate::tui::{
|
use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender;
|
||||||
terminal::core::TerminalCore,
|
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
||||||
functions::{
|
use crate::functions::modes::navigation::{add_table_nav, admin_nav};
|
||||||
common::{form::SaveOutcome, login, register},
|
use crate::modes::general::command_navigation::{
|
||||||
},
|
handle_command_navigation_event, NavigationState, TableDependencyGraph,
|
||||||
{intro, admin},
|
|
||||||
};
|
};
|
||||||
|
use crate::modes::{
|
||||||
|
canvas::{common_mode, edit, read_only},
|
||||||
|
common::{command_mode, commands::CommandHandler},
|
||||||
|
general::{dialog, navigation},
|
||||||
|
handlers::mode_manager::{AppMode, ModeManager},
|
||||||
|
};
|
||||||
|
use crate::services::auth::AuthClient;
|
||||||
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
app::{
|
app::{
|
||||||
|
buffer::{AppView, BufferState},
|
||||||
highlight::HighlightState,
|
highlight::HighlightState,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
buffer::{AppView, BufferState},
|
|
||||||
},
|
},
|
||||||
pages::{
|
pages::{
|
||||||
auth::{AuthState, LoginState, RegisterState},
|
|
||||||
admin::AdminState,
|
admin::AdminState,
|
||||||
|
auth::{AuthState, LoginState, RegisterState},
|
||||||
canvas_state::CanvasState,
|
canvas_state::CanvasState,
|
||||||
form::FormState,
|
form::FormState,
|
||||||
intro::IntroState,
|
intro::IntroState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::modes::{
|
|
||||||
common::{command_mode, commands::CommandHandler},
|
|
||||||
handlers::mode_manager::{ModeManager, AppMode},
|
|
||||||
canvas::{edit, read_only, common_mode},
|
|
||||||
general::{navigation, dialog},
|
|
||||||
};
|
|
||||||
use crate::functions::modes::navigation::{admin_nav, add_table_nav};
|
|
||||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use crate::tui::functions::common::login::LoginResult;
|
use crate::tui::functions::common::login::LoginResult;
|
||||||
use crate::tui::functions::common::register::RegisterResult;
|
use crate::tui::functions::common::register::RegisterResult;
|
||||||
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
use crate::tui::{
|
||||||
use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender;
|
functions::common::{form::SaveOutcome, login, register},
|
||||||
use crate::functions::modes::navigation::add_logic_nav;
|
terminal::core::TerminalCore,
|
||||||
|
{admin, intro},
|
||||||
|
};
|
||||||
|
use crate::ui::handlers::context::UiContext;
|
||||||
|
use crate::ui::handlers::rat_state::UiStateHandler;
|
||||||
|
use anyhow::Result;
|
||||||
|
use crossterm::cursor::SetCursorStyle;
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
use crossterm::event::{Event, KeyEvent};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum EventOutcome {
|
pub enum EventOutcome {
|
||||||
@@ -52,6 +54,15 @@ pub enum EventOutcome {
|
|||||||
ButtonSelected { context: UiContext, index: usize },
|
ButtonSelected { context: UiContext, index: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventOutcome {
|
||||||
|
pub fn get_message_if_ok(&self) -> String {
|
||||||
|
match self {
|
||||||
|
EventOutcome::Ok(msg) => msg.clone(),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EventHandler {
|
pub struct EventHandler {
|
||||||
pub command_mode: bool,
|
pub command_mode: bool,
|
||||||
pub command_input: String,
|
pub command_input: String,
|
||||||
@@ -66,6 +77,7 @@ pub struct EventHandler {
|
|||||||
pub register_result_sender: mpsc::Sender<RegisterResult>,
|
pub register_result_sender: mpsc::Sender<RegisterResult>,
|
||||||
pub save_table_result_sender: SaveTableResultSender,
|
pub save_table_result_sender: SaveTableResultSender,
|
||||||
pub save_logic_result_sender: SaveLogicResultSender,
|
pub save_logic_result_sender: SaveLogicResultSender,
|
||||||
|
pub navigation_state: NavigationState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHandler {
|
impl EventHandler {
|
||||||
@@ -83,15 +95,25 @@ impl EventHandler {
|
|||||||
highlight_state: HighlightState::Off,
|
highlight_state: HighlightState::Off,
|
||||||
edit_mode_cooldown: false,
|
edit_mode_cooldown: false,
|
||||||
ideal_cursor_column: 0,
|
ideal_cursor_column: 0,
|
||||||
key_sequence_tracker: KeySequenceTracker::new(800),
|
key_sequence_tracker: KeySequenceTracker::new(400),
|
||||||
auth_client: AuthClient::new().await?,
|
auth_client: AuthClient::new().await?,
|
||||||
login_result_sender,
|
login_result_sender,
|
||||||
register_result_sender,
|
register_result_sender,
|
||||||
save_table_result_sender,
|
save_table_result_sender,
|
||||||
save_logic_result_sender,
|
save_logic_result_sender,
|
||||||
|
navigation_state: NavigationState::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_navigation_active(&self) -> bool {
|
||||||
|
self.navigation_state.active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_find_file(&mut self, options: Vec<String>) {
|
||||||
|
self.navigation_state.activate_find_file(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn handle_event(
|
pub async fn handle_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: Event,
|
event: Event,
|
||||||
@@ -107,61 +129,104 @@ impl EventHandler {
|
|||||||
admin_state: &mut AdminState,
|
admin_state: &mut AdminState,
|
||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
total_count: u64,
|
|
||||||
current_position: &mut u64,
|
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
let 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 let Event::Key(key_event) = event {
|
||||||
|
let outcome =
|
||||||
|
handle_command_navigation_event(&mut self.navigation_state, key_event, config)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !self.navigation_state.active {
|
||||||
|
self.command_message = outcome.get_message_if_ok();
|
||||||
|
current_mode = ModeManager::derive_mode(app_state, self, admin_state);
|
||||||
|
}
|
||||||
|
app_state.update_mode(current_mode);
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
app_state.update_mode(current_mode);
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
app_state.update_mode(current_mode);
|
app_state.update_mode(current_mode);
|
||||||
|
|
||||||
let current_view = {
|
let current_view = {
|
||||||
let ui = &app_state.ui;
|
let ui = &app_state.ui;
|
||||||
if ui.show_intro { AppView::Intro }
|
if ui.show_intro {
|
||||||
else if ui.show_login { AppView::Login }
|
AppView::Intro
|
||||||
else if ui.show_register { AppView::Register }
|
} else if ui.show_login {
|
||||||
else if ui.show_admin { AppView::Admin }
|
AppView::Login
|
||||||
else if ui.show_add_logic { AppView::AddLogic }
|
} else if ui.show_register {
|
||||||
else if ui.show_add_table { AppView::AddTable }
|
AppView::Register
|
||||||
else if ui.show_form { AppView::Form } // Remove the dynamic name part
|
} else if ui.show_admin {
|
||||||
else { AppView::Scratch }
|
AppView::Admin
|
||||||
|
} 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 Some(dialog_result) = dialog::handle_dialog_event(
|
if let Event::Key(key_event) = event {
|
||||||
&event,
|
if let Some(dialog_result) = dialog::handle_dialog_event(
|
||||||
config,
|
&Event::Key(key_event),
|
||||||
app_state,
|
config,
|
||||||
login_state,
|
app_state,
|
||||||
register_state,
|
login_state,
|
||||||
buffer_state,
|
register_state,
|
||||||
admin_state,
|
buffer_state,
|
||||||
).await {
|
admin_state,
|
||||||
return dialog_result;
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return dialog_result;
|
||||||
|
}
|
||||||
|
} else if let Event::Resize(_, _) = event {
|
||||||
|
// Handle resize if needed
|
||||||
}
|
}
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Event::Key(key) = event {
|
if let Event::Key(key_event) = event {
|
||||||
let key_code = key.code;
|
let key_code = key_event.code;
|
||||||
let modifiers = key.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!("Sidebar {}",
|
let message = format!(
|
||||||
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!("Buffer {}",
|
let message = format!(
|
||||||
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" => {
|
||||||
@@ -171,17 +236,15 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
"previous_buffer" => {
|
"previous_buffer" => {
|
||||||
if buffer::switch_buffer(buffer_state, false) {
|
if buffer::switch_buffer(buffer_state, false) {
|
||||||
return Ok(EventOutcome::Ok("Switched to previous buffer".to_string()));
|
return Ok(EventOutcome::Ok(
|
||||||
|
"Switched to previous buffer".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//"close_buffer" => {
|
|
||||||
// let message = buffer_state.close_buffer_with_intro_fallback();
|
|
||||||
// return Ok(EventOutcome::Ok(message));
|
|
||||||
//}
|
|
||||||
"close_buffer" => {
|
"close_buffer" => {
|
||||||
// TODO: Replace with actual table name from server response
|
let current_table_name = Some("2025_customer");
|
||||||
let current_table_name = Some("2025_customer"); // Your hardcoded table name
|
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));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -191,11 +254,9 @@ impl EventHandler {
|
|||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
AppMode::General => {
|
AppMode::General => {
|
||||||
// Prioritize Admin Panel navigation if it's visible
|
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,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
admin_state,
|
admin_state,
|
||||||
@@ -205,13 +266,13 @@ impl EventHandler {
|
|||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- Add Logic Page Navigation ---
|
|
||||||
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,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
&mut admin_state.add_logic_state,
|
&mut admin_state.add_logic_state,
|
||||||
@@ -225,27 +286,25 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Add Table Page Navigation ---
|
|
||||||
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,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
&mut admin_state.add_table_state,
|
&mut admin_state.add_table_state,
|
||||||
client_clone,
|
client_clone,
|
||||||
sender_clone,
|
sender_clone,
|
||||||
&mut self.command_message,
|
&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,
|
key_event,
|
||||||
config,
|
config,
|
||||||
form_state,
|
form_state,
|
||||||
app_state,
|
app_state,
|
||||||
@@ -256,7 +315,10 @@ impl EventHandler {
|
|||||||
&mut self.command_mode,
|
&mut self.command_mode,
|
||||||
&mut self.command_input,
|
&mut self.command_input,
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
).await;
|
&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 {
|
||||||
@@ -269,26 +331,36 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
format!("Intro Option {} selected", index)
|
format!("Intro Option {} selected", index)
|
||||||
}
|
}
|
||||||
UiContext::Login => {
|
UiContext::Login => match index {
|
||||||
let login_action_message = match index {
|
0 => login::initiate_login(
|
||||||
0 => {
|
login_state,
|
||||||
login::initiate_login(login_state, app_state, self.auth_client.clone(), self.login_result_sender.clone())
|
app_state,
|
||||||
},
|
self.auth_client.clone(),
|
||||||
1 => login::back_to_main(login_state, app_state, buffer_state).await,
|
self.login_result_sender.clone(),
|
||||||
_ => "Invalid Login Option".to_string(),
|
),
|
||||||
};
|
1 => {
|
||||||
login_action_message
|
login::back_to_main(login_state, app_state, buffer_state)
|
||||||
}
|
.await
|
||||||
UiContext::Register => {
|
}
|
||||||
let register_action_message = match index {
|
_ => "Invalid Login Option".to_string(),
|
||||||
0 => {
|
},
|
||||||
register::initiate_registration(register_state, app_state, self.auth_client.clone(), self.register_result_sender.clone())
|
UiContext::Register => match index {
|
||||||
},
|
0 => register::initiate_registration(
|
||||||
1 => register::back_to_login(register_state, app_state, buffer_state).await,
|
register_state,
|
||||||
_ => "Invalid Login Option".to_string(),
|
app_state,
|
||||||
};
|
self.auth_client.clone(),
|
||||||
register_action_message
|
self.register_result_sender.clone(),
|
||||||
}
|
),
|
||||||
|
1 => {
|
||||||
|
register::back_to_login(
|
||||||
|
register_state,
|
||||||
|
app_state,
|
||||||
|
buffer_state,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => "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)
|
||||||
@@ -296,65 +368,84 @@ impl EventHandler {
|
|||||||
UiContext::Dialog => {
|
UiContext::Dialog => {
|
||||||
"Internal error: Unexpected dialog state".to_string()
|
"Internal error: Unexpected dialog state".to_string()
|
||||||
}
|
}
|
||||||
}; // Semicolon added here
|
};
|
||||||
return Ok(EventOutcome::Ok(message));
|
return Ok(EventOutcome::Ok(message));
|
||||||
}
|
}
|
||||||
other => return other,
|
other => return other,
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
AppMode::ReadOnly => {
|
AppMode::ReadOnly => {
|
||||||
// Check for Linewise highlight first
|
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")
|
== Some("enter_highlight_mode_linewise")
|
||||||
&& ModeManager::can_enter_highlight_mode(current_mode)
|
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||||
{
|
{
|
||||||
let current_field_index = if app_state.ui.show_login { login_state.current_field() }
|
let current_field_index = if app_state.ui.show_login {
|
||||||
else if app_state.ui.show_register { register_state.current_field() }
|
login_state.current_field()
|
||||||
else { form_state.current_field() };
|
} else if app_state.ui.show_register {
|
||||||
self.highlight_state = HighlightState::Linewise { anchor_line: current_field_index };
|
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)
|
||||||
// Check for Character-wise highlight
|
== Some("enter_highlight_mode")
|
||||||
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode")
|
|
||||||
&& ModeManager::can_enter_highlight_mode(current_mode)
|
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||||
{
|
{
|
||||||
let current_field_index = if app_state.ui.show_login { login_state.current_field() }
|
let current_field_index = if app_state.ui.show_login {
|
||||||
else if app_state.ui.show_register { register_state.current_field() }
|
login_state.current_field()
|
||||||
else { form_state.current_field() };
|
} else if app_state.ui.show_register {
|
||||||
let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() }
|
register_state.current_field()
|
||||||
else if app_state.ui.show_register { register_state.current_cursor_pos() }
|
} else {
|
||||||
else { form_state.current_cursor_pos() };
|
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
|
||||||
// Check for entering edit mode (before cursor)
|
.get_read_only_action_for_key(key_code, modifiers)
|
||||||
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_before")
|
.as_deref()
|
||||||
&& ModeManager::can_enter_edit_mode(current_mode) {
|
== 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
|
||||||
// Check for entering edit mode (after cursor)
|
.get_read_only_action_for_key(key_code, modifiers)
|
||||||
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after")
|
.as_deref()
|
||||||
&& ModeManager::can_enter_edit_mode(current_mode) {
|
== Some("enter_edit_mode_after")
|
||||||
let current_input = if app_state.ui.show_login || app_state.ui.show_register{
|
&& 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()
|
login_state.get_current_input()
|
||||||
} else {
|
} else {
|
||||||
form_state.get_current_input()
|
form_state.get_current_input()
|
||||||
};
|
};
|
||||||
let current_cursor_pos = if app_state.ui.show_login || app_state.ui.show_register{
|
let current_cursor_pos =
|
||||||
login_state.current_cursor_pos()
|
if app_state.ui.show_login || app_state.ui.show_register {
|
||||||
} else {
|
login_state.current_cursor_pos()
|
||||||
form_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);
|
||||||
self.ideal_cursor_column = login_state.current_cursor_pos();
|
self.ideal_cursor_column = login_state.current_cursor_pos();
|
||||||
} else {
|
} else {
|
||||||
@@ -368,17 +459,16 @@ 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)
|
||||||
// Check for entering command mode
|
== Some("enter_command_mode")
|
||||||
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_command_mode")
|
&& ModeManager::can_enter_command_mode(current_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();
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for common actions (save, quit, etc.) only if no mode change happened
|
|
||||||
if let Some(action) = config.get_common_action(key_code, modifiers) {
|
if let Some(action) = config.get_common_action(key_code, modifiers) {
|
||||||
match action {
|
match action {
|
||||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
@@ -392,18 +482,20 @@ impl EventHandler {
|
|||||||
&mut self.auth_client,
|
&mut self.auth_client,
|
||||||
terminal,
|
terminal,
|
||||||
app_state,
|
app_state,
|
||||||
current_position,
|
)
|
||||||
total_count,
|
.await;
|
||||||
).await;
|
}
|
||||||
},
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no mode change or specific common action handled, delegate to read_only handler
|
// 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,
|
key_event,
|
||||||
config,
|
config,
|
||||||
form_state,
|
form_state,
|
||||||
login_state,
|
login_state,
|
||||||
@@ -411,47 +503,56 @@ impl EventHandler {
|
|||||||
&mut admin_state.add_table_state,
|
&mut admin_state.add_table_state,
|
||||||
&mut admin_state.add_logic_state,
|
&mut admin_state.add_logic_state,
|
||||||
&mut self.key_sequence_tracker,
|
&mut self.key_sequence_tracker,
|
||||||
current_position,
|
&mut current_position,
|
||||||
total_count,
|
total_count,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
&mut self.edit_mode_cooldown,
|
&mut self.edit_mode_cooldown,
|
||||||
&mut self.ideal_cursor_column,
|
&mut self.ideal_cursor_column,
|
||||||
).await?;
|
)
|
||||||
// Note: handle_read_only_event should ignore mode entry keys internally now
|
.await?;
|
||||||
return Ok(EventOutcome::Ok(message));
|
return Ok(EventOutcome::Ok(message));
|
||||||
}, // End AppMode::ReadOnly
|
}
|
||||||
|
|
||||||
AppMode::Highlight => {
|
AppMode::Highlight => {
|
||||||
// --- Handle Highlight Mode Specific Keys ---
|
if config.get_highlight_action_for_key(key_code, modifiers)
|
||||||
// 1. Check for Exit first
|
== Some("exit_highlight_mode")
|
||||||
if config.get_highlight_action_for_key(key_code, modifiers) == 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)
|
||||||
// 2. Check for Switch to Linewise
|
== Some("enter_highlight_mode_linewise")
|
||||||
else if config.get_highlight_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise") {
|
{
|
||||||
// Only switch if currently characterwise
|
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();
|
};
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
self.command_message = "-- LINE HIGHLIGHT --".to_string();
|
||||||
}
|
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, key, config, form_state, login_state,
|
app_state,
|
||||||
register_state,
|
key_event,
|
||||||
&mut admin_state.add_table_state,
|
config,
|
||||||
|
form_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
&mut admin_state.add_table_state,
|
||||||
&mut admin_state.add_logic_state,
|
&mut admin_state.add_logic_state,
|
||||||
&mut self.key_sequence_tracker,
|
&mut self.key_sequence_tracker,
|
||||||
current_position,
|
&mut current_position,
|
||||||
total_count,
|
total_count,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
&mut self.edit_mode_cooldown,
|
&mut self.edit_mode_cooldown,
|
||||||
&mut self.ideal_cursor_column,
|
&mut self.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
@@ -460,14 +561,9 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppMode::Edit => {
|
AppMode::Edit => {
|
||||||
// First, check for common actions (save, revert, etc.) that apply in Edit mode
|
|
||||||
// These might take precedence or have different behavior than the edit handler
|
|
||||||
if let Some(action) = config.get_common_action(key_code, modifiers) {
|
if let Some(action) = config.get_common_action(key_code, modifiers) {
|
||||||
// Handle common actions like save, revert, force_quit, save_and_quit
|
|
||||||
// Ensure these actions return EventOutcome directly if they might exit the app
|
|
||||||
match action {
|
match action {
|
||||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
// This call likely returns EventOutcome, handle it directly
|
|
||||||
return common_mode::handle_core_action(
|
return common_mode::handle_core_action(
|
||||||
action,
|
action,
|
||||||
form_state,
|
form_state,
|
||||||
@@ -478,106 +574,207 @@ impl EventHandler {
|
|||||||
&mut self.auth_client,
|
&mut self.auth_client,
|
||||||
terminal,
|
terminal,
|
||||||
app_state,
|
app_state,
|
||||||
current_position,
|
)
|
||||||
total_count,
|
.await;
|
||||||
).await;
|
}
|
||||||
},
|
|
||||||
// Handle other common actions if necessary
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
// If a common action was handled but didn't return/exit,
|
|
||||||
// we might want to stop further processing for this key event.
|
|
||||||
// Depending on the action, you might return Ok(EventOutcome::Ok(...)) here.
|
|
||||||
// For now, assume common actions either exit or don't prevent further processing.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no common action took precedence, delegate to the edit-specific handler
|
// Extracting values to avoid borrow conflicts
|
||||||
|
let mut current_position = form_state.current_position;
|
||||||
|
let total_count = form_state.total_count;
|
||||||
|
|
||||||
let edit_result = edit::handle_edit_event(
|
let edit_result = edit::handle_edit_event(
|
||||||
key,
|
key_event,
|
||||||
config,
|
config,
|
||||||
form_state,
|
form_state,
|
||||||
login_state,
|
login_state,
|
||||||
register_state,
|
register_state,
|
||||||
admin_state,
|
admin_state,
|
||||||
&mut self.ideal_cursor_column,
|
&mut self.ideal_cursor_column,
|
||||||
current_position,
|
&mut current_position,
|
||||||
total_count,
|
total_count,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
app_state,
|
app_state,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
match edit_result {
|
match edit_result {
|
||||||
Ok(edit::EditEventOutcome::ExitEditMode) => {
|
Ok(edit::EditEventOutcome::ExitEditMode) => {
|
||||||
// The edit handler signaled to exit the mode
|
|
||||||
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 { login_state.has_unsaved_changes() }
|
let has_changes = if app_state.ui.show_login {
|
||||||
else if app_state.ui.show_register { register_state.has_unsaved_changes() }
|
login_state.has_unsaved_changes()
|
||||||
else { form_state.has_unsaved_changes() };
|
} else if app_state.ui.show_register {
|
||||||
|
register_state.has_unsaved_changes()
|
||||||
|
} else {
|
||||||
|
form_state.has_unsaved_changes()
|
||||||
|
};
|
||||||
self.command_message = if has_changes {
|
self.command_message = if has_changes {
|
||||||
"Exited edit mode (unsaved changes remain)".to_string()
|
"Exited edit mode (unsaved changes remain)".to_string()
|
||||||
} else {
|
} else {
|
||||||
"Read-only mode".to_string()
|
"Read-only mode".to_string()
|
||||||
};
|
};
|
||||||
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||||
// Adjust cursor position if needed
|
let current_input = if app_state.ui.show_login {
|
||||||
let current_input = if app_state.ui.show_login { login_state.get_current_input() }
|
login_state.get_current_input()
|
||||||
else if app_state.ui.show_register { register_state.get_current_input() }
|
} else if app_state.ui.show_register {
|
||||||
else { form_state.get_current_input() };
|
register_state.get_current_input()
|
||||||
let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() }
|
} else {
|
||||||
else if app_state.ui.show_register { register_state.current_cursor_pos() }
|
form_state.get_current_input()
|
||||||
else { form_state.current_cursor_pos() };
|
};
|
||||||
if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
|
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 { login_state } else if app_state.ui.show_register { register_state } else { form_state };
|
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
|
||||||
|
};
|
||||||
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)) => {
|
||||||
// Stay in edit mode, update message if not empty
|
|
||||||
if !msg.is_empty() {
|
if !msg.is_empty() {
|
||||||
self.command_message = msg;
|
self.command_message = msg;
|
||||||
}
|
}
|
||||||
self.key_sequence_tracker.reset(); // Reset sequence tracker on successful edit action
|
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) => {
|
||||||
// Handle error from the edit handler
|
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, // End AppMode::Edit
|
}
|
||||||
|
|
||||||
AppMode::Command => {
|
AppMode::Command => {
|
||||||
let outcome = command_mode::handle_command_event(
|
if config.is_exit_command_mode(key_code, modifiers) {
|
||||||
key,
|
self.command_input.clear();
|
||||||
config,
|
self.command_message.clear();
|
||||||
app_state,
|
self.command_mode = false;
|
||||||
login_state,
|
self.key_sequence_tracker.reset();
|
||||||
register_state,
|
return Ok(EventOutcome::Ok("Exited command mode".to_string()));
|
||||||
form_state,
|
|
||||||
&mut self.command_input,
|
|
||||||
&mut self.command_message,
|
|
||||||
grpc_client,
|
|
||||||
command_handler,
|
|
||||||
terminal,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
if let EventOutcome::Ok(msg) = &outcome {
|
|
||||||
if msg == "Exited command mode" {
|
|
||||||
self.command_mode = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Ok(outcome);
|
|
||||||
|
if config.is_command_execute(key_code, modifiers) {
|
||||||
|
|
||||||
|
// Extracting values to avoid borrow conflicts
|
||||||
|
let mut current_position = form_state.current_position;
|
||||||
|
let total_count = form_state.total_count;
|
||||||
|
|
||||||
|
let outcome = command_mode::handle_command_event(
|
||||||
|
key_event,
|
||||||
|
config,
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state,
|
||||||
|
&mut self.command_input,
|
||||||
|
&mut self.command_message,
|
||||||
|
grpc_client,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
&mut current_position,
|
||||||
|
total_count,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Update form_state with potentially changed position
|
||||||
|
form_state.current_position = current_position;
|
||||||
|
|
||||||
|
self.command_mode = false;
|
||||||
|
self.key_sequence_tracker.reset();
|
||||||
|
let new_mode = ModeManager::derive_mode(app_state, self, admin_state);
|
||||||
|
app_state.update_mode(new_mode);
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
if key_code == KeyCode::Backspace {
|
||||||
|
self.command_input.pop();
|
||||||
|
self.key_sequence_tracker.reset();
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let KeyCode::Char(c) = key_code {
|
||||||
|
if c == 'f' {
|
||||||
|
// Assuming 'f' is part of the sequence, e.g. ":f" or " f"
|
||||||
|
self.key_sequence_tracker.add_key(key_code);
|
||||||
|
let sequence = self.key_sequence_tracker.get_sequence();
|
||||||
|
|
||||||
|
if config.matches_key_sequence_generalized(&sequence)
|
||||||
|
== Some("find_file_palette_toggle")
|
||||||
|
{
|
||||||
|
if app_state.ui.show_form || app_state.ui.show_intro {
|
||||||
|
// Build table graph from profile data
|
||||||
|
let graph = TableDependencyGraph::from_profile_tree(
|
||||||
|
&app_state.profile_tree,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Activate navigation with graph
|
||||||
|
self.navigation_state.activate_table_tree(graph);
|
||||||
|
|
||||||
|
self.command_mode = false; // Exit command mode
|
||||||
|
self.command_input.clear();
|
||||||
|
// Message is set by render_find_file_palette's prompt_prefix
|
||||||
|
self.command_message.clear(); // Clear old command message
|
||||||
|
self.key_sequence_tracker.reset();
|
||||||
|
// ModeManager will derive AppMode::General due to navigation_state.active
|
||||||
|
// app_state.update_mode(AppMode::General); // This will be handled by ModeManager
|
||||||
|
return Ok(EventOutcome::Ok(
|
||||||
|
"Table tree palette activated".to_string(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.key_sequence_tracker.reset();
|
||||||
|
self.command_input.push('f');
|
||||||
|
if sequence.len() > 1 && sequence[0] == KeyCode::Char('f') {
|
||||||
|
self.command_input.push('f');
|
||||||
|
}
|
||||||
|
self.command_message =
|
||||||
|
"Find File not available in this view.".to_string();
|
||||||
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.is_key_sequence_prefix(&sequence) {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c != 'f' && !self.key_sequence_tracker.current_sequence.is_empty() {
|
||||||
|
self.key_sequence_tracker.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_input.push(c);
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.key_sequence_tracker.reset();
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if let Event::Resize(_, _) = event {
|
||||||
|
return Ok(EventOutcome::Ok("Resized".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.edit_mode_cooldown = false;
|
self.edit_mode_cooldown = false;
|
||||||
Ok(EventOutcome::Ok(self.command_message.clone()))
|
Ok(EventOutcome::Ok(self.command_message.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_processed_command(&self, command: &str) -> bool {
|
||||||
|
matches!(command, "w" | "q" | "q!" | "wq" | "r")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ impl ModeManager {
|
|||||||
event_handler: &EventHandler,
|
event_handler: &EventHandler,
|
||||||
admin_state: &AdminState,
|
admin_state: &AdminState,
|
||||||
) -> AppMode {
|
) -> AppMode {
|
||||||
|
if event_handler.navigation_state.active {
|
||||||
|
return AppMode::General;
|
||||||
|
}
|
||||||
|
|
||||||
if event_handler.command_mode {
|
if event_handler.command_mode {
|
||||||
return AppMode::Command;
|
return AppMode::Command;
|
||||||
}
|
}
|
||||||
@@ -78,14 +82,14 @@ impl ModeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mode transition rules
|
// Mode transition rules
|
||||||
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
|
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
|
||||||
!matches!(current_mode, AppMode::Edit) // Can't enter from Edit mode
|
!matches!(current_mode, AppMode::Edit)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
|
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
|
||||||
matches!(current_mode, AppMode::ReadOnly) // Only from ReadOnly
|
matches!(current_mode, AppMode::ReadOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
|
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
|
||||||
matches!(current_mode, AppMode::Edit | AppMode::Command | AppMode::Highlight)
|
matches!(current_mode, AppMode::Edit | AppMode::Command | AppMode::Highlight)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,200 @@
|
|||||||
// src/services/grpc_client.rs
|
// src/services/grpc_client.rs
|
||||||
|
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
use common::proto::multieko2::adresar::adresar_client::AdresarClient;
|
use common::proto::multieko2::common::{CountResponse, Empty};
|
||||||
use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, PutAdresarRequest};
|
|
||||||
use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty};
|
|
||||||
use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient;
|
use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient;
|
||||||
// Import the new request type for table structure
|
use common::proto::multieko2::table_structure::{GetTableStructureRequest, TableStructureResponse};
|
||||||
use common::proto::multieko2::table_structure::{TableStructureResponse, GetTableStructureRequest};
|
|
||||||
use common::proto::multieko2::table_definition::{
|
use common::proto::multieko2::table_definition::{
|
||||||
table_definition_client::TableDefinitionClient,
|
table_definition_client::TableDefinitionClient,
|
||||||
ProfileTreeResponse, PostTableDefinitionRequest, TableDefinitionResponse,
|
PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse,
|
||||||
};
|
};
|
||||||
use common::proto::multieko2::table_script::{
|
use common::proto::multieko2::table_script::{
|
||||||
table_script_client::TableScriptClient,
|
table_script_client::TableScriptClient,
|
||||||
PostTableScriptRequest, TableScriptResponse,
|
PostTableScriptRequest, TableScriptResponse,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use common::proto::multieko2::tables_data::{
|
||||||
|
tables_data_client::TablesDataClient,
|
||||||
|
GetTableDataByPositionRequest,
|
||||||
|
GetTableDataResponse,
|
||||||
|
GetTableDataCountRequest,
|
||||||
|
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
||||||
|
PutTableDataResponse,
|
||||||
|
};
|
||||||
|
use anyhow::{Context, Result}; // Added Context
|
||||||
|
use std::collections::HashMap; // NEW
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GrpcClient {
|
pub struct GrpcClient {
|
||||||
adresar_client: AdresarClient<Channel>,
|
|
||||||
table_structure_client: TableStructureServiceClient<Channel>,
|
table_structure_client: TableStructureServiceClient<Channel>,
|
||||||
table_definition_client: TableDefinitionClient<Channel>,
|
table_definition_client: TableDefinitionClient<Channel>,
|
||||||
table_script_client: TableScriptClient<Channel>,
|
table_script_client: TableScriptClient<Channel>,
|
||||||
|
tables_data_client: TablesDataClient<Channel>, // NEW
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GrpcClient {
|
impl GrpcClient {
|
||||||
pub async fn new() -> Result<Self> {
|
pub async fn new() -> Result<Self> {
|
||||||
let adresar_client = AdresarClient::connect("http://[::1]:50051").await?;
|
let table_structure_client = TableStructureServiceClient::connect(
|
||||||
let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?;
|
"http://[::1]:50051",
|
||||||
let table_definition_client = TableDefinitionClient::connect("http://[::1]:50051").await?;
|
)
|
||||||
let table_script_client = TableScriptClient::connect("http://[::1]:50051").await?;
|
.await
|
||||||
|
.context("Failed to connect to TableStructureService")?;
|
||||||
|
let table_definition_client = TableDefinitionClient::connect(
|
||||||
|
"http://[::1]:50051",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to connect to TableDefinitionService")?;
|
||||||
|
let table_script_client =
|
||||||
|
TableScriptClient::connect("http://[::1]:50051")
|
||||||
|
.await
|
||||||
|
.context("Failed to connect to TableScriptService")?;
|
||||||
|
let tables_data_client =
|
||||||
|
TablesDataClient::connect("http://[::1]:50051")
|
||||||
|
.await
|
||||||
|
.context("Failed to connect to TablesDataService")?; // NEW
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
adresar_client,
|
// adresar_client, // REMOVE
|
||||||
table_structure_client,
|
table_structure_client,
|
||||||
table_definition_client,
|
table_definition_client,
|
||||||
table_script_client,
|
table_script_client,
|
||||||
|
tables_data_client, // NEW
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_adresar_count(&mut self) -> Result<u64> {
|
|
||||||
let request = tonic::Request::new(Empty::default());
|
|
||||||
let response: CountResponse = self.adresar_client.get_adresar_count(request).await?.into_inner();
|
|
||||||
Ok(response.count as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_adresar_by_position(&mut self, position: u64) -> Result<AdresarResponse> {
|
|
||||||
let request = tonic::Request::new(PositionRequest { position: position as i64 });
|
|
||||||
let response: AdresarResponse = self.adresar_client.get_adresar_by_position(request).await?.into_inner();
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn post_adresar(&mut self, request: PostAdresarRequest) -> Result<tonic::Response<AdresarResponse>> {
|
|
||||||
let request = tonic::Request::new(request);
|
|
||||||
let response = self.adresar_client.post_adresar(request).await?;
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn put_adresar(&mut self, request: PutAdresarRequest) -> Result<tonic::Response<AdresarResponse>> {
|
|
||||||
let request = tonic::Request::new(request);
|
|
||||||
let response = self.adresar_client.put_adresar(request).await?;
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updated get_table_structure method
|
|
||||||
pub async fn get_table_structure(
|
pub async fn get_table_structure(
|
||||||
&mut self,
|
&mut self,
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
table_name: String,
|
table_name: String,
|
||||||
) -> Result<TableStructureResponse> {
|
) -> Result<TableStructureResponse> {
|
||||||
// Create the new request type
|
|
||||||
let grpc_request = GetTableStructureRequest {
|
let grpc_request = GetTableStructureRequest {
|
||||||
profile_name,
|
profile_name,
|
||||||
table_name,
|
table_name,
|
||||||
};
|
};
|
||||||
let request = tonic::Request::new(grpc_request);
|
let request = tonic::Request::new(grpc_request);
|
||||||
// Call the new gRPC method
|
let response = self
|
||||||
let response = self.table_structure_client.get_table_structure(request).await?;
|
.table_structure_client
|
||||||
|
.get_table_structure(request)
|
||||||
|
.await
|
||||||
|
.context("gRPC GetTableStructure call failed")?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_profile_tree(&mut self) -> Result<ProfileTreeResponse> {
|
pub async fn get_profile_tree(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<ProfileTreeResponse> {
|
||||||
let request = tonic::Request::new(Empty::default());
|
let request = tonic::Request::new(Empty::default());
|
||||||
let response = self.table_definition_client.get_profile_tree(request).await?;
|
let response = self
|
||||||
|
.table_definition_client
|
||||||
|
.get_profile_tree(request)
|
||||||
|
.await
|
||||||
|
.context("gRPC GetProfileTree call failed")?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_table_definition(&mut self, request: PostTableDefinitionRequest) -> Result<TableDefinitionResponse> {
|
pub async fn post_table_definition(
|
||||||
|
&mut self,
|
||||||
|
request: PostTableDefinitionRequest,
|
||||||
|
) -> Result<TableDefinitionResponse> {
|
||||||
let tonic_request = tonic::Request::new(request);
|
let tonic_request = tonic::Request::new(request);
|
||||||
let response = self.table_definition_client.post_table_definition(tonic_request).await?;
|
let response = self
|
||||||
|
.table_definition_client
|
||||||
|
.post_table_definition(tonic_request)
|
||||||
|
.await
|
||||||
|
.context("gRPC PostTableDefinition call failed")?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_table_script(&mut self, request: PostTableScriptRequest) -> Result<TableScriptResponse> {
|
pub async fn post_table_script(
|
||||||
|
&mut self,
|
||||||
|
request: PostTableScriptRequest,
|
||||||
|
) -> Result<TableScriptResponse> {
|
||||||
let tonic_request = tonic::Request::new(request);
|
let tonic_request = tonic::Request::new(request);
|
||||||
let response = self.table_script_client.post_table_script(tonic_request).await?;
|
let response = self
|
||||||
|
.table_script_client
|
||||||
|
.post_table_script(tonic_request)
|
||||||
|
.await
|
||||||
|
.context("gRPC PostTableScript call failed")?;
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW Methods for TablesData service
|
||||||
|
pub async fn get_table_data_count(
|
||||||
|
&mut self,
|
||||||
|
profile_name: String,
|
||||||
|
table_name: String,
|
||||||
|
) -> Result<u64> {
|
||||||
|
let grpc_request = GetTableDataCountRequest {
|
||||||
|
profile_name,
|
||||||
|
table_name,
|
||||||
|
};
|
||||||
|
let request = tonic::Request::new(grpc_request);
|
||||||
|
let response = self
|
||||||
|
.tables_data_client
|
||||||
|
.get_table_data_count(request)
|
||||||
|
.await
|
||||||
|
.context("gRPC GetTableDataCount call failed")?;
|
||||||
|
Ok(response.into_inner().count as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_table_data_by_position(
|
||||||
|
&mut self,
|
||||||
|
profile_name: String,
|
||||||
|
table_name: String,
|
||||||
|
position: i32,
|
||||||
|
) -> Result<GetTableDataResponse> {
|
||||||
|
let grpc_request = GetTableDataByPositionRequest {
|
||||||
|
profile_name,
|
||||||
|
table_name,
|
||||||
|
position,
|
||||||
|
};
|
||||||
|
let request = tonic::Request::new(grpc_request);
|
||||||
|
let response = self
|
||||||
|
.tables_data_client
|
||||||
|
.get_table_data_by_position(request)
|
||||||
|
.await
|
||||||
|
.context("gRPC GetTableDataByPosition call failed")?;
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_table_data(
|
||||||
|
&mut self,
|
||||||
|
profile_name: String,
|
||||||
|
table_name: String,
|
||||||
|
data: HashMap<String, String>,
|
||||||
|
) -> Result<PostTableDataResponse> {
|
||||||
|
let grpc_request = PostTableDataRequest {
|
||||||
|
profile_name,
|
||||||
|
table_name,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
let request = tonic::Request::new(grpc_request);
|
||||||
|
let response = self
|
||||||
|
.tables_data_client
|
||||||
|
.post_table_data(request)
|
||||||
|
.await
|
||||||
|
.context("gRPC PostTableData call failed")?;
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn put_table_data(
|
||||||
|
&mut self,
|
||||||
|
profile_name: String,
|
||||||
|
table_name: String,
|
||||||
|
id: i64,
|
||||||
|
data: HashMap<String, String>,
|
||||||
|
) -> Result<PutTableDataResponse> {
|
||||||
|
let grpc_request = PutTableDataRequest {
|
||||||
|
profile_name,
|
||||||
|
table_name,
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
let request = tonic::Request::new(grpc_request);
|
||||||
|
let response = self
|
||||||
|
.tables_data_client
|
||||||
|
.put_table_data(request)
|
||||||
|
.await
|
||||||
|
.context("gRPC PutTableData call failed")?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,110 +90,173 @@ impl UiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize_app_state(
|
// MODIFIED: To set initial view table in AppState and return initial column names
|
||||||
|
pub async fn initialize_app_state_and_form(
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
) -> Result<Vec<String>> {
|
// Returns (initial_profile, initial_table, initial_columns)
|
||||||
// Fetch profile tree
|
) -> Result<(String, String, Vec<String>)> {
|
||||||
let profile_tree = grpc_client.get_profile_tree().await.context("Failed to get profile tree")?;
|
let profile_tree = grpc_client
|
||||||
|
.get_profile_tree()
|
||||||
|
.await
|
||||||
|
.context("Failed to get profile tree")?;
|
||||||
app_state.profile_tree = profile_tree;
|
app_state.profile_tree = profile_tree;
|
||||||
|
|
||||||
// TODO for general tables and not hardcoded
|
// Determine initial table to load (e.g., first table of first profile, or a default)
|
||||||
let default_profile_name = "default".to_string();
|
// For now, let's hardcode a default for simplicity, but this should be more dynamic
|
||||||
let default_table_name = "2025_customer".to_string();
|
let initial_profile_name = app_state
|
||||||
|
.profile_tree
|
||||||
|
.profiles
|
||||||
|
.first()
|
||||||
|
.map(|p| p.name.clone())
|
||||||
|
.unwrap_or_else(|| "default".to_string());
|
||||||
|
|
||||||
|
let initial_table_name = app_state
|
||||||
|
.profile_tree
|
||||||
|
.profiles
|
||||||
|
.first()
|
||||||
|
.and_then(|p| p.tables.first().map(|t| t.name.clone()))
|
||||||
|
.unwrap_or_else(|| "2025_company_data1".to_string()); // Fallback if no tables
|
||||||
|
|
||||||
|
app_state.set_current_view_table(
|
||||||
|
initial_profile_name.clone(),
|
||||||
|
initial_table_name.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch table structure for the default table
|
|
||||||
let table_structure = grpc_client
|
let table_structure = grpc_client
|
||||||
.get_table_structure(default_profile_name, default_table_name)
|
.get_table_structure(
|
||||||
|
initial_profile_name.clone(),
|
||||||
|
initial_table_name.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.context("Failed to get initial table structure")?;
|
.context(format!(
|
||||||
|
"Failed to get initial table structure for {}.{}",
|
||||||
|
initial_profile_name, initial_table_name
|
||||||
|
))?;
|
||||||
|
|
||||||
// Extract the column names from the response
|
|
||||||
let column_names: Vec<String> = table_structure
|
let column_names: Vec<String> = table_structure
|
||||||
.columns
|
.columns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|col| col.name.clone())
|
.map(|col| col.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(column_names)
|
Ok((initial_profile_name, initial_table_name, column_names))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize_adresar_count(
|
// NEW: Fetches and sets count for the current table in FormState
|
||||||
|
pub async fn fetch_and_set_table_count(
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
app_state: &mut AppState,
|
|
||||||
) -> Result<()> {
|
|
||||||
let total_count = grpc_client.get_adresar_count().await.context("Failed to get adresar count")?;
|
|
||||||
app_state.update_total_count(total_count);
|
|
||||||
app_state.update_current_position(total_count.saturating_add(1)); // Start in new entry mode
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_adresar_count(
|
|
||||||
grpc_client: &mut GrpcClient,
|
|
||||||
app_state: &mut AppState,
|
|
||||||
) -> Result<()> {
|
|
||||||
let total_count = grpc_client.get_adresar_count().await.context("Failed to get adresar by position")?;
|
|
||||||
app_state.update_total_count(total_count);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load_adresar_by_position(
|
|
||||||
grpc_client: &mut GrpcClient,
|
|
||||||
_app_state: &mut AppState,
|
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
position: u64,
|
) -> Result<()> {
|
||||||
|
let total_count = grpc_client
|
||||||
|
.get_table_data_count(
|
||||||
|
form_state.profile_name.clone(),
|
||||||
|
form_state.table_name.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context(format!(
|
||||||
|
"Failed to get count for table {}.{}",
|
||||||
|
form_state.profile_name, form_state.table_name
|
||||||
|
))?;
|
||||||
|
form_state.total_count = total_count;
|
||||||
|
|
||||||
|
// Set initial position: if table has items, point to first, else point to new entry
|
||||||
|
if total_count > 0 {
|
||||||
|
form_state.current_position = 1;
|
||||||
|
} else {
|
||||||
|
form_state.current_position = 1; // For a new entry in an empty table
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MODIFIED: Generic table data loading
|
||||||
|
pub async fn load_table_data_by_position(
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
form_state: &mut FormState, // Takes &mut FormState to update it
|
||||||
|
// position is now read from form_state.current_position
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
match grpc_client.get_adresar_by_position(position).await {
|
// Ensure current_position is valid before fetching
|
||||||
|
if form_state.current_position == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) {
|
||||||
|
// This indicates a "new entry" state, no data to load from server.
|
||||||
|
// The caller should handle this by calling form_state.reset_to_empty()
|
||||||
|
// or ensuring this function isn't called for a new entry position.
|
||||||
|
// For now, let's assume reset_to_empty was called if needed.
|
||||||
|
form_state.reset_to_empty(); // Ensure fields are clear for new entry
|
||||||
|
return Ok(format!(
|
||||||
|
"New entry mode for table {}.{}",
|
||||||
|
form_state.profile_name, form_state.table_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if form_state.total_count == 0 && form_state.current_position == 1 {
|
||||||
|
// Table is empty, this is the position for a new entry
|
||||||
|
form_state.reset_to_empty();
|
||||||
|
return Ok(format!(
|
||||||
|
"New entry mode for empty table {}.{}",
|
||||||
|
form_state.profile_name, form_state.table_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
match grpc_client
|
||||||
|
.get_table_data_by_position(
|
||||||
|
form_state.profile_name.clone(),
|
||||||
|
form_state.table_name.clone(),
|
||||||
|
form_state.current_position as i32,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
// Set the ID properly
|
form_state.update_from_response(&response.data);
|
||||||
form_state.id = response.id;
|
// ID, values, current_field, current_cursor_pos, has_unsaved_changes are set by update_from_response
|
||||||
|
Ok(format!(
|
||||||
// Update form values dynamically
|
"Loaded entry {}/{} for table {}.{}",
|
||||||
form_state.values = vec![
|
form_state.current_position,
|
||||||
response.firma,
|
form_state.total_count,
|
||||||
response.kz,
|
form_state.profile_name,
|
||||||
response.drc,
|
form_state.table_name
|
||||||
response.ulica,
|
))
|
||||||
response.psc,
|
|
||||||
response.mesto,
|
|
||||||
response.stat,
|
|
||||||
response.banka,
|
|
||||||
response.ucet,
|
|
||||||
response.skladm,
|
|
||||||
response.ico,
|
|
||||||
response.kontakt,
|
|
||||||
response.telefon,
|
|
||||||
response.skladu,
|
|
||||||
response.fax,
|
|
||||||
];
|
|
||||||
|
|
||||||
form_state.has_unsaved_changes = false;
|
|
||||||
Ok(format!("Loaded entry {}", position))
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
Ok(format!("Error loading entry: {}", e))
|
// If loading fails (e.g., record deleted, network error), what should happen?
|
||||||
|
// Maybe reset to a new entry state or show an error and keep current data.
|
||||||
|
// For now, log error and return error message.
|
||||||
|
tracing::error!(
|
||||||
|
"Error loading entry {} for table {}.{}: {}",
|
||||||
|
form_state.current_position,
|
||||||
|
form_state.profile_name,
|
||||||
|
form_state.table_name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
// Potentially clear form or revert to a safe state
|
||||||
|
// form_state.reset_to_empty();
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"Error loading entry {}: {}",
|
||||||
|
form_state.current_position,
|
||||||
|
e
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the consequences of a save operation, like updating counts.
|
// MODIFIED: To work with FormState's count and position
|
||||||
pub async fn handle_save_outcome(
|
pub async fn handle_save_outcome(
|
||||||
save_outcome: SaveOutcome,
|
save_outcome: SaveOutcome,
|
||||||
grpc_client: &mut GrpcClient,
|
_grpc_client: &mut GrpcClient, // May not be needed if count is fetched separately
|
||||||
app_state: &mut AppState,
|
_app_state: &mut AppState, // May not be needed directly
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match save_outcome {
|
match save_outcome {
|
||||||
SaveOutcome::CreatedNew(new_id) => {
|
SaveOutcome::CreatedNew(new_id) => {
|
||||||
// A new record was created, update the count!
|
// form_state.total_count and form_state.current_position should have been updated
|
||||||
UiService::update_adresar_count(grpc_client, app_state).await?;
|
// by the `save` function itself.
|
||||||
// Navigate to the new record (now that count is updated)
|
// Ensure form_state.id is set.
|
||||||
app_state.update_current_position(app_state.total_count);
|
form_state.id = new_id;
|
||||||
form_state.id = new_id; // Ensure ID is set (might be redundant if save already did it)
|
// Potentially, re-fetch count to be absolutely sure, but save should be authoritative.
|
||||||
|
// UiService::fetch_and_set_table_count(grpc_client, form_state).await?;
|
||||||
}
|
}
|
||||||
SaveOutcome::UpdatedExisting | SaveOutcome::NoChange => {
|
SaveOutcome::UpdatedExisting | SaveOutcome::NoChange => {
|
||||||
// No count update needed for these outcomes
|
// No changes to total_count or current_position needed from here.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ pub struct UiState {
|
|||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
// Core editor state
|
// Core editor state
|
||||||
pub current_dir: String,
|
pub current_dir: String,
|
||||||
pub total_count: u64,
|
|
||||||
pub current_position: u64,
|
|
||||||
pub profile_tree: ProfileTreeResponse,
|
pub profile_tree: ProfileTreeResponse,
|
||||||
pub selected_profile: Option<String>,
|
pub selected_profile: Option<String>,
|
||||||
pub current_mode: AppMode,
|
pub current_mode: AppMode,
|
||||||
|
pub current_view_profile_name: Option<String>,
|
||||||
|
pub current_view_table_name: Option<String>,
|
||||||
|
|
||||||
pub focused_button_index: usize,
|
pub focused_button_index: usize,
|
||||||
pub pending_table_structure_fetch: Option<(String, String)>,
|
pub pending_table_structure_fetch: Option<(String, String)>,
|
||||||
|
|
||||||
@@ -52,10 +53,10 @@ impl AppState {
|
|||||||
.to_string();
|
.to_string();
|
||||||
Ok(AppState {
|
Ok(AppState {
|
||||||
current_dir,
|
current_dir,
|
||||||
total_count: 0,
|
|
||||||
current_position: 0,
|
|
||||||
profile_tree: ProfileTreeResponse::default(),
|
profile_tree: ProfileTreeResponse::default(),
|
||||||
selected_profile: None,
|
selected_profile: None,
|
||||||
|
current_view_profile_name: None,
|
||||||
|
current_view_table_name: None,
|
||||||
current_mode: AppMode::General,
|
current_mode: AppMode::General,
|
||||||
focused_button_index: 0,
|
focused_button_index: 0,
|
||||||
pending_table_structure_fetch: None,
|
pending_table_structure_fetch: None,
|
||||||
@@ -63,18 +64,14 @@ impl AppState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing methods remain unchanged
|
|
||||||
pub fn update_total_count(&mut self, total_count: u64) {
|
|
||||||
self.total_count = total_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_current_position(&mut self, current_position: u64) {
|
|
||||||
self.current_position = current_position;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_mode(&mut self, mode: AppMode) {
|
pub fn update_mode(&mut self, mode: AppMode) {
|
||||||
self.current_mode = mode;
|
self.current_mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_current_view_table(&mut self, profile_name: String, table_name: String) {
|
||||||
|
self.current_view_profile_name = Some(profile_name);
|
||||||
|
self.current_view_table_name = Some(table_name);
|
||||||
|
}
|
||||||
|
|
||||||
// Add dialog helper methods
|
// Add dialog helper methods
|
||||||
/// Shows a dialog with the given title, message, and buttons.
|
/// Shows a dialog with the given title, message, and buttons.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// src/state/pages/form.rs
|
// src/state/pages/form.rs
|
||||||
|
|
||||||
|
use std::collections::HashMap; // NEW
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -7,7 +9,13 @@ use crate::state::pages::canvas_state::CanvasState;
|
|||||||
|
|
||||||
pub struct FormState {
|
pub struct FormState {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub fields: Vec<String>,
|
// NEW fields for dynamic table context
|
||||||
|
pub profile_name: String,
|
||||||
|
pub table_name: String,
|
||||||
|
pub total_count: u64,
|
||||||
|
pub current_position: u64, // 1-based index, 0 or total_count + 1 for new entry
|
||||||
|
|
||||||
|
pub fields: Vec<String>, // Already dynamic, which is good
|
||||||
pub values: Vec<String>,
|
pub values: Vec<String>,
|
||||||
pub current_field: usize,
|
pub current_field: usize,
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
@@ -15,11 +23,19 @@ pub struct FormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FormState {
|
impl FormState {
|
||||||
/// Create a new FormState with dynamic fields.
|
// MODIFIED constructor
|
||||||
pub fn new(fields: Vec<String>) -> Self {
|
pub fn new(
|
||||||
let values = vec![String::new(); fields.len()]; // Initialize values for each field
|
profile_name: String,
|
||||||
|
table_name: String,
|
||||||
|
fields: Vec<String>,
|
||||||
|
) -> Self {
|
||||||
|
let values = vec![String::new(); fields.len()];
|
||||||
FormState {
|
FormState {
|
||||||
id: 0,
|
id: 0, // Default to 0, indicating a new or unloaded record
|
||||||
|
profile_name,
|
||||||
|
table_name,
|
||||||
|
total_count: 0, // Will be fetched after initialization
|
||||||
|
current_position: 0, // Will be set after count is fetched (e.g., 1 or total_count + 1)
|
||||||
fields,
|
fields,
|
||||||
values,
|
values,
|
||||||
current_field: 0,
|
current_field: 0,
|
||||||
@@ -35,31 +51,42 @@ impl FormState {
|
|||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
is_edit_mode: bool,
|
is_edit_mode: bool,
|
||||||
highlight_state: &HighlightState,
|
highlight_state: &HighlightState,
|
||||||
total_count: u64,
|
// total_count and current_position are now part of self
|
||||||
current_position: u64,
|
|
||||||
) {
|
) {
|
||||||
let fields: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect();
|
let fields_str_slice: Vec<&str> =
|
||||||
let values: Vec<&String> = self.values.iter().collect();
|
self.fields.iter().map(|s| s.as_str()).collect();
|
||||||
|
let values_str_slice: Vec<&String> = self.values.iter().collect();
|
||||||
|
|
||||||
crate::components::form::form::render_form(
|
crate::components::form::form::render_form(
|
||||||
f,
|
f,
|
||||||
area,
|
area,
|
||||||
self,
|
self, // Pass self as CanvasState
|
||||||
&fields,
|
&fields_str_slice,
|
||||||
&self.current_field,
|
&self.current_field,
|
||||||
&values,
|
&values_str_slice,
|
||||||
theme,
|
theme,
|
||||||
is_edit_mode,
|
is_edit_mode,
|
||||||
highlight_state,
|
highlight_state,
|
||||||
total_count,
|
self.total_count, // MODIFIED: Use self.total_count
|
||||||
current_position,
|
self.current_position, // MODIFIED: Use self.current_position
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MODIFIED: Reset now also considers table context for counts
|
||||||
pub fn reset_to_empty(&mut self) {
|
pub fn reset_to_empty(&mut self) {
|
||||||
self.id = 0; // Reset ID to 0 for new entries
|
self.id = 0;
|
||||||
self.values.iter_mut().for_each(|v| v.clear()); // Clear all values
|
self.values.iter_mut().for_each(|v| v.clear());
|
||||||
|
self.current_field = 0;
|
||||||
|
self.current_cursor_pos = 0;
|
||||||
self.has_unsaved_changes = false;
|
self.has_unsaved_changes = false;
|
||||||
|
// current_position should be set to total_count + 1 for a new entry
|
||||||
|
// This might be better handled by the logic that calls reset_to_empty
|
||||||
|
// For now, let's ensure it's consistent with a "new" state.
|
||||||
|
if self.total_count > 0 {
|
||||||
|
self.current_position = self.total_count + 1;
|
||||||
|
} else {
|
||||||
|
self.current_position = 1; // If table is empty, new record is at position 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_input(&self) -> &str {
|
pub fn get_current_input(&self) -> &str {
|
||||||
@@ -75,15 +102,43 @@ impl FormState {
|
|||||||
.expect("Invalid current_field index")
|
.expect("Invalid current_field index")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_from_response(&mut self, response: common::proto::multieko2::adresar::AdresarResponse) {
|
// MODIFIED: Update from a generic HashMap response
|
||||||
self.id = response.id;
|
pub fn update_from_response(
|
||||||
self.values = vec![
|
&mut self,
|
||||||
response.firma, response.kz, response.drc,
|
response_data: &HashMap<String, String>,
|
||||||
response.ulica, response.psc, response.mesto,
|
) {
|
||||||
response.stat, response.banka, response.ucet,
|
self.values = self.fields
|
||||||
response.skladm, response.ico, response.kontakt,
|
.iter()
|
||||||
response.telefon, response.skladu, response.fax,
|
.map(|field_name| {
|
||||||
];
|
response_data.get(field_name).cloned().unwrap_or_default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(id_str) = response_data.get("id") {
|
||||||
|
match id_str.parse::<i64>() {
|
||||||
|
Ok(parsed_id) => self.id = parsed_id,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to parse 'id' field '{}' for table {}.{}: {}",
|
||||||
|
id_str,
|
||||||
|
self.profile_name,
|
||||||
|
self.table_name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
self.id = 0; // Default to 0 if parsing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no ID is present, it might be a new record structure or an error
|
||||||
|
// For now, assume it means the record doesn't have an ID from the server yet
|
||||||
|
self.id = 0;
|
||||||
|
}
|
||||||
|
self.has_unsaved_changes = false;
|
||||||
|
// current_field and current_cursor_pos might need resetting or adjusting
|
||||||
|
// depending on the desired behavior after loading data.
|
||||||
|
// For now, let's reset current_field to 0.
|
||||||
|
self.current_field = 0;
|
||||||
|
self.current_cursor_pos = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,31 +160,26 @@ impl CanvasState for FormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input(&self) -> &str {
|
fn get_current_input(&self) -> &str {
|
||||||
self.values
|
// Re-use the struct's own method
|
||||||
.get(self.current_field)
|
FormState::get_current_input(self)
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input_mut(&mut self) -> &mut String {
|
fn get_current_input_mut(&mut self) -> &mut String {
|
||||||
self.values
|
// Re-use the struct's own method
|
||||||
.get_mut(self.current_field)
|
FormState::get_current_input_mut(self)
|
||||||
.expect("Invalid current_field index")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fields(&self) -> Vec<&str> {
|
fn fields(&self) -> Vec<&str> {
|
||||||
self.fields.iter().map(|s| s.as_str()).collect()
|
self.fields.iter().map(|s| s.as_str()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Implement the setter methods ---
|
|
||||||
fn set_current_field(&mut self, index: usize) {
|
fn set_current_field(&mut self, index: usize) {
|
||||||
if index < self.fields.len() { // Basic bounds check
|
if index < self.fields.len() {
|
||||||
self.current_field = index;
|
self.current_field = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
// Optional: Add validation based on current input length if needed
|
|
||||||
self.current_cursor_pos = pos;
|
self.current_cursor_pos = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,12 +187,11 @@ impl CanvasState for FormState {
|
|||||||
self.has_unsaved_changes = changed;
|
self.has_unsaved_changes = changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Autocomplete Support (Not Used for FormState) ---
|
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
fn get_suggestions(&self) -> Option<&[String]> {
|
||||||
None // FormState doesn't provide suggestions
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||||
None // FormState doesn't have selected suggestions
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,114 +2,130 @@
|
|||||||
|
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::state::pages::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest};
|
use anyhow::{Context, Result}; // Added Context
|
||||||
use anyhow::Result;
|
use std::collections::HashMap; // NEW
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SaveOutcome {
|
pub enum SaveOutcome {
|
||||||
NoChange, // Nothing needed saving
|
NoChange,
|
||||||
UpdatedExisting, // An existing record was updated
|
UpdatedExisting,
|
||||||
CreatedNew(i64), // A new record was created (include its new ID)
|
CreatedNew(i64), // Keep the ID
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared logic for saving the current form state
|
// MODIFIED save function
|
||||||
pub async fn save(
|
pub async fn save(
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
current_position: &mut u64,
|
) -> Result<SaveOutcome> {
|
||||||
total_count: u64,
|
|
||||||
) -> Result<SaveOutcome> { // <-- Return SaveOutcome
|
|
||||||
if !form_state.has_unsaved_changes {
|
if !form_state.has_unsaved_changes {
|
||||||
return Ok(SaveOutcome::NoChange); // Early exit if no changes
|
return Ok(SaveOutcome::NoChange);
|
||||||
}
|
}
|
||||||
let is_new = *current_position == total_count + 1;
|
|
||||||
|
|
||||||
let outcome = if is_new {
|
let data_map: HashMap<String, String> = form_state
|
||||||
let post_request = PostAdresarRequest {
|
.fields
|
||||||
firma: form_state.values[0].clone(),
|
.iter()
|
||||||
kz: form_state.values[1].clone(),
|
.zip(form_state.values.iter())
|
||||||
drc: form_state.values[2].clone(),
|
.map(|(field, value)| (field.clone(), value.clone()))
|
||||||
ulica: form_state.values[3].clone(),
|
.collect();
|
||||||
psc: form_state.values[4].clone(),
|
|
||||||
mesto: form_state.values[5].clone(),
|
let outcome: SaveOutcome;
|
||||||
stat: form_state.values[6].clone(),
|
|
||||||
banka: form_state.values[7].clone(),
|
let is_new_entry = form_state.id == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) || (form_state.total_count == 0 && form_state.current_position == 1) ;
|
||||||
ucet: form_state.values[8].clone(),
|
|
||||||
skladm: form_state.values[9].clone(),
|
|
||||||
ico: form_state.values[10].clone(),
|
if is_new_entry {
|
||||||
kontakt: form_state.values[11].clone(),
|
let response = grpc_client
|
||||||
telefon: form_state.values[12].clone(),
|
.post_table_data(
|
||||||
skladu: form_state.values[13].clone(),
|
form_state.profile_name.clone(),
|
||||||
fax: form_state.values[14].clone(),
|
form_state.table_name.clone(),
|
||||||
};
|
data_map,
|
||||||
let response = grpc_client.post_adresar(post_request).await?;
|
)
|
||||||
let new_id = response.into_inner().id;
|
.await
|
||||||
form_state.id = new_id;
|
.context("Failed to post new table data")?;
|
||||||
SaveOutcome::CreatedNew(new_id) // <-- Return CreatedNew with ID
|
|
||||||
|
if response.success {
|
||||||
|
form_state.id = response.inserted_id;
|
||||||
|
// After creating a new entry, total_count increases, and current_position becomes this new total_count
|
||||||
|
form_state.total_count += 1;
|
||||||
|
form_state.current_position = form_state.total_count;
|
||||||
|
outcome = SaveOutcome::CreatedNew(response.inserted_id);
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Server failed to insert data: {}",
|
||||||
|
response.message
|
||||||
|
));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let put_request = PutAdresarRequest {
|
// This assumes form_state.id is valid for an existing record
|
||||||
id: form_state.id,
|
if form_state.id == 0 {
|
||||||
firma: form_state.values[0].clone(),
|
return Err(anyhow::anyhow!(
|
||||||
kz: form_state.values[1].clone(),
|
"Cannot update record: ID is 0, but not classified as new entry."
|
||||||
drc: form_state.values[2].clone(),
|
));
|
||||||
ulica: form_state.values[3].clone(),
|
}
|
||||||
psc: form_state.values[4].clone(),
|
let response = grpc_client
|
||||||
mesto: form_state.values[5].clone(),
|
.put_table_data(
|
||||||
stat: form_state.values[6].clone(),
|
form_state.profile_name.clone(),
|
||||||
banka: form_state.values[7].clone(),
|
form_state.table_name.clone(),
|
||||||
ucet: form_state.values[8].clone(),
|
form_state.id,
|
||||||
skladm: form_state.values[9].clone(),
|
data_map,
|
||||||
ico: form_state.values[10].clone(),
|
)
|
||||||
kontakt: form_state.values[11].clone(),
|
.await
|
||||||
telefon: form_state.values[12].clone(),
|
.context("Failed to put (update) table data")?;
|
||||||
skladu: form_state.values[13].clone(),
|
|
||||||
fax: form_state.values[14].clone(),
|
if response.success {
|
||||||
};
|
outcome = SaveOutcome::UpdatedExisting;
|
||||||
let _ = grpc_client.put_adresar(put_request).await?;
|
} else {
|
||||||
SaveOutcome::UpdatedExisting
|
return Err(anyhow::anyhow!(
|
||||||
};
|
"Server failed to update data: {}",
|
||||||
|
response.message
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
form_state.has_unsaved_changes = false;
|
form_state.has_unsaved_changes = false;
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discard changes since last save
|
|
||||||
pub async fn revert(
|
pub async fn revert(
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState, // Takes &mut FormState to update it
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
current_position: &mut u64,
|
|
||||||
total_count: u64,
|
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let is_new = *current_position == total_count + 1;
|
if form_state.id == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) || (form_state.total_count == 0 && form_state.current_position == 1) {
|
||||||
|
let old_total_count = form_state.total_count; // Preserve for correct new position
|
||||||
if is_new {
|
form_state.reset_to_empty(); // reset_to_empty will clear values and set id=0
|
||||||
// Clear all fields for new entries
|
form_state.total_count = old_total_count; // Restore total_count
|
||||||
form_state.values.iter_mut().for_each(|v| *v = String::new());
|
if form_state.total_count > 0 { // Correctly set current_position for new
|
||||||
form_state.has_unsaved_changes = false;
|
form_state.current_position = form_state.total_count + 1;
|
||||||
|
} else {
|
||||||
|
form_state.current_position = 1;
|
||||||
|
}
|
||||||
return Ok("New entry cleared".to_string());
|
return Ok("New entry cleared".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = grpc_client.get_adresar_by_position(*current_position).await?;
|
if form_state.current_position == 0 || form_state.current_position > form_state.total_count {
|
||||||
|
if form_state.total_count > 0 {
|
||||||
|
form_state.current_position = 1;
|
||||||
|
} else {
|
||||||
|
// No records to revert to, effectively a new entry state.
|
||||||
|
form_state.reset_to_empty();
|
||||||
|
return Ok("No saved data to revert to; form cleared.".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update form fields with saved values
|
let response = grpc_client
|
||||||
form_state.values = vec![
|
.get_table_data_by_position(
|
||||||
data.firma,
|
form_state.profile_name.clone(),
|
||||||
data.kz,
|
form_state.table_name.clone(),
|
||||||
data.drc,
|
form_state.current_position as i32,
|
||||||
data.ulica,
|
)
|
||||||
data.psc,
|
.await
|
||||||
data.mesto,
|
.context(format!(
|
||||||
data.stat,
|
"Failed to get table data by position {} for table {}.{}",
|
||||||
data.banka,
|
form_state.current_position,
|
||||||
data.ucet,
|
form_state.profile_name,
|
||||||
data.skladm,
|
form_state.table_name
|
||||||
data.ico,
|
))?;
|
||||||
data.kontakt,
|
|
||||||
data.telefon,
|
|
||||||
data.skladu,
|
|
||||||
data.fax,
|
|
||||||
];
|
|
||||||
|
|
||||||
form_state.has_unsaved_changes = false;
|
form_state.update_from_response(&response.data);
|
||||||
Ok("Changes discarded, reloaded last saved version".to_string())
|
Ok("Changes discarded, reloaded last saved version".to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
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::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(
|
||||||
@@ -12,8 +13,7 @@ pub async fn handle_action(
|
|||||||
total_count: u64,
|
total_count: u64,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// TODO store unsaved changes without deleting form state values
|
// Check for unsaved changes in both cases
|
||||||
// First 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."
|
||||||
@@ -23,57 +23,43 @@ pub async fn handle_action(
|
|||||||
|
|
||||||
match action {
|
match action {
|
||||||
"previous_entry" => {
|
"previous_entry" => {
|
||||||
let new_position = current_position.saturating_sub(1);
|
let new_position = form_state.current_position.saturating_sub(1);
|
||||||
if new_position >= 1 {
|
if new_position >= 1 {
|
||||||
|
form_state.current_position = new_position;
|
||||||
*current_position = new_position;
|
*current_position = new_position;
|
||||||
let response = grpc_client.get_adresar_by_position(*current_position).await?;
|
|
||||||
|
if new_position <= form_state.total_count {
|
||||||
// Direct field assignments
|
let load_message = UiService::load_table_data_by_position(grpc_client, form_state).await?;
|
||||||
form_state.id = response.id;
|
|
||||||
form_state.values = vec![
|
|
||||||
response.firma, response.kz, response.drc,
|
|
||||||
response.ulica, response.psc, response.mesto,
|
|
||||||
response.stat, response.banka, response.ucet,
|
|
||||||
response.skladm, response.ico, response.kontakt,
|
|
||||||
response.telefon, response.skladu, response.fax,
|
|
||||||
];
|
|
||||||
|
|
||||||
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 = std::cmp::min(*ideal_cursor_column, max_cursor_pos);
|
|
||||||
form_state.has_unsaved_changes = false;
|
|
||||||
|
|
||||||
Ok(format!("Loaded form entry {}", *current_position))
|
|
||||||
} else {
|
|
||||||
Ok("Already at first form entry".into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"next_entry" => {
|
|
||||||
if *current_position <= total_count {
|
|
||||||
*current_position += 1;
|
|
||||||
if *current_position <= total_count {
|
|
||||||
let response = grpc_client.get_adresar_by_position(*current_position).await?;
|
|
||||||
|
|
||||||
// Direct field assignments
|
|
||||||
form_state.id = response.id;
|
|
||||||
form_state.values = vec![
|
|
||||||
response.firma, response.kz, response.drc,
|
|
||||||
response.ulica, response.psc, response.mesto,
|
|
||||||
response.stat, response.banka, response.ucet,
|
|
||||||
response.skladm, response.ico, response.kontakt,
|
|
||||||
response.telefon, response.skladu, response.fax,
|
|
||||||
];
|
|
||||||
|
|
||||||
let current_input = form_state.get_current_input();
|
let current_input = form_state.get_current_input();
|
||||||
let max_cursor_pos = if !current_input.is_empty() {
|
let max_cursor_pos = if !current_input.is_empty() {
|
||||||
current_input.len() - 1
|
current_input.len() - 1
|
||||||
} else { 0 };
|
} else { 0 };
|
||||||
form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos);
|
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||||
form_state.has_unsaved_changes = false;
|
|
||||||
|
Ok(load_message)
|
||||||
Ok(format!("Loaded form entry {}", *current_position))
|
} else {
|
||||||
|
Ok(format!("Moved to position {}", new_position))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok("Already at first position".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"next_entry" => {
|
||||||
|
if form_state.current_position <= form_state.total_count {
|
||||||
|
form_state.current_position += 1;
|
||||||
|
*current_position = form_state.current_position;
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
form_state.reset_to_empty();
|
form_state.reset_to_empty();
|
||||||
form_state.current_field = 0;
|
form_state.current_field = 0;
|
||||||
@@ -86,6 +72,5 @@ pub async fn handle_action(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(anyhow!("Unknown form action: {}", action))
|
_ => Err(anyhow!("Unknown form action: {}", action))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,3 @@ pub enum DialogPurpose {
|
|||||||
// TODO in the future:
|
// TODO in the future:
|
||||||
// ConfirmQuit,
|
// ConfirmQuit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/ui/handlers/rat_state.rs
|
// client/src/ui/handlers/rat_state.rs
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::state::app::state::UiState;
|
use crate::state::app::state::UiState;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/ui/handlers/render.rs
|
// client/src/ui/handlers/render.rs
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
render_background,
|
render_background,
|
||||||
@@ -11,10 +11,14 @@ use crate::components::{
|
|||||||
admin::render_add_table,
|
admin::render_add_table,
|
||||||
admin::add_logic::render_add_logic,
|
admin::add_logic::render_add_logic,
|
||||||
auth::{login::render_login, register::render_register},
|
auth::{login::render_login, register::render_register},
|
||||||
|
common::find_file_palette,
|
||||||
};
|
};
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::layout::{Constraint, Direction, Layout};
|
use ratatui::{
|
||||||
use ratatui::Frame;
|
layout::{Constraint, Direction, Layout},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use crate::state::pages::canvas_state::CanvasState;
|
||||||
use crate::state::pages::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::pages::auth::LoginState;
|
use crate::state::pages::auth::LoginState;
|
||||||
@@ -24,7 +28,9 @@ use crate::state::app::buffer::BufferState;
|
|||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::state::pages::admin::AdminState;
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
|
use crate::modes::general::command_navigation::NavigationState;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_ui(
|
pub fn render_ui(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
@@ -35,175 +41,154 @@ pub fn render_ui(
|
|||||||
admin_state: &mut AdminState,
|
admin_state: &mut AdminState,
|
||||||
buffer_state: &BufferState,
|
buffer_state: &BufferState,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
is_edit_mode: bool,
|
is_event_handler_edit_mode: bool,
|
||||||
highlight_state: &HighlightState,
|
highlight_state: &HighlightState,
|
||||||
total_count: u64,
|
event_handler_command_input: &str,
|
||||||
current_position: u64,
|
event_handler_command_mode_active: bool,
|
||||||
|
event_handler_command_message: &str,
|
||||||
|
navigation_state: &NavigationState,
|
||||||
current_dir: &str,
|
current_dir: &str,
|
||||||
command_input: &str,
|
|
||||||
command_mode: bool,
|
|
||||||
command_message: &str,
|
|
||||||
current_fps: f64,
|
current_fps: f64,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
) {
|
) {
|
||||||
render_background(f, f.area(), theme);
|
render_background(f, f.area(), theme);
|
||||||
|
|
||||||
// Adjust layout based on whether buffer list is shown
|
const PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT: u16 = 15;
|
||||||
let constraints = if app_state.ui.show_buffer_list {
|
|
||||||
vec![
|
let mut bottom_area_constraints: Vec<Constraint> = vec![Constraint::Length(1)];
|
||||||
Constraint::Length(1), // Buffer list
|
|
||||||
Constraint::Min(1), // Main content
|
let command_palette_area_height = if navigation_state.active {
|
||||||
Constraint::Length(1), // Status line
|
1 + PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT
|
||||||
Constraint::Length(1), // Command line
|
} else if event_handler_command_mode_active {
|
||||||
]
|
1
|
||||||
} else {
|
} else {
|
||||||
vec![
|
0 // Neither is active
|
||||||
Constraint::Min(1), // Main content
|
|
||||||
Constraint::Length(1), // Status line (no buffer list)
|
|
||||||
Constraint::Length(1), // Command line
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let root = Layout::default()
|
if command_palette_area_height > 0 {
|
||||||
|
bottom_area_constraints.push(Constraint::Length(command_palette_area_height));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut main_layout_constraints = vec![Constraint::Min(1)];
|
||||||
|
if app_state.ui.show_buffer_list {
|
||||||
|
main_layout_constraints.insert(0, Constraint::Length(1));
|
||||||
|
}
|
||||||
|
main_layout_constraints.extend(bottom_area_constraints);
|
||||||
|
|
||||||
|
|
||||||
|
let root_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(constraints)
|
.constraints(main_layout_constraints)
|
||||||
.split(f.area());
|
.split(f.area());
|
||||||
|
|
||||||
let mut buffer_list_area = None;
|
let mut chunk_idx = 0;
|
||||||
let main_content_area;
|
let buffer_list_area = if app_state.ui.show_buffer_list {
|
||||||
let status_line_area;
|
let area = Some(root_chunks[chunk_idx]);
|
||||||
let command_line_area;
|
chunk_idx += 1;
|
||||||
|
area
|
||||||
// Assign areas based on layout
|
|
||||||
if app_state.ui.show_buffer_list {
|
|
||||||
buffer_list_area = Some(root[0]);
|
|
||||||
main_content_area = root[1];
|
|
||||||
status_line_area = root[2];
|
|
||||||
command_line_area = root[3];
|
|
||||||
} else {
|
} else {
|
||||||
main_content_area = root[0];
|
None
|
||||||
status_line_area = root[1];
|
};
|
||||||
command_line_area = root[2];
|
|
||||||
}
|
let main_content_area = root_chunks[chunk_idx];
|
||||||
|
chunk_idx += 1;
|
||||||
|
|
||||||
|
let status_line_area = root_chunks[chunk_idx];
|
||||||
|
chunk_idx += 1;
|
||||||
|
|
||||||
|
let command_render_area = if command_palette_area_height > 0 {
|
||||||
|
if root_chunks.len() > chunk_idx {
|
||||||
|
Some(root_chunks[chunk_idx])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
if app_state.ui.show_intro {
|
if app_state.ui.show_intro {
|
||||||
render_intro(f, intro_state, main_content_area, theme);
|
render_intro(f, intro_state, main_content_area, theme);
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
render_register(
|
render_register(
|
||||||
f,
|
f, main_content_area, theme, register_state, app_state,
|
||||||
main_content_area,
|
register_state.current_field() < 4,
|
||||||
theme,
|
|
||||||
register_state,
|
|
||||||
app_state,
|
|
||||||
register_state.current_field < 4,
|
|
||||||
highlight_state,
|
highlight_state,
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
render_add_table(
|
render_add_table(
|
||||||
f,
|
f, main_content_area, theme, app_state, &mut admin_state.add_table_state,
|
||||||
main_content_area,
|
is_event_handler_edit_mode,
|
||||||
theme,
|
|
||||||
app_state,
|
|
||||||
&mut admin_state.add_table_state,
|
|
||||||
login_state.current_field < 3,
|
|
||||||
highlight_state,
|
highlight_state,
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_add_logic {
|
} else if app_state.ui.show_add_logic {
|
||||||
render_add_logic(
|
render_add_logic(
|
||||||
f,
|
f, main_content_area, theme, app_state, &mut admin_state.add_logic_state,
|
||||||
main_content_area,
|
is_event_handler_edit_mode, highlight_state,
|
||||||
theme,
|
|
||||||
app_state,
|
|
||||||
&mut admin_state.add_logic_state,
|
|
||||||
is_edit_mode, // Pass the general edit mode status
|
|
||||||
highlight_state,
|
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_login {
|
} else if app_state.ui.show_login {
|
||||||
render_login(
|
render_login(
|
||||||
f,
|
f, main_content_area, theme, login_state, app_state,
|
||||||
main_content_area,
|
login_state.current_field() < 2,
|
||||||
theme,
|
|
||||||
login_state,
|
|
||||||
app_state,
|
|
||||||
login_state.current_field < 2,
|
|
||||||
highlight_state,
|
highlight_state,
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_admin {
|
} else if app_state.ui.show_admin {
|
||||||
crate::components::admin::admin_panel::render_admin_panel(
|
crate::components::admin::admin_panel::render_admin_panel(
|
||||||
f,
|
f, app_state, auth_state, admin_state, main_content_area, theme,
|
||||||
app_state,
|
&app_state.profile_tree, &app_state.selected_profile,
|
||||||
auth_state,
|
|
||||||
admin_state,
|
|
||||||
main_content_area,
|
|
||||||
theme,
|
|
||||||
&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_area) = calculate_sidebar_layout(
|
let (sidebar_area, form_actual_area) = calculate_sidebar_layout(
|
||||||
app_state.ui.show_sidebar,
|
app_state.ui.show_sidebar, main_content_area
|
||||||
main_content_area
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(sidebar_rect) = sidebar_area {
|
if let Some(sidebar_rect) = sidebar_area {
|
||||||
sidebar::render_sidebar(
|
sidebar::render_sidebar(
|
||||||
f,
|
f, sidebar_rect, theme, &app_state.profile_tree, &app_state.selected_profile
|
||||||
sidebar_rect,
|
|
||||||
theme,
|
|
||||||
&app_state.profile_tree,
|
|
||||||
&app_state.selected_profile
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let available_width = form_actual_area.width;
|
||||||
// This change makes the form stay stationary when toggling sidebar
|
let form_render_area = if available_width >= 80 {
|
||||||
let available_width = form_area.width;
|
Layout::default().direction(Direction::Horizontal)
|
||||||
let form_constraint = if available_width >= 80 {
|
.constraints([Constraint::Min(0), Constraint::Length(80), Constraint::Min(0)])
|
||||||
// Use main_content_area for centering when enough space
|
.split(form_actual_area)[1]
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(80),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(main_content_area)[1]
|
|
||||||
} else {
|
} else {
|
||||||
// Use form_area (post sidebar) when limited space
|
Layout::default().direction(Direction::Horizontal)
|
||||||
Layout::default()
|
.constraints([Constraint::Min(0), Constraint::Length(available_width), Constraint::Min(0)])
|
||||||
.direction(Direction::Horizontal)
|
.split(form_actual_area)[1]
|
||||||
.constraints([
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(80.min(available_width)),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(form_area)[1]
|
|
||||||
};
|
};
|
||||||
|
let fields_vec: Vec<&str> = form_state.fields.iter().map(AsRef::as_ref).collect();
|
||||||
// Convert fields to &[&str] and values to &[&String]
|
let values_vec: Vec<&String> = form_state.values.iter().collect();
|
||||||
let fields: Vec<&str> = form_state.fields.iter().map(|s| s.as_str()).collect();
|
|
||||||
let values: Vec<&String> = form_state.values.iter().collect();
|
|
||||||
|
|
||||||
render_form(
|
render_form(
|
||||||
f,
|
f, form_render_area, form_state, &fields_vec, &form_state.current_field,
|
||||||
form_constraint,
|
&values_vec, theme, is_event_handler_edit_mode, highlight_state,
|
||||||
form_state,
|
form_state.total_count,
|
||||||
&fields,
|
form_state.current_position,
|
||||||
&form_state.current_field,
|
|
||||||
&values,
|
|
||||||
theme,
|
|
||||||
is_edit_mode,
|
|
||||||
highlight_state,
|
|
||||||
total_count,
|
|
||||||
current_position,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render buffer list if enabled and area is available
|
|
||||||
if let Some(area) = buffer_list_area {
|
if let Some(area) = buffer_list_area {
|
||||||
if app_state.ui.show_buffer_list {
|
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);
|
||||||
|
|
||||||
|
if let Some(palette_or_command_area) = command_render_area { // Use the calculated area
|
||||||
|
if navigation_state.active {
|
||||||
|
find_file_palette::render_find_file_palette(
|
||||||
|
f,
|
||||||
|
palette_or_command_area, // Use the correct area
|
||||||
|
theme,
|
||||||
|
navigation_state, // Pass the navigation_state directly
|
||||||
|
);
|
||||||
|
} else if event_handler_command_mode_active {
|
||||||
|
render_command_line(
|
||||||
|
f,
|
||||||
|
palette_or_command_area, // Use the correct area
|
||||||
|
event_handler_command_input,
|
||||||
|
true, // Assuming it's always active when this branch is hit
|
||||||
|
theme,
|
||||||
|
event_handler_command_message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render_status_line(f, status_line_area, current_dir, theme, is_edit_mode, current_fps);
|
|
||||||
render_command_line(f, command_line_area, command_input, command_mode, theme, command_message);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,42 +23,36 @@ use crate::tui::terminal::{EventReader, TerminalCore};
|
|||||||
use crate::ui::handlers::render::render_ui;
|
use crate::ui::handlers::render::render_ui;
|
||||||
use crate::tui::functions::common::login::LoginResult;
|
use crate::tui::functions::common::login::LoginResult;
|
||||||
use crate::tui::functions::common::register::RegisterResult;
|
use crate::tui::functions::common::register::RegisterResult;
|
||||||
// Removed: use crate::tui::functions::common::add_table::handle_save_table_action;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
// Removed: use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
|
||||||
use crate::ui::handlers::context::DialogPurpose; // UiContext removed if not used directly
|
|
||||||
use crate::tui::functions::common::login;
|
use crate::tui::functions::common::login;
|
||||||
use crate::tui::functions::common::register;
|
use crate::tui::functions::common::register;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::SetCursorStyle;
|
||||||
use crossterm::event as crossterm_event;
|
use crossterm::event as crossterm_event;
|
||||||
use tracing::{error, info, warn}; // Added warn
|
use tracing::{error, info, warn};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
|
||||||
pub async fn run_ui() -> Result<()> {
|
pub async fn run_ui() -> Result<()> {
|
||||||
let config = Config::load().context("Failed to load configuration")?;
|
let config = Config::load().context("Failed to load configuration")?;
|
||||||
let theme = Theme::from_str(&config.colors.theme);
|
let theme = Theme::from_str(&config.colors.theme);
|
||||||
let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?;
|
let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?;
|
||||||
let mut grpc_client = GrpcClient::new().await?;
|
let mut grpc_client = GrpcClient::new().await.context("Failed to create GrpcClient")?;
|
||||||
let mut command_handler = CommandHandler::new();
|
let mut command_handler = CommandHandler::new();
|
||||||
|
|
||||||
// --- Channel for Login Results ---
|
let (login_result_sender, mut login_result_receiver) = mpsc::channel::<LoginResult>(1);
|
||||||
let (login_result_sender, mut login_result_receiver) =
|
let (register_result_sender, mut register_result_receiver) = mpsc::channel::<RegisterResult>(1);
|
||||||
mpsc::channel::<LoginResult>(1);
|
let (save_table_result_sender, mut save_table_result_receiver) = mpsc::channel::<Result<String>>(1);
|
||||||
let (register_result_sender, mut register_result_receiver) =
|
let (save_logic_result_sender, _save_logic_result_receiver) = mpsc::channel::<Result<String>>(1);
|
||||||
mpsc::channel::<RegisterResult>(1);
|
|
||||||
let (save_table_result_sender, mut save_table_result_receiver) =
|
|
||||||
mpsc::channel::<Result<String>>(1);
|
|
||||||
let (save_logic_result_sender, _save_logic_result_receiver) = // Prefixed and removed mut
|
|
||||||
mpsc::channel::<Result<String>>(1);
|
|
||||||
|
|
||||||
let mut event_handler = EventHandler::new(
|
let mut event_handler = EventHandler::new(
|
||||||
login_result_sender.clone(),
|
login_result_sender.clone(),
|
||||||
register_result_sender.clone(),
|
register_result_sender.clone(),
|
||||||
save_table_result_sender.clone(),
|
save_table_result_sender.clone(),
|
||||||
save_logic_result_sender.clone(),
|
save_logic_result_sender.clone(),
|
||||||
).await.context("Failed to create event handler")?;
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to create event handler")?;
|
||||||
let event_reader = EventReader::new();
|
let event_reader = EventReader::new();
|
||||||
|
|
||||||
let mut auth_state = AuthState::default();
|
let mut auth_state = AuthState::default();
|
||||||
@@ -69,7 +63,6 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let mut buffer_state = BufferState::default();
|
let mut buffer_state = BufferState::default();
|
||||||
let mut app_state = AppState::new().context("Failed to create initial app state")?;
|
let mut app_state = AppState::new().context("Failed to create initial app state")?;
|
||||||
|
|
||||||
// --- DATA: Load auth data from file at startup ---
|
|
||||||
let mut auto_logged_in = false;
|
let mut auto_logged_in = false;
|
||||||
match load_auth_data() {
|
match load_auth_data() {
|
||||||
Ok(Some(stored_data)) => {
|
Ok(Some(stored_data)) => {
|
||||||
@@ -87,15 +80,34 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
error!("Failed to load auth data: {}", e);
|
error!("Failed to load auth data: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END DATA ---
|
|
||||||
|
|
||||||
let column_names =
|
// Initialize AppState and FormState with table data
|
||||||
UiService::initialize_app_state(&mut grpc_client, &mut app_state)
|
let (initial_profile, initial_table, initial_columns) =
|
||||||
.await.context("Failed to initialize app state from UI service")?;
|
UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state)
|
||||||
let mut form_state = FormState::new(column_names);
|
.await
|
||||||
|
.context("Failed to initialize app state and form")?;
|
||||||
|
|
||||||
UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?;
|
let mut form_state = FormState::new(
|
||||||
form_state.reset_to_empty();
|
initial_profile.clone(),
|
||||||
|
initial_table.clone(),
|
||||||
|
initial_columns,
|
||||||
|
);
|
||||||
|
|
||||||
|
UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state)
|
||||||
|
.await
|
||||||
|
.context(format!(
|
||||||
|
"Failed to fetch initial count for table {}.{}",
|
||||||
|
initial_profile, initial_table
|
||||||
|
))?;
|
||||||
|
|
||||||
|
// Load initial data for the form
|
||||||
|
if form_state.total_count > 0 {
|
||||||
|
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
|
||||||
|
event_handler.command_message = format!("Error loading initial data: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form_state.reset_to_empty();
|
||||||
|
}
|
||||||
|
|
||||||
if auto_logged_in {
|
if auto_logged_in {
|
||||||
buffer_state.history = vec![AppView::Form];
|
buffer_state.history = vec![AppView::Form];
|
||||||
@@ -106,9 +118,10 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let mut last_frame_time = Instant::now();
|
let mut last_frame_time = Instant::now();
|
||||||
let mut current_fps = 0.0;
|
let mut current_fps = 0.0;
|
||||||
let mut needs_redraw = true;
|
let mut needs_redraw = true;
|
||||||
|
let mut prev_view_profile_name = app_state.current_view_profile_name.clone();
|
||||||
|
let mut prev_view_table_name = app_state.current_view_table_name.clone();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// --- Synchronize UI View from Active Buffer ---
|
|
||||||
if let Some(active_view) = buffer_state.get_active_view() {
|
if let Some(active_view) = buffer_state.get_active_view() {
|
||||||
app_state.ui.show_intro = false;
|
app_state.ui.show_intro = false;
|
||||||
app_state.ui.show_login = false;
|
app_state.ui.show_login = false;
|
||||||
@@ -155,20 +168,59 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
AppView::Scratch => {}
|
AppView::Scratch => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- End Synchronization ---
|
|
||||||
|
|
||||||
// --- Handle Pending Table Structure Fetches ---
|
// Handle table change for FormView
|
||||||
|
if app_state.ui.show_form {
|
||||||
|
let current_view_profile = app_state.current_view_profile_name.clone();
|
||||||
|
let current_view_table = app_state.current_view_table_name.clone();
|
||||||
|
|
||||||
|
if prev_view_profile_name != current_view_profile || prev_view_table_name != current_view_table {
|
||||||
|
if let (Some(prof_name), Some(tbl_name)) = (current_view_profile.as_ref(), current_view_table.as_ref()) {
|
||||||
|
app_state.show_loading_dialog("Loading Table", &format!("Fetching data for {}.{}...", prof_name, tbl_name));
|
||||||
|
needs_redraw = true;
|
||||||
|
|
||||||
|
match grpc_client.get_table_structure(prof_name.clone(), tbl_name.clone()).await {
|
||||||
|
Ok(structure_response) => {
|
||||||
|
let new_columns: Vec<String> = structure_response.columns.iter().map(|c| c.name.clone()).collect();
|
||||||
|
form_state = FormState::new(prof_name.clone(), tbl_name.clone(), new_columns);
|
||||||
|
|
||||||
|
if let Err(e) = UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state).await {
|
||||||
|
app_state.update_dialog_content(&format!("Error fetching count: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
||||||
|
} else {
|
||||||
|
if form_state.total_count > 0 {
|
||||||
|
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
|
||||||
|
app_state.update_dialog_content(&format!("Error loading data: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
||||||
|
} else {
|
||||||
|
app_state.hide_dialog();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form_state.reset_to_empty();
|
||||||
|
app_state.hide_dialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
app_state.update_dialog_content(&format!("Error fetching table structure: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
||||||
|
app_state.current_view_profile_name = prev_view_profile_name.clone();
|
||||||
|
app_state.current_view_table_name = prev_view_table_name.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev_view_profile_name = current_view_profile;
|
||||||
|
prev_view_table_name = current_view_table;
|
||||||
|
needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
||||||
if app_state.ui.show_add_logic {
|
if app_state.ui.show_add_logic {
|
||||||
// Ensure admin_state.add_logic_state matches the pending fetch
|
|
||||||
if admin_state.add_logic_state.profile_name == profile_name &&
|
if admin_state.add_logic_state.profile_name == profile_name &&
|
||||||
admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) {
|
admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) {
|
||||||
|
|
||||||
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
||||||
let fetch_message = UiService::initialize_add_logic_table_data(
|
let fetch_message = UiService::initialize_add_logic_table_data(
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
&mut admin_state.add_logic_state,
|
&mut admin_state.add_logic_state,
|
||||||
&app_state.profile_tree, // Pass the profile tree
|
&app_state.profile_tree,
|
||||||
).await.unwrap_or_else(|e| {
|
).await.unwrap_or_else(|e| {
|
||||||
error!("Error initializing add_logic_table_data: {}", e);
|
error!("Error initializing add_logic_table_data: {}", e);
|
||||||
format!("Error fetching table structure: {}", e)
|
format!("Error fetching table structure: {}", e)
|
||||||
@@ -196,7 +248,6 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Draw UI ---
|
|
||||||
if needs_redraw {
|
if needs_redraw {
|
||||||
terminal.draw(|f| {
|
terminal.draw(|f| {
|
||||||
render_ui(
|
render_ui(
|
||||||
@@ -211,12 +262,11 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
&theme,
|
&theme,
|
||||||
event_handler.is_edit_mode,
|
event_handler.is_edit_mode,
|
||||||
&event_handler.highlight_state,
|
&event_handler.highlight_state,
|
||||||
app_state.total_count,
|
|
||||||
app_state.current_position,
|
|
||||||
&app_state.current_dir,
|
|
||||||
&event_handler.command_input,
|
&event_handler.command_input,
|
||||||
event_handler.command_mode,
|
event_handler.command_mode,
|
||||||
&event_handler.command_message,
|
&event_handler.command_message,
|
||||||
|
&event_handler.navigation_state,
|
||||||
|
&app_state.current_dir,
|
||||||
current_fps,
|
current_fps,
|
||||||
&app_state,
|
&app_state,
|
||||||
);
|
);
|
||||||
@@ -224,11 +274,10 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
needs_redraw = false;
|
needs_redraw = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Handle Pending Column Autocomplete for Table Selection ---
|
|
||||||
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
|
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
|
||||||
if app_state.ui.show_add_logic {
|
if app_state.ui.show_add_logic {
|
||||||
let profile_name = admin_state.add_logic_state.profile_name.clone();
|
let profile_name = admin_state.add_logic_state.profile_name.clone();
|
||||||
|
|
||||||
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
|
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
|
||||||
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
|
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
|
||||||
Ok(columns) => {
|
Ok(columns) => {
|
||||||
@@ -247,7 +296,6 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Cursor Visibility Logic ---
|
|
||||||
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
|
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
|
||||||
match current_mode {
|
match current_mode {
|
||||||
AppMode::Edit => { terminal.show_cursor()?; }
|
AppMode::Edit => { terminal.show_cursor()?; }
|
||||||
@@ -263,16 +311,13 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; }
|
AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; }
|
||||||
}
|
}
|
||||||
// --- End Cursor Visibility Logic ---
|
|
||||||
|
|
||||||
let total_count = app_state.total_count;
|
let position_before_event = form_state.current_position;
|
||||||
let mut current_position = app_state.current_position;
|
|
||||||
let position_before_event = current_position;
|
|
||||||
if app_state.ui.dialog.is_loading {
|
if app_state.ui.dialog.is_loading {
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 1. Handle Terminal Events ---
|
|
||||||
let mut event_outcome_result = Ok(EventOutcome::Ok(String::new()));
|
let mut event_outcome_result = Ok(EventOutcome::Ok(String::new()));
|
||||||
let mut event_processed = false;
|
let mut event_processed = false;
|
||||||
if crossterm_event::poll(std::time::Duration::from_millis(1))? {
|
if crossterm_event::poll(std::time::Duration::from_millis(1))? {
|
||||||
@@ -292,42 +337,37 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
&mut admin_state,
|
&mut admin_state,
|
||||||
&mut buffer_state,
|
&mut buffer_state,
|
||||||
&mut app_state,
|
&mut app_state,
|
||||||
total_count,
|
|
||||||
&mut current_position,
|
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if event_processed {
|
if event_processed {
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
app_state.current_position = current_position;
|
|
||||||
|
|
||||||
// --- Check for Login Results from Channel ---
|
|
||||||
match login_result_receiver.try_recv() {
|
match login_result_receiver.try_recv() {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state) {
|
if login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state) {
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ }
|
Err(mpsc::error::TryRecvError::Empty) => {}
|
||||||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||||||
error!("Login result channel disconnected unexpectedly.");
|
error!("Login result channel disconnected unexpectedly.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Check for Register Results from Channel ---
|
|
||||||
match register_result_receiver.try_recv() {
|
match register_result_receiver.try_recv() {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if register::handle_registration_result(result, &mut app_state, &mut register_state) {
|
if register::handle_registration_result(result, &mut app_state, &mut register_state) {
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ }
|
Err(mpsc::error::TryRecvError::Empty) => {}
|
||||||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||||||
error!("Register result channel disconnected unexpectedly.");
|
error!("Register result channel disconnected unexpectedly.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- Check for Save Table Results ---
|
|
||||||
match save_table_result_receiver.try_recv() {
|
match save_table_result_receiver.try_recv() {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
@@ -353,13 +393,10 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Centralized Consequence Handling ---
|
|
||||||
let mut should_exit = false;
|
let mut should_exit = false;
|
||||||
match event_outcome_result {
|
match event_outcome_result {
|
||||||
Ok(outcome) => match outcome {
|
Ok(outcome) => match outcome {
|
||||||
EventOutcome::Ok(_message) => {
|
EventOutcome::Ok(_message) => {}
|
||||||
// Message is often set directly in event_handler.command_message
|
|
||||||
}
|
|
||||||
EventOutcome::Exit(message) => {
|
EventOutcome::Exit(message) => {
|
||||||
event_handler.command_message = message;
|
event_handler.command_message = message;
|
||||||
should_exit = true;
|
should_exit = true;
|
||||||
@@ -378,77 +415,93 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
format!("Error handling save outcome: {}", e);
|
format!("Error handling save outcome: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventOutcome::ButtonSelected { context: _, index: _ } => {
|
EventOutcome::ButtonSelected { context: _, index: _ } => {}
|
||||||
// Handled within event_handler or specific navigation modules
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
event_handler.command_message = format!("Error: {}", e);
|
event_handler.command_message = format!("Error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- End Consequence Handling ---
|
|
||||||
|
|
||||||
// --- Position Change Handling ---
|
// --- MODIFIED: Position Change Handling (operates on form_state) ---
|
||||||
let position_changed = app_state.current_position != position_before_event;
|
let position_changed = form_state.current_position != position_before_event;
|
||||||
let current_total_count = app_state.total_count; // Use current total_count
|
|
||||||
let mut position_logic_needs_redraw = false;
|
let mut position_logic_needs_redraw = false;
|
||||||
|
|
||||||
if app_state.ui.show_form {
|
if app_state.ui.show_form { // Only if the form is active
|
||||||
if position_changed && !event_handler.is_edit_mode {
|
if position_changed && !event_handler.is_edit_mode {
|
||||||
let current_input = form_state.get_current_input();
|
// This part is okay: update cursor for the current field BEFORE loading new data
|
||||||
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
let current_input_before_load = form_state.get_current_input();
|
||||||
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
let max_cursor_pos_before_load = if !current_input_before_load.is_empty() { current_input_before_load.chars().count() } else { 0 };
|
||||||
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_before_load);
|
||||||
position_logic_needs_redraw = true;
|
position_logic_needs_redraw = true;
|
||||||
|
|
||||||
if app_state.current_position > current_total_count + 1 {
|
// Validate new form_state.current_position
|
||||||
app_state.current_position = current_total_count + 1;
|
if form_state.total_count > 0 && form_state.current_position > form_state.total_count + 1 {
|
||||||
|
form_state.current_position = form_state.total_count + 1; // Cap at new entry
|
||||||
|
} else if form_state.total_count == 0 && form_state.current_position > 1 {
|
||||||
|
form_state.current_position = 1; // Cap at new entry for empty table
|
||||||
|
}
|
||||||
|
if form_state.current_position == 0 && form_state.total_count > 0 {
|
||||||
|
form_state.current_position = 1; // Don't allow 0 if there are records
|
||||||
}
|
}
|
||||||
|
|
||||||
if app_state.current_position > current_total_count {
|
|
||||||
form_state.reset_to_empty();
|
|
||||||
form_state.current_field = 0;
|
|
||||||
} else if app_state.current_position >= 1 && app_state.current_position <= current_total_count {
|
|
||||||
let current_position_to_load = app_state.current_position;
|
|
||||||
let load_message = UiService::load_adresar_by_position(
|
|
||||||
&mut grpc_client,
|
|
||||||
&mut app_state,
|
|
||||||
&mut form_state,
|
|
||||||
current_position_to_load,
|
|
||||||
)
|
|
||||||
.await.with_context(|| format!("Failed to load adresar by position: {}", current_position_to_load))?;
|
|
||||||
|
|
||||||
let current_input_after_load = form_state.get_current_input();
|
// Load data for the new position OR reset for new entry
|
||||||
let max_cursor_pos_after_load = if !event_handler.is_edit_mode && !current_input_after_load.is_empty() {
|
if (form_state.total_count > 0 && form_state.current_position <= form_state.total_count && form_state.current_position > 0)
|
||||||
current_input_after_load.len() - 1
|
{
|
||||||
} else {
|
// It's an existing record position
|
||||||
current_input_after_load.len()
|
match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
|
||||||
};
|
Ok(load_message) => {
|
||||||
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_after_load);
|
if event_handler.command_message.is_empty() || !load_message.starts_with("Error") {
|
||||||
|
event_handler.command_message = load_message;
|
||||||
if !load_message.starts_with("Loaded entry") || event_handler.command_message.is_empty() {
|
}
|
||||||
event_handler.command_message = load_message;
|
}
|
||||||
|
Err(e) => {
|
||||||
|
event_handler.command_message = format!("Error loading data: {}", e);
|
||||||
|
// Consider what to do with form_state here - maybe revert position or clear form
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else { // current_position is 0 or invalid
|
} else {
|
||||||
app_state.current_position = 1.min(current_total_count + 1);
|
// Position indicates a new entry (or table is empty and position is 1)
|
||||||
if app_state.current_position > current_total_count { // Handles empty db case
|
form_state.reset_to_empty(); // This sets id=0, clears values, and sets current_position correctly
|
||||||
form_state.reset_to_empty();
|
event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name);
|
||||||
form_state.current_field = 0;
|
|
||||||
}
|
|
||||||
// If db is not empty, this will trigger load in next iteration if position changed to 1
|
|
||||||
}
|
}
|
||||||
} else if !position_changed && !event_handler.is_edit_mode {
|
|
||||||
let current_input = form_state.get_current_input();
|
// NOW, after data is loaded or form is reset, get the current input string and its length
|
||||||
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
let current_input_after_load_str = form_state.get_current_input();
|
||||||
|
let current_input_len_after_load = current_input_after_load_str.chars().count();
|
||||||
|
|
||||||
|
let max_cursor_pos_for_readonly_after_load = if current_input_len_after_load > 0 {
|
||||||
|
current_input_len_after_load.saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
if event_handler.is_edit_mode {
|
||||||
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(current_input_len_after_load);
|
||||||
|
} else {
|
||||||
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_for_readonly_after_load);
|
||||||
|
// The check for empty string is implicitly handled by max_cursor_pos_for_readonly_after_load being 0
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if !position_changed && !event_handler.is_edit_mode && 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_len = current_input_str.chars().count();
|
||||||
|
let max_cursor_pos = if current_input_len > 0 {
|
||||||
|
current_input_len.saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
}
|
}
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
if !event_handler.is_edit_mode {
|
if !event_handler.is_edit_mode {
|
||||||
let current_input = register_state.get_current_input();
|
let current_input = register_state.get_current_input();
|
||||||
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
||||||
register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
}
|
}
|
||||||
} else if app_state.ui.show_login {
|
} else if app_state.ui.show_login {
|
||||||
if !event_handler.is_edit_mode {
|
if !event_handler.is_edit_mode {
|
||||||
let current_input = login_state.get_current_input();
|
let current_input = login_state.get_current_input();
|
||||||
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
||||||
login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
@@ -458,18 +511,16 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
if position_logic_needs_redraw {
|
if position_logic_needs_redraw {
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
// --- End Position Change Handling ---
|
|
||||||
|
|
||||||
if should_exit {
|
if should_exit {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FPS Calculation ---
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let frame_duration = now.duration_since(last_frame_time);
|
let frame_duration = now.duration_since(last_frame_time);
|
||||||
last_frame_time = now;
|
last_frame_time = now;
|
||||||
if frame_duration.as_secs_f64() > 1e-6 { // Avoid division by zero
|
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();
|
||||||
}
|
}
|
||||||
} // End main loop
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
server/migrations/20250528110810_create_gen_schema.sql
Normal file
3
server/migrations/20250528110810_create_gen_schema.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE SCHEMA IF NOT EXISTS gen;
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
// src/shared/mod.rs
|
// src/shared/mod.rs
|
||||||
pub mod date_utils;
|
pub mod date_utils;
|
||||||
|
pub mod schema_qualifier;
|
||||||
|
|||||||
34
server/src/shared/schema_qualifier.rs
Normal file
34
server/src/shared/schema_qualifier.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// src/shared/schema_qualifier.rs
|
||||||
|
use tonic::Status;
|
||||||
|
|
||||||
|
/// Qualifies table names with the appropriate schema
|
||||||
|
///
|
||||||
|
/// Rules:
|
||||||
|
/// - Tables created via PostTableDefinition (dynamically created tables) are in 'gen' schema
|
||||||
|
/// - System tables (like users, profiles) remain in 'public' schema
|
||||||
|
pub fn qualify_table_name(table_name: &str) -> String {
|
||||||
|
// Check if table matches the pattern of dynamically created tables (e.g., 2025_something)
|
||||||
|
if table_name.starts_with(|c: char| c.is_ascii_digit()) && table_name.contains('_') {
|
||||||
|
format!("gen.\"{}\"", table_name)
|
||||||
|
} else {
|
||||||
|
format!("\"{}\"", table_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Qualifies table names for data operations
|
||||||
|
pub fn qualify_table_name_for_data(table_name: &str) -> Result<String, Status> {
|
||||||
|
Ok(qualify_table_name(table_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_qualify_table_name() {
|
||||||
|
assert_eq!(qualify_table_name("2025_test_schema3"), "gen.\"2025_test_schema3\"");
|
||||||
|
assert_eq!(qualify_table_name("users"), "\"users\"");
|
||||||
|
assert_eq!(qualify_table_name("profiles"), "\"profiles\"");
|
||||||
|
assert_eq!(qualify_table_name("adresar"), "\"adresar\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
// src/table_definition/handlers/post_table_definition.rs
|
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use sqlx::{PgPool, Transaction, Postgres};
|
use sqlx::{PgPool, Transaction, Postgres};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use common::proto::multieko2::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
|
use common::proto::multieko2::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
|
||||||
|
|
||||||
|
const GENERATED_SCHEMA_NAME: &str = "gen";
|
||||||
|
|
||||||
const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
|
const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
|
||||||
("text", "TEXT"),
|
("text", "TEXT"),
|
||||||
("psc", "TEXT"),
|
("psc", "TEXT"),
|
||||||
@@ -27,7 +28,6 @@ fn sanitize_table_name(s: &str) -> String {
|
|||||||
let cleaned = s.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "")
|
let cleaned = s.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "")
|
||||||
.trim()
|
.trim()
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
format!("{}_{}", year, cleaned)
|
format!("{}_{}", year, cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,31 +47,30 @@ fn map_field_type(field_type: &str) -> Result<&str, Status> {
|
|||||||
|
|
||||||
pub async fn post_table_definition(
|
pub async fn post_table_definition(
|
||||||
db_pool: &PgPool,
|
db_pool: &PgPool,
|
||||||
request: PostTableDefinitionRequest, // Removed `mut` since it's not needed here
|
request: PostTableDefinitionRequest,
|
||||||
) -> Result<TableDefinitionResponse, Status> {
|
) -> Result<TableDefinitionResponse, Status> {
|
||||||
// Validate and sanitize table name
|
let base_name = sanitize_table_name(&request.table_name);
|
||||||
let table_name = sanitize_table_name(&request.table_name);
|
let user_part_cleaned = request.table_name
|
||||||
if !is_valid_identifier(&request.table_name) {
|
.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "")
|
||||||
|
.trim_matches('_')
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
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() {
|
||||||
|
return Err(Status::invalid_argument("Table name cannot be empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a transaction to ensure atomicity
|
|
||||||
let mut tx = db_pool.begin().await
|
let mut tx = db_pool.begin().await
|
||||||
.map_err(|e| Status::internal(format!("Failed to start transaction: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Failed to start transaction: {}", e)))?;
|
||||||
|
|
||||||
// Execute all database operations within the transaction
|
match execute_table_definition(&mut tx, request, base_name).await {
|
||||||
let result = execute_table_definition(&mut tx, request, table_name).await;
|
|
||||||
|
|
||||||
// Commit or rollback based on the result
|
|
||||||
match result {
|
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
// Commit the transaction
|
|
||||||
tx.commit().await
|
tx.commit().await
|
||||||
.map_err(|e| Status::internal(format!("Failed to commit transaction: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Failed to commit transaction: {}", e)))?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Explicitly roll back the transaction (optional but good for clarity)
|
|
||||||
let _ = tx.rollback().await;
|
let _ = tx.rollback().await;
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
@@ -83,7 +82,6 @@ async fn execute_table_definition(
|
|||||||
mut request: PostTableDefinitionRequest,
|
mut request: PostTableDefinitionRequest,
|
||||||
table_name: String,
|
table_name: String,
|
||||||
) -> Result<TableDefinitionResponse, Status> {
|
) -> Result<TableDefinitionResponse, Status> {
|
||||||
// Lookup or create profile
|
|
||||||
let profile = sqlx::query!(
|
let profile = sqlx::query!(
|
||||||
"INSERT INTO profiles (name) VALUES ($1)
|
"INSERT INTO profiles (name) VALUES ($1)
|
||||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||||
@@ -94,7 +92,6 @@ async fn execute_table_definition(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("Profile error: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Profile error: {}", e)))?;
|
||||||
|
|
||||||
// Process table links
|
|
||||||
let mut links = Vec::new();
|
let mut links = Vec::new();
|
||||||
for link in request.links.drain(..) {
|
for link in request.links.drain(..) {
|
||||||
let linked_table = sqlx::query!(
|
let linked_table = sqlx::query!(
|
||||||
@@ -114,7 +111,6 @@ async fn execute_table_definition(
|
|||||||
links.push((linked_id, link.required));
|
links.push((linked_id, link.required));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process columns
|
|
||||||
let mut columns = Vec::new();
|
let mut columns = Vec::new();
|
||||||
for col_def in request.columns.drain(..) {
|
for col_def in request.columns.drain(..) {
|
||||||
let col_name = sanitize_identifier(&col_def.name);
|
let col_name = sanitize_identifier(&col_def.name);
|
||||||
@@ -125,20 +121,20 @@ async fn execute_table_definition(
|
|||||||
columns.push(format!("\"{}\" {}", col_name, sql_type));
|
columns.push(format!("\"{}\" {}", col_name, sql_type));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process indexes
|
|
||||||
let mut indexes = Vec::new();
|
let mut indexes = Vec::new();
|
||||||
for idx in request.indexes.drain(..) {
|
for idx in request.indexes.drain(..) {
|
||||||
let idx_name = sanitize_identifier(&idx);
|
let idx_name = sanitize_identifier(&idx);
|
||||||
if !is_valid_identifier(&idx) {
|
if !is_valid_identifier(&idx) {
|
||||||
return Err(Status::invalid_argument(format!("Invalid index name: {}", idx)));
|
return Err(Status::invalid_argument(format!("Invalid index name: {}", idx)));
|
||||||
}
|
}
|
||||||
|
if !columns.iter().any(|c| c.starts_with(&format!("\"{}\"", idx_name))) {
|
||||||
|
return Err(Status::invalid_argument(format!("Index column {} not found", idx_name)));
|
||||||
|
}
|
||||||
indexes.push(idx_name);
|
indexes.push(idx_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate SQL with multiple links
|
|
||||||
let (create_sql, index_sql) = generate_table_sql(tx, &table_name, &columns, &indexes, &links).await?;
|
let (create_sql, index_sql) = generate_table_sql(tx, &table_name, &columns, &indexes, &links).await?;
|
||||||
|
|
||||||
// Store main table definition
|
|
||||||
let table_def = sqlx::query!(
|
let table_def = sqlx::query!(
|
||||||
r#"INSERT INTO table_definitions
|
r#"INSERT INTO table_definitions
|
||||||
(profile_id, table_name, columns, indexes)
|
(profile_id, table_name, columns, indexes)
|
||||||
@@ -146,8 +142,8 @@ async fn execute_table_definition(
|
|||||||
RETURNING id"#,
|
RETURNING id"#,
|
||||||
profile.id,
|
profile.id,
|
||||||
&table_name,
|
&table_name,
|
||||||
json!(columns),
|
json!(request.columns.iter().map(|c| c.name.clone()).collect::<Vec<_>>()),
|
||||||
json!(indexes)
|
json!(request.indexes.iter().map(|i| i.clone()).collect::<Vec<_>>())
|
||||||
)
|
)
|
||||||
.fetch_one(&mut **tx)
|
.fetch_one(&mut **tx)
|
||||||
.await
|
.await
|
||||||
@@ -160,7 +156,6 @@ async fn execute_table_definition(
|
|||||||
Status::internal(format!("Database error: {}", e))
|
Status::internal(format!("Database error: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Store relationships
|
|
||||||
for (linked_id, is_required) in links {
|
for (linked_id, is_required) in links {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO table_definition_links
|
"INSERT INTO table_definition_links
|
||||||
@@ -175,7 +170,6 @@ async fn execute_table_definition(
|
|||||||
.map_err(|e| Status::internal(format!("Failed to save link: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Failed to save link: {}", e)))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute generated SQL within the transaction
|
|
||||||
sqlx::query(&create_sql)
|
sqlx::query(&create_sql)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
@@ -201,60 +195,60 @@ async fn generate_table_sql(
|
|||||||
indexes: &[String],
|
indexes: &[String],
|
||||||
links: &[(i64, bool)],
|
links: &[(i64, bool)],
|
||||||
) -> Result<(String, Vec<String>), Status> {
|
) -> Result<(String, Vec<String>), Status> {
|
||||||
|
let qualified_table = format!("{}.\"{}\"", GENERATED_SCHEMA_NAME, table_name);
|
||||||
|
|
||||||
let mut system_columns = vec![
|
let mut system_columns = vec![
|
||||||
"id BIGSERIAL PRIMARY KEY".to_string(),
|
"id BIGSERIAL PRIMARY KEY".to_string(),
|
||||||
"deleted BOOLEAN NOT NULL DEFAULT FALSE".to_string(),
|
"deleted BOOLEAN NOT NULL DEFAULT FALSE".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add foreign key columns
|
|
||||||
let mut link_info = Vec::new();
|
|
||||||
for (linked_id, required) in links {
|
for (linked_id, required) in links {
|
||||||
let linked_table = get_table_name_by_id(tx, *linked_id).await?;
|
let linked_table = get_table_name_by_id(tx, *linked_id).await?;
|
||||||
|
let qualified_linked_table = format!("{}.\"{}\"", GENERATED_SCHEMA_NAME, linked_table);
|
||||||
// Extract base name after year prefix
|
|
||||||
let base_name = linked_table.split_once('_')
|
let base_name = linked_table.split_once('_')
|
||||||
.map(|(_, rest)| rest)
|
.map(|(_, rest)| rest)
|
||||||
.unwrap_or(&linked_table)
|
.unwrap_or(&linked_table)
|
||||||
.to_string();
|
.to_string();
|
||||||
let null_clause = if *required { "NOT NULL" } else { "" };
|
let null_clause = if *required { "NOT NULL" } else { "" };
|
||||||
|
|
||||||
system_columns.push(
|
system_columns.push(
|
||||||
format!("\"{0}_id\" BIGINT {1} REFERENCES \"{2}\"(id)",
|
format!("\"{0}_id\" BIGINT {1} REFERENCES {2}(id)",
|
||||||
base_name, null_clause, linked_table
|
base_name, null_clause, qualified_linked_table
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
link_info.push((base_name, linked_table));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine all columns
|
|
||||||
let all_columns = system_columns
|
let all_columns = system_columns
|
||||||
.iter()
|
.iter()
|
||||||
.chain(columns.iter())
|
.chain(columns.iter())
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Build CREATE TABLE statement
|
|
||||||
let create_sql = format!(
|
let create_sql = format!(
|
||||||
"CREATE TABLE \"{}\" (\n {},\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)",
|
"CREATE TABLE {} (\n {},\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)",
|
||||||
table_name,
|
qualified_table,
|
||||||
all_columns.join(",\n ")
|
all_columns.join(",\n ")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate indexes
|
let mut all_indexes = Vec::new();
|
||||||
let mut system_indexes = Vec::new();
|
for (linked_id, _) in links {
|
||||||
for (base_name, _) in &link_info {
|
let linked_table = get_table_name_by_id(tx, *linked_id).await?;
|
||||||
system_indexes.push(format!(
|
let base_name = linked_table.split_once('_')
|
||||||
"CREATE INDEX idx_{}_{}_fk ON \"{}\" (\"{}_id\")",
|
.map(|(_, rest)| rest)
|
||||||
table_name, base_name, table_name, base_name
|
.unwrap_or(&linked_table)
|
||||||
|
.to_string();
|
||||||
|
all_indexes.push(format!(
|
||||||
|
"CREATE INDEX \"idx_{}_{}_fk\" ON {} (\"{}_id\")",
|
||||||
|
table_name, base_name, qualified_table, base_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let all_indexes = system_indexes
|
for idx in indexes {
|
||||||
.into_iter()
|
all_indexes.push(format!(
|
||||||
.chain(indexes.iter().map(|idx| {
|
"CREATE INDEX \"idx_{}_{}\" ON {} (\"{}\")",
|
||||||
format!("CREATE INDEX idx_{}_{} ON \"{}\" (\"{}\")",
|
table_name, idx, qualified_table, idx
|
||||||
table_name, idx, table_name, idx)
|
));
|
||||||
}))
|
}
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok((create_sql, all_indexes))
|
Ok((create_sql, all_indexes))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use common::proto::multieko2::table_structure::{
|
use common::proto::multieko2::table_structure::{
|
||||||
GetTableStructureRequest, TableColumn, TableStructureResponse,
|
GetTableStructureRequest, TableColumn, TableStructureResponse,
|
||||||
};
|
};
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::PgPool;
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
|
|
||||||
// Helper struct to map query results
|
// Helper struct to map query results
|
||||||
@@ -19,8 +19,8 @@ pub async fn get_table_structure(
|
|||||||
request: GetTableStructureRequest,
|
request: GetTableStructureRequest,
|
||||||
) -> Result<TableStructureResponse, Status> {
|
) -> Result<TableStructureResponse, Status> {
|
||||||
let profile_name = request.profile_name;
|
let profile_name = request.profile_name;
|
||||||
let table_name = request.table_name; // This should be the full table name, e.g., "2025_adresar6"
|
let table_name = request.table_name;
|
||||||
let table_schema = "public"; // Assuming tables are in the 'public' schema
|
let table_schema = "gen";
|
||||||
|
|
||||||
// 1. Validate Profile
|
// 1. Validate Profile
|
||||||
let profile = sqlx::query!(
|
let profile = sqlx::query!(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use common::proto::multieko2::tables_data::{DeleteTableDataRequest, DeleteTableDataResponse};
|
use common::proto::multieko2::tables_data::{DeleteTableDataRequest, DeleteTableDataResponse};
|
||||||
|
use crate::shared::schema_qualifier::qualify_table_name_for_data; // Import schema qualifier
|
||||||
|
|
||||||
pub async fn delete_table_data(
|
pub async fn delete_table_data(
|
||||||
db_pool: &PgPool,
|
db_pool: &PgPool,
|
||||||
@@ -36,20 +37,37 @@ pub async fn delete_table_data(
|
|||||||
return Err(Status::not_found("Table not found in profile"));
|
return Err(Status::not_found("Table not found in profile"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform soft delete
|
// Qualify table name with schema
|
||||||
|
let qualified_table = qualify_table_name_for_data(&request.table_name)?;
|
||||||
|
|
||||||
|
// Perform soft delete using qualified table name
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"UPDATE \"{}\"
|
"UPDATE {}
|
||||||
SET deleted = true
|
SET deleted = true
|
||||||
WHERE id = $1 AND deleted = false",
|
WHERE id = $1 AND deleted = false",
|
||||||
request.table_name
|
qualified_table
|
||||||
);
|
);
|
||||||
|
|
||||||
let rows_affected = sqlx::query(&query)
|
let result = sqlx::query(&query)
|
||||||
.bind(request.record_id)
|
.bind(request.record_id)
|
||||||
.execute(db_pool)
|
.execute(db_pool)
|
||||||
.await
|
.await;
|
||||||
.map_err(|e| Status::internal(format!("Delete operation failed: {}", e)))?
|
|
||||||
.rows_affected();
|
let rows_affected = match result {
|
||||||
|
Ok(result) => result.rows_affected(),
|
||||||
|
Err(e) => {
|
||||||
|
// Handle "relation does not exist" error specifically
|
||||||
|
if let Some(db_err) = e.as_database_error() {
|
||||||
|
if db_err.code() == Some(std::borrow::Cow::Borrowed("42P01")) {
|
||||||
|
return Err(Status::internal(format!(
|
||||||
|
"Table '{}' is defined but does not physically exist in the database as {}",
|
||||||
|
request.table_name, qualified_table
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(Status::internal(format!("Delete operation failed: {}", e)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(DeleteTableDataResponse {
|
Ok(DeleteTableDataResponse {
|
||||||
success: rows_affected > 0,
|
success: rows_affected > 0,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use tonic::Status;
|
|||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use common::proto::multieko2::tables_data::{GetTableDataRequest, GetTableDataResponse};
|
use common::proto::multieko2::tables_data::{GetTableDataRequest, GetTableDataResponse};
|
||||||
|
use crate::shared::schema_qualifier::qualify_table_name_for_data; // Import schema qualifier
|
||||||
|
|
||||||
pub async fn get_table_data(
|
pub async fn get_table_data(
|
||||||
db_pool: &PgPool,
|
db_pool: &PgPool,
|
||||||
@@ -69,20 +70,36 @@ pub async fn get_table_data(
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
|
// Qualify table name with schema
|
||||||
|
let qualified_table = qualify_table_name_for_data(&table_name)?;
|
||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT {} FROM \"{}\" WHERE id = $1 AND deleted = false",
|
"SELECT {} FROM {} WHERE id = $1 AND deleted = false",
|
||||||
columns_clause, table_name
|
columns_clause, qualified_table
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute query
|
// Execute query with enhanced error handling
|
||||||
let row = sqlx::query(&sql)
|
let row_result = sqlx::query(&sql)
|
||||||
.bind(record_id)
|
.bind(record_id)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
.await
|
.await;
|
||||||
.map_err(|e| match e {
|
|
||||||
sqlx::Error::RowNotFound => Status::not_found("Record not found"),
|
let row = match row_result {
|
||||||
_ => Status::internal(format!("Database error: {}", e)),
|
Ok(row) => row,
|
||||||
})?;
|
Err(sqlx::Error::RowNotFound) => return Err(Status::not_found("Record not found")),
|
||||||
|
Err(e) => {
|
||||||
|
// Handle "relation does not exist" error specifically
|
||||||
|
if let Some(db_err) = e.as_database_error() {
|
||||||
|
if db_err.code() == Some(std::borrow::Cow::Borrowed("42P01")) {
|
||||||
|
return Err(Status::internal(format!(
|
||||||
|
"Table '{}' is defined but does not physically exist in the database as {}",
|
||||||
|
table_name, qualified_table
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(Status::internal(format!("Database error: {}", e)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Build response data
|
// Build response data
|
||||||
let mut data = HashMap::new();
|
let mut data = HashMap::new();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use common::proto::multieko2::tables_data::{
|
|||||||
GetTableDataByPositionRequest, GetTableDataRequest, GetTableDataResponse
|
GetTableDataByPositionRequest, GetTableDataRequest, GetTableDataResponse
|
||||||
};
|
};
|
||||||
use super::get_table_data;
|
use super::get_table_data;
|
||||||
|
use crate::shared::schema_qualifier::qualify_table_name_for_data; // Import schema qualifier
|
||||||
|
|
||||||
pub async fn get_table_data_by_position(
|
pub async fn get_table_data_by_position(
|
||||||
db_pool: &PgPool,
|
db_pool: &PgPool,
|
||||||
@@ -27,39 +28,55 @@ pub async fn get_table_data_by_position(
|
|||||||
|
|
||||||
let profile_id = profile.ok_or_else(|| Status::not_found("Profile not found"))?.id;
|
let profile_id = profile.ok_or_else(|| Status::not_found("Profile not found"))?.id;
|
||||||
|
|
||||||
let table_exists = sqlx::query!(
|
let table_exists = sqlx::query_scalar!(
|
||||||
r#"SELECT EXISTS(
|
r#"SELECT EXISTS(
|
||||||
SELECT 1 FROM table_definitions
|
SELECT 1 FROM table_definitions
|
||||||
WHERE profile_id = $1 AND table_name = $2
|
WHERE profile_id = $1 AND table_name = $2
|
||||||
)"#,
|
) AS "exists!""#,
|
||||||
profile_id,
|
profile_id,
|
||||||
table_name
|
table_name
|
||||||
)
|
)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("Table verification error: {}", e)))?
|
.map_err(|e| Status::internal(format!("Table verification error: {}", e)))?;
|
||||||
.exists
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if !table_exists {
|
if !table_exists {
|
||||||
return Err(Status::not_found("Table not found"));
|
return Err(Status::not_found("Table not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let id: i64 = sqlx::query_scalar(
|
// Qualify table name with schema
|
||||||
|
let qualified_table = qualify_table_name_for_data(&table_name)?;
|
||||||
|
|
||||||
|
let id_result = sqlx::query_scalar(
|
||||||
&format!(
|
&format!(
|
||||||
r#"SELECT id FROM "{}"
|
r#"SELECT id FROM {}
|
||||||
WHERE deleted = FALSE
|
WHERE deleted = FALSE
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
OFFSET $1
|
OFFSET $1
|
||||||
LIMIT 1"#,
|
LIMIT 1"#,
|
||||||
table_name
|
qualified_table
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.bind(request.position - 1)
|
.bind(request.position - 1)
|
||||||
.fetch_optional(db_pool)
|
.fetch_optional(db_pool)
|
||||||
.await
|
.await;
|
||||||
.map_err(|e| Status::internal(format!("Position query failed: {}", e)))?
|
|
||||||
.ok_or_else(|| Status::not_found("Position out of bounds"))?;
|
let id: i64 = match id_result {
|
||||||
|
Ok(Some(id)) => id,
|
||||||
|
Ok(None) => return Err(Status::not_found("Position out of bounds")),
|
||||||
|
Err(e) => {
|
||||||
|
// Handle "relation does not exist" error specifically
|
||||||
|
if let Some(db_err) = e.as_database_error() {
|
||||||
|
if db_err.code() == Some(std::borrow::Cow::Borrowed("42P01")) {
|
||||||
|
return Err(Status::internal(format!(
|
||||||
|
"Table '{}' is defined but does not physically exist in the database as {}",
|
||||||
|
table_name, qualified_table
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(Status::internal(format!("Position query failed: {}", e)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
get_table_data(
|
get_table_data(
|
||||||
db_pool,
|
db_pool,
|
||||||
|
|||||||
@@ -3,59 +3,93 @@ use tonic::Status;
|
|||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use common::proto::multieko2::common::CountResponse;
|
use common::proto::multieko2::common::CountResponse;
|
||||||
use common::proto::multieko2::tables_data::GetTableDataCountRequest;
|
use common::proto::multieko2::tables_data::GetTableDataCountRequest;
|
||||||
|
use crate::shared::schema_qualifier::qualify_table_name_for_data; // 1. IMPORT THE FUNCTION
|
||||||
|
|
||||||
pub async fn get_table_data_count(
|
pub async fn get_table_data_count(
|
||||||
db_pool: &PgPool,
|
db_pool: &PgPool,
|
||||||
request: GetTableDataCountRequest,
|
request: GetTableDataCountRequest,
|
||||||
) -> Result<CountResponse, Status> {
|
) -> Result<CountResponse, Status> {
|
||||||
let profile_name = request.profile_name;
|
// We still need to verify that the table is logically defined for the profile.
|
||||||
let table_name = request.table_name;
|
// The schema qualifier handles *how* to access it physically, but this check
|
||||||
|
// ensures the request is valid in the context of the application's definitions.
|
||||||
// Lookup profile
|
|
||||||
let profile = sqlx::query!(
|
let profile = sqlx::query!(
|
||||||
"SELECT id FROM profiles WHERE name = $1",
|
"SELECT id FROM profiles WHERE name = $1",
|
||||||
profile_name
|
request.profile_name
|
||||||
)
|
)
|
||||||
.fetch_optional(db_pool)
|
.fetch_optional(db_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("Profile lookup error: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Profile lookup error for '{}': {}", request.profile_name, e)))?;
|
||||||
|
|
||||||
let profile_id = profile.ok_or_else(|| Status::not_found("Profile not found"))?.id;
|
let profile_id = match profile {
|
||||||
|
Some(p) => p.id,
|
||||||
|
None => return Err(Status::not_found(format!("Profile '{}' not found", request.profile_name))),
|
||||||
|
};
|
||||||
|
|
||||||
// Verify table exists and belongs to profile
|
let table_defined_for_profile = sqlx::query_scalar!(
|
||||||
let table_exists = sqlx::query!(
|
|
||||||
r#"SELECT EXISTS(
|
r#"SELECT EXISTS(
|
||||||
SELECT 1 FROM table_definitions
|
SELECT 1 FROM table_definitions
|
||||||
WHERE profile_id = $1 AND table_name = $2
|
WHERE profile_id = $1 AND table_name = $2
|
||||||
)"#,
|
) AS "exists!" "#, // Added AS "exists!" for clarity with sqlx macro
|
||||||
profile_id,
|
profile_id,
|
||||||
table_name
|
request.table_name
|
||||||
)
|
)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("Table verification error: {}", e)))?
|
.map_err(|e| Status::internal(format!("Table definition verification error for '{}.{}': {}", request.profile_name, request.table_name, e)))?;
|
||||||
.exists
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if !table_exists {
|
if !table_defined_for_profile {
|
||||||
return Err(Status::not_found("Table not found"));
|
// If the table isn't even defined for this profile in table_definitions,
|
||||||
|
// it's an error, regardless of whether a physical table with that name exists somewhere.
|
||||||
|
return Err(Status::not_found(format!(
|
||||||
|
"Table '{}' is not defined for profile '{}'",
|
||||||
|
request.table_name, request.profile_name
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get count of non-deleted records
|
// 2. QUALIFY THE TABLE NAME using the imported function
|
||||||
let query = format!(
|
let qualified_table_name = qualify_table_name_for_data(&request.table_name)?;
|
||||||
|
|
||||||
|
// 3. USE THE QUALIFIED NAME in the SQL query
|
||||||
|
let query_sql = format!(
|
||||||
r#"
|
r#"
|
||||||
SELECT COUNT(*) AS count
|
SELECT COUNT(*) AS count
|
||||||
FROM "{}"
|
FROM {}
|
||||||
WHERE deleted = FALSE
|
WHERE deleted = FALSE
|
||||||
"#,
|
"#,
|
||||||
table_name
|
qualified_table_name // Use the schema-qualified name here
|
||||||
);
|
);
|
||||||
|
|
||||||
let count: i64 = sqlx::query_scalar::<_, Option<i64>>(&query)
|
// The rest of the logic remains largely the same, but error messages can be more specific.
|
||||||
|
let count_result = sqlx::query_scalar::<_, Option<i64>>(&query_sql)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
.await
|
.await;
|
||||||
.map_err(|e| Status::internal(format!("Count query failed: {}", e)))?
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
Ok(CountResponse { count })
|
match count_result {
|
||||||
|
Ok(Some(count_val)) => Ok(CountResponse { count: count_val }),
|
||||||
|
Ok(None) => {
|
||||||
|
// This case should ideally not be reached with COUNT(*),
|
||||||
|
// as it always returns a row, even if the count is 0.
|
||||||
|
// If it does, it might indicate an issue or an empty table if the query was different.
|
||||||
|
// For COUNT(*), a 0 count is expected if no non-deleted rows.
|
||||||
|
Ok(CountResponse { count: 0 })
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Check if the error is "relation does not exist" (PostgreSQL error code 42P01)
|
||||||
|
if let Some(db_err) = e.as_database_error() {
|
||||||
|
if db_err.code() == Some(std::borrow::Cow::Borrowed("42P01")) {
|
||||||
|
// This means the table (e.g., gen."2025_test_schema3") does not physically exist,
|
||||||
|
// even though it was defined in table_definitions. This is an inconsistency.
|
||||||
|
return Err(Status::internal(format!(
|
||||||
|
"Table '{}' is defined but does not physically exist in the database as {}.",
|
||||||
|
request.table_name, qualified_table_name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For other errors, provide a general message.
|
||||||
|
Err(Status::internal(format!(
|
||||||
|
"Count query failed for table {}: {}",
|
||||||
|
qualified_table_name, e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use common::proto::multieko2::tables_data::{PostTableDataRequest, PostTableDataResponse};
|
use common::proto::multieko2::tables_data::{PostTableDataRequest, PostTableDataResponse};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use crate::shared::schema_qualifier::qualify_table_name_for_data; // Import schema qualifier
|
||||||
|
|
||||||
use crate::steel::server::execution::{self, Value};
|
use crate::steel::server::execution::{self, Value};
|
||||||
use crate::steel::server::functions::SteelContext;
|
use crate::steel::server::functions::SteelContext;
|
||||||
@@ -97,7 +98,7 @@ pub async fn post_table_data(
|
|||||||
// Validate all data columns
|
// Validate all data columns
|
||||||
let user_columns: Vec<&String> = columns.iter().map(|(name, _)| name).collect();
|
let user_columns: Vec<&String> = columns.iter().map(|(name, _)| name).collect();
|
||||||
for key in data.keys() {
|
for key in data.keys() {
|
||||||
if !system_columns_set.contains(key.as_str()) &&
|
if !system_columns_set.contains(key.as_str()) &&
|
||||||
!user_columns.contains(&&key.to_string()) {
|
!user_columns.contains(&&key.to_string()) {
|
||||||
return Err(Status::invalid_argument(format!("Invalid column: {}", key)));
|
return Err(Status::invalid_argument(format!("Invalid column: {}", key)));
|
||||||
}
|
}
|
||||||
@@ -123,13 +124,12 @@ pub async fn post_table_data(
|
|||||||
|
|
||||||
// Create execution context
|
// Create execution context
|
||||||
let context = SteelContext {
|
let context = SteelContext {
|
||||||
current_table: table_name.clone(),
|
current_table: table_name.clone(), // Keep base name for scripts
|
||||||
profile_id,
|
profile_id,
|
||||||
row_data: data.clone(),
|
row_data: data.clone(),
|
||||||
db_pool: Arc::new(db_pool.clone()),
|
db_pool: Arc::new(db_pool.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Execute validation script
|
// Execute validation script
|
||||||
let script_result = execution::execute_script(
|
let script_result = execution::execute_script(
|
||||||
script_record.script,
|
script_record.script,
|
||||||
@@ -220,17 +220,36 @@ pub async fn post_table_data(
|
|||||||
return Err(Status::invalid_argument("No valid columns to insert"));
|
return Err(Status::invalid_argument("No valid columns to insert"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Qualify table name with schema
|
||||||
|
let qualified_table = qualify_table_name_for_data(&table_name)?;
|
||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"INSERT INTO \"{}\" ({}) VALUES ({}) RETURNING id",
|
"INSERT INTO {} ({}) VALUES ({}) RETURNING id",
|
||||||
table_name,
|
qualified_table,
|
||||||
columns_list.join(", "),
|
columns_list.join(", "),
|
||||||
placeholders.join(", ")
|
placeholders.join(", ")
|
||||||
);
|
);
|
||||||
|
|
||||||
let inserted_id: i64 = sqlx::query_scalar_with(&sql, params)
|
// Execute query with enhanced error handling
|
||||||
|
let result = sqlx::query_scalar_with::<_, i64, _>(&sql, params)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
.await
|
.await;
|
||||||
.map_err(|e| Status::internal(format!("Insert failed: {}", e)))?;
|
|
||||||
|
let inserted_id = match result {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
// Handle "relation does not exist" error specifically
|
||||||
|
if let Some(db_err) = e.as_database_error() {
|
||||||
|
if db_err.code() == Some(std::borrow::Cow::Borrowed("42P01")) {
|
||||||
|
return Err(Status::internal(format!(
|
||||||
|
"Table '{}' is defined but does not physically exist in the database as {}",
|
||||||
|
table_name, qualified_table
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(Status::internal(format!("Insert failed: {}", e)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(PostTableDataResponse {
|
Ok(PostTableDataResponse {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use sqlx::postgres::PgArguments;
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use common::proto::multieko2::tables_data::{PutTableDataRequest, PutTableDataResponse};
|
use common::proto::multieko2::tables_data::{PutTableDataRequest, PutTableDataResponse};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use crate::shared::schema_qualifier::qualify_table_name_for_data; // Import schema qualifier
|
||||||
|
|
||||||
pub async fn put_table_data(
|
pub async fn put_table_data(
|
||||||
db_pool: &PgPool,
|
db_pool: &PgPool,
|
||||||
@@ -13,18 +14,18 @@ pub async fn put_table_data(
|
|||||||
let profile_name = request.profile_name;
|
let profile_name = request.profile_name;
|
||||||
let table_name = request.table_name;
|
let table_name = request.table_name;
|
||||||
let record_id = request.id;
|
let record_id = request.id;
|
||||||
|
|
||||||
// Preprocess and validate data
|
// Preprocess and validate 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();
|
||||||
|
|
||||||
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 key == "firma" && trimmed.is_empty() {
|
if key == "firma" && trimmed.is_empty() {
|
||||||
return Err(Status::invalid_argument("Firma cannot be empty"));
|
return Err(Status::invalid_argument("Firma cannot be empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store fields that should be set to NULL
|
// Store fields that should be set to NULL
|
||||||
if key != "firma" && trimmed.is_empty() {
|
if key != "firma" && trimmed.is_empty() {
|
||||||
null_fields.push(key);
|
null_fields.push(key);
|
||||||
@@ -103,7 +104,6 @@ pub async fn put_table_data(
|
|||||||
.ok_or_else(|| Status::invalid_argument(format!("Column not found: {}", col)))?
|
.ok_or_else(|| Status::invalid_argument(format!("Column not found: {}", col)))?
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO strong testing by user pick in the future
|
|
||||||
match sql_type {
|
match sql_type {
|
||||||
"TEXT" | "VARCHAR(15)" | "VARCHAR(255)" => {
|
"TEXT" | "VARCHAR(15)" | "VARCHAR(255)" => {
|
||||||
if let Some(max_len) = sql_type.strip_prefix("VARCHAR(")
|
if let Some(max_len) = sql_type.strip_prefix("VARCHAR(")
|
||||||
@@ -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)
|
||||||
@@ -154,25 +154,39 @@ pub async fn put_table_data(
|
|||||||
params.add(record_id)
|
params.add(record_id)
|
||||||
.map_err(|e| Status::internal(format!("Failed to add record_id parameter: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Failed to add record_id parameter: {}", e)))?;
|
||||||
|
|
||||||
|
// Qualify table name with schema
|
||||||
|
let qualified_table = qualify_table_name_for_data(&table_name)?;
|
||||||
|
|
||||||
let set_clause = set_clauses.join(", ");
|
let set_clause = set_clauses.join(", ");
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"UPDATE \"{}\" SET {} WHERE id = ${} AND deleted = FALSE RETURNING id",
|
"UPDATE {} SET {} WHERE id = ${} AND deleted = FALSE RETURNING id",
|
||||||
table_name,
|
qualified_table,
|
||||||
set_clause,
|
set_clause,
|
||||||
param_idx
|
param_idx
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = sqlx::query_scalar_with::<Postgres, i64, _>(&sql, params)
|
let result = sqlx::query_scalar_with::<Postgres, i64, _>(&sql, params)
|
||||||
.fetch_optional(db_pool)
|
.fetch_optional(db_pool)
|
||||||
.await
|
.await;
|
||||||
.map_err(|e| Status::internal(format!("Update failed: {}", e)))?;
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Some(updated_id) => Ok(PutTableDataResponse {
|
Ok(Some(updated_id)) => Ok(PutTableDataResponse {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Data updated successfully".into(),
|
message: "Data updated successfully".into(),
|
||||||
updated_id,
|
updated_id,
|
||||||
}),
|
}),
|
||||||
None => Err(Status::not_found("Record not found or already deleted")),
|
Ok(None) => Err(Status::not_found("Record not found or already deleted")),
|
||||||
|
Err(e) => {
|
||||||
|
// Handle "relation does not exist" error specifically
|
||||||
|
if let Some(db_err) = e.as_database_error() {
|
||||||
|
if db_err.code() == Some(std::borrow::Cow::Borrowed("42P01")) {
|
||||||
|
return Err(Status::internal(format!(
|
||||||
|
"Table '{}' is defined but does not physically exist in the database as {}",
|
||||||
|
table_name, qualified_table
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Status::internal(format!("Update failed: {}", e)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user