add logic is now using canvas library now

This commit is contained in:
filipriec
2025-08-30 21:10:10 +02:00
parent c7d524c76a
commit 4edec5e72d
9 changed files with 426 additions and 600 deletions

View File

@@ -321,6 +321,18 @@ impl EventHandler {
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} else if let Page::AddLogic(add_logic_page) = &mut router.current {
let outcome = add_logic::event::handle_add_logic_event(
event,
config,
app_state,
add_logic_page,
self.grpc_client.clone(),
self.save_logic_result_sender.clone(),
)?;
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} else if let Page::Form(path) = &router.current {
let outcome = forms::event::handle_form_event(
event,
@@ -469,24 +481,6 @@ impl EventHandler {
}
}
// Optional page-specific handlers (non-movement or rich actions)
let client_clone = self.grpc_client.clone();
let sender_clone = self.save_logic_result_sender.clone();
if add_logic::nav::handle_add_logic_navigation(
key_event,
config,
app_state,
buffer_state,
client_clone,
sender_clone,
&mut self.command_message,
router,
) {
return Ok(EventOutcome::Ok(
self.command_message.clone(),
));
}
if let Page::AddTable(add_table_state) = &mut router.current {
let client_clone = self.grpc_client.clone();
let sender_clone = self.save_table_result_sender.clone();

View File

@@ -0,0 +1,199 @@
// src/pages/admin_panel/add_logic/event.rs
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyModifiers};
use crate::config::binds::config::Config;
use crate::modes::handlers::event::EventOutcome;
use crate::pages::admin_panel::add_logic::state::{AddLogicFormState, AddLogicFocus};
use crate::components::common::text_editor::TextEditor;
use crate::services::grpc_client::GrpcClient;
use crate::pages::admin_panel::add_logic::nav::SaveLogicResultSender; // keep type alias
use crate::state::app::state::AppState;
use canvas::DataProvider;
pub fn handle_add_logic_event(
event: Event,
config: &Config,
app_state: &mut AppState,
add_logic_page: &mut AddLogicFormState,
grpc_client: GrpcClient,
save_logic_sender: SaveLogicResultSender,
) -> Result<EventOutcome> {
if let Event::Key(key_event) = event {
let st = &mut add_logic_page.state;
let key_code = key_event.code;
let modifiers = key_event.modifiers;
// 1) Fullscreen Script Editor mode
if st.current_focus == AddLogicFocus::InsideScriptContent {
match key_code {
KeyCode::Esc if modifiers == KeyModifiers::NONE => {
st.current_focus = AddLogicFocus::ScriptContentPreview;
app_state.ui.focus_outside_canvas = true;
return Ok(EventOutcome::Ok("Exited script editing.".into()));
}
_ => {
let changed = {
let mut editor_borrow = st.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&st.editor_keybinding_mode,
&mut st.vim_state,
)
};
if changed {
st.has_unsaved_changes = true;
return Ok(EventOutcome::Ok("Script updated".into()));
} else {
return Ok(EventOutcome::Ok(String::new()));
}
}
}
}
// 2) Canvas inputs (three fields) forward to FormEditor
let inside_canvas_inputs = matches!(
st.current_focus,
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription
);
if inside_canvas_inputs {
// Handoff from last field to Script Preview on "down"/"next"
let last_idx = add_logic_page
.editor
.data_provider()
.field_count()
.saturating_sub(1);
let at_last = add_logic_page.editor.current_field() >= last_idx;
// Map to generic "down/next" via config or raw keys
let action_opt = config.get_general_action(key_code, modifiers);
let is_down_or_next = matches!(
action_opt,
Some("down") | Some("next") | Some("move_down") | Some("next_field")
)
|| matches!(key_code, KeyCode::Down)
|| matches!(key_code, KeyCode::Char('j') if modifiers.is_empty());
if at_last && is_down_or_next {
st.last_canvas_field = last_idx;
st.current_focus = AddLogicFocus::ScriptContentPreview;
add_logic_page.focus_outside_canvas = true;
app_state.ui.focus_outside_canvas = true;
return Ok(EventOutcome::Ok(
"Moved to Script Preview".to_string(),
));
}
// Normal canvas input
match add_logic_page.editor.handle_key_event(key_event) {
canvas::keymap::KeyEventOutcome::Consumed(Some(msg)) => {
add_logic_page.sync_from_editor();
return Ok(EventOutcome::Ok(msg));
}
canvas::keymap::KeyEventOutcome::Consumed(None) => {
add_logic_page.sync_from_editor();
return Ok(EventOutcome::Ok("Input updated".into()));
}
canvas::keymap::KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok(String::new()));
}
canvas::keymap::KeyEventOutcome::NotMatched => {
// fall through to outside handling
}
}
}
// 3) Outside-canvas focus (Script Preview, Save, Cancel)
let action_opt = config.get_general_action(key_code, modifiers);
match action_opt {
Some("up") | Some("move_up") | Some("previous") | Some("previous_option") | Some("prev_field") => {
match st.current_focus {
AddLogicFocus::ScriptContentPreview => {
st.current_focus = AddLogicFocus::InputDescription;
add_logic_page.focus_outside_canvas = false;
app_state.ui.focus_outside_canvas = false;
return Ok(EventOutcome::Ok(
"Back to Description".to_string(),
));
}
AddLogicFocus::SaveButton => {
st.current_focus = AddLogicFocus::ScriptContentPreview;
return Ok(EventOutcome::Ok(
"Back to Script Preview".to_string(),
));
}
AddLogicFocus::CancelButton => {
st.current_focus = AddLogicFocus::SaveButton;
return Ok(EventOutcome::Ok("Back to Save".to_string()));
}
_ => {}
}
}
Some("down") | Some("move_down") | Some("next") | Some("next_option") | Some("next_field") => {
match st.current_focus {
AddLogicFocus::ScriptContentPreview => {
st.current_focus = AddLogicFocus::SaveButton;
return Ok(EventOutcome::Ok("Focus: Save".to_string()));
}
AddLogicFocus::SaveButton => {
st.current_focus = AddLogicFocus::CancelButton;
return Ok(EventOutcome::Ok("Focus: Cancel".to_string()));
}
_ => {}
}
}
Some("select") => {
match st.current_focus {
AddLogicFocus::ScriptContentPreview => {
st.current_focus = AddLogicFocus::InsideScriptContent;
add_logic_page.focus_outside_canvas = false;
app_state.ui.focus_outside_canvas = false;
return Ok(EventOutcome::Ok(
"Fullscreen script editing. Esc to exit.".into(),
));
}
AddLogicFocus::SaveButton => {
if let Some(msg) = st.save_logic() {
return Ok(EventOutcome::Ok(msg));
}
return Ok(EventOutcome::Ok(
"Saved (no changes)".to_string(),
));
}
AddLogicFocus::CancelButton => {
// Keep this simple: you can wire buffer/view navigation where needed
return Ok(EventOutcome::Ok(
"Cancelled Add Logic".to_string(),
));
}
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription => {
add_logic_page.focus_outside_canvas = false;
app_state.ui.focus_outside_canvas = false;
return Ok(EventOutcome::Ok(String::new()));
}
_ => {}
}
}
Some("esc") => {
if st.current_focus == AddLogicFocus::ScriptContentPreview {
// Go back to Description (last canvas field)
st.current_focus = AddLogicFocus::InputDescription;
add_logic_page.focus_outside_canvas = false;
app_state.ui.focus_outside_canvas = false;
return Ok(EventOutcome::Ok(
"Back to Description".to_string(),
));
}
}
_ => {}
}
}
Ok(EventOutcome::Ok(String::new()))
}

