Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e0338276f | ||
|
|
fe193f4f91 | ||
|
|
0011ba0c04 | ||
|
|
3c2eef9596 | ||
|
|
dac788351f | ||
|
|
8d5bc1296e | ||
|
|
969ad229e4 | ||
|
|
0d291fcf57 | ||
|
|
d711f4c491 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -482,6 +482,8 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-test",
|
"tokio-test",
|
||||||
"toml",
|
"toml",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ toml = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
unicode-width.workspace = true
|
unicode-width.workspace = true
|
||||||
|
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = "0.3.19"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4.4"
|
tokio-test = "0.4.4"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ highlight_current_field = true
|
|||||||
move_left = ["h"]
|
move_left = ["h"]
|
||||||
move_right = ["l"]
|
move_right = ["l"]
|
||||||
move_up = ["k"]
|
move_up = ["k"]
|
||||||
move_down = ["p"]
|
move_down = ["j"]
|
||||||
move_word_next = ["w"]
|
move_word_next = ["w"]
|
||||||
move_word_end = ["e"]
|
move_word_end = ["e"]
|
||||||
move_word_prev = ["b"]
|
move_word_prev = ["b"]
|
||||||
@@ -42,6 +42,8 @@ move_word_next = ["Ctrl+Right"]
|
|||||||
move_word_prev = ["Ctrl+Left"]
|
move_word_prev = ["Ctrl+Left"]
|
||||||
next_field = ["Tab"]
|
next_field = ["Tab"]
|
||||||
prev_field = ["Shift+Tab"]
|
prev_field = ["Shift+Tab"]
|
||||||
|
trigger_autocomplete = ["Ctrl+p"]
|
||||||
|
|
||||||
|
|
||||||
# Suggestion/autocomplete keybindings
|
# Suggestion/autocomplete keybindings
|
||||||
[keybindings.suggestions]
|
[keybindings.suggestions]
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// src/autocomplete/actions.rs
|
// canvas/src/autocomplete/actions.rs
|
||||||
|
|
||||||
use crate::canvas::state::{CanvasState, ActionContext};
|
use crate::canvas::state::{CanvasState, ActionContext};
|
||||||
use crate::autocomplete::state::AutocompleteCanvasState;
|
use crate::autocomplete::state::AutocompleteCanvasState;
|
||||||
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
||||||
use crate::canvas::actions::edit::handle_generic_canvas_action; // Import the core function
|
use crate::canvas::actions::edit::handle_generic_canvas_action;
|
||||||
|
use crate::config::CanvasConfig;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
/// Version for states that implement rich autocomplete
|
/// Version for states that implement rich autocomplete
|
||||||
@@ -11,6 +12,7 @@ pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + Autocomple
|
|||||||
action: CanvasAction,
|
action: CanvasAction,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
|
config: Option<&CanvasConfig>,
|
||||||
) -> Result<ActionResult> {
|
) -> Result<ActionResult> {
|
||||||
// 1. Try feature-specific handler first
|
// 1. Try feature-specific handler first
|
||||||
let context = ActionContext {
|
let context = ActionContext {
|
||||||
@@ -20,31 +22,77 @@ pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + Autocomple
|
|||||||
current_field: state.current_field(),
|
current_field: state.current_field(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(result) = state.handle_feature_action(&action, &context) {
|
if let Some(result) = handle_rich_autocomplete_action(action.clone(), state, &context) {
|
||||||
return Ok(ActionResult::HandledByFeature(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Handle rich autocomplete actions
|
|
||||||
if let Some(result) = handle_rich_autocomplete_action(&action, state)? {
|
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Handle generic canvas actions
|
// 2. Handle generic actions and add auto-trigger logic
|
||||||
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
let result = handle_generic_canvas_action(action.clone(), state, ideal_cursor_column, config).await?;
|
||||||
|
|
||||||
|
// 3. AUTO-TRIGGER LOGIC: Check if we should activate/deactivate autocomplete
|
||||||
|
if let Some(cfg) = config {
|
||||||
|
println!("{:?}, {}", action, cfg.should_auto_trigger_autocomplete());
|
||||||
|
if cfg.should_auto_trigger_autocomplete() {
|
||||||
|
println!("AUTO-TRIGGER");
|
||||||
|
match action {
|
||||||
|
CanvasAction::InsertChar(_) => {
|
||||||
|
println!("AUTO-T on Ins");
|
||||||
|
let current_field = state.current_field();
|
||||||
|
let current_input = state.get_current_input();
|
||||||
|
|
||||||
|
if state.supports_autocomplete(current_field)
|
||||||
|
&& !state.is_autocomplete_active()
|
||||||
|
&& current_input.len() >= 1
|
||||||
|
{
|
||||||
|
println!("ACT AUTOC");
|
||||||
|
state.activate_autocomplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasAction::NextField | CanvasAction::PrevField => {
|
||||||
|
println!("AUTO-T on nav");
|
||||||
|
let current_field = state.current_field();
|
||||||
|
|
||||||
|
if state.supports_autocomplete(current_field) && !state.is_autocomplete_active() {
|
||||||
|
state.activate_autocomplete();
|
||||||
|
} else if !state.supports_autocomplete(current_field) && state.is_autocomplete_active() {
|
||||||
|
state.deactivate_autocomplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {} // No auto-trigger for other actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle rich autocomplete actions for AutocompleteCanvasState
|
/// Handle rich autocomplete actions for AutocompleteCanvasState
|
||||||
fn handle_rich_autocomplete_action<S: CanvasState + AutocompleteCanvasState>(
|
fn handle_rich_autocomplete_action<S: CanvasState + AutocompleteCanvasState>(
|
||||||
action: &CanvasAction,
|
action: CanvasAction,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
) -> Result<Option<ActionResult>> {
|
_context: &ActionContext,
|
||||||
|
) -> Option<ActionResult> {
|
||||||
match action {
|
match action {
|
||||||
CanvasAction::TriggerAutocomplete => {
|
CanvasAction::TriggerAutocomplete => {
|
||||||
if state.supports_autocomplete(state.current_field()) {
|
let current_field = state.current_field();
|
||||||
|
if state.supports_autocomplete(current_field) {
|
||||||
state.activate_autocomplete();
|
state.activate_autocomplete();
|
||||||
Ok(Some(ActionResult::success_with_message("Autocomplete activated - fetching suggestions...")))
|
Some(ActionResult::success_with_message("Autocomplete activated"))
|
||||||
} else {
|
} else {
|
||||||
Ok(Some(ActionResult::error("Autocomplete not supported for this field")))
|
Some(ActionResult::success_with_message("Autocomplete not supported for this field"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasAction::SuggestionUp => {
|
||||||
|
if state.is_autocomplete_ready() {
|
||||||
|
if let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
||||||
|
autocomplete_state.select_previous();
|
||||||
|
}
|
||||||
|
Some(ActionResult::success())
|
||||||
|
} else {
|
||||||
|
Some(ActionResult::success_with_message("No suggestions available"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,42 +100,34 @@ fn handle_rich_autocomplete_action<S: CanvasState + AutocompleteCanvasState>(
|
|||||||
if state.is_autocomplete_ready() {
|
if state.is_autocomplete_ready() {
|
||||||
if let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
if let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
||||||
autocomplete_state.select_next();
|
autocomplete_state.select_next();
|
||||||
return Ok(Some(ActionResult::success()));
|
|
||||||
}
|
}
|
||||||
|
Some(ActionResult::success())
|
||||||
|
} else {
|
||||||
|
Some(ActionResult::success_with_message("No suggestions available"))
|
||||||
}
|
}
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::SuggestionUp => {
|
|
||||||
if state.is_autocomplete_ready() {
|
|
||||||
if let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
|
||||||
autocomplete_state.select_previous();
|
|
||||||
return Ok(Some(ActionResult::success()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::SelectSuggestion => {
|
CanvasAction::SelectSuggestion => {
|
||||||
if state.is_autocomplete_ready() {
|
if state.is_autocomplete_ready() {
|
||||||
if let Some(message) = state.apply_autocomplete_selection() {
|
if let Some(msg) = state.apply_autocomplete_selection() {
|
||||||
return Ok(Some(ActionResult::success_with_message(message)));
|
Some(ActionResult::success_with_message(&msg))
|
||||||
} else {
|
} else {
|
||||||
return Ok(Some(ActionResult::error("No suggestion selected")));
|
Some(ActionResult::success_with_message("No suggestion selected"))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Some(ActionResult::success_with_message("No suggestions available"))
|
||||||
}
|
}
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::ExitSuggestions => {
|
CanvasAction::ExitSuggestions => {
|
||||||
if state.is_autocomplete_active() {
|
if state.is_autocomplete_active() {
|
||||||
state.deactivate_autocomplete();
|
state.deactivate_autocomplete();
|
||||||
Ok(Some(ActionResult::success_with_message("Autocomplete cancelled")))
|
Some(ActionResult::success_with_message("Exited autocomplete"))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Some(ActionResult::success())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => Ok(None),
|
_ => None, // Not a rich autocomplete action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ fn render_loading_indicator<T: CanvasTheme>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let loading_block = Block::default()
|
let loading_block = Block::default()
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(theme.accent()))
|
|
||||||
.style(Style::default().bg(theme.bg()));
|
.style(Style::default().bg(theme.bg()));
|
||||||
|
|
||||||
let loading_paragraph = Paragraph::new(loading_text)
|
let loading_paragraph = Paragraph::new(loading_text)
|
||||||
@@ -92,8 +90,6 @@ fn render_suggestions_dropdown<T: CanvasTheme>(
|
|||||||
|
|
||||||
// Background
|
// Background
|
||||||
let dropdown_block = Block::default()
|
let dropdown_block = Block::default()
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(theme.accent()))
|
|
||||||
.style(Style::default().bg(theme.bg()));
|
.style(Style::default().bg(theme.bg()));
|
||||||
|
|
||||||
// List items
|
// List items
|
||||||
@@ -111,7 +107,7 @@ fn render_suggestions_dropdown<T: CanvasTheme>(
|
|||||||
f.render_stateful_widget(list, dropdown_area, &mut list_state);
|
f.render_stateful_widget(list, dropdown_area, &mut list_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate dropdown size based on suggestions
|
/// Calculate dropdown size based on suggestions - updated to match client dimensions
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn calculate_dropdown_dimensions(display_texts: &[&str]) -> DropdownDimensions {
|
fn calculate_dropdown_dimensions(display_texts: &[&str]) -> DropdownDimensions {
|
||||||
let max_width = display_texts
|
let max_width = display_texts
|
||||||
@@ -120,9 +116,9 @@ fn calculate_dropdown_dimensions(display_texts: &[&str]) -> DropdownDimensions {
|
|||||||
.max()
|
.max()
|
||||||
.unwrap_or(0) as u16;
|
.unwrap_or(0) as u16;
|
||||||
|
|
||||||
let horizontal_padding = 4; // borders + padding
|
let horizontal_padding = 2; // Changed from 4 to 2 to match client
|
||||||
let width = (max_width + horizontal_padding).max(12);
|
let width = (max_width + horizontal_padding).max(10); // Changed from 12 to 10 to match client
|
||||||
let height = (display_texts.len() as u16).min(8) + 2; // max 8 visible items + borders
|
let height = (display_texts.len() as u16).min(5); // Removed +2 since no borders
|
||||||
|
|
||||||
DropdownDimensions { width, height }
|
DropdownDimensions { width, height }
|
||||||
}
|
}
|
||||||
@@ -155,7 +151,7 @@ fn calculate_dropdown_position(
|
|||||||
dropdown_area
|
dropdown_area
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create styled list items
|
/// Create styled list items - updated to match client spacing
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn create_suggestion_list_items<'a, T: CanvasTheme>(
|
fn create_suggestion_list_items<'a, T: CanvasTheme>(
|
||||||
display_texts: &'a [&'a str],
|
display_texts: &'a [&'a str],
|
||||||
@@ -163,8 +159,8 @@ fn create_suggestion_list_items<'a, T: CanvasTheme>(
|
|||||||
dropdown_width: u16,
|
dropdown_width: u16,
|
||||||
theme: &T,
|
theme: &T,
|
||||||
) -> Vec<ListItem<'a>> {
|
) -> Vec<ListItem<'a>> {
|
||||||
let horizontal_padding = 4;
|
let horizontal_padding = 2; // Changed from 4 to 2 to match client
|
||||||
let available_width = dropdown_width.saturating_sub(horizontal_padding);
|
let available_width = dropdown_width; // No border padding needed
|
||||||
|
|
||||||
display_texts
|
display_texts
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// canvas/src/actions/edit.rs
|
// canvas/src/canvas/actions/edit.rs
|
||||||
|
|
||||||
use crate::canvas::state::{CanvasState, ActionContext};
|
use crate::canvas::state::{CanvasState, ActionContext};
|
||||||
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
||||||
|
use crate::config::CanvasConfig;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
/// Execute a typed canvas action on any CanvasState implementation
|
/// Execute a typed canvas action on any CanvasState implementation
|
||||||
@@ -9,6 +10,7 @@ pub async fn execute_canvas_action<S: CanvasState>(
|
|||||||
action: CanvasAction,
|
action: CanvasAction,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
|
config: Option<&CanvasConfig>,
|
||||||
) -> Result<ActionResult> {
|
) -> Result<ActionResult> {
|
||||||
let context = ActionContext {
|
let context = ActionContext {
|
||||||
key_code: None,
|
key_code: None,
|
||||||
@@ -21,7 +23,7 @@ pub async fn execute_canvas_action<S: CanvasState>(
|
|||||||
return Ok(ActionResult::HandledByFeature(result));
|
return Ok(ActionResult::HandledByFeature(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
handle_generic_canvas_action(action, state, ideal_cursor_column, config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle core canvas actions with full type safety
|
/// Handle core canvas actions with full type safety
|
||||||
@@ -29,126 +31,84 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
|||||||
action: CanvasAction,
|
action: CanvasAction,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
|
config: Option<&CanvasConfig>,
|
||||||
) -> Result<ActionResult> {
|
) -> Result<ActionResult> {
|
||||||
match action {
|
match action {
|
||||||
CanvasAction::InsertChar(c) => {
|
CanvasAction::InsertChar(c) => {
|
||||||
let cursor_pos = state.current_cursor_pos();
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let field_value = state.get_current_input_mut();
|
let input = state.get_current_input_mut();
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
input.insert(cursor_pos, c);
|
||||||
|
state.set_current_cursor_pos(cursor_pos + 1);
|
||||||
|
state.set_has_unsaved_changes(true);
|
||||||
|
*ideal_cursor_column = cursor_pos + 1;
|
||||||
|
Ok(ActionResult::success())
|
||||||
|
}
|
||||||
|
|
||||||
if cursor_pos <= chars.len() {
|
CanvasAction::NextField | CanvasAction::PrevField => {
|
||||||
chars.insert(cursor_pos, c);
|
let old_field = state.current_field();
|
||||||
*field_value = chars.into_iter().collect();
|
let total_fields = state.fields().len();
|
||||||
state.set_current_cursor_pos(cursor_pos + 1);
|
|
||||||
state.set_has_unsaved_changes(true);
|
// Perform field navigation
|
||||||
*ideal_cursor_column = state.current_cursor_pos();
|
let new_field = match action {
|
||||||
Ok(ActionResult::success())
|
CanvasAction::NextField => {
|
||||||
} else {
|
if config.map_or(true, |c| c.behavior.wrap_around_fields) {
|
||||||
Ok(ActionResult::error("Invalid cursor position for character insertion"))
|
(old_field + 1) % total_fields
|
||||||
}
|
} else {
|
||||||
|
(old_field + 1).min(total_fields - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CanvasAction::PrevField => {
|
||||||
|
if config.map_or(true, |c| c.behavior.wrap_around_fields) {
|
||||||
|
if old_field == 0 { total_fields - 1 } else { old_field - 1 }
|
||||||
|
} else {
|
||||||
|
old_field.saturating_sub(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.set_current_field(new_field);
|
||||||
|
*ideal_cursor_column = state.current_cursor_pos();
|
||||||
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::DeleteBackward => {
|
CanvasAction::DeleteBackward => {
|
||||||
if state.current_cursor_pos() > 0 {
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let cursor_pos = state.current_cursor_pos();
|
if cursor_pos > 0 {
|
||||||
let field_value = state.get_current_input_mut();
|
let input = state.get_current_input_mut();
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
input.remove(cursor_pos - 1);
|
||||||
|
state.set_current_cursor_pos(cursor_pos - 1);
|
||||||
if cursor_pos <= chars.len() {
|
state.set_has_unsaved_changes(true);
|
||||||
chars.remove(cursor_pos - 1);
|
*ideal_cursor_column = 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(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::DeleteForward => {
|
CanvasAction::DeleteForward => {
|
||||||
let cursor_pos = state.current_cursor_pos();
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let field_value = state.get_current_input_mut();
|
let input = state.get_current_input_mut();
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
if cursor_pos < input.len() {
|
||||||
|
input.remove(cursor_pos);
|
||||||
if cursor_pos < chars.len() {
|
|
||||||
chars.remove(cursor_pos);
|
|
||||||
*field_value = chars.into_iter().collect();
|
|
||||||
state.set_has_unsaved_changes(true);
|
state.set_has_unsaved_changes(true);
|
||||||
*ideal_cursor_column = cursor_pos;
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success())
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::NextField => {
|
|
||||||
let num_fields = state.fields().len();
|
|
||||||
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(ActionResult::success())
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::PrevField => {
|
|
||||||
let num_fields = state.fields().len();
|
|
||||||
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(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveLeft => {
|
CanvasAction::MoveLeft => {
|
||||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
let cursor_pos = state.current_cursor_pos();
|
||||||
state.set_current_cursor_pos(new_pos);
|
if cursor_pos > 0 {
|
||||||
*ideal_cursor_column = new_pos;
|
state.set_current_cursor_pos(cursor_pos - 1);
|
||||||
|
*ideal_cursor_column = cursor_pos - 1;
|
||||||
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveRight => {
|
CanvasAction::MoveRight => {
|
||||||
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
let current_pos = state.current_cursor_pos();
|
if cursor_pos < current_input.len() {
|
||||||
if current_pos < current_input.len() {
|
state.set_current_cursor_pos(cursor_pos + 1);
|
||||||
let new_pos = current_pos + 1;
|
*ideal_cursor_column = cursor_pos + 1;
|
||||||
state.set_current_cursor_pos(new_pos);
|
|
||||||
*ideal_cursor_column = new_pos;
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success())
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::MoveUp => {
|
|
||||||
let num_fields = state.fields().len();
|
|
||||||
if num_fields > 0 {
|
|
||||||
let current_field = state.current_field();
|
|
||||||
let new_field = current_field.saturating_sub(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(ActionResult::success())
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::MoveDown => {
|
|
||||||
let num_fields = state.fields().len();
|
|
||||||
if num_fields > 0 {
|
|
||||||
let new_field = (state.current_field() + 1).min(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(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
@@ -160,43 +120,55 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveLineEnd => {
|
CanvasAction::MoveLineEnd => {
|
||||||
let current_input = state.get_current_input();
|
let end_pos = state.get_current_input().len();
|
||||||
let new_pos = current_input.len();
|
state.set_current_cursor_pos(end_pos);
|
||||||
state.set_current_cursor_pos(new_pos);
|
*ideal_cursor_column = end_pos;
|
||||||
*ideal_cursor_column = new_pos;
|
Ok(ActionResult::success())
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasAction::MoveUp => {
|
||||||
|
// For single-line fields, move to previous field
|
||||||
|
let current_field = state.current_field();
|
||||||
|
if current_field > 0 {
|
||||||
|
state.set_current_field(current_field - 1);
|
||||||
|
*ideal_cursor_column = state.current_cursor_pos();
|
||||||
|
}
|
||||||
|
Ok(ActionResult::success())
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasAction::MoveDown => {
|
||||||
|
// For single-line fields, move to next field
|
||||||
|
let current_field = state.current_field();
|
||||||
|
let total_fields = state.fields().len();
|
||||||
|
if current_field < total_fields - 1 {
|
||||||
|
state.set_current_field(current_field + 1);
|
||||||
|
*ideal_cursor_column = state.current_cursor_pos();
|
||||||
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveFirstLine => {
|
CanvasAction::MoveFirstLine => {
|
||||||
let num_fields = state.fields().len();
|
state.set_current_field(0);
|
||||||
if num_fields > 0 {
|
state.set_current_cursor_pos(0);
|
||||||
state.set_current_field(0);
|
*ideal_cursor_column = 0;
|
||||||
let current_input = state.get_current_input();
|
Ok(ActionResult::success())
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success_with_message("Moved to first field"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveLastLine => {
|
CanvasAction::MoveLastLine => {
|
||||||
let num_fields = state.fields().len();
|
let last_field = state.fields().len() - 1;
|
||||||
if num_fields > 0 {
|
state.set_current_field(last_field);
|
||||||
let new_field = num_fields - 1;
|
let end_pos = state.get_current_input().len();
|
||||||
state.set_current_field(new_field);
|
state.set_current_cursor_pos(end_pos);
|
||||||
let current_input = state.get_current_input();
|
*ideal_cursor_column = end_pos;
|
||||||
let max_pos = current_input.len();
|
Ok(ActionResult::success())
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success_with_message("Moved to last field"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveWordNext => {
|
CanvasAction::MoveWordNext => {
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
if !current_input.is_empty() {
|
if !current_input.is_empty() {
|
||||||
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
|
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(new_pos);
|
||||||
state.set_current_cursor_pos(final_pos);
|
*ideal_cursor_column = new_pos;
|
||||||
*ideal_cursor_column = final_pos;
|
|
||||||
}
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
@@ -204,19 +176,9 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
|||||||
CanvasAction::MoveWordEnd => {
|
CanvasAction::MoveWordEnd => {
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
if !current_input.is_empty() {
|
if !current_input.is_empty() {
|
||||||
let current_pos = state.current_cursor_pos();
|
let new_pos = find_word_end(current_input, state.current_cursor_pos());
|
||||||
let new_pos = find_word_end(current_input, current_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
|
*ideal_cursor_column = new_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(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
@@ -231,148 +193,61 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
|||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveWordEndPrev => {
|
|
||||||
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(ActionResult::success_with_message("Moved to previous word end"))
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::Custom(action_str) => {
|
CanvasAction::Custom(action_str) => {
|
||||||
Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str)))
|
Ok(ActionResult::success_with_message(&format!("Custom action: {}", action_str)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Autocomplete actions are handled by the autocomplete module
|
_ => Ok(ActionResult::success_with_message("Action not implemented")),
|
||||||
CanvasAction::TriggerAutocomplete | CanvasAction::SuggestionUp | CanvasAction::SuggestionDown |
|
|
||||||
CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => {
|
|
||||||
Ok(ActionResult::error("Autocomplete actions should be handled by autocomplete module"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word movement helper functions
|
// Helper functions for word navigation
|
||||||
#[derive(PartialEq)]
|
fn find_next_word_start(text: &str, cursor_pos: usize) -> usize {
|
||||||
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 chars: Vec<char> = text.chars().collect();
|
||||||
let len = chars.len();
|
let mut pos = cursor_pos;
|
||||||
if len == 0 || current_pos >= len {
|
|
||||||
return len;
|
// Skip current word
|
||||||
}
|
while pos < chars.len() && chars[pos].is_alphanumeric() {
|
||||||
|
|
||||||
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;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
// Skip whitespace
|
||||||
|
while pos < chars.len() && chars[pos].is_whitespace() {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pos
|
pos
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
fn find_word_end(text: &str, cursor_pos: usize) -> usize {
|
||||||
let chars: Vec<char> = text.chars().collect();
|
let chars: Vec<char> = text.chars().collect();
|
||||||
let len = chars.len();
|
let mut pos = cursor_pos;
|
||||||
if len == 0 {
|
|
||||||
return 0;
|
// Move to end of current word
|
||||||
}
|
while pos < chars.len() && chars[pos].is_alphanumeric() {
|
||||||
|
|
||||||
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 += 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
|
pos
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
fn find_prev_word_start(text: &str, cursor_pos: usize) -> usize {
|
||||||
|
if cursor_pos == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
let chars: Vec<char> = text.chars().collect();
|
||||||
let len = chars.len();
|
let mut pos = cursor_pos.saturating_sub(1);
|
||||||
if len == 0 || current_pos == 0 {
|
|
||||||
return 0;
|
// Skip whitespace
|
||||||
}
|
while pos > 0 && chars[pos].is_whitespace() {
|
||||||
|
|
||||||
let mut pos = current_pos.saturating_sub(1);
|
|
||||||
|
|
||||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
|
||||||
pos -= 1;
|
pos -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
// Skip to start of word
|
||||||
return 0;
|
while pos > 0 && chars[pos - 1].is_alphanumeric() {
|
||||||
}
|
|
||||||
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 -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
pos
|
||||||
pos -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if pos > 0 {
|
|
||||||
pos - 1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ pub mod edit;
|
|||||||
|
|
||||||
// Re-export the main types for convenience
|
// Re-export the main types for convenience
|
||||||
pub use types::{CanvasAction, ActionResult};
|
pub use types::{CanvasAction, ActionResult};
|
||||||
pub use edit::execute_canvas_action; // Remove execute_edit_action
|
pub use edit::execute_canvas_action;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
// canvas/src/actions/types.rs
|
// src/canvas/actions/types.rs
|
||||||
|
|
||||||
use crossterm::event::KeyCode;
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|
||||||
/// All possible canvas actions, type-safe and exhaustive
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum CanvasAction {
|
pub enum CanvasAction {
|
||||||
// Character input
|
// Character input
|
||||||
InsertChar(char),
|
InsertChar(char),
|
||||||
@@ -28,37 +25,43 @@ pub enum CanvasAction {
|
|||||||
MoveWordNext,
|
MoveWordNext,
|
||||||
MoveWordEnd,
|
MoveWordEnd,
|
||||||
MoveWordPrev,
|
MoveWordPrev,
|
||||||
MoveWordEndPrev,
|
|
||||||
|
|
||||||
// Field navigation
|
// Field navigation
|
||||||
NextField,
|
NextField,
|
||||||
PrevField,
|
PrevField,
|
||||||
|
|
||||||
// AUTOCOMPLETE ACTIONS (NEW)
|
// Autocomplete actions
|
||||||
/// Manually trigger autocomplete for current field
|
|
||||||
TriggerAutocomplete,
|
TriggerAutocomplete,
|
||||||
/// Move to next suggestion
|
|
||||||
SuggestionUp,
|
SuggestionUp,
|
||||||
/// Move to previous suggestion
|
|
||||||
SuggestionDown,
|
SuggestionDown,
|
||||||
/// Select the currently highlighted suggestion
|
|
||||||
SelectSuggestion,
|
SelectSuggestion,
|
||||||
/// Cancel/exit autocomplete mode
|
|
||||||
ExitSuggestions,
|
ExitSuggestions,
|
||||||
|
|
||||||
// Custom actions (escape hatch for feature-specific behavior)
|
// Custom actions
|
||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CanvasAction {
|
impl CanvasAction {
|
||||||
/// Convert a string action to typed action (for backwards compatibility during migration)
|
pub fn from_key(key: crossterm::event::KeyCode) -> Option<Self> {
|
||||||
|
match key {
|
||||||
|
crossterm::event::KeyCode::Char(c) => Some(Self::InsertChar(c)),
|
||||||
|
crossterm::event::KeyCode::Backspace => Some(Self::DeleteBackward),
|
||||||
|
crossterm::event::KeyCode::Delete => Some(Self::DeleteForward),
|
||||||
|
crossterm::event::KeyCode::Left => Some(Self::MoveLeft),
|
||||||
|
crossterm::event::KeyCode::Right => Some(Self::MoveRight),
|
||||||
|
crossterm::event::KeyCode::Up => Some(Self::MoveUp),
|
||||||
|
crossterm::event::KeyCode::Down => Some(Self::MoveDown),
|
||||||
|
crossterm::event::KeyCode::Home => Some(Self::MoveLineStart),
|
||||||
|
crossterm::event::KeyCode::End => Some(Self::MoveLineEnd),
|
||||||
|
crossterm::event::KeyCode::Tab => Some(Self::NextField),
|
||||||
|
crossterm::event::KeyCode::BackTab => Some(Self::PrevField),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility method
|
||||||
pub fn from_string(action: &str) -> Self {
|
pub fn from_string(action: &str) -> Self {
|
||||||
match action {
|
match action {
|
||||||
"insert_char" => {
|
|
||||||
// This is a bit tricky - we need the char from context
|
|
||||||
// For now, we'll use Custom until we refactor the call sites
|
|
||||||
Self::Custom(action.to_string())
|
|
||||||
}
|
|
||||||
"delete_char_backward" => Self::DeleteBackward,
|
"delete_char_backward" => Self::DeleteBackward,
|
||||||
"delete_char_forward" => Self::DeleteForward,
|
"delete_char_forward" => Self::DeleteForward,
|
||||||
"move_left" => Self::MoveLeft,
|
"move_left" => Self::MoveLeft,
|
||||||
@@ -72,10 +75,8 @@ impl CanvasAction {
|
|||||||
"move_word_next" => Self::MoveWordNext,
|
"move_word_next" => Self::MoveWordNext,
|
||||||
"move_word_end" => Self::MoveWordEnd,
|
"move_word_end" => Self::MoveWordEnd,
|
||||||
"move_word_prev" => Self::MoveWordPrev,
|
"move_word_prev" => Self::MoveWordPrev,
|
||||||
"move_word_end_prev" => Self::MoveWordEndPrev,
|
|
||||||
"next_field" => Self::NextField,
|
"next_field" => Self::NextField,
|
||||||
"prev_field" => Self::PrevField,
|
"prev_field" => Self::PrevField,
|
||||||
// Autocomplete actions
|
|
||||||
"trigger_autocomplete" => Self::TriggerAutocomplete,
|
"trigger_autocomplete" => Self::TriggerAutocomplete,
|
||||||
"suggestion_up" => Self::SuggestionUp,
|
"suggestion_up" => Self::SuggestionUp,
|
||||||
"suggestion_down" => Self::SuggestionDown,
|
"suggestion_down" => Self::SuggestionDown,
|
||||||
@@ -84,94 +85,13 @@ impl CanvasAction {
|
|||||||
_ => Self::Custom(action.to_string()),
|
_ => Self::Custom(action.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get string representation (for logging, debugging)
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::InsertChar(_) => "insert_char",
|
|
||||||
Self::DeleteBackward => "delete_char_backward",
|
|
||||||
Self::DeleteForward => "delete_char_forward",
|
|
||||||
Self::MoveLeft => "move_left",
|
|
||||||
Self::MoveRight => "move_right",
|
|
||||||
Self::MoveUp => "move_up",
|
|
||||||
Self::MoveDown => "move_down",
|
|
||||||
Self::MoveLineStart => "move_line_start",
|
|
||||||
Self::MoveLineEnd => "move_line_end",
|
|
||||||
Self::MoveFirstLine => "move_first_line",
|
|
||||||
Self::MoveLastLine => "move_last_line",
|
|
||||||
Self::MoveWordNext => "move_word_next",
|
|
||||||
Self::MoveWordEnd => "move_word_end",
|
|
||||||
Self::MoveWordPrev => "move_word_prev",
|
|
||||||
Self::MoveWordEndPrev => "move_word_end_prev",
|
|
||||||
Self::NextField => "next_field",
|
|
||||||
Self::PrevField => "prev_field",
|
|
||||||
// Autocomplete actions
|
|
||||||
Self::TriggerAutocomplete => "trigger_autocomplete",
|
|
||||||
Self::SuggestionUp => "suggestion_up",
|
|
||||||
Self::SuggestionDown => "suggestion_down",
|
|
||||||
Self::SelectSuggestion => "select_suggestion",
|
|
||||||
Self::ExitSuggestions => "exit_suggestions",
|
|
||||||
Self::Custom(s) => s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create action from KeyCode for common cases
|
|
||||||
pub fn from_key(key: KeyCode) -> Option<Self> {
|
|
||||||
match key {
|
|
||||||
KeyCode::Char(c) => Some(Self::InsertChar(c)),
|
|
||||||
KeyCode::Backspace => Some(Self::DeleteBackward),
|
|
||||||
KeyCode::Delete => Some(Self::DeleteForward),
|
|
||||||
KeyCode::Left => Some(Self::MoveLeft),
|
|
||||||
KeyCode::Right => Some(Self::MoveRight),
|
|
||||||
KeyCode::Up => Some(Self::MoveUp),
|
|
||||||
KeyCode::Down => Some(Self::MoveDown),
|
|
||||||
KeyCode::Home => Some(Self::MoveLineStart),
|
|
||||||
KeyCode::End => Some(Self::MoveLineEnd),
|
|
||||||
KeyCode::Tab => Some(Self::NextField),
|
|
||||||
KeyCode::BackTab => Some(Self::PrevField),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this action modifies content
|
|
||||||
pub fn is_modifying(&self) -> bool {
|
|
||||||
matches!(self,
|
|
||||||
Self::InsertChar(_) |
|
|
||||||
Self::DeleteBackward |
|
|
||||||
Self::DeleteForward |
|
|
||||||
Self::SelectSuggestion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this action moves the cursor
|
|
||||||
pub fn is_movement(&self) -> bool {
|
|
||||||
matches!(self,
|
|
||||||
Self::MoveLeft | Self::MoveRight | Self::MoveUp | Self::MoveDown |
|
|
||||||
Self::MoveLineStart | Self::MoveLineEnd | Self::MoveFirstLine | Self::MoveLastLine |
|
|
||||||
Self::MoveWordNext | Self::MoveWordEnd | Self::MoveWordPrev | Self::MoveWordEndPrev |
|
|
||||||
Self::NextField | Self::PrevField
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this is a suggestion-related action
|
|
||||||
pub fn is_suggestion(&self) -> bool {
|
|
||||||
matches!(self,
|
|
||||||
Self::TriggerAutocomplete | Self::SuggestionUp | Self::SuggestionDown |
|
|
||||||
Self::SelectSuggestion | Self::ExitSuggestions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of executing a canvas action
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum ActionResult {
|
pub enum ActionResult {
|
||||||
/// Action completed successfully, optional message for user feedback
|
|
||||||
Success(Option<String>),
|
Success(Option<String>),
|
||||||
/// Action was handled by custom feature logic
|
|
||||||
HandledByFeature(String),
|
HandledByFeature(String),
|
||||||
/// Action requires additional context or cannot be performed
|
|
||||||
RequiresContext(String),
|
RequiresContext(String),
|
||||||
/// Action failed with error message
|
|
||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +100,11 @@ impl ActionResult {
|
|||||||
Self::Success(None)
|
Self::Success(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn success_with_message(msg: impl Into<String>) -> Self {
|
pub fn success_with_message(msg: &str) -> Self {
|
||||||
Self::Success(Some(msg.into()))
|
Self::Success(Some(msg.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn error(msg: impl Into<String>) -> Self {
|
pub fn error(msg: &str) -> Self {
|
||||||
Self::Error(msg.into())
|
Self::Error(msg.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,37 +121,3 @@ impl ActionResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_action_from_string() {
|
|
||||||
assert_eq!(CanvasAction::from_string("move_left"), CanvasAction::MoveLeft);
|
|
||||||
assert_eq!(CanvasAction::from_string("delete_char_backward"), CanvasAction::DeleteBackward);
|
|
||||||
assert_eq!(CanvasAction::from_string("trigger_autocomplete"), CanvasAction::TriggerAutocomplete);
|
|
||||||
assert_eq!(CanvasAction::from_string("unknown"), CanvasAction::Custom("unknown".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_action_from_key() {
|
|
||||||
assert_eq!(CanvasAction::from_key(KeyCode::Char('a')), Some(CanvasAction::InsertChar('a')));
|
|
||||||
assert_eq!(CanvasAction::from_key(KeyCode::Left), Some(CanvasAction::MoveLeft));
|
|
||||||
assert_eq!(CanvasAction::from_key(KeyCode::Backspace), Some(CanvasAction::DeleteBackward));
|
|
||||||
assert_eq!(CanvasAction::from_key(KeyCode::F(1)), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_action_properties() {
|
|
||||||
assert!(CanvasAction::InsertChar('a').is_modifying());
|
|
||||||
assert!(!CanvasAction::MoveLeft.is_modifying());
|
|
||||||
|
|
||||||
assert!(CanvasAction::MoveLeft.is_movement());
|
|
||||||
assert!(!CanvasAction::InsertChar('a').is_movement());
|
|
||||||
|
|
||||||
assert!(CanvasAction::SuggestionUp.is_suggestion());
|
|
||||||
assert!(CanvasAction::TriggerAutocomplete.is_suggestion());
|
|
||||||
assert!(!CanvasAction::MoveLeft.is_suggestion());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -180,6 +180,20 @@ impl CanvasConfig {
|
|||||||
Self::from_toml(&contents)
|
Self::from_toml(&contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// NEW: Check if autocomplete should auto-trigger (simple logic)
|
||||||
|
pub fn should_auto_trigger_autocomplete(&self) -> bool {
|
||||||
|
// If trigger_autocomplete keybinding exists anywhere, use manual mode only
|
||||||
|
// If no trigger_autocomplete keybinding, use auto-trigger mode
|
||||||
|
!self.has_trigger_autocomplete_keybinding()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NEW: Check if user has configured manual trigger keybinding
|
||||||
|
pub fn has_trigger_autocomplete_keybinding(&self) -> bool {
|
||||||
|
self.keybindings.edit.contains_key("trigger_autocomplete") ||
|
||||||
|
self.keybindings.read_only.contains_key("trigger_autocomplete") ||
|
||||||
|
self.keybindings.global.contains_key("trigger_autocomplete")
|
||||||
|
}
|
||||||
|
|
||||||
/// Get action for key in read-only mode
|
/// Get action for key in read-only mode
|
||||||
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
self.get_action_in_mode(&self.keybindings.read_only, key, modifiers)
|
self.get_action_in_mode(&self.keybindings.read_only, key, modifiers)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use crate::canvas::state::CanvasState;
|
use crate::canvas::state::CanvasState;
|
||||||
use crate::canvas::actions::{CanvasAction, ActionResult, execute_canvas_action};
|
use crate::canvas::actions::{CanvasAction, ActionResult, execute_canvas_action};
|
||||||
|
use crate::config::CanvasConfig;
|
||||||
|
|
||||||
/// High-level action dispatcher that coordinates between different action types
|
/// High-level action dispatcher that coordinates between different action types
|
||||||
pub struct ActionDispatcher;
|
pub struct ActionDispatcher;
|
||||||
@@ -13,7 +14,9 @@ impl ActionDispatcher {
|
|||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> anyhow::Result<ActionResult> {
|
) -> anyhow::Result<ActionResult> {
|
||||||
execute_canvas_action(action, state, ideal_cursor_column).await
|
|
||||||
|
// Load config once here instead of threading it everywhere
|
||||||
|
execute_canvas_action(action, state, ideal_cursor_column, Some(&CanvasConfig::load())).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quick action dispatch from KeyCode
|
/// Quick action dispatch from KeyCode
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ move_word_next = ["Ctrl+Right"]
|
|||||||
move_word_prev = ["Ctrl+Left"]
|
move_word_prev = ["Ctrl+Left"]
|
||||||
next_field = ["Tab"]
|
next_field = ["Tab"]
|
||||||
prev_field = ["Shift+Tab"]
|
prev_field = ["Shift+Tab"]
|
||||||
|
trigger_autocomplete = ["Ctrl+p"]
|
||||||
|
|
||||||
# Suggestion/autocomplete keybindings
|
# Suggestion/autocomplete keybindings
|
||||||
[keybindings.suggestions]
|
[keybindings.suggestions]
|
||||||
@@ -49,6 +50,7 @@ suggestion_up = ["Up", "Ctrl+p"]
|
|||||||
suggestion_down = ["Down", "Ctrl+n"]
|
suggestion_down = ["Down", "Ctrl+n"]
|
||||||
select_suggestion = ["Enter", "Tab"]
|
select_suggestion = ["Enter", "Tab"]
|
||||||
exit_suggestions = ["Esc"]
|
exit_suggestions = ["Esc"]
|
||||||
|
trigger_autocomplete = ["Tab"]
|
||||||
|
|
||||||
# Global keybindings (work in both modes)
|
# Global keybindings (work in both modes)
|
||||||
[keybindings.global]
|
[keybindings.global]
|
||||||
|
|||||||
@@ -69,11 +69,10 @@ prev_field = ["shift+enter"]
|
|||||||
exit = ["esc", "ctrl+e"]
|
exit = ["esc", "ctrl+e"]
|
||||||
delete_char_forward = ["delete"]
|
delete_char_forward = ["delete"]
|
||||||
delete_char_backward = ["backspace"]
|
delete_char_backward = ["backspace"]
|
||||||
move_left = [""]
|
move_left = ["left"]
|
||||||
move_right = ["right"]
|
move_right = ["right"]
|
||||||
suggestion_down = ["ctrl+n", "tab"]
|
suggestion_down = ["ctrl+n", "tab"]
|
||||||
suggestion_up = ["ctrl+p", "shift+tab"]
|
suggestion_up = ["ctrl+p", "shift+tab"]
|
||||||
trigger_autocomplete = ["left"]
|
|
||||||
|
|
||||||
[keybindings.command]
|
[keybindings.command]
|
||||||
exit_command_mode = ["ctrl+g", "esc"]
|
exit_command_mode = ["ctrl+g", "esc"]
|
||||||
|
|||||||
124
client/docs/canvas_add_functionality.md
Normal file
124
client/docs/canvas_add_functionality.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
## How Canvas Library Custom Functionality Works
|
||||||
|
|
||||||
|
### 1. **The Canvas Library Calls YOUR Custom Code First**
|
||||||
|
|
||||||
|
When you call `ActionDispatcher::dispatch()`, here's what happens:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Inside canvas library (canvas/src/actions/edit.rs):
|
||||||
|
pub async fn execute_canvas_action<S: CanvasState>(
|
||||||
|
action: CanvasAction,
|
||||||
|
state: &mut S,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> Result<ActionResult> {
|
||||||
|
// 1. FIRST: Canvas library calls YOUR custom handler
|
||||||
|
if let Some(result) = state.handle_feature_action(&action, &context) {
|
||||||
|
return Ok(ActionResult::HandledByFeature(result)); // YOUR code handled it
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ONLY IF your code returns None: Canvas handles generic actions
|
||||||
|
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Your Extension Point: `handle_feature_action`**
|
||||||
|
|
||||||
|
You add custom functionality by implementing `handle_feature_action` in your states:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// In src/state/pages/auth.rs
|
||||||
|
impl CanvasState for LoginState {
|
||||||
|
// ... other methods ...
|
||||||
|
|
||||||
|
fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option<String> {
|
||||||
|
match action {
|
||||||
|
// Custom login-specific actions
|
||||||
|
CanvasAction::Custom(action_str) if action_str == "submit_login" => {
|
||||||
|
if self.username.is_empty() || self.password.is_empty() {
|
||||||
|
Some("Please fill in all required fields".to_string())
|
||||||
|
} else {
|
||||||
|
// Trigger login process
|
||||||
|
Some(format!("Logging in user: {}", self.username))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasAction::Custom(action_str) if action_str == "clear_form" => {
|
||||||
|
self.username.clear();
|
||||||
|
self.password.clear();
|
||||||
|
self.set_has_unsaved_changes(false);
|
||||||
|
Some("Login form cleared".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom behavior for standard actions
|
||||||
|
CanvasAction::NextField => {
|
||||||
|
// Custom validation when moving between fields
|
||||||
|
if self.current_field == 0 && self.username.is_empty() {
|
||||||
|
Some("Username cannot be empty".to_string())
|
||||||
|
} else {
|
||||||
|
None // Let canvas library handle the normal field movement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let canvas library handle everything else
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Multiple Ways to Add Custom Functionality**
|
||||||
|
|
||||||
|
#### A) **Custom Actions via Config**
|
||||||
|
```toml
|
||||||
|
# In config.toml
|
||||||
|
[keybindings.edit]
|
||||||
|
submit_login = ["ctrl+enter"]
|
||||||
|
clear_form = ["ctrl+r"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B) **Override Standard Actions**
|
||||||
|
```rust
|
||||||
|
fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option<String> {
|
||||||
|
match action {
|
||||||
|
CanvasAction::InsertChar('p') if self.current_field == 1 => {
|
||||||
|
// Custom behavior when typing 'p' in password field
|
||||||
|
Some("Password field - use secure input".to_string())
|
||||||
|
}
|
||||||
|
_ => None, // Let canvas handle normally
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### C) **Context-Aware Logic**
|
||||||
|
```rust
|
||||||
|
fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option<String> {
|
||||||
|
match action {
|
||||||
|
CanvasAction::MoveDown => {
|
||||||
|
// Custom logic based on current state
|
||||||
|
if context.current_field == 1 && context.current_input.len() < 8 {
|
||||||
|
Some("Password should be at least 8 characters".to_string())
|
||||||
|
} else {
|
||||||
|
None // Normal field movement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Canvas Library Philosophy
|
||||||
|
|
||||||
|
**Canvas Library = Generic behavior + Your extension points**
|
||||||
|
|
||||||
|
- ✅ **Canvas handles**: Character insertion, cursor movement, field navigation, etc.
|
||||||
|
- ✅ **You handle**: Validation, submission, clearing, app-specific logic
|
||||||
|
- ✅ **You decide**: Return `Some(message)` to override, `None` to use canvas default
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
You **don't communicate with the library elsewhere**. Instead:
|
||||||
|
|
||||||
|
1. **Canvas library calls your code first** via `handle_feature_action`
|
||||||
|
2. **Your code decides** whether to handle the action or let canvas handle it
|
||||||
|
3. **Canvas library handles** generic form behavior when you return `None`
|
||||||
|
|
||||||
@@ -3,8 +3,7 @@ use crate::config::colors::themes::Theme;
|
|||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::add_table::{AddTableFocus, AddTableState};
|
use crate::state::pages::add_table::{AddTableFocus, AddTableState};
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use canvas::canvas::{render_canvas, CanvasState, HighlightState as CanvasHighlightState}; // Use canvas library
|
||||||
// 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::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
@@ -12,16 +11,24 @@ use ratatui::{
|
|||||||
widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table},
|
widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::components::handlers::canvas::render_canvas;
|
|
||||||
use crate::components::common::dialog;
|
use crate::components::common::dialog;
|
||||||
|
|
||||||
|
// Helper function to convert between HighlightState types
|
||||||
|
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
|
||||||
|
match local {
|
||||||
|
HighlightState::Off => CanvasHighlightState::Off,
|
||||||
|
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
|
||||||
|
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders the Add New Table page layout, structuring the display of table information,
|
/// Renders the Add New Table page layout, structuring the display of table information,
|
||||||
/// input fields, and action buttons. Adapts layout based on terminal width.
|
/// 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, // Currently unused, might be needed later
|
app_state: &AppState,
|
||||||
add_table_state: &mut AddTableState,
|
add_table_state: &mut AddTableState,
|
||||||
is_edit_mode: bool, // Determines if canvas inputs are in edit mode
|
is_edit_mode: bool, // Determines if canvas inputs are in edit mode
|
||||||
highlight_state: &HighlightState, // For text highlighting in canvas
|
highlight_state: &HighlightState, // For text highlighting in canvas
|
||||||
@@ -349,17 +356,15 @@ pub fn render_add_table(
|
|||||||
&mut add_table_state.column_table_state,
|
&mut add_table_state.column_table_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Canvas Rendering (Column Definition Input) ---
|
// --- Canvas Rendering (Column Definition Input) - USING CANVAS LIBRARY ---
|
||||||
|
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
||||||
let _active_field_rect = render_canvas(
|
let _active_field_rect = render_canvas(
|
||||||
f,
|
f,
|
||||||
canvas_area,
|
canvas_area,
|
||||||
add_table_state,
|
add_table_state, // AddTableState implements CanvasState
|
||||||
&add_table_state.fields(),
|
theme, // Theme implements CanvasTheme
|
||||||
&add_table_state.current_field(),
|
|
||||||
&add_table_state.inputs(),
|
|
||||||
theme,
|
|
||||||
is_edit_mode && focus_on_canvas_inputs,
|
is_edit_mode && focus_on_canvas_inputs,
|
||||||
highlight_state,
|
&canvas_highlight_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Button Style Helpers ---
|
// --- Button Style Helpers ---
|
||||||
@@ -557,7 +562,7 @@ pub fn render_add_table(
|
|||||||
|
|
||||||
// --- DIALOG ---
|
// --- DIALOG ---
|
||||||
// Render the dialog overlay if it's active
|
// Render the dialog overlay if it's active
|
||||||
if app_state.ui.dialog.dialog_show { // Use the passed-in app_state
|
if app_state.ui.dialog.dialog_show {
|
||||||
dialog::render_dialog(
|
dialog::render_dialog(
|
||||||
f,
|
f,
|
||||||
f.area(), // Render over the whole frame area
|
f.area(), // Render over the whole frame area
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
|
use canvas::canvas::{render_canvas, HighlightState as CanvasHighlightState}; // Use canvas library's render function
|
||||||
|
|
||||||
|
// Helper function to convert between HighlightState types
|
||||||
|
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
|
||||||
|
match local {
|
||||||
|
HighlightState::Off => CanvasHighlightState::Off,
|
||||||
|
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
|
||||||
|
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_login(
|
pub fn render_login(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -48,17 +58,15 @@ pub fn render_login(
|
|||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// --- FORM RENDERING ---
|
// --- FORM RENDERING (Using canvas library directly) ---
|
||||||
crate::components::handlers::canvas::render_canvas(
|
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
||||||
|
render_canvas(
|
||||||
f,
|
f,
|
||||||
chunks[0],
|
chunks[0],
|
||||||
login_state,
|
login_state, // LoginState implements CanvasState
|
||||||
&["Username/Email", "Password"],
|
theme, // Theme implements CanvasTheme
|
||||||
&login_state.current_field,
|
|
||||||
&[&login_state.username, &login_state.password],
|
|
||||||
theme,
|
|
||||||
is_edit_mode,
|
is_edit_mode,
|
||||||
highlight_state,
|
&canvas_highlight_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- ERROR MESSAGE ---
|
// --- ERROR MESSAGE ---
|
||||||
@@ -71,7 +79,7 @@ pub fn render_login(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BUTTONS ---
|
// --- BUTTONS (unchanged) ---
|
||||||
let button_chunks = Layout::default()
|
let button_chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
@@ -83,7 +91,7 @@ pub fn render_login(
|
|||||||
app_state.focused_button_index== login_button_index
|
app_state.focused_button_index== login_button_index
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
let mut login_style = Style::default().fg(theme.fg);
|
let mut login_style = Style::default().fg(theme.fg);
|
||||||
let mut login_border = Style::default().fg(theme.border);
|
let mut login_border = Style::default().fg(theme.border);
|
||||||
if login_active {
|
if login_active {
|
||||||
@@ -105,12 +113,12 @@ pub fn render_login(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Return Button
|
// Return Button
|
||||||
let return_button_index = 1; // Assuming Return is the second general element
|
let return_button_index = 1;
|
||||||
let return_active = if app_state.ui.focus_outside_canvas {
|
let return_active = if app_state.ui.focus_outside_canvas {
|
||||||
app_state.focused_button_index== return_button_index
|
app_state.focused_button_index== return_button_index
|
||||||
} else {
|
} else {
|
||||||
false // Not active if focus is in canvas or other modes
|
false
|
||||||
};
|
};
|
||||||
let mut return_style = Style::default().fg(theme.fg);
|
let mut return_style = Style::default().fg(theme.fg);
|
||||||
let mut return_border = Style::default().fg(theme.border);
|
let mut return_border = Style::default().fg(theme.border);
|
||||||
if return_active {
|
if return_active {
|
||||||
@@ -132,17 +140,15 @@ pub fn render_login(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- DIALOG ---
|
// --- DIALOG ---
|
||||||
// Check the correct field name for showing the dialog
|
|
||||||
if app_state.ui.dialog.dialog_show {
|
if app_state.ui.dialog.dialog_show {
|
||||||
// Pass all 7 arguments correctly
|
|
||||||
dialog::render_dialog(
|
dialog::render_dialog(
|
||||||
f,
|
f,
|
||||||
f.area(),
|
f.area(),
|
||||||
theme,
|
theme,
|
||||||
&app_state.ui.dialog.dialog_title,
|
&app_state.ui.dialog.dialog_title,
|
||||||
&app_state.ui.dialog.dialog_message,
|
&app_state.ui.dialog.dialog_message,
|
||||||
&app_state.ui.dialog.dialog_buttons, // Pass buttons slice
|
&app_state.ui.dialog.dialog_buttons,
|
||||||
app_state.ui.dialog.dialog_active_button_index,
|
app_state.ui.dialog.dialog_active_button_index,
|
||||||
app_state.ui.dialog.is_loading,
|
app_state.ui.dialog.is_loading,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::colors::themes::Theme,
|
config::colors::themes::Theme,
|
||||||
state::pages::auth::RegisterState, // Use RegisterState
|
state::pages::auth::RegisterState,
|
||||||
components::common::{dialog, autocomplete},
|
components::common::dialog,
|
||||||
state::app::state::AppState,
|
state::app::state::AppState,
|
||||||
state::pages::canvas_state::CanvasState,
|
|
||||||
modes::handlers::mode_manager::AppMode,
|
modes::handlers::mode_manager::AppMode,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@@ -15,12 +14,24 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
|
use canvas::canvas::{render_canvas, HighlightState as CanvasHighlightState}; // Use canvas library's render function
|
||||||
|
use canvas::autocomplete::gui::render_autocomplete_dropdown;
|
||||||
|
use canvas::autocomplete::AutocompleteCanvasState;
|
||||||
|
|
||||||
|
// Helper function to convert between HighlightState types
|
||||||
|
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
|
||||||
|
match local {
|
||||||
|
HighlightState::Off => CanvasHighlightState::Off,
|
||||||
|
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
|
||||||
|
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_register(
|
pub fn render_register(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
state: &RegisterState, // Use RegisterState
|
state: &RegisterState,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
is_edit_mode: bool,
|
is_edit_mode: bool,
|
||||||
highlight_state: &HighlightState,
|
highlight_state: &HighlightState,
|
||||||
@@ -29,7 +40,7 @@ pub fn render_register(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Plain)
|
.border_type(BorderType::Plain)
|
||||||
.border_style(Style::default().fg(theme.border))
|
.border_style(Style::default().fg(theme.border))
|
||||||
.title(" Register ") // Update title
|
.title(" Register ")
|
||||||
.style(Style::default().bg(theme.bg));
|
.style(Style::default().bg(theme.bg));
|
||||||
|
|
||||||
f.render_widget(block, area);
|
f.render_widget(block, area);
|
||||||
@@ -39,7 +50,6 @@ pub fn render_register(
|
|||||||
vertical: 1,
|
vertical: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Adjust constraints for 4 fields + error + buttons
|
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
@@ -50,23 +60,15 @@ pub fn render_register(
|
|||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// --- FORM RENDERING (Using render_canvas) ---
|
// --- FORM RENDERING (Using canvas library directly) ---
|
||||||
let active_field_rect = crate::components::handlers::canvas::render_canvas(
|
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
||||||
|
let input_rect = render_canvas(
|
||||||
f,
|
f,
|
||||||
chunks[0], // Area for the canvas
|
chunks[0],
|
||||||
state, // The state object (RegisterState)
|
state, // RegisterState implements CanvasState
|
||||||
&[ // Field labels
|
theme, // Theme implements CanvasTheme
|
||||||
"Username",
|
|
||||||
"Email*",
|
|
||||||
"Password*",
|
|
||||||
"Confirm Password",
|
|
||||||
"Role* (Tab)",
|
|
||||||
],
|
|
||||||
&state.current_field(), // Pass current field index
|
|
||||||
&state.inputs().iter().map(|s| *s).collect::<Vec<&String>>(), // Pass inputs directly
|
|
||||||
theme,
|
|
||||||
is_edit_mode,
|
is_edit_mode,
|
||||||
highlight_state,
|
&canvas_highlight_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- HELP TEXT ---
|
// --- HELP TEXT ---
|
||||||
@@ -75,7 +77,6 @@ pub fn render_register(
|
|||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
f.render_widget(help_text, chunks[1]);
|
f.render_widget(help_text, chunks[1]);
|
||||||
|
|
||||||
|
|
||||||
// --- ERROR MESSAGE ---
|
// --- ERROR MESSAGE ---
|
||||||
if let Some(err) = &state.error_message {
|
if let Some(err) = &state.error_message {
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
@@ -107,7 +108,7 @@ pub fn render_register(
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new("Register") // Update button text
|
Paragraph::new("Register")
|
||||||
.style(register_style)
|
.style(register_style)
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
@@ -119,7 +120,7 @@ pub fn render_register(
|
|||||||
button_chunks[0],
|
button_chunks[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Return Button (logic remains similar)
|
// Return Button
|
||||||
let return_button_index = 1;
|
let return_button_index = 1;
|
||||||
let return_active = if app_state.ui.focus_outside_canvas {
|
let return_active = if app_state.ui.focus_outside_canvas {
|
||||||
app_state.focused_button_index== return_button_index
|
app_state.focused_button_index== return_button_index
|
||||||
@@ -146,19 +147,22 @@ pub fn render_register(
|
|||||||
button_chunks[1],
|
button_chunks[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Render Autocomplete Dropdown (Draw AFTER buttons) ---
|
// --- AUTOCOMPLETE DROPDOWN (Using canvas library directly) ---
|
||||||
if app_state.current_mode == AppMode::Edit {
|
if app_state.current_mode == AppMode::Edit {
|
||||||
if let Some(suggestions) = state.get_suggestions() {
|
if let Some(autocomplete_state) = state.autocomplete_state() {
|
||||||
let selected = state.get_selected_suggestion_index();
|
if let Some(input_rect) = input_rect {
|
||||||
if !suggestions.is_empty() {
|
render_autocomplete_dropdown(
|
||||||
if let Some(input_rect) = active_field_rect {
|
f,
|
||||||
autocomplete::render_autocomplete_dropdown(f, input_rect, f.area(), theme, suggestions, selected);
|
f.area(), // Frame area
|
||||||
}
|
input_rect, // Current input field rect
|
||||||
|
theme, // Theme implements CanvasTheme
|
||||||
|
autocomplete_state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DIALOG --- (Keep dialog logic)
|
// --- DIALOG ---
|
||||||
if app_state.ui.dialog.dialog_show {
|
if app_state.ui.dialog.dialog_show {
|
||||||
dialog::render_dialog(
|
dialog::render_dialog(
|
||||||
f,
|
f,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/functions/modes/edit/add_table_e.rs
|
// src/functions/modes/edit/add_table_e.rs
|
||||||
use crate::state::pages::add_table::AddTableState;
|
use crate::state::pages::add_table::AddTableState;
|
||||||
use crate::state::pages::canvas_state::CanvasState; // Use trait
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use canvas::canvas::CanvasState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// src/functions/modes/edit/auth_e.rs
|
// src/functions/modes/edit/auth_e.rs
|
||||||
|
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
|
||||||
use crate::state::pages::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use crate::state::pages::auth::RegisterState;
|
use crate::state::pages::auth::RegisterState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::tui::functions::common::form::{revert, save};
|
use crate::tui::functions::common::form::{revert, save};
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use canvas::autocomplete::AutocompleteCanvasState;
|
||||||
|
use canvas::canvas::CanvasState;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
@@ -295,53 +296,42 @@ pub async fn execute_edit_action<S: CanvasState + Any + Send>(
|
|||||||
"suggestion_down" | "suggestion_up" | "select_suggestion" | "exit_suggestion_mode" => {
|
"suggestion_down" | "suggestion_up" | "select_suggestion" | "exit_suggestion_mode" => {
|
||||||
// Attempt to downcast to RegisterState to handle suggestion logic here
|
// Attempt to downcast to RegisterState to handle suggestion logic here
|
||||||
if let Some(register_state) = (state as &mut dyn Any).downcast_mut::<RegisterState>() {
|
if let Some(register_state) = (state as &mut dyn Any).downcast_mut::<RegisterState>() {
|
||||||
// Only handle if it's the role field (index 4)
|
// Only handle if it's the role field (index 4) and autocomplete is active
|
||||||
if register_state.current_field() == 4 {
|
if register_state.current_field() == 4 && register_state.is_autocomplete_active() {
|
||||||
match action {
|
match action {
|
||||||
"suggestion_down" if register_state.in_suggestion_mode => {
|
"suggestion_down" => {
|
||||||
let max_index = register_state.role_suggestions.len().saturating_sub(1);
|
if let Some(autocomplete_state) = register_state.autocomplete_state_mut() {
|
||||||
let current_index = register_state.selected_suggestion_index.unwrap_or(0);
|
autocomplete_state.select_next();
|
||||||
register_state.selected_suggestion_index = Some(if current_index >= max_index { 0 } else { current_index + 1 });
|
Ok("Suggestion changed down".to_string())
|
||||||
Ok("Suggestion changed down".to_string())
|
} else {
|
||||||
|
Ok("No autocomplete state".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"suggestion_up" if register_state.in_suggestion_mode => {
|
"suggestion_up" => {
|
||||||
let max_index = register_state.role_suggestions.len().saturating_sub(1);
|
if let Some(autocomplete_state) = register_state.autocomplete_state_mut() {
|
||||||
let current_index = register_state.selected_suggestion_index.unwrap_or(0);
|
autocomplete_state.select_previous();
|
||||||
register_state.selected_suggestion_index = Some(if current_index == 0 { max_index } else { current_index.saturating_sub(1) });
|
Ok("Suggestion changed up".to_string())
|
||||||
Ok("Suggestion changed up".to_string())
|
} else {
|
||||||
|
Ok("No autocomplete state".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"select_suggestion" if register_state.in_suggestion_mode => {
|
"select_suggestion" => {
|
||||||
if let Some(index) = register_state.selected_suggestion_index {
|
if let Some(message) = register_state.apply_autocomplete_selection() {
|
||||||
if let Some(selected_role) = register_state.role_suggestions.get(index).cloned() {
|
Ok(message)
|
||||||
register_state.role = selected_role.clone(); // Update the role field
|
|
||||||
register_state.in_suggestion_mode = false; // Exit suggestion mode
|
|
||||||
register_state.show_role_suggestions = false; // Hide suggestions
|
|
||||||
register_state.selected_suggestion_index = None; // Clear selection
|
|
||||||
Ok(format!("Selected role: {}", selected_role)) // Return success message
|
|
||||||
} else {
|
|
||||||
Ok("Selected suggestion index out of bounds.".to_string()) // Error case
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Ok("No suggestion selected".to_string())
|
Ok("No suggestion selected".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"exit_suggestion_mode" => { // Handle Esc or other conditions
|
"exit_suggestion_mode" => {
|
||||||
register_state.show_role_suggestions = false;
|
register_state.deactivate_autocomplete();
|
||||||
register_state.selected_suggestion_index = None;
|
|
||||||
register_state.in_suggestion_mode = false;
|
|
||||||
Ok("Suggestions hidden".to_string())
|
Ok("Suggestions hidden".to_string())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => Ok("Suggestion action ignored: State mismatch.".to_string())
|
||||||
// Action is suggestion-related but state doesn't match (e.g., not in suggestion mode)
|
|
||||||
Ok("Suggestion action ignored: State mismatch.".to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// It's RegisterState, but not the role field
|
Ok("Suggestion action ignored: Not on role field or autocomplete not active.".to_string())
|
||||||
Ok("Suggestion action ignored: Not on role field.".to_string())
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Downcast failed - this action is only for RegisterState
|
|
||||||
Ok(format!("Action '{}' not applicable for this state type.", action))
|
Ok(format!("Action '{}' not applicable for this state type.", action))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// src/functions/modes/read_only/add_table_ro.rs
|
// src/functions/modes/read_only/add_table_ro.rs
|
||||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
use crate::state::pages::add_table::AddTableState;
|
use crate::state::pages::add_table::AddTableState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
|
use canvas::canvas::CanvasState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
// Re-use word navigation helpers if they are public or move them to a common module
|
// Re-use word navigation helpers if they are public or move them to a common module
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// src/functions/modes/read_only/auth_ro.rs
|
// src/functions/modes/read_only/auth_ro.rs
|
||||||
|
|
||||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
|
use canvas::canvas::CanvasState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/modes/canvas/edit.rs
|
// src/modes/canvas/edit.rs
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::functions::modes::edit::{
|
use crate::functions::modes::edit::{
|
||||||
add_logic_e, add_table_e, auth_e, form_e,
|
add_logic_e, add_table_e, form_e,
|
||||||
};
|
};
|
||||||
use crate::modes::handlers::event::EventHandler;
|
use crate::modes::handlers::event::EventHandler;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
@@ -127,6 +127,60 @@ pub async fn handle_form_edit_with_canvas(
|
|||||||
Ok(String::new())
|
Ok(String::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// NEW: Unified canvas action handler for any CanvasState (LoginState, RegisterState, etc.)
|
||||||
|
/// This replaces the old auth_e::execute_edit_action calls with the new canvas library
|
||||||
|
async fn handle_canvas_state_edit<S: CanvasState>(
|
||||||
|
key: KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
state: &mut S,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> Result<String> {
|
||||||
|
// Try direct key mapping first (same pattern as FormState)
|
||||||
|
if let Some(canvas_action) = CanvasAction::from_key(key.code) {
|
||||||
|
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
||||||
|
Ok(ActionResult::Success(msg)) => {
|
||||||
|
return Ok(msg.unwrap_or_default());
|
||||||
|
}
|
||||||
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||||
|
return Ok(msg);
|
||||||
|
}
|
||||||
|
Ok(ActionResult::Error(msg)) => {
|
||||||
|
return Ok(format!("Error: {}", msg));
|
||||||
|
}
|
||||||
|
Ok(ActionResult::RequiresContext(msg)) => {
|
||||||
|
return Ok(format!("Context needed: {}", msg));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Fall through to try config mapping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try config-mapped action (same pattern as FormState)
|
||||||
|
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
||||||
|
let canvas_action = CanvasAction::from_string(&action_str);
|
||||||
|
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
||||||
|
Ok(ActionResult::Success(msg)) => {
|
||||||
|
return Ok(msg.unwrap_or_default());
|
||||||
|
}
|
||||||
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||||
|
return Ok(msg);
|
||||||
|
}
|
||||||
|
Ok(ActionResult::Error(msg)) => {
|
||||||
|
return Ok(format!("Error: {}", msg));
|
||||||
|
}
|
||||||
|
Ok(ActionResult::RequiresContext(msg)) => {
|
||||||
|
return Ok(format!("Context needed: {}", msg));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Ok(format!("Action failed: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn handle_edit_event(
|
pub async fn handle_edit_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
@@ -283,21 +337,21 @@ pub async fn handle_edit_event(
|
|||||||
return Ok(EditEventOutcome::ExitEditMode);
|
return Ok(EditEventOutcome::ExitEditMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle all other edit actions
|
// Handle all other edit actions - NOW USING CANVAS LIBRARY
|
||||||
let msg = if app_state.ui.show_login {
|
let msg = if app_state.ui.show_login {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||||
auth_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
action_str,
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
login_state,
|
login_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of add_table_e::execute_edit_action
|
||||||
add_table_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
action_str,
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
&mut admin_state.add_table_state,
|
&mut admin_state.add_table_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
@@ -312,10 +366,10 @@ pub async fn handle_edit_event(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||||
auth_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
action_str,
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
register_state,
|
register_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
@@ -336,19 +390,19 @@ pub async fn handle_edit_event(
|
|||||||
// --- FALLBACK FOR CHARACTER INSERTION (IF NO OTHER BINDING MATCHED) ---
|
// --- FALLBACK FOR CHARACTER INSERTION (IF NO OTHER BINDING MATCHED) ---
|
||||||
if let KeyCode::Char(_) = key.code {
|
if let KeyCode::Char(_) = key.code {
|
||||||
let msg = if app_state.ui.show_login {
|
let msg = if app_state.ui.show_login {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||||
auth_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
"insert_char",
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
login_state,
|
login_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of add_table_e::execute_edit_action
|
||||||
add_table_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
"insert_char",
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
&mut admin_state.add_table_state,
|
&mut admin_state.add_table_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
@@ -363,10 +417,10 @@ pub async fn handle_edit_event(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||||
auth_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
"insert_char",
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
register_state,
|
register_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
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::auth::LoginState;
|
||||||
|
use crate::state::pages::auth::RegisterState;
|
||||||
|
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState;
|
||||||
use crate::state::pages::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use crate::state::pages::add_logic::AddLogicState;
|
use crate::state::pages::add_logic::AddLogicState;
|
||||||
use crate::state::pages::add_table::AddTableState;
|
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::{add_logic_ro, auth_ro, form_ro, add_table_ro};
|
use crate::functions::modes::read_only::{add_logic_ro, auth_ro, form_ro, add_table_ro};
|
||||||
use canvas::{canvas::CanvasAction, dispatcher::ActionDispatcher, canvas::ActionResult};
|
use canvas::{canvas::{CanvasAction, CanvasState, ActionResult}, dispatcher::ActionDispatcher};
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use crate::tui::terminal::core::TerminalCore;
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::{form::FormState, auth::LoginState, auth::RegisterState};
|
use crate::state::pages::{form::FormState, auth::LoginState, auth::RegisterState};
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use canvas::canvas::CanvasState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub struct CommandHandler;
|
pub struct CommandHandler;
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ use crate::state::pages::auth::LoginState;
|
|||||||
use crate::state::pages::auth::RegisterState;
|
use crate::state::pages::auth::RegisterState;
|
||||||
use crate::state::pages::intro::IntroState;
|
use crate::state::pages::intro::IntroState;
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::state::pages::admin::AdminState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
|
||||||
use crate::ui::handlers::context::UiContext;
|
use crate::ui::handlers::context::UiContext;
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crate::modes::general::command_navigation::{handle_command_navigation_event, NavigationState};
|
use crate::modes::general::command_navigation::{handle_command_navigation_event, NavigationState};
|
||||||
|
use canvas::canvas::CanvasState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub async fn handle_navigation_event(
|
pub async fn handle_navigation_event(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/state/pages/add_table.rs
|
// src/state/pages/add_table.rs
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use canvas::canvas::{CanvasState, ActionContext, CanvasAction}; // External library
|
||||||
use ratatui::widgets::TableState;
|
use ratatui::widgets::TableState;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -67,7 +67,6 @@ pub struct AddTableState {
|
|||||||
|
|
||||||
impl Default for AddTableState {
|
impl Default for AddTableState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
// Initialize with some dummy data for demonstration
|
|
||||||
AddTableState {
|
AddTableState {
|
||||||
profile_name: "default".to_string(),
|
profile_name: "default".to_string(),
|
||||||
table_name: String::new(),
|
table_name: String::new(),
|
||||||
@@ -92,16 +91,91 @@ impl Default for AddTableState {
|
|||||||
|
|
||||||
impl AddTableState {
|
impl AddTableState {
|
||||||
pub const INPUT_FIELD_COUNT: usize = 3;
|
pub const INPUT_FIELD_COUNT: usize = 3;
|
||||||
|
|
||||||
|
/// Helper method to add a column from current inputs
|
||||||
|
pub fn add_column_from_inputs(&mut self) -> Option<String> {
|
||||||
|
if self.column_name_input.trim().is_empty() || self.column_type_input.trim().is_empty() {
|
||||||
|
return Some("Both column name and type are required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate column names
|
||||||
|
if self.columns.iter().any(|col| col.name == self.column_name_input.trim()) {
|
||||||
|
return Some("Column name already exists".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the column
|
||||||
|
self.columns.push(ColumnDefinition {
|
||||||
|
name: self.column_name_input.trim().to_string(),
|
||||||
|
data_type: self.column_type_input.trim().to_string(),
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear inputs and reset focus to column name for next entry
|
||||||
|
self.column_name_input.clear();
|
||||||
|
self.column_type_input.clear();
|
||||||
|
self.column_name_cursor_pos = 0;
|
||||||
|
self.column_type_cursor_pos = 0;
|
||||||
|
self.current_focus = AddTableFocus::InputColumnName;
|
||||||
|
self.last_canvas_field = 1;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
|
||||||
|
Some(format!("Column '{}' added successfully", self.columns.last().unwrap().name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to delete selected items
|
||||||
|
pub fn delete_selected_items(&mut self) -> Option<String> {
|
||||||
|
let mut deleted_items = Vec::new();
|
||||||
|
|
||||||
|
// Remove selected columns
|
||||||
|
let initial_column_count = self.columns.len();
|
||||||
|
self.columns.retain(|col| {
|
||||||
|
if col.selected {
|
||||||
|
deleted_items.push(format!("column '{}'", col.name));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove selected indexes
|
||||||
|
let initial_index_count = self.indexes.len();
|
||||||
|
self.indexes.retain(|idx| {
|
||||||
|
if idx.selected {
|
||||||
|
deleted_items.push(format!("index '{}'", idx.name));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove selected links
|
||||||
|
let initial_link_count = self.links.len();
|
||||||
|
self.links.retain(|link| {
|
||||||
|
if link.selected {
|
||||||
|
deleted_items.push(format!("link to '{}'", link.linked_table_name));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if deleted_items.is_empty() {
|
||||||
|
Some("No items selected for deletion".to_string())
|
||||||
|
} else {
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
Some(format!("Deleted: {}", deleted_items.join(", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement CanvasState for the input fields
|
// Implement external library's CanvasState for AddTableState
|
||||||
impl CanvasState for AddTableState {
|
impl CanvasState for AddTableState {
|
||||||
fn current_field(&self) -> usize {
|
fn current_field(&self) -> usize {
|
||||||
match self.current_focus {
|
match self.current_focus {
|
||||||
AddTableFocus::InputTableName => 0,
|
AddTableFocus::InputTableName => 0,
|
||||||
AddTableFocus::InputColumnName => 1,
|
AddTableFocus::InputColumnName => 1,
|
||||||
AddTableFocus::InputColumnType => 2,
|
AddTableFocus::InputColumnType => 2,
|
||||||
// If focus is elsewhere, default to the first field for canvas rendering logic
|
// If focus is elsewhere, return the last canvas field used
|
||||||
_ => self.last_canvas_field,
|
_ => self.last_canvas_field,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,37 +189,6 @@ impl CanvasState for AddTableState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
fn set_current_field(&mut self, index: usize) {
|
||||||
// Update both current focus and last canvas field
|
// Update both current focus and last canvas field
|
||||||
self.current_focus = match index {
|
self.current_focus = match index {
|
||||||
@@ -174,17 +217,84 @@ impl CanvasState for AddTableState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, // Fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inputs(&self) -> Vec<&String> {
|
||||||
|
vec![&self.table_name_input, &self.column_name_input, &self.column_type_input]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fields(&self) -> Vec<&str> {
|
||||||
|
// These must match the order used in render_add_table
|
||||||
|
vec!["Table name", "Name", "Type"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_unsaved_changes(&self) -> bool {
|
||||||
|
self.has_unsaved_changes
|
||||||
|
}
|
||||||
|
|
||||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||||
self.has_unsaved_changes = changed;
|
self.has_unsaved_changes = changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Autocomplete Support (Not needed for this form yet) ---
|
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
match action {
|
||||||
None
|
// Handle adding column when user presses Enter on the Add button or uses specific action
|
||||||
}
|
CanvasAction::Custom(action_str) if action_str == "add_column" => {
|
||||||
|
self.add_column_from_inputs()
|
||||||
|
}
|
||||||
|
|
||||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
// Handle table saving
|
||||||
None
|
CanvasAction::Custom(action_str) if action_str == "save_table" => {
|
||||||
|
if self.table_name_input.trim().is_empty() {
|
||||||
|
Some("Table name is required".to_string())
|
||||||
|
} else if self.columns.is_empty() {
|
||||||
|
Some("At least one column is required".to_string())
|
||||||
|
} else {
|
||||||
|
Some(format!("Saving table: {}", self.table_name_input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deleting selected items
|
||||||
|
CanvasAction::Custom(action_str) if action_str == "delete_selected" => {
|
||||||
|
self.delete_selected_items()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle canceling (clear form)
|
||||||
|
CanvasAction::Custom(action_str) if action_str == "cancel" => {
|
||||||
|
// Reset to defaults but keep profile_name
|
||||||
|
let profile = self.profile_name.clone();
|
||||||
|
*self = Self::default();
|
||||||
|
self.profile_name = profile;
|
||||||
|
Some("Form cleared".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validation when moving between fields
|
||||||
|
CanvasAction::NextField => {
|
||||||
|
// When leaving table name field, update the table_name for display
|
||||||
|
if self.current_field() == 0 && !self.table_name_input.trim().is_empty() {
|
||||||
|
self.table_name = self.table_name_input.trim().to_string();
|
||||||
|
}
|
||||||
|
None // Let canvas library handle the normal field movement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let canvas library handle everything else
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/state/pages/auth.rs
|
// src/state/pages/auth.rs
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use canvas::canvas::{CanvasState, ActionContext, CanvasAction};
|
||||||
|
use canvas::autocomplete::{AutocompleteCanvasState, AutocompleteState, SuggestionItem};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -44,91 +45,61 @@ pub struct RegisterState {
|
|||||||
pub current_field: usize,
|
pub current_field: usize,
|
||||||
pub current_cursor_pos: usize,
|
pub current_cursor_pos: usize,
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
pub show_role_suggestions: bool,
|
// NEW: Replace old autocomplete with external library's system
|
||||||
pub role_suggestions: Vec<String>,
|
pub autocomplete: AutocompleteState<String>,
|
||||||
pub selected_suggestion_index: Option<usize>,
|
|
||||||
pub in_suggestion_mode: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthState {
|
impl AuthState {
|
||||||
/// Creates a new empty AuthState (unauthenticated)
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self::default()
|
||||||
auth_token: None,
|
|
||||||
user_id: None,
|
|
||||||
role: None,
|
|
||||||
decoded_username: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoginState {
|
impl LoginState {
|
||||||
/// Creates a new empty LoginState
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self::default()
|
||||||
username: String::new(),
|
|
||||||
password: String::new(),
|
|
||||||
error_message: None,
|
|
||||||
current_field: 0,
|
|
||||||
current_cursor_pos: 0,
|
|
||||||
has_unsaved_changes: false,
|
|
||||||
login_request_pending: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegisterState {
|
impl RegisterState {
|
||||||
/// Creates a new empty RegisterState
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
let mut state = Self {
|
||||||
username: String::new(),
|
autocomplete: AutocompleteState::new(),
|
||||||
email: String::new(),
|
..Default::default()
|
||||||
password: String::new(),
|
};
|
||||||
password_confirmation: String::new(),
|
|
||||||
role: String::new(),
|
// Initialize autocomplete with role suggestions
|
||||||
error_message: None,
|
let suggestions: Vec<SuggestionItem<String>> = AVAILABLE_ROLES
|
||||||
current_field: 0,
|
|
||||||
current_cursor_pos: 0,
|
|
||||||
has_unsaved_changes: false,
|
|
||||||
show_role_suggestions: false,
|
|
||||||
role_suggestions: Vec::new(),
|
|
||||||
selected_suggestion_index: None,
|
|
||||||
in_suggestion_mode: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates role suggestions based on current input
|
|
||||||
pub fn update_role_suggestions(&mut self) {
|
|
||||||
let current_input = self.role.to_lowercase();
|
|
||||||
self.role_suggestions = AVAILABLE_ROLES
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|role| role.to_lowercase().contains(¤t_input))
|
.map(|role| SuggestionItem::simple(role.clone(), role.clone()))
|
||||||
.cloned()
|
|
||||||
.collect();
|
.collect();
|
||||||
self.show_role_suggestions = !self.role_suggestions.is_empty();
|
|
||||||
|
// Set suggestions but keep inactive initially
|
||||||
|
state.autocomplete.set_suggestions(suggestions);
|
||||||
|
state.autocomplete.is_active = false; // Not active by default
|
||||||
|
|
||||||
|
state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement external library's CanvasState for LoginState
|
||||||
impl CanvasState for LoginState {
|
impl CanvasState for LoginState {
|
||||||
fn current_field(&self) -> usize {
|
fn current_field(&self) -> usize {
|
||||||
self.current_field
|
self.current_field
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_cursor_pos(&self) -> usize {
|
fn current_cursor_pos(&self) -> usize {
|
||||||
let len = match self.current_field {
|
self.current_cursor_pos
|
||||||
0 => self.username.len(),
|
|
||||||
1 => self.password.len(),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
self.current_cursor_pos.min(len)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_unsaved_changes(&self) -> bool {
|
fn set_current_field(&mut self, index: usize) {
|
||||||
self.has_unsaved_changes
|
if index < 2 {
|
||||||
|
self.current_field = index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inputs(&self) -> Vec<&String> {
|
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
vec![&self.username, &self.password]
|
self.current_cursor_pos = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input(&self) -> &str {
|
fn get_current_input(&self) -> &str {
|
||||||
@@ -147,73 +118,61 @@ impl CanvasState for LoginState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inputs(&self) -> Vec<&String> {
|
||||||
|
vec![&self.username, &self.password]
|
||||||
|
}
|
||||||
|
|
||||||
fn fields(&self) -> Vec<&str> {
|
fn fields(&self) -> Vec<&str> {
|
||||||
vec!["Username/Email", "Password"]
|
vec!["Username/Email", "Password"]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_field(&mut self, index: usize) {
|
|
||||||
if index < 2 {
|
|
||||||
self.current_field = index;
|
|
||||||
let len = match self.current_field {
|
|
||||||
0 => self.username.len(),
|
|
||||||
1 => self.password.len(),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
self.current_cursor_pos = self.current_cursor_pos.min(len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
|
||||||
let len = match self.current_field {
|
|
||||||
0 => self.username.len(),
|
|
||||||
1 => self.password.len(),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
self.current_cursor_pos = pos.min(len);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
|
||||||
self.has_unsaved_changes = changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CanvasState for RegisterState {
|
|
||||||
fn current_field(&self) -> usize {
|
|
||||||
self.current_field
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_cursor_pos(&self) -> usize {
|
|
||||||
let len = match self.current_field {
|
|
||||||
0 => self.username.len(),
|
|
||||||
1 => self.email.len(),
|
|
||||||
2 => self.password.len(),
|
|
||||||
3 => self.password_confirmation.len(),
|
|
||||||
4 => self.role.len(),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
self.current_cursor_pos.min(len)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_unsaved_changes(&self) -> bool {
|
fn has_unsaved_changes(&self) -> bool {
|
||||||
self.has_unsaved_changes
|
self.has_unsaved_changes
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inputs(&self) -> Vec<&String> {
|
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||||
vec![
|
self.has_unsaved_changes = changed;
|
||||||
&self.username,
|
}
|
||||||
&self.email,
|
|
||||||
&self.password,
|
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||||
&self.password_confirmation,
|
match action {
|
||||||
&self.role,
|
CanvasAction::Custom(action_str) if action_str == "submit" => {
|
||||||
]
|
if !self.username.is_empty() && !self.password.is_empty() {
|
||||||
|
Some(format!("Submitting login for: {}", self.username))
|
||||||
|
} else {
|
||||||
|
Some("Please fill in all required fields".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement external library's CanvasState for RegisterState
|
||||||
|
impl CanvasState for RegisterState {
|
||||||
|
fn current_field(&self) -> usize {
|
||||||
|
self.current_field
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_cursor_pos(&self) -> usize {
|
||||||
|
self.current_cursor_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_current_field(&mut self, index: usize) {
|
||||||
|
if index < 5 {
|
||||||
|
self.current_field = index;
|
||||||
|
|
||||||
|
// Auto-activate autocomplete when moving to role field (index 4)
|
||||||
|
if index == 4 && !self.autocomplete.is_active {
|
||||||
|
self.activate_autocomplete();
|
||||||
|
} else if index != 4 && self.autocomplete.is_active {
|
||||||
|
self.deactivate_autocomplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
|
self.current_cursor_pos = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input(&self) -> &str {
|
fn get_current_input(&self) -> &str {
|
||||||
@@ -238,6 +197,16 @@ impl CanvasState for RegisterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inputs(&self) -> Vec<&String> {
|
||||||
|
vec![
|
||||||
|
&self.username,
|
||||||
|
&self.email,
|
||||||
|
&self.password,
|
||||||
|
&self.password_confirmation,
|
||||||
|
&self.role,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn fields(&self) -> Vec<&str> {
|
fn fields(&self) -> Vec<&str> {
|
||||||
vec![
|
vec![
|
||||||
"Username",
|
"Username",
|
||||||
@@ -248,50 +217,99 @@ impl CanvasState for RegisterState {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_field(&mut self, index: usize) {
|
fn has_unsaved_changes(&self) -> bool {
|
||||||
if index < 5 {
|
self.has_unsaved_changes
|
||||||
self.current_field = index;
|
|
||||||
let len = match self.current_field {
|
|
||||||
0 => self.username.len(),
|
|
||||||
1 => self.email.len(),
|
|
||||||
2 => self.password.len(),
|
|
||||||
3 => self.password_confirmation.len(),
|
|
||||||
4 => self.role.len(),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
self.current_cursor_pos = self.current_cursor_pos.min(len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
|
||||||
let len = match self.current_field {
|
|
||||||
0 => self.username.len(),
|
|
||||||
1 => self.email.len(),
|
|
||||||
2 => self.password.len(),
|
|
||||||
3 => self.password_confirmation.len(),
|
|
||||||
4 => self.role.len(),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
self.current_cursor_pos = pos.min(len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||||
self.has_unsaved_changes = changed;
|
self.has_unsaved_changes = changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||||
if self.current_field == 4 && self.in_suggestion_mode && self.show_role_suggestions {
|
match action {
|
||||||
Some(&self.role_suggestions)
|
CanvasAction::Custom(action_str) if action_str == "submit" => {
|
||||||
|
if !self.username.is_empty() {
|
||||||
|
Some(format!("Submitting registration for: {}", self.username))
|
||||||
|
} else {
|
||||||
|
Some("Username is required".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add autocomplete support for RegisterState
|
||||||
|
impl AutocompleteCanvasState for RegisterState {
|
||||||
|
type SuggestionData = String;
|
||||||
|
|
||||||
|
fn supports_autocomplete(&self, field_index: usize) -> bool {
|
||||||
|
field_index == 4 // Only role field supports autocomplete
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autocomplete_state(&self) -> Option<&AutocompleteState<Self::SuggestionData>> {
|
||||||
|
Some(&self.autocomplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState<Self::SuggestionData>> {
|
||||||
|
Some(&mut self.autocomplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate_autocomplete(&mut self) {
|
||||||
|
let current_field = self.current_field();
|
||||||
|
if self.supports_autocomplete(current_field) {
|
||||||
|
self.autocomplete.activate(current_field);
|
||||||
|
|
||||||
|
// Re-filter suggestions based on current input
|
||||||
|
let current_input = self.role.to_lowercase();
|
||||||
|
let filtered_suggestions: Vec<SuggestionItem<String>> = AVAILABLE_ROLES
|
||||||
|
.iter()
|
||||||
|
.filter(|role| role.to_lowercase().contains(¤t_input))
|
||||||
|
.map(|role| SuggestionItem::simple(role.clone(), role.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.autocomplete.set_suggestions(filtered_suggestions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivate_autocomplete(&mut self) {
|
||||||
|
self.autocomplete.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_autocomplete_active(&self) -> bool {
|
||||||
|
self.autocomplete.is_active
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_autocomplete_ready(&self) -> bool {
|
||||||
|
self.autocomplete.is_ready()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_autocomplete_selection(&mut self) -> Option<String> {
|
||||||
|
// First, get the data we need and clone it to avoid borrowing conflicts
|
||||||
|
let selection_info = self.autocomplete.get_selected().map(|selected| {
|
||||||
|
(selected.value_to_store.clone(), selected.display_text.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now do the mutable operations
|
||||||
|
if let Some((value, display_text)) = selection_info {
|
||||||
|
self.role = value;
|
||||||
|
self.set_has_unsaved_changes(true);
|
||||||
|
self.deactivate_autocomplete();
|
||||||
|
Some(format!("Selected role: {}", display_text))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
fn set_autocomplete_suggestions(&mut self, suggestions: Vec<SuggestionItem<Self::SuggestionData>>) {
|
||||||
if self.current_field == 4 && self.in_suggestion_mode && self.show_role_suggestions {
|
if let Some(state) = self.autocomplete_state_mut() {
|
||||||
self.selected_suggestion_index
|
state.set_suggestions(suggestions);
|
||||||
} else {
|
}
|
||||||
None
|
}
|
||||||
|
|
||||||
|
fn set_autocomplete_loading(&mut self, loading: bool) {
|
||||||
|
if let Some(state) = self.autocomplete_state_mut() {
|
||||||
|
state.is_loading = loading;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ use crate::state::pages::auth::LoginState;
|
|||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::app::buffer::{AppView, BufferState};
|
use crate::state::app::buffer::{AppView, BufferState};
|
||||||
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
|
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use common::proto::komp_ac::auth::LoginResponse;
|
use common::proto::komp_ac::auth::LoginResponse;
|
||||||
|
use canvas::canvas::CanvasState;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ use crate::services::auth::AuthClient;
|
|||||||
use crate::state::{
|
use crate::state::{
|
||||||
pages::auth::RegisterState,
|
pages::auth::RegisterState,
|
||||||
app::state::AppState,
|
app::state::AppState,
|
||||||
pages::canvas_state::CanvasState,
|
|
||||||
};
|
};
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use crate::state::app::buffer::{AppView, BufferState};
|
use crate::state::app::buffer::{AppView, BufferState};
|
||||||
use common::proto::komp_ac::auth::AuthResponse;
|
use common::proto::komp_ac::auth::AuthResponse;
|
||||||
|
use canvas::canvas::CanvasState;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ use crate::components::{
|
|||||||
};
|
};
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::modes::general::command_navigation::NavigationState;
|
use crate::modes::general::command_navigation::NavigationState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState; // Keep local one with alias
|
||||||
|
use canvas::canvas::CanvasState; // Import external library's CanvasState trait
|
||||||
use crate::state::app::buffer::BufferState;
|
use crate::state::app::buffer::BufferState;
|
||||||
use crate::state::app::highlight::HighlightState as LocalHighlightState; // CHANGED: Alias local version
|
use crate::state::app::highlight::HighlightState as LocalHighlightState; // CHANGED: Alias local version
|
||||||
use canvas::canvas::HighlightState as CanvasHighlightState; // CHANGED: Import canvas version with alias
|
use canvas::canvas::HighlightState as CanvasHighlightState; // CHANGED: Import canvas version with alias
|
||||||
@@ -136,7 +137,7 @@ pub fn render_ui(
|
|||||||
theme,
|
theme,
|
||||||
register_state,
|
register_state,
|
||||||
app_state,
|
app_state,
|
||||||
register_state.current_field() < 4,
|
register_state.current_field() < 4, // Now using CanvasState trait method
|
||||||
highlight_state, // Uses local version
|
highlight_state, // Uses local version
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
@@ -166,7 +167,7 @@ pub fn render_ui(
|
|||||||
theme,
|
theme,
|
||||||
login_state,
|
login_state,
|
||||||
app_state,
|
app_state,
|
||||||
login_state.current_field() < 2,
|
login_state.current_field() < 2, // Now using CanvasState trait method
|
||||||
highlight_state, // Uses local version
|
highlight_state, // Uses local version
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_admin {
|
} else if app_state.ui.show_admin {
|
||||||
@@ -208,7 +209,7 @@ pub fn render_ui(
|
|||||||
])
|
])
|
||||||
.split(form_actual_area)[1]
|
.split(form_actual_area)[1]
|
||||||
};
|
};
|
||||||
|
|
||||||
// CHANGED: Convert local HighlightState to canvas HighlightState for FormState
|
// CHANGED: Convert local HighlightState to canvas HighlightState for FormState
|
||||||
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
||||||
form_state.render(
|
form_state.render(
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use crate::config::storage::storage::load_auth_data;
|
|||||||
use crate::modes::common::commands::CommandHandler;
|
use crate::modes::common::commands::CommandHandler;
|
||||||
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
||||||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState; // Keep local one with alias
|
||||||
|
use canvas::canvas::CanvasState; // Import external library's CanvasState trait
|
||||||
use crate::state::pages::form::{FormState, FieldDefinition}; // Import FieldDefinition
|
use crate::state::pages::form::{FormState, FieldDefinition}; // Import FieldDefinition
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::pages::auth::LoginState;
|
use crate::state::pages::auth::LoginState;
|
||||||
@@ -38,6 +39,7 @@ use crate::state::app::state::DebugState;
|
|||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
use crate::utils::debug_logger::pop_next_debug_message;
|
use crate::utils::debug_logger::pop_next_debug_message;
|
||||||
|
|
||||||
|
// Rest of the file remains the same...
|
||||||
pub async fn run_ui() -> Result<()> {
|
pub async fn run_ui() -> Result<()> {
|
||||||
let config = Config::load().context("Failed to load configuration")?;
|
let config = Config::load().context("Failed to load configuration")?;
|
||||||
let theme = Theme::from_str(&config.colors.theme);
|
let theme = Theme::from_str(&config.colors.theme);
|
||||||
@@ -346,25 +348,25 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue with the rest of the function...
|
||||||
|
// (The rest remains the same, but now CanvasState trait methods are available)
|
||||||
|
|
||||||
if app_state.ui.show_form {
|
if app_state.ui.show_form {
|
||||||
let current_view_profile = app_state.current_view_profile_name.clone();
|
let current_view_profile = app_state.current_view_profile_name.clone();
|
||||||
let current_view_table = app_state.current_view_table_name.clone();
|
let current_view_table = app_state.current_view_table_name.clone();
|
||||||
|
|
||||||
// This condition correctly detects a table switch.
|
|
||||||
if prev_view_profile_name != current_view_profile
|
if prev_view_profile_name != current_view_profile
|
||||||
|| prev_view_table_name != current_view_table
|
|| prev_view_table_name != current_view_table
|
||||||
{
|
{
|
||||||
if let (Some(prof_name), Some(tbl_name)) =
|
if let (Some(prof_name), Some(tbl_name)) =
|
||||||
(current_view_profile.as_ref(), current_view_table.as_ref())
|
(current_view_profile.as_ref(), current_view_table.as_ref())
|
||||||
{
|
{
|
||||||
// --- START OF REFACTORED LOGIC ---
|
|
||||||
app_state.show_loading_dialog(
|
app_state.show_loading_dialog(
|
||||||
"Loading Table",
|
"Loading Table",
|
||||||
&format!("Fetching data for {}.{}...", prof_name, tbl_name),
|
&format!("Fetching data for {}.{}...", prof_name, tbl_name),
|
||||||
);
|
);
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
|
|
||||||
// 1. Call our new, central function. It handles fetching AND caching.
|
|
||||||
match UiService::load_table_view(
|
match UiService::load_table_view(
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
&mut app_state,
|
&mut app_state,
|
||||||
@@ -374,72 +376,62 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(mut new_form_state) => {
|
Ok(mut new_form_state) => {
|
||||||
// 2. The function succeeded, we have a new FormState.
|
|
||||||
// Now, fetch its data.
|
|
||||||
if let Err(e) = UiService::fetch_and_set_table_count(
|
if let Err(e) = UiService::fetch_and_set_table_count(
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
&mut new_form_state,
|
&mut new_form_state,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// Handle count fetching error
|
|
||||||
app_state.update_dialog_content(
|
app_state.update_dialog_content(
|
||||||
&format!("Error fetching count: {}", e),
|
&format!("Error fetching count: {}", e),
|
||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginFailed, // Or a more appropriate purpose
|
DialogPurpose::LoginFailed,
|
||||||
);
|
);
|
||||||
} else if new_form_state.total_count > 0 {
|
} else if new_form_state.total_count > 0 {
|
||||||
// If there are records, load the first/last one
|
|
||||||
if let Err(e) = UiService::load_table_data_by_position(
|
if let Err(e) = UiService::load_table_data_by_position(
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
&mut new_form_state,
|
&mut new_form_state,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// Handle data loading error
|
|
||||||
app_state.update_dialog_content(
|
app_state.update_dialog_content(
|
||||||
&format!("Error loading data: {}", e),
|
&format!("Error loading data: {}", e),
|
||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginFailed, // Or a more appropriate purpose
|
DialogPurpose::LoginFailed,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Success! Hide the loading dialog.
|
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No records, so just reset to an empty form.
|
|
||||||
new_form_state.reset_to_empty();
|
new_form_state.reset_to_empty();
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. CRITICAL: Replace the old form_state with the new one.
|
|
||||||
form_state = new_form_state;
|
form_state = new_form_state;
|
||||||
|
|
||||||
// 4. Update our tracking variables.
|
|
||||||
prev_view_profile_name = current_view_profile;
|
prev_view_profile_name = current_view_profile;
|
||||||
prev_view_table_name = current_view_table;
|
prev_view_table_name = current_view_table;
|
||||||
table_just_switched = true;
|
table_just_switched = true;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// This handles errors from load_table_view (e.g., schema fetch failed)
|
|
||||||
app_state.update_dialog_content(
|
app_state.update_dialog_content(
|
||||||
&format!("Error loading table: {}", e),
|
&format!("Error loading table: {}", e),
|
||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginFailed, // Or a more appropriate purpose
|
DialogPurpose::LoginFailed,
|
||||||
);
|
);
|
||||||
// Revert the view change in app_state to avoid a loop
|
|
||||||
app_state.current_view_profile_name =
|
app_state.current_view_profile_name =
|
||||||
prev_view_profile_name.clone();
|
prev_view_profile_name.clone();
|
||||||
app_state.current_view_table_name =
|
app_state.current_view_table_name =
|
||||||
prev_view_table_name.clone();
|
prev_view_table_name.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END OF REFACTORED LOGIC ---
|
|
||||||
}
|
}
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue with the rest of the positioning logic...
|
||||||
|
// Now we can use CanvasState methods like get_current_input(), current_field(), etc.
|
||||||
|
|
||||||
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
||||||
if app_state.ui.show_add_logic {
|
if app_state.ui.show_add_logic {
|
||||||
if admin_state.add_logic_state.profile_name == profile_name &&
|
if admin_state.add_logic_state.profile_name == profile_name &&
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use sqlx::{PgPool, Transaction, Postgres};
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use common::proto::komp_ac::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
|
use common::proto::komp_ac::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
|
||||||
|
|
||||||
|
// TODO CRITICAL add decimal with optional precision"
|
||||||
const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
|
const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
|
||||||
("text", "TEXT"),
|
("text", "TEXT"),
|
||||||
("string", "TEXT"),
|
("string", "TEXT"),
|
||||||
|
|||||||
Reference in New Issue
Block a user