Compare commits

..

3 Commits

18 changed files with 472 additions and 224 deletions

View File

@@ -82,8 +82,6 @@ impl TableDependencyGraph {
} }
} }
// ... (NavigationState struct and its new(), activate_*, deactivate(), add_char(), remove_char(), move_*, autocomplete_selected(), get_display_input() methods are unchanged) ...
pub struct NavigationState { pub struct NavigationState {
pub active: bool, pub active: bool,
pub input: String, pub input: String,

View File

@@ -8,7 +8,7 @@ use crate::pages::admin_panel::add_logic;
use crate::pages::admin_panel::add_table; use crate::pages::admin_panel::add_table;
use crate::pages::register::suggestions::RoleSuggestionsProvider; use crate::pages::register::suggestions::RoleSuggestionsProvider;
use crate::pages::admin::main::logic::handle_admin_navigation; use crate::pages::admin::main::logic::handle_admin_navigation;
use crate::pages::admin::admin::tui::handle_admin_selection; use crate::pages::admin::admin;
use crate::modes::general::command_navigation::{ use crate::modes::general::command_navigation::{
handle_command_navigation_event, NavigationState, handle_command_navigation_event, NavigationState,
}; };
@@ -36,6 +36,7 @@ use crate::pages::register::RegisterResult;
use crate::pages::routing::{Router, Page}; use crate::pages::routing::{Router, Page};
use crate::movement::MovementAction; use crate::movement::MovementAction;
use crate::dialog; use crate::dialog;
use crate::pages::forms;
use crate::pages::forms::FormState; use crate::pages::forms::FormState;
use crate::pages::forms::logic::{save, revert, SaveOutcome}; use crate::pages::forms::logic::{save, revert, SaveOutcome};
use crate::search::state::SearchState; use crate::search::state::SearchState;
@@ -304,83 +305,45 @@ impl EventHandler {
if !overlay_active { if !overlay_active {
if let Page::Login(login_page) = &mut router.current { if let Page::Login(login_page) = &mut router.current {
use crossterm::event::{KeyCode, KeyModifiers}; let outcome =
login::event::handle_login_event(event, app_state, login_page)?;
// Inside canvas: at the last field, 'j' or Down moves focus to buttons // Only return if the login page actually consumed the key
if !app_state.ui.focus_outside_canvas { if !outcome.get_message_if_ok().is_empty() {
let last_idx = login_page return Ok(outcome);
.editor
.data_provider()
.field_count()
.saturating_sub(1);
let at_last = login_page.editor.current_field() >= last_idx;
if at_last
&& matches!(
(key_code, modifiers),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
)
{
app_state.ui.focus_outside_canvas = true;
app_state.focused_button_index = 0; // focus "Login" button
// Ensure canvas mode is ReadOnly when leaving
login_page.editor.set_mode(CanvasMode::ReadOnly);
return Ok(EventOutcome::Ok("Focus moved to buttons".to_string()));
}
}
// Only forward to the canvas while focus is inside it
if !app_state.ui.focus_outside_canvas {
match login_page.handle_key_event(key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
self.command_message = msg;
return Ok(EventOutcome::Ok("Login input updated".to_string()));
}
KeyEventOutcome::Consumed(None) => {
return Ok(EventOutcome::Ok("Login input updated".to_string()));
}
KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok("Waiting for next key...".to_string()));
}
KeyEventOutcome::NotMatched => {
// fall through to other handlers (buttons, etc.)
}
}
} }
} else if let Page::Register(register_page) = &mut router.current { } else if let Page::Register(register_page) = &mut router.current {
use crossterm::event::{KeyCode, KeyModifiers}; let outcome = crate::pages::register::event::handle_register_event(
event,
// Inside canvas: at the last field, 'j' or Down moves focus to buttons app_state,
if !app_state.ui.focus_outside_canvas { register_page,
let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1); )?;
let at_last = register_page.editor.current_field() >= last_idx; // Only stop if page actually consumed the key; else fall through to global handling
if at_last if !outcome.get_message_if_ok().is_empty() {
&& matches!( return Ok(outcome);
(key_code, modifiers),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
)
{
app_state.ui.focus_outside_canvas = true;
app_state.focused_button_index = 0; // focus "Register" button
register_page.editor.set_mode(CanvasMode::ReadOnly);
return Ok(EventOutcome::Ok("Focus moved to buttons".to_string()));
}
} }
} else if let Page::Form(path) = &router.current {
// Only forward to the canvas while focus is inside it let outcome = forms::event::handle_form_event(
if !app_state.ui.focus_outside_canvas { event,
match register_page.handle_key_event(key_event) { app_state,
KeyEventOutcome::Consumed(Some(msg)) => { path,
self.command_message = msg; &mut self.ideal_cursor_column,
return Ok(EventOutcome::Ok("Register input updated".to_string())); )?;
} // Only return if the form page actually consumed the key
KeyEventOutcome::Consumed(None) => { if !outcome.get_message_if_ok().is_empty() {
return Ok(EventOutcome::Ok("Register input updated".to_string())); return Ok(outcome);
} }
KeyEventOutcome::Pending => { } else if let Page::Admin(admin_state) = &mut router.current {
return Ok(EventOutcome::Ok("Waiting for next key...".to_string())); if matches!(auth_state.role, Some(UserRole::Admin)) {
} if let Event::Key(key_event) = event {
KeyEventOutcome::NotMatched => { if admin::event::handle_admin_event(
// fall through key_event,
config,
app_state,
admin_state,
buffer_state,
&mut self.command_message,
)? {
return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
} }
} }
@@ -499,19 +462,6 @@ impl EventHandler {
match &mut router.current { match &mut router.current {
// LOGIN: From buttons (general) back into the canvas with 'k' (Up), // LOGIN: From buttons (general) back into the canvas with 'k' (Up),
// but ONLY from the left-most "Login" button. // but ONLY from the left-most "Login" button.
Page::Login(page) => {
if app_state.ui.focus_outside_canvas {
if app_state.focused_button_index == 0
&& matches!(ma, crate::movement::MovementAction::Up)
{
app_state.ui.focus_outside_canvas = false;
// Enter canvas in ReadOnly mode (never jump straight to Edit)
page.editor.set_mode(CanvasMode::ReadOnly);
// Optional: keep current field (usually 0 initially)
return Ok(EventOutcome::Ok(String::new()));
}
}
}
Page::AddTable(state) => { Page::AddTable(state) => {
if state.handle_movement(ma) { if state.handle_movement(ma) {
// Keep UI focus consistent with inputs vs. outer elements // Keep UI focus consistent with inputs vs. outer elements
@@ -526,13 +476,6 @@ impl EventHandler {
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
} }
Page::Admin(state) => {
if matches!(auth_state.role, Some(UserRole::Admin)) {
if state.handle_movement(app_state, ma) {
return Ok(EventOutcome::Ok(String::new()));
}
}
}
Page::Intro(state) => { Page::Intro(state) => {
if state.handle_movement(ma) { if state.handle_movement(ma) {
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
@@ -543,22 +486,6 @@ impl EventHandler {
} }
// Optional page-specific handlers (non-movement or rich actions) // Optional page-specific handlers (non-movement or rich actions)
if let Page::Admin(admin_state) = &mut router.current {
if matches!(auth_state.role, Some(UserRole::Admin)) {
// Full admin navigation
if handle_admin_navigation(
key_event,
config,
app_state,
admin_state,
buffer_state,
&mut self.command_message,
) {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
}
}
let client_clone = self.grpc_client.clone(); let client_clone = self.grpc_client.clone();
let sender_clone = self.save_logic_result_sender.clone(); let sender_clone = self.save_logic_result_sender.clone();
if add_logic::nav::handle_add_logic_navigation( if add_logic::nav::handle_add_logic_navigation(
@@ -671,7 +598,7 @@ impl EventHandler {
} }
UiContext::Admin => { UiContext::Admin => {
if let Page::Admin(admin_state) = &router.current { if let Page::Admin(admin_state) = &router.current {
handle_admin_selection( admin::tui::handle_admin_selection(
app_state, app_state,
admin_state, admin_state,
); );
@@ -858,25 +785,14 @@ impl EventHandler {
&mut self.auth_client, &mut self.auth_client,
app_state, app_state,
) )
.await?; .await?;
Ok(EventOutcome::Ok(message)) Ok(EventOutcome::Ok(message))
} else { } else {
let save_outcome = if let Page::Form(path) = &router.current { if let Page::Form(path) = &router.current {
save( forms::event::save_form(app_state, path, &mut self.grpc_client).await
app_state,
path,
&mut self.grpc_client,
)
.await?
} else { } else {
SaveOutcome::NoChange Ok(EventOutcome::Ok("Nothing to save".to_string()))
}; }
let message = match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
};
Ok(EventOutcome::DataSaved(save_outcome, message))
} }
} }
"force_quit" => { "force_quit" => {
@@ -898,19 +814,18 @@ impl EventHandler {
&mut self.auth_client, &mut self.auth_client,
app_state, app_state,
) )
.await? .await?
} else { } else if let Page::Form(path) = &router.current {
let save_outcome = if let Page::Form(path) = &router.current { let save_result = forms::event::save_form(app_state, path, &mut self.grpc_client).await?;
save(app_state, path, &mut self.grpc_client).await? match save_result {
} else { EventOutcome::DataSaved(_, msg) => msg,
SaveOutcome::NoChange EventOutcome::Ok(msg) => msg,
}; _ => "Saved".to_string(),
match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
} }
} else {
"No changes to save.".to_string()
}; };
if let Page::Form(path) = &router.current { if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) { if let Some(editor) = app_state.editor_for_path(path) {
editor.cleanup_cursor()?; editor.cleanup_cursor()?;
@@ -918,8 +833,8 @@ impl EventHandler {
} }
terminal.cleanup()?; terminal.cleanup()?;
Ok(EventOutcome::Exit(format!( Ok(EventOutcome::Exit(format!(
"{}. Exiting application.", "{}. Exiting application.",
message message
))) )))
} }
"revert" => { "revert" => {
@@ -933,7 +848,7 @@ impl EventHandler {
.await .await
} else { } else {
if let Page::Form(path) = &router.current { if let Page::Form(path) = &router.current {
revert(app_state, path, &mut self.grpc_client).await? return forms::event::revert_form(app_state, path, &mut self.grpc_client).await;
} else { } else {
"Nothing to revert".to_string() "Nothing to revert".to_string()
} }

View File

@@ -0,0 +1,60 @@
// src/pages/admin/admin/event.rs
use anyhow::Result;
use crossterm::event::KeyEvent;
use crate::buffer::state::BufferState;
use crate::config::binds::config::Config;
use crate::pages::admin::AdminState;
use crate::pages::admin::main::logic::handle_admin_navigation;
use crate::state::app::state::AppState;
/// Handle all Admin page-specific key events (movement + actions).
/// Returns true if the key was handled (so the caller should stop propagation).
pub fn handle_admin_event(
key_event: KeyEvent,
config: &Config,
app_state: &mut AppState,
admin_state: &mut AdminState,
buffer_state: &mut BufferState,
command_message: &mut String,
) -> Result<bool> {
// 1) Map general action to MovementAction (same mapping used in event.rs)
let movement_action = if let Some(act) =
config.get_general_action(key_event.code, key_event.modifiers)
{
use crate::movement::MovementAction;
match act {
"up" => Some(MovementAction::Up),
"down" => Some(MovementAction::Down),
"left" => Some(MovementAction::Left),
"right" => Some(MovementAction::Right),
"next" => Some(MovementAction::Next),
"previous" => Some(MovementAction::Previous),
"select" => Some(MovementAction::Select),
"esc" => Some(MovementAction::Esc),
_ => None,
}
} else {
None
};
if let Some(ma) = movement_action {
if admin_state.handle_movement(app_state, ma) {
return Ok(true);
}
}
// 2) Rich Admin navigation (buttons, selections, etc.)
if handle_admin_navigation(
key_event,
config,
app_state,
admin_state,
buffer_state,
command_message,
) {
return Ok(true);
}
Ok(false)
}

View File

@@ -0,0 +1,54 @@
// src/pages/admin/admin/loader.rs
use anyhow::{Context, Result};
use crate::pages::admin::{AdminFocus, AdminState};
use crate::services::grpc_client::GrpcClient;
use crate::state::app::state::AppState;
/// Refresh admin data and ensure focus and selections are valid.
pub async fn refresh_admin_state(
grpc_client: &mut GrpcClient,
app_state: &mut AppState,
admin_state: &mut AdminState,
) -> Result<()> {
// Fetch latest profile tree
let refreshed_tree = grpc_client
.get_profile_tree()
.await
.context("Failed to refresh profile tree for Admin panel")?;
app_state.profile_tree = refreshed_tree;
// Populate profile names for AdminState's list
let profile_names = app_state
.profile_tree
.profiles
.iter()
.map(|p| p.name.clone())
.collect::<Vec<_>>();
admin_state.set_profiles(profile_names);
// Ensure a sane focus
if admin_state.current_focus == AdminFocus::default()
|| !matches!(
admin_state.current_focus,
AdminFocus::InsideProfilesList
| AdminFocus::Tables
| AdminFocus::InsideTablesList
| AdminFocus::Button1
| AdminFocus::Button2
| AdminFocus::Button3
| AdminFocus::ProfilesPane
)
{
admin_state.current_focus = AdminFocus::ProfilesPane;
}
// Ensure a selection exists when profiles are present
if admin_state.profile_list_state.selected().is_none()
&& !app_state.profile_tree.profiles.is_empty()
{
admin_state.profile_list_state.select(Some(0));
}
Ok(())
}

View File

@@ -3,5 +3,7 @@
pub mod state; pub mod state;
pub mod ui; pub mod ui;
pub mod tui; pub mod tui;
pub mod event;
pub mod loader;
pub use state::{AdminState, AdminFocus}; pub use state::{AdminState, AdminFocus};

View File

@@ -0,0 +1,62 @@
// src/pages/forms/event.rs
use anyhow::Result;
use crossterm::event::Event;
use canvas::keymap::KeyEventOutcome;
use crate::{
state::app::state::AppState,
pages::forms::{FormState, logic},
modes::handlers::event::EventOutcome,
};
pub fn handle_form_event(
event: Event,
app_state: &mut AppState,
path: &str,
ideal_cursor_column: &mut usize,
) -> Result<EventOutcome> {
if let Event::Key(key_event) = event {
if let Some(editor) = app_state.editor_for_path(path) {
match editor.handle_key_event(key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
return Ok(EventOutcome::Ok(msg));
}
KeyEventOutcome::Consumed(None) => {
return Ok(EventOutcome::Ok("Form input updated".into()));
}
KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok("Waiting for next key...".into()));
}
KeyEventOutcome::NotMatched => {
// fall through to navigation / save / revert
}
}
}
}
Ok(EventOutcome::Ok(String::new()))
}
// Save wrapper
pub async fn save_form(
app_state: &mut AppState,
path: &str,
grpc_client: &mut crate::services::grpc_client::GrpcClient,
) -> Result<EventOutcome> {
let outcome = logic::save(app_state, path, grpc_client).await?;
let message = match outcome {
logic::SaveOutcome::NoChange => "No changes to save.".to_string(),
logic::SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
logic::SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
};
Ok(EventOutcome::DataSaved(outcome, message))
}
pub async fn revert_form(
app_state: &mut AppState,
path: &str,
grpc_client: &mut crate::services::grpc_client::GrpcClient,
) -> Result<EventOutcome> {
let message = logic::revert(app_state, path, grpc_client).await?;
Ok(EventOutcome::Ok(message))
}

View File

@@ -0,0 +1,39 @@
// src/pages/forms/loader.rs
use anyhow::{Context, Result};
use crate::{
state::app::state::AppState,
services::grpc_client::GrpcClient,
services::ui_service::UiService, // ✅ import UiService
config::binds::Config,
pages::forms::FormState,
};
pub async fn ensure_form_loaded_and_count(
grpc_client: &mut GrpcClient,
app_state: &mut AppState,
config: &Config,
profile: &str,
table: &str,
) -> Result<()> {
let path = format!("{}/{}", profile, table);
app_state.ensure_form_editor(&path, config, || {
FormState::new(profile.to_string(), table.to_string(), vec![])
});
if let Some(form_state) = app_state.form_state_for_path(&path) {
UiService::fetch_and_set_table_count(grpc_client, form_state)
.await
.context("Failed to fetch table count")?;
if form_state.total_count > 0 {
UiService::load_table_data_by_position(grpc_client, form_state)
.await
.context("Failed to load table data")?;
} else {
form_state.reset_to_empty();
}
}
Ok(())
}

View File

@@ -3,7 +3,11 @@
pub mod ui; pub mod ui;
pub mod state; pub mod state;
pub mod logic; pub mod logic;
pub mod event;
pub mod loader;
pub use ui::*; pub use ui::*;
pub use state::*; pub use state::*;
pub use logic::*; pub use logic::*;
pub use event::*;
pub use loader::*;

View File

@@ -0,0 +1,73 @@
// src/pages/login/event.rs
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyModifiers};
use canvas::{keymap::KeyEventOutcome, AppMode as CanvasMode};
use crate::{
state::app::state::AppState,
pages::login::LoginFormState,
modes::handlers::event::EventOutcome,
};
use canvas::DataProvider;
/// Handles all Login page-specific events
pub fn handle_login_event(
event: Event,
app_state: &mut AppState,
login_page: &mut LoginFormState,
) -> Result<EventOutcome> {
if let Event::Key(key_event) = event {
let key_code = key_event.code;
let modifiers = key_event.modifiers;
// From buttons (outside) back into the canvas (ReadOnly) with Up/k from the left-most button
if login_page.focus_outside_canvas
&& login_page.focused_button_index == 0
&& matches!(key_code, KeyCode::Up | KeyCode::Char('k'))
&& modifiers.is_empty()
{
login_page.focus_outside_canvas = false;
app_state.ui.focus_outside_canvas = false; // 🔑 keep global in sync
login_page.editor.set_mode(CanvasMode::ReadOnly);
return Ok(EventOutcome::Ok(String::new()));
}
// Focus handoff: inside canvas → buttons
if !login_page.focus_outside_canvas {
let last_idx = login_page.editor.data_provider().field_count().saturating_sub(1);
let at_last = login_page.editor.current_field() >= last_idx;
if at_last
&& matches!(
(key_code, modifiers),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
)
{
login_page.focus_outside_canvas = true;
login_page.focused_button_index = 0; // focus "Login" button
app_state.ui.focus_outside_canvas = true;
app_state.focused_button_index = 0;
login_page.editor.set_mode(CanvasMode::ReadOnly);
return Ok(EventOutcome::Ok("Focus moved to buttons".into()));
}
}
// Forward to canvas if focus is inside
if !login_page.focus_outside_canvas {
match login_page.handle_key_event(key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
return Ok(EventOutcome::Ok(msg));
}
KeyEventOutcome::Consumed(None) => {
return Ok(EventOutcome::Ok("Login input updated".into()));
}
KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok("Waiting for next key...".into()));
}
KeyEventOutcome::NotMatched => {
// fall through to button handling
}
}
}
}
Ok(EventOutcome::Ok(String::new()))
}

View File

@@ -3,7 +3,9 @@
pub mod state; pub mod state;
pub mod ui; pub mod ui;
pub mod logic; pub mod logic;
pub mod event;
pub use state::*; pub use state::*;
pub use ui::render_login; pub use ui::render_login;
pub use logic::*; pub use logic::*;
pub use event::*;

View File

@@ -127,6 +127,8 @@ impl DataProvider for LoginState {
pub struct LoginFormState { pub struct LoginFormState {
pub state: LoginState, pub state: LoginState,
pub editor: FormEditor<LoginState>, pub editor: FormEditor<LoginState>,
pub focus_outside_canvas: bool,
pub focused_button_index: usize,
} }
// manual debug because FormEditor doesnt implement debug // manual debug because FormEditor doesnt implement debug
@@ -150,7 +152,12 @@ impl LoginFormState {
pub fn new() -> Self { pub fn new() -> Self {
let state = LoginState::default(); let state = LoginState::default();
let editor = FormEditor::new(state.clone()); let editor = FormEditor::new(state.clone());
Self { state, editor } Self {
state,
editor,
focus_outside_canvas: false,
focused_button_index: 0,
}
} }
// === Delegates to LoginState fields === // === Delegates to LoginState fields ===

View File

@@ -80,7 +80,7 @@ pub fn render_login(
// Login Button // Login Button
let login_button_index = 0; let login_button_index = 0;
let login_active = if app_state.ui.focus_outside_canvas { let login_active = if login_page.focus_outside_canvas {
app_state.focused_button_index == login_button_index app_state.focused_button_index == login_button_index
} else { } else {
false false

View File

@@ -0,0 +1,76 @@
// src/pages/register/event.rs
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyModifiers};
use canvas::{keymap::KeyEventOutcome, AppMode as CanvasMode};
use canvas::DataProvider;
use crate::{
state::app::state::AppState,
pages::register::RegisterFormState,
modes::handlers::event::EventOutcome,
};
/// Handles all Register page-specific events.
/// Return a non-empty Ok(message) only when the page actually consumed the key,
/// otherwise return Ok("") to let global handling proceed.
pub fn handle_register_event(
event: Event,
app_state: &mut AppState,
register_page: &mut RegisterFormState,
)-> Result<EventOutcome> {
if let Event::Key(key_event) = event {
let key_code = key_event.code;
let modifiers = key_event.modifiers;
// From buttons (outside) back into the canvas (ReadOnly) with Up/k from the left-most button
if register_page.focus_outside_canvas
&& register_page.focused_button_index == 0
&& matches!(key_code, KeyCode::Up | KeyCode::Char('k'))
&& modifiers.is_empty()
{
register_page.focus_outside_canvas = false;
// Keep global in sync for now (cursor styling elsewhere still reads it)
app_state.ui.focus_outside_canvas = false;
register_page.editor.set_mode(CanvasMode::ReadOnly);
return Ok(EventOutcome::Ok(String::new()));
}
// Focus handoff: inside canvas → buttons
if !register_page.focus_outside_canvas {
let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1);
let at_last = register_page.editor.current_field() >= last_idx;
if at_last
&& matches!(
(key_code, modifiers),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
)
{
register_page.focus_outside_canvas = true;
register_page.focused_button_index = 0; // focus "Register" button
// Keep global in sync for now
app_state.ui.focus_outside_canvas = true;
app_state.focused_button_index = 0;
register_page.editor.set_mode(CanvasMode::ReadOnly);
return Ok(EventOutcome::Ok("Focus moved to buttons".into()));
}
}
// Forward to canvas if focus is inside
if !register_page.focus_outside_canvas {
match register_page.handle_key_event(key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
return Ok(EventOutcome::Ok(msg));
}
KeyEventOutcome::Consumed(None) => {
return Ok(EventOutcome::Ok("Register input updated".into()));
}
KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok("Waiting for next key...".into()));
}
KeyEventOutcome::NotMatched => {
// fall through
}
}
}
}
Ok(EventOutcome::Ok(String::new()))
}

View File

@@ -54,6 +54,8 @@ pub async fn back_to_login(
buffer_state.update_history(AppView::Login); buffer_state.update_history(AppView::Login);
// Reset focus state // Reset focus state
register_state.focus_outside_canvas = false;
register_state.focused_button_index = 0;
app_state.ui.focus_outside_canvas = false; app_state.ui.focus_outside_canvas = false;
app_state.focused_button_index = 0; app_state.focused_button_index = 0;

View File

@@ -5,8 +5,10 @@ pub mod ui;
pub mod state; pub mod state;
pub mod logic; pub mod logic;
pub mod suggestions; pub mod suggestions;
pub mod event;
// pub use state::*; // pub use state::*;
pub use ui::render_register; pub use ui::render_register;
pub use logic::*; pub use logic::*;
pub use state::*; pub use state::*;
pub use event::*;

View File

@@ -146,6 +146,8 @@ impl DataProvider for RegisterState {
pub struct RegisterFormState { pub struct RegisterFormState {
pub state: RegisterState, pub state: RegisterState,
pub editor: FormEditor<RegisterState>, pub editor: FormEditor<RegisterState>,
pub focus_outside_canvas: bool,
pub focused_button_index: usize,
} }
impl Default for RegisterFormState { impl Default for RegisterFormState {
@@ -174,7 +176,12 @@ impl RegisterFormState {
pub fn new() -> Self { pub fn new() -> Self {
let state = RegisterState::default(); let state = RegisterState::default();
let editor = FormEditor::new(state.clone()); let editor = FormEditor::new(state.clone());
Self { state, editor } Self {
state,
editor,
focus_outside_canvas: false,
focused_button_index: 0,
}
} }
// === Delegates to RegisterState === // === Delegates to RegisterState ===

View File

@@ -80,8 +80,9 @@ pub fn render_register(
// Register Button // Register Button
let register_button_index = 0; let register_button_index = 0;
let register_active = app_state.ui.focus_outside_canvas let register_active =
&& app_state.focused_button_index == register_button_index; register_page.focus_outside_canvas
&& register_page.focused_button_index == register_button_index;
let mut register_style = Style::default().fg(theme.fg); let mut register_style = Style::default().fg(theme.fg);
let mut register_border = Style::default().fg(theme.border); let mut register_border = Style::default().fg(theme.border);
if register_active { if register_active {
@@ -104,8 +105,9 @@ pub fn render_register(
// Return Button // Return Button
let return_button_index = 1; let return_button_index = 1;
let return_active = app_state.ui.focus_outside_canvas let return_active =
&& app_state.focused_button_index == return_button_index; register_page.focus_outside_canvas
&& register_page.focused_button_index == return_button_index;
let mut return_style = Style::default().fg(theme.fg); let mut return_style = Style::default().fg(theme.fg);
let mut return_border = Style::default().fg(theme.border); let mut return_border = Style::default().fg(theme.border);
if return_active { if return_active {

View File

@@ -14,8 +14,10 @@ use crate::pages::login::LoginFormState;
use crate::pages::register::RegisterFormState; use crate::pages::register::RegisterFormState;
use crate::pages::admin::AdminState; use crate::pages::admin::AdminState;
use crate::pages::admin::AdminFocus; use crate::pages::admin::AdminFocus;
use crate::pages::admin::admin;
use crate::pages::intro::IntroState; use crate::pages::intro::IntroState;
use crate::pages::forms::{FormState, FieldDefinition}; use crate::pages::forms::{FormState, FieldDefinition};
use crate::pages::forms;
use crate::pages::routing::{Router, Page}; use crate::pages::routing::{Router, Page};
use crate::buffer::state::BufferState; use crate::buffer::state::BufferState;
use crate::buffer::state::AppView; use crate::buffer::state::AppView;
@@ -412,33 +414,11 @@ pub async fn run_ui() -> Result<()> {
admin_state = current.clone(); admin_state = current.clone();
} }
info!("Auth role at render: {:?}", auth_state.role); info!("Auth role at render: {:?}", auth_state.role);
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);
}
}
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() // Use the admin loader instead of inline logic
|| !matches!(admin_state.current_focus, if let Err(e) = admin::loader::refresh_admin_state(&mut grpc_client, &mut app_state, &mut admin_state).await {
AdminFocus::InsideProfilesList | error!("Failed to refresh admin state: {}", e);
AdminFocus::Tables | AdminFocus::InsideTablesList | event_handler.command_message = format!("Error refreshing admin data: {}", e);
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));
} }
router.navigate(Page::Admin(admin_state.clone())); router.navigate(Page::Admin(admin_state.clone()));
@@ -459,7 +439,7 @@ pub async fn run_ui() -> Result<()> {
} }
} }
if let Page::Form(current_path) = &router.current { if let Page::Form(_current_path) = &router.current {
let current_view_profile = app_state.current_view_profile_name.clone(); let current_view_profile = app_state.current_view_profile_name.clone();
let current_view_table = app_state.current_view_table_name.clone(); let current_view_table = app_state.current_view_table_name.clone();
@@ -475,52 +455,16 @@ pub async fn run_ui() -> Result<()> {
); );
needs_redraw = true; needs_redraw = true;
match UiService::load_table_view( // DELEGATE to the forms loader
match forms::loader::ensure_form_loaded_and_count(
&mut grpc_client, &mut grpc_client,
&mut app_state, &mut app_state,
&config,
prof_name, prof_name,
tbl_name, tbl_name,
) ).await {
.await Ok(()) => {
{ app_state.hide_dialog();
Ok(new_form_state) => {
// Set the new form state and fetch count
let path = format!("{}/{}", prof_name, tbl_name);
app_state.ensure_form_editor(&path, &config, || new_form_state);
if let Some(form_state) = app_state.form_state_for_path(&path) {
if let Err(e) = UiService::fetch_and_set_table_count(
&mut grpc_client,
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,
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();
}
}
prev_view_profile_name = current_view_profile; prev_view_profile_name = current_view_profile;
prev_view_table_name = current_view_table; prev_view_table_name = current_view_table;
table_just_switched = true; table_just_switched = true;
@@ -531,10 +475,9 @@ pub async fn run_ui() -> Result<()> {
vec!["OK".to_string()], vec!["OK".to_string()],
DialogPurpose::LoginFailed, DialogPurpose::LoginFailed,
); );
app_state.current_view_profile_name = // Reset to previous state on error
prev_view_profile_name.clone(); app_state.current_view_profile_name = prev_view_profile_name.clone();
app_state.current_view_table_name = app_state.current_view_table_name = prev_view_table_name.clone();
prev_view_table_name.clone();
} }
} }
} }