add logic is now using canvas library now
This commit is contained in:
@@ -321,6 +321,18 @@ impl EventHandler {
|
|||||||
if !outcome.get_message_if_ok().is_empty() {
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
return Ok(outcome);
|
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 {
|
} else if let Page::Form(path) = &router.current {
|
||||||
let outcome = forms::event::handle_form_event(
|
let outcome = forms::event::handle_form_event(
|
||||||
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 {
|
if let Page::AddTable(add_table_state) = &mut router.current {
|
||||||
let client_clone = self.grpc_client.clone();
|
let client_clone = self.grpc_client.clone();
|
||||||
let sender_clone = self.save_table_result_sender.clone();
|
let sender_clone = self.save_table_result_sender.clone();
|
||||||
|
|||||||
199
client/src/pages/admin_panel/add_logic/event.rs
Normal file
199
client/src/pages/admin_panel/add_logic/event.rs
Normal 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()))
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use tracing::{error, info, warn};
|
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::pages::routing::{Page, Router};
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::services::ui_service::UiService;
|
use crate::services::ui_service::UiService;
|
||||||
@@ -19,9 +19,9 @@ pub async fn process_pending_table_structure_fetch(
|
|||||||
let mut needs_redraw = false;
|
let mut needs_redraw = false;
|
||||||
|
|
||||||
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 let Page::AddLogic(state) = &mut router.current {
|
if let Page::AddLogic(page) = &mut router.current {
|
||||||
if state.profile_name == profile_name
|
if page.profile_name() == profile_name
|
||||||
&& state.selected_table_name.as_deref() == Some(table_name.as_str())
|
&& page.selected_table_name().map(|s| s.as_str()) == Some(table_name.as_str())
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"Fetching table structure for {}.{}",
|
"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(
|
let fetch_message = UiService::initialize_add_logic_table_data(
|
||||||
grpc_client,
|
grpc_client,
|
||||||
state,
|
&mut page.state, // keep state here, UiService expects AddLogicState
|
||||||
&app_state.profile_tree,
|
&app_state.profile_tree,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -53,7 +53,10 @@ pub async fn process_pending_table_structure_fetch(
|
|||||||
error!(
|
error!(
|
||||||
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, \
|
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, \
|
||||||
but AddLogic state is for {}.{:?}",
|
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 {
|
} 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.
|
/// fetch them and update the state. Returns true if UI needs a redraw.
|
||||||
pub async fn maybe_fetch_columns_for_awaiting_table(
|
pub async fn maybe_fetch_columns_for_awaiting_table(
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
state: &mut AddLogicState,
|
page: &mut AddLogicFormState,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
if let Some(table_name) = state
|
if let Some(table_name) = page
|
||||||
|
.state
|
||||||
.script_editor_awaiting_column_autocomplete
|
.script_editor_awaiting_column_autocomplete
|
||||||
.clone()
|
.clone()
|
||||||
{
|
{
|
||||||
let profile_name = state.profile_name.clone();
|
let profile_name = page.state.profile_name.clone();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Fetching columns for table selection: {}.{}",
|
"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 {
|
match UiService::fetch_columns_for_table(grpc_client, &profile_name, &table_name).await {
|
||||||
Ok(columns) => {
|
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);
|
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
|
||||||
*command_message =
|
*command_message =
|
||||||
format!("Columns for '{}' loaded. Select a column.", table_name);
|
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 {}.{}: {}",
|
"Failed to fetch columns for {}.{}: {}",
|
||||||
profile_name, table_name, e
|
profile_name, table_name, e
|
||||||
);
|
);
|
||||||
state.script_editor_awaiting_column_autocomplete = None;
|
page.state.script_editor_awaiting_column_autocomplete = None;
|
||||||
state.deactivate_script_editor_autocomplete();
|
page.state.deactivate_script_editor_autocomplete();
|
||||||
*command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
*command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ pub mod ui;
|
|||||||
pub mod nav;
|
pub mod nav;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod loader;
|
pub mod loader;
|
||||||
|
pub mod event;
|
||||||
|
|||||||
@@ -1,531 +1,6 @@
|
|||||||
// src/pages/admin_panel/add_logic/nav.rs
|
// 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 anyhow::Result;
|
||||||
use crate::components::common::text_editor::TextEditor;
|
use tokio::sync::mpsc;
|
||||||
use crate::services::ui_service::UiService;
|
|
||||||
use tui_textarea::CursorMove;
|
|
||||||
use crate::pages::admin::AdminState;
|
|
||||||
use crate::pages::routing::{Router, Page};
|
|
||||||
|
|
||||||
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/pages/admin_panel/add_logic/state.rs
|
// src/pages/admin_panel/add_logic/state.rs
|
||||||
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
||||||
use crate::components::common::text_editor::{TextEditor, VimState};
|
use crate::components::common::text_editor::{TextEditor, VimState};
|
||||||
use canvas::{DataProvider, AppMode};
|
use canvas::{DataProvider, AppMode, FormEditor};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
@@ -315,3 +315,141 @@ impl DataProvider for AddLogicState {
|
|||||||
field_index == 1
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/pages/admin_panel/add_logic/ui.rs
|
// src/pages/admin_panel/add_logic/ui.rs
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
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 canvas::{render_canvas, FormEditor};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
@@ -19,7 +19,7 @@ pub fn render_add_logic(
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
add_logic_state: &mut AddLogicState,
|
add_logic_state: &mut AddLogicFormState,
|
||||||
) {
|
) {
|
||||||
let main_block = Block::default()
|
let main_block = Block::default()
|
||||||
.title(" Add New Logic Script ")
|
.title(" Add New Logic Script ")
|
||||||
@@ -32,9 +32,13 @@ pub fn render_add_logic(
|
|||||||
f.render_widget(main_block, area);
|
f.render_widget(main_block, area);
|
||||||
|
|
||||||
// Handle full-screen script editing
|
// Handle full-screen script editing
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
if add_logic_state.current_focus() == AddLogicFocus::InsideScriptContent {
|
||||||
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_ref = add_logic_state
|
||||||
let border_style_color = if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_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
|
theme.highlight
|
||||||
} else {
|
} else {
|
||||||
theme.secondary
|
theme.secondary
|
||||||
@@ -44,13 +48,13 @@ pub fn render_add_logic(
|
|||||||
editor_ref.set_cursor_line_style(Style::default());
|
editor_ref.set_cursor_line_style(Style::default());
|
||||||
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||||
|
|
||||||
let script_title_hint = match add_logic_state.editor_keybinding_mode {
|
let script_title_hint = match add_logic_state.editor_keybinding_mode() {
|
||||||
EditorKeybindingMode::Vim => {
|
EditorKeybindingMode::Vim => {
|
||||||
let vim_mode_status = 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)
|
format!("Script {}", vim_mode_status)
|
||||||
}
|
}
|
||||||
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
|
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()
|
"Script (Editing)".to_string()
|
||||||
} else {
|
} else {
|
||||||
"Script".to_string()
|
"Script".to_string()
|
||||||
@@ -72,10 +76,10 @@ pub fn render_add_logic(
|
|||||||
drop(editor_ref);
|
drop(editor_ref);
|
||||||
|
|
||||||
// === SCRIPT EDITOR AUTOCOMPLETE RENDERING ===
|
// === 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
|
// Get the current cursor position from textarea
|
||||||
let current_cursor = {
|
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)
|
editor_borrow.cursor() // Returns (row, col) as (usize, usize)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,8 +107,8 @@ pub fn render_add_logic(
|
|||||||
input_rect,
|
input_rect,
|
||||||
f.area(), // Full frame area for clamping
|
f.area(), // Full frame area for clamping
|
||||||
theme,
|
theme,
|
||||||
&add_logic_state.script_editor_suggestions,
|
add_logic_state.script_editor_suggestions(),
|
||||||
add_logic_state.script_editor_selected_suggestion_index,
|
add_logic_state.script_editor_selected_suggestion_index(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,21 +132,21 @@ pub fn render_add_logic(
|
|||||||
let buttons_area = main_chunks[3];
|
let buttons_area = main_chunks[3];
|
||||||
|
|
||||||
// Top info
|
// 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![
|
let profile_text = Paragraph::new(vec![
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!("Profile: {}", add_logic_state.profile_name),
|
format!("Profile: {}", add_logic_state.profile_name()),
|
||||||
Style::default().fg(theme.fg),
|
Style::default().fg(theme.fg),
|
||||||
)),
|
)),
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!(
|
format!("Table: {}", table_label),
|
||||||
"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()))
|
|
||||||
),
|
|
||||||
Style::default().fg(theme.fg),
|
Style::default().fg(theme.fg),
|
||||||
)),
|
)),
|
||||||
])
|
])
|
||||||
@@ -155,27 +159,27 @@ pub fn render_add_logic(
|
|||||||
|
|
||||||
// Canvas - USING CANVAS LIBRARY
|
// Canvas - USING CANVAS LIBRARY
|
||||||
let focus_on_canvas_inputs = matches!(
|
let focus_on_canvas_inputs = matches!(
|
||||||
add_logic_state.current_focus,
|
add_logic_state.current_focus(),
|
||||||
AddLogicFocus::InputLogicName
|
AddLogicFocus::InputLogicName
|
||||||
| AddLogicFocus::InputTargetColumn
|
| AddLogicFocus::InputTargetColumn
|
||||||
| AddLogicFocus::InputDescription
|
| AddLogicFocus::InputDescription
|
||||||
);
|
);
|
||||||
|
|
||||||
let editor = FormEditor::new(add_logic_state.clone());
|
let editor = &add_logic_state.editor;
|
||||||
let active_field_rect = render_canvas(f, canvas_area, &editor, theme);
|
let active_field_rect = render_canvas(f, canvas_area, editor, theme);
|
||||||
|
|
||||||
// --- Render Autocomplete for Target Column ---
|
// --- Render Autocomplete for Target Column ---
|
||||||
if editor.mode() == canvas::AppMode::Edit && editor.current_field() == 1 { // Target Column field
|
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.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.target_column_suggestions().is_empty() {
|
||||||
if let Some(input_rect) = active_field_rect {
|
if let Some(input_rect) = active_field_rect {
|
||||||
autocomplete::render_autocomplete_dropdown(
|
autocomplete::render_autocomplete_dropdown(
|
||||||
f,
|
f,
|
||||||
input_rect,
|
input_rect,
|
||||||
f.area(), // Full frame area for clamping
|
f.area(), // Full frame area for clamping
|
||||||
theme,
|
theme,
|
||||||
&add_logic_state.target_column_suggestions,
|
add_logic_state.target_column_suggestions(),
|
||||||
add_logic_state.selected_target_column_suggestion_index,
|
add_logic_state.selected_target_column_suggestion_index(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,10 +188,10 @@ pub fn render_add_logic(
|
|||||||
|
|
||||||
// Script content preview
|
// Script content preview
|
||||||
{
|
{
|
||||||
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_ref = add_logic_state.script_content_editor().borrow_mut();
|
||||||
editor_ref.set_cursor_line_style(Style::default());
|
editor_ref.set_cursor_line_style(Style::default());
|
||||||
|
|
||||||
let 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 {
|
if is_script_preview_focused {
|
||||||
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
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 ")
|
let save_button = Paragraph::new(" Save Logic ")
|
||||||
.style(get_button_style(
|
.style(get_button_style(
|
||||||
AddLogicFocus::SaveButton,
|
AddLogicFocus::SaveButton,
|
||||||
add_logic_state.current_focus,
|
add_logic_state.current_focus(),
|
||||||
))
|
))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
@@ -264,7 +268,7 @@ pub fn render_add_logic(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(get_button_border_style(
|
.border_style(get_button_border_style(
|
||||||
add_logic_state.current_focus == AddLogicFocus::SaveButton,
|
add_logic_state.current_focus() == AddLogicFocus::SaveButton,
|
||||||
theme,
|
theme,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -273,7 +277,7 @@ pub fn render_add_logic(
|
|||||||
let cancel_button = Paragraph::new(" Cancel ")
|
let cancel_button = Paragraph::new(" Cancel ")
|
||||||
.style(get_button_style(
|
.style(get_button_style(
|
||||||
AddLogicFocus::CancelButton,
|
AddLogicFocus::CancelButton,
|
||||||
add_logic_state.current_focus,
|
add_logic_state.current_focus(),
|
||||||
))
|
))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
@@ -281,7 +285,7 @@ pub fn render_add_logic(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(get_button_border_style(
|
.border_style(get_button_border_style(
|
||||||
add_logic_state.current_focus == AddLogicFocus::CancelButton,
|
add_logic_state.current_focus() == AddLogicFocus::CancelButton,
|
||||||
theme,
|
theme,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/pages/routing/router.rs
|
// src/pages/routing/router.rs
|
||||||
use crate::state::pages::auth::AuthState;
|
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_panel::add_table::state::AddTableState;
|
||||||
use crate::pages::admin::AdminState;
|
use crate::pages::admin::AdminState;
|
||||||
use crate::pages::forms::FormState;
|
use crate::pages::forms::FormState;
|
||||||
@@ -14,7 +14,7 @@ pub enum Page {
|
|||||||
Login(LoginFormState),
|
Login(LoginFormState),
|
||||||
Register(RegisterFormState),
|
Register(RegisterFormState),
|
||||||
Admin(AdminState),
|
Admin(AdminState),
|
||||||
AddLogic(AddLogicState),
|
AddLogic(AddLogicFormState),
|
||||||
AddTable(AddTableState),
|
AddTable(AddTableState),
|
||||||
Form(String),
|
Form(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -425,7 +425,18 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
router.navigate(Page::Admin(admin_state.clone()));
|
router.navigate(Page::Admin(admin_state.clone()));
|
||||||
}
|
}
|
||||||
AppView::AddTable => router.navigate(Page::AddTable(admin_state.add_table_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) => {
|
AppView::Form(path) => {
|
||||||
// Keep current_view_* consistent with the active buffer path
|
// Keep current_view_* consistent with the active buffer path
|
||||||
if let Some((profile, table)) = path.split_once('/') {
|
if let Some((profile, table)) = path.split_once('/') {
|
||||||
|
|||||||
Reference in New Issue
Block a user