571 lines
20 KiB
Rust
571 lines
20 KiB
Rust
// 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, FormEditor, SuggestionItem};
|
|
use crossterm::event::KeyCode;
|
|
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: canvas::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: canvas::AppMode::Edit,
|
|
}
|
|
}
|
|
|
|
pub const INPUT_FIELD_COUNT: usize = 3;
|
|
|
|
/// Build canvas SuggestionItem list for target column
|
|
pub fn column_suggestions_sync(&self, query: &str) -> Vec<SuggestionItem> {
|
|
let q = query.to_lowercase();
|
|
self.table_columns_for_suggestions
|
|
.iter()
|
|
.filter(|c| q.is_empty() || c.to_lowercase().contains(&q))
|
|
.map(|c| SuggestionItem {
|
|
display_text: c.clone(),
|
|
value_to_store: c.clone(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// 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 = canvas::AppMode::Edit;
|
|
state
|
|
}
|
|
}
|
|
|
|
impl DataProvider for AddLogicState {
|
|
fn field_count(&self) -> usize {
|
|
3 // Logic Name, Target Column, Description
|
|
}
|
|
|
|
fn field_name(&self, index: usize) -> &str {
|
|
match index {
|
|
0 => "Logic Name",
|
|
1 => "Target Column",
|
|
2 => "Description",
|
|
_ => "",
|
|
}
|
|
}
|
|
|
|
fn field_value(&self, index: usize) -> &str {
|
|
match index {
|
|
0 => &self.logic_name_input,
|
|
1 => &self.target_column_input,
|
|
2 => &self.description_input,
|
|
_ => "",
|
|
}
|
|
}
|
|
|
|
fn set_field_value(&mut self, index: usize, value: String) {
|
|
match index {
|
|
0 => self.logic_name_input = value,
|
|
1 => self.target_column_input = value,
|
|
2 => self.description_input = value,
|
|
_ => {}
|
|
}
|
|
self.has_unsaved_changes = true;
|
|
}
|
|
|
|
fn supports_suggestions(&self, field_index: usize) -> bool {
|
|
// Only Target Column supports suggestions
|
|
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,
|
|
pub focused_button_index: usize,
|
|
}
|
|
|
|
// 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)
|
|
.field("focused_button_index", &self.focused_button_index)
|
|
.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,
|
|
focused_button_index: 0,
|
|
}
|
|
}
|
|
|
|
pub fn new_with_table(
|
|
editor_config: &EditorConfig,
|
|
profile_name: String,
|
|
table_id: Option<i64>,
|
|
table_name: String,
|
|
) -> Self {
|
|
let mut state = AddLogicState::new(editor_config);
|
|
state.profile_name = profile_name;
|
|
state.selected_table_id = table_id;
|
|
state.selected_table_name = Some(table_name);
|
|
let editor = FormEditor::new(state.clone());
|
|
Self {
|
|
state,
|
|
editor,
|
|
focus_outside_canvas: false,
|
|
focused_button_index: 0,
|
|
}
|
|
}
|
|
|
|
pub fn from_state(state: AddLogicState) -> Self {
|
|
let editor = FormEditor::new(state.clone());
|
|
Self {
|
|
state,
|
|
editor,
|
|
focus_outside_canvas: false,
|
|
focused_button_index: 0,
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
// Customize behavior for Target Column (field index 1) in Edit mode,
|
|
// mirroring how Register page does suggestions for Role.
|
|
let in_target_col_field = self.editor.current_field() == 1;
|
|
let in_edit_mode = self.editor.mode() == canvas::AppMode::Edit;
|
|
|
|
if in_target_col_field && in_edit_mode {
|
|
match key_event.code {
|
|
// Tab: open suggestions if inactive; otherwise cycle next
|
|
KeyCode::Tab => {
|
|
if !self.editor.is_suggestions_active() {
|
|
if let Some(query) = self.editor.start_suggestions(1) {
|
|
let items = self.state.column_suggestions_sync(&query);
|
|
let applied =
|
|
self.editor.apply_suggestions_result(1, &query, items);
|
|
if applied {
|
|
self.editor.update_inline_completion();
|
|
}
|
|
}
|
|
} else {
|
|
self.editor.suggestions_next();
|
|
}
|
|
return canvas::keymap::KeyEventOutcome::Consumed(None);
|
|
}
|
|
// Shift+Tab: cycle suggestions too (fallback to next)
|
|
KeyCode::BackTab => {
|
|
if self.editor.is_suggestions_active() {
|
|
self.editor.suggestions_next();
|
|
return canvas::keymap::KeyEventOutcome::Consumed(None);
|
|
}
|
|
}
|
|
// Enter: apply selected suggestion (if active)
|
|
KeyCode::Enter => {
|
|
if self.editor.is_suggestions_active() {
|
|
let _ = self.editor.apply_suggestion();
|
|
return canvas::keymap::KeyEventOutcome::Consumed(None);
|
|
}
|
|
}
|
|
// Esc: close suggestions if active
|
|
KeyCode::Esc => {
|
|
if self.editor.is_suggestions_active() {
|
|
self.editor.close_suggestions();
|
|
return canvas::keymap::KeyEventOutcome::Consumed(None);
|
|
}
|
|
}
|
|
// Character input: mutate then refresh suggestions if active
|
|
KeyCode::Char(_) => {
|
|
let outcome = self.editor.handle_key_event(key_event);
|
|
if self.editor.is_suggestions_active() {
|
|
if let Some(query) = self.editor.start_suggestions(1) {
|
|
let items = self.state.column_suggestions_sync(&query);
|
|
let applied =
|
|
self.editor.apply_suggestions_result(1, &query, items);
|
|
if applied {
|
|
self.editor.update_inline_completion();
|
|
}
|
|
}
|
|
}
|
|
return outcome;
|
|
}
|
|
// Backspace/Delete: mutate then refresh suggestions if active
|
|
KeyCode::Backspace | KeyCode::Delete => {
|
|
let outcome = self.editor.handle_key_event(key_event);
|
|
if self.editor.is_suggestions_active() {
|
|
if let Some(query) = self.editor.start_suggestions(1) {
|
|
let items = self.state.column_suggestions_sync(&query);
|
|
let applied =
|
|
self.editor.apply_suggestions_result(1, &query, items);
|
|
if applied {
|
|
self.editor.update_inline_completion();
|
|
}
|
|
}
|
|
}
|
|
return outcome;
|
|
}
|
|
_ => { /* fall through */ }
|
|
}
|
|
}
|
|
// Default: let canvas handle it
|
|
self.editor.handle_key_event(key_event)
|
|
}
|
|
}
|