diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 13d954e..05699b4 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -8,6 +8,7 @@ use crate::functions::modes::navigation::add_logic_nav; use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender; use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; use crate::functions::modes::navigation::add_table_nav; +use crate::pages::register::suggestions::RoleSuggestionsProvider; use crate::pages::admin::main::logic::handle_admin_navigation; use crate::pages::admin::admin::tui::handle_admin_selection; use crate::modes::general::command_navigation::{ @@ -44,6 +45,7 @@ use crate::tui::{ }; use crate::ui::handlers::context::UiContext; use canvas::KeyEventOutcome; +use canvas::SuggestionsProvider; use anyhow::Result; use common::proto::komp_ac::search::search_response::Hit; use crossterm::event::{Event, KeyCode}; diff --git a/client/src/pages/register/mod.rs b/client/src/pages/register/mod.rs index 8d39eb0..8ec204d 100644 --- a/client/src/pages/register/mod.rs +++ b/client/src/pages/register/mod.rs @@ -4,6 +4,7 @@ pub mod ui; pub mod state; pub mod logic; +pub mod suggestions; // pub use state::*; pub use ui::render_register; diff --git a/client/src/pages/register/state.rs b/client/src/pages/register/state.rs index faa41bf..aefe612 100644 --- a/client/src/pages/register/state.rs +++ b/client/src/pages/register/state.rs @@ -2,16 +2,10 @@ use canvas::{DataProvider, AppMode, FormEditor}; use std::fmt; -use lazy_static::lazy_static; -lazy_static! { - pub static ref AVAILABLE_ROLES: Vec = vec![ - "admin".to_string(), - "moderator".to_string(), - "accountant".to_string(), - "viewer".to_string(), - ]; -} +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use canvas::keymap::KeyEventOutcome; +use crate::pages::register::suggestions::role_suggestions_sync; /// Represents the state of the Registration form UI #[derive(Debug, Clone)] @@ -26,8 +20,6 @@ pub struct RegisterState { pub current_cursor_pos: usize, pub has_unsaved_changes: bool, pub app_mode: canvas::AppMode, - pub role_suggestions: Vec, - pub role_suggestions_active: bool, } impl Default for RegisterState { @@ -43,8 +35,6 @@ impl Default for RegisterState { current_cursor_pos: 0, has_unsaved_changes: false, app_mode: canvas::AppMode::Edit, - role_suggestions: AVAILABLE_ROLES.clone(), - role_suggestions_active: false, } } } @@ -53,8 +43,6 @@ impl RegisterState { pub fn new() -> Self { Self { app_mode: canvas::AppMode::Edit, - role_suggestions: AVAILABLE_ROLES.clone(), - role_suggestions_active: false, ..Default::default() } } @@ -70,12 +58,6 @@ impl RegisterState { pub fn set_current_field(&mut self, index: usize) { if index < 5 { self.current_field = index; - - if index == 4 { - self.activate_role_suggestions(); - } else { - self.deactivate_role_suggestions(); - } } } @@ -109,28 +91,6 @@ impl RegisterState { self.app_mode } - pub fn activate_role_suggestions(&mut self) { - self.role_suggestions_active = true; - let current_input = self.role.to_lowercase(); - self.role_suggestions = AVAILABLE_ROLES - .iter() - .filter(|role| role.to_lowercase().contains(¤t_input)) - .cloned() - .collect(); - } - - pub fn deactivate_role_suggestions(&mut self) { - self.role_suggestions_active = false; - } - - pub fn is_role_suggestions_active(&self) -> bool { - self.role_suggestions_active - } - - pub fn get_role_suggestions(&self) -> &[String] { - &self.role_suggestions - } - pub fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } @@ -141,9 +101,7 @@ impl RegisterState { } impl DataProvider for RegisterState { - fn field_count(&self) -> usize { - 5 - } + fn field_count(&self) -> usize { 5 } fn field_name(&self, index: usize) -> &str { match index { @@ -308,10 +266,98 @@ impl RegisterFormState { 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 { + // Only customize behavior for the Role field (index 4) in Edit mode + let in_role_field = self.editor.current_field() == 4; + let in_edit_mode = self.editor.mode() == canvas::AppMode::Edit; + + if in_role_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(4) { + let items = role_suggestions_sync(&query); + let applied = + self.editor.apply_suggestions_result(4, &query, items); + if applied { + self.editor.update_inline_completion(); + } + } + } else { + // Cycle to next suggestion + self.editor.suggestions_next(); + } + return KeyEventOutcome::Consumed(None); + } + + // Shift+Tab (BackTab): cycle suggestions too (fallback to next) + KeyCode::BackTab => { + if self.editor.is_suggestions_active() { + // If your canvas exposes suggestions_prev(), use it here. + // Fallback: cycle next. + self.editor.suggestions_next(); + return KeyEventOutcome::Consumed(None); + } + } + + // Enter: if suggestions active — apply selected suggestion + KeyCode::Enter => { + if self.editor.is_suggestions_active() { + let _ = self.editor.apply_suggestion(); + return KeyEventOutcome::Consumed(None); + } + } + + // Esc: close suggestions if active + KeyCode::Esc => { + if self.editor.is_suggestions_active() { + self.editor.close_suggestions(); + return KeyEventOutcome::Consumed(None); + } + } + + // Character input: first let editor mutate text, then refilter 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(4) { + let items = role_suggestions_sync(&query); + let applied = + self.editor.apply_suggestions_result(4, &query, items); + if applied { + self.editor.update_inline_completion(); + } + } + } + return outcome; + } + + // Backspace/Delete: mutate then refilter 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(4) { + let items = role_suggestions_sync(&query); + let applied = + self.editor.apply_suggestions_result(4, &query, items); + if applied { + self.editor.update_inline_completion(); + } + } + } + return outcome; + } + + _ => { /* fall through to default */ } + } + } + + // Default: let canvas handle it self.editor.handle_key_event(key_event) } } diff --git a/client/src/pages/register/suggestions.rs b/client/src/pages/register/suggestions.rs new file mode 100644 index 0000000..0ec12cc --- /dev/null +++ b/client/src/pages/register/suggestions.rs @@ -0,0 +1,36 @@ +// src/pages/register/suggestions.rs + +use anyhow::Result; +use async_trait::async_trait; +use canvas::{SuggestionItem, SuggestionsProvider}; + +// Keep the async provider if you want, but add this sync helper and shared data. +const ROLES: &[&str] = &["admin", "moderator", "accountant", "viewer"]; + +pub fn role_suggestions_sync(query: &str) -> Vec { + let q = query.to_lowercase(); + ROLES + .iter() + .filter(|r| q.is_empty() || r.to_lowercase().contains(&q)) + .map(|r| SuggestionItem { + display_text: (*r).to_string(), + value_to_store: (*r).to_string(), + }) + .collect() +} + +pub struct RoleSuggestionsProvider; + +#[async_trait] +impl SuggestionsProvider for RoleSuggestionsProvider { + async fn fetch_suggestions( + &mut self, + field_index: usize, + query: &str, + ) -> Result> { + if field_index != 4 { + return Ok(Vec::new()); + } + Ok(role_suggestions_sync(query)) + } +} diff --git a/client/src/pages/register/ui.rs b/client/src/pages/register/ui.rs index 6bf4a72..e61908c 100644 --- a/client/src/pages/register/ui.rs +++ b/client/src/pages/register/ui.rs @@ -12,7 +12,10 @@ use ratatui::{ }; use crate::dialog; use crate::pages::register::RegisterFormState; +use crate::pages::register::suggestions::RoleSuggestionsProvider; +use tokio::runtime::Handle; use canvas::{render_canvas, render_suggestions_dropdown, DefaultCanvasTheme}; +use canvas::SuggestionsProvider; pub fn render_register( f: &mut Frame, @@ -49,9 +52,10 @@ pub fn render_register( ]) .split(inner_area); - // ✅ Render the form canvas + // Render the form canvas let input_rect = render_canvas(f, chunks[0], editor, theme); + // --- HELP TEXT --- let help_text = Paragraph::new("* are optional fields") .style(Style::default().fg(theme.fg)) @@ -122,19 +126,6 @@ pub fn render_register( button_chunks[1], ); - // --- AUTOCOMPLETE DROPDOWN --- - if editor.mode() == canvas::AppMode::Edit { - if let Some(input_rect) = input_rect { - render_suggestions_dropdown( - f, - f.area(), - input_rect, - &DefaultCanvasTheme, - editor, - ); - } - } - // --- DIALOG --- if app_state.ui.dialog.dialog_show { dialog::render_dialog( @@ -148,4 +139,17 @@ pub fn render_register( app_state.ui.dialog.is_loading, ); } + + // Render suggestions dropdown if active (library GUI) + if editor.mode() == canvas::AppMode::Edit { + if let Some(input_rect) = input_rect { + render_suggestions_dropdown( + f, + f.area(), + input_rect, + &DefaultCanvasTheme, + editor, + ); + } + } } diff --git a/client/ui.rs b/client/ui.rs new file mode 100644 index 0000000..e69de29