login page using canvas for forms
This commit is contained in:
@@ -34,7 +34,7 @@ impl CommandHandler {
|
|||||||
) -> Result<(bool, String)> {
|
) -> Result<(bool, String)> {
|
||||||
// Use router to check unsaved changes
|
// Use router to check unsaved changes
|
||||||
let has_unsaved = match &router.current {
|
let has_unsaved = match &router.current {
|
||||||
Page::Login(state) => state.has_unsaved_changes(),
|
Page::Login(page) => page.state.has_unsaved_changes(),
|
||||||
Page::Register(state) => state.has_unsaved_changes(),
|
Page::Register(state) => state.has_unsaved_changes(),
|
||||||
Page::Form(fs) => fs.has_unsaved_changes,
|
Page::Form(fs) => fs.has_unsaved_changes,
|
||||||
_ => false,
|
_ => false,
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ pub async fn handle_navigation_event(
|
|||||||
|
|
||||||
pub fn up(app_state: &mut AppState, router: &mut Router) {
|
pub fn up(app_state: &mut AppState, router: &mut Router) {
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
Page::Login(state) if app_state.ui.focus_outside_canvas => {
|
Page::Login(page) if app_state.ui.focus_outside_canvas => {
|
||||||
if app_state.focused_button_index == 0 {
|
if app_state.focused_button_index == 0 {
|
||||||
app_state.ui.focus_outside_canvas = false;
|
app_state.ui.focus_outside_canvas = false;
|
||||||
let last_field_index = state.field_count().saturating_sub(1);
|
let last_field_index = page.state.field_count().saturating_sub(1);
|
||||||
state.set_current_field(last_field_index);
|
page.state.set_current_field(last_field_index);
|
||||||
} else {
|
} else {
|
||||||
app_state.focused_button_index =
|
app_state.focused_button_index =
|
||||||
app_state.focused_button_index.saturating_sub(1);
|
app_state.focused_button_index.saturating_sub(1);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/tui/functions/common/login.rs
|
// src/pages/login/logic.rs
|
||||||
|
|
||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
@@ -7,12 +7,11 @@ use crate::buffer::state::{AppView, BufferState};
|
|||||||
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
|
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use common::proto::komp_ac::auth::LoginResponse;
|
use common::proto::komp_ac::auth::LoginResponse;
|
||||||
use crate::pages::login::LoginState;
|
use crate::pages::login::LoginFormState;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{info, error};
|
use tracing::{info, error};
|
||||||
use anyhow::anyhow;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginResult {
|
pub enum LoginResult {
|
||||||
@@ -25,15 +24,14 @@ pub enum LoginResult {
|
|||||||
/// Updates AuthState and AppState on success or failure.
|
/// Updates AuthState and AppState on success or failure.
|
||||||
pub async fn save(
|
pub async fn save(
|
||||||
auth_state: &mut AuthState,
|
auth_state: &mut AuthState,
|
||||||
login_state: &mut LoginState,
|
login_state: &mut LoginFormState,
|
||||||
auth_client: &mut AuthClient,
|
auth_client: &mut AuthClient,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let identifier = login_state.username.clone();
|
let identifier = login_state.username().to_string();
|
||||||
let password = login_state.password.clone();
|
let password = login_state.password().to_string();
|
||||||
|
|
||||||
// --- Client-side validation ---
|
// --- Client-side validation ---
|
||||||
// Prevent login attempt if the identifier field is empty or whitespace.
|
|
||||||
if identifier.trim().is_empty() {
|
if identifier.trim().is_empty() {
|
||||||
let error_message = "Username/Email cannot be empty.".to_string();
|
let error_message = "Username/Email cannot be empty.".to_string();
|
||||||
app_state.show_dialog(
|
app_state.show_dialog(
|
||||||
@@ -42,33 +40,33 @@ pub async fn save(
|
|||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginFailed,
|
DialogPurpose::LoginFailed,
|
||||||
);
|
);
|
||||||
login_state.error_message = Some(error_message.clone());
|
login_state.set_error_message(Some(error_message.clone()));
|
||||||
return Err(anyhow::anyhow!(error_message));
|
return Err(anyhow!(error_message));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear previous error/dialog state before attempting
|
// Clear previous error/dialog state before attempting
|
||||||
login_state.error_message = None;
|
login_state.set_error_message(None);
|
||||||
app_state.hide_dialog(); // Hide any previous dialog
|
app_state.hide_dialog();
|
||||||
|
|
||||||
// Call the gRPC login method
|
// Call the gRPC login method
|
||||||
match auth_client.login(identifier.clone(), password).await
|
match auth_client.login(identifier.clone(), password).await
|
||||||
.with_context(|| format!("gRPC login attempt failed for identifier: {}", identifier))
|
.with_context(|| format!("gRPC login attempt failed for identifier: {}", identifier))
|
||||||
{
|
{
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
// Store authentication details using correct field names
|
// Store authentication details
|
||||||
auth_state.auth_token = Some(response.access_token.clone());
|
auth_state.auth_token = Some(response.access_token.clone());
|
||||||
auth_state.user_id = Some(response.user_id.clone());
|
auth_state.user_id = Some(response.user_id.clone());
|
||||||
auth_state.role = Some(response.role.clone());
|
auth_state.role = Some(response.role.clone());
|
||||||
auth_state.decoded_username = Some(response.username.clone());
|
auth_state.decoded_username = Some(response.username.clone());
|
||||||
login_state.set_has_unsaved_changes(false);
|
|
||||||
login_state.error_message = None;
|
|
||||||
|
|
||||||
// Format the success message using response data
|
login_state.set_has_unsaved_changes(false);
|
||||||
|
login_state.set_error_message(None);
|
||||||
|
|
||||||
let success_message = format!(
|
let success_message = format!(
|
||||||
"Login Successful!\n\n\
|
"Login Successful!\n\n\
|
||||||
Username: {}\n\
|
Username: {}\n\
|
||||||
User ID: {}\n\
|
User ID: {}\n\
|
||||||
Role: {}",
|
Role: {}",
|
||||||
response.username,
|
response.username,
|
||||||
response.user_id,
|
response.user_id,
|
||||||
response.role
|
response.role
|
||||||
@@ -80,9 +78,11 @@ pub async fn save(
|
|||||||
vec!["Menu".to_string(), "Exit".to_string()],
|
vec!["Menu".to_string(), "Exit".to_string()],
|
||||||
DialogPurpose::LoginSuccess,
|
DialogPurpose::LoginSuccess,
|
||||||
);
|
);
|
||||||
login_state.password.clear();
|
|
||||||
login_state.username.clear();
|
login_state.username_mut().clear();
|
||||||
login_state.current_cursor_pos = 0;
|
login_state.password_mut().clear();
|
||||||
|
login_state.set_current_cursor_pos(0);
|
||||||
|
|
||||||
Ok("Login successful, details shown in dialog.".to_string())
|
Ok("Login successful, details shown in dialog.".to_string())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -93,10 +93,10 @@ pub async fn save(
|
|||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginFailed,
|
DialogPurpose::LoginFailed,
|
||||||
);
|
);
|
||||||
login_state.error_message = Some(error_message.clone());
|
login_state.set_error_message(Some(error_message.clone()));
|
||||||
login_state.set_has_unsaved_changes(true);
|
login_state.set_has_unsaved_changes(true);
|
||||||
login_state.username.clear();
|
login_state.username_mut().clear();
|
||||||
login_state.password.clear();
|
login_state.password_mut().clear();
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,56 +104,42 @@ pub async fn save(
|
|||||||
|
|
||||||
/// Reverts the login form fields to empty and returns to the previous screen (Intro).
|
/// Reverts the login form fields to empty and returns to the previous screen (Intro).
|
||||||
pub async fn revert(
|
pub async fn revert(
|
||||||
login_state: &mut LoginState,
|
login_state: &mut LoginFormState,
|
||||||
_app_state: &mut AppState, // Keep signature consistent if needed elsewhere
|
app_state: &mut AppState,
|
||||||
) -> String {
|
) -> String {
|
||||||
// Clear the input fields
|
login_state.clear();
|
||||||
login_state.username.clear();
|
app_state.hide_dialog();
|
||||||
login_state.password.clear();
|
|
||||||
login_state.error_message = None;
|
|
||||||
login_state.set_has_unsaved_changes(false);
|
|
||||||
login_state.login_request_pending = false; // Ensure flag is reset on revert
|
|
||||||
|
|
||||||
"Login reverted".to_string()
|
"Login reverted".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clears login form and navigates back to main menu.
|
||||||
pub async fn back_to_main(
|
pub async fn back_to_main(
|
||||||
login_state: &mut LoginState,
|
login_state: &mut LoginFormState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
) -> String {
|
) -> String {
|
||||||
// Clear the input fields
|
login_state.clear();
|
||||||
login_state.username.clear();
|
|
||||||
login_state.password.clear();
|
|
||||||
login_state.error_message = None;
|
|
||||||
login_state.set_has_unsaved_changes(false);
|
|
||||||
login_state.login_request_pending = false; // Ensure flag is reset
|
|
||||||
|
|
||||||
// Ensure dialog is hidden if revert is called
|
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
|
|
||||||
// Navigation logic
|
|
||||||
buffer_state.close_active_buffer();
|
buffer_state.close_active_buffer();
|
||||||
buffer_state.update_history(AppView::Intro);
|
buffer_state.update_history(AppView::Intro);
|
||||||
|
|
||||||
// Reset focus state
|
|
||||||
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;
|
||||||
|
|
||||||
"Returned to main menu".to_string()
|
"Returned to main menu".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates input, shows loading, and spawns the login task.
|
/// Validates input, shows loading, and spawns the login task.
|
||||||
pub fn initiate_login(
|
pub fn initiate_login(
|
||||||
login_state: &LoginState,
|
login_state: &LoginFormState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
mut auth_client: AuthClient,
|
mut auth_client: AuthClient,
|
||||||
sender: mpsc::Sender<LoginResult>,
|
sender: mpsc::Sender<LoginResult>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let username = login_state.username.clone();
|
let username = login_state.username().to_string();
|
||||||
let password = login_state.password.clone();
|
let password = login_state.password().to_string();
|
||||||
|
|
||||||
// 1. Client-side validation
|
|
||||||
if username.trim().is_empty() {
|
if username.trim().is_empty() {
|
||||||
app_state.show_dialog(
|
app_state.show_dialog(
|
||||||
"Login Failed",
|
"Login Failed",
|
||||||
@@ -163,25 +149,20 @@ pub fn initiate_login(
|
|||||||
);
|
);
|
||||||
"Username cannot be empty.".to_string()
|
"Username cannot be empty.".to_string()
|
||||||
} else {
|
} else {
|
||||||
// 2. Show Loading Dialog
|
|
||||||
app_state.show_loading_dialog("Logging In", "Please wait...");
|
app_state.show_loading_dialog("Logging In", "Please wait...");
|
||||||
|
|
||||||
// 3. Spawn the login task
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
// Use the passed-in (and moved) auth_client directly
|
|
||||||
let login_outcome = match auth_client.login(username.clone(), password).await
|
let login_outcome = match auth_client.login(username.clone(), password).await
|
||||||
.with_context(|| format!("Spawned login task failed for identifier: {}", username))
|
.with_context(|| format!("Spawned login task failed for identifier: {}", username))
|
||||||
{
|
{
|
||||||
Ok(response) => LoginResult::Success(response),
|
Ok(response) => LoginResult::Success(response),
|
||||||
Err(e) => LoginResult::Failure(format!("{}", e)),
|
Err(e) => LoginResult::Failure(format!("{}", e)),
|
||||||
};
|
};
|
||||||
// Send result back to the main UI thread
|
|
||||||
if let Err(e) = sender.send(login_outcome).await {
|
if let Err(e) = sender.send(login_outcome).await {
|
||||||
error!("Failed to send login result: {}", e);
|
error!("Failed to send login result: {}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Return immediately
|
|
||||||
"Login initiated.".to_string()
|
"Login initiated.".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +173,7 @@ pub fn handle_login_result(
|
|||||||
result: LoginResult,
|
result: LoginResult,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
auth_state: &mut AuthState,
|
auth_state: &mut AuthState,
|
||||||
login_state: &mut LoginState,
|
login_state: &mut LoginFormState,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match result {
|
match result {
|
||||||
LoginResult::Success(response) => {
|
LoginResult::Success(response) => {
|
||||||
@@ -201,19 +182,15 @@ pub fn handle_login_result(
|
|||||||
auth_state.role = Some(response.role.clone());
|
auth_state.role = Some(response.role.clone());
|
||||||
auth_state.decoded_username = Some(response.username.clone());
|
auth_state.decoded_username = Some(response.username.clone());
|
||||||
|
|
||||||
// --- NEW: Save auth data to file ---
|
|
||||||
let data_to_store = StoredAuthData {
|
let data_to_store = StoredAuthData {
|
||||||
access_token: response.access_token.clone(),
|
access_token: response.access_token.clone(),
|
||||||
user_id: response.user_id.clone(),
|
user_id: response.user_id.clone(),
|
||||||
role: response.role.clone(),
|
role: response.role.clone(),
|
||||||
username: response.username.clone(),
|
username: response.username.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = save_auth_data(&data_to_store) {
|
if let Err(e) = save_auth_data(&data_to_store) {
|
||||||
error!("Failed to save auth data to file: {}", e);
|
error!("Failed to save auth data to file: {}", e);
|
||||||
// Continue anyway - user is still logged in for this session
|
|
||||||
}
|
}
|
||||||
// --- END NEW ---
|
|
||||||
|
|
||||||
let success_message = format!(
|
let success_message = format!(
|
||||||
"Login Successful!\n\nUsername: {}\nUser ID: {}\nRole: {}",
|
"Login Successful!\n\nUsername: {}\nUser ID: {}\nRole: {}",
|
||||||
@@ -227,26 +204,28 @@ pub fn handle_login_result(
|
|||||||
info!(message = %success_message, "Login successful");
|
info!(message = %success_message, "Login successful");
|
||||||
}
|
}
|
||||||
LoginResult::Failure(err_msg) | LoginResult::ConnectionError(err_msg) => {
|
LoginResult::Failure(err_msg) | LoginResult::ConnectionError(err_msg) => {
|
||||||
app_state.update_dialog_content(&err_msg, vec!["OK".to_string()], DialogPurpose::LoginFailed);
|
app_state.update_dialog_content(
|
||||||
login_state.error_message = Some(err_msg.clone());
|
&err_msg,
|
||||||
|
vec!["OK".to_string()],
|
||||||
|
DialogPurpose::LoginFailed,
|
||||||
|
);
|
||||||
|
login_state.set_error_message(Some(err_msg.clone()));
|
||||||
error!(error = %err_msg, "Login failed/connection error");
|
error!(error = %err_msg, "Login failed/connection error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
login_state.username.clear();
|
|
||||||
login_state.password.clear();
|
login_state.username_mut().clear();
|
||||||
|
login_state.password_mut().clear();
|
||||||
login_state.set_has_unsaved_changes(false);
|
login_state.set_has_unsaved_changes(false);
|
||||||
login_state.current_cursor_pos = 0;
|
login_state.set_current_cursor_pos(0);
|
||||||
true // Request redraw as dialog content changed
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_action(action: &str,) -> Result<String> {
|
pub async fn handle_action(action: &str) -> Result<String> {
|
||||||
match action {
|
match action {
|
||||||
"previous_entry" => {
|
"previous_entry" => Ok("Previous entry not implemented".into()),
|
||||||
Ok("Previous entry at tui/functions/login.rs not implemented".into())
|
"next_entry" => Ok("Next entry not implemented".into()),
|
||||||
}
|
_ => Err(anyhow!("Unknown login action: {}", action)),
|
||||||
"next_entry" => {
|
|
||||||
Ok("Next entry at tui/functions/login.rs not implemented".into())
|
|
||||||
}
|
|
||||||
_ => Err(anyhow!("Unknown login action: {}", action))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// src/pages/login/state.rs
|
// src/pages/login/state.rs
|
||||||
|
|
||||||
use canvas::{AppMode, DataProvider};
|
use canvas::{AppMode, DataProvider};
|
||||||
|
use canvas::FormEditor;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LoginState {
|
pub struct LoginState {
|
||||||
@@ -119,3 +121,114 @@ impl DataProvider for LoginState {
|
|||||||
false // Login form doesn't support suggestions
|
false // Login form doesn't support suggestions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper that owns both the raw login state and its editor
|
||||||
|
|
||||||
|
pub struct LoginFormState {
|
||||||
|
pub state: LoginState,
|
||||||
|
pub editor: FormEditor<LoginState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// manual debug because FormEditor doesnt implement debug
|
||||||
|
impl fmt::Debug for LoginFormState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("LoginFormState")
|
||||||
|
.field("state", &self.state) // ✅ only print the data
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginFormState {
|
||||||
|
/// Create a new LoginFormState with default LoginState and FormEditor
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let state = LoginState::default();
|
||||||
|
let editor = FormEditor::new(state.clone());
|
||||||
|
Self { state, editor }
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Delegates to LoginState fields ===
|
||||||
|
|
||||||
|
pub fn username(&self) -> &str {
|
||||||
|
&self.state.username
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn username_mut(&mut self) -> &mut String {
|
||||||
|
&mut self.state.username
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> &str {
|
||||||
|
&self.state.password
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password_mut(&mut self) -> &mut String {
|
||||||
|
&mut self.state.password
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error_message(&self) -> Option<&String> {
|
||||||
|
self.state.error_message.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_error_message(&mut self, msg: Option<String>) {
|
||||||
|
self.state.error_message = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 clear(&mut self) {
|
||||||
|
self.state.username.clear();
|
||||||
|
self.state.password.clear();
|
||||||
|
self.state.error_message = None;
|
||||||
|
self.state.has_unsaved_changes = false;
|
||||||
|
self.state.login_request_pending = false;
|
||||||
|
self.state.current_cursor_pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Delegates to LoginState cursor/input ===
|
||||||
|
|
||||||
|
pub fn current_field(&self) -> usize {
|
||||||
|
self.state.current_field()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_field(&mut self, index: usize) {
|
||||||
|
self.state.set_current_field(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_cursor_pos(&self) -> usize {
|
||||||
|
self.state.current_cursor_pos()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
|
self.state.set_current_cursor_pos(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_input(&self) -> &str {
|
||||||
|
self.state.get_current_input()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_input_mut(&mut self) -> &mut String {
|
||||||
|
self.state.get_current_input_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 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 {
|
||||||
|
self.editor.handle_key_event(key_event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,17 +16,20 @@ use canvas::{
|
|||||||
render_suggestions_dropdown,
|
render_suggestions_dropdown,
|
||||||
DefaultCanvasTheme,
|
DefaultCanvasTheme,
|
||||||
};
|
};
|
||||||
use crate::pages::login::LoginState;
|
|
||||||
|
use crate::pages::login::LoginFormState;
|
||||||
use crate::dialog;
|
use crate::dialog;
|
||||||
|
|
||||||
pub fn render_login(
|
pub fn render_login(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
// FIX: take &LoginState (reference), not owned
|
login_page: &LoginFormState,
|
||||||
login_state: &LoginState,
|
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
) {
|
) {
|
||||||
|
let login_state = &login_page.state;
|
||||||
|
let editor = &login_page.editor;
|
||||||
|
|
||||||
// Main container
|
// Main container
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
@@ -52,14 +55,10 @@ pub fn render_login(
|
|||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// Wrap LoginState in FormEditor (no clone needed)
|
|
||||||
let editor = FormEditor::new(login_state.clone());
|
|
||||||
|
|
||||||
// Use DefaultCanvasTheme instead of app Theme
|
|
||||||
let input_rect = render_canvas(
|
let input_rect = render_canvas(
|
||||||
f,
|
f,
|
||||||
chunks[0],
|
chunks[0],
|
||||||
&editor,
|
editor,
|
||||||
&DefaultCanvasTheme,
|
&DefaultCanvasTheme,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -134,14 +133,14 @@ pub fn render_login(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- SUGGESTIONS DROPDOWN (if active) ---
|
// --- SUGGESTIONS DROPDOWN (if active) ---
|
||||||
if editor.mode() == canvas::AppMode::Edit {
|
if editor.mode() == canvas::AppMode::Edit {
|
||||||
if let Some(input_rect) = input_rect {
|
if let Some(input_rect) = input_rect {
|
||||||
render_suggestions_dropdown(
|
render_suggestions_dropdown(
|
||||||
f,
|
f,
|
||||||
f.area(),
|
chunks[0],
|
||||||
input_rect,
|
input_rect,
|
||||||
&DefaultCanvasTheme,
|
&DefaultCanvasTheme,
|
||||||
&editor, // FIX: pass &editor
|
editor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ use crate::state::pages::{
|
|||||||
};
|
};
|
||||||
use crate::pages::admin::AdminState;
|
use crate::pages::admin::AdminState;
|
||||||
use crate::pages::forms::FormState;
|
use crate::pages::forms::FormState;
|
||||||
use crate::pages::login::LoginState;
|
use crate::pages::login::LoginFormState;
|
||||||
use crate::pages::register::RegisterState;
|
use crate::pages::register::RegisterState;
|
||||||
use crate::pages::intro::IntroState;
|
use crate::pages::intro::IntroState;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Page {
|
pub enum Page {
|
||||||
Intro(IntroState),
|
Intro(IntroState),
|
||||||
Login(LoginState),
|
Login(LoginFormState),
|
||||||
Register(RegisterState),
|
Register(RegisterState),
|
||||||
Admin(AdminState),
|
Admin(AdminState),
|
||||||
AddLogic(AddLogicState),
|
AddLogic(AddLogicState),
|
||||||
|
|||||||
@@ -76,11 +76,11 @@ pub fn render_ui(
|
|||||||
// Page rendering is now fully router-driven
|
// Page rendering is now fully router-driven
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
Page::Intro(state) => render_intro(f, state, main_content_area, theme),
|
Page::Intro(state) => render_intro(f, state, main_content_area, theme),
|
||||||
Page::Login(state) => render_login(
|
Page::Login(page) => render_login(
|
||||||
f,
|
f,
|
||||||
main_content_area,
|
main_content_area,
|
||||||
theme,
|
theme,
|
||||||
state,
|
page,
|
||||||
app_state,
|
app_state,
|
||||||
),
|
),
|
||||||
Page::Register(state) => render_register(
|
Page::Register(state) => render_register(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
|||||||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::pages::register::RegisterState;
|
use crate::pages::register::RegisterState;
|
||||||
|
use crate::pages::login::LoginFormState;
|
||||||
use crate::pages::admin::AdminState;
|
use crate::pages::admin::AdminState;
|
||||||
use crate::pages::admin::AdminFocus;
|
use crate::pages::admin::AdminFocus;
|
||||||
use crate::pages::intro::IntroState;
|
use crate::pages::intro::IntroState;
|
||||||
@@ -28,6 +29,7 @@ use crate::pages::register::RegisterResult;
|
|||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use crate::utils::columns::filter_user_columns;
|
use crate::utils::columns::filter_user_columns;
|
||||||
use canvas::keymap::KeyEventOutcome;
|
use canvas::keymap::KeyEventOutcome;
|
||||||
|
use canvas::FormEditor;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use crossterm::cursor::{SetCursorStyle, MoveTo};
|
use crossterm::cursor::{SetCursorStyle, MoveTo};
|
||||||
use crossterm::event as crossterm_event;
|
use crossterm::event as crossterm_event;
|
||||||
@@ -66,7 +68,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let event_reader = EventReader::new();
|
let event_reader = EventReader::new();
|
||||||
|
|
||||||
let mut auth_state = AuthState::default();
|
let mut auth_state = AuthState::default();
|
||||||
let mut login_state = LoginState::default();
|
let mut login_state = LoginFormState::new();
|
||||||
let mut register_state = RegisterState::default();
|
let mut register_state = RegisterState::default();
|
||||||
let mut intro_state = IntroState::default();
|
let mut intro_state = IntroState::default();
|
||||||
let mut admin_state = AdminState::default();
|
let mut admin_state = AdminState::default();
|
||||||
@@ -350,7 +352,9 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
// Navigate with the up-to-date state
|
// Navigate with the up-to-date state
|
||||||
router.navigate(Page::Intro(intro_state.clone()));
|
router.navigate(Page::Intro(intro_state.clone()));
|
||||||
}
|
}
|
||||||
AppView::Login => router.navigate(Page::Login(login_state.clone())),
|
AppView::Login => {
|
||||||
|
router.navigate(Page::Login(LoginFormState::new()))
|
||||||
|
}
|
||||||
AppView::Register => router.navigate(Page::Register(register_state.clone())),
|
AppView::Register => router.navigate(Page::Register(register_state.clone())),
|
||||||
AppView::Admin => {
|
AppView::Admin => {
|
||||||
if let Page::Admin(current) = &router.current {
|
if let Page::Admin(current) = &router.current {
|
||||||
@@ -619,8 +623,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
let max_cursor_pos =
|
let max_cursor_pos =
|
||||||
if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
||||||
state.current_cursor_pos =
|
state.set_current_cursor_pos(event_handler.ideal_cursor_column.min(max_cursor_pos));
|
||||||
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user