Compare commits

..

18 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
filipriec
57f789290d needs improvements but at least it looks like it exists 2025-04-17 11:53:31 +02:00
filipriec
f34317e504 from template to the working page 2025-04-17 11:11:33 +02:00
filipriec
8159a84447 movement 2025-04-16 23:31:28 +02:00
17 changed files with 1750 additions and 289 deletions

View File

@@ -1,209 +1,440 @@
// src/components/admin/add_table.rs
use crate::config::colors::themes::Theme;
use crate::state::{
app::state::AppState,
pages::add_table::{AddTableFocus, AddTableState},
};
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::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style, Stylize},
style::{Modifier, Style},
text::{Line, Span},
widgets::{
Block, BorderType, Borders, Cell, Paragraph, Row, Table, TableState,
},
widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table},
Frame,
};
use crate::components::handlers::canvas::render_canvas;
/// Renders the detailed page for adding a new table.
/// 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(
f: &mut Frame,
area: Rect,
theme: &Theme,
app_state: &AppState,
_app_state: &AppState, // Currently unused, might be needed later
add_table_state: &mut AddTableState,
is_edit_mode: bool, // Determines if canvas inputs are in edit mode
highlight_state: &HighlightState, // For text highlighting in canvas
) {
let focused_style = Style::default().fg(theme.highlight);
let unfocused_style = Style::default().fg(theme.border);
let base_style = Style::default().fg(theme.fg);
let header_style = Style::default().fg(theme.secondary);
let selected_row_style = Style::default().bg(theme.highlight).fg(theme.bg);
// --- Configuration ---
// Threshold width to switch between wide and narrow layouts
const NARROW_LAYOUT_THRESHOLD: u16 = 120; // Adjust this value as needed
// --- Main Block ---
// --- 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()
.title(" Create New Table ")
.title(" Add New Table ")
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent))
.border_style(Style::default().fg(theme.border))
.style(Style::default().bg(theme.bg));
let inner_area = main_block.inner(area);
f.render_widget(main_block, area);
// --- Layout ---
let constraints = [
Constraint::Length(1), // Profile line
Constraint::Length(1), // Table Name line
Constraint::Length(1), // Spacer
Constraint::Length(1), // Columns Header
Constraint::Min(3), // Columns Table (at least 3 rows visible)
Constraint::Length(1), // Spacer
Constraint::Length(1), // Indexes Header
Constraint::Min(2), // Indexes Table
Constraint::Length(1), // Spacer
Constraint::Length(1), // Links Header
Constraint::Min(3), // Links Table
Constraint::Length(1), // Spacer
Constraint::Length(1), // Action Buttons
];
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.margin(1) // Add margin inside the main block
.split(inner_area);
// --- Area Variable Declarations ---
let top_info_area: Rect;
let columns_area: Rect;
let canvas_area: Rect;
let add_button_area: Rect;
let indexes_area: Rect;
let links_area: Rect;
let bottom_buttons_area: Rect;
// --- Helper: Get Border Style based on Focus ---
let get_border_style = |focus: AddTableFocus| {
if add_table_state.current_focus == focus {
focused_style
} else {
unfocused_style
}
};
// --- Layout Decision ---
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);
// --- 1. Profile Line ---
// TODO: Fetch actual selected profile if needed, using app_state.selected_profile or admin_state
let profile_text = format!("Profile: {}", add_table_state.profile_name);
f.render_widget(Paragraph::new(profile_text).style(base_style), chunks[0]);
top_info_area = main_chunks[0];
let middle_area = main_chunks[1];
bottom_buttons_area = main_chunks[2];
// --- 2. Table Name Line ---
let table_name_block = Block::default()
.borders(Borders::BOTTOM)
.border_style(get_border_style(AddTableFocus::TableName));
// Basic rendering for now, cursor needs event handling logic
let table_name_text = format!("Table Name: [ {} ]", add_table_state.table_name);
let table_name_paragraph = Paragraph::new(table_name_text)
.style(base_style)
.block(table_name_block);
f.render_widget(table_name_paragraph, chunks[1]);
// Split Middle Horizontally
let middle_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(60), // Left: Columns Table
Constraint::Percentage(40), // Right: Inputs etc.
])
.split(middle_area);
// --- 4. Columns Header ---
let columns_header = Paragraph::new(Line::from(vec![
Span::styled(" Name ", header_style),
Span::raw("| "),
Span::styled("Type ", header_style),
Span::raw("| "),
Span::styled(" <- Add Column (Ctrl+A)", header_style.add_modifier(Modifier::ITALIC)),
]));
f.render_widget(columns_header, chunks[3]);
columns_area = middle_chunks[0];
let right_pane_area = middle_chunks[1];
// --- 5. Columns Table ---
let columns_block = Block::default()
.borders(Borders::TOP | Borders::BOTTOM) // Only top/bottom for visual separation
.border_style(get_border_style(AddTableFocus::Columns));
let columns_table_area = columns_block.inner(chunks[4]);
f.render_widget(columns_block, chunks[4]);
// 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);
let column_rows = add_table_state.columns.iter().map(|col| {
Row::new(vec![
Cell::from(col.name.as_str()),
Cell::from(col.data_type.as_str()),
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,
)),
])
.style(base_style)
});
let column_widths = [Constraint::Percentage(50), Constraint::Percentage(50)];
let columns_table = Table::new(column_rows, column_widths)
.highlight_style(selected_row_style)
.highlight_symbol("* "); // Indicate selection
.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::default()
.title(Span::styled(" Columns ", theme.fg))
.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_table_area,
columns_area,
&mut add_table_state.column_table_state,
);
// --- 7. Indexes Header ---
let indexes_header = Paragraph::new(Line::from(vec![
Span::styled(" Column Name ", header_style),
Span::raw("| "),
Span::styled(" <- Add Index (Ctrl+I), Remove (Ctrl+X)", header_style.add_modifier(Modifier::ITALIC)),
]));
f.render_widget(indexes_header, chunks[6]);
// --- Canvas Rendering (Column Definition Input) ---
let _active_field_rect = render_canvas(
f,
canvas_area,
add_table_state,
&add_table_state.fields(),
&add_table_state.current_field(),
&add_table_state.inputs(),
theme,
is_edit_mode && focus_on_canvas_inputs,
highlight_state,
);
// --- 8. Indexes Table ---
let indexes_block = Block::default()
.borders(Borders::TOP | Borders::BOTTOM)
.border_style(get_border_style(AddTableFocus::Indexes));
let indexes_table_area = indexes_block.inner(chunks[7]);
f.render_widget(indexes_block, chunks[7]);
// --- 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)
}
};
let index_rows = add_table_state.indexes.iter().map(|idx_col_name| {
Row::new(vec![Cell::from(idx_col_name.as_str())]).style(base_style)
});
let index_widths = [Constraint::Percentage(100)];
let indexes_table = Table::new(index_rows, index_widths)
.highlight_style(selected_row_style)
.highlight_symbol("* ");
// --- Add Button Rendering ---
let add_button = Paragraph::new(" Add ")
.style(get_button_style(
AddTableFocus::AddColumnButton,
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::AddColumnButton,
add_table_state.current_focus,
)),
);
f.render_widget(add_button, add_button_area); // Render into the calculated area
// --- 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_table_area,
indexes_area,
&mut add_table_state.index_table_state,
);
// --- 10. Links Header ---
let links_header = Paragraph::new(Line::from(vec![
Span::styled(" Linked Table ", header_style),
Span::raw("| "),
Span::styled("Required? ", header_style),
Span::raw("| "),
Span::styled(" <- Toggle Required (Space)", header_style.add_modifier(Modifier::ITALIC)),
]));
f.render_widget(links_header, chunks[9]);
// --- 11. Links Table ---
let links_block = Block::default()
.borders(Borders::TOP | Borders::BOTTOM)
.border_style(get_border_style(AddTableFocus::Links));
let links_table_area = links_block.inner(chunks[10]);
f.render_widget(links_block, chunks[10]);
let link_rows = add_table_state.links.iter().map(|link| {
let required_text = if link.is_required { "[X] Yes" } else { "[ ] No " }; // Pad No for alignment
Row::new(vec![
Cell::from(link.linked_table_name.as_str()),
Cell::from(required_text),
])
.style(base_style)
});
let link_widths = [Constraint::Percentage(70), Constraint::Percentage(30)];
let links_table = Table::new(link_rows, link_widths)
.highlight_style(selected_row_style)
.highlight_symbol("* ");
// --- 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_table_area,
links_area,
&mut add_table_state.link_table_state,
);
// --- 13. Action Buttons ---
let button_layout = Layout::default()
// --- Save/Cancel Buttons Rendering ---
let bottom_button_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[12]);
.constraints([
Constraint::Percentage(33), // Save Button
Constraint::Percentage(34), // Delete Button
Constraint::Percentage(33), // Cancel Button
])
.split(bottom_buttons_area);
let save_button = Paragraph::new("[ Save Table ]")
let save_button = Paragraph::new(" Save table ")
.style(get_button_style(
AddTableFocus::SaveButton,
add_table_state.current_focus,
))
.alignment(Alignment::Center)
.style(if add_table_state.current_focus == AddTableFocus::SaveButton {
selected_row_style // Use selected style for focused button
} else {
base_style
});
let cancel_button = Paragraph::new("[ Cancel ]")
.alignment(Alignment::Center)
.style(if add_table_state.current_focus == AddTableFocus::CancelButton {
selected_row_style
} else {
base_style
});
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.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, button_layout[0]);
f.render_widget(cancel_button, button_layout[1]);
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 ")
.style(get_button_style(
AddTableFocus::CancelButton,
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::CancelButton,
add_table_state.current_focus,
)),
);
f.render_widget(cancel_button, bottom_button_chunks[2]);
}

