451 lines
17 KiB
Rust
451 lines
17 KiB
Rust
// src/state/pages/add_logic.rs
|
|
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
|
use canvas::canvas::{CanvasState, ActionContext, CanvasAction, AppMode};
|
|
use crate::components::common::text_editor::{TextEditor, VimState};
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
use tui_textarea::TextArea;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub enum AddLogicFocus {
|
|
#[default]
|
|
InputLogicName,
|
|
InputTargetColumn,
|
|
InputDescription,
|
|
ScriptContentPreview,
|
|
InsideScriptContent,
|
|
SaveButton,
|
|
CancelButton,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct AddLogicState {
|
|
pub profile_name: String,
|
|
pub selected_table_id: Option<i64>,
|
|
pub selected_table_name: Option<String>,
|
|
pub logic_name_input: String,
|
|
pub target_column_input: String,
|
|
pub script_content_editor: Rc<RefCell<TextArea<'static>>>,
|
|
pub description_input: String,
|
|
pub current_focus: AddLogicFocus,
|
|
pub last_canvas_field: usize,
|
|
pub logic_name_cursor_pos: usize,
|
|
pub target_column_cursor_pos: usize,
|
|
pub description_cursor_pos: usize,
|
|
pub has_unsaved_changes: bool,
|
|
pub editor_keybinding_mode: EditorKeybindingMode,
|
|
pub vim_state: VimState,
|
|
|
|
// New fields for Target Column Autocomplete
|
|
pub table_columns_for_suggestions: Vec<String>, // All columns for the table
|
|
pub target_column_suggestions: Vec<String>, // Filtered suggestions
|
|
pub show_target_column_suggestions: bool,
|
|
pub selected_target_column_suggestion_index: Option<usize>,
|
|
pub in_target_column_suggestion_mode: bool,
|
|
|
|
// Script Editor Autocomplete
|
|
pub script_editor_autocomplete_active: bool,
|
|
pub script_editor_suggestions: Vec<String>,
|
|
pub script_editor_selected_suggestion_index: Option<usize>,
|
|
pub script_editor_trigger_position: Option<(usize, usize)>, // (line, column)
|
|
pub all_table_names: Vec<String>,
|
|
pub script_editor_filter_text: String,
|
|
|
|
// New fields for same-profile table names and column autocomplete
|
|
pub same_profile_table_names: Vec<String>, // Tables from same profile only
|
|
pub script_editor_awaiting_column_autocomplete: Option<String>, // Table name waiting for column fetch
|
|
pub app_mode: AppMode,
|
|
}
|
|
|
|
impl AddLogicState {
|
|
pub fn new(editor_config: &EditorConfig) -> Self {
|
|
let editor = TextEditor::new_textarea(editor_config);
|
|
AddLogicState {
|
|
profile_name: "default".to_string(),
|
|
selected_table_id: None,
|
|
selected_table_name: None,
|
|
logic_name_input: String::new(),
|
|
target_column_input: String::new(),
|
|
script_content_editor: Rc::new(RefCell::new(editor)),
|
|
description_input: String::new(),
|
|
current_focus: AddLogicFocus::InputLogicName,
|
|
last_canvas_field: 2,
|
|
logic_name_cursor_pos: 0,
|
|
target_column_cursor_pos: 0,
|
|
description_cursor_pos: 0,
|
|
has_unsaved_changes: false,
|
|
editor_keybinding_mode: editor_config.keybinding_mode.clone(),
|
|
vim_state: VimState::default(),
|
|
|
|
table_columns_for_suggestions: Vec::new(),
|
|
target_column_suggestions: Vec::new(),
|
|
show_target_column_suggestions: false,
|
|
selected_target_column_suggestion_index: None,
|
|
in_target_column_suggestion_mode: false,
|
|
|
|
script_editor_autocomplete_active: false,
|
|
script_editor_suggestions: Vec::new(),
|
|
script_editor_selected_suggestion_index: None,
|
|
script_editor_trigger_position: None,
|
|
all_table_names: Vec::new(),
|
|
script_editor_filter_text: String::new(),
|
|
|
|
same_profile_table_names: Vec::new(),
|
|
script_editor_awaiting_column_autocomplete: None,
|
|
app_mode: AppMode::Edit,
|
|
}
|
|
}
|
|
|
|
pub const INPUT_FIELD_COUNT: usize = 3;
|
|
|
|
/// Updates the target_column_suggestions based on current input.
|
|
pub fn update_target_column_suggestions(&mut self) {
|
|
let current_input = self.target_column_input.to_lowercase();
|
|
if self.table_columns_for_suggestions.is_empty() {
|
|
self.target_column_suggestions.clear();
|
|
self.show_target_column_suggestions = false;
|
|
self.selected_target_column_suggestion_index = None;
|
|
return;
|
|
}
|
|
|
|
if current_input.is_empty() {
|
|
self.target_column_suggestions = self.table_columns_for_suggestions.clone();
|
|
} else {
|
|
self.target_column_suggestions = self
|
|
.table_columns_for_suggestions
|
|
.iter()
|
|
.filter(|name| name.to_lowercase().contains(¤t_input))
|
|
.cloned()
|
|
.collect();
|
|
}
|
|
|
|
self.show_target_column_suggestions = !self.target_column_suggestions.is_empty();
|
|
if self.show_target_column_suggestions {
|
|
if let Some(selected_idx) = self.selected_target_column_suggestion_index {
|
|
if selected_idx >= self.target_column_suggestions.len() {
|
|
self.selected_target_column_suggestion_index = Some(0);
|
|
}
|
|
} else {
|
|
self.selected_target_column_suggestion_index = Some(0);
|
|
}
|
|
} else {
|
|
self.selected_target_column_suggestion_index = None;
|
|
}
|
|
}
|
|
|
|
/// Updates script editor suggestions based on current filter text
|
|
pub fn update_script_editor_suggestions(&mut self) {
|
|
let mut suggestions = vec!["sql".to_string()];
|
|
|
|
if self.selected_table_name.is_some() {
|
|
suggestions.extend(self.table_columns_for_suggestions.clone());
|
|
}
|
|
|
|
let current_selected_table_name = self.selected_table_name.as_deref();
|
|
suggestions.extend(
|
|
self.same_profile_table_names
|
|
.iter()
|
|
.filter(|tn| Some(tn.as_str()) != current_selected_table_name)
|
|
.cloned()
|
|
);
|
|
|
|
if self.script_editor_filter_text.is_empty() {
|
|
self.script_editor_suggestions = suggestions;
|
|
} else {
|
|
let filter_lower = self.script_editor_filter_text.to_lowercase();
|
|
self.script_editor_suggestions = suggestions
|
|
.into_iter()
|
|
.filter(|suggestion| suggestion.to_lowercase().contains(&filter_lower))
|
|
.collect();
|
|
}
|
|
|
|
// Update selection index
|
|
if self.script_editor_suggestions.is_empty() {
|
|
self.script_editor_selected_suggestion_index = None;
|
|
self.script_editor_autocomplete_active = false;
|
|
} else if let Some(selected_idx) = self.script_editor_selected_suggestion_index {
|
|
if selected_idx >= self.script_editor_suggestions.len() {
|
|
self.script_editor_selected_suggestion_index = Some(0);
|
|
}
|
|
} else {
|
|
self.script_editor_selected_suggestion_index = Some(0);
|
|
}
|
|
}
|
|
|
|
/// Checks if a suggestion is a table name (for triggering column autocomplete)
|
|
pub fn is_table_name_suggestion(&self, suggestion: &str) -> bool {
|
|
// Not "sql"
|
|
if suggestion == "sql" {
|
|
return false;
|
|
}
|
|
if self.table_columns_for_suggestions.contains(&suggestion.to_string()) {
|
|
return false;
|
|
}
|
|
self.same_profile_table_names.contains(&suggestion.to_string())
|
|
}
|
|
|
|
/// Sets table columns for autocomplete suggestions
|
|
pub fn set_table_columns(&mut self, columns: Vec<String>) {
|
|
self.table_columns_for_suggestions = columns.clone();
|
|
if !columns.is_empty() {
|
|
self.update_target_column_suggestions();
|
|
}
|
|
}
|
|
|
|
/// Sets all available table names for autocomplete suggestions
|
|
pub fn set_all_table_names(&mut self, table_names: Vec<String>) {
|
|
self.all_table_names = table_names;
|
|
}
|
|
|
|
/// Sets table names from the same profile for autocomplete suggestions
|
|
pub fn set_same_profile_table_names(&mut self, table_names: Vec<String>) {
|
|
self.same_profile_table_names = table_names;
|
|
}
|
|
|
|
/// Triggers waiting for column autocomplete for a specific table
|
|
pub fn trigger_column_autocomplete_for_table(&mut self, table_name: String) {
|
|
self.script_editor_awaiting_column_autocomplete = Some(table_name);
|
|
}
|
|
|
|
/// Updates autocomplete with columns for a specific table
|
|
pub fn set_columns_for_table_autocomplete(&mut self, columns: Vec<String>) {
|
|
self.script_editor_suggestions = columns;
|
|
self.script_editor_selected_suggestion_index = if self.script_editor_suggestions.is_empty() {
|
|
None
|
|
} else {
|
|
Some(0)
|
|
};
|
|
self.script_editor_autocomplete_active = !self.script_editor_suggestions.is_empty();
|
|
self.script_editor_awaiting_column_autocomplete = None;
|
|
}
|
|
|
|
/// Deactivates script editor autocomplete and clears related state
|
|
pub fn deactivate_script_editor_autocomplete(&mut self) {
|
|
self.script_editor_autocomplete_active = false;
|
|
self.script_editor_suggestions.clear();
|
|
self.script_editor_selected_suggestion_index = None;
|
|
self.script_editor_trigger_position = None;
|
|
self.script_editor_filter_text.clear();
|
|
}
|
|
|
|
/// Helper method to validate and save logic
|
|
pub fn save_logic(&mut self) -> Option<String> {
|
|
if self.logic_name_input.trim().is_empty() {
|
|
return Some("Logic name is required".to_string());
|
|
}
|
|
|
|
if self.target_column_input.trim().is_empty() {
|
|
return Some("Target column is required".to_string());
|
|
}
|
|
|
|
let script_content = {
|
|
let editor_borrow = self.script_content_editor.borrow();
|
|
editor_borrow.lines().join("\n")
|
|
};
|
|
|
|
if script_content.trim().is_empty() {
|
|
return Some("Script content is required".to_string());
|
|
}
|
|
|
|
// Here you would typically save to database/storage
|
|
// For now, just clear the form and mark as saved
|
|
self.has_unsaved_changes = false;
|
|
Some(format!("Logic '{}' saved successfully", self.logic_name_input.trim()))
|
|
}
|
|
|
|
/// Helper method to clear the form
|
|
pub fn clear_form(&mut self) -> Option<String> {
|
|
let profile = self.profile_name.clone();
|
|
let table_id = self.selected_table_id;
|
|
let table_name = self.selected_table_name.clone();
|
|
let editor_config = EditorConfig::default(); // You might want to preserve the actual config
|
|
|
|
*self = Self::new(&editor_config);
|
|
self.profile_name = profile;
|
|
self.selected_table_id = table_id;
|
|
self.selected_table_name = table_name;
|
|
|
|
Some("Form cleared".to_string())
|
|
}
|
|
}
|
|
|
|
impl Default for AddLogicState {
|
|
fn default() -> Self {
|
|
let mut state = Self::new(&EditorConfig::default());
|
|
state.app_mode = AppMode::Edit;
|
|
state
|
|
}
|
|
}
|
|
|
|
// Implement external library's CanvasState for AddLogicState
|
|
impl CanvasState for AddLogicState {
|
|
fn current_field(&self) -> usize {
|
|
match self.current_focus {
|
|
AddLogicFocus::InputLogicName => 0,
|
|
AddLogicFocus::InputTargetColumn => 1,
|
|
AddLogicFocus::InputDescription => 2,
|
|
// If focus is elsewhere, return the last canvas field used
|
|
_ => self.last_canvas_field,
|
|
}
|
|
}
|
|
|
|
fn set_current_field(&mut self, index: usize) {
|
|
let new_focus = match index {
|
|
0 => AddLogicFocus::InputLogicName,
|
|
1 => AddLogicFocus::InputTargetColumn,
|
|
2 => AddLogicFocus::InputDescription,
|
|
_ => return,
|
|
};
|
|
if self.current_focus != new_focus {
|
|
if self.current_focus == AddLogicFocus::InputTargetColumn {
|
|
self.in_target_column_suggestion_mode = false;
|
|
self.show_target_column_suggestions = false;
|
|
}
|
|
self.current_focus = new_focus;
|
|
self.last_canvas_field = index;
|
|
}
|
|
}
|
|
|
|
fn current_cursor_pos(&self) -> usize {
|
|
match self.current_focus {
|
|
AddLogicFocus::InputLogicName => self.logic_name_cursor_pos,
|
|
AddLogicFocus::InputTargetColumn => self.target_column_cursor_pos,
|
|
AddLogicFocus::InputDescription => self.description_cursor_pos,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
fn set_current_cursor_pos(&mut self, pos: usize) {
|
|
match self.current_focus {
|
|
AddLogicFocus::InputLogicName => {
|
|
self.logic_name_cursor_pos = pos.min(self.logic_name_input.len());
|
|
}
|
|
AddLogicFocus::InputTargetColumn => {
|
|
self.target_column_cursor_pos = pos.min(self.target_column_input.len());
|
|
}
|
|
AddLogicFocus::InputDescription => {
|
|
self.description_cursor_pos = pos.min(self.description_input.len());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn get_current_input(&self) -> &str {
|
|
match self.current_focus {
|
|
AddLogicFocus::InputLogicName => &self.logic_name_input,
|
|
AddLogicFocus::InputTargetColumn => &self.target_column_input,
|
|
AddLogicFocus::InputDescription => &self.description_input,
|
|
_ => "", // Should not happen if called correctly
|
|
}
|
|
}
|
|
|
|
fn get_current_input_mut(&mut self) -> &mut String {
|
|
match self.current_focus {
|
|
AddLogicFocus::InputLogicName => &mut self.logic_name_input,
|
|
AddLogicFocus::InputTargetColumn => &mut self.target_column_input,
|
|
AddLogicFocus::InputDescription => &mut self.description_input,
|
|
_ => &mut self.logic_name_input, // Fallback
|
|
}
|
|
}
|
|
|
|
fn inputs(&self) -> Vec<&String> {
|
|
vec![
|
|
&self.logic_name_input,
|
|
&self.target_column_input,
|
|
&self.description_input,
|
|
]
|
|
}
|
|
|
|
fn fields(&self) -> Vec<&str> {
|
|
vec!["Logic Name", "Target Column", "Description"]
|
|
}
|
|
|
|
fn has_unsaved_changes(&self) -> bool {
|
|
self.has_unsaved_changes
|
|
}
|
|
|
|
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
|
self.has_unsaved_changes = changed;
|
|
}
|
|
|
|
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
|
match action {
|
|
// Handle saving logic script
|
|
CanvasAction::Custom(action_str) if action_str == "save_logic" => {
|
|
self.save_logic()
|
|
}
|
|
|
|
// Handle clearing the form
|
|
CanvasAction::Custom(action_str) if action_str == "clear_form" => {
|
|
self.clear_form()
|
|
}
|
|
|
|
// Handle target column autocomplete activation
|
|
CanvasAction::Custom(action_str) if action_str == "activate_autocomplete" => {
|
|
if self.current_field() == 1 { // Target Column field
|
|
self.in_target_column_suggestion_mode = true;
|
|
self.update_target_column_suggestions();
|
|
Some("Autocomplete activated".to_string())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// Handle target column suggestion selection
|
|
CanvasAction::Custom(action_str) if action_str == "select_suggestion" => {
|
|
if self.current_field() == 1 && self.in_target_column_suggestion_mode {
|
|
if let Some(selected_idx) = self.selected_target_column_suggestion_index {
|
|
if let Some(suggestion) = self.target_column_suggestions.get(selected_idx) {
|
|
self.target_column_input = suggestion.clone();
|
|
self.target_column_cursor_pos = suggestion.len();
|
|
self.in_target_column_suggestion_mode = false;
|
|
self.show_target_column_suggestions = false;
|
|
self.has_unsaved_changes = true;
|
|
return Some(format!("Selected: {}", suggestion));
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
// Custom validation when moving between fields
|
|
CanvasAction::NextField => {
|
|
match self.current_field() {
|
|
0 => { // Logic Name field
|
|
if self.logic_name_input.trim().is_empty() {
|
|
Some("Logic name cannot be empty".to_string())
|
|
} else {
|
|
None // Let canvas library handle the normal field movement
|
|
}
|
|
}
|
|
1 => { // Target Column field
|
|
// Update suggestions when entering target column field
|
|
self.update_target_column_suggestions();
|
|
None
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// Handle character insertion with validation
|
|
CanvasAction::InsertChar(c) => {
|
|
if self.current_field() == 1 { // Target Column field
|
|
// Update suggestions after character insertion
|
|
// Note: Canvas library will handle the actual insertion
|
|
// This is just for triggering suggestion updates
|
|
None // Let canvas handle insertion, then we'll update suggestions
|
|
} else {
|
|
None // Let canvas handle normally
|
|
}
|
|
}
|
|
|
|
// Let canvas library handle everything else
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn current_mode(&self) -> AppMode {
|
|
self.app_mode
|
|
}
|
|
}
|