View File

@@ -2,7 +2,7 @@
use anyhow::{Context, Result};
use tracing::{error, info, warn};
use crate::pages::admin_panel::add_logic::state::AddLogicState;
use crate::pages::admin_panel::add_logic::state::AddLogicFormState;
use crate::pages::routing::{Page, Router};
use crate::services::grpc_client::GrpcClient;
use crate::services::ui_service::UiService;
@@ -19,9 +19,9 @@ pub async fn process_pending_table_structure_fetch(
let mut needs_redraw = false;
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
if let Page::AddLogic(state) = &mut router.current {
if state.profile_name == profile_name
&& state.selected_table_name.as_deref() == Some(table_name.as_str())
if let Page::AddLogic(page) = &mut router.current {
if page.profile_name() == profile_name
&& page.selected_table_name().map(|s| s.as_str()) == Some(table_name.as_str())
{
info!(
"Fetching table structure for {}.{}",
@@ -30,7 +30,7 @@ pub async fn process_pending_table_structure_fetch(
let fetch_message = UiService::initialize_add_logic_table_data(
grpc_client,
state,
&mut page.state, // keep state here, UiService expects AddLogicState
&app_state.profile_tree,
)
.await
@@ -53,7 +53,10 @@ pub async fn process_pending_table_structure_fetch(
error!(
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, \
but AddLogic state is for {}.{:?}",
profile_name, table_name, state.profile_name, state.selected_table_name
profile_name,
table_name,
page.profile_name(),
page.selected_table_name()
);
}
} else {
@@ -71,14 +74,15 @@ pub async fn process_pending_table_structure_fetch(
/// fetch them and update the state. Returns true if UI needs a redraw.
pub async fn maybe_fetch_columns_for_awaiting_table(
grpc_client: &mut GrpcClient,
state: &mut AddLogicState,
page: &mut AddLogicFormState,
command_message: &mut String,
) -> Result<bool> {
if let Some(table_name) = state
if let Some(table_name) = page
.state
.script_editor_awaiting_column_autocomplete
.clone()
{
let profile_name = state.profile_name.clone();
let profile_name = page.state.profile_name.clone();
info!(
"Fetching columns for table selection: {}.{}",
@@ -86,7 +90,7 @@ pub async fn maybe_fetch_columns_for_awaiting_table(
);
match UiService::fetch_columns_for_table(grpc_client, &profile_name, &table_name).await {
Ok(columns) => {
state.set_columns_for_table_autocomplete(columns.clone());
page.state.set_columns_for_table_autocomplete(columns.clone());
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
*command_message =
format!("Columns for '{}' loaded. Select a column.", table_name);
@@ -96,8 +100,8 @@ pub async fn maybe_fetch_columns_for_awaiting_table(
"Failed to fetch columns for {}.{}: {}",
profile_name, table_name, e
);
state.script_editor_awaiting_column_autocomplete = None;
state.deactivate_script_editor_autocomplete();
page.state.script_editor_awaiting_column_autocomplete = None;
page.state.deactivate_script_editor_autocomplete();
*command_message = format!("Error loading columns for '{}': {}", table_name, e);
}
}

View File

@@ -4,3 +4,4 @@ pub mod ui;
pub mod nav;
pub mod state;
pub mod loader;
pub mod event;

View File

@@ -1,531 +1,6 @@
// src/pages/admin_panel/add_logic/nav.rs
use crate::config::binds::config::{Config, EditorKeybindingMode};
use crate::state::app::state::AppState;
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState};
use crate::buffer::{AppView, BufferState};
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use crate::services::GrpcClient;
use tokio::sync::mpsc;
use anyhow::Result;
use crate::components::common::text_editor::TextEditor;
use crate::services::ui_service::UiService;
use tui_textarea::CursorMove;
use crate::pages::admin::AdminState;
use crate::pages::routing::{Router, Page};
use tokio::sync::mpsc;
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
pub fn handle_add_logic_navigation(
key_event: KeyEvent,
config: &Config,
app_state: &mut AppState,
buffer_state: &mut BufferState,
grpc_client: GrpcClient,
save_logic_sender: SaveLogicResultSender,
command_message: &mut String,
router: &mut Router,
) -> bool {
if let Page::AddLogic(add_logic_state) = &mut router.current {
// === FULLSCREEN SCRIPT EDITING ===
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
// === AUTOCOMPLETE HANDLING ===
if add_logic_state.script_editor_autocomplete_active {
match key_event.code {
KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => {
add_logic_state.script_editor_filter_text.push(c);
add_logic_state.update_script_editor_suggestions();
{
let mut editor_borrow =
add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
*command_message =
format!("Filtering: @{}", add_logic_state.script_editor_filter_text);
return true;
}
KeyCode::Backspace => {
if !add_logic_state.script_editor_filter_text.is_empty() {
add_logic_state.script_editor_filter_text.pop();
add_logic_state.update_script_editor_suggestions();
{
let mut editor_borrow =
add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
*command_message =
if add_logic_state.script_editor_filter_text.is_empty() {
"Autocomplete: @".to_string()
} else {
format!(
"Filtering: @{}",
add_logic_state.script_editor_filter_text
)
};
} else {
let should_deactivate =
if let Some((trigger_line, trigger_col)) =
add_logic_state.script_editor_trigger_position
{
let current_cursor = {
let editor_borrow =
add_logic_state.script_content_editor.borrow();
editor_borrow.cursor()
};
current_cursor.0 == trigger_line
&& current_cursor.1 == trigger_col + 1
} else {
false
};
if should_deactivate {
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
}
{
let mut editor_borrow =
add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
}
return true;
}
KeyCode::Tab | KeyCode::Down => {
if !add_logic_state.script_editor_suggestions.is_empty() {
let current = add_logic_state
.script_editor_selected_suggestion_index
.unwrap_or(0);
let next =
(current + 1) % add_logic_state.script_editor_suggestions.len();
add_logic_state.script_editor_selected_suggestion_index = Some(next);
*command_message = format!(
"Selected: {}",
add_logic_state.script_editor_suggestions[next]
);
}
return true;
}
KeyCode::Up => {
if !add_logic_state.script_editor_suggestions.is_empty() {
let current = add_logic_state
.script_editor_selected_suggestion_index
.unwrap_or(0);
let prev = if current == 0 {
add_logic_state.script_editor_suggestions.len() - 1
} else {
current - 1
};
add_logic_state.script_editor_selected_suggestion_index = Some(prev);
*command_message = format!(
"Selected: {}",
add_logic_state.script_editor_suggestions[prev]
);
}
return true;
}
KeyCode::Enter => {
if let Some(selected_idx) =
add_logic_state.script_editor_selected_suggestion_index
{
if let Some(suggestion) = add_logic_state
.script_editor_suggestions
.get(selected_idx)
.cloned()
{
let trigger_pos =
add_logic_state.script_editor_trigger_position;
let filter_len =
add_logic_state.script_editor_filter_text.len();
add_logic_state.deactivate_script_editor_autocomplete();
add_logic_state.has_unsaved_changes = true;
if let Some(pos) = trigger_pos {
let mut editor_borrow =
add_logic_state.script_content_editor.borrow_mut();
if suggestion == "sql" {
replace_autocomplete_text(
&mut editor_borrow,
pos,
filter_len,
"sql",
);
editor_borrow.insert_str("('')");
editor_borrow.move_cursor(CursorMove::Back);
editor_borrow.move_cursor(CursorMove::Back);
*command_message = "Inserted: @sql('')".to_string();
} else {
let is_table_selection =
add_logic_state.is_table_name_suggestion(&suggestion);
replace_autocomplete_text(
&mut editor_borrow,
pos,
filter_len,
&suggestion,
);
if is_table_selection {
editor_borrow.insert_str(".");
let new_cursor = editor_borrow.cursor();
drop(editor_borrow);
add_logic_state.script_editor_trigger_position =
Some(new_cursor);
add_logic_state.script_editor_autocomplete_active = true;
add_logic_state.script_editor_filter_text.clear();
add_logic_state
.trigger_column_autocomplete_for_table(
suggestion.clone(),
);
let profile_name =
add_logic_state.profile_name.clone();
let table_name_for_fetch = suggestion.clone();
let mut client_clone = grpc_client.clone();
tokio::spawn(async move {
if let Err(e) = UiService::fetch_columns_for_table(
&mut client_clone,
&profile_name,
&table_name_for_fetch,
)
.await
{
tracing::error!(
"Failed to fetch columns for {}.{}: {}",
profile_name,
table_name_for_fetch,
e
);
}
});
*command_message = format!(
"Selected table '{}', fetching columns...",
suggestion
);
} else {
*command_message =
format!("Inserted: {}", suggestion);
}
}
}
return true;
}
}
add_logic_state.deactivate_script_editor_autocomplete();
{
let mut editor_borrow =
add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
return true;
}
KeyCode::Esc => {
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
}
_ => {
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
{
let mut editor_borrow =
add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
return true;
}
}
}
// Trigger autocomplete with '@'
if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE {
let should_trigger = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
TextEditor::is_vim_insert_mode(&add_logic_state.vim_state)
}
_ => true,
};
if should_trigger {
let cursor_before = {
let editor_borrow = add_logic_state.script_content_editor.borrow();
editor_borrow.cursor()
};
{
let mut editor_borrow =
add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
add_logic_state.script_editor_trigger_position = Some(cursor_before);
add_logic_state.script_editor_autocomplete_active = true;
add_logic_state.script_editor_filter_text.clear();
add_logic_state.update_script_editor_suggestions();
add_logic_state.has_unsaved_changes = true;
*command_message = "Autocomplete: @ (Tab/↑↓ to navigate, Enter to select, Esc to cancel)".to_string();
return true;
}
}
// Esc handling
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
let was_insert =
TextEditor::is_vim_insert_mode(&add_logic_state.vim_state);
{
let mut editor_borrow =
add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
if was_insert {
*command_message =
"VIM: Normal Mode. Esc again to exit script.".to_string();
} else {
add_logic_state.current_focus =
AddLogicFocus::ScriptContentPreview;
app_state.ui.focus_outside_canvas = true;
*command_message = "Exited script editing.".to_string();
}
}
_ => {
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
app_state.ui.focus_outside_canvas = true;
*command_message = "Exited script editing.".to_string();
}
}
return true;
}
// Normal text input
let changed = {
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
)
};
if changed {
add_logic_state.has_unsaved_changes = true;
}
return true;
}
// === NON-FULLSCREEN NAVIGATION ===
let action = config.get_general_action(key_event.code, key_event.modifiers);
let current_focus = add_logic_state.current_focus;
let mut handled = true;
let mut new_focus = current_focus;
match action.as_deref() {
Some("exit_table_scroll") => {
handled = false;
}
Some("move_up") => {
match current_focus {
AddLogicFocus::InputLogicName => {}
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
AddLogicFocus::InputDescription => {
new_focus = AddLogicFocus::InputTargetColumn
}
AddLogicFocus::ScriptContentPreview => {
new_focus = AddLogicFocus::InputDescription
}
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
_ => handled = false,
}
}
Some("move_down") => {
match current_focus {
AddLogicFocus::InputLogicName => {
new_focus = AddLogicFocus::InputTargetColumn
}
AddLogicFocus::InputTargetColumn => {
new_focus = AddLogicFocus::InputDescription
}
AddLogicFocus::InputDescription => {
add_logic_state.last_canvas_field = 2;
new_focus = AddLogicFocus::ScriptContentPreview;
}
AddLogicFocus::ScriptContentPreview => {
new_focus = AddLogicFocus::SaveButton
}
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => {}
_ => handled = false,
}
}
Some("next_option") => {
match current_focus {
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription => {
new_focus = AddLogicFocus::ScriptContentPreview
}
AddLogicFocus::ScriptContentPreview => {
new_focus = AddLogicFocus::SaveButton
}
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => {}
_ => handled = false,
}
}
Some("previous_option") => {
match current_focus {
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription => {}
AddLogicFocus::ScriptContentPreview => {
new_focus = AddLogicFocus::InputDescription
}
AddLogicFocus::SaveButton => {
new_focus = AddLogicFocus::ScriptContentPreview
}
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
_ => handled = false,
}
}
Some("next_field") => {
new_focus = match current_focus {
AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn,
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription,
AddLogicFocus::InputDescription => AddLogicFocus::ScriptContentPreview,
AddLogicFocus::ScriptContentPreview => AddLogicFocus::SaveButton,
AddLogicFocus::SaveButton => AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName,
_ => current_focus,
};
}
Some("prev_field") => {
new_focus = match current_focus {
AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton,
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName,
AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn,
AddLogicFocus::ScriptContentPreview => AddLogicFocus::InputDescription,
AddLogicFocus::SaveButton => AddLogicFocus::ScriptContentPreview,
AddLogicFocus::CancelButton => AddLogicFocus::SaveButton,
_ => current_focus,
};
}
Some("select") => {
match current_focus {
AddLogicFocus::ScriptContentPreview => {
new_focus = AddLogicFocus::InsideScriptContent;
app_state.ui.focus_outside_canvas = false;
let mode_hint = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
"VIM mode - 'i'/'a'/'o' to edit"
}
_ => "Enter/Ctrl+E to edit",
};
*command_message = format!(
"Fullscreen script editing. {} or Esc to exit.",
mode_hint
);
handled = true;
}
AddLogicFocus::SaveButton => {
*command_message = "Save logic action".to_string();
handled = true;
}
AddLogicFocus::CancelButton => {
buffer_state.update_history(AppView::Admin);
*command_message = "Cancelled Add Logic".to_string();
handled = true;
}
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription => {
// Focus canvas inputs; let canvas keymap handle editing
app_state.ui.focus_outside_canvas = false;
handled = false; // forward to canvas
}
_ => handled = false,
}
}
Some("toggle_edit_mode") => {
match current_focus {
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription => {
app_state.ui.focus_outside_canvas = false;
*command_message =
"Focus moved to input. Use i/a (Vim) or type to edit.".to_string();
handled = true;
}
_ => {
*command_message = "Cannot toggle edit mode here.".to_string();
}
}
}
_ => handled = false,
}
if handled && current_focus != new_focus {
add_logic_state.current_focus = new_focus;
let new_is_canvas_input_focus = matches!(
new_focus,
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription
);
if new_is_canvas_input_focus {
app_state.ui.focus_outside_canvas = false;
} else {
app_state.ui.focus_outside_canvas = true;
}
}
handled
} else {
false
}
}
fn replace_autocomplete_text(
editor: &mut tui_textarea::TextArea,
trigger_pos: (usize, usize),
filter_len: usize,
replacement: &str,
) {
let filter_start_pos = (trigger_pos.0, trigger_pos.1 + 1);
editor.move_cursor(CursorMove::Jump(filter_start_pos.0 as u16, filter_start_pos.1 as u16));
for _ in 0..filter_len {
editor.delete_next_char();
}
editor.insert_str(replacement);
}

View File

@@ -1,7 +1,7 @@
// src/pages/admin_panel/add_logic/state.rs
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
use crate::components::common::text_editor::{TextEditor, VimState};
use canvas::{DataProvider, AppMode};
use canvas::{DataProvider, AppMode, FormEditor};
use std::cell::RefCell;
use std::rc::Rc;
use tui_textarea::TextArea;
@@ -315,3 +315,141 @@ impl DataProvider for AddLogicState {
field_index == 1
}
}
// Wrapper that owns both the raw state and its FormEditor (like LoginFormState)
pub struct AddLogicFormState {
pub state: AddLogicState,
pub editor: FormEditor<AddLogicState>,
pub focus_outside_canvas: bool,
}
// manual Debug because FormEditor may not implement Debug
impl std::fmt::Debug for AddLogicFormState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AddLogicFormState")
.field("state", &self.state)
.field("focus_outside_canvas", &self.focus_outside_canvas)
.finish()
}
}
impl AddLogicFormState {
pub fn new(editor_config: &EditorConfig) -> Self {
let state = AddLogicState::new(editor_config);
let editor = FormEditor::new(state.clone());
Self {
state,
editor,
focus_outside_canvas: false,
}
}
pub fn from_state(state: AddLogicState) -> Self {
let editor = FormEditor::new(state.clone());
Self {
state,
editor,
focus_outside_canvas: false,
}
}
/// Sync state from editor's data provider snapshot
pub fn sync_from_editor(&mut self) {
self.state = self.editor.data_provider().clone();
}
// === Delegates to AddLogicState fields ===
pub fn current_focus(&self) -> AddLogicFocus {
self.state.current_focus
}
pub fn set_current_focus(&mut self, focus: AddLogicFocus) {
self.state.current_focus = focus;
}
pub fn has_unsaved_changes(&self) -> bool {
self.state.has_unsaved_changes
}
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
self.state.has_unsaved_changes = changed;
}
pub fn profile_name(&self) -> &str {
&self.state.profile_name
}
pub fn selected_table_name(&self) -> Option<&String> {
self.state.selected_table_name.as_ref()
}
pub fn selected_table_id(&self) -> Option<i64> {
self.state.selected_table_id
}
pub fn script_content_editor(&self) -> &Rc<RefCell<TextArea<'static>>> {
&self.state.script_content_editor
}
pub fn script_content_editor_mut(&mut self) -> &mut Rc<RefCell<TextArea<'static>>> {
&mut self.state.script_content_editor
}
pub fn vim_state(&self) -> &VimState {
&self.state.vim_state
}
pub fn vim_state_mut(&mut self) -> &mut VimState {
&mut self.state.vim_state
}
pub fn editor_keybinding_mode(&self) -> &EditorKeybindingMode {
&self.state.editor_keybinding_mode
}
pub fn script_editor_autocomplete_active(&self) -> bool {
self.state.script_editor_autocomplete_active
}
pub fn script_editor_suggestions(&self) -> &Vec<String> {
&self.state.script_editor_suggestions
}
pub fn script_editor_selected_suggestion_index(&self) -> Option<usize> {
self.state.script_editor_selected_suggestion_index
}
pub fn target_column_suggestions(&self) -> &Vec<String> {
&self.state.target_column_suggestions
}
pub fn selected_target_column_suggestion_index(&self) -> Option<usize> {
self.state.selected_target_column_suggestion_index
}
pub fn in_target_column_suggestion_mode(&self) -> bool {
self.state.in_target_column_suggestion_mode
}
pub fn show_target_column_suggestions(&self) -> bool {
self.state.show_target_column_suggestions
}
// === Delegates to FormEditor ===
pub fn mode(&self) -> AppMode {
self.editor.mode()
}
pub fn cursor_position(&self) -> usize {
self.editor.cursor_position()
}
pub fn handle_key_event(
&mut self,
key_event: crossterm::event::KeyEvent,
) -> canvas::keymap::KeyEventOutcome {
self.editor.handle_key_event(key_event)
}
}

View File

@@ -1,7 +1,7 @@
// src/pages/admin_panel/add_logic/ui.rs
use crate::config::colors::themes::Theme;
use crate::state::app::state::AppState;
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState};
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState, AddLogicFormState};
use canvas::{render_canvas, FormEditor};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
@@ -19,7 +19,7 @@ pub fn render_add_logic(
area: Rect,
theme: &Theme,
app_state: &AppState,
add_logic_state: &mut AddLogicState,
add_logic_state: &mut AddLogicFormState,
) {
let main_block = Block::default()
.title(" Add New Logic Script ")
@@ -32,9 +32,13 @@ pub fn render_add_logic(
f.render_widget(main_block, area);
// Handle full-screen script editing
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
let border_style_color = if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) {
if add_logic_state.current_focus() == AddLogicFocus::InsideScriptContent {
let mut editor_ref = add_logic_state
.state
.script_content_editor
.borrow_mut();
let border_style_color = if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(add_logic_state.vim_state()) {
theme.highlight
} else {
theme.secondary
@@ -44,13 +48,13 @@ pub fn render_add_logic(
editor_ref.set_cursor_line_style(Style::default());
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let script_title_hint = match add_logic_state.editor_keybinding_mode {
let script_title_hint = match add_logic_state.editor_keybinding_mode() {
EditorKeybindingMode::Vim => {
let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(&add_logic_state.vim_state);
let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(add_logic_state.vim_state());
format!("Script {}", vim_mode_status)
}
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) {
if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(add_logic_state.vim_state()) {
"Script (Editing)".to_string()
} else {
"Script".to_string()
@@ -72,10 +76,10 @@ pub fn render_add_logic(
drop(editor_ref);
// === SCRIPT EDITOR AUTOCOMPLETE RENDERING ===
if add_logic_state.script_editor_autocomplete_active && !add_logic_state.script_editor_suggestions.is_empty() {
if add_logic_state.script_editor_autocomplete_active() && !add_logic_state.script_editor_suggestions().is_empty() {
// Get the current cursor position from textarea
let current_cursor = {
let editor_borrow = add_logic_state.script_content_editor.borrow();
let editor_borrow = add_logic_state.script_content_editor().borrow();
editor_borrow.cursor() // Returns (row, col) as (usize, usize)
};
@@ -103,8 +107,8 @@ pub fn render_add_logic(
input_rect,
f.area(), // Full frame area for clamping
theme,
&add_logic_state.script_editor_suggestions,
add_logic_state.script_editor_selected_suggestion_index,
add_logic_state.script_editor_suggestions(),
add_logic_state.script_editor_selected_suggestion_index(),
);
}
@@ -128,21 +132,21 @@ pub fn render_add_logic(
let buttons_area = main_chunks[3];
// Top info
let table_label = if let Some(name) = add_logic_state.selected_table_name() {
name.clone()
} else if let Some(id) = add_logic_state.selected_table_id() {
format!("ID {}", id)
} else {
"Global (Not Selected)".to_string()
};
let profile_text = Paragraph::new(vec![
Line::from(Span::styled(
format!("Profile: {}", add_logic_state.profile_name),
format!("Profile: {}", add_logic_state.profile_name()),
Style::default().fg(theme.fg),
)),
Line::from(Span::styled(
format!(
"Table: {}",
add_logic_state
.selected_table_name
.clone()
.unwrap_or_else(|| add_logic_state.selected_table_id
.map(|id| format!("ID {}", id))
.unwrap_or_else(|| "Global (Not Selected)".to_string()))
),
format!("Table: {}", table_label),
Style::default().fg(theme.fg),
)),
])
@@ -155,27 +159,27 @@ pub fn render_add_logic(
// Canvas - USING CANVAS LIBRARY
let focus_on_canvas_inputs = matches!(
add_logic_state.current_focus,
add_logic_state.current_focus(),
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription
);
let editor = FormEditor::new(add_logic_state.clone());
let active_field_rect = render_canvas(f, canvas_area, &editor, theme);
let editor = &add_logic_state.editor;
let active_field_rect = render_canvas(f, canvas_area, editor, theme);
// --- Render Autocomplete for Target Column ---
if editor.mode() == canvas::AppMode::Edit && editor.current_field() == 1 { // Target Column field
if add_logic_state.in_target_column_suggestion_mode && add_logic_state.show_target_column_suggestions {
if !add_logic_state.target_column_suggestions.is_empty() {
if add_logic_state.in_target_column_suggestion_mode() && add_logic_state.show_target_column_suggestions() {
if !add_logic_state.target_column_suggestions().is_empty() {
if let Some(input_rect) = active_field_rect {
autocomplete::render_autocomplete_dropdown(
f,
input_rect,
f.area(), // Full frame area for clamping
theme,
&add_logic_state.target_column_suggestions,
add_logic_state.selected_target_column_suggestion_index,
add_logic_state.target_column_suggestions(),
add_logic_state.selected_target_column_suggestion_index(),
);
}
}
@@ -184,10 +188,10 @@ pub fn render_add_logic(
// Script content preview
{
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
let mut editor_ref = add_logic_state.script_content_editor().borrow_mut();
editor_ref.set_cursor_line_style(Style::default());
let is_script_preview_focused = add_logic_state.current_focus == AddLogicFocus::ScriptContentPreview;
let is_script_preview_focused = add_logic_state.current_focus() == AddLogicFocus::ScriptContentPreview;
if is_script_preview_focused {
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
@@ -256,7 +260,7 @@ pub fn render_add_logic(
let save_button = Paragraph::new(" Save Logic ")
.style(get_button_style(
AddLogicFocus::SaveButton,
add_logic_state.current_focus,
add_logic_state.current_focus(),
))
.alignment(Alignment::Center)
.block(
@@ -264,7 +268,7 @@ pub fn render_add_logic(
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(get_button_border_style(
add_logic_state.current_focus == AddLogicFocus::SaveButton,
add_logic_state.current_focus() == AddLogicFocus::SaveButton,
theme,
)),
);
@@ -273,7 +277,7 @@ pub fn render_add_logic(
let cancel_button = Paragraph::new(" Cancel ")
.style(get_button_style(
AddLogicFocus::CancelButton,
add_logic_state.current_focus,
add_logic_state.current_focus(),
))
.alignment(Alignment::Center)
.block(
@@ -281,7 +285,7 @@ pub fn render_add_logic(
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(get_button_border_style(
add_logic_state.current_focus == AddLogicFocus::CancelButton,
add_logic_state.current_focus() == AddLogicFocus::CancelButton,
theme,
)),
);

View File

@@ -1,6 +1,6 @@
// src/pages/routing/router.rs
use crate::state::pages::auth::AuthState;
use crate::pages::admin_panel::add_logic::state::AddLogicState;
use crate::pages::admin_panel::add_logic::state::AddLogicFormState;
use crate::pages::admin_panel::add_table::state::AddTableState;
use crate::pages::admin::AdminState;
use crate::pages::forms::FormState;
@@ -14,7 +14,7 @@ pub enum Page {
Login(LoginFormState),
Register(RegisterFormState),
Admin(AdminState),
AddLogic(AddLogicState),
AddLogic(AddLogicFormState),
AddTable(AddTableState),
Form(String),
}

View File

@@ -425,7 +425,18 @@ pub async fn run_ui() -> Result<()> {
router.navigate(Page::Admin(admin_state.clone()));
}
AppView::AddTable => router.navigate(Page::AddTable(admin_state.add_table_state.clone())),
AppView::AddLogic => router.navigate(Page::AddLogic(admin_state.add_logic_state.clone())),
AppView::AddLogic => {
// Create once, like Login/Register
if let Page::AddLogic(_) = &router.current {
// already on page
} else {
let mut page = add_logic::state::AddLogicFormState::from_state(
admin_state.add_logic_state.clone(),
);
page.editor.set_keymap(config.build_canvas_keymap());
router.navigate(Page::AddLogic(page));
}
}
AppView::Form(path) => {
// Keep current_view_* consistent with the active buffer path
if let Some((profile, table)) = path.split_once('/') {