Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7213d1aa | ||
|
|
5afb427bb4 | ||
|
|
685361a11a | ||
|
|
bd7c97ca91 | ||
|
|
81235c67dc | ||
|
|
65e8e03224 | ||
|
|
85eb3adec7 |
@@ -12,17 +12,17 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::components::handlers::canvas::render_canvas;
|
use crate::components::handlers::canvas::render_canvas;
|
||||||
use crate::components::common::dialog;
|
use crate::components::common::{dialog, autocomplete}; // Added autocomplete
|
||||||
use crate::config::binds::config::EditorKeybindingMode;
|
use crate::config::binds::config::EditorKeybindingMode;
|
||||||
use crate::components::common::text_editor::TextEditor;
|
use crate::modes::handlers::mode_manager::AppMode; // For checking AppMode::Edit
|
||||||
|
|
||||||
pub fn render_add_logic(
|
pub fn render_add_logic(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
add_logic_state: &mut AddLogicState,
|
add_logic_state: &mut AddLogicState, // Changed to &mut
|
||||||
is_edit_mode: bool,
|
is_edit_mode: bool, // This is the general edit mode from EventHandler
|
||||||
highlight_state: &HighlightState,
|
highlight_state: &HighlightState,
|
||||||
) {
|
) {
|
||||||
let main_block = Block::default()
|
let main_block = Block::default()
|
||||||
@@ -35,27 +35,28 @@ pub fn render_add_logic(
|
|||||||
let inner_area = main_block.inner(area);
|
let inner_area = main_block.inner(area);
|
||||||
f.render_widget(main_block, area);
|
f.render_widget(main_block, area);
|
||||||
|
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InputScriptContent {
|
// Handle full-screen script editing
|
||||||
|
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
||||||
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
||||||
let border_style_color = if is_edit_mode { theme.highlight } else { theme.secondary };
|
let border_style_color = if is_edit_mode { theme.highlight } else { theme.secondary };
|
||||||
let border_style = Style::default().fg(border_style_color);
|
let border_style = Style::default().fg(border_style_color);
|
||||||
|
|
||||||
editor_ref.set_cursor_line_style(Style::default());
|
editor_ref.set_cursor_line_style(Style::default());
|
||||||
|
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||||
|
|
||||||
let script_title_hint = match add_logic_state.editor_keybinding_mode {
|
let script_title_hint = match add_logic_state.editor_keybinding_mode {
|
||||||
EditorKeybindingMode::Vim => {
|
EditorKeybindingMode::Vim => {
|
||||||
let vim_mode_status = TextEditor::get_vim_mode_status(&add_logic_state.vim_state);
|
let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(&add_logic_state.vim_state);
|
||||||
if is_edit_mode {
|
// Vim mode status is relevant regardless of the general `is_edit_mode`
|
||||||
format!("Script (VIM {}) - Esc for Normal. Tab navigates from Normal.", vim_mode_status)
|
format!("Script {}", vim_mode_status)
|
||||||
} else {
|
|
||||||
format!("Script (VIM {}) - 'i'/'a'/'o' for Insert. Tab to navigate.", vim_mode_status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
|
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
|
||||||
if is_edit_mode {
|
// For default/emacs, the general `is_edit_mode` (passed to this function)
|
||||||
"Script (Editing - Esc to exit edit. Tab navigates after exit.)".to_string()
|
// indicates if the text area itself is in an "editing" state.
|
||||||
|
if is_edit_mode { // This `is_edit_mode` refers to the text area's active editing.
|
||||||
|
"Script (Editing)".to_string()
|
||||||
} else {
|
} else {
|
||||||
"Script (Press Enter or Ctrl+E to edit. Tab to navigate.)".to_string()
|
"Script".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -68,19 +69,18 @@ pub fn render_add_logic(
|
|||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(border_style),
|
.border_style(border_style),
|
||||||
);
|
);
|
||||||
// Remove .widget() call - just pass the reference directly
|
|
||||||
f.render_widget(&*editor_ref, inner_area);
|
f.render_widget(&*editor_ref, inner_area);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... rest of the layout code ...
|
// Regular layout with preview
|
||||||
let main_chunks = Layout::default()
|
let main_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(3),
|
Constraint::Length(3), // Top info
|
||||||
Constraint::Length(9),
|
Constraint::Length(9), // Canvas for 3 inputs (each 1 line + 1 padding = 2 lines * 3 + 2 border = 8, +1 for good measure)
|
||||||
Constraint::Min(5),
|
Constraint::Min(5), // Script preview
|
||||||
Constraint::Length(3),
|
Constraint::Length(3), // Buttons
|
||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
@@ -89,6 +89,7 @@ pub fn render_add_logic(
|
|||||||
let script_content_area = main_chunks[2];
|
let script_content_area = main_chunks[2];
|
||||||
let buttons_area = main_chunks[3];
|
let buttons_area = main_chunks[3];
|
||||||
|
|
||||||
|
// Top info
|
||||||
let profile_text = Paragraph::new(vec![
|
let profile_text = Paragraph::new(vec![
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!("Profile: {}", add_logic_state.profile_name),
|
format!("Profile: {}", add_logic_state.profile_name),
|
||||||
@@ -114,52 +115,89 @@ pub fn render_add_logic(
|
|||||||
);
|
);
|
||||||
f.render_widget(profile_text, top_info_area);
|
f.render_widget(profile_text, top_info_area);
|
||||||
|
|
||||||
|
// Canvas
|
||||||
let focus_on_canvas_inputs = matches!(
|
let focus_on_canvas_inputs = matches!(
|
||||||
add_logic_state.current_focus,
|
add_logic_state.current_focus,
|
||||||
AddLogicFocus::InputLogicName
|
AddLogicFocus::InputLogicName
|
||||||
| AddLogicFocus::InputTargetColumn
|
| AddLogicFocus::InputTargetColumn
|
||||||
| AddLogicFocus::InputDescription
|
| AddLogicFocus::InputDescription
|
||||||
);
|
);
|
||||||
render_canvas(
|
// Call render_canvas and get the active_field_rect
|
||||||
|
let active_field_rect = render_canvas(
|
||||||
f,
|
f,
|
||||||
canvas_area,
|
canvas_area,
|
||||||
add_logic_state,
|
add_logic_state, // Pass the whole state as it impl CanvasState
|
||||||
&add_logic_state.fields(),
|
&add_logic_state.fields(),
|
||||||
&add_logic_state.current_field(),
|
&add_logic_state.current_field(),
|
||||||
&add_logic_state.inputs(),
|
&add_logic_state.inputs(),
|
||||||
theme,
|
theme,
|
||||||
is_edit_mode && focus_on_canvas_inputs,
|
is_edit_mode && focus_on_canvas_inputs, // is_edit_mode for canvas fields
|
||||||
highlight_state,
|
highlight_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- Render Autocomplete for Target Column ---
|
||||||
|
// `is_edit_mode` here refers to the general edit mode of the EventHandler
|
||||||
|
if is_edit_mode && add_logic_state.current_field() == 1 { // Target Column field
|
||||||
|
if let Some(suggestions) = add_logic_state.get_suggestions() { // Uses CanvasState impl
|
||||||
|
let selected = add_logic_state.get_selected_suggestion_index();
|
||||||
|
if !suggestions.is_empty() { // Only render if there are suggestions to show
|
||||||
|
if let Some(input_rect) = active_field_rect {
|
||||||
|
autocomplete::render_autocomplete_dropdown(
|
||||||
|
f,
|
||||||
|
input_rect,
|
||||||
|
f.area(), // Full frame area for clamping
|
||||||
|
theme,
|
||||||
|
suggestions,
|
||||||
|
selected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Script content preview
|
||||||
{
|
{
|
||||||
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
||||||
editor_ref.set_cursor_line_style(Style::default());
|
editor_ref.set_cursor_line_style(Style::default());
|
||||||
|
|
||||||
let border_style_color = if add_logic_state.current_focus == AddLogicFocus::InputScriptContent {
|
let is_script_preview_focused = add_logic_state.current_focus == AddLogicFocus::ScriptContentPreview;
|
||||||
|
|
||||||
|
if is_script_preview_focused {
|
||||||
|
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||||
|
} else {
|
||||||
|
let underscore_cursor_style = Style::default()
|
||||||
|
.add_modifier(Modifier::UNDERLINED)
|
||||||
|
.fg(theme.secondary);
|
||||||
|
editor_ref.set_cursor_style(underscore_cursor_style);
|
||||||
|
}
|
||||||
|
|
||||||
|
let border_style_color = if is_script_preview_focused {
|
||||||
theme.highlight
|
theme.highlight
|
||||||
} else {
|
} else {
|
||||||
theme.secondary
|
theme.secondary
|
||||||
};
|
};
|
||||||
|
|
||||||
let title_hint = match add_logic_state.editor_keybinding_mode {
|
let title_text = "Script Preview"; // Title doesn't need to change based on focus here
|
||||||
EditorKeybindingMode::Vim => "Script Preview (VIM - Focus with Tab, then 'i'/'a'/'o' to edit)",
|
|
||||||
_ => "Script Preview (Focus with Tab, then Enter/Ctrl+E to edit)",
|
let title_style = if is_script_preview_focused {
|
||||||
|
Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(theme.fg)
|
||||||
};
|
};
|
||||||
|
|
||||||
editor_ref.set_block(
|
editor_ref.set_block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title(title_hint)
|
.title(Span::styled(title_text, title_style))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(Style::default().fg(border_style_color)),
|
.border_style(Style::default().fg(border_style_color)),
|
||||||
);
|
);
|
||||||
// Remove .widget() call here too
|
|
||||||
f.render_widget(&*editor_ref, script_content_area);
|
f.render_widget(&*editor_ref, script_content_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
let get_button_style = |button_focus: AddLogicFocus, current_focus| {
|
// Buttons
|
||||||
let is_focused = current_focus == button_focus;
|
let get_button_style = |button_focus: AddLogicFocus, current_focus_state: AddLogicFocus| {
|
||||||
|
let is_focused = current_focus_state == button_focus;
|
||||||
let base_style = Style::default().fg(if is_focused {
|
let base_style = Style::default().fg(if is_focused {
|
||||||
theme.highlight
|
theme.highlight
|
||||||
} else {
|
} else {
|
||||||
@@ -172,11 +210,11 @@ pub fn render_add_logic(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let get_button_border_style = |is_focused: bool, theme: &Theme| {
|
let get_button_border_style = |is_focused: bool, current_theme: &Theme| {
|
||||||
if is_focused {
|
if is_focused {
|
||||||
Style::default().fg(theme.highlight)
|
Style::default().fg(current_theme.highlight)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme.secondary)
|
Style::default().fg(current_theme.secondary)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -222,6 +260,7 @@ pub fn render_add_logic(
|
|||||||
);
|
);
|
||||||
f.render_widget(cancel_button, button_chunks[1]);
|
f.render_widget(cancel_button, button_chunks[1]);
|
||||||
|
|
||||||
|
// Dialog
|
||||||
if app_state.ui.dialog.dialog_show {
|
if app_state.ui.dialog.dialog_show {
|
||||||
dialog::render_dialog(
|
dialog::render_dialog(
|
||||||
f,
|
f,
|
||||||
|
|||||||
@@ -1,277 +1,135 @@
|
|||||||
// src/functions/modes/edit/add_logic_e.rs
|
// src/functions/modes/edit/add_logic_e.rs
|
||||||
use crate::state::pages::add_logic::AddLogicState; // Changed
|
use crate::state::pages::add_logic::AddLogicState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use crate::state::pages::canvas_state::CanvasState;
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
|
||||||
// Word navigation helpers (get_char_type, find_next_word_start, etc.)
|
|
||||||
// can be kept as they are generic.
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
enum CharType {
|
|
||||||
Whitespace,
|
|
||||||
Alphanumeric,
|
|
||||||
Punctuation,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_char_type(c: char) -> CharType {
|
|
||||||
if c.is_whitespace() { CharType::Whitespace }
|
|
||||||
else if c.is_alphanumeric() { CharType::Alphanumeric }
|
|
||||||
else { CharType::Punctuation }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
|
||||||
let len = chars.len();
|
|
||||||
if len == 0 || current_pos >= len { return len; }
|
|
||||||
let mut pos = current_pos;
|
|
||||||
let initial_type = get_char_type(chars[pos]);
|
|
||||||
while pos < len && get_char_type(chars[pos]) == initial_type { pos += 1; }
|
|
||||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace { pos += 1; }
|
|
||||||
pos
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
|
||||||
let len = chars.len();
|
|
||||||
if len == 0 { return 0; }
|
|
||||||
let mut pos = current_pos.min(len - 1);
|
|
||||||
if get_char_type(chars[pos]) == CharType::Whitespace {
|
|
||||||
pos = find_next_word_start(text, pos);
|
|
||||||
}
|
|
||||||
if pos >= len { return len.saturating_sub(1); }
|
|
||||||
let word_type = get_char_type(chars[pos]);
|
|
||||||
while pos < len && get_char_type(chars[pos]) == word_type { pos += 1; }
|
|
||||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
|
||||||
if chars.is_empty() || current_pos == 0 { return 0; }
|
|
||||||
let mut pos = current_pos.saturating_sub(1);
|
|
||||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { pos -= 1; }
|
|
||||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { return 0; }
|
|
||||||
let word_type = get_char_type(chars[pos]);
|
|
||||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; }
|
|
||||||
pos
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
|
||||||
let len = chars.len();
|
|
||||||
if len == 0 || current_pos == 0 { return 0; }
|
|
||||||
let mut pos = current_pos.saturating_sub(1);
|
|
||||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { pos -= 1; }
|
|
||||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { return 0; }
|
|
||||||
if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace { return 0; }
|
|
||||||
let word_type = get_char_type(chars[pos]);
|
|
||||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; }
|
|
||||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace { pos -= 1; }
|
|
||||||
if pos > 0 { pos - 1 } else { 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Executes edit actions for the AddLogic view canvas.
|
|
||||||
pub async fn execute_edit_action(
|
pub async fn execute_edit_action(
|
||||||
action: &str,
|
action: &str,
|
||||||
key: KeyEvent,
|
key: KeyEvent, // Keep key for insert_char
|
||||||
state: &mut AddLogicState, // Changed
|
state: &mut AddLogicState,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
let mut message = String::new();
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
"insert_char" => {
|
|
||||||
if let KeyCode::Char(c) = key.code {
|
|
||||||
let cursor_pos = state.current_cursor_pos();
|
|
||||||
let field_value = state.get_current_input_mut();
|
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
|
||||||
if cursor_pos <= chars.len() {
|
|
||||||
chars.insert(cursor_pos, c);
|
|
||||||
*field_value = chars.into_iter().collect();
|
|
||||||
state.set_current_cursor_pos(cursor_pos + 1);
|
|
||||||
state.set_has_unsaved_changes(true);
|
|
||||||
*ideal_cursor_column = state.current_cursor_pos();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Ok("Error: insert_char called without a char key.".to_string());
|
|
||||||
}
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
|
||||||
"delete_char_backward" => {
|
|
||||||
if state.current_cursor_pos() > 0 {
|
|
||||||
let cursor_pos = state.current_cursor_pos();
|
|
||||||
let field_value = state.get_current_input_mut();
|
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
|
||||||
if cursor_pos <= chars.len() {
|
|
||||||
chars.remove(cursor_pos - 1);
|
|
||||||
*field_value = chars.into_iter().collect();
|
|
||||||
let new_pos = cursor_pos - 1;
|
|
||||||
state.set_current_cursor_pos(new_pos);
|
|
||||||
state.set_has_unsaved_changes(true);
|
|
||||||
*ideal_cursor_column = new_pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
|
||||||
"delete_char_forward" => {
|
|
||||||
let cursor_pos = state.current_cursor_pos();
|
|
||||||
let field_value = state.get_current_input_mut();
|
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
|
||||||
if cursor_pos < chars.len() {
|
|
||||||
chars.remove(cursor_pos);
|
|
||||||
*field_value = chars.into_iter().collect();
|
|
||||||
state.set_has_unsaved_changes(true);
|
|
||||||
*ideal_cursor_column = cursor_pos;
|
|
||||||
}
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
|
||||||
"next_field" => {
|
"next_field" => {
|
||||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed
|
let current_field = state.current_field();
|
||||||
if num_fields > 0 {
|
let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT;
|
||||||
let current_field = state.current_field();
|
state.set_current_field(next_field);
|
||||||
let last_field_index = num_fields - 1;
|
*ideal_cursor_column = state.current_cursor_pos();
|
||||||
if current_field < last_field_index { // Prevent cycling
|
message = format!("Focus on field {}", state.fields()[next_field]);
|
||||||
state.set_current_field(current_field + 1);
|
|
||||||
}
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
}
|
||||||
"prev_field" => {
|
"prev_field" => {
|
||||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed
|
let current_field = state.current_field();
|
||||||
if num_fields > 0 {
|
let prev_field = if current_field == 0 {
|
||||||
let current_field = state.current_field();
|
AddLogicState::INPUT_FIELD_COUNT - 1
|
||||||
if current_field > 0 { // Prevent cycling
|
} else {
|
||||||
state.set_current_field(current_field - 1);
|
current_field - 1
|
||||||
}
|
};
|
||||||
let current_input = state.get_current_input();
|
state.set_current_field(prev_field);
|
||||||
let max_pos = current_input.len();
|
*ideal_cursor_column = state.current_cursor_pos();
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
message = format!("Focus on field {}", state.fields()[prev_field]);
|
||||||
|
}
|
||||||
|
"delete_char_forward" => {
|
||||||
|
let current_pos = state.current_cursor_pos();
|
||||||
|
let current_input_mut = state.get_current_input_mut();
|
||||||
|
if current_pos < current_input_mut.len() {
|
||||||
|
current_input_mut.remove(current_pos);
|
||||||
|
state.set_has_unsaved_changes(true);
|
||||||
|
if state.current_field() == 1 { state.update_target_column_suggestions(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"delete_char_backward" => {
|
||||||
|
let current_pos = state.current_cursor_pos();
|
||||||
|
if current_pos > 0 {
|
||||||
|
let new_pos = current_pos - 1;
|
||||||
|
state.get_current_input_mut().remove(new_pos);
|
||||||
|
state.set_current_cursor_pos(new_pos);
|
||||||
|
*ideal_cursor_column = new_pos;
|
||||||
|
state.set_has_unsaved_changes(true);
|
||||||
|
if state.current_field() == 1 { state.update_target_column_suggestions(); }
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
|
||||||
}
|
}
|
||||||
"move_left" => {
|
"move_left" => {
|
||||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
let current_pos = state.current_cursor_pos();
|
||||||
state.set_current_cursor_pos(new_pos);
|
if current_pos > 0 {
|
||||||
*ideal_cursor_column = new_pos;
|
let new_pos = current_pos - 1;
|
||||||
Ok("".to_string())
|
state.set_current_cursor_pos(new_pos);
|
||||||
|
*ideal_cursor_column = new_pos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"move_right" => {
|
"move_right" => {
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let current_pos = state.current_cursor_pos();
|
let current_pos = state.current_cursor_pos();
|
||||||
if current_pos < current_input.len() {
|
let input_len = state.get_current_input().len();
|
||||||
|
if current_pos < input_len {
|
||||||
let new_pos = current_pos + 1;
|
let new_pos = current_pos + 1;
|
||||||
state.set_current_cursor_pos(new_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
*ideal_cursor_column = new_pos;
|
*ideal_cursor_column = new_pos;
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
|
||||||
}
|
}
|
||||||
"move_up" => { // In edit mode, up/down usually means prev/next field
|
"insert_char" => {
|
||||||
let current_field = state.current_field();
|
if let KeyCode::Char(c) = key.code {
|
||||||
if current_field > 0 {
|
let current_pos = state.current_cursor_pos();
|
||||||
let new_field = current_field - 1;
|
state.get_current_input_mut().insert(current_pos, c);
|
||||||
state.set_current_field(new_field);
|
let new_pos = current_pos + 1;
|
||||||
let current_input = state.get_current_input();
|
state.set_current_cursor_pos(new_pos);
|
||||||
let max_pos = current_input.len();
|
*ideal_cursor_column = new_pos;
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
state.set_has_unsaved_changes(true);
|
||||||
}
|
if state.current_field() == 1 {
|
||||||
Ok("".to_string())
|
state.update_target_column_suggestions();
|
||||||
}
|
|
||||||
"move_down" => { // In edit mode, up/down usually means prev/next field
|
|
||||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed
|
|
||||||
if num_fields > 0 {
|
|
||||||
let current_field = state.current_field();
|
|
||||||
let last_field_index = num_fields - 1;
|
|
||||||
if current_field < last_field_index {
|
|
||||||
let new_field = current_field + 1;
|
|
||||||
state.set_current_field(new_field);
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
|
||||||
}
|
}
|
||||||
"move_line_start" => {
|
"suggestion_down" => {
|
||||||
state.set_current_cursor_pos(0);
|
if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() {
|
||||||
*ideal_cursor_column = 0;
|
let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0);
|
||||||
Ok("".to_string())
|
let next_selection = (current_selection + 1) % state.target_column_suggestions.len();
|
||||||
}
|
state.selected_target_column_suggestion_index = Some(next_selection);
|
||||||
"move_line_end" => {
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let new_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos(new_pos);
|
|
||||||
*ideal_cursor_column = new_pos;
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
|
||||||
"move_first_line" => {
|
|
||||||
if AddLogicState::INPUT_FIELD_COUNT > 0 { // Changed
|
|
||||||
state.set_current_field(0);
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
|
||||||
}
|
}
|
||||||
"move_last_line" => {
|
"suggestion_up" => {
|
||||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed
|
if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() {
|
||||||
if num_fields > 0 {
|
let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0);
|
||||||
let new_field = num_fields - 1;
|
let prev_selection = if current_selection == 0 {
|
||||||
state.set_current_field(new_field);
|
state.target_column_suggestions.len() - 1
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
|
||||||
"move_word_next" => {
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
if !current_input.is_empty() {
|
|
||||||
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
|
|
||||||
let final_pos = new_pos.min(current_input.len());
|
|
||||||
state.set_current_cursor_pos(final_pos);
|
|
||||||
*ideal_cursor_column = final_pos;
|
|
||||||
}
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
|
||||||
"move_word_end" => {
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
if !current_input.is_empty() {
|
|
||||||
let current_pos = state.current_cursor_pos();
|
|
||||||
let new_pos = find_word_end(current_input, current_pos);
|
|
||||||
let final_pos = if new_pos == current_pos && current_pos < current_input.len() { // Ensure not to go past end
|
|
||||||
find_word_end(current_input, current_pos + 1)
|
|
||||||
} else {
|
} else {
|
||||||
new_pos
|
current_selection - 1
|
||||||
};
|
};
|
||||||
let max_valid_index = current_input.len(); // Allow cursor at end
|
state.selected_target_column_suggestion_index = Some(prev_selection);
|
||||||
let clamped_pos = final_pos.min(max_valid_index);
|
|
||||||
state.set_current_cursor_pos(clamped_pos);
|
|
||||||
*ideal_cursor_column = clamped_pos;
|
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
|
||||||
}
|
}
|
||||||
"move_word_prev" => {
|
"select_suggestion" => {
|
||||||
let current_input = state.get_current_input();
|
if state.in_target_column_suggestion_mode {
|
||||||
if !current_input.is_empty() {
|
let mut selected_suggestion_text: Option<String> = None;
|
||||||
let new_pos = find_prev_word_start(current_input, state.current_cursor_pos());
|
|
||||||
state.set_current_cursor_pos(new_pos);
|
|
||||||
*ideal_cursor_column = new_pos;
|
|
||||||
}
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
|
||||||
"move_word_end_prev" => {
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
if !current_input.is_empty() {
|
|
||||||
let new_pos = find_prev_word_end(current_input, state.current_cursor_pos());
|
|
||||||
state.set_current_cursor_pos(new_pos);
|
|
||||||
*ideal_cursor_column = new_pos;
|
|
||||||
}
|
|
||||||
Ok("".to_string())
|
|
||||||
}
|
|
||||||
"exit_edit_mode" | "save" | "revert" => {
|
|
||||||
Ok("Action handled by main loop".to_string())
|
|
||||||
}
|
|
||||||
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if let Some(selected_idx) = state.selected_target_column_suggestion_index {
|
||||||
|
if let Some(suggestion) = state.target_column_suggestions.get(selected_idx) {
|
||||||
|
selected_suggestion_text = Some(suggestion.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(suggestion_text) = selected_suggestion_text {
|
||||||
|
state.target_column_input = suggestion_text.clone();
|
||||||
|
state.target_column_cursor_pos = state.target_column_input.len();
|
||||||
|
*ideal_cursor_column = state.target_column_cursor_pos;
|
||||||
|
state.set_has_unsaved_changes(true);
|
||||||
|
message = format!("Selected column: '{}'", suggestion_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.in_target_column_suggestion_mode = false;
|
||||||
|
state.show_target_column_suggestions = false;
|
||||||
|
state.selected_target_column_suggestion_index = None;
|
||||||
|
state.update_target_column_suggestions();
|
||||||
|
} else {
|
||||||
|
let current_field = state.current_field();
|
||||||
|
let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT;
|
||||||
|
state.set_current_field(next_field);
|
||||||
|
*ideal_cursor_column = state.current_cursor_pos();
|
||||||
|
message = format!("Focus on field {}", state.fields()[next_field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
|||||||
use crate::services::GrpcClient;
|
use crate::services::GrpcClient;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use common::proto::multieko2::table_script::PostTableScriptRequest;
|
|
||||||
use crate::components::common::text_editor::TextEditor;
|
use crate::components::common::text_editor::TextEditor;
|
||||||
use tui_textarea::Input as TextAreaInput;
|
|
||||||
|
|
||||||
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
||||||
|
|
||||||
@@ -27,236 +25,214 @@ pub fn handle_add_logic_navigation(
|
|||||||
save_logic_sender: SaveLogicResultSender,
|
save_logic_sender: SaveLogicResultSender,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let mut handled = false;
|
// === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION ===
|
||||||
let general_action = config.get_general_action(key_event.code, key_event.modifiers);
|
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
||||||
|
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InputScriptContent {
|
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
|
|
||||||
match add_logic_state.editor_keybinding_mode {
|
// Handle ONLY Escape to exit fullscreen mode
|
||||||
EditorKeybindingMode::Vim => {
|
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
|
||||||
if *is_edit_mode { // App considers textarea to be in "typing" (Insert) mode
|
match add_logic_state.editor_keybinding_mode {
|
||||||
let changed = TextEditor::handle_input(
|
EditorKeybindingMode::Vim => {
|
||||||
&mut editor_borrow,
|
if *is_edit_mode {
|
||||||
key_event,
|
// First escape: try to go to Vim Normal mode
|
||||||
&add_logic_state.editor_keybinding_mode,
|
TextEditor::handle_input(
|
||||||
&mut add_logic_state.vim_state,
|
&mut editor_borrow,
|
||||||
);
|
key_event,
|
||||||
if changed { add_logic_state.has_unsaved_changes = true; }
|
&add_logic_state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_state.vim_state,
|
||||||
// Check if we've transitioned to Normal mode
|
|
||||||
if key_event.code == KeyCode::Esc && TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
|
|
||||||
*is_edit_mode = false;
|
|
||||||
*command_message = "VIM: Normal Mode. Tab to navigate.".to_string();
|
|
||||||
}
|
|
||||||
handled = true;
|
|
||||||
} else { // App considers textarea to be in "navigation" (Normal) mode
|
|
||||||
match key_event.code {
|
|
||||||
// Keys to enter Vim Insert mode
|
|
||||||
KeyCode::Char('i') | KeyCode::Char('a') | KeyCode::Char('o') |
|
|
||||||
KeyCode::Char('I') | KeyCode::Char('A') | KeyCode::Char('O') => {
|
|
||||||
*is_edit_mode = true;
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state
|
|
||||||
);
|
|
||||||
*command_message = "VIM: Insert Mode.".to_string();
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if general_action.is_none() {
|
|
||||||
let changed = TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
if changed { add_logic_state.has_unsaved_changes = true; }
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
|
|
||||||
if *is_edit_mode {
|
|
||||||
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
|
|
||||||
*is_edit_mode = false;
|
|
||||||
*command_message = "Exited script edit. Tab to navigate.".to_string();
|
|
||||||
handled = true;
|
|
||||||
} else if general_action.is_some() && (general_action.unwrap() == "next_field" || general_action.unwrap() == "prev_field") {
|
|
||||||
let changed = TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state
|
|
||||||
);
|
);
|
||||||
if changed { add_logic_state.has_unsaved_changes = true; }
|
if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
|
||||||
handled = true;
|
*is_edit_mode = false;
|
||||||
|
*command_message = "VIM: Normal Mode. Esc again to exit script.".to_string();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let changed = TextEditor::handle_input(
|
// Second escape: exit fullscreen
|
||||||
&mut editor_borrow,
|
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
key_event,
|
app_state.ui.focus_outside_canvas = true;
|
||||||
&add_logic_state.editor_keybinding_mode,
|
*is_edit_mode = false;
|
||||||
&mut add_logic_state.vim_state
|
*command_message = "Exited script editing.".to_string();
|
||||||
);
|
}
|
||||||
if changed { add_logic_state.has_unsaved_changes = true; }
|
}
|
||||||
handled = true;
|
_ => {
|
||||||
|
if *is_edit_mode {
|
||||||
|
*is_edit_mode = false;
|
||||||
|
*command_message = "Exited script edit. Esc again to exit script.".to_string();
|
||||||
|
} else {
|
||||||
|
// Exit fullscreen
|
||||||
|
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
|
app_state.ui.focus_outside_canvas = true;
|
||||||
|
*is_edit_mode = false;
|
||||||
|
*command_message = "Exited script editing.".to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if handled { return true; }
|
|
||||||
|
// ALL OTHER KEYS: Pass directly to textarea without any interference
|
||||||
|
let changed = TextEditor::handle_input(
|
||||||
|
&mut editor_borrow,
|
||||||
|
key_event,
|
||||||
|
&add_logic_state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_state.vim_state,
|
||||||
|
);
|
||||||
|
if changed {
|
||||||
|
add_logic_state.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update edit mode status for Vim
|
||||||
|
if add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim {
|
||||||
|
*is_edit_mode = !TextEditor::is_vim_normal_mode(&add_logic_state.vim_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Always consume the event in fullscreen mode
|
||||||
}
|
}
|
||||||
|
// === END FULLSCREEN ISOLATION ===
|
||||||
|
|
||||||
// If not handled above (e.g., Tab/Shift+Tab, or Enter when script content not in edit mode),
|
// Regular navigation logic for non-fullscreen elements
|
||||||
// process general application-level actions.
|
let action = config.get_general_action(key_event.code, key_event.modifiers);
|
||||||
let action_str = general_action.map(String::from);
|
let current_focus = add_logic_state.current_focus;
|
||||||
match action_str.as_deref() {
|
let mut handled = true;
|
||||||
Some("exit_view") | Some("cancel_action") => {
|
let mut new_focus = current_focus;
|
||||||
buffer_state.update_history(AppView::Admin);
|
|
||||||
app_state.ui.show_add_logic = false;
|
match action.as_deref() {
|
||||||
*command_message = "Exited Add Logic".to_string();
|
Some("exit_table_scroll") => {
|
||||||
*is_edit_mode = false;
|
// This shouldn't happen since we handle InsideScriptContent above
|
||||||
handled = true;
|
handled = false;
|
||||||
}
|
}
|
||||||
Some("next_field") | Some("prev_field") => {
|
Some("move_up") => {
|
||||||
let is_next = action_str.as_deref() == Some("next_field");
|
match current_focus {
|
||||||
let previous_focus = add_logic_state.current_focus;
|
AddLogicFocus::InputLogicName => {
|
||||||
|
// Stay at top
|
||||||
add_logic_state.current_focus = if is_next {
|
|
||||||
match add_logic_state.current_focus {
|
|
||||||
AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn,
|
|
||||||
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription,
|
|
||||||
AddLogicFocus::InputDescription => AddLogicFocus::InputScriptContent,
|
|
||||||
AddLogicFocus::InputScriptContent => AddLogicFocus::SaveButton,
|
|
||||||
AddLogicFocus::SaveButton => AddLogicFocus::CancelButton,
|
|
||||||
AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName,
|
|
||||||
}
|
}
|
||||||
} else {
|
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
|
||||||
match add_logic_state.current_focus {
|
AddLogicFocus::InputDescription => new_focus = AddLogicFocus::InputTargetColumn,
|
||||||
AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton,
|
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
|
||||||
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName,
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
||||||
AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn,
|
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
||||||
AddLogicFocus::InputScriptContent => AddLogicFocus::InputDescription,
|
_ => handled = false,
|
||||||
AddLogicFocus::SaveButton => AddLogicFocus::InputScriptContent,
|
|
||||||
AddLogicFocus::CancelButton => AddLogicFocus::SaveButton,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InputScriptContent {
|
|
||||||
*is_edit_mode = false;
|
|
||||||
let mode_hint = match add_logic_state.editor_keybinding_mode {
|
|
||||||
EditorKeybindingMode::Vim => "'i'/'a'/'o' to insert",
|
|
||||||
_ => "Enter/Ctrl+E to edit",
|
|
||||||
};
|
|
||||||
*command_message = format!("Focus: Script Content. Press {} or Tab.", mode_hint);
|
|
||||||
} else if matches!(add_logic_state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) {
|
|
||||||
*is_edit_mode = true;
|
|
||||||
*command_message = format!("Focus: {:?}. Edit mode ON.", add_logic_state.current_focus);
|
|
||||||
} else {
|
|
||||||
*is_edit_mode = false;
|
|
||||||
*command_message = format!("Focus: {:?}", add_logic_state.current_focus);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
app_state.ui.focus_outside_canvas = !matches!(
|
Some("move_down") => {
|
||||||
add_logic_state.current_focus,
|
match current_focus {
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent
|
AddLogicFocus::InputLogicName => new_focus = AddLogicFocus::InputTargetColumn,
|
||||||
);
|
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputDescription,
|
||||||
handled = true;
|
AddLogicFocus::InputDescription => {
|
||||||
|
add_logic_state.last_canvas_field = 2;
|
||||||
|
new_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
|
},
|
||||||
|
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
|
||||||
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
||||||
|
AddLogicFocus::CancelButton => {
|
||||||
|
// Stay at bottom
|
||||||
|
}
|
||||||
|
_ => handled = false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("next_option") => {
|
||||||
|
match current_focus {
|
||||||
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
|
||||||
|
{ new_focus = AddLogicFocus::ScriptContentPreview; }
|
||||||
|
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
|
||||||
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
||||||
|
AddLogicFocus::CancelButton => { /* Stay at last */ }
|
||||||
|
_ => handled = false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("previous_option") => {
|
||||||
|
match current_focus {
|
||||||
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
|
||||||
|
{ /* Stay at first */ }
|
||||||
|
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
|
||||||
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
||||||
|
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
||||||
|
_ => handled = false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("next_field") => {
|
||||||
|
new_focus = match current_focus {
|
||||||
|
AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn,
|
||||||
|
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription,
|
||||||
|
AddLogicFocus::InputDescription => AddLogicFocus::ScriptContentPreview,
|
||||||
|
AddLogicFocus::ScriptContentPreview => AddLogicFocus::SaveButton,
|
||||||
|
AddLogicFocus::SaveButton => AddLogicFocus::CancelButton,
|
||||||
|
AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName,
|
||||||
|
_ => current_focus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some("prev_field") => {
|
||||||
|
new_focus = match current_focus {
|
||||||
|
AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton,
|
||||||
|
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName,
|
||||||
|
AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn,
|
||||||
|
AddLogicFocus::ScriptContentPreview => AddLogicFocus::InputDescription,
|
||||||
|
AddLogicFocus::SaveButton => AddLogicFocus::ScriptContentPreview,
|
||||||
|
AddLogicFocus::CancelButton => AddLogicFocus::SaveButton,
|
||||||
|
_ => current_focus,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
match add_logic_state.current_focus {
|
match current_focus {
|
||||||
AddLogicFocus::InputScriptContent => {
|
AddLogicFocus::ScriptContentPreview => {
|
||||||
*is_edit_mode = true;
|
new_focus = AddLogicFocus::InsideScriptContent;
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
*is_edit_mode = false; // Start in preview mode
|
||||||
match add_logic_state.editor_keybinding_mode {
|
app_state.ui.focus_outside_canvas = false; // Script is like canvas
|
||||||
EditorKeybindingMode::Vim => {
|
let mode_hint = match add_logic_state.editor_keybinding_mode {
|
||||||
TextEditor::handle_input(
|
EditorKeybindingMode::Vim => "VIM mode - 'i'/'a'/'o' to edit",
|
||||||
&mut editor_borrow,
|
_ => "Enter/Ctrl+E to edit",
|
||||||
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
|
};
|
||||||
&add_logic_state.editor_keybinding_mode,
|
*command_message = format!("Fullscreen script editing. {} or Esc to exit.", mode_hint);
|
||||||
&mut add_logic_state.vim_state,
|
}
|
||||||
);
|
AddLogicFocus::SaveButton => {
|
||||||
*command_message = "VIM: Insert Mode.".to_string();
|
*command_message = "Save logic action".to_string();
|
||||||
}
|
}
|
||||||
_ => {
|
AddLogicFocus::CancelButton => {
|
||||||
*command_message = "Entered script edit mode.".to_string();
|
buffer_state.update_history(AppView::Admin);
|
||||||
}
|
app_state.ui.show_add_logic = false;
|
||||||
}
|
*command_message = "Cancelled Add Logic".to_string();
|
||||||
handled = true;
|
*is_edit_mode = false;
|
||||||
}
|
}
|
||||||
AddLogicFocus::SaveButton => { handled = true; }
|
|
||||||
AddLogicFocus::CancelButton => { *is_edit_mode = false; handled = true; }
|
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
||||||
*is_edit_mode = !*is_edit_mode;
|
*is_edit_mode = !*is_edit_mode;
|
||||||
*command_message = format!("Field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
*command_message = format!("Field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
|
_ => handled = false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("toggle_edit_mode") => {
|
Some("toggle_edit_mode") => {
|
||||||
match add_logic_state.current_focus {
|
match current_focus {
|
||||||
AddLogicFocus::InputScriptContent => {
|
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
match add_logic_state.editor_keybinding_mode {
|
|
||||||
EditorKeybindingMode::Vim => {
|
|
||||||
if *is_edit_mode {
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
|
|
||||||
*is_edit_mode = false;
|
|
||||||
*command_message = "VIM: Normal Mode. Tab to navigate.".to_string();
|
|
||||||
} else {
|
|
||||||
*command_message = "VIM: Still in Insert Mode (toggle error?).".to_string();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
*is_edit_mode = true;
|
|
||||||
*command_message = "VIM: Insert Mode.".to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
*is_edit_mode = !*is_edit_mode;
|
|
||||||
*command_message = format!("Script edit mode: {}", if *is_edit_mode { "ON" } else { "OFF. Tab to navigate." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
||||||
*is_edit_mode = !*is_edit_mode;
|
*is_edit_mode = !*is_edit_mode;
|
||||||
*command_message = format!("Canvas field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
*command_message = format!("Canvas field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
_ => { *command_message = "Cannot toggle edit mode here.".to_string(); handled = true; }
|
_ => {
|
||||||
|
*command_message = "Cannot toggle edit mode here.".to_string();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => handled = false,
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InputScriptContent &&
|
}
|
||||||
!*is_edit_mode &&
|
|
||||||
add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim {
|
if handled && current_focus != new_focus {
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
add_logic_state.current_focus = new_focus;
|
||||||
let changed = TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
// Set edit mode and canvas focus based on new focus
|
||||||
key_event,
|
let new_is_canvas_input_focus = matches!(new_focus,
|
||||||
&add_logic_state.editor_keybinding_mode,
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
|
||||||
&mut add_logic_state.vim_state
|
);
|
||||||
);
|
|
||||||
if changed { add_logic_state.has_unsaved_changes = true; }
|
if new_is_canvas_input_focus {
|
||||||
handled = true;
|
// Entering canvas - start in readonly mode
|
||||||
|
*is_edit_mode = false;
|
||||||
|
app_state.ui.focus_outside_canvas = false;
|
||||||
|
} else {
|
||||||
|
// Outside canvas
|
||||||
|
app_state.ui.focus_outside_canvas = true;
|
||||||
|
if matches!(new_focus, AddLogicFocus::ScriptContentPreview) {
|
||||||
|
*is_edit_mode = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::config::binds::config::Config;
|
|||||||
use crate::state::app::buffer::{BufferState, AppView};
|
use crate::state::app::buffer::{BufferState, AppView};
|
||||||
use crate::state::pages::add_table::{AddTableState, LinkDefinition};
|
use crate::state::pages::add_table::{AddTableState, LinkDefinition};
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use crate::state::pages::add_logic::AddLogicState;
|
use crate::state::pages::add_logic::{AddLogicState, AddLogicFocus}; // Added AddLogicFocus import
|
||||||
|
|
||||||
// Helper functions list_select_next and list_select_previous remain the same
|
// Helper functions list_select_next and list_select_previous remain the same
|
||||||
fn list_select_next(list_state: &mut ListState, item_count: usize) {
|
fn list_select_next(list_state: &mut ListState, item_count: usize) {
|
||||||
@@ -90,11 +90,11 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
admin_state.selected_profile_index = admin_state.profile_list_state.selected();
|
admin_state.selected_profile_index = admin_state.profile_list_state.selected();
|
||||||
admin_state.selected_table_index = None;
|
admin_state.selected_table_index = None; // Deselect table when profile changes
|
||||||
if let Some(profile_idx) = admin_state.selected_profile_index {
|
if let Some(profile_idx) = admin_state.selected_profile_index {
|
||||||
if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) {
|
if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) {
|
||||||
if !profile.tables.is_empty() {
|
if !profile.tables.is_empty() {
|
||||||
admin_state.table_list_state.select(Some(0));
|
admin_state.table_list_state.select(Some(0)); // Auto-select first table for nav
|
||||||
} else {
|
} else {
|
||||||
admin_state.table_list_state.select(None);
|
admin_state.table_list_state.select(None);
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ pub fn handle_admin_navigation(
|
|||||||
} else {
|
} else {
|
||||||
*command_message = "No tables in selected profile.".to_string();
|
*command_message = "No tables in selected profile.".to_string();
|
||||||
}
|
}
|
||||||
admin_state.current_focus = AdminFocus::Tables;
|
admin_state.current_focus = AdminFocus::Tables; // Stay in Tables pane if no tables to enter
|
||||||
}
|
}
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@@ -205,10 +205,9 @@ pub fn handle_admin_navigation(
|
|||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("select") => {
|
Some("select") => { // This is for persistently selecting a table with [*]
|
||||||
admin_state.selected_table_index = admin_state.table_list_state.selected();
|
admin_state.selected_table_index = admin_state.table_list_state.selected();
|
||||||
let table_name = admin_state.selected_profile_index
|
let table_name = admin_state.selected_profile_index
|
||||||
.or_else(|| admin_state.profile_list_state.selected())
|
|
||||||
.and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx))
|
.and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx))
|
||||||
.and_then(|p| admin_state.selected_table_index.and_then(|t_idx| p.tables.get(t_idx)))
|
.and_then(|p| admin_state.selected_table_index.and_then(|t_idx| p.tables.get(t_idx)))
|
||||||
.map_or("N/A", |t| t.name.as_str());
|
.map_or("N/A", |t| t.name.as_str());
|
||||||
@@ -224,30 +223,44 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AdminFocus::Button1 => {
|
AdminFocus::Button1 => { // Add Logic Button
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("select") => {
|
Some("select") => { // Typically "Enter" key
|
||||||
let mut logic_state_profile_name = "None (Global)".to_string();
|
|
||||||
let mut selected_table_name_for_logic: Option<String> = None;
|
|
||||||
if let Some(p_idx) = admin_state.selected_profile_index {
|
if let Some(p_idx) = admin_state.selected_profile_index {
|
||||||
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
|
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
|
||||||
logic_state_profile_name = profile.name.clone();
|
|
||||||
if let Some(t_idx) = admin_state.selected_table_index {
|
if let Some(t_idx) = admin_state.selected_table_index {
|
||||||
if let Some(table) = profile.tables.get(t_idx) {
|
if let Some(table) = profile.tables.get(t_idx) {
|
||||||
selected_table_name_for_logic = Some(table.name.clone());
|
// Both profile and table are selected, proceed
|
||||||
|
admin_state.add_logic_state = AddLogicState {
|
||||||
|
profile_name: profile.name.clone(),
|
||||||
|
selected_table_name: Some(table.name.clone()),
|
||||||
|
// selected_table_id: table.id, // If you have table IDs
|
||||||
|
editor_keybinding_mode: config.editor.keybinding_mode.clone(),
|
||||||
|
current_focus: AddLogicFocus::default(), // Reset focus for the new screen
|
||||||
|
..AddLogicState::default()
|
||||||
|
};
|
||||||
|
buffer_state.update_history(AppView::AddLogic); // Switch view
|
||||||
|
app_state.ui.focus_outside_canvas = false; // Ensure canvas focus
|
||||||
|
*command_message = format!(
|
||||||
|
"Opening Add Logic for table '{}' in profile '{}'...",
|
||||||
|
table.name, profile.name
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// This case should ideally not be reached if indices are managed correctly
|
||||||
|
*command_message = "Error: Selected table data not found.".to_string();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Profile is selected, but table is not
|
||||||
|
*command_message = "Select a table first!".to_string();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// This case should ideally not be reached if p_idx is valid
|
||||||
|
*command_message = "Error: Selected profile data not found.".to_string();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Profile is not selected
|
||||||
|
*command_message = "Select a profile first!".to_string();
|
||||||
}
|
}
|
||||||
admin_state.add_logic_state = AddLogicState {
|
|
||||||
profile_name: logic_state_profile_name.clone(),
|
|
||||||
selected_table_name: selected_table_name_for_logic,
|
|
||||||
editor_keybinding_mode: config.editor.keybinding_mode.clone(),
|
|
||||||
..AddLogicState::default()
|
|
||||||
};
|
|
||||||
buffer_state.update_history(AppView::AddLogic);
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
*command_message = "Opening Add Logic...".to_string();
|
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
Some("previous_option") | Some("move_up") => {
|
Some("previous_option") | Some("move_up") => {
|
||||||
@@ -264,20 +277,24 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AdminFocus::Button2 => {
|
AdminFocus::Button2 => { // Add Table Button
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
if let Some(p_idx) = admin_state.selected_profile_index {
|
if let Some(p_idx) = admin_state.selected_profile_index {
|
||||||
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
|
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
|
||||||
let selected_profile_name = profile.name.clone();
|
let selected_profile_name = profile.name.clone();
|
||||||
|
// Prepare links from the selected profile's existing tables
|
||||||
let available_links: Vec<LinkDefinition> = profile.tables.iter()
|
let available_links: Vec<LinkDefinition> = profile.tables.iter()
|
||||||
.map(|table| LinkDefinition {
|
.map(|table| LinkDefinition {
|
||||||
linked_table_name: table.name.clone(),
|
linked_table_name: table.name.clone(),
|
||||||
is_required: false, selected: false,
|
is_required: false, // Default, can be changed in AddTable screen
|
||||||
|
selected: false,
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
admin_state.add_table_state = AddTableState {
|
admin_state.add_table_state = AddTableState {
|
||||||
profile_name: selected_profile_name, links: available_links,
|
profile_name: selected_profile_name,
|
||||||
..AddTableState::default()
|
links: available_links,
|
||||||
|
..AddTableState::default() // Reset other fields
|
||||||
};
|
};
|
||||||
buffer_state.update_history(AppView::AddTable);
|
buffer_state.update_history(AppView::AddTable);
|
||||||
app_state.ui.focus_outside_canvas = false;
|
app_state.ui.focus_outside_canvas = false;
|
||||||
@@ -306,9 +323,10 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AdminFocus::Button3 => {
|
AdminFocus::Button3 => { // Change Table Button
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
|
// Future: Logic to load selected table into AddTableState for editing
|
||||||
*command_message = "Action: Change Table (Not Implemented)".to_string();
|
*command_message = "Action: Change Table (Not Implemented)".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
|||||||
pub async fn execute_action(
|
pub async fn execute_action(
|
||||||
action: &str,
|
action: &str,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
state: &mut AddLogicState, // Changed
|
state: &mut AddLogicState,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
key_sequence_tracker: &mut KeySequenceTracker,
|
key_sequence_tracker: &mut KeySequenceTracker,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
@@ -75,7 +75,7 @@ pub async fn execute_action(
|
|||||||
match action {
|
match action {
|
||||||
"move_up" => {
|
"move_up" => {
|
||||||
key_sequence_tracker.reset();
|
key_sequence_tracker.reset();
|
||||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed
|
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||||
if num_fields == 0 { return Ok("No fields.".to_string()); }
|
if num_fields == 0 { return Ok("No fields.".to_string()); }
|
||||||
let current_field = state.current_field();
|
let current_field = state.current_field();
|
||||||
|
|
||||||
@@ -87,20 +87,13 @@ pub async fn execute_action(
|
|||||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||||
state.set_current_cursor_pos(new_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
} else {
|
} else {
|
||||||
// Moving up from the first field (InputLogicName)
|
*command_message = "At top of form.".to_string();
|
||||||
app_state.ui.focus_outside_canvas = true;
|
|
||||||
// Focus should go to the element logically above the canvas.
|
|
||||||
// Based on AddLogicFocus, this might be CancelButton or another element.
|
|
||||||
// For AddLogic, let's assume it's CancelButton, similar to AddTable.
|
|
||||||
state.current_focus = crate::state::pages::add_logic::AddLogicFocus::CancelButton; // Changed
|
|
||||||
key_sequence_tracker.reset();
|
|
||||||
return Ok("Focus moved above canvas".to_string());
|
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(command_message.clone())
|
||||||
}
|
}
|
||||||
"move_down" => {
|
"move_down" => {
|
||||||
key_sequence_tracker.reset();
|
key_sequence_tracker.reset();
|
||||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed
|
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||||
if num_fields == 0 { return Ok("No fields.".to_string()); }
|
if num_fields == 0 { return Ok("No fields.".to_string()); }
|
||||||
let current_field = state.current_field();
|
let current_field = state.current_field();
|
||||||
let last_field_index = num_fields - 1;
|
let last_field_index = num_fields - 1;
|
||||||
@@ -113,21 +106,19 @@ pub async fn execute_action(
|
|||||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||||
state.set_current_cursor_pos(new_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
} else {
|
} else {
|
||||||
// Moving down from the last field (InputDescription)
|
// Move focus outside canvas when moving down from the last field
|
||||||
|
// FIX: Go to ScriptContentPreview instead of SaveButton
|
||||||
app_state.ui.focus_outside_canvas = true;
|
app_state.ui.focus_outside_canvas = true;
|
||||||
// Focus should go to the element logically below the canvas.
|
state.last_canvas_field = 2;
|
||||||
// This is likely InputScriptContent or SaveButton.
|
state.current_focus = crate::state::pages::add_logic::AddLogicFocus::ScriptContentPreview; // FIXED!
|
||||||
// The add_logic_nav.rs handles transitions to InputScriptContent.
|
*command_message = "Focus moved to script preview".to_string();
|
||||||
// If moving from canvas directly to buttons, it would be SaveButton.
|
|
||||||
state.current_focus = crate::state::pages::add_logic::AddLogicFocus::InputScriptContent; // Or SaveButton
|
|
||||||
key_sequence_tracker.reset();
|
|
||||||
return Ok("Focus moved to script/button area".to_string());
|
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(command_message.clone())
|
||||||
}
|
}
|
||||||
|
// ... (rest of the actions remain the same) ...
|
||||||
"move_first_line" => {
|
"move_first_line" => {
|
||||||
key_sequence_tracker.reset();
|
key_sequence_tracker.reset();
|
||||||
if AddLogicState::INPUT_FIELD_COUNT > 0 { // Changed
|
if AddLogicState::INPUT_FIELD_COUNT > 0 {
|
||||||
state.set_current_field(0);
|
state.set_current_field(0);
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||||
@@ -139,7 +130,7 @@ pub async fn execute_action(
|
|||||||
}
|
}
|
||||||
"move_last_line" => {
|
"move_last_line" => {
|
||||||
key_sequence_tracker.reset();
|
key_sequence_tracker.reset();
|
||||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed
|
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||||
if num_fields > 0 {
|
if num_fields > 0 {
|
||||||
let last_field_index = num_fields - 1;
|
let last_field_index = num_fields - 1;
|
||||||
state.set_current_field(last_field_index);
|
state.set_current_field(last_field_index);
|
||||||
|
|||||||
@@ -5,26 +5,27 @@ use crate::state::pages::{
|
|||||||
auth::{LoginState, RegisterState},
|
auth::{LoginState, RegisterState},
|
||||||
canvas_state::CanvasState,
|
canvas_state::CanvasState,
|
||||||
};
|
};
|
||||||
use crate::state::pages::add_logic::AddLogicState;
|
use crate::state::pages::form::FormState; // <<< ADD THIS LINE
|
||||||
use crate::state::pages::form::FormState;
|
// AddLogicState is already imported
|
||||||
use crate::state::pages::add_table::AddTableState;
|
// AddTableState is already imported
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::state::pages::admin::AdminState;
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e};
|
use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e};
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::KeyEvent; // Removed KeyCode, KeyModifiers as they were unused
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum EditEventOutcome {
|
pub enum EditEventOutcome {
|
||||||
Message(String), // Return a message, stay in Edit mode
|
Message(String),
|
||||||
ExitEditMode, // Signal to exit Edit mode
|
ExitEditMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_edit_event(
|
pub async fn handle_edit_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState, // Now FormState is in scope
|
||||||
login_state: &mut LoginState,
|
login_state: &mut LoginState,
|
||||||
register_state: &mut RegisterState,
|
register_state: &mut RegisterState,
|
||||||
admin_state: &mut AdminState,
|
admin_state: &mut AdminState,
|
||||||
@@ -34,17 +35,20 @@ pub async fn handle_edit_event(
|
|||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
) -> Result<EditEventOutcome> {
|
) -> Result<EditEventOutcome> {
|
||||||
// Global command mode check (should ideally be handled before calling this function)
|
// --- Global command mode check ---
|
||||||
if let Some("enter_command_mode") = config.get_action_for_key_in_mode(
|
if let Some("enter_command_mode") = config.get_action_for_key_in_mode(
|
||||||
&config.keybindings.global,
|
&config.keybindings.global, // Assuming command mode can be entered globally
|
||||||
key.code,
|
key.code,
|
||||||
key.modifiers,
|
key.modifiers,
|
||||||
) {
|
) {
|
||||||
|
// This check might be redundant if EventHandler already prevents entering Edit mode
|
||||||
|
// when command_mode is true. However, it's a safeguard.
|
||||||
return Ok(EditEventOutcome::Message(
|
return Ok(EditEventOutcome::Message(
|
||||||
"Command mode entry handled globally.".to_string(),
|
"Cannot enter command mode from edit mode here.".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Common actions (save, revert) ---
|
||||||
if let Some(action) = config.get_action_for_key_in_mode(
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
&config.keybindings.common,
|
&config.keybindings.common,
|
||||||
key.code,
|
key.code,
|
||||||
@@ -52,261 +56,197 @@ pub async fn handle_edit_event(
|
|||||||
).as_deref() {
|
).as_deref() {
|
||||||
if matches!(action, "save" | "revert") {
|
if matches!(action, "save" | "revert") {
|
||||||
let message_string: String = if app_state.ui.show_login {
|
let message_string: String = if app_state.ui.show_login {
|
||||||
auth_e::execute_common_action(
|
auth_e::execute_common_action(action, login_state, grpc_client, current_position, total_count).await?
|
||||||
action,
|
|
||||||
login_state,
|
|
||||||
grpc_client,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
auth_e::execute_common_action(
|
auth_e::execute_common_action(action, register_state, grpc_client, current_position, total_count).await?
|
||||||
action,
|
|
||||||
register_state,
|
|
||||||
grpc_client,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
format!(
|
// TODO: Implement common actions for AddTable if needed
|
||||||
"Action '{}' not fully implemented for Add Table view here.",
|
format!("Action '{}' not implemented for Add Table in edit mode.", action)
|
||||||
action
|
|
||||||
)
|
|
||||||
} else if app_state.ui.show_add_logic {
|
} else if app_state.ui.show_add_logic {
|
||||||
format!(
|
// TODO: Implement common actions for AddLogic if needed
|
||||||
"Action '{}' not fully implemented for Add Logic view here.",
|
format!("Action '{}' not implemented for Add Logic in edit mode.", action)
|
||||||
action
|
} else { // Assuming Form view
|
||||||
)
|
let outcome = form_e::execute_common_action(action, form_state, grpc_client, current_position, total_count).await?;
|
||||||
} else {
|
|
||||||
let outcome = form_e::execute_common_action(
|
|
||||||
action,
|
|
||||||
form_state,
|
|
||||||
grpc_client,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
match outcome {
|
match outcome {
|
||||||
EventOutcome::Ok(msg) => msg,
|
EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg,
|
||||||
EventOutcome::DataSaved(_, msg) => msg,
|
_ => format!("Unexpected outcome from common action: {:?}", outcome),
|
||||||
_ => format!(
|
|
||||||
"Unexpected outcome from common action: {:?}",
|
|
||||||
outcome
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Ok(EditEventOutcome::Message(message_string));
|
return Ok(EditEventOutcome::Message(message_string));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit-specific actions
|
// --- Edit-specific actions ---
|
||||||
if let Some(action) =
|
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers).as_deref() {
|
||||||
config.get_edit_action_for_key(key.code, key.modifiers)
|
// --- Handle "enter_decider" (Enter key) ---
|
||||||
.as_deref() {
|
if action_str == "enter_decider" {
|
||||||
// Handle enter_decider first
|
|
||||||
if action == "enter_decider" {
|
|
||||||
let effective_action = if app_state.ui.show_register
|
let effective_action = if app_state.ui.show_register
|
||||||
&& register_state.in_suggestion_mode
|
&& register_state.in_suggestion_mode
|
||||||
&& register_state.current_field() == 4 {
|
&& register_state.current_field() == 4 { // Role field
|
||||||
|
"select_suggestion"
|
||||||
|
} else if app_state.ui.show_add_logic
|
||||||
|
&& admin_state.add_logic_state.in_target_column_suggestion_mode
|
||||||
|
&& admin_state.add_logic_state.current_field() == 1 { // Target Column field
|
||||||
"select_suggestion"
|
"select_suggestion"
|
||||||
} else {
|
} else {
|
||||||
"next_field"
|
"next_field" // Default action for Enter
|
||||||
};
|
};
|
||||||
|
|
||||||
let msg = if app_state.ui.show_login {
|
let msg = if app_state.ui.show_login {
|
||||||
auth_e::execute_edit_action(
|
auth_e::execute_edit_action(effective_action, key, login_state, ideal_cursor_column).await?
|
||||||
effective_action,
|
|
||||||
key,
|
|
||||||
login_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
add_table_e::execute_edit_action(
|
add_table_e::execute_edit_action(effective_action, key, &mut admin_state.add_table_state, ideal_cursor_column).await?
|
||||||
effective_action,
|
|
||||||
key,
|
|
||||||
&mut admin_state.add_table_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_add_logic {
|
} else if app_state.ui.show_add_logic {
|
||||||
add_logic_e::execute_edit_action(
|
add_logic_e::execute_edit_action(effective_action, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
|
||||||
effective_action,
|
|
||||||
key,
|
|
||||||
&mut admin_state.add_logic_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
auth_e::execute_edit_action(
|
auth_e::execute_edit_action(effective_action, key, register_state, ideal_cursor_column).await?
|
||||||
effective_action,
|
} else { // Form view
|
||||||
key,
|
form_e::execute_edit_action(effective_action, key, form_state, ideal_cursor_column).await?
|
||||||
register_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else {
|
|
||||||
form_e::execute_edit_action(
|
|
||||||
effective_action,
|
|
||||||
key,
|
|
||||||
form_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
};
|
};
|
||||||
return Ok(EditEventOutcome::Message(msg));
|
return Ok(EditEventOutcome::Message(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == "exit" {
|
// --- Handle "exit" (Escape key) ---
|
||||||
|
if action_str == "exit" {
|
||||||
if app_state.ui.show_register && register_state.in_suggestion_mode {
|
if app_state.ui.show_register && register_state.in_suggestion_mode {
|
||||||
let msg = auth_e::execute_edit_action(
|
let msg = auth_e::execute_edit_action("exit_suggestion_mode", key, register_state, ideal_cursor_column).await?;
|
||||||
"exit_suggestion_mode",
|
|
||||||
key,
|
|
||||||
register_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(EditEventOutcome::Message(msg));
|
return Ok(EditEventOutcome::Message(msg));
|
||||||
|
} else if app_state.ui.show_add_logic && admin_state.add_logic_state.in_target_column_suggestion_mode {
|
||||||
|
admin_state.add_logic_state.in_target_column_suggestion_mode = false;
|
||||||
|
admin_state.add_logic_state.show_target_column_suggestions = false;
|
||||||
|
admin_state.add_logic_state.selected_target_column_suggestion_index = None;
|
||||||
|
return Ok(EditEventOutcome::Message("Exited column suggestions".to_string()));
|
||||||
} else {
|
} else {
|
||||||
return Ok(EditEventOutcome::ExitEditMode);
|
return Ok(EditEventOutcome::ExitEditMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for role field suggestions (Register view only)
|
// --- Autocomplete for AddLogicState Target Column ---
|
||||||
if app_state.ui.show_register && register_state.current_field() == 4 {
|
if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 { // Target Column field
|
||||||
if !register_state.in_suggestion_mode
|
if action_str == "suggestion_down" { // "Tab" is mapped to suggestion_down
|
||||||
&& key.code == KeyCode::Tab
|
if !admin_state.add_logic_state.in_target_column_suggestion_mode {
|
||||||
&& key.modifiers == KeyModifiers::NONE
|
// Attempt to open suggestions
|
||||||
{
|
if let Some(profile_name) = admin_state.add_logic_state.profile_name.clone().into() {
|
||||||
register_state.update_role_suggestions();
|
if let Some(table_name) = admin_state.add_logic_state.selected_table_name.clone() {
|
||||||
if !register_state.role_suggestions.is_empty() {
|
debug!("Fetching table structure for autocomplete: Profile='{}', Table='{}'", profile_name, table_name);
|
||||||
register_state.in_suggestion_mode = true;
|
match grpc_client.get_table_structure(profile_name, table_name).await {
|
||||||
register_state.selected_suggestion_index = Some(0);
|
Ok(ts_response) => {
|
||||||
return Ok(EditEventOutcome::Message(
|
admin_state.add_logic_state.table_columns_for_suggestions =
|
||||||
"Suggestions shown".to_string(),
|
ts_response.columns.into_iter().map(|c| c.name).collect();
|
||||||
));
|
admin_state.add_logic_state.update_target_column_suggestions();
|
||||||
} else {
|
if !admin_state.add_logic_state.target_column_suggestions.is_empty() {
|
||||||
return Ok(EditEventOutcome::Message(
|
admin_state.add_logic_state.in_target_column_suggestion_mode = true;
|
||||||
"No suggestions available".to_string(),
|
// update_target_column_suggestions handles initial selection
|
||||||
));
|
return Ok(EditEventOutcome::Message("Column suggestions shown".to_string()));
|
||||||
|
} else {
|
||||||
|
return Ok(EditEventOutcome::Message("No column suggestions for current input".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Error fetching table structure: {}", e);
|
||||||
|
admin_state.add_logic_state.table_columns_for_suggestions.clear(); // Clear old data on error
|
||||||
|
admin_state.add_logic_state.update_target_column_suggestions();
|
||||||
|
return Ok(EditEventOutcome::Message(format!("Error fetching columns: {}", e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(EditEventOutcome::Message("No table selected for column suggestions".to_string()));
|
||||||
|
}
|
||||||
|
} else { // Should not happen if AddLogic is properly initialized
|
||||||
|
return Ok(EditEventOutcome::Message("Profile name missing for column suggestions".to_string()));
|
||||||
|
}
|
||||||
|
} else { // Already in suggestion mode, navigate down
|
||||||
|
let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?;
|
||||||
|
return Ok(EditEventOutcome::Message(msg));
|
||||||
}
|
}
|
||||||
}
|
} else if admin_state.add_logic_state.in_target_column_suggestion_mode && action_str == "suggestion_up" {
|
||||||
if register_state.in_suggestion_mode
|
let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?;
|
||||||
&& matches!(
|
|
||||||
action,
|
|
||||||
"suggestion_down" | "suggestion_up"
|
|
||||||
)
|
|
||||||
{
|
|
||||||
let msg = auth_e::execute_edit_action(
|
|
||||||
action,
|
|
||||||
key,
|
|
||||||
register_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(EditEventOutcome::Message(msg));
|
return Ok(EditEventOutcome::Message(msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute other edit actions based on the current view
|
// --- Autocomplete for RegisterState Role Field ---
|
||||||
|
if app_state.ui.show_register && register_state.current_field() == 4 { // Role field
|
||||||
|
if !register_state.in_suggestion_mode && action_str == "suggestion_down" { // Tab
|
||||||
|
register_state.update_role_suggestions();
|
||||||
|
if !register_state.role_suggestions.is_empty() {
|
||||||
|
register_state.in_suggestion_mode = true;
|
||||||
|
// update_role_suggestions should handle initial selection
|
||||||
|
return Ok(EditEventOutcome::Message("Role suggestions shown".to_string()));
|
||||||
|
} else {
|
||||||
|
// If Tab doesn't open suggestions, it might fall through to "next_field"
|
||||||
|
// or you might want specific behavior. For now, let it fall through.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up") {
|
||||||
|
let msg = auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await?;
|
||||||
|
return Ok(EditEventOutcome::Message(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dispatch other edit actions ---
|
||||||
let msg = if app_state.ui.show_login {
|
let msg = if app_state.ui.show_login {
|
||||||
auth_e::execute_edit_action(
|
auth_e::execute_edit_action(action_str, key, login_state, ideal_cursor_column).await?
|
||||||
action,
|
|
||||||
key,
|
|
||||||
login_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
add_table_e::execute_edit_action(
|
add_table_e::execute_edit_action(action_str, key, &mut admin_state.add_table_state, ideal_cursor_column).await?
|
||||||
action,
|
|
||||||
key,
|
|
||||||
&mut admin_state.add_table_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_add_logic {
|
} else if app_state.ui.show_add_logic {
|
||||||
add_logic_e::execute_edit_action(
|
// If not a suggestion action handled above for AddLogic
|
||||||
action,
|
if !(admin_state.add_logic_state.in_target_column_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) {
|
||||||
key,
|
add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
|
||||||
&mut admin_state.add_logic_state,
|
} else { String::new() /* Already handled */ }
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
auth_e::execute_edit_action(
|
if !(register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) {
|
||||||
action,
|
auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await?
|
||||||
key,
|
} else { String::new() /* Already handled */ }
|
||||||
register_state,
|
} else { // Form view
|
||||||
ideal_cursor_column,
|
form_e::execute_edit_action(action_str, key, form_state, ideal_cursor_column).await?
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else {
|
|
||||||
form_e::execute_edit_action(
|
|
||||||
action,
|
|
||||||
key,
|
|
||||||
form_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
};
|
};
|
||||||
return Ok(EditEventOutcome::Message(msg));
|
return Ok(EditEventOutcome::Message(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Character insertion ---
|
// --- Character insertion ---
|
||||||
|
// If character insertion happens while in suggestion mode, exit suggestion mode first.
|
||||||
|
let mut exited_suggestion_mode_for_typing = false;
|
||||||
if app_state.ui.show_register && register_state.in_suggestion_mode {
|
if app_state.ui.show_register && register_state.in_suggestion_mode {
|
||||||
register_state.in_suggestion_mode = false;
|
register_state.in_suggestion_mode = false;
|
||||||
register_state.show_role_suggestions = false;
|
register_state.show_role_suggestions = false;
|
||||||
register_state.selected_suggestion_index = None;
|
register_state.selected_suggestion_index = None;
|
||||||
|
exited_suggestion_mode_for_typing = true;
|
||||||
|
}
|
||||||
|
if app_state.ui.show_add_logic && admin_state.add_logic_state.in_target_column_suggestion_mode {
|
||||||
|
admin_state.add_logic_state.in_target_column_suggestion_mode = false;
|
||||||
|
admin_state.add_logic_state.show_target_column_suggestions = false;
|
||||||
|
admin_state.add_logic_state.selected_target_column_suggestion_index = None;
|
||||||
|
exited_suggestion_mode_for_typing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = if app_state.ui.show_login {
|
let mut char_insert_msg = if app_state.ui.show_login {
|
||||||
auth_e::execute_edit_action(
|
auth_e::execute_edit_action("insert_char", key, login_state, ideal_cursor_column).await?
|
||||||
"insert_char",
|
|
||||||
key,
|
|
||||||
login_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
add_table_e::execute_edit_action(
|
add_table_e::execute_edit_action("insert_char", key, &mut admin_state.add_table_state, ideal_cursor_column).await?
|
||||||
"insert_char",
|
|
||||||
key,
|
|
||||||
&mut admin_state.add_table_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_add_logic {
|
} else if app_state.ui.show_add_logic {
|
||||||
add_logic_e::execute_edit_action(
|
add_logic_e::execute_edit_action("insert_char", key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
|
||||||
"insert_char",
|
|
||||||
key,
|
|
||||||
&mut admin_state.add_logic_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
auth_e::execute_edit_action(
|
auth_e::execute_edit_action("insert_char", key, register_state, ideal_cursor_column).await?
|
||||||
"insert_char",
|
} else { // Form view
|
||||||
key,
|
form_e::execute_edit_action("insert_char", key, form_state, ideal_cursor_column).await?
|
||||||
register_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else {
|
|
||||||
form_e::execute_edit_action(
|
|
||||||
"insert_char",
|
|
||||||
key,
|
|
||||||
form_state,
|
|
||||||
ideal_cursor_column,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// After character insertion, update suggestions if applicable
|
||||||
if app_state.ui.show_register && register_state.current_field() == 4 {
|
if app_state.ui.show_register && register_state.current_field() == 4 {
|
||||||
register_state.update_role_suggestions();
|
register_state.update_role_suggestions();
|
||||||
|
// If we just exited suggestion mode by typing, don't immediately show them again unless Tab is pressed.
|
||||||
|
// However, update_role_suggestions will set show_role_suggestions if matches are found.
|
||||||
|
// This is fine, as the render logic checks in_suggestion_mode.
|
||||||
|
}
|
||||||
|
if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 {
|
||||||
|
admin_state.add_logic_state.update_target_column_suggestions();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(EditEventOutcome::Message(msg));
|
if exited_suggestion_mode_for_typing && char_insert_msg.is_empty() {
|
||||||
|
char_insert_msg = "Suggestions hidden".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Ok(EditEventOutcome::Message(char_insert_msg))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use common::proto::multieko2::adresar::adresar_client::AdresarClient;
|
|||||||
use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, PutAdresarRequest};
|
use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, PutAdresarRequest};
|
||||||
use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty};
|
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;
|
||||||
use common::proto::multieko2::table_structure::TableStructureResponse;
|
// Import the new request type for table structure
|
||||||
|
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,
|
ProfileTreeResponse, PostTableDefinitionRequest, TableDefinitionResponse,
|
||||||
@@ -63,9 +64,20 @@ impl GrpcClient {
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_table_structure(&mut self) -> Result<TableStructureResponse> {
|
// Updated get_table_structure method
|
||||||
let request = tonic::Request::new(Empty::default());
|
pub async fn get_table_structure(
|
||||||
let response = self.table_structure_client.get_adresar_table_structure(request).await?;
|
&mut self,
|
||||||
|
profile_name: String,
|
||||||
|
table_name: String,
|
||||||
|
) -> Result<TableStructureResponse> {
|
||||||
|
// Create the new request type
|
||||||
|
let grpc_request = GetTableStructureRequest {
|
||||||
|
profile_name,
|
||||||
|
table_name,
|
||||||
|
};
|
||||||
|
let request = tonic::Request::new(grpc_request);
|
||||||
|
// Call the new gRPC method
|
||||||
|
let response = self.table_structure_client.get_table_structure(request).await?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,4 +99,3 @@ impl GrpcClient {
|
|||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,15 @@ impl UiService {
|
|||||||
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;
|
||||||
|
|
||||||
// Fetch table structure
|
// TODO for general tables and not hardcoded
|
||||||
let table_structure = grpc_client.get_table_structure().await?;
|
let default_profile_name = "default".to_string();
|
||||||
|
let default_table_name = "2025_customer".to_string();
|
||||||
|
|
||||||
|
// Fetch table structure for the default table
|
||||||
|
let table_structure = grpc_client
|
||||||
|
.get_table_structure(default_profile_name, default_table_name)
|
||||||
|
.await
|
||||||
|
.context("Failed to get initial table structure")?;
|
||||||
|
|
||||||
// Extract the column names from the response
|
// Extract the column names from the response
|
||||||
let column_names: Vec<String> = table_structure
|
let column_names: Vec<String> = table_structure
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/state/pages/add_logic.rs
|
// src/state/pages/add_logic.rs
|
||||||
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use crate::state::pages::canvas_state::CanvasState;
|
||||||
use crate::components::common::text_editor::{TextEditor, VimState}; // Add VimState import
|
use crate::components::common::text_editor::{TextEditor, VimState};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
@@ -11,8 +11,9 @@ pub enum AddLogicFocus {
|
|||||||
#[default]
|
#[default]
|
||||||
InputLogicName,
|
InputLogicName,
|
||||||
InputTargetColumn,
|
InputTargetColumn,
|
||||||
InputScriptContent,
|
|
||||||
InputDescription,
|
InputDescription,
|
||||||
|
ScriptContentPreview,
|
||||||
|
InsideScriptContent,
|
||||||
SaveButton,
|
SaveButton,
|
||||||
CancelButton,
|
CancelButton,
|
||||||
}
|
}
|
||||||
@@ -27,12 +28,20 @@ pub struct AddLogicState {
|
|||||||
pub script_content_editor: Rc<RefCell<TextArea<'static>>>,
|
pub script_content_editor: Rc<RefCell<TextArea<'static>>>,
|
||||||
pub description_input: String,
|
pub description_input: String,
|
||||||
pub current_focus: AddLogicFocus,
|
pub current_focus: AddLogicFocus,
|
||||||
|
pub last_canvas_field: usize,
|
||||||
pub logic_name_cursor_pos: usize,
|
pub logic_name_cursor_pos: usize,
|
||||||
pub target_column_cursor_pos: usize,
|
pub target_column_cursor_pos: usize,
|
||||||
pub description_cursor_pos: usize,
|
pub description_cursor_pos: usize,
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
pub editor_keybinding_mode: EditorKeybindingMode,
|
pub editor_keybinding_mode: EditorKeybindingMode,
|
||||||
pub vim_state: VimState, // Add this field
|
pub vim_state: VimState,
|
||||||
|
|
||||||
|
// New fields for Target Column Autocomplete
|
||||||
|
pub table_columns_for_suggestions: Vec<String>, // All columns for the table
|
||||||
|
pub target_column_suggestions: Vec<String>, // Filtered suggestions
|
||||||
|
pub show_target_column_suggestions: bool,
|
||||||
|
pub selected_target_column_suggestion_index: Option<usize>,
|
||||||
|
pub in_target_column_suggestion_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddLogicState {
|
impl AddLogicState {
|
||||||
@@ -47,16 +56,64 @@ impl AddLogicState {
|
|||||||
script_content_editor: Rc::new(RefCell::new(editor)),
|
script_content_editor: Rc::new(RefCell::new(editor)),
|
||||||
description_input: String::new(),
|
description_input: String::new(),
|
||||||
current_focus: AddLogicFocus::InputLogicName,
|
current_focus: AddLogicFocus::InputLogicName,
|
||||||
|
last_canvas_field: 2,
|
||||||
logic_name_cursor_pos: 0,
|
logic_name_cursor_pos: 0,
|
||||||
target_column_cursor_pos: 0,
|
target_column_cursor_pos: 0,
|
||||||
description_cursor_pos: 0,
|
description_cursor_pos: 0,
|
||||||
has_unsaved_changes: false,
|
has_unsaved_changes: false,
|
||||||
editor_keybinding_mode: editor_config.keybinding_mode.clone(),
|
editor_keybinding_mode: editor_config.keybinding_mode.clone(),
|
||||||
vim_state: VimState::default(), // Add this field initialization
|
vim_state: VimState::default(),
|
||||||
|
|
||||||
|
table_columns_for_suggestions: Vec::new(),
|
||||||
|
target_column_suggestions: Vec::new(),
|
||||||
|
show_target_column_suggestions: false,
|
||||||
|
selected_target_column_suggestion_index: None,
|
||||||
|
in_target_column_suggestion_mode: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const INPUT_FIELD_COUNT: usize = 3;
|
pub const INPUT_FIELD_COUNT: usize = 3;
|
||||||
|
|
||||||
|
/// Updates the target_column_suggestions based on current input.
|
||||||
|
pub fn update_target_column_suggestions(&mut self) {
|
||||||
|
let current_input = self.target_column_input.to_lowercase();
|
||||||
|
if self.table_columns_for_suggestions.is_empty() {
|
||||||
|
self.target_column_suggestions.clear();
|
||||||
|
self.show_target_column_suggestions = false;
|
||||||
|
self.selected_target_column_suggestion_index = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_input.is_empty() {
|
||||||
|
self.target_column_suggestions = self.table_columns_for_suggestions.clone();
|
||||||
|
} else {
|
||||||
|
self.target_column_suggestions = self
|
||||||
|
.table_columns_for_suggestions
|
||||||
|
.iter()
|
||||||
|
.filter(|name| name.to_lowercase().contains(¤t_input))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.show_target_column_suggestions = !self.target_column_suggestions.is_empty();
|
||||||
|
if self.show_target_column_suggestions {
|
||||||
|
// If suggestions are shown, ensure a selection (usually the first)
|
||||||
|
// or maintain current if it's still valid.
|
||||||
|
if let Some(selected_idx) = self.selected_target_column_suggestion_index {
|
||||||
|
if selected_idx >= self.target_column_suggestions.len() {
|
||||||
|
self.selected_target_column_suggestion_index = Some(0);
|
||||||
|
}
|
||||||
|
// If the previously selected item is no longer in the filtered list, reset.
|
||||||
|
// This is a bit more complex to check perfectly without iterating again.
|
||||||
|
// For now, just ensuring it's within bounds is a good start.
|
||||||
|
// A more robust way would be to check if the string at selected_idx still matches.
|
||||||
|
} else {
|
||||||
|
self.selected_target_column_suggestion_index = Some(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.selected_target_column_suggestion_index = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AddLogicState {
|
impl Default for AddLogicState {
|
||||||
@@ -65,14 +122,13 @@ impl Default for AddLogicState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... rest of the CanvasState implementation remains the same
|
|
||||||
impl CanvasState for AddLogicState {
|
impl CanvasState for AddLogicState {
|
||||||
fn current_field(&self) -> usize {
|
fn current_field(&self) -> usize {
|
||||||
match self.current_focus {
|
match self.current_focus {
|
||||||
AddLogicFocus::InputLogicName => 0,
|
AddLogicFocus::InputLogicName => 0,
|
||||||
AddLogicFocus::InputTargetColumn => 1,
|
AddLogicFocus::InputTargetColumn => 1,
|
||||||
AddLogicFocus::InputDescription => 2,
|
AddLogicFocus::InputDescription => 2,
|
||||||
_ => 0,
|
_ => self.last_canvas_field,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,12 +176,21 @@ impl CanvasState for AddLogicState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_field(&mut self, index: usize) {
|
fn set_current_field(&mut self, index: usize) {
|
||||||
self.current_focus = match index {
|
let new_focus = match index {
|
||||||
0 => AddLogicFocus::InputLogicName,
|
0 => AddLogicFocus::InputLogicName,
|
||||||
1 => AddLogicFocus::InputTargetColumn,
|
1 => AddLogicFocus::InputTargetColumn,
|
||||||
2 => AddLogicFocus::InputDescription,
|
2 => AddLogicFocus::InputDescription,
|
||||||
_ => self.current_focus,
|
_ => return, // Or handle error/default
|
||||||
};
|
};
|
||||||
|
if self.current_focus != new_focus {
|
||||||
|
// If changing field, exit suggestion mode for target column
|
||||||
|
if self.current_focus == AddLogicFocus::InputTargetColumn {
|
||||||
|
self.in_target_column_suggestion_mode = false;
|
||||||
|
self.show_target_column_suggestions = false;
|
||||||
|
}
|
||||||
|
self.current_focus = new_focus;
|
||||||
|
self.last_canvas_field = index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
@@ -150,10 +215,24 @@ impl CanvasState for AddLogicState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
fn get_suggestions(&self) -> Option<&[String]> {
|
||||||
None
|
if self.current_field() == 1 // Target Column field index
|
||||||
|
&& self.in_target_column_suggestion_mode
|
||||||
|
&& self.show_target_column_suggestions
|
||||||
|
{
|
||||||
|
Some(&self.target_column_suggestions)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||||
None
|
if self.current_field() == 1 // Target Column field index
|
||||||
|
&& self.in_target_column_suggestion_mode
|
||||||
|
&& self.show_target_column_suggestions
|
||||||
|
{
|
||||||
|
self.selected_target_column_suggestion_index
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,22 @@ package multieko2.table_structure;
|
|||||||
|
|
||||||
import "common.proto";
|
import "common.proto";
|
||||||
|
|
||||||
|
message GetTableStructureRequest {
|
||||||
|
string profile_name = 1; // e.g., "default"
|
||||||
|
string table_name = 2; // e.g., "2025_adresar6"
|
||||||
|
}
|
||||||
|
|
||||||
message TableStructureResponse {
|
message TableStructureResponse {
|
||||||
repeated TableColumn columns = 1;
|
repeated TableColumn columns = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TableColumn {
|
message TableColumn {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
string data_type = 2;
|
string data_type = 2; // e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ"
|
||||||
bool is_nullable = 3;
|
bool is_nullable = 3;
|
||||||
bool is_primary_key = 4;
|
bool is_primary_key = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
service TableStructureService {
|
service TableStructureService {
|
||||||
rpc GetAdresarTableStructure (common.Empty) returns (TableStructureResponse);
|
rpc GetTableStructure (GetTableStructureRequest) returns (TableStructureResponse);
|
||||||
rpc GetUctovnictvoTableStructure (common.Empty) returns (TableStructureResponse);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,14 @@
|
|||||||
// This file is @generated by prost-build.
|
// This file is @generated by prost-build.
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct GetTableStructureRequest {
|
||||||
|
/// e.g., "default"
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub profile_name: ::prost::alloc::string::String,
|
||||||
|
/// e.g., "2025_adresar6"
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub table_name: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct TableStructureResponse {
|
pub struct TableStructureResponse {
|
||||||
#[prost(message, repeated, tag = "1")]
|
#[prost(message, repeated, tag = "1")]
|
||||||
pub columns: ::prost::alloc::vec::Vec<TableColumn>,
|
pub columns: ::prost::alloc::vec::Vec<TableColumn>,
|
||||||
@@ -8,6 +17,7 @@ pub struct TableStructureResponse {
|
|||||||
pub struct TableColumn {
|
pub struct TableColumn {
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
pub name: ::prost::alloc::string::String,
|
pub name: ::prost::alloc::string::String,
|
||||||
|
/// e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ"
|
||||||
#[prost(string, tag = "2")]
|
#[prost(string, tag = "2")]
|
||||||
pub data_type: ::prost::alloc::string::String,
|
pub data_type: ::prost::alloc::string::String,
|
||||||
#[prost(bool, tag = "3")]
|
#[prost(bool, tag = "3")]
|
||||||
@@ -106,9 +116,9 @@ pub mod table_structure_service_client {
|
|||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
self.inner = self.inner.max_encoding_message_size(limit);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub async fn get_adresar_table_structure(
|
pub async fn get_table_structure(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: impl tonic::IntoRequest<super::super::common::Empty>,
|
request: impl tonic::IntoRequest<super::GetTableStructureRequest>,
|
||||||
) -> std::result::Result<
|
) -> std::result::Result<
|
||||||
tonic::Response<super::TableStructureResponse>,
|
tonic::Response<super::TableStructureResponse>,
|
||||||
tonic::Status,
|
tonic::Status,
|
||||||
@@ -123,43 +133,14 @@ pub mod table_structure_service_client {
|
|||||||
})?;
|
})?;
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
"/multieko2.table_structure.TableStructureService/GetAdresarTableStructure",
|
"/multieko2.table_structure.TableStructureService/GetTableStructure",
|
||||||
);
|
);
|
||||||
let mut req = request.into_request();
|
let mut req = request.into_request();
|
||||||
req.extensions_mut()
|
req.extensions_mut()
|
||||||
.insert(
|
.insert(
|
||||||
GrpcMethod::new(
|
GrpcMethod::new(
|
||||||
"multieko2.table_structure.TableStructureService",
|
"multieko2.table_structure.TableStructureService",
|
||||||
"GetAdresarTableStructure",
|
"GetTableStructure",
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_uctovnictvo_table_structure(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::super::common::Empty>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::TableStructureResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/multieko2.table_structure.TableStructureService/GetUctovnictvoTableStructure",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"multieko2.table_structure.TableStructureService",
|
|
||||||
"GetUctovnictvoTableStructure",
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
self.inner.unary(req, path, codec).await
|
self.inner.unary(req, path, codec).await
|
||||||
@@ -179,16 +160,9 @@ pub mod table_structure_service_server {
|
|||||||
/// Generated trait containing gRPC methods that should be implemented for use with TableStructureServiceServer.
|
/// Generated trait containing gRPC methods that should be implemented for use with TableStructureServiceServer.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static {
|
pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static {
|
||||||
async fn get_adresar_table_structure(
|
async fn get_table_structure(
|
||||||
&self,
|
&self,
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
request: tonic::Request<super::GetTableStructureRequest>,
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::TableStructureResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_uctovnictvo_table_structure(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
|
||||||
) -> std::result::Result<
|
) -> std::result::Result<
|
||||||
tonic::Response<super::TableStructureResponse>,
|
tonic::Response<super::TableStructureResponse>,
|
||||||
tonic::Status,
|
tonic::Status,
|
||||||
@@ -271,15 +245,13 @@ pub mod table_structure_service_server {
|
|||||||
}
|
}
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||||
match req.uri().path() {
|
match req.uri().path() {
|
||||||
"/multieko2.table_structure.TableStructureService/GetAdresarTableStructure" => {
|
"/multieko2.table_structure.TableStructureService/GetTableStructure" => {
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
struct GetAdresarTableStructureSvc<T: TableStructureService>(
|
struct GetTableStructureSvc<T: TableStructureService>(pub Arc<T>);
|
||||||
pub Arc<T>,
|
|
||||||
);
|
|
||||||
impl<
|
impl<
|
||||||
T: TableStructureService,
|
T: TableStructureService,
|
||||||
> tonic::server::UnaryService<super::super::common::Empty>
|
> tonic::server::UnaryService<super::GetTableStructureRequest>
|
||||||
for GetAdresarTableStructureSvc<T> {
|
for GetTableStructureSvc<T> {
|
||||||
type Response = super::TableStructureResponse;
|
type Response = super::TableStructureResponse;
|
||||||
type Future = BoxFuture<
|
type Future = BoxFuture<
|
||||||
tonic::Response<Self::Response>,
|
tonic::Response<Self::Response>,
|
||||||
@@ -287,11 +259,11 @@ pub mod table_structure_service_server {
|
|||||||
>;
|
>;
|
||||||
fn call(
|
fn call(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
request: tonic::Request<super::GetTableStructureRequest>,
|
||||||
) -> Self::Future {
|
) -> Self::Future {
|
||||||
let inner = Arc::clone(&self.0);
|
let inner = Arc::clone(&self.0);
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
<T as TableStructureService>::get_adresar_table_structure(
|
<T as TableStructureService>::get_table_structure(
|
||||||
&inner,
|
&inner,
|
||||||
request,
|
request,
|
||||||
)
|
)
|
||||||
@@ -306,58 +278,7 @@ pub mod table_structure_service_server {
|
|||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
let max_encoding_message_size = self.max_encoding_message_size;
|
||||||
let inner = self.inner.clone();
|
let inner = self.inner.clone();
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
let method = GetAdresarTableStructureSvc(inner);
|
let method = GetTableStructureSvc(inner);
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/multieko2.table_structure.TableStructureService/GetUctovnictvoTableStructure" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetUctovnictvoTableStructureSvc<T: TableStructureService>(
|
|
||||||
pub Arc<T>,
|
|
||||||
);
|
|
||||||
impl<
|
|
||||||
T: TableStructureService,
|
|
||||||
> tonic::server::UnaryService<super::super::common::Empty>
|
|
||||||
for GetUctovnictvoTableStructureSvc<T> {
|
|
||||||
type Response = super::TableStructureResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TableStructureService>::get_uctovnictvo_table_structure(
|
|
||||||
&inner,
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetUctovnictvoTableStructureSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
let mut grpc = tonic::server::Grpc::new(codec)
|
||||||
.apply_compression_config(
|
.apply_compression_config(
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// src/server/services/table_structure_service.rs
|
// src/server/services/table_structure_service.rs
|
||||||
use tonic::{Request, Response, Status};
|
use tonic::{Request, Response, Status};
|
||||||
|
// Correct the import path for the TableStructureService trait
|
||||||
use common::proto::multieko2::table_structure::table_structure_service_server::TableStructureService;
|
use common::proto::multieko2::table_structure::table_structure_service_server::TableStructureService;
|
||||||
use common::proto::multieko2::table_structure::TableStructureResponse;
|
use common::proto::multieko2::table_structure::{
|
||||||
use common::proto::multieko2::common::Empty;
|
GetTableStructureRequest,
|
||||||
use crate::table_structure::handlers::{
|
TableStructureResponse,
|
||||||
get_adresar_table_structure, get_uctovnictvo_table_structure,
|
|
||||||
};
|
};
|
||||||
|
use crate::table_structure::handlers::get_table_structure;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -13,22 +14,21 @@ pub struct TableStructureHandler {
|
|||||||
pub db_pool: PgPool,
|
pub db_pool: PgPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tonic::async_trait]
|
impl TableStructureHandler {
|
||||||
impl TableStructureService for TableStructureHandler {
|
pub fn new(db_pool: PgPool) -> Self {
|
||||||
async fn get_adresar_table_structure(
|
Self { db_pool }
|
||||||
&self,
|
|
||||||
request: Request<Empty>,
|
|
||||||
) -> Result<Response<TableStructureResponse>, Status> {
|
|
||||||
let response = get_adresar_table_structure(&self.db_pool, request.into_inner())
|
|
||||||
.await?;
|
|
||||||
Ok(Response::new(response))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_uctovnictvo_table_structure(
|
#[tonic::async_trait]
|
||||||
|
impl TableStructureService for TableStructureHandler { // This line should now be correct
|
||||||
|
async fn get_table_structure(
|
||||||
&self,
|
&self,
|
||||||
request: Request<Empty>,
|
request: Request<GetTableStructureRequest>,
|
||||||
) -> Result<Response<TableStructureResponse>, Status> {
|
) -> Result<Response<TableStructureResponse>, Status> {
|
||||||
let response = get_uctovnictvo_table_structure(&self.db_pool, request.into_inner()).await?;
|
let req_payload = request.into_inner();
|
||||||
|
let response =
|
||||||
|
get_table_structure(&self.db_pool, req_payload).await?;
|
||||||
Ok(Response::new(response))
|
Ok(Response::new(response))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,39 @@
|
|||||||
Adresar response:
|
grpcurl -plaintext \
|
||||||
❯ grpcurl -plaintext \
|
-d '{
|
||||||
-proto proto/table_structure.proto \
|
"profile_name": "default",
|
||||||
-import-path proto \
|
"table_name": "2025_customer"
|
||||||
|
}' \
|
||||||
localhost:50051 \
|
localhost:50051 \
|
||||||
multieko2.table_structure.TableStructureService/GetAdresarTableStructure
|
multieko2.table_structure.TableStructureService/GetTableStructure
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"name": "firma",
|
"name": "id",
|
||||||
"dataType": "TEXT"
|
"dataType": "INT8",
|
||||||
|
"isPrimaryKey": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "kz",
|
"name": "deleted",
|
||||||
|
"dataType": "BOOL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "full_name",
|
||||||
"dataType": "TEXT",
|
"dataType": "TEXT",
|
||||||
"isNullable": true
|
"isNullable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "drc",
|
"name": "email",
|
||||||
"dataType": "TEXT",
|
"dataType": "VARCHAR(255)",
|
||||||
"isNullable": true
|
"isNullable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ulica",
|
"name": "loyalty_status",
|
||||||
"dataType": "TEXT",
|
"dataType": "BOOL",
|
||||||
"isNullable": true
|
"isNullable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "psc",
|
"name": "created_at",
|
||||||
"dataType": "TEXT",
|
"dataType": "TIMESTAMPTZ",
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mesto",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stat",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "banka",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ucet",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "skladm",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ico",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kontakt",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "telefon",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "skladu",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "fax",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"isNullable": true
|
"isNullable": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/table_structure/handlers.rs
|
// src/table_structure/handlers.rs
|
||||||
pub mod table_structure;
|
pub mod table_structure;
|
||||||
|
|
||||||
pub use table_structure::{get_adresar_table_structure, get_uctovnictvo_table_structure};
|
pub use table_structure::get_table_structure;
|
||||||
|
|||||||
@@ -1,181 +1,134 @@
|
|||||||
// src/table_structure/handlers/table_structure.rs
|
// src/table_structure/handlers/table_structure.rs
|
||||||
use tonic::Status;
|
use common::proto::multieko2::table_structure::{
|
||||||
use sqlx::PgPool;
|
GetTableStructureRequest, TableColumn, TableStructureResponse,
|
||||||
use common::proto::multieko2::{
|
|
||||||
table_structure::{TableStructureResponse, TableColumn},
|
|
||||||
common::Empty
|
|
||||||
};
|
};
|
||||||
|
use sqlx::{PgPool, Row};
|
||||||
|
use tonic::Status;
|
||||||
|
|
||||||
pub async fn get_adresar_table_structure(
|
// Helper struct to map query results
|
||||||
_db_pool: &PgPool,
|
#[derive(sqlx::FromRow, Debug)]
|
||||||
_request: Empty,
|
struct DbColumnInfo {
|
||||||
) -> Result<TableStructureResponse, Status> {
|
column_name: String,
|
||||||
let columns = vec![
|
formatted_data_type: String,
|
||||||
TableColumn {
|
is_nullable: bool,
|
||||||
name: "firma".to_string(),
|
is_primary_key: bool,
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: false,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "kz".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "drc".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "ulica".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "psc".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "mesto".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "stat".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "banka".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "ucet".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "skladm".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "ico".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "kontakt".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "telefon".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "skladu".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
TableColumn {
|
|
||||||
name: "fax".to_string(),
|
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
|
||||||
is_primary_key: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
Ok(TableStructureResponse { columns })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_uctovnictvo_table_structure(
|
pub async fn get_table_structure(
|
||||||
_db_pool: &PgPool,
|
db_pool: &PgPool,
|
||||||
_request: Empty,
|
request: GetTableStructureRequest,
|
||||||
) -> Result<TableStructureResponse, Status> {
|
) -> Result<TableStructureResponse, Status> {
|
||||||
let columns = vec![
|
let profile_name = request.profile_name;
|
||||||
TableColumn {
|
let table_name = request.table_name; // This should be the full table name, e.g., "2025_adresar6"
|
||||||
name: "adresar_id".to_string(),
|
let table_schema = "public"; // Assuming tables are in the 'public' schema
|
||||||
data_type: "BIGINT".to_string(),
|
|
||||||
is_nullable: false,
|
// 1. Validate Profile
|
||||||
is_primary_key: false,
|
let profile = sqlx::query!(
|
||||||
},
|
"SELECT id FROM profiles WHERE name = $1",
|
||||||
TableColumn {
|
profile_name
|
||||||
name: "c_dokladu".to_string(),
|
)
|
||||||
data_type: "TEXT".to_string(),
|
.fetch_optional(db_pool)
|
||||||
is_nullable: false,
|
.await
|
||||||
is_primary_key: false,
|
.map_err(|e| {
|
||||||
},
|
Status::internal(format!(
|
||||||
TableColumn {
|
"Failed to query profile '{}': {}",
|
||||||
name: "datum".to_string(),
|
profile_name, e
|
||||||
data_type: "DATE".to_string(),
|
))
|
||||||
is_nullable: false,
|
})?;
|
||||||
is_primary_key: false,
|
|
||||||
},
|
let profile_id = match profile {
|
||||||
TableColumn {
|
Some(p) => p.id,
|
||||||
name: "c_faktury".to_string(),
|
None => {
|
||||||
data_type: "TEXT".to_string(),
|
return Err(Status::not_found(format!(
|
||||||
is_nullable: false,
|
"Profile '{}' not found",
|
||||||
is_primary_key: false,
|
profile_name
|
||||||
},
|
)));
|
||||||
TableColumn {
|
}
|
||||||
name: "obsah".to_string(),
|
};
|
||||||
data_type: "TEXT".to_string(),
|
|
||||||
is_nullable: true,
|
// 2. Validate Table within Profile
|
||||||
is_primary_key: false,
|
sqlx::query!(
|
||||||
},
|
"SELECT id FROM table_definitions WHERE profile_id = $1 AND table_name = $2",
|
||||||
TableColumn {
|
profile_id,
|
||||||
name: "stredisko".to_string(),
|
table_name
|
||||||
data_type: "TEXT".to_string(),
|
)
|
||||||
is_nullable: true,
|
.fetch_optional(db_pool)
|
||||||
is_primary_key: false,
|
.await
|
||||||
},
|
.map_err(|e| Status::internal(format!("Failed to query table_definitions: {}", e)))?
|
||||||
TableColumn {
|
.ok_or_else(|| Status::not_found(format!(
|
||||||
name: "c_uctu".to_string(),
|
"Table '{}' not found in profile '{}'",
|
||||||
data_type: "TEXT".to_string(),
|
table_name,
|
||||||
is_nullable: true,
|
profile_name
|
||||||
is_primary_key: false,
|
)))?;
|
||||||
},
|
|
||||||
TableColumn {
|
// 3. Query information_schema for column details
|
||||||
name: "md".to_string(),
|
let query_str = r#"
|
||||||
data_type: "TEXT".to_string(),
|
SELECT
|
||||||
is_nullable: true,
|
c.column_name,
|
||||||
is_primary_key: false,
|
CASE
|
||||||
},
|
WHEN c.udt_name = 'varchar' AND c.character_maximum_length IS NOT NULL THEN
|
||||||
TableColumn {
|
'VARCHAR(' || c.character_maximum_length || ')'
|
||||||
name: "identif".to_string(),
|
WHEN c.udt_name = 'bpchar' AND c.character_maximum_length IS NOT NULL THEN
|
||||||
data_type: "TEXT".to_string(),
|
'CHAR(' || c.character_maximum_length || ')'
|
||||||
is_nullable: true,
|
WHEN c.udt_name = 'numeric' AND c.numeric_precision IS NOT NULL AND c.numeric_scale IS NOT NULL THEN
|
||||||
is_primary_key: false,
|
'NUMERIC(' || c.numeric_precision || ',' || c.numeric_scale || ')'
|
||||||
},
|
WHEN c.udt_name = 'numeric' AND c.numeric_precision IS NOT NULL THEN
|
||||||
TableColumn {
|
'NUMERIC(' || c.numeric_precision || ')'
|
||||||
name: "poznanka".to_string(),
|
WHEN STARTS_WITH(c.udt_name, '_') THEN
|
||||||
data_type: "TEXT".to_string(),
|
UPPER(SUBSTRING(c.udt_name FROM 2)) || '[]'
|
||||||
is_nullable: true,
|
ELSE
|
||||||
is_primary_key: false,
|
UPPER(c.udt_name)
|
||||||
},
|
END AS formatted_data_type,
|
||||||
TableColumn {
|
c.is_nullable = 'YES' AS is_nullable,
|
||||||
name: "firma".to_string(),
|
EXISTS (
|
||||||
data_type: "TEXT".to_string(),
|
SELECT 1
|
||||||
is_nullable: false,
|
FROM information_schema.key_column_usage kcu
|
||||||
is_primary_key: false,
|
JOIN information_schema.table_constraints tc
|
||||||
},
|
ON kcu.constraint_name = tc.constraint_name
|
||||||
];
|
AND kcu.table_schema = tc.table_schema
|
||||||
|
AND kcu.table_name = tc.table_name
|
||||||
|
WHERE tc.table_schema = c.table_schema
|
||||||
|
AND tc.table_name = c.table_name
|
||||||
|
AND tc.constraint_type = 'PRIMARY KEY'
|
||||||
|
AND kcu.column_name = c.column_name
|
||||||
|
) AS is_primary_key
|
||||||
|
FROM
|
||||||
|
information_schema.columns c
|
||||||
|
WHERE
|
||||||
|
c.table_schema = $1
|
||||||
|
AND c.table_name = $2
|
||||||
|
ORDER BY
|
||||||
|
c.ordinal_position;
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let db_columns = sqlx::query_as::<_, DbColumnInfo>(query_str)
|
||||||
|
.bind(table_schema)
|
||||||
|
.bind(&table_name) // Use the validated table_name
|
||||||
|
.fetch_all(db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
Status::internal(format!(
|
||||||
|
"Failed to query column information for table '{}': {}",
|
||||||
|
table_name, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if db_columns.is_empty() {
|
||||||
|
// This could mean the table exists in table_definitions but not in information_schema,
|
||||||
|
// or it has no columns. The latter is unlikely for a created table.
|
||||||
|
// Depending on desired behavior, you could return an error or an empty list.
|
||||||
|
// For now, returning an empty list if the table was validated.
|
||||||
|
}
|
||||||
|
|
||||||
|
let columns = db_columns
|
||||||
|
.into_iter()
|
||||||
|
.map(|db_col| TableColumn {
|
||||||
|
name: db_col.column_name,
|
||||||
|
data_type: db_col.formatted_data_type,
|
||||||
|
is_nullable: db_col.is_nullable,
|
||||||
|
is_primary_key: db_col.is_primary_key,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(TableStructureResponse { columns })
|
Ok(TableStructureResponse { columns })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user