From 9adcbf103a0c0318837d4ddfe282488b48c9c619 Mon Sep 17 00:00:00 2001 From: BADtochka Date: Sat, 6 Jun 2026 20:33:04 +0300 Subject: [PATCH] fix(admin): stabilize user modal lifecycle --- frontend/src/admin/AdminPanel.svelte | 33 +++-- frontend/src/lib/admin/stores/usersStore.js | 148 ++++++++++++-------- 2 files changed, 115 insertions(+), 66 deletions(-) diff --git a/frontend/src/admin/AdminPanel.svelte b/frontend/src/admin/AdminPanel.svelte index 48ea266..55c80ac 100644 --- a/frontend/src/admin/AdminPanel.svelte +++ b/frontend/src/admin/AdminPanel.svelte @@ -223,6 +223,8 @@ } let sidebarOpen = false; let isCompact = false; + let dismissedUserRouteKey = ""; + let lastUserRouteKey = ""; let adminLanguageMenuOpen = false; let adminLanguageClickGuard = false; let adminLanguageClickGuardArmed = false; @@ -379,6 +381,7 @@ const uid = Number(userId); // Synthetic email-only users use negative user_id; still a valid admin target. if (!Number.isFinite(uid) || uid === 0) return; + dismissedUserRouteKey = ""; const next = normalizeSection("payments"); sidebarOpen = false; if (active !== next) { @@ -395,6 +398,7 @@ function openLogsUserCard(userId) { const uid = Number(userId); if (!Number.isFinite(uid) || uid === 0) return; + dismissedUserRouteKey = ""; const next = normalizeSection("logs"); sidebarOpen = false; if (active !== next) { @@ -410,19 +414,20 @@ function openUserCard(userId) { const uid = Number(userId); if (!Number.isFinite(uid) || uid === 0) return; - const next = normalizeSection("users"); + dismissedUserRouteKey = ""; sidebarOpen = false; - usersStore.setActive(next); - if (active !== next) { - active = next; - paymentsStore.closePayment({ skipPush: true }); - supportStore.closeTicketView({ skipPush: true }); - onSectionChange(next, uid); - } - usersStore.openUser(uid); + usersStore.setActive(active); + usersStore.openUser(uid, { skipPush: true, pathContext: active }); + } + + function userRouteKey(section = active) { + if (section === "users" && initialUserId) return `users:${initialUserId}`; + if (section === "payments" && initialPaymentUserId) return `payments:${initialPaymentUserId}`; + return ""; } function closeUserCard() { + dismissedUserRouteKey = userRouteKey(); usersStore.closeUser({ skipPush: true }); if (active === "users" || active === "payments") { onSectionChange(active, 0); @@ -540,9 +545,18 @@ $: sectionFade = reduceMotion ? { duration: 0 } : { duration: 200 }; $: sidebarBackdropFade = reduceMotion ? { duration: 0 } : { duration: 180 }; + $: { + const currentUserRouteKey = userRouteKey(); + if (currentUserRouteKey !== lastUserRouteKey) { + if (currentUserRouteKey !== dismissedUserRouteKey) dismissedUserRouteKey = ""; + lastUserRouteKey = currentUserRouteKey; + } + } + $: if ( active === "users" && initialUserId && + dismissedUserRouteKey !== `users:${initialUserId}` && (!$usersStore.openedUser || $usersStore.openedUser.user_id !== initialUserId) ) { usersStore.openUser(initialUserId, { skipPush: true }); @@ -559,6 +573,7 @@ $: if ( active === "payments" && initialPaymentUserId && + dismissedUserRouteKey !== `payments:${initialPaymentUserId}` && (!$usersStore.openedUser || $usersStore.openedUser.user_id !== initialPaymentUserId) ) { usersStore.openUser(initialPaymentUserId, { skipPush: true, pathContext: "payments" }); diff --git a/frontend/src/lib/admin/stores/usersStore.js b/frontend/src/lib/admin/stores/usersStore.js index 716e1e8..6ba9e49 100644 --- a/frontend/src/lib/admin/stores/usersStore.js +++ b/frontend/src/lib/admin/stores/usersStore.js @@ -54,6 +54,58 @@ export function createUsersStore({ api, onToast, at, routePrefix = "" }) { let _activeRef = "stats"; // fallback if active isn't tracked let _pathContext = null; + let _openUserRequestId = 0; + + function _closedUserModalState() { + return { + openedUser: null, + openedUserDetail: null, + userDetailLoading: false, + userMessageDraft: "", + userExtendDays: 30, + userExtendHwidDevices: true, + userDeleteOpen: false, + userBanConfirmOpen: false, + userMessageConfirmOpen: false, + userReferralsOpen: false, + userReferralsLoading: false, + userReferrals: [], + userReferralsTotal: 0, + userReferralsPage: 0, + userReferralsInviter: null, + userDetailTab: "profile", + premiumUnlimitedDraft: false, + premiumBonusGbDraft: "", + regularUnlimitedDraft: false, + regularBonusGbDraft: "", + hwidUnlimitedDraft: false, + hwidDeviceLimitDraft: "", + grantTrafficGbDraft: "", + grantTrafficKindDraft: "regular", + userLogs: [], + userLogsTotal: 0, + userLogsPage: 0, + userLogsLoading: false, + userLogsLoaded: false, + userLogsUserId: null, + }; + } + + function _openingUserModalState(user, userId) { + return { + ..._closedUserModalState(), + openedUser: user, + userDetailLoading: true, + userDetailTab: "subscription", + userLogsUserId: userId, + }; + } + + function _isCurrentUserRequest(s, requestId, userId) { + return ( + requestId === _openUserRequestId && Boolean(s.openedUser) && s.openedUser.user_id === userId + ); + } function setActive(active) { _activeRef = active; @@ -124,31 +176,15 @@ export function createUsersStore({ api, onToast, at, routePrefix = "" }) { const userId = typeof userOrId === "object" && userOrId !== null ? userOrId.user_id : Number(userOrId); if (!userId) return; + const requestId = ++_openUserRequestId; _setPathContext(opts.pathContext); + const openedUser = + typeof userOrId === "object" && userOrId !== null ? userOrId : { user_id: userId }; state.update((s) => ({ ...s, - openedUser: - typeof userOrId === "object" && userOrId !== null ? userOrId : { user_id: userId }, - openedUserDetail: null, - userMessageDraft: "", - userMessageConfirmOpen: false, - userExtendDays: 30, - userExtendHwidDevices: true, - userDetailLoading: true, - userDetailTab: "subscription", - userReferralsOpen: false, - userReferralsLoading: false, - userReferrals: [], - userReferralsTotal: 0, - userReferralsPage: 0, - userReferralsInviter: null, - userLogs: [], - userLogsTotal: 0, - userLogsPage: 0, - userLogsLoading: false, - userLogsLoaded: false, - userLogsUserId: userId, + ..._openingUserModalState(openedUser, userId), + userActionBusy: s.userActionBusy, })); if (!opts.skipPush) _pushUserPath(userId); @@ -161,54 +197,52 @@ export function createUsersStore({ api, onToast, at, routePrefix = "" }) { const hasHwidLimit = sub?.hwid_device_limit !== null && sub?.hwid_device_limit !== undefined; const hwidLimit = hasHwidLimit ? Number(sub?.hwid_device_limit) : null; - state.update((s) => ({ - ...s, - openedUserDetail: res, - openedUser: res.user ? { ...res.user, ...s.openedUser, ...res.user } : s.openedUser, - premiumUnlimitedDraft: Boolean(sub?.premium_unlimited_override), - premiumBonusGbDraft: bonusBytes > 0 ? +(bonusBytes / 1024 ** 3).toFixed(2) : "", - regularUnlimitedDraft: Boolean(sub?.regular_unlimited_override), - regularBonusGbDraft: - regularBonusBytes > 0 ? +(regularBonusBytes / 1024 ** 3).toFixed(2) : "", - hwidUnlimitedDraft: hasHwidLimit && hwidLimit === 0, - hwidDeviceLimitDraft: hasHwidLimit && hwidLimit > 0 ? String(hwidLimit) : "", - grantTrafficGbDraft: "", - grantTrafficKindDraft: "regular", - })); + state.update((s) => { + if (!_isCurrentUserRequest(s, requestId, userId)) return s; + return { + ...s, + openedUserDetail: res, + openedUser: res.user ? { ...res.user, ...s.openedUser, ...res.user } : s.openedUser, + premiumUnlimitedDraft: Boolean(sub?.premium_unlimited_override), + premiumBonusGbDraft: bonusBytes > 0 ? +(bonusBytes / 1024 ** 3).toFixed(2) : "", + regularUnlimitedDraft: Boolean(sub?.regular_unlimited_override), + regularBonusGbDraft: + regularBonusBytes > 0 ? +(regularBonusBytes / 1024 ** 3).toFixed(2) : "", + hwidUnlimitedDraft: hasHwidLimit && hwidLimit === 0, + hwidDeviceLimitDraft: hasHwidLimit && hwidLimit > 0 ? String(hwidLimit) : "", + grantTrafficGbDraft: "", + grantTrafficKindDraft: "regular", + }; + }); } else { - onToast(res?.error || "load_failed"); - state.update((s) => ({ ...s, openedUser: null })); - if (!opts.skipPush) _pushUserPath(null); - _pathContext = null; + let shouldClearPath = false; + let shouldShowError = false; + state.update((s) => { + if (!_isCurrentUserRequest(s, requestId, userId)) return s; + shouldShowError = true; + shouldClearPath = true; + _pathContext = null; + return { ...s, ..._closedUserModalState() }; + }); + if (shouldShowError) onToast(res?.error || "load_failed"); + if (shouldClearPath && !opts.skipPush) _pushUserPath(null); } } finally { - state.update((s) => ({ ...s, userDetailLoading: false })); + state.update((s) => { + if (!_isCurrentUserRequest(s, requestId, userId)) return s; + return { ...s, userDetailLoading: false }; + }); } } function closeUser(opts = {}) { let wasOpen = false; + _openUserRequestId += 1; state.update((s) => { wasOpen = Boolean(s.openedUser); return { ...s, - openedUser: null, - openedUserDetail: null, - userDeleteOpen: false, - userBanConfirmOpen: false, - userMessageConfirmOpen: false, - userReferralsOpen: false, - userReferralsLoading: false, - userReferrals: [], - userReferralsTotal: 0, - userReferralsPage: 0, - userReferralsInviter: null, - userLogs: [], - userLogsTotal: 0, - userLogsPage: 0, - userLogsLoading: false, - userLogsLoaded: false, - userLogsUserId: null, + ..._closedUserModalState(), }; }); if (wasOpen && !opts.skipPush) _pushUserPath(null);