Skip to content
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- The Identities page lets you load an existing identity when you already have
identities. A new "Add" menu in the top bar offers "Create a new identity" and
"Load an existing identity", and the "Add a new identity" card now offers both
choices instead of opening only identity creation.

- Token detail headers sort rows by identity name, identity ID, or numeric balance;
unchecked balances stay last in either direction.

Expand Down
1 change: 1 addition & 0 deletions docs/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ As a power user, I want to load an existing identity by its ID and owner private

- Enter identity ID and private key.
- Identity details are fetched and displayed.
- Reachable from the Identities page at any time, including when identities are already loaded: the top-bar "Add" menu and the "Add a new identity" card both offer "Load an existing identity".

### IDN-003: Load evonode/masternode identity [Superseded by MN-001]
**Persona:** Priya
Expand Down
12 changes: 11 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1270,8 +1270,18 @@ pub struct AppState {
prompt_was_blocking: bool,
}

/// An action displayed in a toolbar dropdown.
#[derive(Debug, Clone, PartialEq)]
pub struct ToolbarMenuItem {
pub label: &'static str,
pub action: DesiredAppAction,
pub enabled: bool,
pub tooltip: &'static str,
}

#[derive(Debug, Clone, PartialEq)]
pub enum DesiredAppAction {
Menu(Vec<ToolbarMenuItem>),
None,
Refresh,
AddScreenType(Box<ScreenType>),
Expand All @@ -1283,7 +1293,7 @@ pub enum DesiredAppAction {
impl DesiredAppAction {
pub fn create_action(&self, app_context: &Arc<AppContext>) -> AppAction {
match self {
DesiredAppAction::None => AppAction::None,
DesiredAppAction::None | DesiredAppAction::Menu(_) => AppAction::None,
DesiredAppAction::Refresh => AppAction::Refresh,
DesiredAppAction::Custom(message) => AppAction::Custom(message.clone()),
DesiredAppAction::AddScreenType(screen_type) => {
Expand Down
9 changes: 0 additions & 9 deletions src/ui/components/global_nav_switcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

use crate::context::AppContext;
use crate::model::qualified_identity::QualifiedIdentity;
use crate::model::user_role::UserRole;
use crate::model::wallet::WalletSeedHash;
use crate::model::wallet_association::{NOT_IN_WALLET_LABEL, WalletAssociation};
use crate::ui::RootScreenType;
Expand Down Expand Up @@ -62,8 +61,6 @@ pub enum GlobalNavEffect {
AddIdentityCreate,
/// "Add another identity" → load an existing identity.
AddIdentityLoad,
/// Dev-mode: bulk-create test identities.
CreateTestIdentities,
}

/// Wallet-pill mode by HD-wallet count: 0 → placeholder, 1 → subdued (info
Expand Down Expand Up @@ -614,12 +611,6 @@ fn render_app_global_identity_pill(
*effect = GlobalNavEffect::AddIdentityLoad;
ui.close();
}
if app_context.user_role().at_least(UserRole::Power)
&& ui.button("Create multiple test identities").clicked()
{
*effect = GlobalNavEffect::CreateTestIdentities;
ui.close();
}
});
});
}
Expand Down
119 changes: 115 additions & 4 deletions src/ui/components/top_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,56 @@ fn render_top_island(
// Render other buttons normally
for (text, btn_act) in other_actions.into_iter().rev() {
ui.add_space(3.0);
if ComponentStyles::add_toolbar_button(ui, text, network_accent)
.clicked()
{
let label = if matches!(btn_act, DesiredAppAction::Menu(_)) {
format!("{text} ▾")
} else {
text.to_owned()
};
let response = ComponentStyles::add_toolbar_button(
ui,
&label,
network_accent,
);
if let DesiredAppAction::Menu(items) = btn_act {
egui::Popup::new(
ui.make_persistent_id(text),
ui.ctx().clone(),
&response,
response.layer_id,
)
.open_memory(
response
.clicked()
.then_some(egui::SetOpenCommand::Toggle),
)
.close_behavior(
egui::PopupCloseBehavior::CloseOnClickOutside,
)
.frame(
egui::Frame::popup(ui.style()).fill(
DashColors::popup_fill(ui.visuals().dark_mode),
),
)
.show(|ui| {
for item in items {
let clicked = ui
.add_enabled_ui(item.enabled, |ui| {
ComponentStyles::add_button(
ui,
egui::Button::new(item.label),
)
})
.inner
.clickable_tooltip(item.tooltip)
.disabled_tooltip(item.tooltip)
.clicked();
if clicked {
action = item.action.create_action(app_context);
ui.close();
}
}
});
} else if response.clicked() {
action = btn_act.create_action(app_context);
}
}
Expand Down Expand Up @@ -406,7 +453,7 @@ pub fn apply_global_nav_effect(
GlobalNavEffect::AddWallet => {
AppAction::SetMainScreen(RootScreenType::RootScreenWalletsBalances)
}
GlobalNavEffect::AddIdentityCreate | GlobalNavEffect::CreateTestIdentities => {
GlobalNavEffect::AddIdentityCreate => {
AppAction::AddScreen(ScreenType::AddNewIdentity.create_screen(app_context))
}
GlobalNavEffect::AddIdentityLoad => {
Expand Down Expand Up @@ -479,6 +526,70 @@ pub fn add_top_panel_with_global_nav_capturing(
mod tests {
use super::*;

#[test]
fn advanced_menu_opens_dispatches_and_closes_in_both_themes() {
use crate::app::ToolbarMenuItem;
use egui_kittest::{
Harness,
kittest::{NodeT, Queryable},
};
for dark in [false, true] {
let tmp = tempfile::tempdir().expect("temp dir");
let ctx = crate::context::test_support::test_app_context(tmp.path());
let mut harness = Harness::builder()
.with_size(egui::vec2(1200.0, 400.0))
.build_ui_state(
|ui, last_action| {
let action = render_top_island(
ui,
&ctx,
|_| AppAction::None,
vec![(
"Advanced",
DesiredAppAction::Menu(vec![
ToolbarMenuItem {
label: "Import key",
action: DesiredAppAction::Custom("import".into()),
enabled: true,
tooltip: "Import a key.",
},
ToolbarMenuItem {
label: "Full resync",
action: DesiredAppAction::Custom("resync".into()),
enabled: false,
tooltip: "Wait for sync.",
},
]),
)],
);
if action != AppAction::None {
*last_action = action;
}
},
AppAction::None,
);
harness.ctx.set_visuals(if dark {
egui::Visuals::dark()
} else {
egui::Visuals::light()
});
harness.run();
assert!(harness.query_by_label("Import key").is_none());
harness.get_by_label("Advanced ▾").click();
harness.run();
assert!(
harness
.get_by_label("Full resync")
.accesskit_node()
.is_disabled()
);
harness.get_by_label("Import key").click();
harness.run();
assert_eq!(harness.state(), &AppAction::Custom("import".into()));
assert!(harness.query_by_label("Import key").is_none());
}
}

/// TC-WALLETLINK-02: the Wallets page's spec exposes an **interactive**
/// (`Consumed`) wallet pill with no how-to tooltip — the pill drives the
/// selection here rather than pointing elsewhere.
Expand Down
81 changes: 76 additions & 5 deletions src/ui/identity/breadcrumb_switcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//!
//! [`global_nav_switcher`]: crate::ui::components::global_nav_switcher

use crate::app::{AppAction, DesiredAppAction, ToolbarMenuItem};
use crate::context::AppContext;
use crate::ui::RootScreenType;
use crate::ui::components::global_nav_switcher::{self, GlobalNavEffect};
Expand Down Expand Up @@ -39,8 +40,6 @@ pub enum BreadcrumbEffect {
AddIdentityCreate,
/// "Add another identity" → load an existing identity.
AddIdentityLoad,
/// Dev-mode: bulk-create test identities.
CreateTestIdentities,
}

/// The hub's page-nav spec: `Identities` segment-1 linking to the hub root, an
Expand Down Expand Up @@ -69,7 +68,45 @@ fn map_effect(effect: GlobalNavEffect) -> BreadcrumbEffect {
GlobalNavEffect::AddWallet => BreadcrumbEffect::AddWallet,
GlobalNavEffect::AddIdentityCreate => BreadcrumbEffect::AddIdentityCreate,
GlobalNavEffect::AddIdentityLoad => BreadcrumbEffect::AddIdentityLoad,
GlobalNavEffect::CreateTestIdentities => BreadcrumbEffect::CreateTestIdentities,
}
}

/// `AppAction::Custom` command emitted by the "Create a new identity" item.
const ADD_IDENTITY_CREATE_COMMAND: &str = "IdentityHubAddCreate";
/// `AppAction::Custom` command emitted by the "Load an existing identity" item.
const ADD_IDENTITY_LOAD_COMMAND: &str = "IdentityHubAddLoad";

/// Items of the hub's "Add" menu, shared by the top-bar dropdown and the
/// picker's "Add a new identity" card so both offer the same choices as the
/// identity pill's dropdown. Each item emits an `AppAction::Custom` command the
/// hub maps back via [`add_identity_command_effect`].
pub(crate) fn add_identity_menu_items() -> Vec<ToolbarMenuItem> {
vec![
ToolbarMenuItem {
label: "Create a new identity",
action: DesiredAppAction::Custom(ADD_IDENTITY_CREATE_COMMAND.into()),
enabled: true,
tooltip: "Create a new identity and fund it from one of your wallets.",
},
ToolbarMenuItem {
label: "Load an existing identity",
action: DesiredAppAction::Custom(ADD_IDENTITY_LOAD_COMMAND.into()),
enabled: true,
tooltip: "Load an identity you already own, by its ID or from your wallet.",
},
]
}
Comment thread
lklimek marked this conversation as resolved.
Outdated

/// Map an action produced by an [`add_identity_menu_items`] entry to the hub
/// effect it requests; `None` for any other action.
pub(crate) fn add_identity_command_effect(action: &AppAction) -> Option<BreadcrumbEffect> {
let AppAction::Custom(command) = action else {
return None;
};
match command.as_str() {
ADD_IDENTITY_CREATE_COMMAND => Some(BreadcrumbEffect::AddIdentityCreate),
ADD_IDENTITY_LOAD_COMMAND => Some(BreadcrumbEffect::AddIdentityLoad),
_ => None,
}
}

Expand Down Expand Up @@ -129,9 +166,43 @@ mod tests {
map_effect(GlobalNavEffect::AddWallet),
BreadcrumbEffect::AddWallet
);
}

/// The Add menu offers exactly create and load, both enabled, matching
/// the identity pill's dropdown.
#[test]
fn add_menu_offers_create_and_load() {
let items = add_identity_menu_items();
let labels: Vec<_> = items.iter().map(|item| item.label).collect();
assert_eq!(
labels,
["Create a new identity", "Load an existing identity"]
);
assert!(items.iter().all(|item| item.enabled));
}

/// Every add-menu item maps back to its hub effect; unrelated actions do not.
#[test]
fn add_menu_actions_map_to_hub_effects() {
let ctx_free_action = |item: &ToolbarMenuItem| match &item.action {
DesiredAppAction::Custom(command) => AppAction::Custom(command.clone()),
other => panic!("unexpected menu action {other:?}"),
};
let effects: Vec<_> = add_identity_menu_items()
.iter()
.map(|item| add_identity_command_effect(&ctx_free_action(item)))
.collect();
assert_eq!(
effects,
[
Some(BreadcrumbEffect::AddIdentityCreate),
Some(BreadcrumbEffect::AddIdentityLoad),
]
);
assert_eq!(
map_effect(GlobalNavEffect::CreateTestIdentities),
BreadcrumbEffect::CreateTestIdentities
add_identity_command_effect(&AppAction::Custom("OpenImportSingleKey".into())),
None
);
assert_eq!(add_identity_command_effect(&AppAction::None), None);
}
}
17 changes: 12 additions & 5 deletions src/ui/identity/hub_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use super::breadcrumb_switcher::{self, BreadcrumbEffect};
use super::identity_hub_tab_bar::IdentityHubTabBar;
use crate::app::AppAction;
use crate::app::{AppAction, DesiredAppAction};
use crate::backend_task::dashpay::DashPayTask;
use crate::backend_task::error::TaskError;
use crate::backend_task::identity::IdentityTask;
Expand Down Expand Up @@ -374,9 +374,7 @@ impl IdentityHubScreen {
BreadcrumbEffect::AddWallet => {
AppAction::SetMainScreen(RootScreenType::RootScreenWalletsBalances)
}
// The bulk-create flow is not wired yet; route to the single-create
// screen so the dev entry is functional in the interim.
BreadcrumbEffect::AddIdentityCreate | BreadcrumbEffect::CreateTestIdentities => {
BreadcrumbEffect::AddIdentityCreate => {
AppAction::AddScreen(crate::ui::Screen::AddNewIdentityScreen(
super::add_new_identity_screen::AddNewIdentityScreen::new_with_wallet(
&self.app_context,
Expand Down Expand Up @@ -426,7 +424,10 @@ impl ScreenLike for IdentityHubScreen {
breadcrumb_effect = breadcrumb_switcher::render(ui, &app_context, selection);
AppAction::None
},
vec![],
vec![(
"Add",
DesiredAppAction::Menu(breadcrumb_switcher::add_identity_menu_items()),
)],
);
Comment thread
lklimek marked this conversation as resolved.

action |= add_left_panel(ui, &self.app_context, RootScreenType::RootScreenIdentityHub);
Expand Down Expand Up @@ -564,6 +565,12 @@ impl ScreenLike for IdentityHubScreen {
}

action |= self.apply_breadcrumb_effect(breadcrumb_effect);
// The top-bar "Add" menu and the picker's add card emit
// `AppAction::Custom` commands; route them like the identity pill's
// add entries.
if let Some(effect) = breadcrumb_switcher::add_identity_command_effect(&action) {
action = self.apply_breadcrumb_effect(effect);
}

self.prepare_contact_info_dialog();
if let Some((dialog, key)) = &mut self.contact_info_overwrite_dialog {
Expand Down
Loading
Loading