fix(admin): stabilize user modal lifecycle

This commit is contained in:
BADtochka
2026-06-06 20:33:04 +03:00
parent fcb8e51ec0
commit 9adcbf103a
2 changed files with 115 additions and 66 deletions
+23 -8
View File
@@ -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.setActive(active);
usersStore.openUser(uid, { skipPush: true, pathContext: active });
}
usersStore.openUser(uid);
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" });
+78 -44
View File
@@ -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,7 +197,9 @@ 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) => ({
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,
@@ -174,41 +212,37 @@ export function createUsersStore({ api, onToast, at, routePrefix = "" }) {
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);
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);