Compare commits

..

15 Commits

Author SHA1 Message Date
filipriec
75af0c3be1 the profile name is now passed from admin panel to the add table page 2025-04-17 21:59:19 +02:00
filipriec
6f36b84f85 delete selected button now in the add table page working 2025-04-17 21:21:37 +02:00
filipriec
e723198b72 best ever, working as intended 2025-04-17 21:07:07 +02:00
filipriec
8f74febff1 combined, full width need a table name that is missing now 2025-04-17 20:41:48 +02:00
filipriec
d9bd6f8e1d working, but full width is not working, lets combine it with the full width now 2025-04-17 20:17:21 +02:00
filipriec
bf55417901 professional layout working 2025-04-17 19:50:48 +02:00
filipriec
9511970a1a removed placeholder 2025-04-17 19:12:28 +02:00
filipriec
5c8557b369 adding stuff to the column 2025-04-17 19:06:54 +02:00
filipriec
5e47c53fcf canvas required fields 2025-04-17 18:28:06 +02:00
filipriec
f7493a8bc4 canvas properly updating table name 2025-04-17 18:17:01 +02:00
filipriec
4f39b93edd logic of the add button, needs redesign 2025-04-17 16:48:03 +02:00
filipriec
ff8b4eb0f6 canvas now working properly well 2025-04-17 15:40:07 +02:00
filipriec
e921862a7f compiled, still not working for canvas 2025-04-17 14:41:09 +02:00
filipriec
4d7177f15a mistake fixed 2025-04-17 14:21:39 +02:00
filipriec
c5d7f56399 readonly and edit functionality to add table 2025-04-17 14:14:36 +02:00
15 changed files with 1444 additions and 336 deletions

View File

@@ -1,31 +1,46 @@
// src/components/admin/add_table.rs // src/components/admin/add_table.rs
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use crate::state::app::highlight::HighlightState;
use crate::state::app::state::AppState;
use crate::state::pages::add_table::{AddTableFocus, AddTableState};
use crate::state::pages::canvas_state::CanvasState;
// use crate::state::pages::add_table::{ColumnDefinition, LinkDefinition}; // Not directly used here
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Style, Stylize}, style::{Modifier, Style},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph}, widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table},
Frame, Frame,
}; };
use crate::state::app::state::AppState;
use crate::state::app::highlight::HighlightState;
use crate::state::pages::canvas_state::CanvasState;
use crate::components::handlers::canvas::render_canvas; use crate::components::handlers::canvas::render_canvas;
use crate::state::pages::add_table::AddTableState;
/// Renders a placeholder page for adding tables. /// Renders the Add New Table page layout, structuring the display of table information,
/// input fields, and action buttons. Adapts layout based on terminal width.
pub fn render_add_table( pub fn render_add_table(
f: &mut Frame, f: &mut Frame,
area: Rect, area: Rect,
theme: &Theme, theme: &Theme,
_app_state: &AppState, _app_state: &AppState, // Currently unused, might be needed later
add_table_state: &mut AddTableState, add_table_state: &mut AddTableState,
is_edit_mode: bool, is_edit_mode: bool, // Determines if canvas inputs are in edit mode
highlight_state: &HighlightState, highlight_state: &HighlightState, // For text highlighting in canvas
) { ) {
// Main block for the whole page // --- Configuration ---
// Threshold width to switch between wide and narrow layouts
const NARROW_LAYOUT_THRESHOLD: u16 = 120; // Adjust this value as needed
// --- State Checks ---
let focus_on_canvas_inputs = matches!(
add_table_state.current_focus,
AddTableFocus::InputTableName
| AddTableFocus::InputColumnName
| AddTableFocus::InputColumnType
);
// --- Main Page Block ---
let main_block = Block::default() let main_block = Block::default()
.title(" Add New Table ") .title(" Add New Table ")
.title_alignment(Alignment::Center)
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.border)) .border_style(Style::default().fg(theme.border))
@@ -33,148 +48,393 @@ pub fn render_add_table(
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);
// Split the inner area horizontally: Left info pane | Right input/action pane // --- Area Variable Declarations ---
let horizontal_chunks = Layout::default() let top_info_area: Rect;
.direction(Direction::Horizontal) let columns_area: Rect;
.constraints([ let canvas_area: Rect;
Constraint::Percentage(50), // Left Pane let add_button_area: Rect;
Constraint::Percentage(50), // Right Pane let indexes_area: Rect;
].as_ref()) let links_area: Rect;
.split(inner_area); let bottom_buttons_area: Rect;
let left_pane = horizontal_chunks[0]; // --- Layout Decision ---
let right_pane = horizontal_chunks[1]; if area.width >= NARROW_LAYOUT_THRESHOLD {
// --- WIDE Layout (Based on first screenshot) ---
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Top Info (Profile/Table Name) - Increased to 3 lines
Constraint::Min(10), // Middle Area (Columns | Right Pane)
Constraint::Length(3), // Bottom Buttons
])
.split(inner_area);
// --- Left Pane --- top_info_area = main_chunks[0];
let left_vertical_chunks = Layout::default() let middle_area = main_chunks[1];
.direction(Direction::Vertical) bottom_buttons_area = main_chunks[2];
.constraints([
Constraint::Length(3), // Profile & Table Name header
Constraint::Min(5), // Columns section (expandable)
Constraint::Length(1), // Separator
Constraint::Min(3), // Indexes section (expandable)
Constraint::Length(1), // Separator
Constraint::Min(3), // Links section (expandable)
].as_ref())
.split(left_pane);
// Profile & Table Name section // Split Middle Horizontally
let profile_text = Paragraph::new(vec![ let middle_chunks = Layout::default()
Line::from(Span::styled("profile: default", theme.fg)), // Placeholder .direction(Direction::Horizontal)
Line::from(Span::styled("table name: [tablename]", theme.fg)), // Placeholder .constraints([
]) Constraint::Percentage(60), // Left: Columns Table
Constraint::Percentage(40), // Right: Inputs etc.
])
.split(middle_area);
columns_area = middle_chunks[0];
let right_pane_area = middle_chunks[1];
// Split Right Pane Vertically
let right_pane_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5), // Input Canvas Area
Constraint::Length(3), // Add Button Area
Constraint::Min(5), // Indexes & Links Area
])
.split(right_pane_area);
canvas_area = right_pane_chunks[0];
add_button_area = right_pane_chunks[1];
let indexes_links_area = right_pane_chunks[2];
// Split Indexes/Links Horizontally
let indexes_links_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50), // Indexes Table
Constraint::Percentage(50), // Links Table
])
.split(indexes_links_area);
indexes_area = indexes_links_chunks[0];
links_area = indexes_links_chunks[1];
// --- Top Info Rendering (Wide - 2 lines) ---
let profile_text = Paragraph::new(vec![
Line::from(Span::styled(
format!("Profile: {}", add_table_state.profile_name),
theme.fg,
)),
Line::from(Span::styled(
format!("Table name: {}", add_table_state.table_name),
theme.fg,
)),
])
.block(
Block::default()
.borders(Borders::BOTTOM)
.border_style(Style::default().fg(theme.secondary)),
);
f.render_widget(profile_text, top_info_area);
} else {
// --- NARROW Layout (Based on second screenshot) ---
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // Top: Profile & Table Name (Single Row)
Constraint::Length(5), // Column Definition Input Canvas Area
Constraint::Length(3), // Add Button Area
Constraint::Min(5), // Columns Table Area
Constraint::Min(5), // Indexes & Links Area
Constraint::Length(3), // Bottom: Save/Cancel Buttons
])
.split(inner_area);
top_info_area = main_chunks[0];
canvas_area = main_chunks[1];
add_button_area = main_chunks[2];
columns_area = main_chunks[3];
let indexes_links_area = main_chunks[4];
bottom_buttons_area = main_chunks[5];
// Split Indexes/Links Horizontally
let indexes_links_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50), // Indexes Table
Constraint::Percentage(50), // Links Table
])
.split(indexes_links_area);
indexes_area = indexes_links_chunks[0];
links_area = indexes_links_chunks[1];
// --- Top Info Rendering (Narrow - 1 line) ---
let top_info_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(top_info_area);
let profile_text = Paragraph::new(Span::styled(
format!("Profile: {}", add_table_state.profile_name),
theme.fg,
))
.alignment(Alignment::Left);
f.render_widget(profile_text, top_info_chunks[0]);
let table_name_text = Paragraph::new(Span::styled(
format!("Table: {}", add_table_state.table_name),
theme.fg,
))
.alignment(Alignment::Left);
f.render_widget(table_name_text, top_info_chunks[1]);
}
// --- Common Widget Rendering (Uses calculated areas) ---
// --- Columns Table Rendering ---
let columns_focused =
add_table_state.current_focus == AddTableFocus::ColumnsTable;
let columns_border_style = if columns_focused {
Style::default().fg(theme.highlight)
} else {
Style::default().fg(theme.secondary)
};
let column_rows: Vec<Row<'_>> = add_table_state
.columns
.iter()
.map(|col_def| {
Row::new(vec![
Cell::from(col_def.name.clone()),
Cell::from(col_def.data_type.clone()),
])
.style(Style::default().fg(theme.fg))
})
.collect();
// Use different headers/constraints based on layout? For now, keep consistent.
let header_cells = ["Name", "Type"]
.iter()
.map(|h| Cell::from(*h).style(Style::default().fg(theme.accent)));
let header = Row::new(header_cells).height(1).bottom_margin(1);
let columns_table = Table::new(
column_rows,
[Constraint::Percentage(60), Constraint::Percentage(40)],
)
.header(header)
.block( .block(
Block::default() Block::default()
.borders(Borders::BOTTOM) .title(Span::styled(" Columns ", theme.fg))
.border_style(Style::default().fg(theme.secondary)), .title_alignment(Alignment::Center)
.borders(Borders::ALL) // Use ALL borders for consistency
.border_type(BorderType::Rounded)
.border_style(columns_border_style),
)
.row_highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(theme.highlight),
)
.highlight_symbol(" > ");
f.render_stateful_widget(
columns_table,
columns_area,
&mut add_table_state.column_table_state,
); );
f.render_widget(profile_text, left_vertical_chunks[0]);
// Columns section // --- Canvas Rendering (Column Definition Input) ---
let columns_text = Paragraph::new(vec![
Line::from(Span::styled("Name Type", theme.accent)), // Header
])
.block(Block::default().title(Span::styled(" Columns ", theme.fg)));
f.render_widget(columns_text, left_vertical_chunks[1]);
// Indexes section
let indexes_text = Paragraph::new(vec![
Line::from(Span::styled("Column name", theme.accent)), // Header
])
.block(
Block::default()
.title(Span::styled(" Indexes ", theme.fg))
.borders(Borders::TOP) // Separator from Columns
.border_style(Style::default().fg(theme.secondary)),
);
f.render_widget(indexes_text, left_vertical_chunks[3]);
// Links section
let links_text = Paragraph::new(vec![
Line::from(Span::styled("Linked table Required", theme.accent)), // Header
])
.block(
Block::default()
.title(Span::styled(" Links ", theme.fg))
.borders(Borders::TOP) // Separator from Indexes
.border_style(Style::default().fg(theme.secondary)),
);
f.render_widget(links_text, left_vertical_chunks[5]);
// --- Right Pane ---
let right_vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5), // Area for render_canvas (3 fields + 2 border)
Constraint::Length(3), // Add Button Area
Constraint::Min(1), // Spacer
Constraint::Length(3), // Save/Cancel buttons area
].as_ref())
.split(right_pane);
let canvas_area = right_vertical_chunks[0];
let add_button_area = right_vertical_chunks[1];
let bottom_buttons_area = right_vertical_chunks[3];
// --- Use render_canvas for Inputs ---
let _active_field_rect = render_canvas( let _active_field_rect = render_canvas(
f, f,
canvas_area, canvas_area,
add_table_state, add_table_state,
&[ &add_table_state.fields(),
"Table name",
"Name",
"Type",
],
&add_table_state.current_field(), &add_table_state.current_field(),
&add_table_state.inputs().iter().map(|s| *s).collect::<Vec<&String>>(), &add_table_state.inputs(),
theme, theme,
is_edit_mode, is_edit_mode && focus_on_canvas_inputs,
highlight_state, highlight_state,
); );
// Add Button (Placeholder) // --- Button Style Helpers ---
let get_button_style = |button_focus: AddTableFocus, current_focus| {
let is_focused = current_focus == button_focus;
let base_style = Style::default().fg(if is_focused {
theme.bg // Reversed text color
} else {
theme.secondary // Normal text color
});
if is_focused {
base_style
.add_modifier(Modifier::BOLD)
.bg(theme.highlight) // Reversed background
} else {
base_style
}
};
let get_button_border_style = |button_focus: AddTableFocus, current_focus| {
if current_focus == button_focus {
Style::default().fg(theme.highlight)
} else {
Style::default().fg(theme.secondary)
}
};
// --- Add Button Rendering ---
let add_button = Paragraph::new(" Add ") let add_button = Paragraph::new(" Add ")
.style(Style::default().fg(theme.secondary)) .style(get_button_style(
AddTableFocus::AddColumnButton,
add_table_state.current_focus,
))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.secondary)), .border_style(get_button_border_style(
AddTableFocus::AddColumnButton,
add_table_state.current_focus,
)),
); );
f.render_widget(add_button, add_button_area); f.render_widget(add_button, add_button_area); // Render into the calculated area
// Bottom Buttons Area (Save, Cancel) // --- Indexes Table Rendering ---
let indexes_focused =
add_table_state.current_focus == AddTableFocus::IndexesTable;
let indexes_border_style = if indexes_focused {
Style::default().fg(theme.highlight)
} else {
Style::default().fg(theme.secondary)
};
let index_rows: Vec<Row<'_>> = add_table_state
.indexes
.iter()
.map(|index_name| {
Row::new(vec![Cell::from(index_name.clone())])
.style(Style::default().fg(theme.fg))
})
.collect();
let index_header_cells = ["Column Name"]
.iter()
.map(|h| Cell::from(*h).style(Style::default().fg(theme.accent)));
let index_header = Row::new(index_header_cells).height(1).bottom_margin(1);
let indexes_table =
Table::new(index_rows, [Constraint::Percentage(100)])
.header(index_header)
.block(
Block::default()
.title(Span::styled(" Indexes ", theme.fg))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(indexes_border_style),
)
.row_highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(theme.highlight),
)
.highlight_symbol(" > ");
f.render_stateful_widget(
indexes_table,
indexes_area,
&mut add_table_state.index_table_state,
);
// --- Links Table Rendering ---
let links_focused = add_table_state.current_focus == AddTableFocus::LinksTable;
let links_border_style = if links_focused {
Style::default().fg(theme.highlight)
} else {
Style::default().fg(theme.secondary)
};
let link_rows: Vec<Row<'_>> = add_table_state
.links
.iter()
.map(|link_def| {
Row::new(vec![
Cell::from(link_def.linked_table_name.clone()),
Cell::from(if link_def.is_required { "[X]" } else { "[ ]" }),
])
.style(Style::default().fg(theme.fg))
})
.collect();
let link_header_cells = ["Linked Table", "Req"]
.iter()
.map(|h| Cell::from(*h).style(Style::default().fg(theme.accent)));
let link_header = Row::new(link_header_cells).height(1).bottom_margin(1);
let links_table =
Table::new(link_rows, [Constraint::Percentage(80), Constraint::Min(5)])
.header(link_header)
.block(
Block::default()
.title(Span::styled(" Links ", theme.fg))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(links_border_style),
)
.row_highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(theme.highlight),
)
.highlight_symbol(" > ");
f.render_stateful_widget(
links_table,
links_area,
&mut add_table_state.link_table_state,
);
// --- Save/Cancel Buttons Rendering ---
let bottom_button_chunks = Layout::default() let bottom_button_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([ .constraints([
Constraint::Percentage(50), // Save Button Constraint::Percentage(33), // Save Button
Constraint::Percentage(50), // Cancel Button Constraint::Percentage(34), // Delete Button
].as_ref()) Constraint::Percentage(33), // Cancel Button
])
.split(bottom_buttons_area); .split(bottom_buttons_area);
// Save Button (Placeholder)
let save_button = Paragraph::new(" Save table ") let save_button = Paragraph::new(" Save table ")
.style(Style::default().fg(theme.secondary)) .style(get_button_style(
AddTableFocus::SaveButton,
add_table_state.current_focus,
))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.secondary)), .border_style(get_button_border_style(
AddTableFocus::SaveButton,
add_table_state.current_focus,
)),
); );
f.render_widget(save_button, bottom_button_chunks[0]); f.render_widget(save_button, bottom_button_chunks[0]);
// Cancel Button (Placeholder) let delete_button = Paragraph::new(" Delete Selected ")
.style(get_button_style(
AddTableFocus::DeleteSelectedButton,
add_table_state.current_focus,
))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(get_button_border_style(
AddTableFocus::DeleteSelectedButton,
add_table_state.current_focus,
)),
);
f.render_widget(delete_button, bottom_button_chunks[1]);
let cancel_button = Paragraph::new(" Cancel ") let cancel_button = Paragraph::new(" Cancel ")
.style(Style::default().fg(theme.secondary)) .style(get_button_style(
AddTableFocus::CancelButton,
add_table_state.current_focus,
))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.secondary)), .border_style(get_button_border_style(
AddTableFocus::CancelButton,
add_table_state.current_focus,
)),
); );
f.render_widget(cancel_button, bottom_button_chunks[1]); f.render_widget(cancel_button, bottom_button_chunks[2]);
} }