View File

@@ -2,3 +2,4 @@
pub mod form_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

@@ -1,3 +1,4 @@
// src/functions/modes/navigation.rs
pub mod admin_nav;
pub mod add_table_nav;

View File

@@ -0,0 +1,283 @@
// src/functions/modes/navigation/add_table_nav.rs
use crate::config::binds::config::Config;
use crate::state::{
app::state::AppState,
pages::add_table::{AddTableFocus, AddTableState},
};
use crossterm::event::{KeyEvent};
use ratatui::widgets::TableState;
use crate::tui::functions::common::add_table::handle_add_column_action;
/// Handles navigation events specifically for the Add Table view.
/// Returns true if the event was handled, false otherwise.
pub fn handle_add_table_navigation(
key: KeyEvent,
config: &Config,
app_state: &mut AppState,
add_table_state: &mut AddTableState,
command_message: &mut String,
) -> bool {
let action = config.get_general_action(key.code, key.modifiers);
let current_focus = add_table_state.current_focus;
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() {
// --- Vertical Navigation (Up/Down) ---
Some("move_up") => {
match current_focus {
AddTableFocus::InputTableName => new_focus = AddTableFocus::CancelButton, // Wrap top (right pane)
AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputTableName,
AddTableFocus::InputColumnType => new_focus = AddTableFocus::InputColumnName,
AddTableFocus::AddColumnButton => new_focus = AddTableFocus::InputColumnType,
AddTableFocus::ColumnsTable => { // Left pane navigation
if !navigate_table_up(&mut add_table_state.column_table_state, add_table_state.columns.len()) {
// 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
}
}
AddTableFocus::IndexesTable => {
if !navigate_table_up(&mut add_table_state.index_table_state, add_table_state.indexes.len()) {
new_focus = AddTableFocus::ColumnsTable;
}
}
AddTableFocus::LinksTable => {
if !navigate_table_up(&mut add_table_state.link_table_state, add_table_state.links.len()) {
new_focus = AddTableFocus::IndexesTable;
}
}
AddTableFocus::SaveButton => new_focus = AddTableFocus::LinksTable, // Move up to left pane bottom
AddTableFocus::DeleteSelectedButton => new_focus = AddTableFocus::SaveButton,
AddTableFocus::CancelButton => new_focus = AddTableFocus::DeleteSelectedButton,
}
}
Some("move_down") => {
match current_focus {
AddTableFocus::InputTableName => new_focus = AddTableFocus::InputColumnName,
AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputColumnType,
AddTableFocus::InputColumnType => new_focus = AddTableFocus::AddColumnButton,
AddTableFocus::AddColumnButton => new_focus = AddTableFocus::ColumnsTable, // Move down to left pane top
AddTableFocus::ColumnsTable => { // Left pane navigation
if !navigate_table_down(&mut add_table_state.column_table_state, add_table_state.columns.len()) {
new_focus = AddTableFocus::IndexesTable; // Move to next left pane item
}
}
AddTableFocus::IndexesTable => {
if !navigate_table_down(&mut add_table_state.index_table_state, add_table_state.indexes.len()) {
new_focus = AddTableFocus::LinksTable;
}
}
AddTableFocus::LinksTable => {
if !navigate_table_down(&mut add_table_state.link_table_state, add_table_state.links.len()) {
new_focus = AddTableFocus::SaveButton; // Move down to right pane bottom
}
}
AddTableFocus::SaveButton => new_focus = AddTableFocus::DeleteSelectedButton,
AddTableFocus::DeleteSelectedButton => new_focus = AddTableFocus::CancelButton,
AddTableFocus::CancelButton => new_focus = AddTableFocus::InputTableName, // Wrap bottom (right pane)
}
}
// --- Horizontal Navigation (Left/Right) ---
Some("next_option") => { // 'l' or Right: Move from Left Pane to Right Pane
if is_left_pane_focus {
new_focus = match current_focus {
// Map left pane items to corresponding right pane items (approximate vertical alignment)
AddTableFocus::ColumnsTable => AddTableFocus::InputTableName,
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: Move from Right Pane to Left Pane
if is_right_pane_general_focus {
new_focus = match current_focus {
// 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
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 (Keep as vertical cycle) ---
Some("next_field") => { // Tab
new_focus = match current_focus {
AddTableFocus::InputTableName => AddTableFocus::InputColumnName,
AddTableFocus::InputColumnName => AddTableFocus::InputColumnType,
AddTableFocus::InputColumnType => AddTableFocus::AddColumnButton,
AddTableFocus::AddColumnButton => AddTableFocus::ColumnsTable,
AddTableFocus::ColumnsTable => AddTableFocus::IndexesTable,
AddTableFocus::IndexesTable => AddTableFocus::LinksTable,
AddTableFocus::LinksTable => AddTableFocus::SaveButton,
AddTableFocus::SaveButton => AddTableFocus::DeleteSelectedButton,
AddTableFocus::DeleteSelectedButton => AddTableFocus::CancelButton,
AddTableFocus::CancelButton => AddTableFocus::InputTableName, // Wrap
};
}
Some("prev_field") => { // Shift+Tab
new_focus = match current_focus {
AddTableFocus::InputTableName => AddTableFocus::CancelButton, // Wrap
AddTableFocus::InputColumnName => AddTableFocus::InputTableName,
AddTableFocus::InputColumnType => AddTableFocus::InputColumnName,
AddTableFocus::AddColumnButton => AddTableFocus::InputColumnType,
AddTableFocus::ColumnsTable => AddTableFocus::AddColumnButton,
AddTableFocus::IndexesTable => AddTableFocus::ColumnsTable,
AddTableFocus::LinksTable => AddTableFocus::IndexesTable,
AddTableFocus::SaveButton => AddTableFocus::LinksTable,
AddTableFocus::DeleteSelectedButton => AddTableFocus::SaveButton,
AddTableFocus::CancelButton => AddTableFocus::DeleteSelectedButton,
};
}
// --- Selection ---
Some("select") => {
match current_focus {
AddTableFocus::AddColumnButton => {
if let Some(focus_after_add) = handle_add_column_action(add_table_state, command_message) {
new_focus = focus_after_add;
}
}
AddTableFocus::SaveButton => {
*command_message = "Action: Save Table (Not Implemented)".to_string();
// TODO: Implement logic
}
AddTableFocus::DeleteSelectedButton => {
*command_message = "Action: Delete selected".to_string();
// TODO: Implement logic
}
AddTableFocus::CancelButton => {
*command_message = "Action: Cancel Add Table".to_string();
// TODO: Implement logic
}
AddTableFocus::ColumnsTable => {
if let Some(index) = add_table_state.column_table_state.selected() {
*command_message = format!("Selected column index {}", index);
} else { *command_message = "No column selected".to_string(); }
}
AddTableFocus::IndexesTable => {
if let Some(index) = add_table_state.index_table_state.selected() {
*command_message = format!("Selected index index {}", index);
} else { *command_message = "No index selected".to_string(); }
}
AddTableFocus::LinksTable => {
if let Some(index) = add_table_state.link_table_state.selected() {
*command_message = format!("Selected link index {}", index);
} else { *command_message = "No link selected".to_string(); }
}
_ => { // Input fields
*command_message = format!("Select on {:?}", current_focus);
handled = false; // Let main loop handle edit mode toggle maybe
}
}
// Keep handled = true for select actions unless specifically set to false
}
// --- Other General Keys ---
Some("toggle_sidebar") | Some("toggle_buffer_list") => {
handled = false;
}
// --- No matching action ---
_ => handled = false,
}
// Update focus state if it changed and was handled
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 {
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));
}
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));
}
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));
}_ => {}
}
} else if !handled {
// ...
}
handled
}
// Helper function for navigating up within a table state
// 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 {
if item_count == 0 { return false; }
let current_selection = table_state.selected();
match current_selection {
Some(index) => {
if index > 0 {
table_state.select(Some(index - 1));
true
} else {
false // Was at the top
}
}
None => {
table_state.select(Some(item_count - 1)); // Select last item
true
}
}
}
// Helper function for navigating down within a table state
// 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 {
if item_count == 0 { return false; }
let current_selection = table_state.selected();
match current_selection {
Some(index) => {
if index < item_count - 1 {
table_state.select(Some(index + 1));
true
} else {
false // Was at the bottom
}
}
None => {
table_state.select(Some(0)); // Select first item
true
}
}
}

