Compare commits
32 Commits
0.5.4
...
9ed558562b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ed558562b | ||
|
|
43f5c1a764 | ||
|
|
46149c09db | ||
|
|
a0757efe8b | ||
|
|
10f4b9d8e2 | ||
|
|
42db496ad7 | ||
|
|
d6fd672409 | ||
|
|
60eb1c9f51 | ||
|
|
a09c804595 | ||
|
|
a17f73fd54 | ||
|
|
2373ae4b8c | ||
|
|
16dd460469 | ||
|
|
58f109ca91 | ||
|
|
75da9c0f4b | ||
|
|
833b918c5b | ||
|
|
72c2691a17 | ||
|
|
cf79bc7bd5 | ||
|
|
f5f2f2cdef | ||
|
|
19a9bab8c2 | ||
|
|
6e221ef8c1 | ||
|
|
e142f56706 | ||
|
|
a794f22366 | ||
|
|
cfe4903c79 | ||
|
|
a0a473f96c | ||
|
|
9e4dd3b4c7 | ||
|
|
e5db0334c0 | ||
|
|
d641ad1bbb | ||
|
|
18393ff661 | ||
|
|
b2a82fba30 | ||
|
|
f6c2fd627f | ||
|
|
15d9b31cb6 | ||
|
|
06cc1663b3 |
@@ -7,16 +7,14 @@ previous_buffer = ["space+b+p"]
|
|||||||
close_buffer = ["space+b+d"]
|
close_buffer = ["space+b+d"]
|
||||||
|
|
||||||
[keybindings.general]
|
[keybindings.general]
|
||||||
move_up = ["k", "Up"]
|
up = ["k", "Up"]
|
||||||
move_down = ["j", "Down"]
|
down = ["j", "Down"]
|
||||||
next_option = ["l", "Right"]
|
left = ["h", "Left"]
|
||||||
previous_option = ["h", "Left"]
|
right = ["l", "Right"]
|
||||||
|
next = ["Tab"]
|
||||||
|
previous = ["Shift+Tab"]
|
||||||
select = ["Enter"]
|
select = ["Enter"]
|
||||||
toggle_sidebar = ["ctrl+t"]
|
esc = ["esc"]
|
||||||
toggle_buffer_list = ["ctrl+b"]
|
|
||||||
next_field = ["Tab"]
|
|
||||||
prev_field = ["Shift+Tab"]
|
|
||||||
exit_table_scroll = ["esc"]
|
|
||||||
open_search = ["ctrl+f"]
|
open_search = ["ctrl+f"]
|
||||||
|
|
||||||
[keybindings.common]
|
[keybindings.common]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::bottom_panel::find_file_palette;
|
|||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::modes::general::command_navigation::NavigationState;
|
use crate::modes::general::command_navigation::NavigationState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::pages::routing::Router;
|
||||||
|
|
||||||
/// Calculate the layout constraints for the bottom panel (status line + command line/palette).
|
/// Calculate the layout constraints for the bottom panel (status line + command line/palette).
|
||||||
pub fn bottom_panel_constraints(
|
pub fn bottom_panel_constraints(
|
||||||
@@ -46,9 +47,9 @@ pub fn render_bottom_panel(
|
|||||||
chunk_idx: &mut usize,
|
chunk_idx: &mut usize,
|
||||||
current_dir: &str,
|
current_dir: &str,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
is_event_handler_edit_mode: bool,
|
|
||||||
current_fps: f64,
|
current_fps: f64,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
|
router: &Router,
|
||||||
navigation_state: &NavigationState,
|
navigation_state: &NavigationState,
|
||||||
event_handler_command_input: &str,
|
event_handler_command_input: &str,
|
||||||
event_handler_command_mode_active: bool,
|
event_handler_command_mode_active: bool,
|
||||||
@@ -74,9 +75,9 @@ pub fn render_bottom_panel(
|
|||||||
status_line_area,
|
status_line_area,
|
||||||
current_dir,
|
current_dir,
|
||||||
theme,
|
theme,
|
||||||
is_event_handler_edit_mode,
|
|
||||||
current_fps,
|
current_fps,
|
||||||
app_state,
|
app_state,
|
||||||
|
router,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Render command line or palette ---
|
// --- Render command line or palette ---
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use ratatui::{
|
|||||||
widgets::{Paragraph, Wrap},
|
widgets::{Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
use crate::pages::routing::Page;
|
||||||
|
use crate::pages::routing::Router;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
@@ -16,9 +18,9 @@ pub fn render_status_line(
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
current_dir: &str,
|
current_dir: &str,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
is_edit_mode: bool,
|
|
||||||
current_fps: f64,
|
current_fps: f64,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
|
router: &Router,
|
||||||
) {
|
) {
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
{
|
{
|
||||||
@@ -48,7 +50,20 @@ pub fn render_status_line(
|
|||||||
|
|
||||||
// --- The normal status line rendering logic (unchanged) ---
|
// --- The normal status line rendering logic (unchanged) ---
|
||||||
let program_info = format!("komp_ac v{}", env!("CARGO_PKG_VERSION"));
|
let program_info = format!("komp_ac v{}", env!("CARGO_PKG_VERSION"));
|
||||||
let mode_text = if is_edit_mode { "[EDIT]" } else { "[READ-ONLY]" };
|
let mode_text = if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path_ref(path) {
|
||||||
|
match editor.mode() {
|
||||||
|
canvas::AppMode::Edit => "[EDIT]",
|
||||||
|
canvas::AppMode::ReadOnly => "[READ-ONLY]",
|
||||||
|
canvas::AppMode::Highlight => "[VISUAL]",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"" // No canvas active
|
||||||
|
};
|
||||||
|
|
||||||
let home_dir = dirs::home_dir()
|
let home_dir = dirs::home_dir()
|
||||||
.map(|p| p.to_string_lossy().into_owned())
|
.map(|p| p.to_string_lossy().into_owned())
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ pub fn get_view_layer(view: &AppView) -> u8 {
|
|||||||
match view {
|
match view {
|
||||||
AppView::Intro => 1,
|
AppView::Intro => 1,
|
||||||
AppView::Login | AppView::Register | AppView::Admin | AppView::AddTable | AppView::AddLogic => 2,
|
AppView::Login | AppView::Register | AppView::Admin | AppView::AddTable | AppView::AddLogic => 2,
|
||||||
AppView::Form | AppView::Scratch => 3,
|
AppView::Form(_) | AppView::Scratch => 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub enum AppView {
|
|||||||
Admin,
|
Admin,
|
||||||
AddTable,
|
AddTable,
|
||||||
AddLogic,
|
AddLogic,
|
||||||
Form,
|
Form(String),
|
||||||
Scratch,
|
Scratch,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ impl AppView {
|
|||||||
AppView::Admin => "Admin_Panel",
|
AppView::Admin => "Admin_Panel",
|
||||||
AppView::AddTable => "Add_Table",
|
AppView::AddTable => "Add_Table",
|
||||||
AppView::AddLogic => "Add_Logic",
|
AppView::AddLogic => "Add_Logic",
|
||||||
AppView::Form => "Form",
|
AppView::Form(_) => "Form",
|
||||||
AppView::Scratch => "*scratch*",
|
AppView::Scratch => "*scratch*",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,10 +31,14 @@ impl AppView {
|
|||||||
/// Returns the display name with dynamic context (for Form buffers)
|
/// Returns the display name with dynamic context (for Form buffers)
|
||||||
pub fn display_name_with_context(&self, current_table_name: Option<&str>) -> String {
|
pub fn display_name_with_context(&self, current_table_name: Option<&str>) -> String {
|
||||||
match self {
|
match self {
|
||||||
AppView::Form => {
|
AppView::Form(path) => {
|
||||||
current_table_name
|
// Derive table name from "profile/table" path
|
||||||
.unwrap_or("Data Form")
|
let table = path.split('/').nth(1).unwrap_or("");
|
||||||
.to_string()
|
if !table.is_empty() {
|
||||||
|
table.to_string()
|
||||||
|
} else {
|
||||||
|
current_table_name.unwrap_or("Data Form").to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => self.display_name().to_string(),
|
_ => self.display_name().to_string(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// src/components/admin.rs
|
|
||||||
pub mod admin_panel;
|
|
||||||
pub mod admin_panel_admin;
|
|
||||||
pub mod add_table;
|
|
||||||
pub mod add_logic;
|
|
||||||
|
|
||||||
pub use admin_panel::*;
|
|
||||||
pub use admin_panel_admin::*;
|
|
||||||
pub use add_table::*;
|
|
||||||
pub use add_logic::*;
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
// src/components/mod.rs
|
// src/components/mod.rs
|
||||||
|
|
||||||
pub mod admin;
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub use admin::*;
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
pub use utils::*;
|
pub use utils::*;
|
||||||
|
|||||||
@@ -148,19 +148,17 @@ impl Config {
|
|||||||
/// Context-aware keybinding resolution
|
/// Context-aware keybinding resolution
|
||||||
pub fn get_action_for_current_context(
|
pub fn get_action_for_current_context(
|
||||||
&self,
|
&self,
|
||||||
is_edit_mode: bool,
|
|
||||||
command_mode: bool,
|
command_mode: bool,
|
||||||
key: KeyCode,
|
key: KeyCode,
|
||||||
modifiers: KeyModifiers
|
modifiers: KeyModifiers
|
||||||
) -> Option<&str> {
|
) -> Option<&str> {
|
||||||
match (command_mode, is_edit_mode) {
|
if command_mode {
|
||||||
(true, _) => self.get_command_action_for_key(key, modifiers),
|
self.get_command_action_for_key(key, modifiers)
|
||||||
(_, true) => self.get_edit_action_for_key(key, modifiers)
|
} else {
|
||||||
.or_else(|| self.get_common_action(key, modifiers)),
|
// fallback: read-only + common + global
|
||||||
_ => self.get_read_only_action_for_key(key, modifiers)
|
self.get_read_only_action_for_key(key, modifiers)
|
||||||
.or_else(|| self.get_common_action(key, modifiers))
|
.or_else(|| self.get_common_action(key, modifiers))
|
||||||
// Add global bindings check for read-only mode
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
||||||
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use crate::buffer::state::BufferState;
|
|||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crate::pages::register;
|
use crate::pages::register;
|
||||||
use crate::pages::login;
|
use crate::pages::login;
|
||||||
use crate::tui::functions::common::add_table::handle_delete_selected_columns;
|
use crate::pages::admin_panel::add_table::logic::handle_delete_selected_columns;
|
||||||
use crate::pages::routing::{Router, Page};
|
use crate::pages::routing::{Router, Page};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
// src/functions/mod.rs
|
|
||||||
|
|
||||||
pub mod modes;
|
|
||||||
|
|
||||||
pub use modes::*;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// src/functions/modes.rs
|
|
||||||
|
|
||||||
pub mod navigation;
|
|
||||||
|
|
||||||
pub use navigation::*;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// src/functions/modes/navigation.rs
|
|
||||||
|
|
||||||
pub mod admin_nav;
|
|
||||||
pub mod add_table_nav;
|
|
||||||
pub mod add_logic_nav;
|
|
||||||
@@ -5,7 +5,6 @@ pub mod config;
|
|||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod modes;
|
pub mod modes;
|
||||||
pub mod functions;
|
|
||||||
pub mod services;
|
pub mod services;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod buffer;
|
pub mod buffer;
|
||||||
@@ -14,6 +13,7 @@ pub mod dialog;
|
|||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod bottom_panel;
|
pub mod bottom_panel;
|
||||||
pub mod pages;
|
pub mod pages;
|
||||||
|
pub mod movement;
|
||||||
|
|
||||||
pub use ui::run_ui;
|
pub use ui::run_ui;
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ async fn process_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"save" => {
|
"save" => {
|
||||||
let outcome = save(app_state, grpc_client).await?;
|
if let Page::Form(path) = &router.current {
|
||||||
|
let outcome = save(app_state, path, grpc_client).await?;
|
||||||
let message = match outcome {
|
let message = match outcome {
|
||||||
SaveOutcome::CreatedNew(_) => "New entry created".to_string(),
|
SaveOutcome::CreatedNew(_) => "New entry created".to_string(),
|
||||||
SaveOutcome::UpdatedExisting => "Entry updated".to_string(),
|
SaveOutcome::UpdatedExisting => "Entry updated".to_string(),
|
||||||
@@ -106,11 +107,18 @@ async fn process_command(
|
|||||||
};
|
};
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
Ok(EventOutcome::DataSaved(outcome, message))
|
Ok(EventOutcome::DataSaved(outcome, message))
|
||||||
|
} else {
|
||||||
|
Ok(EventOutcome::Ok("Not in a form page".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"revert" => {
|
"revert" => {
|
||||||
let message = revert(app_state, grpc_client).await?;
|
if let Page::Form(path) = &router.current {
|
||||||
|
let message = revert(app_state, path, grpc_client).await?;
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
Ok(EventOutcome::Ok(message))
|
Ok(EventOutcome::Ok(message))
|
||||||
|
} else {
|
||||||
|
Ok(EventOutcome::Ok("Not in a form page".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let message = format!("Unhandled action: {}", action);
|
let message = format!("Unhandled action: {}", action);
|
||||||
|
|||||||
@@ -34,9 +34,12 @@ 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(path) => app_state
|
||||||
|
.form_state_for_path_ref(path)
|
||||||
|
.map(|fs| fs.has_unsaved_changes())
|
||||||
|
.unwrap_or(false),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ pub async fn handle_navigation_event(
|
|||||||
|
|
||||||
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
||||||
match action {
|
match action {
|
||||||
"move_up" => {
|
"up" => {
|
||||||
move_up(app_state, router);
|
up(app_state, router);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"move_down" => {
|
"down" => {
|
||||||
move_down(app_state, router);
|
down(app_state, router);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"next_option" => {
|
"next_option" => {
|
||||||
@@ -45,15 +45,19 @@ pub async fn handle_navigation_event(
|
|||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"next_field" => {
|
"next_field" => {
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
next_field(fs);
|
next_field(fs);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"prev_field" => {
|
"prev_field" => {
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
prev_field(fs);
|
prev_field(fs);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"enter_command_mode" => {
|
"enter_command_mode" => {
|
||||||
@@ -85,13 +89,13 @@ pub async fn handle_navigation_event(
|
|||||||
Ok(EventOutcome::Ok(String::new()))
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_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);
|
||||||
@@ -100,7 +104,7 @@ pub fn move_up(app_state: &mut AppState, router: &mut Router) {
|
|||||||
Page::Register(state) if app_state.ui.focus_outside_canvas => {
|
Page::Register(state) 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 = state.state.field_count().saturating_sub(1);
|
||||||
state.set_current_field(last_field_index);
|
state.set_current_field(last_field_index);
|
||||||
} else {
|
} else {
|
||||||
app_state.focused_button_index =
|
app_state.focused_button_index =
|
||||||
@@ -113,7 +117,7 @@ pub fn move_up(app_state: &mut AppState, router: &mut Router) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_down(app_state: &mut AppState, router: &mut Router) {
|
pub fn down(app_state: &mut AppState, router: &mut Router) {
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
Page::Login(_) | Page::Register(_) if app_state.ui.focus_outside_canvas => {
|
Page::Login(_) | Page::Register(_) if app_state.ui.focus_outside_canvas => {
|
||||||
let num_general_elements = 2;
|
let num_general_elements = 2;
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ use crate::config::binds::key_sequences::KeySequenceTracker;
|
|||||||
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
|
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
|
||||||
use crate::sidebar::toggle_sidebar;
|
use crate::sidebar::toggle_sidebar;
|
||||||
use crate::search::event::handle_search_palette_event;
|
use crate::search::event::handle_search_palette_event;
|
||||||
use crate::functions::modes::navigation::add_logic_nav;
|
use crate::pages::admin_panel::add_logic;
|
||||||
use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender;
|
use crate::pages::admin_panel::add_table;
|
||||||
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
use crate::pages::register::suggestions::RoleSuggestionsProvider;
|
||||||
use crate::functions::modes::navigation::{add_table_nav, admin_nav};
|
use crate::pages::admin::main::logic::handle_admin_navigation;
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
@@ -19,15 +20,11 @@ use crate::modes::{
|
|||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use canvas::AppMode as CanvasMode;
|
use canvas::AppMode as CanvasMode;
|
||||||
use crate::state::{
|
use canvas::DataProvider;
|
||||||
app::{
|
use crate::state::app::state::AppState;
|
||||||
state::AppState,
|
use crate::pages::admin::AdminState;
|
||||||
},
|
use crate::state::pages::auth::AuthState;
|
||||||
pages::{
|
use crate::state::pages::auth::UserRole;
|
||||||
admin::AdminState,
|
|
||||||
auth::AuthState,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use crate::pages::login::LoginState;
|
use crate::pages::login::LoginState;
|
||||||
use crate::pages::register::RegisterState;
|
use crate::pages::register::RegisterState;
|
||||||
use crate::pages::intro::IntroState;
|
use crate::pages::intro::IntroState;
|
||||||
@@ -37,16 +34,18 @@ use crate::pages::intro;
|
|||||||
use crate::pages::login::logic::LoginResult;
|
use crate::pages::login::logic::LoginResult;
|
||||||
use crate::pages::register::RegisterResult;
|
use crate::pages::register::RegisterResult;
|
||||||
use crate::pages::routing::{Router, Page};
|
use crate::pages::routing::{Router, Page};
|
||||||
|
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;
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
terminal::core::TerminalCore,
|
terminal::core::TerminalCore,
|
||||||
admin,
|
|
||||||
};
|
};
|
||||||
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};
|
||||||
@@ -75,7 +74,6 @@ pub struct EventHandler {
|
|||||||
pub command_mode: bool,
|
pub command_mode: bool,
|
||||||
pub command_input: String,
|
pub command_input: String,
|
||||||
pub command_message: String,
|
pub command_message: String,
|
||||||
pub is_edit_mode: bool,
|
|
||||||
pub edit_mode_cooldown: bool,
|
pub edit_mode_cooldown: bool,
|
||||||
pub ideal_cursor_column: usize,
|
pub ideal_cursor_column: usize,
|
||||||
pub key_sequence_tracker: KeySequenceTracker,
|
pub key_sequence_tracker: KeySequenceTracker,
|
||||||
@@ -83,8 +81,8 @@ pub struct EventHandler {
|
|||||||
pub grpc_client: GrpcClient,
|
pub grpc_client: GrpcClient,
|
||||||
pub login_result_sender: mpsc::Sender<LoginResult>,
|
pub login_result_sender: mpsc::Sender<LoginResult>,
|
||||||
pub register_result_sender: mpsc::Sender<RegisterResult>,
|
pub register_result_sender: mpsc::Sender<RegisterResult>,
|
||||||
pub save_table_result_sender: SaveTableResultSender,
|
pub save_table_result_sender: add_table::nav::SaveTableResultSender,
|
||||||
pub save_logic_result_sender: SaveLogicResultSender,
|
pub save_logic_result_sender: add_logic::nav::SaveLogicResultSender,
|
||||||
pub navigation_state: NavigationState,
|
pub navigation_state: NavigationState,
|
||||||
pub search_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
|
pub search_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
|
||||||
pub search_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
|
pub search_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
|
||||||
@@ -96,8 +94,8 @@ impl EventHandler {
|
|||||||
pub async fn new(
|
pub async fn new(
|
||||||
login_result_sender: mpsc::Sender<LoginResult>,
|
login_result_sender: mpsc::Sender<LoginResult>,
|
||||||
register_result_sender: mpsc::Sender<RegisterResult>,
|
register_result_sender: mpsc::Sender<RegisterResult>,
|
||||||
save_table_result_sender: SaveTableResultSender,
|
save_table_result_sender: add_table::nav::SaveTableResultSender,
|
||||||
save_logic_result_sender: SaveLogicResultSender,
|
save_logic_result_sender: add_logic::nav::SaveLogicResultSender,
|
||||||
grpc_client: GrpcClient,
|
grpc_client: GrpcClient,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let (search_tx, search_rx) = unbounded_channel();
|
let (search_tx, search_rx) = unbounded_channel();
|
||||||
@@ -106,7 +104,6 @@ impl EventHandler {
|
|||||||
command_mode: false,
|
command_mode: false,
|
||||||
command_input: String::new(),
|
command_input: String::new(),
|
||||||
command_message: String::new(),
|
command_message: String::new(),
|
||||||
is_edit_mode: false,
|
|
||||||
edit_mode_cooldown: false,
|
edit_mode_cooldown: false,
|
||||||
ideal_cursor_column: 0,
|
ideal_cursor_column: 0,
|
||||||
key_sequence_tracker: KeySequenceTracker::new(400),
|
key_sequence_tracker: KeySequenceTracker::new(400),
|
||||||
@@ -133,69 +130,84 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions - replace the removed event_helper functions
|
// Helper functions - replace the removed event_helper functions
|
||||||
fn get_current_field_for_state(
|
fn get_current_field_for_state(&self, router: &Router, app_state: &AppState) -> usize {
|
||||||
router: &Router,
|
|
||||||
) -> usize {
|
|
||||||
match &router.current {
|
match &router.current {
|
||||||
Page::Login(state) => state.current_field(),
|
Page::Login(state) => state.current_field(),
|
||||||
Page::Register(state) => state.current_field(),
|
Page::Register(state) => state.current_field(),
|
||||||
Page::Form(state) => state.current_field(),
|
Page::Form(path) => app_state
|
||||||
|
.editor_for_path_ref(path)
|
||||||
|
.map(|e| e.data_provider().current_field())
|
||||||
|
.unwrap_or(0),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_cursor_pos_for_state(
|
fn get_current_cursor_pos_for_state(&self, router: &Router, app_state: &AppState) -> usize {
|
||||||
router: &Router,
|
|
||||||
) -> usize {
|
|
||||||
match &router.current {
|
match &router.current {
|
||||||
Page::Login(state) => state.current_cursor_pos(),
|
Page::Login(state) => state.current_cursor_pos(),
|
||||||
Page::Register(state) => state.current_cursor_pos(),
|
Page::Register(state) => state.current_cursor_pos(),
|
||||||
Page::Form(state) => state.current_cursor_pos(),
|
Page::Form(path) => app_state
|
||||||
|
.form_state_for_path_ref(path)
|
||||||
|
.map(|fs| fs.current_cursor_pos())
|
||||||
|
.unwrap_or(0),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_has_unsaved_changes_for_state(
|
fn get_has_unsaved_changes_for_state(&self, router: &Router, app_state: &AppState) -> bool {
|
||||||
router: &Router,
|
|
||||||
) -> bool {
|
|
||||||
match &router.current {
|
match &router.current {
|
||||||
Page::Login(state) => state.has_unsaved_changes(),
|
Page::Login(state) => state.has_unsaved_changes(),
|
||||||
Page::Register(state) => state.has_unsaved_changes(),
|
Page::Register(state) => state.has_unsaved_changes(),
|
||||||
Page::Form(state) => state.has_unsaved_changes(),
|
Page::Form(path) => app_state
|
||||||
|
.form_state_for_path_ref(path)
|
||||||
|
.map(|fs| fs.has_unsaved_changes())
|
||||||
|
.unwrap_or(false),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input_for_state<'a>(
|
fn get_current_input_for_state<'a>(
|
||||||
|
&'a self,
|
||||||
router: &'a Router,
|
router: &'a Router,
|
||||||
|
app_state: &'a AppState,
|
||||||
) -> &'a str {
|
) -> &'a str {
|
||||||
match &router.current {
|
match &router.current {
|
||||||
Page::Login(state) => state.get_current_input(),
|
Page::Login(state) => state.get_current_input(),
|
||||||
Page::Register(state) => state.get_current_input(),
|
Page::Register(state) => state.get_current_input(),
|
||||||
Page::Form(state) => state.get_current_input(),
|
Page::Form(path) => app_state
|
||||||
|
.form_state_for_path_ref(path)
|
||||||
|
.map(|fs| fs.get_current_input())
|
||||||
|
.unwrap_or(""),
|
||||||
_ => "",
|
_ => "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_cursor_pos_for_state(
|
fn set_current_cursor_pos_for_state(
|
||||||
|
&mut self,
|
||||||
router: &mut Router,
|
router: &mut Router,
|
||||||
|
app_state: &mut AppState,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
) {
|
) {
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
Page::Login(state) => state.set_current_cursor_pos(pos),
|
Page::Login(state) => state.set_current_cursor_pos(pos),
|
||||||
Page::Register(state) => state.set_current_cursor_pos(pos),
|
Page::Register(state) => state.set_current_cursor_pos(pos),
|
||||||
Page::Form(state) => state.set_current_cursor_pos(pos),
|
Page::Form(path) => {
|
||||||
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
|
fs.set_current_cursor_pos(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cursor_pos_for_mixed_state(
|
fn get_cursor_pos_for_mixed_state(&self, router: &Router, app_state: &AppState) -> usize {
|
||||||
router: &Router,
|
|
||||||
) -> usize {
|
|
||||||
match &router.current {
|
match &router.current {
|
||||||
Page::Login(state) => state.current_cursor_pos(),
|
Page::Login(state) => state.current_cursor_pos(),
|
||||||
Page::Register(state) => state.current_cursor_pos(),
|
Page::Register(state) => state.current_cursor_pos(),
|
||||||
Page::Form(state) => state.current_cursor_pos(),
|
Page::Form(path) => app_state
|
||||||
|
.form_state_for_path_ref(path)
|
||||||
|
.map(|fs| fs.current_cursor_pos())
|
||||||
|
.unwrap_or(0),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +271,7 @@ impl EventHandler {
|
|||||||
Page::Admin(_) => AppView::Admin,
|
Page::Admin(_) => AppView::Admin,
|
||||||
Page::AddLogic(_) => AppView::AddLogic,
|
Page::AddLogic(_) => AppView::AddLogic,
|
||||||
Page::AddTable(_) => AppView::AddTable,
|
Page::AddTable(_) => AppView::AddTable,
|
||||||
Page::Form(_) => AppView::Form,
|
Page::Form(path) => AppView::Form(path.clone()),
|
||||||
};
|
};
|
||||||
buffer_state.update_history(current_view);
|
buffer_state.update_history(current_view);
|
||||||
|
|
||||||
@@ -285,6 +297,58 @@ impl EventHandler {
|
|||||||
let key_code = key_event.code;
|
let key_code = key_event.code;
|
||||||
let modifiers = key_event.modifiers;
|
let modifiers = key_event.modifiers;
|
||||||
|
|
||||||
|
// LOGIN: canvas <-> buttons focus handoff
|
||||||
|
// Do not let Login canvas receive keys when overlays/palettes are active
|
||||||
|
let overlay_active = self.command_mode
|
||||||
|
|| app_state.ui.show_search_palette
|
||||||
|
|| self.navigation_state.active;
|
||||||
|
|
||||||
|
if !overlay_active {
|
||||||
|
if let Page::Login(login_page) = &mut router.current {
|
||||||
|
let outcome =
|
||||||
|
login::event::handle_login_event(event, app_state, login_page)?;
|
||||||
|
// Only return if the login page actually consumed the key
|
||||||
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
} else if let Page::Register(register_page) = &mut router.current {
|
||||||
|
let outcome = crate::pages::register::event::handle_register_event(
|
||||||
|
event,
|
||||||
|
app_state,
|
||||||
|
register_page,
|
||||||
|
)?;
|
||||||
|
// Only stop if page actually consumed the key; else fall through to global handling
|
||||||
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
} else if let Page::Form(path) = &router.current {
|
||||||
|
let outcome = forms::event::handle_form_event(
|
||||||
|
event,
|
||||||
|
app_state,
|
||||||
|
path,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
)?;
|
||||||
|
// Only return if the form page actually consumed the key
|
||||||
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
} else if let Page::Admin(admin_state) = &mut router.current {
|
||||||
|
if matches!(auth_state.role, Some(UserRole::Admin)) {
|
||||||
|
if let Event::Key(key_event) = event {
|
||||||
|
if admin::event::handle_admin_event(
|
||||||
|
key_event,
|
||||||
|
config,
|
||||||
|
app_state,
|
||||||
|
admin_state,
|
||||||
|
buffer_state,
|
||||||
|
&mut self.command_message,
|
||||||
|
)? {
|
||||||
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if toggle_sidebar(
|
if toggle_sidebar(
|
||||||
&mut app_state.ui,
|
&mut app_state.ui,
|
||||||
config,
|
config,
|
||||||
@@ -318,7 +382,7 @@ impl EventHandler {
|
|||||||
return Ok(EventOutcome::Ok(message));
|
return Ok(EventOutcome::Ok(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matches!(current_mode, AppMode::Edit | AppMode::Command) {
|
if current_mode == AppMode::General {
|
||||||
if let Some(action) = config.get_action_for_key_in_mode(
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
&config.keybindings.global,
|
&config.keybindings.global,
|
||||||
key_code,
|
key_code,
|
||||||
@@ -352,9 +416,7 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(action) =
|
if let Some(action) = config.get_general_action(key_code, modifiers) {
|
||||||
config.get_general_action(key_code, modifiers)
|
|
||||||
{
|
|
||||||
if action == "open_search" {
|
if action == "open_search" {
|
||||||
if let Page::Form(_) = &router.current {
|
if let Page::Form(_) = &router.current {
|
||||||
if let Some(table_name) =
|
if let Some(table_name) =
|
||||||
@@ -375,30 +437,61 @@ impl EventHandler {
|
|||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
AppMode::General => {
|
AppMode::General => {
|
||||||
if let Page::Admin(admin_state) = &mut router.current {
|
// Map keys to MovementAction
|
||||||
if auth_state.role.as_deref() == Some("admin") {
|
let movement_action = if let Some(act) =
|
||||||
if admin_nav::handle_admin_navigation(
|
config.get_general_action(key_event.code, key_event.modifiers)
|
||||||
key_event,
|
{
|
||||||
config,
|
use crate::movement::MovementAction;
|
||||||
app_state,
|
match act {
|
||||||
admin_state,
|
"up" => Some(MovementAction::Up),
|
||||||
buffer_state,
|
"down" => Some(MovementAction::Down),
|
||||||
&mut self.command_message,
|
"left" => Some(MovementAction::Left),
|
||||||
) {
|
"right" => Some(MovementAction::Right),
|
||||||
return Ok(EventOutcome::Ok(
|
"next" => Some(MovementAction::Next),
|
||||||
self.command_message.clone(),
|
"previous" => Some(MovementAction::Previous),
|
||||||
));
|
"select" => Some(MovementAction::Select),
|
||||||
|
"esc" => Some(MovementAction::Esc),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Let the current page handle decoupled movement first
|
||||||
|
if let Some(ma) = movement_action {
|
||||||
|
match &mut router.current {
|
||||||
|
// LOGIN: From buttons (general) back into the canvas with 'k' (Up),
|
||||||
|
// but ONLY from the left-most "Login" button.
|
||||||
|
Page::AddTable(state) => {
|
||||||
|
if state.handle_movement(ma) {
|
||||||
|
// Keep UI focus consistent with inputs vs. outer elements
|
||||||
|
use crate::pages::admin_panel::add_table::state::AddTableFocus;
|
||||||
|
let is_canvas_input = matches!(
|
||||||
|
state.current_focus,
|
||||||
|
AddTableFocus::InputTableName
|
||||||
|
| AddTableFocus::InputColumnName
|
||||||
|
| AddTableFocus::InputColumnType
|
||||||
|
);
|
||||||
|
app_state.ui.focus_outside_canvas = !is_canvas_input;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Page::Intro(state) => {
|
||||||
|
if state.handle_movement(ma) {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional page-specific handlers (non-movement or rich actions)
|
||||||
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(
|
||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
&mut self.is_edit_mode,
|
|
||||||
buffer_state,
|
buffer_state,
|
||||||
client_clone,
|
client_clone,
|
||||||
sender_clone,
|
sender_clone,
|
||||||
@@ -413,7 +506,7 @@ impl EventHandler {
|
|||||||
if let Page::AddTable(add_table_state) = &mut router.current {
|
if let Page::AddTable(add_table_state) = &mut router.current {
|
||||||
let client_clone = self.grpc_client.clone();
|
let client_clone = self.grpc_client.clone();
|
||||||
let sender_clone = self.save_table_result_sender.clone();
|
let sender_clone = self.save_table_result_sender.clone();
|
||||||
if add_table_nav::handle_add_table_navigation(
|
if add_table::nav::handle_add_table_navigation(
|
||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
@@ -428,7 +521,12 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let nav_outcome = navigation::handle_navigation_event(
|
// Generic navigation for the rest (Intro/Login/Register/Form)
|
||||||
|
let nav_outcome = if matches!(&router.current, Page::AddTable(_) | Page::AddLogic(_)) {
|
||||||
|
// Skip generic navigation for AddTable/AddLogic (they have their own handlers)
|
||||||
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
|
} else {
|
||||||
|
navigation::handle_navigation_event(
|
||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
@@ -437,7 +535,9 @@ impl EventHandler {
|
|||||||
&mut self.command_input,
|
&mut self.command_input,
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
&mut self.navigation_state,
|
&mut self.navigation_state,
|
||||||
).await;
|
).await
|
||||||
|
};
|
||||||
|
|
||||||
match nav_outcome {
|
match nav_outcome {
|
||||||
Ok(EventOutcome::ButtonSelected { context, index }) => {
|
Ok(EventOutcome::ButtonSelected { context, index }) => {
|
||||||
let message = match context {
|
let message = match context {
|
||||||
@@ -448,14 +548,8 @@ impl EventHandler {
|
|||||||
index,
|
index,
|
||||||
);
|
);
|
||||||
if let Page::Admin(admin_state) = &mut router.current {
|
if let Page::Admin(admin_state) = &mut router.current {
|
||||||
if !app_state
|
if !app_state.profile_tree.profiles.is_empty() {
|
||||||
.profile_tree
|
admin_state.profile_list_state.select(Some(0));
|
||||||
.profiles
|
|
||||||
.is_empty()
|
|
||||||
{
|
|
||||||
admin_state
|
|
||||||
.profile_list_state
|
|
||||||
.select(Some(0));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
format!("Intro Option {} selected", index)
|
format!("Intro Option {} selected", index)
|
||||||
@@ -504,15 +598,16 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
UiContext::Admin => {
|
UiContext::Admin => {
|
||||||
if let Page::Admin(admin_state) = &router.current {
|
if let Page::Admin(admin_state) = &router.current {
|
||||||
admin::handle_admin_selection(
|
admin::tui::handle_admin_selection(
|
||||||
app_state,
|
app_state,
|
||||||
admin_state,
|
admin_state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
format!("Admin Option {} selected", index)
|
format!("Admin Option {} selected", index)
|
||||||
}
|
}
|
||||||
UiContext::Dialog => "Internal error: Unexpected dialog state"
|
UiContext::Dialog => {
|
||||||
.to_string(),
|
"Internal error: Unexpected dialog state".to_string()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return Ok(EventOutcome::Ok(message));
|
return Ok(EventOutcome::Ok(message));
|
||||||
}
|
}
|
||||||
@@ -520,164 +615,34 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppMode::ReadOnly => {
|
|
||||||
// First let the canvas editor try to handle the key
|
|
||||||
if let Page::Form(_) = &router.current {
|
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
|
||||||
let outcome = editor.handle_key_event(key_event);
|
|
||||||
let new_mode = AppMode::from(editor.mode());
|
|
||||||
match outcome {
|
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(msg));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Consumed(None) => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Pending => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::NotMatched => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
// Fall through
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entering command mode is still a client-level action
|
|
||||||
if config.get_app_action(key_code, modifiers) == Some("enter_command_mode")
|
|
||||||
&& ModeManager::can_enter_command_mode(current_mode)
|
|
||||||
{
|
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
|
||||||
editor.set_mode(CanvasMode::Command);
|
|
||||||
}
|
|
||||||
self.command_mode = true;
|
|
||||||
self.command_input.clear();
|
|
||||||
self.command_message.clear();
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle common actions (save, quit, etc.)
|
|
||||||
if let Some(action) = config.get_app_action(key_code, modifiers) {
|
|
||||||
match action {
|
|
||||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
|
||||||
return self
|
|
||||||
.handle_core_action(
|
|
||||||
action,
|
|
||||||
auth_state,
|
|
||||||
terminal,
|
|
||||||
app_state,
|
|
||||||
router,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
AppMode::Highlight => {
|
|
||||||
if let Page::Form(_) = &router.current {
|
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
|
||||||
let outcome = editor.handle_key_event(key_event);
|
|
||||||
let new_mode = AppMode::from(editor.mode());
|
|
||||||
match outcome {
|
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(msg));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Consumed(None) => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Pending => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::NotMatched => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
// Fall through
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
AppMode::Edit => {
|
|
||||||
// Handle common actions (save, quit, etc.)
|
|
||||||
if let Some(action) = config.get_app_action(key_code, modifiers) {
|
|
||||||
match action {
|
|
||||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
|
||||||
return self
|
|
||||||
.handle_core_action(
|
|
||||||
action,
|
|
||||||
auth_state,
|
|
||||||
terminal,
|
|
||||||
app_state,
|
|
||||||
router,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let the canvas editor handle edit-mode keys
|
|
||||||
if let Page::Form(_) = &router.current {
|
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
|
||||||
let outcome = editor.handle_key_event(key_event);
|
|
||||||
let new_mode = AppMode::from(editor.mode());
|
|
||||||
match outcome {
|
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
||||||
self.command_message = msg.clone();
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(msg));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Consumed(None) => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Pending => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::NotMatched => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
// Fall through
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
AppMode::Command => {
|
AppMode::Command => {
|
||||||
if config.is_exit_command_mode(key_code, modifiers) {
|
if config.is_exit_command_mode(key_code, modifiers) {
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
self.command_message.clear();
|
self.command_message.clear();
|
||||||
self.command_mode = false;
|
self.command_mode = false;
|
||||||
self.key_sequence_tracker.reset();
|
self.key_sequence_tracker.reset();
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
editor.set_mode(CanvasMode::ReadOnly);
|
editor.set_mode(CanvasMode::ReadOnly);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Ok(EventOutcome::Ok(
|
return Ok(EventOutcome::Ok(
|
||||||
"Exited command mode".to_string(),
|
"Exited command mode".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.is_command_execute(key_code, modifiers) {
|
if config.is_command_execute(key_code, modifiers) {
|
||||||
let (mut current_position, total_count) = if let Page::Form(fs) = &router.current {
|
let (mut current_position, total_count) =
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(fs) =
|
||||||
|
app_state.form_state_for_path_ref(path)
|
||||||
|
{
|
||||||
(fs.current_position, fs.total_count)
|
(fs.current_position, fs.total_count)
|
||||||
} else {
|
} else {
|
||||||
(1, 0)
|
(1, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(1, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
let outcome = command_mode::handle_command_event(
|
let outcome = command_mode::handle_command_event(
|
||||||
@@ -693,9 +658,11 @@ impl EventHandler {
|
|||||||
&mut current_position,
|
&mut current_position,
|
||||||
total_count,
|
total_count,
|
||||||
).await?;
|
).await?;
|
||||||
if let Page::Form(fs) = &mut router.current {
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
fs.current_position = current_position;
|
fs.current_position = current_position;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.command_mode = false;
|
self.command_mode = false;
|
||||||
self.key_sequence_tracker.reset();
|
self.key_sequence_tracker.reset();
|
||||||
let new_mode = ModeManager::derive_mode(
|
let new_mode = ModeManager::derive_mode(
|
||||||
@@ -821,27 +788,19 @@ impl EventHandler {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(EventOutcome::Ok(message))
|
Ok(EventOutcome::Ok(message))
|
||||||
} else {
|
} else {
|
||||||
let save_outcome = if let Page::Form(_) = &router.current {
|
if let Page::Form(path) = &router.current {
|
||||||
save(
|
forms::event::save_form(app_state, path, &mut self.grpc_client).await
|
||||||
app_state,
|
|
||||||
&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" => {
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
editor.cleanup_cursor()?;
|
editor.cleanup_cursor()?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok(EventOutcome::Exit(
|
Ok(EventOutcome::Exit(
|
||||||
"Force exiting without saving.".to_string(),
|
"Force exiting without saving.".to_string(),
|
||||||
@@ -856,20 +815,22 @@ impl EventHandler {
|
|||||||
app_state,
|
app_state,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else if let Page::Form(path) = &router.current {
|
||||||
let save_outcome = save(
|
let save_result = forms::event::save_form(app_state, path, &mut self.grpc_client).await?;
|
||||||
app_state,
|
match save_result {
|
||||||
&mut self.grpc_client,
|
EventOutcome::DataSaved(_, msg) => msg,
|
||||||
).await?;
|
EventOutcome::Ok(msg) => msg,
|
||||||
match save_outcome {
|
_ => "Saved".to_string(),
|
||||||
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 Some(editor) = &mut app_state.form_editor {
|
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
editor.cleanup_cursor()?;
|
editor.cleanup_cursor()?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok(EventOutcome::Exit(format!(
|
Ok(EventOutcome::Exit(format!(
|
||||||
"{}. Exiting application.",
|
"{}. Exiting application.",
|
||||||
@@ -886,12 +847,8 @@ impl EventHandler {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
if let Page::Form(_) = &router.current {
|
if let Page::Form(path) = &router.current {
|
||||||
revert(
|
return forms::event::revert_form(app_state, path, &mut self.grpc_client).await;
|
||||||
app_state,
|
|
||||||
&mut self.grpc_client,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else {
|
} else {
|
||||||
"Nothing to revert".to_string()
|
"Nothing to revert".to_string()
|
||||||
}
|
}
|
||||||
@@ -918,7 +875,7 @@ impl EventHandler {
|
|||||||
"exit_highlight_mode" |
|
"exit_highlight_mode" |
|
||||||
"save" |
|
"save" |
|
||||||
"quit" |
|
"quit" |
|
||||||
"force_quit" |
|
"Force_quit" |
|
||||||
"save_and_quit" |
|
"save_and_quit" |
|
||||||
"revert" |
|
"revert" |
|
||||||
"enter_decider" |
|
"enter_decider" |
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
// src/modes/handlers/mode_manager.rs
|
// src/modes/handlers/mode_manager.rs
|
||||||
|
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::modes::handlers::event::EventHandler;
|
use crate::modes::handlers::event::EventHandler;
|
||||||
use crate::state::pages::add_logic::AddLogicFocus;
|
|
||||||
use crate::pages::routing::{Router, Page};
|
use crate::pages::routing::{Router, Page};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AppMode {
|
pub enum AppMode {
|
||||||
General, // For intro and admin screens
|
/// General mode = when focus is outside any canvas
|
||||||
ReadOnly, // Canvas read-only mode
|
/// (Intro, Admin, Login/Register buttons, AddTable/AddLogic menus, dialogs, etc.)
|
||||||
Edit, // Canvas edit mode
|
General,
|
||||||
Highlight, // Canvas highlight/visual mode
|
|
||||||
Command, // Command mode overlay
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<canvas::AppMode> for AppMode {
|
/// Command overlay (":" or "ctrl+;"), available globally
|
||||||
fn from(mode: canvas::AppMode) -> Self {
|
Command,
|
||||||
match mode {
|
|
||||||
canvas::AppMode::General => AppMode::General,
|
|
||||||
canvas::AppMode::ReadOnly => AppMode::ReadOnly,
|
|
||||||
canvas::AppMode::Edit => AppMode::Edit,
|
|
||||||
canvas::AppMode::Highlight => AppMode::Highlight,
|
|
||||||
canvas::AppMode::Command => AppMode::Command,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ModeManager;
|
pub struct ModeManager;
|
||||||
|
|
||||||
impl ModeManager {
|
impl ModeManager {
|
||||||
/// Determine current mode based on app state + router
|
/// Determine current mode:
|
||||||
|
/// - If navigation palette is active → General
|
||||||
|
/// - If command overlay is active → Command
|
||||||
|
/// - If focus is inside a canvas (Form, Login, Register, AddTable, AddLogic) → let canvas handle its own mode
|
||||||
|
/// - Otherwise → General
|
||||||
pub fn derive_mode(
|
pub fn derive_mode(
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
event_handler: &EventHandler,
|
event_handler: &EventHandler,
|
||||||
@@ -39,76 +32,28 @@ impl ModeManager {
|
|||||||
return AppMode::General;
|
return AppMode::General;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit command mode flag
|
// Explicit command overlay flag
|
||||||
if event_handler.command_mode {
|
if event_handler.command_mode {
|
||||||
return AppMode::Command;
|
return AppMode::Command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If focus is inside a canvas, we don't duplicate canvas modes here.
|
||||||
|
// Canvas crate owns ReadOnly/Edit/Highlight internally.
|
||||||
match &router.current {
|
match &router.current {
|
||||||
// --- Form view ---
|
Page::Form(_)
|
||||||
Page::Form(_) if !app_state.ui.focus_outside_canvas => {
|
| Page::Login(_)
|
||||||
if let Some(editor) = &app_state.form_editor {
|
| Page::Register(_)
|
||||||
return AppMode::from(editor.mode());
|
| Page::AddTable(_)
|
||||||
}
|
| Page::AddLogic(_) if !app_state.ui.focus_outside_canvas => {
|
||||||
|
// Canvas active → let canvas handle its own AppMode
|
||||||
AppMode::General
|
AppMode::General
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- AddLogic view ---
|
|
||||||
Page::AddLogic(state) => match state.current_focus {
|
|
||||||
AddLogicFocus::InputLogicName
|
|
||||||
| AddLogicFocus::InputTargetColumn
|
|
||||||
| AddLogicFocus::InputDescription => {
|
|
||||||
if event_handler.is_edit_mode {
|
|
||||||
AppMode::Edit
|
|
||||||
} else {
|
|
||||||
AppMode::ReadOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => AppMode::General,
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- AddTable view ---
|
|
||||||
Page::AddTable(_) => {
|
|
||||||
if app_state.ui.focus_outside_canvas {
|
|
||||||
AppMode::General
|
|
||||||
} else if event_handler.is_edit_mode {
|
|
||||||
AppMode::Edit
|
|
||||||
} else {
|
|
||||||
AppMode::ReadOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Login/Register views ---
|
|
||||||
Page::Login(_) | Page::Register(_) => {
|
|
||||||
if event_handler.is_edit_mode {
|
|
||||||
AppMode::Edit
|
|
||||||
} else {
|
|
||||||
AppMode::ReadOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Everything else (Intro, Admin, etc.) ---
|
|
||||||
_ => AppMode::General,
|
_ => AppMode::General,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode transition rules
|
/// Command overlay can be entered from anywhere (General or Canvas).
|
||||||
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
|
pub fn can_enter_command_mode(_current_mode: AppMode) -> bool {
|
||||||
!matches!(current_mode, AppMode::Edit)
|
true
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
|
|
||||||
matches!(current_mode, AppMode::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
|
|
||||||
matches!(
|
|
||||||
current_mode,
|
|
||||||
AppMode::Edit | AppMode::Command | AppMode::Highlight
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool {
|
|
||||||
matches!(current_mode, AppMode::ReadOnly)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
client/src/movement/actions.rs
Normal file
12
client/src/movement/actions.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// src/movement/actions.rs
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum MovementAction {
|
||||||
|
Next,
|
||||||
|
Previous,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Select,
|
||||||
|
Esc,
|
||||||
|
}
|
||||||
32
client/src/movement/lib.rs
Normal file
32
client/src/movement/lib.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// src/movement/lib.rs
|
||||||
|
|
||||||
|
use crate::movement::MovementAction;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn move_focus<T: Copy + Eq>(
|
||||||
|
order: &[T],
|
||||||
|
current: &mut T,
|
||||||
|
action: MovementAction,
|
||||||
|
) -> bool {
|
||||||
|
if order.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(pos) = order.iter().position(|k| *k == *current) {
|
||||||
|
match action {
|
||||||
|
MovementAction::Previous | MovementAction::Up | MovementAction::Left => {
|
||||||
|
if pos > 0 {
|
||||||
|
*current = order[pos - 1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Down | MovementAction::Right => {
|
||||||
|
if pos + 1 < order.len() {
|
||||||
|
*current = order[pos + 1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
6
client/src/movement/mod.rs
Normal file
6
client/src/movement/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// src/movement/mod.rs
|
||||||
|
pub mod actions;
|
||||||
|
pub mod lib;
|
||||||
|
|
||||||
|
pub use actions::MovementAction;
|
||||||
|
pub use lib::move_focus;
|
||||||
60
client/src/pages/admin/admin/event.rs
Normal file
60
client/src/pages/admin/admin/event.rs
Normal 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)
|
||||||
|
}
|
||||||
54
client/src/pages/admin/admin/loader.rs
Normal file
54
client/src/pages/admin/admin/loader.rs
Normal 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(())
|
||||||
|
}
|
||||||
9
client/src/pages/admin/admin/mod.rs
Normal file
9
client/src/pages/admin/admin/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// src/pages/admin/admin/mod.rs
|
||||||
|
|
||||||
|
pub mod state;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod tui;
|
||||||
|
pub mod event;
|
||||||
|
pub mod loader;
|
||||||
|
|
||||||
|
pub use state::{AdminState, AdminFocus};
|
||||||
195
client/src/pages/admin/admin/state.rs
Normal file
195
client/src/pages/admin/admin/state.rs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// src/pages/admin/admin/state.rs
|
||||||
|
use ratatui::widgets::ListState;
|
||||||
|
use crate::pages::admin_panel::add_table::state::AddTableState;
|
||||||
|
use crate::pages::admin_panel::add_logic::state::AddLogicState;
|
||||||
|
use crate::movement::{move_focus, MovementAction};
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
|
||||||
|
/// Focus states for the admin panel
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum AdminFocus {
|
||||||
|
#[default]
|
||||||
|
ProfilesPane,
|
||||||
|
InsideProfilesList,
|
||||||
|
Tables,
|
||||||
|
InsideTablesList,
|
||||||
|
Button1,
|
||||||
|
Button2,
|
||||||
|
Button3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full admin panel state (for logged-in admins)
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub struct AdminState {
|
||||||
|
pub profiles: Vec<String>,
|
||||||
|
pub profile_list_state: ListState,
|
||||||
|
pub table_list_state: ListState,
|
||||||
|
pub selected_profile_index: Option<usize>,
|
||||||
|
pub selected_table_index: Option<usize>,
|
||||||
|
pub current_focus: AdminFocus,
|
||||||
|
pub add_table_state: AddTableState,
|
||||||
|
pub add_logic_state: AddLogicState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminState {
|
||||||
|
pub fn get_selected_index(&self) -> Option<usize> {
|
||||||
|
self.profile_list_state.selected()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_profile_name(&self) -> Option<&String> {
|
||||||
|
self.profile_list_state.selected().and_then(|i| self.profiles.get(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_profiles(&mut self, new_profiles: Vec<String>) {
|
||||||
|
let current_selection_index = self.profile_list_state.selected();
|
||||||
|
self.profiles = new_profiles;
|
||||||
|
|
||||||
|
if self.profiles.is_empty() {
|
||||||
|
self.profile_list_state.select(None);
|
||||||
|
} else {
|
||||||
|
let new_selection = match current_selection_index {
|
||||||
|
Some(index) => Some(index.min(self.profiles.len() - 1)),
|
||||||
|
None => Some(0),
|
||||||
|
};
|
||||||
|
self.profile_list_state.select(new_selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
if self.profiles.is_empty() {
|
||||||
|
self.profile_list_state.select(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = match self.profile_list_state.selected() {
|
||||||
|
Some(i) => if i >= self.profiles.len() - 1 { 0 } else { i + 1 },
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.profile_list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&mut self) {
|
||||||
|
if self.profiles.is_empty() {
|
||||||
|
self.profile_list_state.select(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = match self.profile_list_state.selected() {
|
||||||
|
Some(i) => if i == 0 { self.profiles.len() - 1 } else { i - 1 },
|
||||||
|
None => self.profiles.len() - 1,
|
||||||
|
};
|
||||||
|
self.profile_list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_movement(
|
||||||
|
&mut self,
|
||||||
|
app: &AppState,
|
||||||
|
action: MovementAction,
|
||||||
|
) -> bool {
|
||||||
|
use AdminFocus::*;
|
||||||
|
|
||||||
|
const ORDER: [AdminFocus; 5] = [
|
||||||
|
ProfilesPane,
|
||||||
|
Tables,
|
||||||
|
Button1,
|
||||||
|
Button2,
|
||||||
|
Button3,
|
||||||
|
];
|
||||||
|
|
||||||
|
match (self.current_focus, action) {
|
||||||
|
(ProfilesPane, MovementAction::Select) => {
|
||||||
|
if !app.profile_tree.profiles.is_empty()
|
||||||
|
&& self.profile_list_state.selected().is_none()
|
||||||
|
{
|
||||||
|
self.profile_list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.current_focus = InsideProfilesList;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(Tables, MovementAction::Select) => {
|
||||||
|
let p_idx = self
|
||||||
|
.selected_profile_index
|
||||||
|
.or_else(|| self.profile_list_state.selected());
|
||||||
|
if let Some(pi) = p_idx {
|
||||||
|
let len = app
|
||||||
|
.profile_tree
|
||||||
|
.profiles
|
||||||
|
.get(pi)
|
||||||
|
.map(|p| p.tables.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
if len > 0 && self.table_list_state.selected().is_none() {
|
||||||
|
self.table_list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.current_focus = InsideTablesList;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.current_focus {
|
||||||
|
InsideProfilesList => match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if !app.profile_tree.profiles.is_empty() {
|
||||||
|
let curr = self.profile_list_state.selected().unwrap_or(0);
|
||||||
|
let next = curr.saturating_sub(1);
|
||||||
|
self.profile_list_state.select(Some(next));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
let len = app.profile_tree.profiles.len();
|
||||||
|
if len > 0 {
|
||||||
|
let curr = self.profile_list_state.selected().unwrap_or(0);
|
||||||
|
let next = if curr + 1 < len { curr + 1 } else { curr };
|
||||||
|
self.profile_list_state.select(Some(next));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.current_focus = ProfilesPane;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => true,
|
||||||
|
MovementAction::Select => false,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
InsideTablesList => {
|
||||||
|
let tables_len = {
|
||||||
|
let p_idx = self
|
||||||
|
.selected_profile_index
|
||||||
|
.or_else(|| self.profile_list_state.selected());
|
||||||
|
p_idx.and_then(|pi| app.profile_tree.profiles.get(pi))
|
||||||
|
.map(|p| p.tables.len())
|
||||||
|
.unwrap_or(0)
|
||||||
|
};
|
||||||
|
match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if tables_len > 0 {
|
||||||
|
let curr = self.table_list_state.selected().unwrap_or(0);
|
||||||
|
let next = curr.saturating_sub(1);
|
||||||
|
self.table_list_state.select(Some(next));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if tables_len > 0 {
|
||||||
|
let curr = self.table_list_state.selected().unwrap_or(0);
|
||||||
|
let next = if curr + 1 < tables_len { curr + 1 } else { curr };
|
||||||
|
self.table_list_state.select(Some(next));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.current_focus = Tables;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => true,
|
||||||
|
MovementAction::Select => false,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
move_focus(&ORDER, &mut self.current_focus, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
// src/pages/admin/admin/tui.rs
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::pages::admin::AdminState;
|
||||||
|
|
||||||
pub fn handle_admin_selection(app_state: &mut AppState, admin_state: &AdminState) {
|
pub fn handle_admin_selection(app_state: &mut AppState, admin_state: &AdminState) {
|
||||||
let profiles = &app_state.profile_tree.profiles;
|
let profiles = &app_state.profile_tree.profiles;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/components/admin/admin_panel_admin.rs
|
// src/pages/admin/admin/ui.rs
|
||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::pages::admin::{AdminFocus, AdminState};
|
use crate::pages::admin::{AdminFocus, AdminState};
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
// src/functions/modes/navigation/admin_nav.rs
|
// src/pages/admin/main/logic.rs
|
||||||
use crate::state::pages::admin::{AdminFocus, AdminState};
|
use crate::pages::admin::{AdminFocus, AdminState};
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::buffer::state::{BufferState, AppView};
|
use crate::buffer::state::{BufferState, AppView};
|
||||||
use crate::state::pages::add_table::{AddTableState, LinkDefinition};
|
use crate::pages::admin_panel::add_table::state::{AddTableState, LinkDefinition};
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use crate::state::pages::add_logic::{AddLogicState, AddLogicFocus}; // Added AddLogicFocus import
|
use crate::pages::admin_panel::add_logic::state::{AddLogicState, AddLogicFocus};
|
||||||
|
|
||||||
// Helper functions list_select_next and list_select_previous remain the same
|
// Helper functions list_select_next and list_select_previous remain the same
|
||||||
fn list_select_next(list_state: &mut ListState, item_count: usize) {
|
fn list_select_next(list_state: &mut ListState, item_count: usize) {
|
||||||
7
client/src/pages/admin/main/mod.rs
Normal file
7
client/src/pages/admin/main/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// src/pages/admin/main/mod.rs
|
||||||
|
|
||||||
|
pub mod state;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod logic;
|
||||||
|
|
||||||
|
pub use state::NonAdminState;
|
||||||
55
client/src/pages/admin/main/state.rs
Normal file
55
client/src/pages/admin/main/state.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// src/pages/admin/main/state.rs
|
||||||
|
use ratatui::widgets::ListState;
|
||||||
|
|
||||||
|
/// State for non-admin users (simple profile browser)
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub struct NonAdminState {
|
||||||
|
pub profiles: Vec<String>, // profile names
|
||||||
|
pub profile_list_state: ListState, // highlight state
|
||||||
|
pub selected_profile_index: Option<usize>, // persistent selection
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NonAdminState {
|
||||||
|
pub fn get_selected_index(&self) -> Option<usize> {
|
||||||
|
self.profile_list_state.selected()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_profiles(&mut self, new_profiles: Vec<String>) {
|
||||||
|
let current_selection_index = self.profile_list_state.selected();
|
||||||
|
self.profiles = new_profiles;
|
||||||
|
|
||||||
|
if self.profiles.is_empty() {
|
||||||
|
self.profile_list_state.select(None);
|
||||||
|
} else {
|
||||||
|
let new_selection = match current_selection_index {
|
||||||
|
Some(index) => Some(index.min(self.profiles.len() - 1)),
|
||||||
|
None => Some(0),
|
||||||
|
};
|
||||||
|
self.profile_list_state.select(new_selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
if self.profiles.is_empty() {
|
||||||
|
self.profile_list_state.select(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = match self.profile_list_state.selected() {
|
||||||
|
Some(i) => if i >= self.profiles.len() - 1 { 0 } else { i + 1 },
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.profile_list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&mut self) {
|
||||||
|
if self.profiles.is_empty() {
|
||||||
|
self.profile_list_state.select(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = match self.profile_list_state.selected() {
|
||||||
|
Some(i) => if i == 0 { self.profiles.len() - 1 } else { i - 1 },
|
||||||
|
None => self.profiles.len() - 1,
|
||||||
|
};
|
||||||
|
self.profile_list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// src/components/admin/admin_panel.rs
|
// src/pages/admin/main/ui.rs
|
||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::pages::admin::AdminState;
|
||||||
use common::proto::komp_ac::table_definition::ProfileTreeResponse;
|
use common::proto::komp_ac::table_definition::ProfileTreeResponse;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
@@ -12,7 +12,8 @@ use ratatui::{
|
|||||||
widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Wrap},
|
widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use super::admin_panel_admin::render_admin_panel_admin;
|
use crate::state::pages::auth::UserRole;
|
||||||
|
use crate::pages::admin::admin::ui::render_admin_panel_admin;
|
||||||
|
|
||||||
pub fn render_admin_panel(
|
pub fn render_admin_panel(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -44,7 +45,11 @@ pub fn render_admin_panel(
|
|||||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||||
.split(chunks[1]);
|
.split(chunks[1]);
|
||||||
|
|
||||||
if auth_state.role.as_deref() != Some("admin") {
|
match auth_state.role {
|
||||||
|
Some(UserRole::Admin) => {
|
||||||
|
render_admin_panel_admin(f, chunks[1], app_state, admin_state, theme);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
render_admin_panel_non_admin(
|
render_admin_panel_non_admin(
|
||||||
f,
|
f,
|
||||||
admin_state,
|
admin_state,
|
||||||
@@ -53,21 +58,14 @@ pub fn render_admin_panel(
|
|||||||
profile_tree,
|
profile_tree,
|
||||||
selected_profile,
|
selected_profile,
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
render_admin_panel_admin(
|
|
||||||
f,
|
|
||||||
chunks[1],
|
|
||||||
app_state,
|
|
||||||
admin_state,
|
|
||||||
theme,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the view for non-admin users (profile list and details).
|
/// Renders the view for non-admin users (profile list and details).
|
||||||
fn render_admin_panel_non_admin(
|
fn render_admin_panel_non_admin(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
admin_state: &AdminState,
|
admin_state: &mut AdminState,
|
||||||
content_chunks: &[Rect],
|
content_chunks: &[Rect],
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
profile_tree: &ProfileTreeResponse,
|
profile_tree: &ProfileTreeResponse,
|
||||||
@@ -92,8 +90,7 @@ fn render_admin_panel_non_admin(
|
|||||||
.block(Block::default().title("Profiles"))
|
.block(Block::default().title("Profiles"))
|
||||||
.highlight_style(Style::default().bg(theme.highlight).fg(theme.bg));
|
.highlight_style(Style::default().bg(theme.highlight).fg(theme.bg));
|
||||||
|
|
||||||
let mut profile_list_state_clone = admin_state.profile_list_state.clone();
|
f.render_stateful_widget(list, content_chunks[0], &mut admin_state.profile_list_state);
|
||||||
f.render_stateful_widget(list, content_chunks[0], &mut profile_list_state_clone);
|
|
||||||
|
|
||||||
// Profile details - Use selection info from admin_state
|
// Profile details - Use selection info from admin_state
|
||||||
if let Some(profile) = admin_state
|
if let Some(profile) = admin_state
|
||||||
7
client/src/pages/admin/mod.rs
Normal file
7
client/src/pages/admin/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// src/pages/admin/mod.rs
|
||||||
|
|
||||||
|
pub mod main; // non-admin
|
||||||
|
pub mod admin; // full admin panel
|
||||||
|
|
||||||
|
pub use main::NonAdminState;
|
||||||
|
pub use admin::{AdminState, AdminFocus};
|
||||||
5
client/src/pages/admin_panel/add_logic/mod.rs
Normal file
5
client/src/pages/admin_panel/add_logic/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// src/pages/admin_panel/add_logic/mod.rs
|
||||||
|
|
||||||
|
pub mod ui;
|
||||||
|
pub mod nav;
|
||||||
|
pub mod state;
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// src/functions/modes/navigation/add_logic_nav.rs
|
// src/pages/admin_panel/add_logic/nav.rs
|
||||||
|
|
||||||
use crate::config::binds::config::{Config, EditorKeybindingMode};
|
use crate::config::binds::config::{Config, EditorKeybindingMode};
|
||||||
use crate::state::{
|
use crate::state::app::state::AppState;
|
||||||
app::state::AppState,
|
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState};
|
||||||
pages::add_logic::{AddLogicFocus, AddLogicState},
|
|
||||||
};
|
|
||||||
use crate::buffer::{AppView, BufferState};
|
use crate::buffer::{AppView, BufferState};
|
||||||
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
||||||
use crate::services::GrpcClient;
|
use crate::services::GrpcClient;
|
||||||
@@ -12,7 +11,7 @@ use anyhow::Result;
|
|||||||
use crate::components::common::text_editor::TextEditor;
|
use crate::components::common::text_editor::TextEditor;
|
||||||
use crate::services::ui_service::UiService;
|
use crate::services::ui_service::UiService;
|
||||||
use tui_textarea::CursorMove;
|
use tui_textarea::CursorMove;
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::pages::admin::AdminState;
|
||||||
use crate::pages::routing::{Router, Page};
|
use crate::pages::routing::{Router, Page};
|
||||||
|
|
||||||
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
||||||
@@ -21,7 +20,6 @@ pub fn handle_add_logic_navigation(
|
|||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
is_edit_mode: &mut bool,
|
|
||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
grpc_client: GrpcClient,
|
grpc_client: GrpcClient,
|
||||||
save_logic_sender: SaveLogicResultSender,
|
save_logic_sender: SaveLogicResultSender,
|
||||||
@@ -29,18 +27,17 @@ pub fn handle_add_logic_navigation(
|
|||||||
router: &mut Router,
|
router: &mut Router,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Page::AddLogic(add_logic_state) = &mut router.current {
|
if let Page::AddLogic(add_logic_state) = &mut router.current {
|
||||||
|
// === FULLSCREEN SCRIPT EDITING ===
|
||||||
// === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION ===
|
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
||||||
// === AUTOCOMPLETE HANDLING ===
|
// === AUTOCOMPLETE HANDLING ===
|
||||||
if add_logic_state.script_editor_autocomplete_active {
|
if add_logic_state.script_editor_autocomplete_active {
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
// ... (Char, Backspace, Tab, Down, Up cases remain the same) ...
|
|
||||||
KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => {
|
KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => {
|
||||||
add_logic_state.script_editor_filter_text.push(c);
|
add_logic_state.script_editor_filter_text.push(c);
|
||||||
add_logic_state.update_script_editor_suggestions();
|
add_logic_state.update_script_editor_suggestions();
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
&mut editor_borrow,
|
&mut editor_borrow,
|
||||||
key_event,
|
key_event,
|
||||||
@@ -48,7 +45,8 @@ pub fn handle_add_logic_navigation(
|
|||||||
&mut add_logic_state.vim_state,
|
&mut add_logic_state.vim_state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
*command_message = format!("Filtering: @{}", add_logic_state.script_editor_filter_text);
|
*command_message =
|
||||||
|
format!("Filtering: @{}", add_logic_state.script_editor_filter_text);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
@@ -56,7 +54,8 @@ pub fn handle_add_logic_navigation(
|
|||||||
add_logic_state.script_editor_filter_text.pop();
|
add_logic_state.script_editor_filter_text.pop();
|
||||||
add_logic_state.update_script_editor_suggestions();
|
add_logic_state.update_script_editor_suggestions();
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
&mut editor_borrow,
|
&mut editor_borrow,
|
||||||
key_event,
|
key_event,
|
||||||
@@ -64,18 +63,27 @@ pub fn handle_add_logic_navigation(
|
|||||||
&mut add_logic_state.vim_state,
|
&mut add_logic_state.vim_state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
*command_message = if add_logic_state.script_editor_filter_text.is_empty() {
|
*command_message =
|
||||||
|
if add_logic_state.script_editor_filter_text.is_empty() {
|
||||||
"Autocomplete: @".to_string()
|
"Autocomplete: @".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("Filtering: @{}", add_logic_state.script_editor_filter_text)
|
format!(
|
||||||
|
"Filtering: @{}",
|
||||||
|
add_logic_state.script_editor_filter_text
|
||||||
|
)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let should_deactivate = if let Some((trigger_line, trigger_col)) = add_logic_state.script_editor_trigger_position {
|
let should_deactivate =
|
||||||
|
if let Some((trigger_line, trigger_col)) =
|
||||||
|
add_logic_state.script_editor_trigger_position
|
||||||
|
{
|
||||||
let current_cursor = {
|
let current_cursor = {
|
||||||
let editor_borrow = add_logic_state.script_content_editor.borrow();
|
let editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow();
|
||||||
editor_borrow.cursor()
|
editor_borrow.cursor()
|
||||||
};
|
};
|
||||||
current_cursor.0 == trigger_line && current_cursor.1 == trigger_col + 1
|
current_cursor.0 == trigger_line
|
||||||
|
&& current_cursor.1 == trigger_col + 1
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
@@ -84,7 +92,8 @@ pub fn handle_add_logic_navigation(
|
|||||||
*command_message = "Autocomplete cancelled".to_string();
|
*command_message = "Autocomplete cancelled".to_string();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
&mut editor_borrow,
|
&mut editor_borrow,
|
||||||
key_event,
|
key_event,
|
||||||
@@ -97,75 +106,120 @@ pub fn handle_add_logic_navigation(
|
|||||||
}
|
}
|
||||||
KeyCode::Tab | KeyCode::Down => {
|
KeyCode::Tab | KeyCode::Down => {
|
||||||
if !add_logic_state.script_editor_suggestions.is_empty() {
|
if !add_logic_state.script_editor_suggestions.is_empty() {
|
||||||
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
|
let current = add_logic_state
|
||||||
let next = (current + 1) % add_logic_state.script_editor_suggestions.len();
|
.script_editor_selected_suggestion_index
|
||||||
|
.unwrap_or(0);
|
||||||
|
let next =
|
||||||
|
(current + 1) % add_logic_state.script_editor_suggestions.len();
|
||||||
add_logic_state.script_editor_selected_suggestion_index = Some(next);
|
add_logic_state.script_editor_selected_suggestion_index = Some(next);
|
||||||
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[next]);
|
*command_message = format!(
|
||||||
|
"Selected: {}",
|
||||||
|
add_logic_state.script_editor_suggestions[next]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
if !add_logic_state.script_editor_suggestions.is_empty() {
|
if !add_logic_state.script_editor_suggestions.is_empty() {
|
||||||
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
|
let current = add_logic_state
|
||||||
|
.script_editor_selected_suggestion_index
|
||||||
|
.unwrap_or(0);
|
||||||
let prev = if current == 0 {
|
let prev = if current == 0 {
|
||||||
add_logic_state.script_editor_suggestions.len() - 1
|
add_logic_state.script_editor_suggestions.len() - 1
|
||||||
} else {
|
} else {
|
||||||
current - 1
|
current - 1
|
||||||
};
|
};
|
||||||
add_logic_state.script_editor_selected_suggestion_index = Some(prev);
|
add_logic_state.script_editor_selected_suggestion_index = Some(prev);
|
||||||
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[prev]);
|
*command_message = format!(
|
||||||
|
"Selected: {}",
|
||||||
|
add_logic_state.script_editor_suggestions[prev]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if let Some(selected_idx) = add_logic_state.script_editor_selected_suggestion_index {
|
if let Some(selected_idx) =
|
||||||
if let Some(suggestion) = add_logic_state.script_editor_suggestions.get(selected_idx).cloned() {
|
add_logic_state.script_editor_selected_suggestion_index
|
||||||
let trigger_pos = add_logic_state.script_editor_trigger_position;
|
{
|
||||||
let filter_len = add_logic_state.script_editor_filter_text.len();
|
if let Some(suggestion) = add_logic_state
|
||||||
|
.script_editor_suggestions
|
||||||
|
.get(selected_idx)
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
let trigger_pos =
|
||||||
|
add_logic_state.script_editor_trigger_position;
|
||||||
|
let filter_len =
|
||||||
|
add_logic_state.script_editor_filter_text.len();
|
||||||
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
add_logic_state.deactivate_script_editor_autocomplete();
|
||||||
add_logic_state.has_unsaved_changes = true;
|
add_logic_state.has_unsaved_changes = true;
|
||||||
|
|
||||||
if let Some(pos) = trigger_pos {
|
if let Some(pos) = trigger_pos {
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow_mut();
|
||||||
|
|
||||||
if suggestion == "sql" {
|
if suggestion == "sql" {
|
||||||
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, "sql");
|
replace_autocomplete_text(
|
||||||
|
&mut editor_borrow,
|
||||||
|
pos,
|
||||||
|
filter_len,
|
||||||
|
"sql",
|
||||||
|
);
|
||||||
editor_borrow.insert_str("('')");
|
editor_borrow.insert_str("('')");
|
||||||
// Move cursor back twice to be between the single quotes
|
editor_borrow.move_cursor(CursorMove::Back);
|
||||||
editor_borrow.move_cursor(CursorMove::Back); // Before ')'
|
editor_borrow.move_cursor(CursorMove::Back);
|
||||||
editor_borrow.move_cursor(CursorMove::Back); // Before ''' (inside '')
|
|
||||||
*command_message = "Inserted: @sql('')".to_string();
|
*command_message = "Inserted: @sql('')".to_string();
|
||||||
} else {
|
} else {
|
||||||
let is_table_selection = add_logic_state.is_table_name_suggestion(&suggestion);
|
let is_table_selection =
|
||||||
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, &suggestion);
|
add_logic_state.is_table_name_suggestion(&suggestion);
|
||||||
|
replace_autocomplete_text(
|
||||||
|
&mut editor_borrow,
|
||||||
|
pos,
|
||||||
|
filter_len,
|
||||||
|
&suggestion,
|
||||||
|
);
|
||||||
|
|
||||||
if is_table_selection {
|
if is_table_selection {
|
||||||
editor_borrow.insert_str(".");
|
editor_borrow.insert_str(".");
|
||||||
let new_cursor = editor_borrow.cursor();
|
let new_cursor = editor_borrow.cursor();
|
||||||
drop(editor_borrow); // Release borrow before calling add_logic_state methods
|
drop(editor_borrow);
|
||||||
|
|
||||||
add_logic_state.script_editor_trigger_position = Some(new_cursor);
|
add_logic_state.script_editor_trigger_position =
|
||||||
|
Some(new_cursor);
|
||||||
add_logic_state.script_editor_autocomplete_active = true;
|
add_logic_state.script_editor_autocomplete_active = true;
|
||||||
add_logic_state.script_editor_filter_text.clear();
|
add_logic_state.script_editor_filter_text.clear();
|
||||||
add_logic_state.trigger_column_autocomplete_for_table(suggestion.clone());
|
add_logic_state
|
||||||
|
.trigger_column_autocomplete_for_table(
|
||||||
|
suggestion.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let profile_name = add_logic_state.profile_name.clone();
|
let profile_name =
|
||||||
|
add_logic_state.profile_name.clone();
|
||||||
let table_name_for_fetch = suggestion.clone();
|
let table_name_for_fetch = suggestion.clone();
|
||||||
let mut client_clone = grpc_client.clone();
|
let mut client_clone = grpc_client.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
match UiService::fetch_columns_for_table(&mut client_clone, &profile_name, &table_name_for_fetch).await {
|
if let Err(e) = UiService::fetch_columns_for_table(
|
||||||
Ok(_columns) => {
|
&mut client_clone,
|
||||||
// Result handled by main UI loop
|
&profile_name,
|
||||||
}
|
&table_name_for_fetch,
|
||||||
Err(e) => {
|
)
|
||||||
tracing::error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name_for_fetch, e);
|
.await
|
||||||
}
|
{
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to fetch columns for {}.{}: {}",
|
||||||
|
profile_name,
|
||||||
|
table_name_for_fetch,
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
*command_message = format!("Selected table '{}', fetching columns...", suggestion);
|
*command_message = format!(
|
||||||
|
"Selected table '{}', fetching columns...",
|
||||||
|
suggestion
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
*command_message = format!("Inserted: {}", suggestion);
|
*command_message =
|
||||||
|
format!("Inserted: {}", suggestion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +228,8 @@ pub fn handle_add_logic_navigation(
|
|||||||
}
|
}
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
add_logic_state.deactivate_script_editor_autocomplete();
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
&mut editor_borrow,
|
&mut editor_borrow,
|
||||||
key_event,
|
key_event,
|
||||||
@@ -192,7 +247,8 @@ pub fn handle_add_logic_navigation(
|
|||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
add_logic_state.deactivate_script_editor_autocomplete();
|
||||||
*command_message = "Autocomplete cancelled".to_string();
|
*command_message = "Autocomplete cancelled".to_string();
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
&mut editor_borrow,
|
&mut editor_borrow,
|
||||||
key_event,
|
key_event,
|
||||||
@@ -205,9 +261,12 @@ pub fn handle_add_logic_navigation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger autocomplete with '@'
|
||||||
if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE {
|
if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE {
|
||||||
let should_trigger = match add_logic_state.editor_keybinding_mode {
|
let should_trigger = match add_logic_state.editor_keybinding_mode {
|
||||||
EditorKeybindingMode::Vim => *is_edit_mode,
|
EditorKeybindingMode::Vim => {
|
||||||
|
TextEditor::is_vim_insert_mode(&add_logic_state.vim_state)
|
||||||
|
}
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
if should_trigger {
|
if should_trigger {
|
||||||
@@ -216,7 +275,8 @@ pub fn handle_add_logic_navigation(
|
|||||||
editor_borrow.cursor()
|
editor_borrow.cursor()
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
&mut editor_borrow,
|
&mut editor_borrow,
|
||||||
key_event,
|
key_event,
|
||||||
@@ -234,12 +294,15 @@ pub fn handle_add_logic_navigation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Esc handling
|
||||||
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
|
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
|
||||||
match add_logic_state.editor_keybinding_mode {
|
match add_logic_state.editor_keybinding_mode {
|
||||||
EditorKeybindingMode::Vim => {
|
EditorKeybindingMode::Vim => {
|
||||||
if *is_edit_mode {
|
let was_insert =
|
||||||
|
TextEditor::is_vim_insert_mode(&add_logic_state.vim_state);
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow =
|
||||||
|
add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
&mut editor_borrow,
|
&mut editor_borrow,
|
||||||
key_event,
|
key_event,
|
||||||
@@ -247,32 +310,26 @@ pub fn handle_add_logic_navigation(
|
|||||||
&mut add_logic_state.vim_state,
|
&mut add_logic_state.vim_state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
|
if was_insert {
|
||||||
*is_edit_mode = false;
|
*command_message =
|
||||||
*command_message = "VIM: Normal Mode. Esc again to exit script.".to_string();
|
"VIM: Normal Mode. Esc again to exit script.".to_string();
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
add_logic_state.current_focus =
|
||||||
|
AddLogicFocus::ScriptContentPreview;
|
||||||
app_state.ui.focus_outside_canvas = true;
|
app_state.ui.focus_outside_canvas = true;
|
||||||
*is_edit_mode = false;
|
|
||||||
*command_message = "Exited script editing.".to_string();
|
*command_message = "Exited script editing.".to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if *is_edit_mode {
|
|
||||||
*is_edit_mode = false;
|
|
||||||
*command_message = "Exited script edit. Esc again to exit script.".to_string();
|
|
||||||
} else {
|
|
||||||
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
app_state.ui.focus_outside_canvas = true;
|
app_state.ui.focus_outside_canvas = true;
|
||||||
*is_edit_mode = false;
|
|
||||||
*command_message = "Exited script editing.".to_string();
|
*command_message = "Exited script editing.".to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normal text input
|
||||||
let changed = {
|
let changed = {
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
@@ -285,12 +342,10 @@ pub fn handle_add_logic_navigation(
|
|||||||
if changed {
|
if changed {
|
||||||
add_logic_state.has_unsaved_changes = true;
|
add_logic_state.has_unsaved_changes = true;
|
||||||
}
|
}
|
||||||
if add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim {
|
|
||||||
*is_edit_mode = !TextEditor::is_vim_normal_mode(&add_logic_state.vim_state);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === NON-FULLSCREEN NAVIGATION ===
|
||||||
let action = config.get_general_action(key_event.code, key_event.modifiers);
|
let action = config.get_general_action(key_event.code, key_event.modifiers);
|
||||||
let current_focus = add_logic_state.current_focus;
|
let current_focus = add_logic_state.current_focus;
|
||||||
let mut handled = true;
|
let mut handled = true;
|
||||||
@@ -304,8 +359,12 @@ pub fn handle_add_logic_navigation(
|
|||||||
match current_focus {
|
match current_focus {
|
||||||
AddLogicFocus::InputLogicName => {}
|
AddLogicFocus::InputLogicName => {}
|
||||||
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
|
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
|
||||||
AddLogicFocus::InputDescription => new_focus = AddLogicFocus::InputTargetColumn,
|
AddLogicFocus::InputDescription => {
|
||||||
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
|
new_focus = AddLogicFocus::InputTargetColumn
|
||||||
|
}
|
||||||
|
AddLogicFocus::ScriptContentPreview => {
|
||||||
|
new_focus = AddLogicFocus::InputDescription
|
||||||
|
}
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
||||||
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
||||||
_ => handled = false,
|
_ => handled = false,
|
||||||
@@ -313,13 +372,19 @@ pub fn handle_add_logic_navigation(
|
|||||||
}
|
}
|
||||||
Some("move_down") => {
|
Some("move_down") => {
|
||||||
match current_focus {
|
match current_focus {
|
||||||
AddLogicFocus::InputLogicName => new_focus = AddLogicFocus::InputTargetColumn,
|
AddLogicFocus::InputLogicName => {
|
||||||
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputDescription,
|
new_focus = AddLogicFocus::InputTargetColumn
|
||||||
|
}
|
||||||
|
AddLogicFocus::InputTargetColumn => {
|
||||||
|
new_focus = AddLogicFocus::InputDescription
|
||||||
|
}
|
||||||
AddLogicFocus::InputDescription => {
|
AddLogicFocus::InputDescription => {
|
||||||
add_logic_state.last_canvas_field = 2;
|
add_logic_state.last_canvas_field = 2;
|
||||||
new_focus = AddLogicFocus::ScriptContentPreview;
|
new_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
},
|
}
|
||||||
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
|
AddLogicFocus::ScriptContentPreview => {
|
||||||
|
new_focus = AddLogicFocus::SaveButton
|
||||||
|
}
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
||||||
AddLogicFocus::CancelButton => {}
|
AddLogicFocus::CancelButton => {}
|
||||||
_ => handled = false,
|
_ => handled = false,
|
||||||
@@ -327,20 +392,30 @@ pub fn handle_add_logic_navigation(
|
|||||||
}
|
}
|
||||||
Some("next_option") => {
|
Some("next_option") => {
|
||||||
match current_focus {
|
match current_focus {
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
|
AddLogicFocus::InputLogicName
|
||||||
{ new_focus = AddLogicFocus::ScriptContentPreview; }
|
| AddLogicFocus::InputTargetColumn
|
||||||
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
|
| AddLogicFocus::InputDescription => {
|
||||||
|
new_focus = AddLogicFocus::ScriptContentPreview
|
||||||
|
}
|
||||||
|
AddLogicFocus::ScriptContentPreview => {
|
||||||
|
new_focus = AddLogicFocus::SaveButton
|
||||||
|
}
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
||||||
AddLogicFocus::CancelButton => { }
|
AddLogicFocus::CancelButton => {}
|
||||||
_ => handled = false,
|
_ => handled = false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("previous_option") => {
|
Some("previous_option") => {
|
||||||
match current_focus {
|
match current_focus {
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
|
AddLogicFocus::InputLogicName
|
||||||
{ }
|
| AddLogicFocus::InputTargetColumn
|
||||||
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
|
| AddLogicFocus::InputDescription => {}
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
AddLogicFocus::ScriptContentPreview => {
|
||||||
|
new_focus = AddLogicFocus::InputDescription
|
||||||
|
}
|
||||||
|
AddLogicFocus::SaveButton => {
|
||||||
|
new_focus = AddLogicFocus::ScriptContentPreview
|
||||||
|
}
|
||||||
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
||||||
_ => handled = false,
|
_ => handled = false,
|
||||||
}
|
}
|
||||||
@@ -371,35 +446,47 @@ pub fn handle_add_logic_navigation(
|
|||||||
match current_focus {
|
match current_focus {
|
||||||
AddLogicFocus::ScriptContentPreview => {
|
AddLogicFocus::ScriptContentPreview => {
|
||||||
new_focus = AddLogicFocus::InsideScriptContent;
|
new_focus = AddLogicFocus::InsideScriptContent;
|
||||||
*is_edit_mode = false;
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
app_state.ui.focus_outside_canvas = false;
|
||||||
let mode_hint = match add_logic_state.editor_keybinding_mode {
|
let mode_hint = match add_logic_state.editor_keybinding_mode {
|
||||||
EditorKeybindingMode::Vim => "VIM mode - 'i'/'a'/'o' to edit",
|
EditorKeybindingMode::Vim => {
|
||||||
|
"VIM mode - 'i'/'a'/'o' to edit"
|
||||||
|
}
|
||||||
_ => "Enter/Ctrl+E to edit",
|
_ => "Enter/Ctrl+E to edit",
|
||||||
};
|
};
|
||||||
*command_message = format!("Fullscreen script editing. {} or Esc to exit.", mode_hint);
|
*command_message = format!(
|
||||||
|
"Fullscreen script editing. {} or Esc to exit.",
|
||||||
|
mode_hint
|
||||||
|
);
|
||||||
|
handled = true;
|
||||||
}
|
}
|
||||||
AddLogicFocus::SaveButton => {
|
AddLogicFocus::SaveButton => {
|
||||||
*command_message = "Save logic action".to_string();
|
*command_message = "Save logic action".to_string();
|
||||||
|
handled = true;
|
||||||
}
|
}
|
||||||
AddLogicFocus::CancelButton => {
|
AddLogicFocus::CancelButton => {
|
||||||
buffer_state.update_history(AppView::Admin);
|
buffer_state.update_history(AppView::Admin);
|
||||||
*command_message = "Cancelled Add Logic".to_string();
|
*command_message = "Cancelled Add Logic".to_string();
|
||||||
*is_edit_mode = false;
|
handled = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
AddLogicFocus::InputLogicName
|
||||||
*is_edit_mode = !*is_edit_mode;
|
| AddLogicFocus::InputTargetColumn
|
||||||
*command_message = format!("Field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
| AddLogicFocus::InputDescription => {
|
||||||
|
// Focus canvas inputs; let canvas keymap handle editing
|
||||||
|
app_state.ui.focus_outside_canvas = false;
|
||||||
|
handled = false; // forward to canvas
|
||||||
}
|
}
|
||||||
_ => handled = false,
|
_ => handled = false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("toggle_edit_mode") => {
|
Some("toggle_edit_mode") => {
|
||||||
match current_focus {
|
match current_focus {
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
AddLogicFocus::InputLogicName
|
||||||
*is_edit_mode = !*is_edit_mode;
|
| AddLogicFocus::InputTargetColumn
|
||||||
*command_message = format!("Canvas field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
| AddLogicFocus::InputDescription => {
|
||||||
|
app_state.ui.focus_outside_canvas = false;
|
||||||
|
*command_message =
|
||||||
|
"Focus moved to input. Use i/a (Vim) or type to edit.".to_string();
|
||||||
|
handled = true;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
*command_message = "Cannot toggle edit mode here.".to_string();
|
*command_message = "Cannot toggle edit mode here.".to_string();
|
||||||
@@ -411,22 +498,21 @@ pub fn handle_add_logic_navigation(
|
|||||||
|
|
||||||
if handled && current_focus != new_focus {
|
if handled && current_focus != new_focus {
|
||||||
add_logic_state.current_focus = new_focus;
|
add_logic_state.current_focus = new_focus;
|
||||||
let new_is_canvas_input_focus = matches!(new_focus,
|
let new_is_canvas_input_focus = matches!(
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
|
new_focus,
|
||||||
|
AddLogicFocus::InputLogicName
|
||||||
|
| AddLogicFocus::InputTargetColumn
|
||||||
|
| AddLogicFocus::InputDescription
|
||||||
);
|
);
|
||||||
if new_is_canvas_input_focus {
|
if new_is_canvas_input_focus {
|
||||||
*is_edit_mode = false;
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
app_state.ui.focus_outside_canvas = false;
|
||||||
} else {
|
} else {
|
||||||
app_state.ui.focus_outside_canvas = true;
|
app_state.ui.focus_outside_canvas = true;
|
||||||
if matches!(new_focus, AddLogicFocus::ScriptContentPreview) {
|
|
||||||
*is_edit_mode = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handled
|
handled
|
||||||
} else {
|
} else {
|
||||||
return false; // not on AddLogic page
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +522,6 @@ fn replace_autocomplete_text(
|
|||||||
filter_len: usize,
|
filter_len: usize,
|
||||||
replacement: &str,
|
replacement: &str,
|
||||||
) {
|
) {
|
||||||
// use tui_textarea::CursorMove; // Already imported at the top of the module
|
|
||||||
let filter_start_pos = (trigger_pos.0, trigger_pos.1 + 1);
|
let filter_start_pos = (trigger_pos.0, trigger_pos.1 + 1);
|
||||||
editor.move_cursor(CursorMove::Jump(filter_start_pos.0 as u16, filter_start_pos.1 as u16));
|
editor.move_cursor(CursorMove::Jump(filter_start_pos.0 as u16, filter_start_pos.1 as u16));
|
||||||
for _ in 0..filter_len {
|
for _ in 0..filter_len {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/state/pages/add_logic.rs
|
// src/pages/admin_panel/add_logic/state.rs
|
||||||
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
||||||
use crate::components::common::text_editor::{TextEditor, VimState};
|
use crate::components::common::text_editor::{TextEditor, VimState};
|
||||||
use canvas::{DataProvider, AppMode};
|
use canvas::{DataProvider, AppMode};
|
||||||
@@ -54,7 +54,7 @@ pub struct AddLogicState {
|
|||||||
// New fields for same-profile table names and column autocomplete
|
// New fields for same-profile table names and column autocomplete
|
||||||
pub same_profile_table_names: Vec<String>, // Tables from same profile only
|
pub same_profile_table_names: Vec<String>, // Tables from same profile only
|
||||||
pub script_editor_awaiting_column_autocomplete: Option<String>, // Table name waiting for column fetch
|
pub script_editor_awaiting_column_autocomplete: Option<String>, // Table name waiting for column fetch
|
||||||
pub app_mode: AppMode,
|
pub app_mode: canvas::AppMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddLogicState {
|
impl AddLogicState {
|
||||||
@@ -92,7 +92,7 @@ impl AddLogicState {
|
|||||||
|
|
||||||
same_profile_table_names: Vec::new(),
|
same_profile_table_names: Vec::new(),
|
||||||
script_editor_awaiting_column_autocomplete: None,
|
script_editor_awaiting_column_autocomplete: None,
|
||||||
app_mode: AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ impl AddLogicState {
|
|||||||
impl Default for AddLogicState {
|
impl Default for AddLogicState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut state = Self::new(&EditorConfig::default());
|
let mut state = Self::new(&EditorConfig::default());
|
||||||
state.app_mode = AppMode::Edit;
|
state.app_mode = canvas::AppMode::Edit;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/components/admin/add_logic.rs
|
// src/pages/admin_panel/add_logic/ui.rs
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::add_logic::{AddLogicFocus, AddLogicState};
|
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState};
|
||||||
use canvas::{render_canvas, FormEditor};
|
use canvas::{render_canvas, FormEditor};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
@@ -20,7 +20,6 @@ pub fn render_add_logic(
|
|||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
add_logic_state: &mut AddLogicState,
|
add_logic_state: &mut AddLogicState,
|
||||||
is_edit_mode: bool,
|
|
||||||
) {
|
) {
|
||||||
let main_block = Block::default()
|
let main_block = Block::default()
|
||||||
.title(" Add New Logic Script ")
|
.title(" Add New Logic Script ")
|
||||||
@@ -35,7 +34,11 @@ pub fn render_add_logic(
|
|||||||
// Handle full-screen script editing
|
// Handle full-screen script editing
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
||||||
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
||||||
let border_style_color = if is_edit_mode { theme.highlight } else { theme.secondary };
|
let border_style_color = if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) {
|
||||||
|
theme.highlight
|
||||||
|
} else {
|
||||||
|
theme.secondary
|
||||||
|
};
|
||||||
let border_style = Style::default().fg(border_style_color);
|
let border_style = Style::default().fg(border_style_color);
|
||||||
|
|
||||||
editor_ref.set_cursor_line_style(Style::default());
|
editor_ref.set_cursor_line_style(Style::default());
|
||||||
@@ -47,7 +50,7 @@ pub fn render_add_logic(
|
|||||||
format!("Script {}", vim_mode_status)
|
format!("Script {}", vim_mode_status)
|
||||||
}
|
}
|
||||||
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
|
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
|
||||||
if is_edit_mode {
|
if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) {
|
||||||
"Script (Editing)".to_string()
|
"Script (Editing)".to_string()
|
||||||
} else {
|
} else {
|
||||||
"Script".to_string()
|
"Script".to_string()
|
||||||
@@ -162,8 +165,7 @@ pub fn render_add_logic(
|
|||||||
let active_field_rect = render_canvas(f, canvas_area, &editor, theme);
|
let active_field_rect = render_canvas(f, canvas_area, &editor, theme);
|
||||||
|
|
||||||
// --- Render Autocomplete for Target Column ---
|
// --- Render Autocomplete for Target Column ---
|
||||||
// `is_edit_mode` here refers to the general edit mode of the EventHandler
|
if editor.mode() == canvas::AppMode::Edit && editor.current_field() == 1 { // Target Column field
|
||||||
if is_edit_mode && editor.current_field() == 1 { // Target Column field
|
|
||||||
if add_logic_state.in_target_column_suggestion_mode && add_logic_state.show_target_column_suggestions {
|
if add_logic_state.in_target_column_suggestion_mode && add_logic_state.show_target_column_suggestions {
|
||||||
if !add_logic_state.target_column_suggestions.is_empty() {
|
if !add_logic_state.target_column_suggestions.is_empty() {
|
||||||
if let Some(input_rect) = active_field_rect {
|
if let Some(input_rect) = active_field_rect {
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
// src/tui/functions/common/add_table.rs
|
// src/pages/admin_panel/add_table/logic.rs
|
||||||
use crate::state::pages::add_table::{
|
use crate::pages::admin_panel::add_table::state;
|
||||||
AddTableFocus, AddTableState, ColumnDefinition, IndexDefinition,
|
use crate::pages::admin_panel::add_table::state::{AddTableState, AddTableFocus, IndexDefinition, ColumnDefinition};
|
||||||
};
|
|
||||||
use crate::services::GrpcClient;
|
use crate::services::GrpcClient;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use common::proto::komp_ac::table_definition::{
|
use common::proto::komp_ac::table_definition::{
|
||||||
6
client/src/pages/admin_panel/add_table/mod.rs
Normal file
6
client/src/pages/admin_panel/add_table/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// src/pages/admin_panel/add_table/mod.rs
|
||||||
|
|
||||||
|
pub mod ui;
|
||||||
|
pub mod nav;
|
||||||
|
pub mod state;
|
||||||
|
pub mod logic;
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
// src/functions/modes/navigation/add_table_nav.rs
|
// src/pages/admin_panel/add_table/nav.rs
|
||||||
|
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
app::state::AppState,
|
app::state::AppState,
|
||||||
pages::add_table::{AddTableFocus, AddTableState},
|
|
||||||
};
|
};
|
||||||
|
use crate::pages::admin_panel::add_table::state::{AddTableFocus, AddTableState};
|
||||||
use crossterm::event::{KeyEvent};
|
use crossterm::event::{KeyEvent};
|
||||||
use ratatui::widgets::TableState;
|
use ratatui::widgets::TableState;
|
||||||
use crate::tui::functions::common::add_table::{handle_add_column_action, handle_save_table_action};
|
use crate::pages::admin_panel::add_table::logic::{handle_add_column_action, handle_save_table_action};
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use crate::services::GrpcClient;
|
use crate::services::GrpcClient;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
383
client/src/pages/admin_panel/add_table/state.rs
Normal file
383
client/src/pages/admin_panel/add_table/state.rs
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
// src/pages/admin_panel/add_table/state.rs
|
||||||
|
|
||||||
|
use canvas::{DataProvider, AppMode};
|
||||||
|
use ratatui::widgets::TableState;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::movement::{move_focus, MovementAction};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ColumnDefinition {
|
||||||
|
pub name: String,
|
||||||
|
pub data_type: String,
|
||||||
|
pub selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct IndexDefinition {
|
||||||
|
pub name: String,
|
||||||
|
pub selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LinkDefinition {
|
||||||
|
pub linked_table_name: String,
|
||||||
|
pub is_required: bool,
|
||||||
|
pub selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum AddTableFocus {
|
||||||
|
#[default]
|
||||||
|
InputTableName, // Field 0 for CanvasState
|
||||||
|
InputColumnName, // Field 1 for CanvasState
|
||||||
|
InputColumnType, // Field 2 for CanvasState
|
||||||
|
AddColumnButton,
|
||||||
|
// Result Tables
|
||||||
|
ColumnsTable,
|
||||||
|
IndexesTable,
|
||||||
|
LinksTable,
|
||||||
|
// Inside Tables (Scrolling Focus)
|
||||||
|
InsideColumnsTable,
|
||||||
|
InsideIndexesTable,
|
||||||
|
InsideLinksTable,
|
||||||
|
// Buttons
|
||||||
|
SaveButton,
|
||||||
|
DeleteSelectedButton,
|
||||||
|
CancelButton,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AddTableState {
|
||||||
|
pub profile_name: String,
|
||||||
|
pub table_name: String,
|
||||||
|
pub table_name_input: String,
|
||||||
|
pub column_name_input: String,
|
||||||
|
pub column_type_input: String,
|
||||||
|
pub columns: Vec<ColumnDefinition>,
|
||||||
|
pub indexes: Vec<IndexDefinition>,
|
||||||
|
pub links: Vec<LinkDefinition>,
|
||||||
|
pub current_focus: AddTableFocus,
|
||||||
|
pub last_canvas_field: usize,
|
||||||
|
pub column_table_state: TableState,
|
||||||
|
pub index_table_state: TableState,
|
||||||
|
pub link_table_state: TableState,
|
||||||
|
pub table_name_cursor_pos: usize,
|
||||||
|
pub column_name_cursor_pos: usize,
|
||||||
|
pub column_type_cursor_pos: usize,
|
||||||
|
pub has_unsaved_changes: bool,
|
||||||
|
pub app_mode: canvas::AppMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AddTableState {
|
||||||
|
fn default() -> Self {
|
||||||
|
AddTableState {
|
||||||
|
profile_name: "default".to_string(),
|
||||||
|
table_name: String::new(),
|
||||||
|
table_name_input: String::new(),
|
||||||
|
column_name_input: String::new(),
|
||||||
|
column_type_input: String::new(),
|
||||||
|
columns: Vec::new(),
|
||||||
|
indexes: Vec::new(),
|
||||||
|
links: Vec::new(),
|
||||||
|
current_focus: AddTableFocus::InputTableName,
|
||||||
|
last_canvas_field: 2,
|
||||||
|
column_table_state: TableState::default(),
|
||||||
|
index_table_state: TableState::default(),
|
||||||
|
link_table_state: TableState::default(),
|
||||||
|
table_name_cursor_pos: 0,
|
||||||
|
column_name_cursor_pos: 0,
|
||||||
|
column_type_cursor_pos: 0,
|
||||||
|
has_unsaved_changes: false,
|
||||||
|
app_mode: canvas::AppMode::Edit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddTableState {
|
||||||
|
pub const INPUT_FIELD_COUNT: usize = 3;
|
||||||
|
|
||||||
|
/// Helper method to add a column from current inputs
|
||||||
|
pub fn add_column_from_inputs(&mut self) -> Option<String> {
|
||||||
|
if self.column_name_input.trim().is_empty() || self.column_type_input.trim().is_empty() {
|
||||||
|
return Some("Both column name and type are required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate column names
|
||||||
|
if self.columns.iter().any(|col| col.name == self.column_name_input.trim()) {
|
||||||
|
return Some("Column name already exists".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the column
|
||||||
|
self.columns.push(ColumnDefinition {
|
||||||
|
name: self.column_name_input.trim().to_string(),
|
||||||
|
data_type: self.column_type_input.trim().to_string(),
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear inputs and reset focus to column name for next entry
|
||||||
|
self.column_name_input.clear();
|
||||||
|
self.column_type_input.clear();
|
||||||
|
self.column_name_cursor_pos = 0;
|
||||||
|
self.column_type_cursor_pos = 0;
|
||||||
|
self.current_focus = AddTableFocus::InputColumnName;
|
||||||
|
self.last_canvas_field = 1;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
|
||||||
|
Some(format!("Column '{}' added successfully", self.columns.last().unwrap().name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to delete selected items
|
||||||
|
pub fn delete_selected_items(&mut self) -> Option<String> {
|
||||||
|
let mut deleted_items = Vec::new();
|
||||||
|
|
||||||
|
// Remove selected columns
|
||||||
|
let initial_column_count = self.columns.len();
|
||||||
|
self.columns.retain(|col| {
|
||||||
|
if col.selected {
|
||||||
|
deleted_items.push(format!("column '{}'", col.name));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove selected indexes
|
||||||
|
let initial_index_count = self.indexes.len();
|
||||||
|
self.indexes.retain(|idx| {
|
||||||
|
if idx.selected {
|
||||||
|
deleted_items.push(format!("index '{}'", idx.name));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove selected links
|
||||||
|
let initial_link_count = self.links.len();
|
||||||
|
self.links.retain(|link| {
|
||||||
|
if link.selected {
|
||||||
|
deleted_items.push(format!("link to '{}'", link.linked_table_name));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if deleted_items.is_empty() {
|
||||||
|
Some("No items selected for deletion".to_string())
|
||||||
|
} else {
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
Some(format!("Deleted: {}", deleted_items.join(", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataProvider for AddTableState {
|
||||||
|
fn field_count(&self) -> usize {
|
||||||
|
3 // Table name, Column name, Column type
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_name(&self, index: usize) -> &str {
|
||||||
|
match index {
|
||||||
|
0 => "Table name",
|
||||||
|
1 => "Name",
|
||||||
|
2 => "Type",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_value(&self, index: usize) -> &str {
|
||||||
|
match index {
|
||||||
|
0 => &self.table_name_input,
|
||||||
|
1 => &self.column_name_input,
|
||||||
|
2 => &self.column_type_input,
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_field_value(&mut self, index: usize, value: String) {
|
||||||
|
match index {
|
||||||
|
0 => self.table_name_input = value,
|
||||||
|
1 => self.column_name_input = value,
|
||||||
|
2 => self.column_type_input = value,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_suggestions(&self, _field_index: usize) -> bool {
|
||||||
|
false // AddTableState doesn’t use suggestions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl AddTableState {
|
||||||
|
pub fn handle_movement(&mut self, action: MovementAction) -> bool {
|
||||||
|
use AddTableFocus::*;
|
||||||
|
|
||||||
|
// Linear outer focus order
|
||||||
|
const ORDER: [AddTableFocus; 10] = [
|
||||||
|
InputTableName,
|
||||||
|
InputColumnName,
|
||||||
|
InputColumnType,
|
||||||
|
AddColumnButton,
|
||||||
|
ColumnsTable,
|
||||||
|
IndexesTable,
|
||||||
|
LinksTable,
|
||||||
|
SaveButton,
|
||||||
|
DeleteSelectedButton,
|
||||||
|
CancelButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Enter "inside" on Select from outer panes
|
||||||
|
match (self.current_focus, action) {
|
||||||
|
(ColumnsTable, MovementAction::Select) => {
|
||||||
|
if !self.columns.is_empty() && self.column_table_state.selected().is_none() {
|
||||||
|
self.column_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.current_focus = InsideColumnsTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(IndexesTable, MovementAction::Select) => {
|
||||||
|
if !self.indexes.is_empty() && self.index_table_state.selected().is_none() {
|
||||||
|
self.index_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.current_focus = InsideIndexesTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(LinksTable, MovementAction::Select) => {
|
||||||
|
if !self.links.is_empty() && self.link_table_state.selected().is_none() {
|
||||||
|
self.link_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.current_focus = InsideLinksTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle "inside" states: Up/Down/Select/Esc; block outer movement keys
|
||||||
|
match self.current_focus {
|
||||||
|
InsideColumnsTable => {
|
||||||
|
match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = self.column_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
self.column_table_state.select(Some(next));
|
||||||
|
} else if !self.columns.is_empty() {
|
||||||
|
self.column_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = self.column_table_state.selected() {
|
||||||
|
let last = self.columns.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
self.column_table_state.select(Some(next));
|
||||||
|
} else if !self.columns.is_empty() {
|
||||||
|
self.column_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = self.column_table_state.selected() {
|
||||||
|
if let Some(col) = self.columns.get_mut(i) {
|
||||||
|
col.selected = !col.selected;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.column_table_state.select(None);
|
||||||
|
self.current_focus = ColumnsTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InsideIndexesTable => {
|
||||||
|
match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = self.index_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
self.index_table_state.select(Some(next));
|
||||||
|
} else if !self.indexes.is_empty() {
|
||||||
|
self.index_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = self.index_table_state.selected() {
|
||||||
|
let last = self.indexes.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
self.index_table_state.select(Some(next));
|
||||||
|
} else if !self.indexes.is_empty() {
|
||||||
|
self.index_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = self.index_table_state.selected() {
|
||||||
|
if let Some(ix) = self.indexes.get_mut(i) {
|
||||||
|
ix.selected = !ix.selected;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.index_table_state.select(None);
|
||||||
|
self.current_focus = IndexesTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InsideLinksTable => {
|
||||||
|
match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = self.link_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
self.link_table_state.select(Some(next));
|
||||||
|
} else if !self.links.is_empty() {
|
||||||
|
self.link_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = self.link_table_state.selected() {
|
||||||
|
let last = self.links.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
self.link_table_state.select(Some(next));
|
||||||
|
} else if !self.links.is_empty() {
|
||||||
|
self.link_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = self.link_table_state.selected() {
|
||||||
|
if let Some(link) = self.links.get_mut(i) {
|
||||||
|
link.selected = !link.selected;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.link_table_state.select(None);
|
||||||
|
self.current_focus = LinksTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: outer navigation via helper
|
||||||
|
move_focus(&ORDER, &mut self.current_focus, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/components/admin/add_table.rs
|
// src/pages/admin_panel/add_table/ui.rs
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::add_table::{AddTableFocus, AddTableState};
|
use crate::pages::admin_panel::add_table::state::{AddTableFocus, AddTableState};
|
||||||
use canvas::{render_canvas, FormEditor};
|
use canvas::{render_canvas, FormEditor};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
@@ -20,7 +20,6 @@ pub fn render_add_table(
|
|||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
add_table_state: &mut AddTableState,
|
add_table_state: &mut AddTableState,
|
||||||
is_edit_mode: bool, // Determines if canvas inputs are in edit mode
|
|
||||||
) {
|
) {
|
||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
// Threshold width to switch between wide and narrow layouts
|
// Threshold width to switch between wide and narrow layouts
|
||||||
4
client/src/pages/admin_panel/mod.rs
Normal file
4
client/src/pages/admin_panel/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/pages/admin_panel/mod.rs
|
||||||
|
|
||||||
|
pub mod add_table;
|
||||||
|
pub mod add_logic;
|
||||||
62
client/src/pages/forms/event.rs
Normal file
62
client/src/pages/forms/event.rs
Normal 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))
|
||||||
|
}
|
||||||
39
client/src/pages/forms/loader.rs
Normal file
39
client/src/pages/forms/loader.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -15,9 +15,10 @@ pub enum SaveOutcome {
|
|||||||
|
|
||||||
pub async fn save(
|
pub async fn save(
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
|
path: &str,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
) -> Result<SaveOutcome> {
|
) -> Result<SaveOutcome> {
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
if !fs.has_unsaved_changes {
|
if !fs.has_unsaved_changes {
|
||||||
return Ok(SaveOutcome::NoChange);
|
return Ok(SaveOutcome::NoChange);
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ pub async fn save(
|
|||||||
.context("Failed to post new table data")?;
|
.context("Failed to post new table data")?;
|
||||||
|
|
||||||
if response.success {
|
if response.success {
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
fs.id = response.inserted_id;
|
fs.id = response.inserted_id;
|
||||||
fs.total_count += 1;
|
fs.total_count += 1;
|
||||||
fs.current_position = fs.total_count;
|
fs.current_position = fs.total_count;
|
||||||
@@ -84,7 +85,7 @@ pub async fn save(
|
|||||||
.context("Failed to put (update) table data")?;
|
.context("Failed to put (update) table data")?;
|
||||||
|
|
||||||
if response.success {
|
if response.success {
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
fs.has_unsaved_changes = false;
|
fs.has_unsaved_changes = false;
|
||||||
}
|
}
|
||||||
SaveOutcome::UpdatedExisting
|
SaveOutcome::UpdatedExisting
|
||||||
@@ -101,9 +102,10 @@ pub async fn save(
|
|||||||
|
|
||||||
pub async fn revert(
|
pub async fn revert(
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
|
path: &str,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
if fs.id == 0
|
if fs.id == 0
|
||||||
|| (fs.total_count > 0 && fs.current_position > fs.total_count)
|
|| (fs.total_count > 0 && fs.current_position > fs.total_count)
|
||||||
|| (fs.total_count == 0 && fs.current_position == 1)
|
|| (fs.total_count == 0 && fs.current_position == 1)
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ impl FormState {
|
|||||||
selected_suggestion_index: None,
|
selected_suggestion_index: None,
|
||||||
autocomplete_loading: false,
|
autocomplete_loading: false,
|
||||||
link_display_map: HashMap::new(),
|
link_display_map: HashMap::new(),
|
||||||
app_mode: AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
// src/pages/forms/ui.rs
|
// src/pages/forms/ui.rs
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||||
style::Style,
|
style::Style,
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::pages::forms::FormState;
|
|
||||||
use canvas::{
|
use canvas::{
|
||||||
render_canvas, render_suggestions_dropdown, DefaultCanvasTheme,
|
render_canvas, render_suggestions_dropdown, DefaultCanvasTheme, FormEditor,
|
||||||
};
|
};
|
||||||
|
use crate::pages::forms::FormState;
|
||||||
|
|
||||||
pub fn render_form_page(
|
pub fn render_form_page(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
app_state: &AppState,
|
editor: &FormEditor<FormState>,
|
||||||
form_state: &FormState, // not needed directly anymore, editor holds it
|
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
@@ -61,10 +59,7 @@ pub fn render_form_page(
|
|||||||
f.render_widget(count_para, main_layout[0]);
|
f.render_widget(count_para, main_layout[0]);
|
||||||
|
|
||||||
// --- FORM RENDERING (Using persistent FormEditor) ---
|
// --- FORM RENDERING (Using persistent FormEditor) ---
|
||||||
if let Some(editor) = &app_state.form_editor {
|
|
||||||
let active_field_rect = render_canvas(f, main_layout[1], editor, theme);
|
let active_field_rect = render_canvas(f, main_layout[1], editor, theme);
|
||||||
|
|
||||||
// --- SUGGESTIONS DROPDOWN ---
|
|
||||||
if let Some(active_rect) = active_field_rect {
|
if let Some(active_rect) = active_field_rect {
|
||||||
render_suggestions_dropdown(
|
render_suggestions_dropdown(
|
||||||
f,
|
f,
|
||||||
@@ -74,5 +69,4 @@ pub fn render_form_page(
|
|||||||
editor,
|
editor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/tui/functions/intro.rs
|
// src/pages/intro/logic.rs
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::buffer::state::{AppView, BufferState};
|
use crate::buffer::state::{AppView, BufferState};
|
||||||
|
|
||||||
@@ -12,19 +12,65 @@ pub fn handle_intro_selection(
|
|||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
index: usize,
|
index: usize,
|
||||||
) {
|
) {
|
||||||
let target_view = match index {
|
match index {
|
||||||
0 => AppView::Form,
|
// Continue: go to the most recent existing Form tab, or open a sensible default
|
||||||
1 => AppView::Admin,
|
0 => {
|
||||||
2 => AppView::Login,
|
// 1) Try to switch to an already open Form buffer (most recent)
|
||||||
3 => AppView::Register,
|
if let Some(existing_path) = buffer_state
|
||||||
_ => return,
|
.history
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find_map(|view| {
|
||||||
|
if let AppView::Form(p) = view {
|
||||||
|
Some(p.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
buffer_state.update_history(AppView::Form(existing_path));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Otherwise pick a fallback path
|
||||||
|
let fallback_path = if let (Some(profile), Some(table)) = (
|
||||||
|
app_state.current_view_profile_name.clone(),
|
||||||
|
app_state.current_view_table_name.clone(),
|
||||||
|
) {
|
||||||
|
Some(format!("{}/{}", profile, table))
|
||||||
|
} else if let Some(any_key) = app_state.form_editor.keys().next().cloned() {
|
||||||
|
// Use any existing editor key if available
|
||||||
|
Some(any_key)
|
||||||
|
} else {
|
||||||
|
// Otherwise pick the first available table from the profile tree
|
||||||
|
let mut found: Option<String> = None;
|
||||||
|
for prof in &app_state.profile_tree.profiles {
|
||||||
|
if let Some(tbl) = prof.tables.first() {
|
||||||
|
found = Some(format!("{}/{}", prof.name, tbl.name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found
|
||||||
};
|
};
|
||||||
|
|
||||||
buffer_state.update_history(target_view);
|
if let Some(path) = fallback_path {
|
||||||
|
buffer_state.update_history(AppView::Form(path));
|
||||||
|
} else {
|
||||||
|
// No sensible default; stay on Intro
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
buffer_state.update_history(AppView::Admin);
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
buffer_state.update_history(AppView::Login);
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
buffer_state.update_history(AppView::Register);
|
||||||
// Register view requires focus reset
|
// Register view requires focus reset
|
||||||
if index == 3 {
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// src/state/pages/intro.rs
|
// src/state/pages/intro.rs
|
||||||
|
use crate::movement::MovementAction;
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct IntroState {
|
pub struct IntroState {
|
||||||
@@ -23,3 +24,25 @@ impl IntroState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntroState {
|
||||||
|
pub fn handle_movement(&mut self, action: MovementAction) -> bool {
|
||||||
|
match action {
|
||||||
|
MovementAction::Next | MovementAction::Right | MovementAction::Down => {
|
||||||
|
self.next_option();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Previous | MovementAction::Left | MovementAction::Up => {
|
||||||
|
self.previous_option();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
// Actual selection handled in event loop (UiContext::Intro)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
// Nothing special for Intro, but could be used to quit
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
73
client/src/pages/login/event.rs
Normal file
73
client/src/pages/login/event.rs
Normal 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()))
|
||||||
|
}
|
||||||
@@ -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,12 @@ 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 crate::state::pages::auth::UserRole;
|
||||||
|
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 +25,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,28 +41,28 @@ 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(UserRole::from_str(&response.role));
|
||||||
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\
|
||||||
@@ -80,9 +79,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 +94,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 +105,43 @@ 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: &mut 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();
|
login_state.sync_from_editor();
|
||||||
let password = login_state.password.clone();
|
let username = login_state.username().to_string();
|
||||||
|
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 +151,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,28 +175,24 @@ 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) => {
|
||||||
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(UserRole::from_str(&response.role));
|
||||||
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 +206,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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -24,7 +26,7 @@ impl Default for LoginState {
|
|||||||
current_cursor_pos: 0,
|
current_cursor_pos: 0,
|
||||||
has_unsaved_changes: false,
|
has_unsaved_changes: false,
|
||||||
login_request_pending: false,
|
login_request_pending: false,
|
||||||
app_mode: AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +34,7 @@ impl Default for LoginState {
|
|||||||
impl LoginState {
|
impl LoginState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
app_mode: AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,3 +121,128 @@ 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>,
|
||||||
|
pub focus_outside_canvas: bool,
|
||||||
|
pub focused_button_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
/// Sync the editor's data provider back into our state
|
||||||
|
pub fn sync_from_editor(&mut self) {
|
||||||
|
// FormEditor holds the authoritative data
|
||||||
|
let dp = self.editor.data_provider();
|
||||||
|
self.state = dp.clone(); // LoginState implements Clone
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
focus_outside_canvas: false,
|
||||||
|
focused_button_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 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,18 +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,
|
||||||
is_edit_mode: bool,
|
|
||||||
) {
|
) {
|
||||||
|
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)
|
||||||
@@ -53,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,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,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
|
||||||
@@ -135,14 +133,14 @@ pub fn render_login(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- SUGGESTIONS DROPDOWN (if active) ---
|
// --- SUGGESTIONS DROPDOWN (if active) ---
|
||||||
if app_state.current_mode == crate::modes::handlers::mode_manager::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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ pub mod intro;
|
|||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
pub mod forms;
|
pub mod forms;
|
||||||
|
pub mod admin;
|
||||||
|
pub mod admin_panel;
|
||||||
|
|||||||
76
client/src/pages/register/event.rs
Normal file
76
client/src/pages/register/event.rs
Normal 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()))
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
// src/pages/register/logic.rs
|
// src/pages/register/logic.rs
|
||||||
|
|
||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::state::{
|
use crate::state::app::state::AppState;
|
||||||
app::state::AppState,
|
|
||||||
};
|
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use crate::buffer::state::{AppView, BufferState};
|
use crate::buffer::state::{AppView, BufferState};
|
||||||
use common::proto::komp_ac::auth::AuthResponse;
|
use common::proto::komp_ac::auth::AuthResponse;
|
||||||
use crate::pages::register::RegisterState;
|
use crate::pages::register::RegisterFormState;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
@@ -22,24 +20,26 @@ pub enum RegisterResult {
|
|||||||
|
|
||||||
/// Clears the registration form fields.
|
/// Clears the registration form fields.
|
||||||
pub async fn revert(
|
pub async fn revert(
|
||||||
register_state: &mut RegisterState,
|
register_state: &mut RegisterFormState,
|
||||||
_app_state: &mut AppState, // Keep signature consistent if needed elsewhere
|
app_state: &mut AppState,
|
||||||
) -> String {
|
) -> String {
|
||||||
register_state.username.clear();
|
register_state.username_mut().clear();
|
||||||
register_state.email.clear();
|
register_state.email_mut().clear();
|
||||||
register_state.password.clear();
|
register_state.password_mut().clear();
|
||||||
register_state.password_confirmation.clear();
|
register_state.password_confirmation_mut().clear();
|
||||||
register_state.role.clear();
|
register_state.role_mut().clear();
|
||||||
register_state.error_message = None;
|
register_state.set_error_message(None);
|
||||||
register_state.set_has_unsaved_changes(false);
|
register_state.set_has_unsaved_changes(false);
|
||||||
register_state.current_field = 0; // Reset focus to first field
|
register_state.set_current_field(0); // Reset focus to first field
|
||||||
register_state.current_cursor_pos = 0;
|
register_state.set_current_cursor_pos(0);
|
||||||
|
|
||||||
|
app_state.hide_dialog();
|
||||||
"Registration form cleared".to_string()
|
"Registration form cleared".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the form and returns to the intro screen.
|
/// Clears the form and returns to the intro screen.
|
||||||
pub async fn back_to_login(
|
pub async fn back_to_login(
|
||||||
register_state: &mut RegisterState,
|
register_state: &mut RegisterFormState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
) -> String {
|
) -> String {
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -62,24 +64,34 @@ pub async fn back_to_login(
|
|||||||
|
|
||||||
/// Validates input, shows loading, and spawns the registration task.
|
/// Validates input, shows loading, and spawns the registration task.
|
||||||
pub fn initiate_registration(
|
pub fn initiate_registration(
|
||||||
register_state: &RegisterState,
|
register_state: &mut RegisterFormState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
mut auth_client: AuthClient,
|
mut auth_client: AuthClient,
|
||||||
sender: mpsc::Sender<RegisterResult>,
|
sender: mpsc::Sender<RegisterResult>,
|
||||||
) -> String {
|
) -> String {
|
||||||
// Clone necessary data
|
register_state.sync_from_editor();
|
||||||
let username = register_state.username.clone();
|
let username = register_state.username().to_string();
|
||||||
let email = register_state.email.clone();
|
let email = register_state.email().to_string();
|
||||||
let password = register_state.password.clone();
|
let password = register_state.password().to_string();
|
||||||
let password_confirmation = register_state.password_confirmation.clone();
|
let password_confirmation = register_state.password_confirmation().to_string();
|
||||||
let role = register_state.role.clone();
|
let role = register_state.role().to_string();
|
||||||
|
|
||||||
// 1. Client-side validation
|
// 1. Client-side validation
|
||||||
if username.trim().is_empty() {
|
if username.trim().is_empty() {
|
||||||
app_state.show_dialog("Registration Failed", "Username cannot be empty.", vec!["OK".to_string()], DialogPurpose::RegisterFailed);
|
app_state.show_dialog(
|
||||||
|
"Registration Failed",
|
||||||
|
"Username cannot be empty.",
|
||||||
|
vec!["OK".to_string()],
|
||||||
|
DialogPurpose::RegisterFailed,
|
||||||
|
);
|
||||||
"Username cannot be empty.".to_string()
|
"Username cannot be empty.".to_string()
|
||||||
} else if !password.is_empty() && password != password_confirmation {
|
} else if !password.is_empty() && password != password_confirmation {
|
||||||
app_state.show_dialog("Registration Failed", "Passwords do not match.", vec!["OK".to_string()], DialogPurpose::RegisterFailed);
|
app_state.show_dialog(
|
||||||
|
"Registration Failed",
|
||||||
|
"Passwords do not match.",
|
||||||
|
vec!["OK".to_string()],
|
||||||
|
DialogPurpose::RegisterFailed,
|
||||||
|
);
|
||||||
"Passwords do not match.".to_string()
|
"Passwords do not match.".to_string()
|
||||||
} else {
|
} else {
|
||||||
// 2. Show Loading Dialog
|
// 2. Show Loading Dialog
|
||||||
@@ -88,14 +100,19 @@ pub fn initiate_registration(
|
|||||||
// 3. Spawn the registration task
|
// 3. Spawn the registration task
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let password_opt = if password.is_empty() { None } else { Some(password) };
|
let password_opt = if password.is_empty() { None } else { Some(password) };
|
||||||
let password_conf_opt = if password_confirmation.is_empty() { None } else { Some(password_confirmation) };
|
let password_conf_opt =
|
||||||
|
if password_confirmation.is_empty() { None } else { Some(password_confirmation) };
|
||||||
let role_opt = if role.is_empty() { None } else { Some(role) };
|
let role_opt = if role.is_empty() { None } else { Some(role) };
|
||||||
let register_outcome = match auth_client.register(username.clone(), email, password_opt, password_conf_opt, role_opt).await
|
|
||||||
|
let register_outcome = match auth_client
|
||||||
|
.register(username.clone(), email, password_opt, password_conf_opt, role_opt)
|
||||||
|
.await
|
||||||
.with_context(|| format!("Spawned register task failed for username: {}", username))
|
.with_context(|| format!("Spawned register task failed for username: {}", username))
|
||||||
{
|
{
|
||||||
Ok(response) => RegisterResult::Success(response),
|
Ok(response) => RegisterResult::Success(response),
|
||||||
Err(e) => RegisterResult::Failure(format!("{}", e)),
|
Err(e) => RegisterResult::Failure(format!("{}", e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send result back to the main UI thread
|
// Send result back to the main UI thread
|
||||||
if let Err(e) = sender.send(register_outcome).await {
|
if let Err(e) = sender.send(register_outcome).await {
|
||||||
error!("Failed to send registration result: {}", e);
|
error!("Failed to send registration result: {}", e);
|
||||||
@@ -112,7 +129,7 @@ pub fn initiate_registration(
|
|||||||
pub fn handle_registration_result(
|
pub fn handle_registration_result(
|
||||||
result: RegisterResult,
|
result: RegisterResult,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
register_state: &mut RegisterState,
|
register_state: &mut RegisterFormState,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match result {
|
match result {
|
||||||
RegisterResult::Success(response) => {
|
RegisterResult::Success(response) => {
|
||||||
@@ -133,7 +150,7 @@ pub fn handle_registration_result(
|
|||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::RegisterFailed,
|
DialogPurpose::RegisterFailed,
|
||||||
);
|
);
|
||||||
register_state.error_message = Some(err_msg.clone());
|
register_state.set_error_message(Some(err_msg.clone()));
|
||||||
error!(error = %err_msg, "Registration failed/connection error");
|
error!(error = %err_msg, "Registration failed/connection error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@
|
|||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod logic;
|
pub mod logic;
|
||||||
|
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::*;
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
// src/pages/register/state.rs
|
// src/pages/register/state.rs
|
||||||
|
|
||||||
use canvas::{DataProvider, AppMode};
|
use canvas::{DataProvider, AppMode, FormEditor};
|
||||||
use lazy_static::lazy_static;
|
use std::fmt;
|
||||||
|
|
||||||
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)]
|
||||||
@@ -24,9 +19,7 @@ pub struct RegisterState {
|
|||||||
pub current_field: usize,
|
pub current_field: usize,
|
||||||
pub current_cursor_pos: usize,
|
pub current_cursor_pos: usize,
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
pub app_mode: AppMode,
|
pub app_mode: canvas::AppMode,
|
||||||
pub role_suggestions: Vec<String>,
|
|
||||||
pub role_suggestions_active: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RegisterState {
|
impl Default for RegisterState {
|
||||||
@@ -41,9 +34,7 @@ impl Default for RegisterState {
|
|||||||
current_field: 0,
|
current_field: 0,
|
||||||
current_cursor_pos: 0,
|
current_cursor_pos: 0,
|
||||||
has_unsaved_changes: false,
|
has_unsaved_changes: false,
|
||||||
app_mode: AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
role_suggestions: AVAILABLE_ROLES.clone(),
|
|
||||||
role_suggestions_active: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,9 +42,7 @@ impl Default for RegisterState {
|
|||||||
impl RegisterState {
|
impl RegisterState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
app_mode: AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
role_suggestions: AVAILABLE_ROLES.clone(),
|
|
||||||
role_suggestions_active: false,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,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
|
||||||
}
|
}
|
||||||
@@ -140,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 {
|
||||||
@@ -182,3 +141,230 @@ impl DataProvider for RegisterState {
|
|||||||
field_index == 4 // only Role field supports suggestions
|
field_index == 4 // only Role field supports suggestions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper that owns both the raw register state and its editor
|
||||||
|
pub struct RegisterFormState {
|
||||||
|
pub state: RegisterState,
|
||||||
|
pub editor: FormEditor<RegisterState>,
|
||||||
|
pub focus_outside_canvas: bool,
|
||||||
|
pub focused_button_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RegisterFormState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// manual Debug because FormEditor doesn’t implement Debug
|
||||||
|
impl fmt::Debug for RegisterFormState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("RegisterFormState")
|
||||||
|
.field("state", &self.state)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegisterFormState {
|
||||||
|
/// Sync the editor's data provider back into our state
|
||||||
|
pub fn sync_from_editor(&mut self) {
|
||||||
|
// The FormEditor holds the authoritative data
|
||||||
|
let dp = self.editor.data_provider();
|
||||||
|
self.state = dp.clone(); // because RegisterState: Clone
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let state = RegisterState::default();
|
||||||
|
let editor = FormEditor::new(state.clone());
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
editor,
|
||||||
|
focus_outside_canvas: false,
|
||||||
|
focused_button_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Delegates to RegisterState ===
|
||||||
|
pub fn username(&self) -> &str {
|
||||||
|
&self.state.username
|
||||||
|
}
|
||||||
|
pub fn username_mut(&mut self) -> &mut String {
|
||||||
|
&mut self.state.username
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn email(&self) -> &str {
|
||||||
|
&self.state.email
|
||||||
|
}
|
||||||
|
pub fn email_mut(&mut self) -> &mut String {
|
||||||
|
&mut self.state.email
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> &str {
|
||||||
|
&self.state.password
|
||||||
|
}
|
||||||
|
pub fn password_mut(&mut self) -> &mut String {
|
||||||
|
&mut self.state.password
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password_confirmation(&self) -> &str {
|
||||||
|
&self.state.password_confirmation
|
||||||
|
}
|
||||||
|
pub fn password_confirmation_mut(&mut self) -> &mut String {
|
||||||
|
&mut self.state.password_confirmation
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn role(&self) -> &str {
|
||||||
|
&self.state.role
|
||||||
|
}
|
||||||
|
pub fn role_mut(&mut self) -> &mut String {
|
||||||
|
&mut self.state.role
|
||||||
|
}
|
||||||
|
|
||||||
|
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.email.clear();
|
||||||
|
self.state.password.clear();
|
||||||
|
self.state.password_confirmation.clear();
|
||||||
|
self.state.role.clear();
|
||||||
|
self.state.error_message = None;
|
||||||
|
self.state.has_unsaved_changes = false;
|
||||||
|
self.state.current_field = 0;
|
||||||
|
self.state.current_cursor_pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Delegates to 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(self.state.current_field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Delegates to FormEditor ===
|
||||||
|
pub fn mode(&self) -> canvas::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 {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::colors::themes::Theme,
|
config::colors::themes::Theme,
|
||||||
state::app::state::AppState,
|
state::app::state::AppState,
|
||||||
modes::handlers::mode_manager::AppMode,
|
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
||||||
@@ -12,17 +11,23 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::dialog;
|
use crate::dialog;
|
||||||
use crate::pages::register::RegisterState;
|
use crate::pages::register::RegisterFormState;
|
||||||
use canvas::{FormEditor, render_canvas, render_suggestions_dropdown, DefaultCanvasTheme};
|
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(
|
pub fn render_register(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
state: &RegisterState,
|
register_page: &RegisterFormState,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
is_edit_mode: bool,
|
|
||||||
) {
|
) {
|
||||||
|
let state = ®ister_page.state;
|
||||||
|
let editor = ®ister_page.editor;
|
||||||
|
|
||||||
|
// Outer block
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Plain)
|
.border_type(BorderType::Plain)
|
||||||
@@ -47,15 +52,9 @@ pub fn render_register(
|
|||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// Wrap RegisterState in FormEditor
|
// Render the form canvas
|
||||||
let editor = FormEditor::new(state.clone());
|
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")
|
||||||
@@ -81,11 +80,9 @@ pub fn render_register(
|
|||||||
|
|
||||||
// Register Button
|
// Register Button
|
||||||
let register_button_index = 0;
|
let register_button_index = 0;
|
||||||
let register_active = if app_state.ui.focus_outside_canvas {
|
let register_active =
|
||||||
app_state.focused_button_index == register_button_index
|
register_page.focus_outside_canvas
|
||||||
} else {
|
&& register_page.focused_button_index == register_button_index;
|
||||||
false
|
|
||||||
};
|
|
||||||
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 {
|
||||||
@@ -108,11 +105,9 @@ pub fn render_register(
|
|||||||
|
|
||||||
// Return Button
|
// Return Button
|
||||||
let return_button_index = 1;
|
let return_button_index = 1;
|
||||||
let return_active = if app_state.ui.focus_outside_canvas {
|
let return_active =
|
||||||
app_state.focused_button_index == return_button_index
|
register_page.focus_outside_canvas
|
||||||
} else {
|
&& register_page.focused_button_index == return_button_index;
|
||||||
false
|
|
||||||
};
|
|
||||||
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 {
|
||||||
@@ -133,19 +128,6 @@ pub fn render_register(
|
|||||||
button_chunks[1],
|
button_chunks[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- AUTOCOMPLETE DROPDOWN (Using new canvas suggestions) ---
|
|
||||||
if app_state.current_mode == AppMode::Edit {
|
|
||||||
if let Some(input_rect) = input_rect {
|
|
||||||
render_suggestions_dropdown(
|
|
||||||
f,
|
|
||||||
f.area(), // Frame area
|
|
||||||
input_rect, // Current input field 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(
|
||||||
@@ -159,4 +141,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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
// src/pages/routing/router.rs
|
// src/pages/routing/router.rs
|
||||||
use crate::state::pages::{
|
use crate::state::pages::auth::AuthState;
|
||||||
admin::AdminState,
|
use crate::pages::admin_panel::add_logic::state::AddLogicState;
|
||||||
auth::AuthState,
|
use crate::pages::admin_panel::add_table::state::AddTableState;
|
||||||
add_logic::AddLogicState,
|
use crate::pages::admin::AdminState;
|
||||||
add_table::AddTableState,
|
|
||||||
};
|
|
||||||
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::RegisterFormState;
|
||||||
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(RegisterFormState),
|
||||||
Admin(AdminState),
|
Admin(AdminState),
|
||||||
AddLogic(AddLogicState),
|
AddLogic(AddLogicState),
|
||||||
AddTable(AddTableState),
|
AddTable(AddTableState),
|
||||||
Form(FormState),
|
Form(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Router {
|
pub struct Router {
|
||||||
|
|||||||
@@ -34,10 +34,17 @@ pub async fn handle_search_palette_event(
|
|||||||
// Step 2: Process outside the borrow
|
// Step 2: Process outside the borrow
|
||||||
if let Some((id, content_json)) = maybe_data {
|
if let Some((id, content_json)) = maybe_data {
|
||||||
if let Ok(data) = serde_json::from_str::<HashMap<String, String>>(&content_json) {
|
if let Ok(data) = serde_json::from_str::<HashMap<String, String>>(&content_json) {
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
// Use current view path to access the active form
|
||||||
|
if let (Some(profile), Some(table)) = (
|
||||||
|
app_state.current_view_profile_name.clone(),
|
||||||
|
app_state.current_view_table_name.clone(),
|
||||||
|
) {
|
||||||
|
let path = format!("{}/{}", profile, table);
|
||||||
|
if let Some(fs) = app_state.form_state_for_path(&path) {
|
||||||
let detached_pos = fs.total_count + 2;
|
let detached_pos = fs.total_count + 2;
|
||||||
fs.update_from_response(&data, detached_pos);
|
fs.update_from_response(&data, detached_pos);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
should_close = true;
|
should_close = true;
|
||||||
outcome_message = Some(format!("Loaded record ID {}", id));
|
outcome_message = Some(format!("Loaded record ID {}", id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::add_logic::AddLogicState;
|
use crate::pages::admin_panel::add_logic::state::AddLogicState;
|
||||||
use crate::pages::forms::logic::SaveOutcome;
|
use crate::pages::forms::logic::SaveOutcome;
|
||||||
use crate::utils::columns::filter_user_columns;
|
use crate::utils::columns::filter_user_columns;
|
||||||
use crate::pages::forms::{FieldDefinition, FormState};
|
use crate::pages::forms::{FieldDefinition, FormState};
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ pub struct AppState {
|
|||||||
// UI preferences
|
// UI preferences
|
||||||
pub ui: UiState,
|
pub ui: UiState,
|
||||||
|
|
||||||
pub form_editor: Option<FormEditor<FormState>>,
|
pub form_editor: HashMap<String, FormEditor<FormState>>, // key = "profile/table"
|
||||||
|
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
pub debug_state: Option<DebugState>,
|
pub debug_state: Option<DebugState>,
|
||||||
@@ -81,7 +81,7 @@ impl AppState {
|
|||||||
pending_table_structure_fetch: None,
|
pending_table_structure_fetch: None,
|
||||||
search_state: None,
|
search_state: None,
|
||||||
ui: UiState::default(),
|
ui: UiState::default(),
|
||||||
form_editor: None,
|
form_editor: HashMap::new(),
|
||||||
|
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
debug_state: None,
|
debug_state: None,
|
||||||
@@ -99,27 +99,58 @@ impl AppState {
|
|||||||
self.current_view_table_name = Some(table_name);
|
self.current_view_table_name = Some(table_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_form_editor(&mut self, form_state: FormState, config: &Config) {
|
/// Returns true if the current view's editor is in Edit mode.
|
||||||
let mut editor = FormEditor::new(form_state);
|
/// Uses current_view_profile_name/current_view_table_name to build the path.
|
||||||
editor.set_keymap(config.build_canvas_keymap()); // inject keymap
|
pub fn is_canvas_edit_mode(&self) -> bool {
|
||||||
self.form_editor = Some(editor);
|
if let (Some(profile), Some(table)) =
|
||||||
|
(self.current_view_profile_name.as_ref(), self.current_view_table_name.as_ref())
|
||||||
|
{
|
||||||
|
let path = format!("{}/{}", profile, table);
|
||||||
|
if let Some(editor) = self.form_editor.get(&path) {
|
||||||
|
return matches!(editor.mode(), canvas::AppMode::Edit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the current form state and wrap it in a FormEditor with keymap
|
pub fn is_canvas_edit_mode_at(&self, path: &str) -> bool {
|
||||||
pub fn set_form_state(&mut self, form_state: FormState, config: &Config) {
|
self.form_editor
|
||||||
let mut editor = FormEditor::new(form_state);
|
.get(path)
|
||||||
|
.map(|e| matches!(e.mode(), canvas::AppMode::Edit))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutable editor accessor
|
||||||
|
pub fn editor_for_path(&mut self, path: &str) -> Option<&mut FormEditor<FormState>> {
|
||||||
|
self.form_editor.get_mut(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutable FormState accessor
|
||||||
|
pub fn form_state_for_path(&mut self, path: &str) -> Option<&mut FormState> {
|
||||||
|
self.form_editor
|
||||||
|
.get_mut(path)
|
||||||
|
.map(|e| e.data_provider_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immutable editor accessor
|
||||||
|
pub fn editor_for_path_ref(&self, path: &str) -> Option<&FormEditor<FormState>> {
|
||||||
|
self.form_editor.get(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immutable FormState accessor
|
||||||
|
pub fn form_state_for_path_ref(&self, path: &str) -> Option<&FormState> {
|
||||||
|
self.form_editor.get(path).map(|e| e.data_provider())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_form_editor<F>(&mut self, path: &str, config: &Config, loader: F)
|
||||||
|
where
|
||||||
|
F: FnOnce() -> FormState,
|
||||||
|
{
|
||||||
|
if !self.form_editor.contains_key(path) {
|
||||||
|
let mut editor = FormEditor::new(loader());
|
||||||
editor.set_keymap(config.build_canvas_keymap());
|
editor.set_keymap(config.build_canvas_keymap());
|
||||||
self.form_editor = Some(editor);
|
self.form_editor.insert(path.to_string(), editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Immutable access to the underlying FormState
|
|
||||||
pub fn form_state(&self) -> Option<&FormState> {
|
|
||||||
self.form_editor.as_ref().map(|e| e.data_provider())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutable access to the underlying FormState
|
|
||||||
pub fn form_state_mut(&mut self) -> Option<&mut FormState> {
|
|
||||||
self.form_editor.as_mut().map(|e| e.data_provider_mut())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// src/state/pages.rs
|
// src/state/pages.rs
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod admin;
|
|
||||||
pub mod add_table;
|
|
||||||
pub mod add_logic;
|
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
// src/state/pages/add_table.rs
|
|
||||||
|
|
||||||
use canvas::{DataProvider, AppMode};
|
|
||||||
use ratatui::widgets::TableState;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ColumnDefinition {
|
|
||||||
pub name: String,
|
|
||||||
pub data_type: String,
|
|
||||||
pub selected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct IndexDefinition {
|
|
||||||
pub name: String,
|
|
||||||
pub selected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct LinkDefinition {
|
|
||||||
pub linked_table_name: String,
|
|
||||||
pub is_required: bool,
|
|
||||||
pub selected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub enum AddTableFocus {
|
|
||||||
#[default]
|
|
||||||
InputTableName, // Field 0 for CanvasState
|
|
||||||
InputColumnName, // Field 1 for CanvasState
|
|
||||||
InputColumnType, // Field 2 for CanvasState
|
|
||||||
AddColumnButton,
|
|
||||||
// Result Tables
|
|
||||||
ColumnsTable,
|
|
||||||
IndexesTable,
|
|
||||||
LinksTable,
|
|
||||||
// Inside Tables (Scrolling Focus)
|
|
||||||
InsideColumnsTable,
|
|
||||||
InsideIndexesTable,
|
|
||||||
InsideLinksTable,
|
|
||||||
// Buttons
|
|
||||||
SaveButton,
|
|
||||||
DeleteSelectedButton,
|
|
||||||
CancelButton,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AddTableState {
|
|
||||||
pub profile_name: String,
|
|
||||||
pub table_name: String,
|
|
||||||
pub table_name_input: String,
|
|
||||||
pub column_name_input: String,
|
|
||||||
pub column_type_input: String,
|
|
||||||
pub columns: Vec<ColumnDefinition>,
|
|
||||||
pub indexes: Vec<IndexDefinition>,
|
|
||||||
pub links: Vec<LinkDefinition>,
|
|
||||||
pub current_focus: AddTableFocus,
|
|
||||||
pub last_canvas_field: usize,
|
|
||||||
pub column_table_state: TableState,
|
|
||||||
pub index_table_state: TableState,
|
|
||||||
pub link_table_state: TableState,
|
|
||||||
pub table_name_cursor_pos: usize,
|
|
||||||
pub column_name_cursor_pos: usize,
|
|
||||||
pub column_type_cursor_pos: usize,
|
|
||||||
pub has_unsaved_changes: bool,
|
|
||||||
pub app_mode: AppMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AddTableState {
|
|
||||||
fn default() -> Self {
|
|
||||||
AddTableState {
|
|
||||||
profile_name: "default".to_string(),
|
|
||||||
table_name: String::new(),
|
|
||||||
table_name_input: String::new(),
|
|
||||||
column_name_input: String::new(),
|
|
||||||
column_type_input: String::new(),
|
|
||||||
columns: Vec::new(),
|
|
||||||
indexes: Vec::new(),
|
|
||||||
links: Vec::new(),
|
|
||||||
current_focus: AddTableFocus::InputTableName,
|
|
||||||
last_canvas_field: 2,
|
|
||||||
column_table_state: TableState::default(),
|
|
||||||
index_table_state: TableState::default(),
|
|
||||||
link_table_state: TableState::default(),
|
|
||||||
table_name_cursor_pos: 0,
|
|
||||||
column_name_cursor_pos: 0,
|
|
||||||
column_type_cursor_pos: 0,
|
|
||||||
has_unsaved_changes: false,
|
|
||||||
app_mode: AppMode::Edit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AddTableState {
|
|
||||||
pub const INPUT_FIELD_COUNT: usize = 3;
|
|
||||||
|
|
||||||
/// Helper method to add a column from current inputs
|
|
||||||
pub fn add_column_from_inputs(&mut self) -> Option<String> {
|
|
||||||
if self.column_name_input.trim().is_empty() || self.column_type_input.trim().is_empty() {
|
|
||||||
return Some("Both column name and type are required".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate column names
|
|
||||||
if self.columns.iter().any(|col| col.name == self.column_name_input.trim()) {
|
|
||||||
return Some("Column name already exists".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the column
|
|
||||||
self.columns.push(ColumnDefinition {
|
|
||||||
name: self.column_name_input.trim().to_string(),
|
|
||||||
data_type: self.column_type_input.trim().to_string(),
|
|
||||||
selected: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear inputs and reset focus to column name for next entry
|
|
||||||
self.column_name_input.clear();
|
|
||||||
self.column_type_input.clear();
|
|
||||||
self.column_name_cursor_pos = 0;
|
|
||||||
self.column_type_cursor_pos = 0;
|
|
||||||
self.current_focus = AddTableFocus::InputColumnName;
|
|
||||||
self.last_canvas_field = 1;
|
|
||||||
self.has_unsaved_changes = true;
|
|
||||||
|
|
||||||
Some(format!("Column '{}' added successfully", self.columns.last().unwrap().name))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper method to delete selected items
|
|
||||||
pub fn delete_selected_items(&mut self) -> Option<String> {
|
|
||||||
let mut deleted_items = Vec::new();
|
|
||||||
|
|
||||||
// Remove selected columns
|
|
||||||
let initial_column_count = self.columns.len();
|
|
||||||
self.columns.retain(|col| {
|
|
||||||
if col.selected {
|
|
||||||
deleted_items.push(format!("column '{}'", col.name));
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove selected indexes
|
|
||||||
let initial_index_count = self.indexes.len();
|
|
||||||
self.indexes.retain(|idx| {
|
|
||||||
if idx.selected {
|
|
||||||
deleted_items.push(format!("index '{}'", idx.name));
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove selected links
|
|
||||||
let initial_link_count = self.links.len();
|
|
||||||
self.links.retain(|link| {
|
|
||||||
if link.selected {
|
|
||||||
deleted_items.push(format!("link to '{}'", link.linked_table_name));
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if deleted_items.is_empty() {
|
|
||||||
Some("No items selected for deletion".to_string())
|
|
||||||
} else {
|
|
||||||
self.has_unsaved_changes = true;
|
|
||||||
Some(format!("Deleted: {}", deleted_items.join(", ")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataProvider for AddTableState {
|
|
||||||
fn field_count(&self) -> usize {
|
|
||||||
3 // Table name, Column name, Column type
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_name(&self, index: usize) -> &str {
|
|
||||||
match index {
|
|
||||||
0 => "Table name",
|
|
||||||
1 => "Name",
|
|
||||||
2 => "Type",
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_value(&self, index: usize) -> &str {
|
|
||||||
match index {
|
|
||||||
0 => &self.table_name_input,
|
|
||||||
1 => &self.column_name_input,
|
|
||||||
2 => &self.column_type_input,
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_field_value(&mut self, index: usize, value: String) {
|
|
||||||
match index {
|
|
||||||
0 => self.table_name_input = value,
|
|
||||||
1 => self.column_name_input = value,
|
|
||||||
2 => self.column_type_input = value,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.has_unsaved_changes = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supports_suggestions(&self, _field_index: usize) -> bool {
|
|
||||||
false // AddTableState doesn’t use suggestions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
// src/state/pages/admin.rs
|
|
||||||
|
|
||||||
use ratatui::widgets::ListState;
|
|
||||||
use crate::state::pages::add_table::AddTableState;
|
|
||||||
use crate::state::pages::add_logic::AddLogicState;
|
|
||||||
|
|
||||||
// Define the focus states for the admin panel panes
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub enum AdminFocus {
|
|
||||||
#[default] // Default focus is on the profiles list
|
|
||||||
ProfilesPane,
|
|
||||||
InsideProfilesList,
|
|
||||||
Tables,
|
|
||||||
InsideTablesList,
|
|
||||||
Button1,
|
|
||||||
Button2,
|
|
||||||
Button3,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
|
||||||
pub struct AdminState {
|
|
||||||
pub profiles: Vec<String>, // Holds profile names (used by non-admin view)
|
|
||||||
pub profile_list_state: ListState, // Tracks navigation highlight (>) in profiles
|
|
||||||
pub table_list_state: ListState, // Tracks navigation highlight (>) in tables
|
|
||||||
pub selected_profile_index: Option<usize>, // Index with [*] in profiles (persistent)
|
|
||||||
pub selected_table_index: Option<usize>, // Index with [*] in tables (persistent)
|
|
||||||
pub current_focus: AdminFocus, // Tracks which pane is focused
|
|
||||||
pub add_table_state: AddTableState,
|
|
||||||
pub add_logic_state: AddLogicState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AdminState {
|
|
||||||
/// Gets the index of the currently selected item.
|
|
||||||
pub fn get_selected_index(&self) -> Option<usize> {
|
|
||||||
self.profile_list_state.selected()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the name of the currently selected profile.
|
|
||||||
pub fn get_selected_profile_name(&self) -> Option<&String> {
|
|
||||||
self.profile_list_state.selected().and_then(|i| self.profiles.get(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Populates the profile list and updates/resets the selection.
|
|
||||||
pub fn set_profiles(&mut self, new_profiles: Vec<String>) {
|
|
||||||
let current_selection_index = self.profile_list_state.selected();
|
|
||||||
self.profiles = new_profiles;
|
|
||||||
|
|
||||||
if self.profiles.is_empty() {
|
|
||||||
self.profile_list_state.select(None);
|
|
||||||
} else {
|
|
||||||
let new_selection = match current_selection_index {
|
|
||||||
Some(index) => Some(index.min(self.profiles.len() - 1)),
|
|
||||||
None => Some(0),
|
|
||||||
};
|
|
||||||
self.profile_list_state.select(new_selection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selects the next profile in the list, wrapping around.
|
|
||||||
pub fn next(&mut self) {
|
|
||||||
if self.profiles.is_empty() {
|
|
||||||
self.profile_list_state.select(None);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = match self.profile_list_state.selected() {
|
|
||||||
Some(i) => if i >= self.profiles.len() - 1 { 0 } else { i + 1 },
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.profile_list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selects the previous profile in the list, wrapping around.
|
|
||||||
pub fn previous(&mut self) {
|
|
||||||
if self.profiles.is_empty() {
|
|
||||||
self.profile_list_state.select(None);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = match self.profile_list_state.selected() {
|
|
||||||
Some(i) => if i == 0 { self.profiles.len() - 1 } else { i - 1 },
|
|
||||||
None => self.profiles.len() - 1,
|
|
||||||
};
|
|
||||||
self.profile_list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the index of the currently selected profile.
|
|
||||||
pub fn get_selected_profile_index(&self) -> Option<usize> {
|
|
||||||
self.profile_list_state.selected()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the index of the currently selected table.
|
|
||||||
pub fn get_selected_table_index(&self) -> Option<usize> {
|
|
||||||
self.table_list_state.selected()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selects a profile by index and resets table selection.
|
|
||||||
pub fn select_profile(&mut self, index: Option<usize>) {
|
|
||||||
self.profile_list_state.select(index);
|
|
||||||
self.table_list_state.select(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selects a table by index.
|
|
||||||
pub fn select_table(&mut self, index: Option<usize>) {
|
|
||||||
self.table_list_state.select(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selects the next profile, wrapping around.
|
|
||||||
/// `profile_count` should be the total number of profiles available.
|
|
||||||
pub fn next_profile(&mut self, profile_count: usize) {
|
|
||||||
if profile_count == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = match self.get_selected_profile_index() {
|
|
||||||
Some(i) => {
|
|
||||||
if i >= profile_count - 1 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.select_profile(Some(i)); // Use the helper method
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selects the previous profile, wrapping around.
|
|
||||||
/// `profile_count` should be the total number of profiles available.
|
|
||||||
pub fn previous_profile(&mut self, profile_count: usize) {
|
|
||||||
if profile_count == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = match self.get_selected_profile_index() {
|
|
||||||
Some(i) => {
|
|
||||||
if i == 0 {
|
|
||||||
profile_count - 1
|
|
||||||
} else {
|
|
||||||
i - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0, // Or profile_count - 1 if you prefer wrapping from None
|
|
||||||
};
|
|
||||||
self.select_profile(Some(i)); // Use the helper method
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selects the next table, wrapping around.
|
|
||||||
/// `table_count` should be the number of tables in the *currently selected* profile.
|
|
||||||
pub fn next_table(&mut self, table_count: usize) {
|
|
||||||
if table_count == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = match self.get_selected_table_index() {
|
|
||||||
Some(i) => {
|
|
||||||
if i >= table_count - 1 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.select_table(Some(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selects the previous table, wrapping around.
|
|
||||||
/// `table_count` should be the number of tables in the *currently selected* profile.
|
|
||||||
pub fn previous_table(&mut self, table_count: usize) {
|
|
||||||
if table_count == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = match self.get_selected_table_index() {
|
|
||||||
Some(i) => {
|
|
||||||
if i == 0 {
|
|
||||||
table_count - 1
|
|
||||||
} else {
|
|
||||||
i - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0, // Or table_count - 1
|
|
||||||
};
|
|
||||||
self.select_table(Some(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -2,12 +2,44 @@
|
|||||||
|
|
||||||
use canvas::{DataProvider, AppMode};
|
use canvas::{DataProvider, AppMode};
|
||||||
|
|
||||||
|
/// Strongly typed user roles
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum UserRole {
|
||||||
|
Admin,
|
||||||
|
Moderator,
|
||||||
|
Accountant,
|
||||||
|
Viewer,
|
||||||
|
Unknown(String), // fallback for unexpected roles
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserRole {
|
||||||
|
pub fn from_str(s: &str) -> Self {
|
||||||
|
match s.trim().to_lowercase().as_str() {
|
||||||
|
"admin" => UserRole::Admin,
|
||||||
|
"moderator" => UserRole::Moderator,
|
||||||
|
"accountant" => UserRole::Accountant,
|
||||||
|
"viewer" => UserRole::Viewer,
|
||||||
|
other => UserRole::Unknown(other.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
UserRole::Admin => "admin",
|
||||||
|
UserRole::Moderator => "moderator",
|
||||||
|
UserRole::Accountant => "accountant",
|
||||||
|
UserRole::Viewer => "viewer",
|
||||||
|
UserRole::Unknown(s) => s.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents the authenticated session state
|
/// Represents the authenticated session state
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct AuthState {
|
pub struct AuthState {
|
||||||
pub auth_token: Option<String>,
|
pub auth_token: Option<String>,
|
||||||
pub user_id: Option<String>,
|
pub user_id: Option<String>,
|
||||||
pub role: Option<String>,
|
pub role: Option<UserRole>,
|
||||||
pub decoded_username: Option<String>,
|
pub decoded_username: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// src/tui/functions.rs
|
// src/tui/functions.rs
|
||||||
|
|
||||||
pub mod admin;
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
|
|
||||||
pub use admin::*;
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// src/tui/functions/common.rs
|
// src/tui/functions/common.rs
|
||||||
|
|
||||||
pub mod logout;
|
pub mod logout;
|
||||||
pub mod add_table;
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crossterm::{
|
|||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
cursor::{SetCursorStyle, EnableBlinking, Show, Hide, MoveTo},
|
cursor::{SetCursorStyle, EnableBlinking, Show, Hide, MoveTo},
|
||||||
};
|
};
|
||||||
|
use crossterm::ExecutableCommand;
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
use std::io::{self, stdout, Write};
|
use std::io::{self, stdout, Write};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -81,6 +82,12 @@ impl TerminalCore {
|
|||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to a specific (x, y) position on screen.
|
||||||
|
pub fn set_cursor_position(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||||
|
self.terminal.backend_mut().execute(MoveTo(x, y))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TerminalCore {
|
impl Drop for TerminalCore {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
// src/ui/handlers/render.rs
|
// src/ui/handlers/render.rs
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::render_background;
|
||||||
admin::add_logic::render_add_logic,
|
use crate::pages::admin_panel::add_logic::ui::render_add_logic;
|
||||||
admin::render_add_table,
|
use crate::pages::admin_panel::add_table::ui::render_add_table;
|
||||||
render_background,
|
|
||||||
};
|
|
||||||
use crate::pages::login::render_login;
|
use crate::pages::login::render_login;
|
||||||
use crate::pages::register::render_register;
|
use crate::pages::register::render_register;
|
||||||
use crate::pages::intro::render_intro;
|
use crate::pages::intro::render_intro;
|
||||||
@@ -22,6 +20,7 @@ use crate::buffer::state::BufferState;
|
|||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::bottom_panel::layout::{bottom_panel_constraints, render_bottom_panel};
|
use crate::bottom_panel::layout::{bottom_panel_constraints, render_bottom_panel};
|
||||||
|
use canvas::FormEditor;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
Frame,
|
Frame,
|
||||||
@@ -35,7 +34,6 @@ pub fn render_ui(
|
|||||||
router: &mut Router,
|
router: &mut Router,
|
||||||
buffer_state: &BufferState,
|
buffer_state: &BufferState,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
is_event_handler_edit_mode: bool,
|
|
||||||
event_handler_command_input: &str,
|
event_handler_command_input: &str,
|
||||||
event_handler_command_mode_active: bool,
|
event_handler_command_mode_active: bool,
|
||||||
event_handler_command_message: &str,
|
event_handler_command_message: &str,
|
||||||
@@ -43,6 +41,7 @@ pub fn render_ui(
|
|||||||
current_dir: &str,
|
current_dir: &str,
|
||||||
current_fps: f64,
|
current_fps: f64,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
|
auth_state: &AuthState,
|
||||||
) {
|
) {
|
||||||
render_background(f, f.area(), theme);
|
render_background(f, f.area(), theme);
|
||||||
|
|
||||||
@@ -77,13 +76,12 @@ 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,
|
||||||
state.current_field() < 2,
|
|
||||||
),
|
),
|
||||||
Page::Register(state) => render_register(
|
Page::Register(state) => render_register(
|
||||||
f,
|
f,
|
||||||
@@ -91,12 +89,11 @@ pub fn render_ui(
|
|||||||
theme,
|
theme,
|
||||||
state,
|
state,
|
||||||
app_state,
|
app_state,
|
||||||
state.current_field() < 4,
|
|
||||||
),
|
),
|
||||||
Page::Admin(state) => crate::components::admin::admin_panel::render_admin_panel(
|
Page::Admin(state) => crate::pages::admin::main::ui::render_admin_panel(
|
||||||
f,
|
f,
|
||||||
app_state,
|
app_state,
|
||||||
&mut AuthState::default(), // TODO: later move AuthState into Router
|
auth_state,
|
||||||
state,
|
state,
|
||||||
main_content_area,
|
main_content_area,
|
||||||
theme,
|
theme,
|
||||||
@@ -109,7 +106,6 @@ pub fn render_ui(
|
|||||||
theme,
|
theme,
|
||||||
app_state,
|
app_state,
|
||||||
state,
|
state,
|
||||||
is_event_handler_edit_mode,
|
|
||||||
),
|
),
|
||||||
Page::AddTable(state) => render_add_table(
|
Page::AddTable(state) => render_add_table(
|
||||||
f,
|
f,
|
||||||
@@ -117,9 +113,8 @@ pub fn render_ui(
|
|||||||
theme,
|
theme,
|
||||||
app_state,
|
app_state,
|
||||||
state,
|
state,
|
||||||
is_event_handler_edit_mode,
|
|
||||||
),
|
),
|
||||||
Page::Form(state) => {
|
Page::Form(path) => {
|
||||||
let (sidebar_area, form_actual_area) =
|
let (sidebar_area, form_actual_area) =
|
||||||
calculate_sidebar_layout(app_state.ui.show_sidebar, main_content_area);
|
calculate_sidebar_layout(app_state.ui.show_sidebar, main_content_area);
|
||||||
if let Some(sidebar_rect) = sidebar_area {
|
if let Some(sidebar_rect) = sidebar_area {
|
||||||
@@ -148,18 +143,27 @@ pub fn render_ui(
|
|||||||
.split(form_actual_area)[1]
|
.split(form_actual_area)[1]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(editor) = app_state.editor_for_path_ref(path) {
|
||||||
|
let (total_count, current_position) =
|
||||||
|
if let Some(fs) = app_state.form_state_for_path_ref(path) {
|
||||||
|
(fs.total_count, fs.current_position)
|
||||||
|
} else {
|
||||||
|
(0, 1)
|
||||||
|
};
|
||||||
|
let table_name = path.split('/').nth(1).unwrap_or("");
|
||||||
|
|
||||||
render_form_page(
|
render_form_page(
|
||||||
f,
|
f,
|
||||||
form_render_area,
|
form_render_area,
|
||||||
app_state,
|
editor,
|
||||||
state,
|
table_name,
|
||||||
app_state.current_view_table_name.as_deref().unwrap_or(""),
|
|
||||||
theme,
|
theme,
|
||||||
state.total_count,
|
total_count,
|
||||||
state.current_position,
|
current_position,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Global overlays (not tied to a page)
|
// Global overlays (not tied to a page)
|
||||||
if let Some(area) = buffer_list_area {
|
if let Some(area) = buffer_list_area {
|
||||||
@@ -189,9 +193,9 @@ pub fn render_ui(
|
|||||||
&mut chunk_idx,
|
&mut chunk_idx,
|
||||||
current_dir,
|
current_dir,
|
||||||
theme,
|
theme,
|
||||||
is_event_handler_edit_mode,
|
|
||||||
current_fps,
|
current_fps,
|
||||||
app_state,
|
app_state,
|
||||||
|
router,
|
||||||
navigation_state,
|
navigation_state,
|
||||||
event_handler_command_input,
|
event_handler_command_input,
|
||||||
event_handler_command_mode_active,
|
event_handler_command_mode_active,
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ use crate::modes::common::commands::CommandHandler;
|
|||||||
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
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::state::pages::auth::UserRole;
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::pages::login::LoginFormState;
|
||||||
use crate::state::pages::admin::AdminFocus;
|
use crate::pages::register::RegisterFormState;
|
||||||
|
use crate::pages::admin::AdminState;
|
||||||
|
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;
|
||||||
@@ -28,9 +32,12 @@ 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::CursorManager;
|
||||||
|
use canvas::FormEditor;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::{SetCursorStyle, MoveTo};
|
||||||
use crossterm::event as crossterm_event;
|
use crossterm::event as crossterm_event;
|
||||||
|
use crossterm::ExecutableCommand;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -65,8 +72,10 @@ 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();
|
login_state.editor.set_keymap(config.build_canvas_keymap());
|
||||||
|
let mut register_state = RegisterFormState::default();
|
||||||
|
register_state.editor.set_keymap(config.build_canvas_keymap());
|
||||||
let mut intro_state = IntroState::default();
|
let mut intro_state = IntroState::default();
|
||||||
let mut admin_state = AdminState::default();
|
let mut admin_state = AdminState::default();
|
||||||
let mut router = Router::new();
|
let mut router = Router::new();
|
||||||
@@ -78,7 +87,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
Ok(Some(stored_data)) => {
|
Ok(Some(stored_data)) => {
|
||||||
auth_state.auth_token = Some(stored_data.access_token);
|
auth_state.auth_token = Some(stored_data.access_token);
|
||||||
auth_state.user_id = Some(stored_data.user_id);
|
auth_state.user_id = Some(stored_data.user_id);
|
||||||
auth_state.role = Some(stored_data.role);
|
auth_state.role = Some(UserRole::from_str(&stored_data.role));
|
||||||
auth_state.decoded_username = Some(stored_data.username);
|
auth_state.decoded_username = Some(stored_data.username);
|
||||||
auto_logged_in = true;
|
auto_logged_in = true;
|
||||||
info!("Auth data loaded from file. User is auto-logged in.");
|
info!("Auth data loaded from file. User is auto-logged in.");
|
||||||
@@ -107,13 +116,15 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Replace local form_state with app_state.form_editor
|
// Replace local form_state with app_state.form_editor
|
||||||
app_state.set_form_state(
|
let path = format!("{}/{}", initial_profile, initial_table);
|
||||||
FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs),
|
app_state.ensure_form_editor(&path, &config, || {
|
||||||
&config,
|
FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs)
|
||||||
);
|
});
|
||||||
|
buffer_state.update_history(AppView::Form(path.clone()));
|
||||||
|
router.navigate(Page::Form(path.clone()));
|
||||||
|
|
||||||
// Fetch initial count using app_state accessor
|
// Fetch initial count using app_state accessor
|
||||||
if let Some(form_state) = app_state.form_state_mut() {
|
if let Some(form_state) = app_state.form_state_for_path(&path) {
|
||||||
UiService::fetch_and_set_table_count(&mut grpc_client, form_state)
|
UiService::fetch_and_set_table_count(&mut grpc_client, form_state)
|
||||||
.await
|
.await
|
||||||
.context(format!(
|
.context(format!(
|
||||||
@@ -131,7 +142,9 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if auto_logged_in {
|
if auto_logged_in {
|
||||||
buffer_state.history = vec![AppView::Form];
|
let path = format!("{}/{}", initial_profile, initial_table);
|
||||||
|
buffer_state.history = vec![AppView::Form(path.clone())];
|
||||||
|
router.navigate(Page::Form(path));
|
||||||
buffer_state.active_index = 0;
|
buffer_state.active_index = 0;
|
||||||
info!("Initial view set to Form due to auto-login.");
|
info!("Initial view set to Form due to auto-login.");
|
||||||
}
|
}
|
||||||
@@ -144,9 +157,11 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let mut table_just_switched = false;
|
let mut table_just_switched = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let position_before_event = app_state.form_state()
|
let position_before_event = if let Page::Form(path) = &router.current {
|
||||||
.map(|fs| fs.current_position)
|
app_state.form_state_for_path(path).map(|fs| fs.current_position).unwrap_or(1)
|
||||||
.unwrap_or(1);
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
let mut event_processed = false;
|
let mut event_processed = false;
|
||||||
|
|
||||||
// --- CHANNEL RECEIVERS ---
|
// --- CHANNEL RECEIVERS ---
|
||||||
@@ -171,7 +186,8 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
// --- ADDED: For live form autocomplete ---
|
// --- ADDED: For live form autocomplete ---
|
||||||
match event_handler.autocomplete_result_receiver.try_recv() {
|
match event_handler.autocomplete_result_receiver.try_recv() {
|
||||||
Ok(hits) => {
|
Ok(hits) => {
|
||||||
if let Some(form_state) = app_state.form_state_mut() {
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(form_state) = app_state.form_state_for_path(path) {
|
||||||
if form_state.autocomplete_active {
|
if form_state.autocomplete_active {
|
||||||
form_state.autocomplete_suggestions = hits;
|
form_state.autocomplete_suggestions = hits;
|
||||||
form_state.autocomplete_loading = false;
|
form_state.autocomplete_loading = false;
|
||||||
@@ -183,6 +199,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
event_handler.command_message = format!("Found {} suggestions.", form_state.autocomplete_suggestions.len());
|
event_handler.command_message = format!("Found {} suggestions.", form_state.autocomplete_suggestions.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
Err(mpsc::error::TryRecvError::Empty) => {}
|
Err(mpsc::error::TryRecvError::Empty) => {}
|
||||||
@@ -198,9 +215,19 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let event = event_reader.read_event().context("Failed to read terminal event")?;
|
let event = event_reader.read_event().context("Failed to read terminal event")?;
|
||||||
event_processed = true;
|
event_processed = true;
|
||||||
|
|
||||||
|
// Decouple Command Line and palettes from canvas:
|
||||||
|
// Only forward keys to Form canvas when:
|
||||||
|
// - not in command mode
|
||||||
|
// - no search/palette active
|
||||||
|
// - focus is inside the canvas
|
||||||
if let crossterm_event::Event::Key(key_event) = &event {
|
if let crossterm_event::Event::Key(key_event) = &event {
|
||||||
if let Page::Form(_) = &router.current {
|
let overlay_active = event_handler.command_mode
|
||||||
if let Some(editor) = app_state.form_editor.as_mut() {
|
|| app_state.ui.show_search_palette
|
||||||
|
|| event_handler.navigation_state.active;
|
||||||
|
if !overlay_active {
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if !app_state.ui.focus_outside_canvas {
|
||||||
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
match editor.handle_key_event(*key_event) {
|
match editor.handle_key_event(*key_event) {
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
KeyEventOutcome::Consumed(Some(msg)) => {
|
||||||
event_handler.command_message = msg;
|
event_handler.command_message = msg;
|
||||||
@@ -222,10 +249,10 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get form state from app_state and pass to handle_event
|
// Call handle_event directly
|
||||||
let form_state = app_state.form_state_mut().unwrap();
|
|
||||||
|
|
||||||
let event_outcome_result = event_handler.handle_event(
|
let event_outcome_result = event_handler.handle_event(
|
||||||
event,
|
event,
|
||||||
&config,
|
&config,
|
||||||
@@ -250,8 +277,8 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
EventOutcome::DataSaved(save_outcome, message) => {
|
EventOutcome::DataSaved(save_outcome, message) => {
|
||||||
event_handler.command_message = message;
|
event_handler.command_message = message;
|
||||||
// Clone form_state to avoid double borrow
|
if let Page::Form(path) = &router.current {
|
||||||
let mut temp_form_state = app_state.form_state().unwrap().clone();
|
if let Some(mut temp_form_state) = app_state.form_state_for_path(path).cloned() {
|
||||||
if let Err(e) = UiService::handle_save_outcome(
|
if let Err(e) = UiService::handle_save_outcome(
|
||||||
save_outcome,
|
save_outcome,
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
@@ -262,10 +289,12 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
format!("Error handling save outcome: {}", e);
|
format!("Error handling save outcome: {}", e);
|
||||||
}
|
}
|
||||||
// Update app_state with changes
|
// Update app_state with changes
|
||||||
if let Some(form_state) = app_state.form_state_mut() {
|
if let Some(form_state) = app_state.form_state_for_path(path) {
|
||||||
*form_state = temp_form_state;
|
*form_state = temp_form_state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
EventOutcome::ButtonSelected { .. } => {}
|
EventOutcome::ButtonSelected { .. } => {}
|
||||||
EventOutcome::TableSelected { path } => {
|
EventOutcome::TableSelected { path } => {
|
||||||
let parts: Vec<&str> = path.split('/').collect();
|
let parts: Vec<&str> = path.split('/').collect();
|
||||||
@@ -274,7 +303,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let table_name = parts[1].to_string();
|
let table_name = parts[1].to_string();
|
||||||
|
|
||||||
app_state.set_current_view_table(profile_name, table_name);
|
app_state.set_current_view_table(profile_name, table_name);
|
||||||
buffer_state.update_history(AppView::Form);
|
buffer_state.update_history(AppView::Form(path.clone()));
|
||||||
event_handler.command_message = format!("Loading table: {}", path);
|
event_handler.command_message = format!("Loading table: {}", path);
|
||||||
} else {
|
} else {
|
||||||
event_handler.command_message = format!("Invalid table path: {}", path);
|
event_handler.command_message = format!("Invalid table path: {}", path);
|
||||||
@@ -292,9 +321,19 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
|
|
||||||
match login_result_receiver.try_recv() {
|
match login_result_receiver.try_recv() {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state) {
|
// Apply result to the active router Login page if present,
|
||||||
needs_redraw = true;
|
// otherwise update the local copy.
|
||||||
}
|
let updated = if let Page::Login(page) = &mut router.current {
|
||||||
|
login::handle_login_result(
|
||||||
|
result,
|
||||||
|
&mut app_state,
|
||||||
|
&mut auth_state,
|
||||||
|
page,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state)
|
||||||
|
};
|
||||||
|
if updated { needs_redraw = true; }
|
||||||
}
|
}
|
||||||
Err(mpsc::error::TryRecvError::Empty) => {}
|
Err(mpsc::error::TryRecvError::Empty) => {}
|
||||||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||||||
@@ -341,54 +380,66 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
|
|
||||||
if let Some(active_view) = buffer_state.get_active_view() {
|
if let Some(active_view) = buffer_state.get_active_view() {
|
||||||
match active_view {
|
match active_view {
|
||||||
AppView::Intro => router.navigate(Page::Intro(intro_state.clone())),
|
AppView::Intro => {
|
||||||
AppView::Login => router.navigate(Page::Login(login_state.clone())),
|
// Keep external intro_state in sync with the live Router state
|
||||||
AppView::Register => router.navigate(Page::Register(register_state.clone())),
|
if let Page::Intro(current) = &router.current {
|
||||||
|
intro_state = current.clone();
|
||||||
|
}
|
||||||
|
// Navigate with the up-to-date state
|
||||||
|
router.navigate(Page::Intro(intro_state.clone()));
|
||||||
|
}
|
||||||
|
AppView::Login => {
|
||||||
|
// Do not re-create the page every frame. If we're already on Login,
|
||||||
|
// keep it. If we just switched into Login, create it once and
|
||||||
|
// inject the keymap.
|
||||||
|
if let Page::Login(_) = &router.current {
|
||||||
|
// Already on login page; keep existing state
|
||||||
|
} else {
|
||||||
|
let mut page = LoginFormState::new();
|
||||||
|
page.editor.set_keymap(config.build_canvas_keymap());
|
||||||
|
router.navigate(Page::Login(page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppView::Register => {
|
||||||
|
if let Page::Register(_) = &router.current {
|
||||||
|
// already on register page
|
||||||
|
} else {
|
||||||
|
let mut page = RegisterFormState::new();
|
||||||
|
page.editor.set_keymap(config.build_canvas_keymap());
|
||||||
|
router.navigate(Page::Register(page));
|
||||||
|
}
|
||||||
|
}
|
||||||
AppView::Admin => {
|
AppView::Admin => {
|
||||||
info!("Active view is Admin, refreshing profile tree...");
|
if let Page::Admin(current) = &router.current {
|
||||||
match grpc_client.get_profile_tree().await {
|
admin_state = current.clone();
|
||||||
Ok(refreshed_tree) => {
|
|
||||||
app_state.profile_tree = refreshed_tree;
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
info!("Auth role at render: {:?}", auth_state.role);
|
||||||
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()));
|
||||||
}
|
}
|
||||||
AppView::AddTable => router.navigate(Page::AddTable(admin_state.add_table_state.clone())),
|
AppView::AddTable => router.navigate(Page::AddTable(admin_state.add_table_state.clone())),
|
||||||
AppView::AddLogic => router.navigate(Page::AddLogic(admin_state.add_logic_state.clone())),
|
AppView::AddLogic => router.navigate(Page::AddLogic(admin_state.add_logic_state.clone())),
|
||||||
AppView::Form => {
|
AppView::Form(path) => {
|
||||||
if let Some(form_state) = app_state.form_state().cloned() {
|
// Keep current_view_* consistent with the active buffer path
|
||||||
router.navigate(Page::Form(form_state));
|
if let Some((profile, table)) = path.split_once('/') {
|
||||||
|
app_state.set_current_view_table(
|
||||||
|
profile.to_string(),
|
||||||
|
table.to_string(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
router.navigate(Page::Form(path.clone()));
|
||||||
}
|
}
|
||||||
AppView::Scratch => {}
|
AppView::Scratch => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Page::Form(_) = &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();
|
||||||
|
|
||||||
@@ -404,51 +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(()) => {
|
||||||
{
|
|
||||||
Ok(new_form_state) => {
|
|
||||||
// Set the new form state and fetch count
|
|
||||||
app_state.set_form_state(new_form_state, &config);
|
|
||||||
|
|
||||||
if let Some(form_state) = app_state.form_state_mut() {
|
|
||||||
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();
|
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;
|
||||||
@@ -459,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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,18 +550,20 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_position = app_state.form_state()
|
let current_position = if let Page::Form(path) = &router.current {
|
||||||
.map(|fs| fs.current_position)
|
app_state.form_state_for_path(path).map(|fs| fs.current_position).unwrap_or(1)
|
||||||
.unwrap_or(1);
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
let position_changed = current_position != position_before_event;
|
let position_changed = current_position != position_before_event;
|
||||||
let mut position_logic_needs_redraw = false;
|
let mut position_logic_needs_redraw = false;
|
||||||
|
|
||||||
if let Page::Form(form_state) = &mut router.current {
|
if let Page::Form(path) = &router.current {
|
||||||
if !table_just_switched {
|
if !table_just_switched {
|
||||||
if position_changed && !event_handler.is_edit_mode {
|
if position_changed && !app_state.is_canvas_edit_mode_at(path) {
|
||||||
position_logic_needs_redraw = true;
|
position_logic_needs_redraw = true;
|
||||||
|
|
||||||
if let Some(form_state) = app_state.form_state_mut() {
|
if let Some(form_state) = app_state.form_state_for_path(path) {
|
||||||
if form_state.current_position > form_state.total_count {
|
if form_state.current_position > form_state.total_count {
|
||||||
form_state.reset_to_empty();
|
form_state.reset_to_empty();
|
||||||
event_handler.command_message = format!(
|
event_handler.command_message = format!(
|
||||||
@@ -581,8 +598,8 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
form_state.current_cursor_pos =
|
form_state.current_cursor_pos =
|
||||||
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
}
|
}
|
||||||
} else if !position_changed && !event_handler.is_edit_mode {
|
} else if !position_changed && !app_state.is_canvas_edit_mode_at(path) {
|
||||||
if let Some(form_state) = app_state.form_state_mut() {
|
if let Some(form_state) = app_state.form_state_for_path(path) {
|
||||||
let current_input_str = form_state.get_current_input();
|
let current_input_str = form_state.get_current_input();
|
||||||
let current_input_len = current_input_str.chars().count();
|
let current_input_len = current_input_str.chars().count();
|
||||||
let max_cursor_pos = if current_input_len > 0 {
|
let max_cursor_pos = if current_input_len > 0 {
|
||||||
@@ -596,20 +613,18 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Page::Register(state) = &mut router.current {
|
} else if let Page::Register(state) = &mut router.current {
|
||||||
if !event_handler.is_edit_mode {
|
if !app_state.is_canvas_edit_mode() {
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
} else if let Page::Login(state) = &mut router.current {
|
} else if let Page::Login(state) = &mut router.current {
|
||||||
if !event_handler.is_edit_mode {
|
if !app_state.is_canvas_edit_mode() {
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,41 +656,43 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
|
|
||||||
if event_processed || needs_redraw || position_changed {
|
if event_processed || needs_redraw || position_changed {
|
||||||
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router);
|
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router);
|
||||||
|
|
||||||
match current_mode {
|
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 => {
|
AppMode::General => {
|
||||||
if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; }
|
if app_state.ui.focus_outside_canvas {
|
||||||
else { terminal.hide_cursor()?; }
|
// Outside canvas → app decides
|
||||||
|
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
} else {
|
||||||
|
// Inside canvas → let canvas handle it
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
|
let _ = CursorManager::update_for_mode(editor.mode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Page::Login(page) = &router.current {
|
||||||
|
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||||||
|
}
|
||||||
|
if let Page::Register(page) = &router.current {
|
||||||
|
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppMode::Command => {
|
||||||
|
// Command line overlay → app decides
|
||||||
|
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
}
|
}
|
||||||
AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily work around borrow checker by extracting needed values
|
|
||||||
let current_dir = app_state.current_dir.clone();
|
let current_dir = app_state.current_dir.clone();
|
||||||
|
terminal
|
||||||
// Since we can't borrow app_state both mutably and immutably,
|
.draw(|f| {
|
||||||
// we'll need to either:
|
|
||||||
// 1. Modify render_ui to take just app_state and access form_state internally, OR
|
|
||||||
// 2. Extract the specific fields render_ui needs from app_state
|
|
||||||
|
|
||||||
// For now, using approach where we temporarily clone what we need
|
|
||||||
let form_state_clone = app_state.form_state().unwrap().clone();
|
|
||||||
|
|
||||||
terminal.draw(|f| {
|
|
||||||
// Use a mutable clone for rendering
|
|
||||||
let mut temp_form_state = form_state_clone.clone();
|
|
||||||
render_ui(
|
render_ui(
|
||||||
f,
|
f,
|
||||||
&mut router,
|
&mut router,
|
||||||
&buffer_state,
|
&buffer_state,
|
||||||
&theme,
|
&theme,
|
||||||
event_handler.is_edit_mode,
|
|
||||||
&event_handler.command_input,
|
&event_handler.command_input,
|
||||||
event_handler.command_mode,
|
event_handler.command_mode,
|
||||||
&event_handler.command_message,
|
&event_handler.command_message,
|
||||||
@@ -683,11 +700,10 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
¤t_dir,
|
¤t_dir,
|
||||||
current_fps,
|
current_fps,
|
||||||
&app_state,
|
&app_state,
|
||||||
|
&auth_state,
|
||||||
);
|
);
|
||||||
|
})
|
||||||
// If render_ui modified the form_state, we'd need to sync it back
|
.context("Terminal draw call failed")?;
|
||||||
// But typically render functions don't modify state, just read it
|
|
||||||
}).context("Terminal draw call failed")?;
|
|
||||||
needs_redraw = false;
|
needs_redraw = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
client/ui.rs
Normal file
0
client/ui.rs
Normal file
Reference in New Issue
Block a user