|
|
|
|
@@ -96,7 +96,7 @@ pub async fn run_ui() -> Result<()> {
|
|
|
|
|
|
|
|
|
|
// Filter the columns obtained from the service
|
|
|
|
|
let filtered_columns = filter_user_columns(initial_columns_from_service); // Use the correct variable
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let mut form_state = FormState::new(
|
|
|
|
|
initial_profile.clone(),
|
|
|
|
|
initial_table.clone(),
|
|
|
|
|
@@ -132,208 +132,15 @@ pub async fn run_ui() -> Result<()> {
|
|
|
|
|
let mut prev_view_table_name = app_state.current_view_table_name.clone();
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
if let Some(active_view) = buffer_state.get_active_view() {
|
|
|
|
|
app_state.ui.show_intro = false;
|
|
|
|
|
app_state.ui.show_login = false;
|
|
|
|
|
app_state.ui.show_register = false;
|
|
|
|
|
app_state.ui.show_admin = false;
|
|
|
|
|
app_state.ui.show_add_table = false;
|
|
|
|
|
app_state.ui.show_add_logic = false;
|
|
|
|
|
app_state.ui.show_form = false;
|
|
|
|
|
match active_view {
|
|
|
|
|
AppView::Intro => app_state.ui.show_intro = true,
|
|
|
|
|
AppView::Login => app_state.ui.show_login = true,
|
|
|
|
|
AppView::Register => app_state.ui.show_register = true,
|
|
|
|
|
AppView::Admin => {
|
|
|
|
|
info!("Active view is Admin, refreshing profile tree...");
|
|
|
|
|
match grpc_client.get_profile_tree().await {
|
|
|
|
|
Ok(refreshed_tree) => {
|
|
|
|
|
app_state.profile_tree = refreshed_tree;
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Failed to refresh profile tree for Admin panel: {}", e);
|
|
|
|
|
event_handler.command_message = format!("Error refreshing admin data: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
app_state.ui.show_admin = true;
|
|
|
|
|
let profile_names = app_state.profile_tree.profiles.iter()
|
|
|
|
|
.map(|p| p.name.clone())
|
|
|
|
|
.collect();
|
|
|
|
|
admin_state.set_profiles(profile_names);
|
|
|
|
|
|
|
|
|
|
if admin_state.current_focus == AdminFocus::default() ||
|
|
|
|
|
!matches!(admin_state.current_focus,
|
|
|
|
|
AdminFocus::InsideProfilesList |
|
|
|
|
|
AdminFocus::Tables | AdminFocus::InsideTablesList |
|
|
|
|
|
AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) {
|
|
|
|
|
admin_state.current_focus = AdminFocus::ProfilesPane;
|
|
|
|
|
}
|
|
|
|
|
if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() {
|
|
|
|
|
admin_state.profile_list_state.select(Some(0));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
AppView::AddTable => app_state.ui.show_add_table = true,
|
|
|
|
|
AppView::AddLogic => app_state.ui.show_add_logic = true,
|
|
|
|
|
AppView::Form => app_state.ui.show_form = true,
|
|
|
|
|
AppView::Scratch => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle table change for FormView
|
|
|
|
|
if app_state.ui.show_form {
|
|
|
|
|
let current_view_profile = app_state.current_view_profile_name.clone();
|
|
|
|
|
let current_view_table = app_state.current_view_table_name.clone();
|
|
|
|
|
|
|
|
|
|
if prev_view_profile_name != current_view_profile || prev_view_table_name != current_view_table {
|
|
|
|
|
if let (Some(prof_name), Some(tbl_name)) = (current_view_profile.as_ref(), current_view_table.as_ref()) {
|
|
|
|
|
app_state.show_loading_dialog("Loading Table", &format!("Fetching data for {}.{}...", prof_name, tbl_name));
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
|
|
|
|
|
match grpc_client.get_table_structure(prof_name.clone(), tbl_name.clone()).await {
|
|
|
|
|
Ok(structure_response) => {
|
|
|
|
|
let new_columns: Vec<String> = structure_response.columns.iter().map(|c| c.name.clone()).collect();
|
|
|
|
|
form_state = FormState::new(prof_name.clone(), tbl_name.clone(), new_columns);
|
|
|
|
|
|
|
|
|
|
if let Err(e) = UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state).await {
|
|
|
|
|
app_state.update_dialog_content(&format!("Error fetching count: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
|
|
|
|
} else {
|
|
|
|
|
if form_state.total_count > 0 {
|
|
|
|
|
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
|
|
|
|
|
app_state.update_dialog_content(&format!("Error loading data: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
|
|
|
|
} else {
|
|
|
|
|
app_state.hide_dialog();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
form_state.reset_to_empty();
|
|
|
|
|
app_state.hide_dialog();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
app_state.update_dialog_content(&format!("Error fetching table structure: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
|
|
|
|
app_state.current_view_profile_name = prev_view_profile_name.clone();
|
|
|
|
|
app_state.current_view_table_name = prev_view_table_name.clone();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
prev_view_profile_name = current_view_profile;
|
|
|
|
|
prev_view_table_name = current_view_table;
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
|
|
|
|
if app_state.ui.show_add_logic {
|
|
|
|
|
if admin_state.add_logic_state.profile_name == profile_name &&
|
|
|
|
|
admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) {
|
|
|
|
|
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
|
|
|
|
let fetch_message = UiService::initialize_add_logic_table_data(
|
|
|
|
|
&mut grpc_client,
|
|
|
|
|
&mut admin_state.add_logic_state,
|
|
|
|
|
&app_state.profile_tree,
|
|
|
|
|
).await.unwrap_or_else(|e| {
|
|
|
|
|
error!("Error initializing add_logic_table_data: {}", e);
|
|
|
|
|
format!("Error fetching table structure: {}", e)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
|
|
|
|
|
info!("{}", fetch_message);
|
|
|
|
|
} else {
|
|
|
|
|
event_handler.command_message = fetch_message;
|
|
|
|
|
}
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
} else {
|
|
|
|
|
error!(
|
|
|
|
|
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but add_logic_state is for {}.{:?}",
|
|
|
|
|
profile_name, table_name,
|
|
|
|
|
admin_state.add_logic_state.profile_name,
|
|
|
|
|
admin_state.add_logic_state.selected_table_name
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warn!(
|
|
|
|
|
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
|
|
|
|
|
profile_name, table_name
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if needs_redraw {
|
|
|
|
|
terminal.draw(|f| {
|
|
|
|
|
render_ui(
|
|
|
|
|
f,
|
|
|
|
|
&mut form_state,
|
|
|
|
|
&mut auth_state,
|
|
|
|
|
&login_state,
|
|
|
|
|
®ister_state,
|
|
|
|
|
&intro_state,
|
|
|
|
|
&mut admin_state,
|
|
|
|
|
&buffer_state,
|
|
|
|
|
&theme,
|
|
|
|
|
event_handler.is_edit_mode,
|
|
|
|
|
&event_handler.highlight_state,
|
|
|
|
|
&event_handler.command_input,
|
|
|
|
|
event_handler.command_mode,
|
|
|
|
|
&event_handler.command_message,
|
|
|
|
|
&event_handler.navigation_state,
|
|
|
|
|
&app_state.current_dir,
|
|
|
|
|
current_fps,
|
|
|
|
|
&app_state,
|
|
|
|
|
);
|
|
|
|
|
}).context("Terminal draw call failed")?;
|
|
|
|
|
needs_redraw = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
|
|
|
|
|
if app_state.ui.show_add_logic {
|
|
|
|
|
let profile_name = admin_state.add_logic_state.profile_name.clone();
|
|
|
|
|
|
|
|
|
|
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
|
|
|
|
|
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
|
|
|
|
|
Ok(columns) => {
|
|
|
|
|
admin_state.add_logic_state.set_columns_for_table_autocomplete(columns.clone());
|
|
|
|
|
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
|
|
|
|
|
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
|
|
|
|
|
admin_state.add_logic_state.script_editor_awaiting_column_autocomplete = None;
|
|
|
|
|
admin_state.add_logic_state.deactivate_script_editor_autocomplete();
|
|
|
|
|
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
|
|
|
|
|
match current_mode {
|
|
|
|
|
AppMode::Edit => { terminal.show_cursor()?; }
|
|
|
|
|
AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; }
|
|
|
|
|
AppMode::ReadOnly => {
|
|
|
|
|
if !app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; }
|
|
|
|
|
else { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; }
|
|
|
|
|
terminal.show_cursor().context("Failed to show cursor in ReadOnly mode")?;
|
|
|
|
|
}
|
|
|
|
|
AppMode::General => {
|
|
|
|
|
if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; }
|
|
|
|
|
else { terminal.hide_cursor()?; }
|
|
|
|
|
}
|
|
|
|
|
AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// PROCESS EVENTS & UPDATE STATE
|
|
|
|
|
// ===================================================================
|
|
|
|
|
let position_before_event = form_state.current_position;
|
|
|
|
|
|
|
|
|
|
if app_state.ui.dialog.is_loading {
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut event_outcome_result = Ok(EventOutcome::Ok(String::new()));
|
|
|
|
|
let mut event_processed = false;
|
|
|
|
|
if crossterm_event::poll(std::time::Duration::from_millis(1))? {
|
|
|
|
|
let event = event_reader.read_event().context("Failed to read terminal event")?;
|
|
|
|
|
event_processed = true;
|
|
|
|
|
event_outcome_result = event_handler.handle_event(
|
|
|
|
|
let event_outcome_result = event_handler.handle_event(
|
|
|
|
|
event,
|
|
|
|
|
&config,
|
|
|
|
|
&mut terminal,
|
|
|
|
|
@@ -348,12 +155,57 @@ pub async fn run_ui() -> Result<()> {
|
|
|
|
|
&mut buffer_state,
|
|
|
|
|
&mut app_state,
|
|
|
|
|
).await;
|
|
|
|
|
|
|
|
|
|
// --- Handle the outcome of the event ---
|
|
|
|
|
let mut should_exit = false;
|
|
|
|
|
match event_outcome_result {
|
|
|
|
|
Ok(outcome) => match outcome {
|
|
|
|
|
EventOutcome::Ok(message) => {
|
|
|
|
|
if !message.is_empty() {
|
|
|
|
|
event_handler.command_message = message;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EventOutcome::Exit(message) => {
|
|
|
|
|
event_handler.command_message = message;
|
|
|
|
|
should_exit = true;
|
|
|
|
|
}
|
|
|
|
|
EventOutcome::DataSaved(save_outcome, message) => {
|
|
|
|
|
event_handler.command_message = message;
|
|
|
|
|
if let Err(e) = UiService::handle_save_outcome(
|
|
|
|
|
save_outcome,
|
|
|
|
|
&mut grpc_client,
|
|
|
|
|
&mut app_state,
|
|
|
|
|
&mut form_state,
|
|
|
|
|
).await {
|
|
|
|
|
event_handler.command_message =
|
|
|
|
|
format!("Error handling save outcome: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EventOutcome::ButtonSelected { .. } => {}
|
|
|
|
|
EventOutcome::TableSelected { path } => {
|
|
|
|
|
let parts: Vec<&str> = path.split('/').collect();
|
|
|
|
|
if parts.len() == 2 {
|
|
|
|
|
let profile_name = parts[0].to_string();
|
|
|
|
|
let table_name = parts[1].to_string();
|
|
|
|
|
|
|
|
|
|
app_state.set_current_view_table(profile_name, table_name);
|
|
|
|
|
buffer_state.update_history(AppView::Form);
|
|
|
|
|
event_handler.command_message = format!("Loading table: {}", path);
|
|
|
|
|
} else {
|
|
|
|
|
event_handler.command_message = format!("Invalid table path: {}", path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
event_handler.command_message = format!("Error: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if should_exit {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if event_processed {
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Handle async results from other tasks ---
|
|
|
|
|
match login_result_receiver.try_recv() {
|
|
|
|
|
Ok(result) => {
|
|
|
|
|
if login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state) {
|
|
|
|
|
@@ -403,62 +255,182 @@ pub async fn run_ui() -> Result<()> {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut should_exit = false;
|
|
|
|
|
match event_outcome_result {
|
|
|
|
|
Ok(outcome) => match outcome {
|
|
|
|
|
EventOutcome::Ok(_message) => {}
|
|
|
|
|
EventOutcome::Exit(message) => {
|
|
|
|
|
event_handler.command_message = message;
|
|
|
|
|
should_exit = true;
|
|
|
|
|
}
|
|
|
|
|
EventOutcome::DataSaved(save_outcome, message) => {
|
|
|
|
|
event_handler.command_message = message;
|
|
|
|
|
if let Err(e) = UiService::handle_save_outcome(
|
|
|
|
|
save_outcome,
|
|
|
|
|
&mut grpc_client,
|
|
|
|
|
&mut app_state,
|
|
|
|
|
&mut form_state,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
event_handler.command_message =
|
|
|
|
|
format!("Error handling save outcome: {}", e);
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// STATE-DEPENDENT LOGIC (NOW RUNS AFTER STATE IS UPDATED)
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
// --- Determine which view is active ---
|
|
|
|
|
if let Some(active_view) = buffer_state.get_active_view() {
|
|
|
|
|
app_state.ui.show_intro = false;
|
|
|
|
|
app_state.ui.show_login = false;
|
|
|
|
|
app_state.ui.show_register = false;
|
|
|
|
|
app_state.ui.show_admin = false;
|
|
|
|
|
app_state.ui.show_add_table = false;
|
|
|
|
|
app_state.ui.show_add_logic = false;
|
|
|
|
|
app_state.ui.show_form = false;
|
|
|
|
|
match active_view {
|
|
|
|
|
AppView::Intro => app_state.ui.show_intro = true,
|
|
|
|
|
AppView::Login => app_state.ui.show_login = true,
|
|
|
|
|
AppView::Register => app_state.ui.show_register = true,
|
|
|
|
|
AppView::Admin => {
|
|
|
|
|
info!("Active view is Admin, refreshing profile tree...");
|
|
|
|
|
match grpc_client.get_profile_tree().await {
|
|
|
|
|
Ok(refreshed_tree) => {
|
|
|
|
|
app_state.profile_tree = refreshed_tree;
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Failed to refresh profile tree for Admin panel: {}", e);
|
|
|
|
|
event_handler.command_message = format!("Error refreshing admin data: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
app_state.ui.show_admin = true;
|
|
|
|
|
let profile_names = app_state.profile_tree.profiles.iter()
|
|
|
|
|
.map(|p| p.name.clone())
|
|
|
|
|
.collect();
|
|
|
|
|
admin_state.set_profiles(profile_names);
|
|
|
|
|
|
|
|
|
|
if admin_state.current_focus == AdminFocus::default() ||
|
|
|
|
|
!matches!(admin_state.current_focus,
|
|
|
|
|
AdminFocus::InsideProfilesList |
|
|
|
|
|
AdminFocus::Tables | AdminFocus::InsideTablesList |
|
|
|
|
|
AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) {
|
|
|
|
|
admin_state.current_focus = AdminFocus::ProfilesPane;
|
|
|
|
|
}
|
|
|
|
|
if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() {
|
|
|
|
|
admin_state.profile_list_state.select(Some(0));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EventOutcome::ButtonSelected { context: _, index: _ } => {}
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
event_handler.command_message = format!("Error: {}", e);
|
|
|
|
|
AppView::AddTable => app_state.ui.show_add_table = true,
|
|
|
|
|
AppView::AddLogic => app_state.ui.show_add_logic = true,
|
|
|
|
|
AppView::Form => app_state.ui.show_form = true,
|
|
|
|
|
AppView::Scratch => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- MODIFIED: Position Change Handling (operates on form_state) ---
|
|
|
|
|
// --- Handle table change for FormView ---
|
|
|
|
|
if app_state.ui.show_form {
|
|
|
|
|
let current_view_profile = app_state.current_view_profile_name.clone();
|
|
|
|
|
let current_view_table = app_state.current_view_table_name.clone();
|
|
|
|
|
|
|
|
|
|
if prev_view_profile_name != current_view_profile || prev_view_table_name != current_view_table {
|
|
|
|
|
if let (Some(prof_name), Some(tbl_name)) = (current_view_profile.as_ref(), current_view_table.as_ref()) {
|
|
|
|
|
app_state.show_loading_dialog("Loading Table", &format!("Fetching data for {}.{}...", prof_name, tbl_name));
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
|
|
|
|
|
match grpc_client.get_table_structure(prof_name.clone(), tbl_name.clone()).await {
|
|
|
|
|
Ok(structure_response) => {
|
|
|
|
|
let new_columns: Vec<String> = structure_response.columns.iter().map(|c| c.name.clone()).collect();
|
|
|
|
|
form_state = FormState::new(prof_name.clone(), tbl_name.clone(), new_columns);
|
|
|
|
|
|
|
|
|
|
if let Err(e) = UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state).await {
|
|
|
|
|
app_state.update_dialog_content(&format!("Error fetching count: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
|
|
|
|
} else {
|
|
|
|
|
if form_state.total_count > 0 {
|
|
|
|
|
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
|
|
|
|
|
app_state.update_dialog_content(&format!("Error loading data: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
|
|
|
|
} else {
|
|
|
|
|
app_state.hide_dialog();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
form_state.reset_to_empty();
|
|
|
|
|
app_state.hide_dialog();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
app_state.update_dialog_content(&format!("Error fetching table structure: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
|
|
|
|
app_state.current_view_profile_name = prev_view_profile_name.clone();
|
|
|
|
|
app_state.current_view_table_name = prev_view_table_name.clone();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
prev_view_profile_name = current_view_profile;
|
|
|
|
|
prev_view_table_name = current_view_table;
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Handle pending async fetches ---
|
|
|
|
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
|
|
|
|
if app_state.ui.show_add_logic {
|
|
|
|
|
if admin_state.add_logic_state.profile_name == profile_name &&
|
|
|
|
|
admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) {
|
|
|
|
|
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
|
|
|
|
let fetch_message = UiService::initialize_add_logic_table_data(
|
|
|
|
|
&mut grpc_client,
|
|
|
|
|
&mut admin_state.add_logic_state,
|
|
|
|
|
&app_state.profile_tree,
|
|
|
|
|
).await.unwrap_or_else(|e| {
|
|
|
|
|
error!("Error initializing add_logic_table_data: {}", e);
|
|
|
|
|
format!("Error fetching table structure: {}", e)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
|
|
|
|
|
info!("{}", fetch_message);
|
|
|
|
|
} else {
|
|
|
|
|
event_handler.command_message = fetch_message;
|
|
|
|
|
}
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
} else {
|
|
|
|
|
error!(
|
|
|
|
|
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but add_logic_state is for {}.{:?}",
|
|
|
|
|
profile_name, table_name,
|
|
|
|
|
admin_state.add_logic_state.profile_name,
|
|
|
|
|
admin_state.add_logic_state.selected_table_name
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warn!(
|
|
|
|
|
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
|
|
|
|
|
profile_name, table_name
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
|
|
|
|
|
if app_state.ui.show_add_logic {
|
|
|
|
|
let profile_name = admin_state.add_logic_state.profile_name.clone();
|
|
|
|
|
|
|
|
|
|
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
|
|
|
|
|
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
|
|
|
|
|
Ok(columns) => {
|
|
|
|
|
admin_state.add_logic_state.set_columns_for_table_autocomplete(columns.clone());
|
|
|
|
|
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
|
|
|
|
|
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
|
|
|
|
|
admin_state.add_logic_state.script_editor_awaiting_column_autocomplete = None;
|
|
|
|
|
admin_state.add_logic_state.deactivate_script_editor_autocomplete();
|
|
|
|
|
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Handle cursor updates based on position changes ---
|
|
|
|
|
let position_changed = form_state.current_position != position_before_event;
|
|
|
|
|
let mut position_logic_needs_redraw = false;
|
|
|
|
|
|
|
|
|
|
if app_state.ui.show_form { // Only if the form is active
|
|
|
|
|
if app_state.ui.show_form {
|
|
|
|
|
if position_changed && !event_handler.is_edit_mode {
|
|
|
|
|
// This part is okay: update cursor for the current field BEFORE loading new data
|
|
|
|
|
let current_input_before_load = form_state.get_current_input();
|
|
|
|
|
let max_cursor_pos_before_load = if !current_input_before_load.is_empty() { current_input_before_load.chars().count() } else { 0 };
|
|
|
|
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_before_load);
|
|
|
|
|
position_logic_needs_redraw = true;
|
|
|
|
|
|
|
|
|
|
// Validate new form_state.current_position
|
|
|
|
|
if form_state.total_count > 0 && form_state.current_position > form_state.total_count + 1 {
|
|
|
|
|
form_state.current_position = form_state.total_count + 1; // Cap at new entry
|
|
|
|
|
form_state.current_position = form_state.total_count + 1;
|
|
|
|
|
} else if form_state.total_count == 0 && form_state.current_position > 1 {
|
|
|
|
|
form_state.current_position = 1; // Cap at new entry for empty table
|
|
|
|
|
form_state.current_position = 1;
|
|
|
|
|
}
|
|
|
|
|
if form_state.current_position == 0 && form_state.total_count > 0 {
|
|
|
|
|
form_state.current_position = 1; // Don't allow 0 if there are records
|
|
|
|
|
form_state.current_position = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Load data for the new position OR reset for new entry
|
|
|
|
|
if (form_state.total_count > 0 && form_state.current_position <= form_state.total_count && form_state.current_position > 0)
|
|
|
|
|
{
|
|
|
|
|
// It's an existing record position
|
|
|
|
|
match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
|
|
|
|
|
Ok(load_message) => {
|
|
|
|
|
if event_handler.command_message.is_empty() || !load_message.starts_with("Error") {
|
|
|
|
|
@@ -467,16 +439,13 @@ pub async fn run_ui() -> Result<()> {
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
event_handler.command_message = format!("Error loading data: {}", e);
|
|
|
|
|
// Consider what to do with form_state here - maybe revert position or clear form
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Position indicates a new entry (or table is empty and position is 1)
|
|
|
|
|
form_state.reset_to_empty(); // This sets id=0, clears values, and sets current_position correctly
|
|
|
|
|
form_state.reset_to_empty();
|
|
|
|
|
event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOW, after data is loaded or form is reset, get the current input string and its length
|
|
|
|
|
let current_input_after_load_str = form_state.get_current_input();
|
|
|
|
|
let current_input_len_after_load = current_input_after_load_str.chars().count();
|
|
|
|
|
|
|
|
|
|
@@ -490,11 +459,9 @@ pub async fn run_ui() -> Result<()> {
|
|
|
|
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(current_input_len_after_load);
|
|
|
|
|
} else {
|
|
|
|
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_for_readonly_after_load);
|
|
|
|
|
// The check for empty string is implicitly handled by max_cursor_pos_for_readonly_after_load being 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if !position_changed && !event_handler.is_edit_mode && app_state.ui.show_form {
|
|
|
|
|
// Update cursor if not editing and position didn't change (e.g. arrow keys within field)
|
|
|
|
|
let current_input_str = form_state.get_current_input();
|
|
|
|
|
let current_input_len = current_input_str.chars().count();
|
|
|
|
|
let max_cursor_pos = if current_input_len > 0 {
|
|
|
|
|
@@ -522,10 +489,56 @@ pub async fn run_ui() -> Result<()> {
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if should_exit {
|
|
|
|
|
return Ok(());
|
|
|
|
|
if app_state.ui.dialog.is_loading {
|
|
|
|
|
needs_redraw = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// STEP 3: DRAW THE UI
|
|
|
|
|
// ===================================================================
|
|
|
|
|
if event_processed || needs_redraw || position_changed {
|
|
|
|
|
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
|
|
|
|
|
match current_mode {
|
|
|
|
|
AppMode::Edit => { terminal.show_cursor()?; }
|
|
|
|
|
AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; }
|
|
|
|
|
AppMode::ReadOnly => {
|
|
|
|
|
if !app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; }
|
|
|
|
|
else { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; }
|
|
|
|
|
terminal.show_cursor().context("Failed to show cursor in ReadOnly mode")?;
|
|
|
|
|
}
|
|
|
|
|
AppMode::General => {
|
|
|
|
|
if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; }
|
|
|
|
|
else { terminal.hide_cursor()?; }
|
|
|
|
|
}
|
|
|
|
|
AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
terminal.draw(|f| {
|
|
|
|
|
render_ui(
|
|
|
|
|
f,
|
|
|
|
|
&mut form_state,
|
|
|
|
|
&mut auth_state,
|
|
|
|
|
&login_state,
|
|
|
|
|
®ister_state,
|
|
|
|
|
&intro_state,
|
|
|
|
|
&mut admin_state,
|
|
|
|
|
&buffer_state,
|
|
|
|
|
&theme,
|
|
|
|
|
event_handler.is_edit_mode,
|
|
|
|
|
&event_handler.highlight_state,
|
|
|
|
|
&event_handler.command_input,
|
|
|
|
|
event_handler.command_mode,
|
|
|
|
|
&event_handler.command_message,
|
|
|
|
|
&event_handler.navigation_state,
|
|
|
|
|
&app_state.current_dir,
|
|
|
|
|
current_fps,
|
|
|
|
|
&app_state,
|
|
|
|
|
);
|
|
|
|
|
}).context("Terminal draw call failed")?;
|
|
|
|
|
needs_redraw = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- FPS calculation ---
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
let frame_duration = now.duration_since(last_frame_time);
|
|
|
|
|
last_frame_time = now;
|
|
|
|
|
|