View File

@@ -8,13 +8,14 @@ use crate::state::{
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::state::app::buffer::AppView;
use crate::state::app::buffer::BufferState;
use crate::state::pages::add_table::AddTableState;
/// Handles navigation events specifically for the Admin Panel view.
/// Returns true if the event was handled, false otherwise.
pub fn handle_admin_navigation(
key: KeyEvent,
config: &Config,
app_state: &AppState,
app_state: &mut AppState,
admin_state: &mut AdminState,
buffer_state: &mut BufferState,
command_message: &mut String,
@@ -160,8 +161,37 @@ pub fn handle_admin_navigation(
// TODO: Trigger action for Button 1
}
AdminFocus::Button2 => {
buffer_state.update_history(AppView::AddTable);
*command_message = "Navigating to Add Table page...".to_string();
// --- Prepare AddTableState based on persistent selections ---
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 => {
*command_message = "Action: Change Table (Not Implemented)".to_string();

View File

@@ -2,3 +2,4 @@
pub mod auth_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
use crate::config::binds::config::Config;
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::add_table::AddTableState; // Added
use crate::modes::handlers::event::EventOutcome;
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 crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
// Removed duplicate/unused imports
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditEventOutcome {
@@ -21,105 +25,148 @@ pub async fn handle_edit_event(
form_state: &mut FormState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
add_table_state: &mut AddTableState, // Added
ideal_cursor_column: &mut usize,
// command_message: &mut String, // Removed as messages are returned
current_position: &mut u64,
total_count: u64,
grpc_client: &mut GrpcClient,
app_state: &AppState,
) -> Result<EditEventOutcome, Box<dyn std::error::Error>> {
// Global command mode check
// Global command mode check (should ideally be handled before calling this function)
if let Some("enter_command_mode") = config.get_action_for_key_in_mode(
&config.keybindings.global,
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(
&config.keybindings.common,
key.code,
key.modifiers
) {
key.modifiers,
).as_deref() {
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 {
auth_e::execute_common_action(
action,
login_state,
grpc_client,
current_position,
total_count
).await? // Results in String on success
total_count,
)
.await?
} else if app_state.ui.show_register {
// Keeping this block as requested
auth_e::execute_common_action(
action,
register_state,
grpc_client,
current_position,
total_count
).await? // Results in String on success
} else {
total_count,
)
.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(
action,
form_state,
grpc_client,
current_position,
total_count
).await?; // This returns EventOutcome on success
// Extract the message string from the EventOutcome
total_count,
)
.await?;
match outcome {
EventOutcome::Ok(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));
}
}
// 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" {
// Handle exiting suggestion mode in Register view first
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(
"exit_suggestion_mode",
"exit_suggestion_mode", // Specific action for suggestion exit
key,
register_state,
ideal_cursor_column,
grpc_client,
current_position,
total_count,
).await?; // Results in String on success
// Wrap the String message
)
.await?;
return Ok(EditEventOutcome::Message(msg));
} else {
// Signal exit
// Signal exit from Edit mode
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 !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();
if !register_state.role_suggestions.is_empty() {
register_state.in_suggestion_mode = true;
register_state.selected_suggestion_index = Some(0);
return Ok(EditEventOutcome::Message("Suggestions shown".to_string()));
} else { // Added else here for clarity
return Ok(EditEventOutcome::Message("No suggestions available".to_string()));
register_state.selected_suggestion_index = Some(0); // Select first suggestion
return Ok(EditEventOutcome::Message(
"Suggestions shown".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 {
auth_e::execute_edit_action(
action,
@@ -128,8 +175,18 @@ pub async fn handle_edit_event(
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await? // Results in String
total_count,
)
.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 {
auth_e::execute_edit_action(
action,
@@ -138,9 +195,11 @@ pub async fn handle_edit_event(
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await? // Results in String
total_count,
)
.await?
} else {
// Assuming FormState otherwise
form_e::execute_edit_action(
action,
key,
@@ -148,32 +207,42 @@ pub async fn handle_edit_event(
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await? // Results in String
total_count,
)
.await?
};
// Wrap the resulting String message
return Ok(EditEventOutcome::Message(msg));
}
// Character insertion
if let KeyCode::Char(_) = key.code {
// --- Character insertion ---
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 {
register_state.in_suggestion_mode = false;
register_state.show_role_suggestions = false;
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 {
auth_e::execute_edit_action(
"insert_char",
key,
key, // Pass the key event containing the char
login_state,
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await? // Results in String
total_count,
)
.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 {
auth_e::execute_edit_action(
"insert_char",
@@ -182,9 +251,11 @@ pub async fn handle_edit_event(
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await? // Results in String
total_count,
)
.await?
} else {
// Assuming FormState otherwise
form_e::execute_edit_action(
"insert_char",
key,
@@ -192,39 +263,81 @@ pub async fn handle_edit_event(
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await? // Results in String
total_count,
)
.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));
}
// Handle Backspace/Delete
// --- Handle Backspace/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 {
register_state.in_suggestion_mode = false;
register_state.show_role_suggestions = false;
register_state.selected_suggestion_index = None;
}
let action_str = if key.code == KeyCode::Backspace { "backspace" } else { "delete_char" };
// Ensure both branches result in a String *before* wrapping
let result_msg: String = if app_state.ui.show_register {
auth_e::execute_edit_action(
let action_str = if key.code == KeyCode::Backspace {
"delete_char_backward"
} else {
"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,
key,
register_state,
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await? // Results in String
total_count,
)
.await?
} else {
// Return String directly, not Ok(String)
"Action not applicable here".to_string()
}; // Semicolon here ends the if/else expression
// Assuming FormState otherwise
form_e::execute_edit_action(
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));
}

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::auth::LoginState;
use crate::state::pages::form::FormState;
use crate::state::pages::add_table::AddTableState;
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;
pub async fn handle_read_only_event(
@@ -17,6 +18,7 @@ pub async fn handle_read_only_event(
form_state: &mut FormState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
add_table_state: &mut AddTableState,
key_sequence_tracker: &mut KeySequenceTracker,
current_position: &mut 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) {
let (current_input, current_pos) = if app_state.ui.show_login { // Check Login first
(
login_state.get_current_input(),
login_state.current_cursor_pos(),
)
} else if app_state.ui.show_register { // Then check Register
(
register_state.get_current_input(),
register_state.current_cursor_pos(),
)
} else {
(
form_state.get_current_input(),
form_state.current_cursor_pos(),
) // Default to Form
};
// Determine target state to adjust cursor
let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state }
else if app_state.ui.show_register { register_state }
else if app_state.ui.show_add_table { add_table_state }
else { form_state };
let current_input = target_state.get_current_input();
let current_pos = target_state.current_cursor_pos();
if !current_input.is_empty() && current_pos < current_input.len() {
if app_state.ui.show_login {
login_state.set_current_cursor_pos(current_pos + 1);
*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();
}
target_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = target_state.current_cursor_pos();
}
*edit_mode_cooldown = true;
*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);
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) {
crate::tui::functions::form::handle_action(
action,
@@ -96,6 +81,15 @@ pub async fn handle_read_only_event(
.await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions
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{
auth_ro::execute_action(
action,
@@ -134,7 +128,7 @@ pub async fn handle_read_only_event(
}
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) {
crate::tui::functions::form::handle_action(
action,
@@ -147,6 +141,15 @@ pub async fn handle_read_only_event(
.await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions
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
auth_ro::execute_action(
action,
@@ -184,7 +187,7 @@ pub async fn handle_read_only_event(
} else {
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) {
crate::tui::functions::form::handle_action(
action,
@@ -195,8 +198,17 @@ pub async fn handle_read_only_event(
ideal_cursor_column,
)
.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?
} 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
auth_ro::execute_action(
action,

View File

@@ -28,6 +28,7 @@ use crate::state::{
auth::{AuthState, LoginState, RegisterState},
admin::AdminState,
canvas_state::CanvasState,
add_table::AddTableState,
form::FormState,
intro::IntroState,
},
@@ -39,7 +40,7 @@ use crate::modes::{
highlight::highlight,
general::{navigation, dialog},
};
use crate::functions::modes::navigation::admin_nav;
use crate::functions::modes::navigation::{admin_nav, add_table_nav};
use crate::config::binds::key_sequences::KeySequenceTracker;
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -175,6 +176,21 @@ impl EventHandler {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
}
// --- Add Table Page Navigation ---
if app_state.ui.show_add_table {
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
if add_table_nav::handle_add_table_navigation(
key,
config,
app_state,
&mut admin_state.add_table_state,
&mut self.command_message,
) {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
}
}
let nav_outcome = navigation::handle_navigation_event(
key,
@@ -258,7 +274,7 @@ impl EventHandler {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// 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) {
self.is_edit_mode = true;
self.edit_mode_cooldown = true;
@@ -267,7 +283,7 @@ impl EventHandler {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// 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) {
let current_input = if app_state.ui.show_login || app_state.ui.show_register{
login_state.get_current_input()
@@ -291,6 +307,7 @@ impl EventHandler {
}
self.is_edit_mode = true;
self.edit_mode_cooldown = true;
app_state.ui.focus_outside_canvas = false;
self.command_message = "Edit mode (after cursor)".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok(EventOutcome::Ok(self.command_message.clone()));
@@ -334,6 +351,7 @@ impl EventHandler {
form_state,
login_state,
register_state,
&mut admin_state.add_table_state,
&mut self.key_sequence_tracker,
current_position,
total_count,
@@ -368,7 +386,7 @@ impl EventHandler {
let (_should_exit, message) = read_only::handle_read_only_event(
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,
&mut self.command_message, &mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column,
@@ -416,6 +434,7 @@ impl EventHandler {
form_state,
login_state,
register_state,
&mut admin_state.add_table_state,
&mut self.ideal_cursor_column,
current_position,
total_count,

View File

@@ -2,6 +2,8 @@
use crate::state::app::state::AppState;
use crate::modes::handlers::event::EventHandler;
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)]
pub enum AppMode {
@@ -29,17 +31,20 @@ impl ModeManager {
return AppMode::General;
}
if app_state.ui.show_login || app_state.ui.show_register {
if event_handler.is_edit_mode {
AppMode::Edit
let is_canvas_view = app_state.ui.show_login
|| app_state.ui.show_register
|| 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 {
AppMode::ReadOnly
}
} else if app_state.ui.show_form {
if event_handler.is_edit_mode {
AppMode::Edit
} else {
AppMode::ReadOnly
if event_handler.is_edit_mode {
AppMode::Edit
} else {
AppMode::ReadOnly
}
}
} else {
AppMode::General

View File

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

View File

@@ -1,4 +1,5 @@
// src/state/pages/add_table.rs
use crate::state::pages::canvas_state::CanvasState;
use ratatui::widgets::TableState;
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -16,11 +17,17 @@ pub struct LinkDefinition {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AddTableFocus {
#[default]
TableName,
Columns,
Indexes,
Links,
InputTableName, // Field 0 for CanvasState
InputColumnName, // Field 1 for CanvasState
InputColumnType, // Field 2 for CanvasState
AddColumnButton,
// Result Tables
ColumnsTable,
IndexesTable,
LinksTable,
// Buttons
SaveButton,
DeleteSelectedButton,
CancelButton,
}
@@ -28,6 +35,9 @@ pub enum AddTableFocus {
pub struct AddTableState {
pub profile_name: String,
pub table_name: String,
pub table_name_input: String,
pub column_name_input: String,
pub column_type_input: String,
pub columns: Vec<ColumnDefinition>,
pub indexes: Vec<String>,
pub links: Vec<LinkDefinition>,
@@ -36,33 +46,120 @@ pub struct AddTableState {
pub index_table_state: TableState,
pub link_table_state: TableState,
pub table_name_cursor_pos: usize,
pub column_name_cursor_pos: usize,
pub column_type_cursor_pos: usize,
pub has_unsaved_changes: bool,
}
impl Default for AddTableState {
fn default() -> Self {
// Initialize with some dummy data for demonstration
AddTableState {
profile_name: "default".to_string(), // Should be set dynamically
table_name: "new_table".to_string(),
columns: vec![
ColumnDefinition { name: "id".to_string(), data_type: "INTEGER".to_string() },
ColumnDefinition { name: "name".to_string(), data_type: "TEXT".to_string() },
],
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::TableName,
column_table_state: TableState::default().with_selected(0),
index_table_state: TableState::default().with_selected(0),
link_table_state: TableState::default().with_selected(0),
profile_name: "default".to_string(),
table_name: String::new(),
table_name_input: String::new(),
column_name_input: String::new(),
column_type_input: String::new(),
columns: Vec::new(),
indexes: Vec::new(),
links: Vec::new(),
current_focus: AddTableFocus::InputTableName,
column_table_state: TableState::default(),
index_table_state: TableState::default(),
link_table_state: TableState::default(),
table_name_cursor_pos: 0,
column_name_cursor_pos: 0,
column_type_cursor_pos: 0,
has_unsaved_changes: false,
}
}
}
impl AddTableState {
pub const INPUT_FIELD_COUNT: usize = 3;
}
// Implement CanvasState for the input fields
impl CanvasState for AddTableState {
fn current_field(&self) -> usize {
match self.current_focus {
AddTableFocus::InputTableName => 0,
AddTableFocus::InputColumnName => 1,
AddTableFocus::InputColumnType => 2,
// If focus is elsewhere, default to the first field for canvas rendering logic
_ => 0,
}
}
fn current_cursor_pos(&self) -> usize {
match self.current_focus {
AddTableFocus::InputTableName => self.table_name_cursor_pos,
AddTableFocus::InputColumnName => self.column_name_cursor_pos,
AddTableFocus::InputColumnType => self.column_type_cursor_pos,
_ => 0, // Default if focus is not on an input field
}
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
fn inputs(&self) -> Vec<&String> {
vec![&self.table_name_input, &self.column_name_input, &self.column_type_input]
}
fn get_current_input(&self) -> &str {
match self.current_focus {
AddTableFocus::InputTableName => &self.table_name_input,
AddTableFocus::InputColumnName => &self.column_name_input,
AddTableFocus::InputColumnType => &self.column_type_input,
_ => "", // Should not happen if called correctly
}
}
fn get_current_input_mut(&mut self) -> &mut String {
match self.current_focus {
AddTableFocus::InputTableName => &mut self.table_name_input,
AddTableFocus::InputColumnName => &mut self.column_name_input,
AddTableFocus::InputColumnType => &mut self.column_type_input,
_ => &mut self.table_name_input,
}
}
fn fields(&self) -> Vec<&str> {
// These must match the order used in render_add_table
vec!["Table name", "Name", "Type"]
}
fn set_current_field(&mut self, index: usize) {
self.current_focus = match index {
0 => AddTableFocus::InputTableName,
1 => AddTableFocus::InputColumnName,
2 => AddTableFocus::InputColumnType,
_ => self.current_focus, // Stay on current focus if index is out of bounds
};
}
fn set_current_cursor_pos(&mut self, pos: usize) {
match self.current_focus {
AddTableFocus::InputTableName => self.table_name_cursor_pos = pos,
AddTableFocus::InputColumnName => self.column_name_cursor_pos = pos,
AddTableFocus::InputColumnType => self.column_type_cursor_pos = pos,
_ => {} // Do nothing if focus is not on an input field
}
}
fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_unsaved_changes = changed;
}
// --- Autocomplete Support (Not needed for this form yet) ---
fn get_suggestions(&self) -> Option<&[String]> {
None
}
fn get_selected_suggestion_index(&self) -> Option<usize> {
None
}
}

View File

@@ -3,3 +3,4 @@
pub mod form;
pub mod login;
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
}
}
}

View File

@@ -104,6 +104,8 @@ pub fn render_ui(
theme,
app_state,
&mut admin_state.add_table_state,
login_state.current_field < 3,
highlight_state,
);
} else if app_state.ui.show_login {
render_login(