register has dropdown now
This commit is contained in:
@@ -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_logic_nav::SaveLogicResultSender;
|
||||||
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
||||||
use crate::functions::modes::navigation::add_table_nav;
|
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::main::logic::handle_admin_navigation;
|
||||||
use crate::pages::admin::admin::tui::handle_admin_selection;
|
use crate::pages::admin::admin::tui::handle_admin_selection;
|
||||||
use crate::modes::general::command_navigation::{
|
use crate::modes::general::command_navigation::{
|
||||||
@@ -44,6 +45,7 @@ use crate::tui::{
|
|||||||
};
|
};
|
||||||
use crate::ui::handlers::context::UiContext;
|
use crate::ui::handlers::context::UiContext;
|
||||||
use canvas::KeyEventOutcome;
|
use canvas::KeyEventOutcome;
|
||||||
|
use canvas::SuggestionsProvider;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
use crossterm::event::{Event, KeyCode};
|
use crossterm::event::{Event, KeyCode};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod logic;
|
pub mod logic;
|
||||||
|
pub mod suggestions;
|
||||||
|
|
||||||
// pub use state::*;
|
// pub use state::*;
|
||||||
pub use ui::render_register;
|
pub use ui::render_register;
|
||||||
|
|||||||
@@ -2,16 +2,10 @@
|
|||||||
|
|
||||||
use canvas::{DataProvider, AppMode, FormEditor};
|
use canvas::{DataProvider, AppMode, FormEditor};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
lazy_static! {
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
pub static ref AVAILABLE_ROLES: Vec<String> = vec![
|
use canvas::keymap::KeyEventOutcome;
|
||||||
"admin".to_string(),
|
use crate::pages::register::suggestions::role_suggestions_sync;
|
||||||
"moderator".to_string(),
|
|
||||||
"accountant".to_string(),
|
|
||||||
"viewer".to_string(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the state of the Registration form UI
|
/// Represents the state of the Registration form UI
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -26,8 +20,6 @@ pub struct RegisterState {
|
|||||||
pub current_cursor_pos: usize,
|
pub current_cursor_pos: usize,
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
pub app_mode: canvas::AppMode,
|
pub app_mode: canvas::AppMode,
|
||||||
pub role_suggestions: Vec<String>,
|
|
||||||
pub role_suggestions_active: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RegisterState {
|
impl Default for RegisterState {
|
||||||
@@ -43,8 +35,6 @@ impl Default for RegisterState {
|
|||||||
current_cursor_pos: 0,
|
current_cursor_pos: 0,
|
||||||
has_unsaved_changes: false,
|
has_unsaved_changes: false,
|
||||||
app_mode: canvas::AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
role_suggestions: AVAILABLE_ROLES.clone(),
|
|
||||||
role_suggestions_active: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,8 +43,6 @@ impl RegisterState {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
app_mode: canvas::AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
role_suggestions: AVAILABLE_ROLES.clone(),
|
|
||||||
role_suggestions_active: false,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,12 +58,6 @@ impl RegisterState {
|
|||||||
pub fn set_current_field(&mut self, index: usize) {
|
pub fn set_current_field(&mut self, index: usize) {
|
||||||
if index < 5 {
|
if index < 5 {
|
||||||
self.current_field = index;
|
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
|
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 {
|
pub fn has_unsaved_changes(&self) -> bool {
|
||||||
self.has_unsaved_changes
|
self.has_unsaved_changes
|
||||||
}
|
}
|
||||||
@@ -141,9 +101,7 @@ impl RegisterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DataProvider for RegisterState {
|
impl DataProvider for RegisterState {
|
||||||
fn field_count(&self) -> usize {
|
fn field_count(&self) -> usize { 5 }
|
||||||
5
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_name(&self, index: usize) -> &str {
|
fn field_name(&self, index: usize) -> &str {
|
||||||
match index {
|
match index {
|
||||||
@@ -308,10 +266,98 @@ impl RegisterFormState {
|
|||||||
pub fn cursor_position(&self) -> usize {
|
pub fn cursor_position(&self) -> usize {
|
||||||
self.editor.cursor_position()
|
self.editor.cursor_position()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_key_event(
|
pub fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
key_event: crossterm::event::KeyEvent,
|
key_event: crossterm::event::KeyEvent,
|
||||||
) -> canvas::keymap::KeyEventOutcome {
|
) -> 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)
|
self.editor.handle_key_event(key_event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
client/src/pages/register/suggestions.rs
Normal file
36
client/src/pages/register/suggestions.rs
Normal file
@@ -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<SuggestionItem> {
|
||||||
|
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<Vec<SuggestionItem>> {
|
||||||
|
if field_index != 4 {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
Ok(role_suggestions_sync(query))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,10 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use crate::dialog;
|
use crate::dialog;
|
||||||
use crate::pages::register::RegisterFormState;
|
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::{render_canvas, render_suggestions_dropdown, DefaultCanvasTheme};
|
||||||
|
use canvas::SuggestionsProvider;
|
||||||
|
|
||||||
pub fn render_register(
|
pub fn render_register(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -49,9 +52,10 @@ pub fn render_register(
|
|||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// ✅ Render the form canvas
|
// Render the form canvas
|
||||||
let input_rect = render_canvas(f, chunks[0], editor, theme);
|
let input_rect = render_canvas(f, chunks[0], editor, theme);
|
||||||
|
|
||||||
|
|
||||||
// --- HELP TEXT ---
|
// --- HELP TEXT ---
|
||||||
let help_text = Paragraph::new("* are optional fields")
|
let help_text = Paragraph::new("* are optional fields")
|
||||||
.style(Style::default().fg(theme.fg))
|
.style(Style::default().fg(theme.fg))
|
||||||
@@ -122,19 +126,6 @@ pub fn render_register(
|
|||||||
button_chunks[1],
|
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 ---
|
// --- DIALOG ---
|
||||||
if app_state.ui.dialog.dialog_show {
|
if app_state.ui.dialog.dialog_show {
|
||||||
dialog::render_dialog(
|
dialog::render_dialog(
|
||||||
@@ -148,4 +139,17 @@ pub fn render_register(
|
|||||||
app_state.ui.dialog.is_loading,
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
client/ui.rs
Normal file
0
client/ui.rs
Normal file
Reference in New Issue
Block a user