fix(market): scope the portfolio editor to the active wallet
The Market summary filters custom groups by wallet scope, but the editor did not: its master list showed and edited *every* wallet's groups, and "Manage…" opened raw storage index 0 — which could belong to a different wallet. Separately, a group could be persisted with an empty scope (a placeholder added before the wallet identity resolved, then filled in and saved), which the filter treats as "legacy" and shows in *every* wallet's list. - The editor master list now filters to the active wallet's groups (plus legacy/unscoped), mirroring the summary. Storage indices are preserved so selection/delete still target the right entry. - "Manage…" opens the first in-scope group (or the empty state), never index 0. - The selection clamp resets to the first visible group if the current one is out of range or out of scope. - On commit, a group with no scope is claimed for the active wallet when an identity is available, so it can't linger in the global bucket. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -349,8 +349,11 @@ static void pfPersist(config::Settings* settings, const std::vector<config::Sett
|
||||
}
|
||||
|
||||
// Persist the working group when it is named and actually changed; unnamed drafts are dropped.
|
||||
// New groups default to the active wallet's scope (per-wallet visibility); existing groups keep
|
||||
// whatever scope they already had.
|
||||
// New groups adopt the active wallet's scope (per-wallet visibility). An existing group with no scope
|
||||
// — a placeholder added before the wallet identity resolved, or a pre-scoping legacy group — is also
|
||||
// claimed for the active wallet on edit, so it can't linger in the global (empty-scope) bucket and
|
||||
// leak into every wallet's list. No-op when the identity is unavailable (stays unscoped until edited
|
||||
// with a live identity).
|
||||
static void pfCommitIfNeeded(App* app)
|
||||
{
|
||||
config::Settings* settings = app->settings();
|
||||
@@ -360,12 +363,34 @@ static void pfCommitIfNeeded(App* app)
|
||||
config::Settings::PortfolioEntry base;
|
||||
if (existing) { base = entries[s_pfEdit.sel]; if (pfWorkingMatches(base)) return; }
|
||||
auto e = pfBuildWorking(base);
|
||||
if (!existing) e.scope = app->activeWalletIdentityHash();
|
||||
if (e.scope.empty()) {
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
if (!h.empty()) e.scope = h;
|
||||
}
|
||||
if (existing) entries[s_pfEdit.sel] = e;
|
||||
else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; }
|
||||
pfPersist(settings, entries);
|
||||
}
|
||||
|
||||
// A group is visible to the active wallet when it carries the wallet's scope, is unscoped (legacy —
|
||||
// shown everywhere), or the wallet identity isn't resolved yet. The editor mirrors the Market summary's
|
||||
// scope filter so it only lists/edits the current wallet's groups.
|
||||
static bool pfIndexVisible(App* app, int i)
|
||||
{
|
||||
const auto& es = app->settings()->getPortfolioEntries();
|
||||
if (i < 0 || i >= (int)es.size()) return false;
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
return es[i].scope.empty() || h.empty() || es[i].scope == h;
|
||||
}
|
||||
static int pfFirstVisibleIndex(App* app)
|
||||
{
|
||||
const auto& es = app->settings()->getPortfolioEntries();
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
for (int i = 0; i < (int)es.size(); i++)
|
||||
if (es[i].scope.empty() || h.empty() || es[i].scope == h) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---- Detail-pane section renderers (drawn inside ##pfDetailBody; operate on s_pfEdit) ----
|
||||
|
||||
// Appearance: accent-color picker (+ custom), outline-opacity slider, icon grid.
|
||||
@@ -759,11 +784,12 @@ static void RenderPortfolioEditor(App* app)
|
||||
}
|
||||
|
||||
{ // Clamp/validate the selection. The in-editor delete already reloads the working state, but if the
|
||||
// list shrank via any other path, reload after clamping so a later commit can't overlay stale
|
||||
// fields onto the clamped entry.
|
||||
// list shrank or the selection landed on a group outside this wallet's scope via any other path,
|
||||
// reset to the first visible group and reload so a later commit can't overlay stale fields.
|
||||
const auto& entriesRO = settings->getPortfolioEntries();
|
||||
if (s_pfEdit.sel >= (int)entriesRO.size()) {
|
||||
s_pfEdit.sel = entriesRO.empty() ? -1 : 0;
|
||||
if (s_pfEdit.sel >= (int)entriesRO.size() ||
|
||||
(s_pfEdit.sel >= 0 && !pfIndexVisible(app, s_pfEdit.sel))) {
|
||||
s_pfEdit.sel = pfFirstVisibleIndex(app);
|
||||
PortfolioBeginEdit(app, s_pfEdit.sel);
|
||||
}
|
||||
}
|
||||
@@ -792,12 +818,20 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::BeginChild("##pfMasterList", ImVec2(masterW, std::max(48.0f, bodyH - addH - Layout::spacingSm())), false);
|
||||
{
|
||||
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||
if (entries.empty())
|
||||
// Only list groups scoped to the active wallet (or legacy/unscoped) — same filter the
|
||||
// Market summary uses. `i` stays the storage index (selection/delete operate on storage).
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
std::vector<int> vis;
|
||||
for (int i = 0; i < (int)entries.size(); i++)
|
||||
if (entries[i].scope.empty() || activeHash.empty() || entries[i].scope == activeHash)
|
||||
vis.push_back(i);
|
||||
if (vis.empty())
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||
float rowH = 46.0f * dp;
|
||||
float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin
|
||||
int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop
|
||||
for (int i = 0; i < (int)entries.size(); i++) {
|
||||
for (int vi = 0; vi < (int)vis.size(); vi++) {
|
||||
int i = vis[vi];
|
||||
ImGui::PushID(i);
|
||||
const auto& en = entries[i];
|
||||
bool selRow = (s_pfEdit.sel == i);
|
||||
@@ -1824,8 +1858,9 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImGui::SameLine();
|
||||
material::RightAlignX(mBtnW);
|
||||
if (material::TactileButton(ml, ImVec2(mBtnW, 0))) {
|
||||
// Open the combined editor selecting the first group (or the empty state if none).
|
||||
PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -1 : 0);
|
||||
// Open the editor on the first group visible to this wallet (or the empty state if none) —
|
||||
// never a raw index 0 that might belong to a different wallet.
|
||||
PortfolioBeginEdit(app, pfFirstVisibleIndex(app));
|
||||
s_pfEdit.open = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user