View File

@@ -2,3 +2,4 @@
pub mod form_e; pub mod form_e;
pub mod auth_e; pub mod auth_e;
pub mod add_table_e;

View File

@@ -0,0 +1,343 @@
// src/functions/modes/edit/add_table_e.rs
use crate::state::pages::add_table::AddTableState;
use crate::state::pages::canvas_state::CanvasState; // Use trait
use crossterm::event::{KeyCode, KeyEvent};
use std::error::Error;
#[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 AddTable view canvas.
pub async fn execute_edit_action(
action: &str,
key: KeyEvent, // Needed for insert_char
state: &mut AddTableState,
ideal_cursor_column: &mut usize,
// Add other params like grpc_client if needed for future actions (e.g., validation)
) -> Result<String, Box<dyn Error>> {
// Use the CanvasState trait methods implemented for AddTableState
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()) // No message needed for char insertion
}
"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" => {
let num_fields = AddTableState::INPUT_FIELD_COUNT;
if num_fields > 0 {
let current_field = state.current_field();
let new_field = (current_field + 1) % num_fields;
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())
}
"prev_field" => {
let num_fields = AddTableState::INPUT_FIELD_COUNT;
if num_fields > 0 {
let current_field = state.current_field();
let new_field = if current_field == 0 {
num_fields - 1
} else {
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_left" => {
let new_pos = state.current_cursor_pos().saturating_sub(1);
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
Ok("".to_string())
}
"move_right" => {
let current_input = state.get_current_input();
let current_pos = state.current_cursor_pos();
if current_pos < current_input.len() {
let new_pos = current_pos + 1;
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
}
Ok("".to_string())
}
"move_up" => {
let num_fields = AddTableState::INPUT_FIELD_COUNT;
if num_fields > 0 {
let current_field = state.current_field();
if current_field > 0 {
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_down" => {
let num_fields = AddTableState::INPUT_FIELD_COUNT;
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" => {
state.set_current_cursor_pos(0);
*ideal_cursor_column = 0;
Ok("".to_string())
}
"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 AddTableState::INPUT_FIELD_COUNT > 0 {
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" => {
let num_fields = AddTableState::INPUT_FIELD_COUNT;
if num_fields > 0 {
let new_field = num_fields - 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_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 {
find_word_end(current_input, new_pos + 1)
} else {
new_pos
};
let max_valid_index = current_input.len().saturating_sub(1);
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" => {
let current_input = state.get_current_input();
if !current_input.is_empty() {
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())
}
// Actions handled by main event loop (mode changes, save, revert)
"exit_edit_mode" | "save" | "revert" => {
Ok("Action handled by main loop".to_string())
}
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
}
}

View File

@@ -5,101 +5,126 @@ use crate::state::{
pages::add_table::{AddTableFocus, AddTableState}, pages::add_table::{AddTableFocus, AddTableState},
}; };
use crossterm::event::{KeyEvent}; use crossterm::event::{KeyEvent};
use ratatui::widgets::TableState; // Import TableState use ratatui::widgets::TableState;
use crate::tui::functions::common::add_table::handle_add_column_action;
/// Handles navigation events specifically for the Add Table view. /// Handles navigation events specifically for the Add Table view.
/// Returns true if the event was handled, false otherwise. /// Returns true if the event was handled, false otherwise.
pub fn handle_add_table_navigation( pub fn handle_add_table_navigation(
key: KeyEvent, key: KeyEvent,
config: &Config, config: &Config,
_app_state: &AppState, // Keep for potential future use (e.g., checking permissions) app_state: &mut AppState,
add_table_state: &mut AddTableState, add_table_state: &mut AddTableState,
command_message: &mut String, command_message: &mut String,
) -> bool { ) -> bool {
let action = config.get_general_action(key.code, key.modifiers); let action = config.get_general_action(key.code, key.modifiers);
let current_focus = add_table_state.current_focus; let current_focus = add_table_state.current_focus;
let mut handled = true; // Assume handled unless logic determines otherwise let mut handled = true; // Assume handled unless logic determines otherwise
let mut new_focus = current_focus; // Initialize new_focus
// Define focus groups for horizontal navigation
let is_left_pane_focus = matches!(current_focus,
AddTableFocus::ColumnsTable | AddTableFocus::IndexesTable | AddTableFocus::LinksTable
);
let is_right_pane_general_focus = matches!(current_focus, // Non-canvas elements in right pane
AddTableFocus::AddColumnButton | AddTableFocus::SaveButton | AddTableFocus::CancelButton
);
let is_canvas_input_focus = matches!(current_focus,
AddTableFocus::InputTableName | AddTableFocus::InputColumnName | AddTableFocus::InputColumnType
);
match action.as_deref() { match action.as_deref() {
// --- Vertical Navigation (Up/Down) --- // --- Vertical Navigation (Up/Down) ---
Some("move_up") => { Some("move_up") => {
let mut new_focus = current_focus; // Start with current focus
match current_focus { match current_focus {
AddTableFocus::InputTableName => new_focus = AddTableFocus::CancelButton, // Wrap top AddTableFocus::InputTableName => new_focus = AddTableFocus::CancelButton, // Wrap top (right pane)
AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputTableName, AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputTableName,
AddTableFocus::InputColumnType => new_focus = AddTableFocus::InputColumnName, AddTableFocus::InputColumnType => new_focus = AddTableFocus::InputColumnName,
AddTableFocus::AddColumnButton => new_focus = AddTableFocus::InputColumnType, AddTableFocus::AddColumnButton => new_focus = AddTableFocus::InputColumnType,
AddTableFocus::ColumnsTable => { AddTableFocus::ColumnsTable => { // Left pane navigation
if !navigate_table_up(&mut add_table_state.column_table_state, add_table_state.columns.len()) { if !navigate_table_up(&mut add_table_state.column_table_state, add_table_state.columns.len()) {
new_focus = AddTableFocus::AddColumnButton; // Move focus up if at table top // If at top of columns, potentially wrap to bottom of left pane (LinksTable) or stay? Let's stay for now.
// Or maybe move to AddColumnButton? Let's try moving up from right pane instead.
new_focus = AddTableFocus::AddColumnButton; // Tentative: move focus up from right pane
} }
// Keep focus on table while navigating within it
} }
AddTableFocus::IndexesTable => { AddTableFocus::IndexesTable => {
if !navigate_table_up(&mut add_table_state.index_table_state, add_table_state.indexes.len()) { if !navigate_table_up(&mut add_table_state.index_table_state, add_table_state.indexes.len()) {
new_focus = AddTableFocus::ColumnsTable; // Move focus up new_focus = AddTableFocus::ColumnsTable;
} }
} }
AddTableFocus::LinksTable => { AddTableFocus::LinksTable => {
if !navigate_table_up(&mut add_table_state.link_table_state, add_table_state.links.len()) { if !navigate_table_up(&mut add_table_state.link_table_state, add_table_state.links.len()) {
new_focus = AddTableFocus::IndexesTable; // Move focus up new_focus = AddTableFocus::IndexesTable;
} }
} }
AddTableFocus::SaveButton => new_focus = AddTableFocus::LinksTable, AddTableFocus::SaveButton => new_focus = AddTableFocus::LinksTable, // Move up to left pane bottom
AddTableFocus::CancelButton => new_focus = AddTableFocus::SaveButton, AddTableFocus::DeleteSelectedButton => new_focus = AddTableFocus::SaveButton,
AddTableFocus::CancelButton => new_focus = AddTableFocus::DeleteSelectedButton,
} }
add_table_state.current_focus = new_focus;
*command_message = format!("Focus set to {:?}", add_table_state.current_focus);
} }
Some("move_down") => { Some("move_down") => {
let mut new_focus = current_focus; // Start with current focus
match current_focus { match current_focus {
AddTableFocus::InputTableName => new_focus = AddTableFocus::InputColumnName, AddTableFocus::InputTableName => new_focus = AddTableFocus::InputColumnName,
AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputColumnType, AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputColumnType,
AddTableFocus::InputColumnType => new_focus = AddTableFocus::AddColumnButton, AddTableFocus::InputColumnType => new_focus = AddTableFocus::AddColumnButton,
AddTableFocus::AddColumnButton => new_focus = AddTableFocus::ColumnsTable, AddTableFocus::AddColumnButton => new_focus = AddTableFocus::ColumnsTable, // Move down to left pane top
AddTableFocus::ColumnsTable => { AddTableFocus::ColumnsTable => { // Left pane navigation
if !navigate_table_down(&mut add_table_state.column_table_state, add_table_state.columns.len()) { if !navigate_table_down(&mut add_table_state.column_table_state, add_table_state.columns.len()) {
new_focus = AddTableFocus::IndexesTable; // Move focus down if at table bottom new_focus = AddTableFocus::IndexesTable; // Move to next left pane item
} }
// Keep focus on table while navigating within it
} }
AddTableFocus::IndexesTable => { AddTableFocus::IndexesTable => {
if !navigate_table_down(&mut add_table_state.index_table_state, add_table_state.indexes.len()) { if !navigate_table_down(&mut add_table_state.index_table_state, add_table_state.indexes.len()) {
new_focus = AddTableFocus::LinksTable; // Move focus down new_focus = AddTableFocus::LinksTable;
} }
} }
AddTableFocus::LinksTable => { AddTableFocus::LinksTable => {
if !navigate_table_down(&mut add_table_state.link_table_state, add_table_state.links.len()) { if !navigate_table_down(&mut add_table_state.link_table_state, add_table_state.links.len()) {
new_focus = AddTableFocus::SaveButton; // Move focus down new_focus = AddTableFocus::SaveButton; // Move down to right pane bottom
} }
} }
AddTableFocus::SaveButton => new_focus = AddTableFocus::CancelButton, AddTableFocus::SaveButton => new_focus = AddTableFocus::DeleteSelectedButton,
AddTableFocus::CancelButton => new_focus = AddTableFocus::InputTableName, // Wrap bottom AddTableFocus::DeleteSelectedButton => new_focus = AddTableFocus::CancelButton,
AddTableFocus::CancelButton => new_focus = AddTableFocus::InputTableName, // Wrap bottom (right pane)
} }
add_table_state.current_focus = new_focus;
*command_message = format!("Focus set to {:?}", add_table_state.current_focus);
} }
// --- Horizontal Navigation (Left/Right) --- // --- Horizontal Navigation (Left/Right) ---
Some("next_option") => { // 'l' or Right Some("next_option") => { // 'l' or Right: Move from Left Pane to Right Pane
add_table_state.current_focus = match current_focus { if is_left_pane_focus {
AddTableFocus::SaveButton => AddTableFocus::CancelButton, new_focus = match current_focus {
_ => current_focus, // No change for others yet // Map left pane items to corresponding right pane items (approximate vertical alignment)
}; AddTableFocus::ColumnsTable => AddTableFocus::InputTableName,
*command_message = format!("Focus set to {:?}", add_table_state.current_focus); AddTableFocus::IndexesTable => AddTableFocus::InputColumnName, // Or AddColumnButton?
AddTableFocus::LinksTable => AddTableFocus::SaveButton,
_ => current_focus, // Should not happen based on is_left_pane_focus
};
} else if is_right_pane_general_focus || is_canvas_input_focus {
// If already in right pane, maybe wrap Save -> Cancel or stay? Let's handle Save->Cancel only.
if current_focus == AddTableFocus::SaveButton {
new_focus = AddTableFocus::CancelButton;
}
}
} }
Some("previous_option") => { // 'h' or Left Some("previous_option") => { // 'h' or Left: Move from Right Pane to Left Pane
add_table_state.current_focus = match current_focus { if is_right_pane_general_focus {
AddTableFocus::CancelButton => AddTableFocus::SaveButton, new_focus = match current_focus {
_ => current_focus, // No change for others yet // Map right pane items back to left pane items (approximate vertical alignment)
}; AddTableFocus::InputTableName | AddTableFocus::InputColumnName | AddTableFocus::InputColumnType | AddTableFocus::AddColumnButton => AddTableFocus::ColumnsTable, // Go to top of left pane
*command_message = format!("Focus set to {:?}", add_table_state.current_focus); AddTableFocus::SaveButton | AddTableFocus::CancelButton => AddTableFocus::LinksTable, // Go to bottom of left pane
_ => current_focus, // Should not happen
};
} else if is_left_pane_focus {
// If already in left pane, pressing 'h' could wrap to Cancel button?
new_focus = AddTableFocus::CancelButton; // Wrap left-to-right bottom
}
} }
// --- Tab / Shift+Tab Navigation --- // --- Tab / Shift+Tab Navigation (Keep as vertical cycle) ---
Some("next_field") => { // Tab Some("next_field") => { // Tab
add_table_state.current_focus = match current_focus { new_focus = match current_focus {
AddTableFocus::InputTableName => AddTableFocus::InputColumnName, AddTableFocus::InputTableName => AddTableFocus::InputColumnName,
AddTableFocus::InputColumnName => AddTableFocus::InputColumnType, AddTableFocus::InputColumnName => AddTableFocus::InputColumnType,
AddTableFocus::InputColumnType => AddTableFocus::AddColumnButton, AddTableFocus::InputColumnType => AddTableFocus::AddColumnButton,
@@ -107,13 +132,13 @@ pub fn handle_add_table_navigation(
AddTableFocus::ColumnsTable => AddTableFocus::IndexesTable, AddTableFocus::ColumnsTable => AddTableFocus::IndexesTable,
AddTableFocus::IndexesTable => AddTableFocus::LinksTable, AddTableFocus::IndexesTable => AddTableFocus::LinksTable,
AddTableFocus::LinksTable => AddTableFocus::SaveButton, AddTableFocus::LinksTable => AddTableFocus::SaveButton,
AddTableFocus::SaveButton => AddTableFocus::CancelButton, AddTableFocus::SaveButton => AddTableFocus::DeleteSelectedButton,
AddTableFocus::DeleteSelectedButton => AddTableFocus::CancelButton,
AddTableFocus::CancelButton => AddTableFocus::InputTableName, // Wrap AddTableFocus::CancelButton => AddTableFocus::InputTableName, // Wrap
}; };
*command_message = format!("Focus set to {:?}", add_table_state.current_focus);
} }
Some("prev_field") => { // Shift+Tab Some("prev_field") => { // Shift+Tab
add_table_state.current_focus = match current_focus { new_focus = match current_focus {
AddTableFocus::InputTableName => AddTableFocus::CancelButton, // Wrap AddTableFocus::InputTableName => AddTableFocus::CancelButton, // Wrap
AddTableFocus::InputColumnName => AddTableFocus::InputTableName, AddTableFocus::InputColumnName => AddTableFocus::InputTableName,
AddTableFocus::InputColumnType => AddTableFocus::InputColumnName, AddTableFocus::InputColumnType => AddTableFocus::InputColumnName,
@@ -122,92 +147,92 @@ pub fn handle_add_table_navigation(
AddTableFocus::IndexesTable => AddTableFocus::ColumnsTable, AddTableFocus::IndexesTable => AddTableFocus::ColumnsTable,
AddTableFocus::LinksTable => AddTableFocus::IndexesTable, AddTableFocus::LinksTable => AddTableFocus::IndexesTable,
AddTableFocus::SaveButton => AddTableFocus::LinksTable, AddTableFocus::SaveButton => AddTableFocus::LinksTable,
AddTableFocus::CancelButton => AddTableFocus::SaveButton, AddTableFocus::DeleteSelectedButton => AddTableFocus::SaveButton,
AddTableFocus::CancelButton => AddTableFocus::DeleteSelectedButton,
}; };
*command_message = format!("Focus set to {:?}", add_table_state.current_focus);
} }
// --- Selection --- // --- Selection ---
Some("select") => { Some("select") => {
match current_focus { match current_focus {
AddTableFocus::AddColumnButton => { AddTableFocus::AddColumnButton => {
*command_message = "Action: Add Column (Not Implemented)".to_string(); if let Some(focus_after_add) = handle_add_column_action(add_table_state, command_message) {
// TODO: Implement logic to add column based on inputs new_focus = focus_after_add;
// Clear input fields, add to columns list, mark unsaved changes }
// add_table_state.add_column(); // Example method call
} }
AddTableFocus::SaveButton => { AddTableFocus::SaveButton => {
*command_message = "Action: Save Table (Not Implemented)".to_string(); *command_message = "Action: Save Table (Not Implemented)".to_string();
// TODO: Implement logic to save table (e.g., call API) // TODO: Implement logic
// Mark changes as saved }
// add_table_state.save_table(); // Example method call AddTableFocus::DeleteSelectedButton => {
*command_message = "Action: Delete selected".to_string();
// TODO: Implement logic
} }
AddTableFocus::CancelButton => { AddTableFocus::CancelButton => {
*command_message = "Action: Cancel Add Table".to_string(); *command_message = "Action: Cancel Add Table".to_string();
// TODO: Implement logic to navigate back (e.g., update AppView history) // TODO: Implement logic
// Maybe show a confirmation dialog if there are unsaved changes
// buffer_state.go_back(); // Example call
} }
// Selecting input fields usually means entering Edit mode (handled elsewhere)
// Selecting tables might mean focusing on them for editing/deletion (TODO)
AddTableFocus::ColumnsTable => { AddTableFocus::ColumnsTable => {
if let Some(index) = add_table_state.column_table_state.selected() { if let Some(index) = add_table_state.column_table_state.selected() {
*command_message = format!("Selected column index {}", index); *command_message = format!("Selected column index {}", index);
// TODO: Add logic for editing/deleting selected column } else { *command_message = "No column selected".to_string(); }
} else {
*command_message = "No column selected".to_string();
}
} }
AddTableFocus::IndexesTable => { AddTableFocus::IndexesTable => {
if let Some(index) = add_table_state.index_table_state.selected() { if let Some(index) = add_table_state.index_table_state.selected() {
*command_message = format!("Selected index index {}", index); *command_message = format!("Selected index index {}", index);
// TODO: Add logic for editing/deleting selected index } else { *command_message = "No index selected".to_string(); }
} else {
*command_message = "No index selected".to_string();
}
} }
AddTableFocus::LinksTable => { AddTableFocus::LinksTable => {
if let Some(index) = add_table_state.link_table_state.selected() { if let Some(index) = add_table_state.link_table_state.selected() {
*command_message = format!("Selected link index {}", index); *command_message = format!("Selected link index {}", index);
// TODO: Add logic for editing/deleting selected link } else { *command_message = "No link selected".to_string(); }
} else {
*command_message = "No link selected".to_string();
}
} }
_ => { _ => { // Input fields
// For InputTableName, InputColumnName, InputColumnType,
// the main event loop should handle 'select' by potentially
// switching to Edit mode if not already in it.
// We don't need specific logic here for that.
*command_message = format!("Select on {:?}", current_focus); *command_message = format!("Select on {:?}", current_focus);
handled = false; // Let main loop handle edit mode toggle maybe handled = false; // Let main loop handle edit mode toggle maybe
} }
} }
// Keep handled = true for select actions unless specifically set to false
} }
// --- Other General Keys (Ignore for add_table nav) --- // --- Other General Keys ---
Some("toggle_sidebar") | Some("toggle_buffer_list") => { Some("toggle_sidebar") | Some("toggle_buffer_list") => {
handled = false; // Let global handler manage these handled = false;
} }
// --- No matching action --- // --- No matching action ---
_ => handled = false, // Event not handled by add_table navigation _ => handled = false,
} }
// If focus changed TO a table, select the first row if nothing is selected // Update focus state if it changed and was handled
if handled && current_focus != add_table_state.current_focus { if handled && current_focus != new_focus {
add_table_state.current_focus = new_focus;
*command_message = format!("Focus set to {:?}", add_table_state.current_focus);
// --- THIS IS THE KEY PART ---
// Check if the *new* focus target is one of the canvas input fields
let new_is_canvas_input_focus = matches!(new_focus,
AddTableFocus::InputTableName | AddTableFocus::InputColumnName | AddTableFocus::InputColumnType
);
// Set focus_outside_canvas based on whether the new focus is NOT an input field
app_state.ui.focus_outside_canvas = !new_is_canvas_input_focus; // <--- Sets the flag correctly
// --- END KEY PART ---
// Select first item when focusing a table
match add_table_state.current_focus { match add_table_state.current_focus {
AddTableFocus::ColumnsTable if add_table_state.column_table_state.selected().is_none() && !add_table_state.columns.is_empty() => { AddTableFocus::ColumnsTable if add_table_state.column_table_state.selected().is_none() && !add_table_state.columns.is_empty() => {
add_table_state.column_table_state.select(Some(0)); add_table_state.column_table_state.select(Some(0));
} }
AddTableFocus::IndexesTable if add_table_state.index_table_state.selected().is_none() && !add_table_state.indexes.is_empty() => { AddTableFocus::IndexesTable if add_table_state.index_table_state.selected().is_none() && !add_table_state.indexes.is_empty() => {
add_table_state.index_table_state.select(Some(0)); add_table_state.index_table_state.select(Some(0));
} }
AddTableFocus::LinksTable if add_table_state.link_table_state.selected().is_none() && !add_table_state.links.is_empty() => { AddTableFocus::LinksTable if add_table_state.link_table_state.selected().is_none() && !add_table_state.links.is_empty() => {
add_table_state.link_table_state.select(Some(0)); add_table_state.link_table_state.select(Some(0));
} }_ => {}
_ => {} // No action needed for other focus states
} }
} else if !handled {
// ...
} }
handled handled
@@ -217,20 +242,19 @@ pub fn handle_add_table_navigation(
// Helper function for navigating up within a table state // Helper function for navigating up within a table state
// Returns true if navigation happened within the table, false if it reached the top // Returns true if navigation happened within the table, false if it reached the top
fn navigate_table_up(table_state: &mut TableState, item_count: usize) -> bool { fn navigate_table_up(table_state: &mut TableState, item_count: usize) -> bool {
if item_count == 0 { return false; } // Cannot navigate empty table if item_count == 0 { return false; }
let current_selection = table_state.selected(); let current_selection = table_state.selected();
match current_selection { match current_selection {
Some(index) => { Some(index) => {
if index > 0 { if index > 0 {
table_state.select(Some(index - 1)); table_state.select(Some(index - 1));
true // Moved up within table true
} else { } else {
false // Was at the top false // Was at the top
} }
} }
None => { None => {
// If nothing selected, moving up could select the last item table_state.select(Some(item_count - 1)); // Select last item
table_state.select(Some(item_count - 1));
true true
} }
} }
@@ -239,20 +263,19 @@ fn navigate_table_up(table_state: &mut TableState, item_count: usize) -> bool {
// Helper function for navigating down within a table state // Helper function for navigating down within a table state
// Returns true if navigation happened within the table, false if it reached the bottom // Returns true if navigation happened within the table, false if it reached the bottom
fn navigate_table_down(table_state: &mut TableState, item_count: usize) -> bool { fn navigate_table_down(table_state: &mut TableState, item_count: usize) -> bool {
if item_count == 0 { return false; } // Cannot navigate empty table if item_count == 0 { return false; }
let current_selection = table_state.selected(); let current_selection = table_state.selected();
match current_selection { match current_selection {
Some(index) => { Some(index) => {
if index < item_count - 1 { if index < item_count - 1 {
table_state.select(Some(index + 1)); table_state.select(Some(index + 1));
true // Moved down within table true
} else { } else {
false // Was at the bottom false // Was at the bottom
} }
} }
None => { None => {
// If nothing selected, moving down could select the first item table_state.select(Some(0)); // Select first item
table_state.select(Some(0));
true true
} }
} }

View File

@@ -8,13 +8,14 @@ use crate::state::{
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::state::app::buffer::AppView; use crate::state::app::buffer::AppView;
use crate::state::app::buffer::BufferState; use crate::state::app::buffer::BufferState;
use crate::state::pages::add_table::AddTableState;
/// Handles navigation events specifically for the Admin Panel view. /// Handles navigation events specifically for the Admin Panel view.
/// Returns true if the event was handled, false otherwise. /// Returns true if the event was handled, false otherwise.
pub fn handle_admin_navigation( pub fn handle_admin_navigation(
key: KeyEvent, key: KeyEvent,
config: &Config, config: &Config,
app_state: &AppState, app_state: &mut AppState,
admin_state: &mut AdminState, admin_state: &mut AdminState,
buffer_state: &mut BufferState, buffer_state: &mut BufferState,
command_message: &mut String, command_message: &mut String,
@@ -160,8 +161,37 @@ pub fn handle_admin_navigation(
// TODO: Trigger action for Button 1 // TODO: Trigger action for Button 1
} }
AdminFocus::Button2 => { AdminFocus::Button2 => {
buffer_state.update_history(AppView::AddTable); // --- Prepare AddTableState based on persistent selections ---
*command_message = "Navigating to Add Table page...".to_string(); if let Some(p_idx) = admin_state.selected_profile_index {
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
let selected_profile_name = profile.name.clone();
// Create and populate the new AddTableState
let new_add_table_state = AddTableState {
profile_name: selected_profile_name,
// Reset other fields to defaults for a fresh start
..AddTableState::default()
};
// Assign the prepared state
admin_state.add_table_state = new_add_table_state;
// Switch view
buffer_state.update_history(AppView::AddTable);
app_state.ui.focus_outside_canvas = false;
*command_message = format!(
"Navigating to Add Table for profile '{}'...",
admin_state.add_table_state.profile_name
);
} else {
*command_message = "Error: Selected profile index out of bounds.".to_string();
}
} else {
*command_message = "Please select a profile ([*]) first.".to_string();
}
// --- End preparation ---
} }
AdminFocus::Button3 => { AdminFocus::Button3 => {
*command_message = "Action: Change Table (Not Implemented)".to_string(); *command_message = "Action: Change Table (Not Implemented)".to_string();

View File

@@ -2,3 +2,4 @@
pub mod auth_ro; pub mod auth_ro;
pub mod form_ro; pub mod form_ro;
pub mod add_table_ro;

View File

@@ -0,0 +1,244 @@
// src/functions/modes/read_only/add_table_ro.rs
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::state::pages::add_table::AddTableState;
use crate::state::pages::canvas_state::CanvasState; // Use trait for common actions
use crate::state::app::state::AppState;
use std::error::Error;
// Re-use word navigation helpers if they are public or move them to a common module
// For now, duplicating them here for simplicity. Consider refactoring later.
#[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
}
// Note: find_prev_word_end might need adjustments based on desired behavior.
// This version finds the end of the word *before* the previous word start.
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
let prev_start = find_prev_word_start(text, current_pos);
if prev_start == 0 { return 0; }
// Find the end of the word that starts at prev_start - 1
find_word_end(text, prev_start.saturating_sub(1))
}
/// Executes read-only actions for the AddTable view canvas.
pub async fn execute_action(
action: &str,
app_state: &mut AppState, // Needed for focus_outside_canvas
state: &mut AddTableState,
ideal_cursor_column: &mut usize,
key_sequence_tracker: &mut KeySequenceTracker,
command_message: &mut String, // Keep for potential messages
) -> Result<String, Box<dyn Error>> {
// Use the CanvasState trait methods implemented for AddTableState
match action {
"move_up" => {
key_sequence_tracker.reset();
let num_fields = AddTableState::INPUT_FIELD_COUNT;
if num_fields == 0 { return Ok("No fields.".to_string()); }
let current_field = state.current_field(); // Gets the index (0, 1, or 2)
if current_field > 0 {
// This handles moving from field 2 -> 1, or 1 -> 0
let new_field = current_field - 1;
state.set_current_field(new_field);
// ... (rest of the logic to set cursor position) ...
} else {
// --- THIS IS WHERE THE FIX GOES ---
// current_field is 0 (InputTableName), and user pressed Up.
// We need to move focus *outside* the canvas.
// Set the flag to indicate focus is leaving the canvas
app_state.ui.focus_outside_canvas = true;
// Decide which element gets focus. Based on your layout and the
// downward navigation (CancelButton wraps to InputTableName),
// moving up from InputTableName should likely go to CancelButton.
state.current_focus = crate::state::pages::add_table::AddTableFocus::CancelButton;
// Reset the sequence tracker as the action is complete
key_sequence_tracker.reset();
// Return a message indicating the focus change
return Ok("Focus moved above canvas".to_string());
// --- END FIX ---
}
// If we moved within the canvas (e.g., 1 -> 0), return empty string
Ok("".to_string())
}
"move_down" => {
key_sequence_tracker.reset();
let num_fields = AddTableState::INPUT_FIELD_COUNT;
if num_fields == 0 { return Ok("No fields.".to_string()); }
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_cursor_pos = current_input.len(); // Allow cursor at end
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
state.set_current_cursor_pos(new_pos);
} else {
// Move focus outside canvas when moving down from the last field
app_state.ui.focus_outside_canvas = true;
// Set focus to the first element outside canvas (AddColumnButton)
state.current_focus = crate::state::pages::add_table::AddTableFocus::AddColumnButton;
key_sequence_tracker.reset();
return Ok("Focus moved below canvas".to_string());
}
Ok("".to_string())
}
"move_first_line" => {
key_sequence_tracker.reset();
if AddTableState::INPUT_FIELD_COUNT > 0 {
state.set_current_field(0);
let current_input = state.get_current_input();
let max_cursor_pos = current_input.len();
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos; // Update ideal column
}
Ok("".to_string())
}
"move_last_line" => {
key_sequence_tracker.reset();
let num_fields = AddTableState::INPUT_FIELD_COUNT;
if num_fields > 0 {
let last_field_index = num_fields - 1;
state.set_current_field(last_field_index);
let current_input = state.get_current_input();
let max_cursor_pos = current_input.len();
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos; // Update ideal column
}
Ok("".to_string())
}
"move_left" => {
let current_pos = state.current_cursor_pos();
let new_pos = current_pos.saturating_sub(1);
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
Ok("".to_string())
}
"move_right" => {
let current_input = state.get_current_input();
let current_pos = state.current_cursor_pos();
// Allow moving cursor one position past the end
if current_pos < current_input.len() {
let new_pos = current_pos + 1;
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
}
Ok("".to_string())
}
"move_word_next" => {
let current_input = state.get_current_input();
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
let final_pos = new_pos.min(current_input.len()); // Allow cursor at end
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();
let current_pos = state.current_cursor_pos();
let new_pos = find_word_end(current_input, current_pos);
// If find_word_end returns current_pos, try starting search from next char
let final_pos = if new_pos == current_pos && current_pos < current_input.len() {
find_word_end(current_input, current_pos + 1)
} else {
new_pos
};
let max_valid_index = current_input.len(); // Allow cursor at end
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" => {
let current_input = state.get_current_input();
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();
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())
}
"move_line_start" => {
state.set_current_cursor_pos(0);
*ideal_cursor_column = 0;
Ok("".to_string())
}
"move_line_end" => {
let current_input = state.get_current_input();
let new_pos = current_input.len(); // Allow cursor at end
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
Ok("".to_string())
}
// Actions handled by main event loop (mode changes)
"enter_edit_mode_before" | "enter_edit_mode_after" | "enter_command_mode" | "exit_highlight_mode" => {
key_sequence_tracker.reset();
Ok("Mode change handled by main loop".to_string())
}
_ => {
key_sequence_tracker.reset();
command_message.clear(); // Clear message for unhandled actions
Ok(format!("Unknown read-only action: {}", action))
},
}
}

View File

@@ -1,13 +1,17 @@
// src/modes/canvas/edit.rs // src/modes/canvas/edit.rs
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::state::pages::{auth::{LoginState, RegisterState}, canvas_state::CanvasState}; use crate::state::pages::{
auth::{LoginState, RegisterState},
canvas_state::CanvasState,
};
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use crate::state::pages::add_table::AddTableState; // Added
use crate::modes::handlers::event::EventOutcome; use crate::modes::handlers::event::EventOutcome;
use crate::functions::modes::edit::{auth_e, form_e}; use crate::functions::modes::edit::{auth_e, form_e};
use crate::functions::modes::edit::add_table_e; // Added
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
// Removed duplicate/unused imports
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditEventOutcome { pub enum EditEventOutcome {
@@ -21,105 +25,148 @@ pub async fn handle_edit_event(
form_state: &mut FormState, form_state: &mut FormState,
login_state: &mut LoginState, login_state: &mut LoginState,
register_state: &mut RegisterState, register_state: &mut RegisterState,
add_table_state: &mut AddTableState, // Added
ideal_cursor_column: &mut usize, ideal_cursor_column: &mut usize,
// command_message: &mut String, // Removed as messages are returned
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &AppState, app_state: &AppState,
) -> Result<EditEventOutcome, Box<dyn std::error::Error>> { ) -> Result<EditEventOutcome, Box<dyn std::error::Error>> {
// 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,
key.code, key.code,
key.modifiers key.modifiers,
) { ) {
return Ok(EditEventOutcome::Message("Switching to Command Mode...".to_string())); // This mode change should likely be handled in event.rs
// Returning a message here might prevent the mode switch.
// Consider if this check is necessary here.
return Ok(EditEventOutcome::Message(
"Command mode entry handled globally.".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,
key.modifiers key.modifiers,
) { ).as_deref() {
if matches!(action, "save" | "revert") { if matches!(action, "save" | "revert") {
// Ensure all branches result in Result<String, Error> before the final Ok(...) wrap
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, action,
login_state, login_state,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String on success )
.await?
} else if app_state.ui.show_register { } else if app_state.ui.show_register {
// Keeping this block as requested
auth_e::execute_common_action( auth_e::execute_common_action(
action, action,
register_state, register_state,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String on success )
} else { .await? // Results in String on success
} else if app_state.ui.show_add_table {
// Placeholder - common actions for AddTable might be different
format!(
"Action '{}' not fully implemented for Add Table view here.",
action
)
} else { // Assuming FormState otherwise
let outcome = form_e::execute_common_action( let outcome = form_e::execute_common_action(
action, action,
form_state, form_state,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await?; // This returns EventOutcome on success )
.await?;
// Extract the message string from the EventOutcome
match outcome { match outcome {
EventOutcome::Ok(msg) => msg, EventOutcome::Ok(msg) => msg,
EventOutcome::DataSaved(_, msg) => msg, EventOutcome::DataSaved(_, msg) => msg,
_ => format!("Unexpected outcome from common action: {:?}", outcome), _ => format!(
"Unexpected outcome from common action: {:?}",
outcome
),
} }
}; };
// Wrap the resulting String message
return Ok(EditEventOutcome::Message(message_string)); return Ok(EditEventOutcome::Message(message_string));
} }
} }
// Edit-specific actions // Edit-specific actions
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) { if let Some(action) =
config.get_edit_action_for_key(key.code, key.modifiers)
.as_deref() {
if action == "exit" { if action == "exit" {
// Handle exiting suggestion mode in Register view first
if app_state.ui.show_register && register_state.in_suggestion_mode { if app_state.ui.show_register && register_state.in_suggestion_mode {
// Call the action, get Result<String, Error>
let msg = auth_e::execute_edit_action( let msg = auth_e::execute_edit_action(
"exit_suggestion_mode", "exit_suggestion_mode", // Specific action for suggestion exit
key, key,
register_state, register_state,
ideal_cursor_column, ideal_cursor_column,
grpc_client, grpc_client,
current_position, current_position,
total_count, total_count,
).await?; // Results in String on success )
// Wrap the String message .await?;
return Ok(EditEventOutcome::Message(msg)); return Ok(EditEventOutcome::Message(msg));
} else { } else {
// Signal exit // Signal exit from Edit mode
return Ok(EditEventOutcome::ExitEditMode); return Ok(EditEventOutcome::ExitEditMode);
} }
} }
// Special handling for role field suggestions // Special handling for role field suggestions (Register view only)
if app_state.ui.show_register && register_state.current_field() == 4 { if app_state.ui.show_register && register_state.current_field() == 4 {
if !register_state.in_suggestion_mode && key.code == KeyCode::Tab && key.modifiers == KeyModifiers::NONE { // Check if Tab was pressed to *enter* suggestion mode
if !register_state.in_suggestion_mode
&& key.code == KeyCode::Tab
&& key.modifiers == KeyModifiers::NONE
{
register_state.update_role_suggestions(); register_state.update_role_suggestions();
if !register_state.role_suggestions.is_empty() { if !register_state.role_suggestions.is_empty() {
register_state.in_suggestion_mode = true; register_state.in_suggestion_mode = true;
register_state.selected_suggestion_index = Some(0); register_state.selected_suggestion_index = Some(0); // Select first suggestion
return Ok(EditEventOutcome::Message("Suggestions shown".to_string())); return Ok(EditEventOutcome::Message(
} else { // Added else here for clarity "Suggestions shown".to_string(),
return Ok(EditEventOutcome::Message("No suggestions available".to_string())); ));
} else {
return Ok(EditEventOutcome::Message(
"No suggestions available".to_string(),
));
} }
} }
// Handle suggestion navigation/selection if already in suggestion mode
if register_state.in_suggestion_mode
&& matches!(
action,
"suggestion_down"
| "suggestion_up"
| "select_suggestion"
)
{
let msg = auth_e::execute_edit_action(
action, // Pass the specific suggestion action
key,
register_state,
ideal_cursor_column,
grpc_client,
current_position,
total_count,
)
.await?;
return Ok(EditEventOutcome::Message(msg));
}
} }
// Execute other edit actions // Execute other edit actions based on the current view
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, action,
@@ -128,8 +175,18 @@ pub async fn handle_edit_event(
ideal_cursor_column, ideal_cursor_column,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String )
.await?
} else if app_state.ui.show_add_table {
add_table_e::execute_edit_action(
action,
key,
add_table_state,
ideal_cursor_column,
// Pass other necessary params if add_table_e needs them
)
.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(
action, action,
@@ -138,9 +195,11 @@ pub async fn handle_edit_event(
ideal_cursor_column, ideal_cursor_column,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String )
.await?
} else { } else {
// Assuming FormState otherwise
form_e::execute_edit_action( form_e::execute_edit_action(
action, action,
key, key,
@@ -148,32 +207,42 @@ pub async fn handle_edit_event(
ideal_cursor_column, ideal_cursor_column,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String )
.await?
}; };
// Wrap the resulting String message
return Ok(EditEventOutcome::Message(msg)); return Ok(EditEventOutcome::Message(msg));
} }
// Character insertion // --- Character insertion ---
if let KeyCode::Char(_) = key.code { if let KeyCode::Char(c) = key.code {
// Exit suggestion mode in Register view if a character is typed
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;
} }
// Execute insert_char action // Execute insert_char action based on the current view
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(
"insert_char", "insert_char",
key, key, // Pass the key event containing the char
login_state, login_state,
ideal_cursor_column, ideal_cursor_column,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String )
.await?
} else if app_state.ui.show_add_table {
add_table_e::execute_edit_action(
"insert_char",
key,
add_table_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", "insert_char",
@@ -182,9 +251,11 @@ pub async fn handle_edit_event(
ideal_cursor_column, ideal_cursor_column,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String )
.await?
} else { } else {
// Assuming FormState otherwise
form_e::execute_edit_action( form_e::execute_edit_action(
"insert_char", "insert_char",
key, key,
@@ -192,39 +263,81 @@ pub async fn handle_edit_event(
ideal_cursor_column, ideal_cursor_column,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String )
.await?
}; };
// Wrap the resulting String message // Update role suggestions after insertion if needed (Register view)
if app_state.ui.show_register && register_state.current_field() == 4 {
register_state.update_role_suggestions();
}
return Ok(EditEventOutcome::Message(msg)); return Ok(EditEventOutcome::Message(msg));
} }
// Handle Backspace/Delete // --- Handle Backspace/Delete ---
if matches!(key.code, KeyCode::Backspace | KeyCode::Delete) { if matches!(key.code, KeyCode::Backspace | KeyCode::Delete) {
// Exit suggestion mode in Register view
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;
} }
let action_str = if key.code == KeyCode::Backspace { "backspace" } else { "delete_char" }; let action_str = if key.code == KeyCode::Backspace {
// Ensure both branches result in a String *before* wrapping "delete_char_backward"
let result_msg: String = if app_state.ui.show_register { } else {
auth_e::execute_edit_action( "delete_char_forward"
};
// Execute delete action based on the current view
let result_msg: String = if app_state.ui.show_login {
auth_e::execute_edit_action(
action_str,
key,
login_state,
ideal_cursor_column,
grpc_client,
current_position,
total_count,
)
.await?
} else if app_state.ui.show_add_table {
add_table_e::execute_edit_action(
action_str,
key,
add_table_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_register {
auth_e::execute_edit_action(
action_str, action_str,
key, key,
register_state, register_state,
ideal_cursor_column, ideal_cursor_column,
grpc_client, grpc_client,
current_position, current_position,
total_count total_count,
).await? // Results in String )
.await?
} else { } else {
// Return String directly, not Ok(String) // Assuming FormState otherwise
"Action not applicable here".to_string() form_e::execute_edit_action(
}; // Semicolon here ends the if/else expression action_str,
key,
form_state,
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await?
};
// Update role suggestions after deletion if needed (Register view)
if app_state.ui.show_register && register_state.current_field() == 4 {
register_state.update_role_suggestions();
}
// Wrap the resulting String message
return Ok(EditEventOutcome::Message(result_msg)); return Ok(EditEventOutcome::Message(result_msg));
} }

View File

@@ -6,8 +6,9 @@ use crate::services::grpc_client::GrpcClient;
use crate::state::pages::{canvas_state::CanvasState, auth::RegisterState}; use crate::state::pages::{canvas_state::CanvasState, auth::RegisterState};
use crate::state::pages::auth::LoginState; use crate::state::pages::auth::LoginState;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use crate::state::pages::add_table::AddTableState;
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crate::functions::modes::read_only::{auth_ro, form_ro}; use crate::functions::modes::read_only::{auth_ro, form_ro, add_table_ro};
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
pub async fn handle_read_only_event( pub async fn handle_read_only_event(
@@ -17,6 +18,7 @@ pub async fn handle_read_only_event(
form_state: &mut FormState, form_state: &mut FormState,
login_state: &mut LoginState, login_state: &mut LoginState,
register_state: &mut RegisterState, register_state: &mut RegisterState,
add_table_state: &mut AddTableState,
key_sequence_tracker: &mut KeySequenceTracker, key_sequence_tracker: &mut KeySequenceTracker,
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
@@ -32,34 +34,17 @@ pub async fn handle_read_only_event(
} }
if config.is_enter_edit_mode_after(key.code, key.modifiers) { if config.is_enter_edit_mode_after(key.code, key.modifiers) {
let (current_input, current_pos) = if app_state.ui.show_login { // Check Login first // Determine target state to adjust cursor
( let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state }
login_state.get_current_input(), else if app_state.ui.show_register { register_state }
login_state.current_cursor_pos(), else if app_state.ui.show_add_table { add_table_state }
) else { form_state };
} else if app_state.ui.show_register { // Then check Register let current_input = target_state.get_current_input();
( let current_pos = target_state.current_cursor_pos();
register_state.get_current_input(),
register_state.current_cursor_pos(),
)
} else {
(
form_state.get_current_input(),
form_state.current_cursor_pos(),
) // Default to Form
};
if !current_input.is_empty() && current_pos < current_input.len() { if !current_input.is_empty() && current_pos < current_input.len() {
if app_state.ui.show_login { target_state.set_current_cursor_pos(current_pos + 1);
login_state.set_current_cursor_pos(current_pos + 1); *ideal_cursor_column = target_state.current_cursor_pos();
*ideal_cursor_column = login_state.current_cursor_pos();
} else if app_state.ui.show_register {
register_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = register_state.current_cursor_pos();
} else { // Default to Form
form_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = form_state.current_cursor_pos();
}
} }
*edit_mode_cooldown = true; *edit_mode_cooldown = true;
*command_message = "Entering Edit mode (after cursor)".to_string(); *command_message = "Entering Edit mode (after cursor)".to_string();
@@ -83,7 +68,7 @@ pub async fn handle_read_only_event(
key_sequence_tracker.add_key(key.code); key_sequence_tracker.add_key(key.code);
let sequence = key_sequence_tracker.get_sequence(); let sequence = key_sequence_tracker.get_sequence();
if let Some(action) = config.matches_key_sequence_generalized(&sequence) { if let Some(action) = config.matches_key_sequence_generalized(&sequence).as_deref() {
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) { let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
crate::tui::functions::form::handle_action( crate::tui::functions::form::handle_action(
action, action,
@@ -96,6 +81,15 @@ pub async fn handle_read_only_event(
.await? .await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions
crate::tui::functions::login::handle_action(action).await? crate::tui::functions::login::handle_action(action).await?
} else if app_state.ui.show_add_table {
add_table_ro::execute_action(
action,
app_state,
add_table_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
).await?
} else if app_state.ui.show_register{ } else if app_state.ui.show_register{
auth_ro::execute_action( auth_ro::execute_action(
action, action,
@@ -134,7 +128,7 @@ pub async fn handle_read_only_event(
} }
if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) { if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) {
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() {
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) { let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
crate::tui::functions::form::handle_action( crate::tui::functions::form::handle_action(
action, action,
@@ -147,6 +141,15 @@ pub async fn handle_read_only_event(
.await? .await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions
crate::tui::functions::login::handle_action(action).await? crate::tui::functions::login::handle_action(action).await?
} else if app_state.ui.show_add_table {
add_table_ro::execute_action(
action,
app_state,
add_table_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
).await?
} else if app_state.ui.show_register /* && CONTEXT_ACTIONS_REGISTER.contains(&action) */ { // Handle register general actions } else if app_state.ui.show_register /* && CONTEXT_ACTIONS_REGISTER.contains(&action) */ { // Handle register general actions
auth_ro::execute_action( auth_ro::execute_action(
action, action,
@@ -184,7 +187,7 @@ pub async fn handle_read_only_event(
} else { } else {
key_sequence_tracker.reset(); key_sequence_tracker.reset();
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() {
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) { let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
crate::tui::functions::form::handle_action( crate::tui::functions::form::handle_action(
action, action,
@@ -195,8 +198,17 @@ pub async fn handle_read_only_event(
ideal_cursor_column, ideal_cursor_column,
) )
.await? .await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
crate::tui::functions::login::handle_action(action).await? crate::tui::functions::login::handle_action(action).await?
} else if app_state.ui.show_add_table {
add_table_ro::execute_action(
action,
app_state,
add_table_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
).await?
} else if app_state.ui.show_register /* && CONTEXT_ACTIONS_REGISTER.contains(&action) */ { // Handle register general actions } else if app_state.ui.show_register /* && CONTEXT_ACTIONS_REGISTER.contains(&action) */ { // Handle register general actions
auth_ro::execute_action( auth_ro::execute_action(
action, action,

View File

@@ -28,6 +28,7 @@ use crate::state::{
auth::{AuthState, LoginState, RegisterState}, auth::{AuthState, LoginState, RegisterState},
admin::AdminState, admin::AdminState,
canvas_state::CanvasState, canvas_state::CanvasState,
add_table::AddTableState,
form::FormState, form::FormState,
intro::IntroState, intro::IntroState,
}, },
@@ -273,7 +274,7 @@ impl EventHandler {
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
// Check for entering edit mode (before cursor) // Check for entering edit mode (before cursor)
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_edit_mode_before") else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_before")
&& ModeManager::can_enter_edit_mode(current_mode) { && ModeManager::can_enter_edit_mode(current_mode) {
self.is_edit_mode = true; self.is_edit_mode = true;
self.edit_mode_cooldown = true; self.edit_mode_cooldown = true;
@@ -282,7 +283,7 @@ impl EventHandler {
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
// Check for entering edit mode (after cursor) // Check for entering edit mode (after cursor)
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_edit_mode_after") else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after")
&& ModeManager::can_enter_edit_mode(current_mode) { && ModeManager::can_enter_edit_mode(current_mode) {
let current_input = if app_state.ui.show_login || app_state.ui.show_register{ let current_input = if app_state.ui.show_login || app_state.ui.show_register{
login_state.get_current_input() login_state.get_current_input()
@@ -306,6 +307,7 @@ impl EventHandler {
} }
self.is_edit_mode = true; self.is_edit_mode = true;
self.edit_mode_cooldown = true; self.edit_mode_cooldown = true;
app_state.ui.focus_outside_canvas = false;
self.command_message = "Edit mode (after cursor)".to_string(); self.command_message = "Edit mode (after cursor)".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok(EventOutcome::Ok(self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
@@ -349,6 +351,7 @@ impl EventHandler {
form_state, form_state,
login_state, login_state,
register_state, register_state,
&mut admin_state.add_table_state,
&mut self.key_sequence_tracker, &mut self.key_sequence_tracker,
current_position, current_position,
total_count, total_count,
@@ -383,7 +386,7 @@ impl EventHandler {
let (_should_exit, message) = read_only::handle_read_only_event( let (_should_exit, message) = read_only::handle_read_only_event(
app_state, key, config, form_state, login_state, app_state, key, config, form_state, login_state,
register_state, &mut self.key_sequence_tracker, register_state, &mut admin_state.add_table_state, &mut self.key_sequence_tracker,
current_position, total_count, grpc_client, current_position, total_count, grpc_client,
&mut self.command_message, &mut self.edit_mode_cooldown, &mut self.command_message, &mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column, &mut self.ideal_cursor_column,
@@ -431,6 +434,7 @@ impl EventHandler {
form_state, form_state,
login_state, login_state,
register_state, register_state,
&mut admin_state.add_table_state,
&mut self.ideal_cursor_column, &mut self.ideal_cursor_column,
current_position, current_position,
total_count, total_count,

View File

@@ -2,6 +2,8 @@
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crate::modes::handlers::event::EventHandler; use crate::modes::handlers::event::EventHandler;
use crate::state::app::highlight::HighlightState; use crate::state::app::highlight::HighlightState;
use crate::state::pages::add_table::AddTableFocus;
use crate::state::pages::admin::AdminState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode { pub enum AppMode {
@@ -25,21 +27,24 @@ impl ModeManager {
return AppMode::Highlight; return AppMode::Highlight;
} }
if app_state.ui.focus_outside_canvas || app_state.ui.show_add_table{ if app_state.ui.focus_outside_canvas {
return AppMode::General; return AppMode::General;
} }
if app_state.ui.show_login || app_state.ui.show_register { let is_canvas_view = app_state.ui.show_login
if event_handler.is_edit_mode { || app_state.ui.show_register
AppMode::Edit || app_state.ui.show_form
|| app_state.ui.show_add_table;
if is_canvas_view {
if app_state.ui.focus_outside_canvas {
AppMode::General
} else { } else {
AppMode::ReadOnly if event_handler.is_edit_mode {
} AppMode::Edit
} else if app_state.ui.show_form { } else {
if event_handler.is_edit_mode { AppMode::ReadOnly
AppMode::Edit }
} else {
AppMode::ReadOnly
} }
} else { } else {
AppMode::General AppMode::General

View File

@@ -6,6 +6,7 @@ use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crate::state::pages::auth::{LoginState, RegisterState}; use crate::state::pages::auth::{LoginState, RegisterState};
use crate::state::pages::add_table::AddTableState;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use crate::modes::handlers::event::EventOutcome; use crate::modes::handlers::event::EventOutcome;
use crate::modes::read_only; // Import the ReadOnly handler use crate::modes::read_only; // Import the ReadOnly handler
@@ -21,6 +22,7 @@ pub async fn handle_highlight_event(
form_state: &mut FormState, form_state: &mut FormState,
login_state: &mut LoginState, login_state: &mut LoginState,
register_state: &mut RegisterState, register_state: &mut RegisterState,
add_table_state: &mut AddTableState,
key_sequence_tracker: &mut KeySequenceTracker, key_sequence_tracker: &mut KeySequenceTracker,
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
@@ -38,6 +40,7 @@ pub async fn handle_highlight_event(
form_state, form_state,
login_state, login_state,
register_state, register_state,
add_table_state,
key_sequence_tracker, key_sequence_tracker,
current_position, current_position,
total_count, total_count,

View File

@@ -27,6 +27,7 @@ pub enum AddTableFocus {
LinksTable, LinksTable,
// Buttons // Buttons
SaveButton, SaveButton,
DeleteSelectedButton,
CancelButton, CancelButton,
} }
@@ -34,6 +35,7 @@ pub enum AddTableFocus {
pub struct AddTableState { pub struct AddTableState {
pub profile_name: String, pub profile_name: String,
pub table_name: String, pub table_name: String,
pub table_name_input: String,
pub column_name_input: String, pub column_name_input: String,
pub column_type_input: String, pub column_type_input: String,
pub columns: Vec<ColumnDefinition>, pub columns: Vec<ColumnDefinition>,
@@ -53,23 +55,18 @@ impl Default for AddTableState {
fn default() -> Self { fn default() -> Self {
// Initialize with some dummy data for demonstration // Initialize with some dummy data for demonstration
AddTableState { AddTableState {
profile_name: "default".to_string(), // Should be set dynamically profile_name: "default".to_string(),
table_name: String::new(), // Start empty table_name: String::new(),
table_name_input: String::new(),
column_name_input: String::new(), column_name_input: String::new(),
column_type_input: String::new(), column_type_input: String::new(),
columns: vec![ columns: Vec::new(),
ColumnDefinition { name: "id".to_string(), data_type: "INTEGER".to_string() }, indexes: Vec::new(),
ColumnDefinition { name: "name".to_string(), data_type: "TEXT".to_string() }, links: Vec::new(),
],
indexes: vec!["id".to_string()],
links: vec![
LinkDefinition { linked_table_name: "related_table".to_string(), is_required: true },
LinkDefinition { linked_table_name: "another_table".to_string(), is_required: false },
],
current_focus: AddTableFocus::InputTableName, current_focus: AddTableFocus::InputTableName,
column_table_state: TableState::default().with_selected(0), column_table_state: TableState::default(),
index_table_state: TableState::default().with_selected(0), index_table_state: TableState::default(),
link_table_state: TableState::default().with_selected(0), link_table_state: TableState::default(),
table_name_cursor_pos: 0, table_name_cursor_pos: 0,
column_name_cursor_pos: 0, column_name_cursor_pos: 0,
column_type_cursor_pos: 0, column_type_cursor_pos: 0,
@@ -79,7 +76,7 @@ impl Default for AddTableState {
} }
impl AddTableState { impl AddTableState {
const INPUT_FIELD_COUNT: usize = 3; pub const INPUT_FIELD_COUNT: usize = 3;
} }
// Implement CanvasState for the input fields // Implement CanvasState for the input fields
@@ -108,12 +105,12 @@ impl CanvasState for AddTableState {
} }
fn inputs(&self) -> Vec<&String> { fn inputs(&self) -> Vec<&String> {
vec![&self.table_name, &self.column_name_input, &self.column_type_input] vec![&self.table_name_input, &self.column_name_input, &self.column_type_input]
} }
fn get_current_input(&self) -> &str { fn get_current_input(&self) -> &str {
match self.current_focus { match self.current_focus {
AddTableFocus::InputTableName => &self.table_name, AddTableFocus::InputTableName => &self.table_name_input,
AddTableFocus::InputColumnName => &self.column_name_input, AddTableFocus::InputColumnName => &self.column_name_input,
AddTableFocus::InputColumnType => &self.column_type_input, AddTableFocus::InputColumnType => &self.column_type_input,
_ => "", // Should not happen if called correctly _ => "", // Should not happen if called correctly
@@ -122,14 +119,10 @@ impl CanvasState for AddTableState {
fn get_current_input_mut(&mut self) -> &mut String { fn get_current_input_mut(&mut self) -> &mut String {
match self.current_focus { match self.current_focus {
AddTableFocus::InputTableName => &mut self.table_name, AddTableFocus::InputTableName => &mut self.table_name_input,
AddTableFocus::InputColumnName => &mut self.column_name_input, AddTableFocus::InputColumnName => &mut self.column_name_input,
AddTableFocus::InputColumnType => &mut self.column_type_input, AddTableFocus::InputColumnType => &mut self.column_type_input,
// This case needs careful handling. If focus isn't on an input, _ => &mut self.table_name_input,
// which mutable string should we return? Returning the first one
// might be unexpected. Consider panicking or returning Option if this state is invalid.
// For now, returning the first field to avoid panics during rendering.
_ => &mut self.table_name,
} }
} }

View File

@@ -3,3 +3,4 @@
pub mod form; pub mod form;
pub mod login; pub mod login;
pub mod register; pub mod register;
pub mod add_table;

View File

@@ -0,0 +1,75 @@
// src/tui/functions/common/add_table.rs
use crate::state::pages::add_table::{
AddTableFocus, AddTableState, ColumnDefinition,
};
/// Handles the logic for adding a column when the "Add" button is activated.
///
/// Takes the mutable state and command message string.
/// Returns `Some(AddTableFocus)` indicating the desired focus state after a successful add,
/// or `None` if the action failed (e.g., validation error).
pub fn handle_add_column_action(
add_table_state: &mut AddTableState,
command_message: &mut String,
) -> Option<AddTableFocus> {
// Trim and create owned Strings from inputs
let table_name_in = add_table_state.table_name_input.trim();
let column_name_in = add_table_state.column_name_input.trim();
let column_type_in = add_table_state.column_type_input.trim();
// Validate all inputs needed for this combined action
let has_table_name = !table_name_in.is_empty();
let has_column_name = !column_name_in.is_empty();
let has_column_type = !column_type_in.is_empty();
match (has_table_name, has_column_name, has_column_type) {
// Case 1: Both column fields have input (Table name is optional here)
(_, true, true) => {
let mut msg = String::new();
// Optionally update table name if provided
if has_table_name {
add_table_state.table_name = table_name_in.to_string();
msg.push_str(&format!("Table name set to '{}'. ", add_table_state.table_name));
}
// Add the column
let new_column = ColumnDefinition {
name: column_name_in.to_string(),
data_type: column_type_in.to_string(),
};
add_table_state.columns.push(new_column.clone()); // Clone for msg
msg.push_str(&format!("Column '{}' added.", new_column.name));
*command_message = msg;
// Clear all inputs and reset cursors
add_table_state.table_name_input.clear();
add_table_state.column_name_input.clear();
add_table_state.column_type_input.clear();
add_table_state.table_name_cursor_pos = 0;
add_table_state.column_name_cursor_pos = 0;
add_table_state.column_type_cursor_pos = 0;
add_table_state.has_unsaved_changes = true;
Some(AddTableFocus::InputColumnName) // Focus for next column
}
// Case 2: Only one column field has input (Error)
(_, true, false) | (_, false, true) => {
*command_message = "Both Column Name and Type are required to add a column.".to_string();
None // Indicate validation failure
}
// Case 3: Only Table name has input (No column input)
(true, false, false) => {
add_table_state.table_name = table_name_in.to_string();
*command_message = format!("Table name set to '{}'.", add_table_state.table_name);
// Clear only table name input
add_table_state.table_name_input.clear();
add_table_state.table_name_cursor_pos = 0;
add_table_state.has_unsaved_changes = true;
Some(AddTableFocus::InputTableName) // Keep focus here
}
// Case 4: All fields are empty
(false, false, false) => {
*command_message = "No input provided.".to_string();
None
}
}
}