fix(admin): stabilize user modal lifecycle
This commit is contained in:
@@ -223,6 +223,8 @@
|
|||||||
}
|
}
|
||||||
let sidebarOpen = false;
|
let sidebarOpen = false;
|
||||||
let isCompact = false;
|
let isCompact = false;
|
||||||
|
let dismissedUserRouteKey = "";
|
||||||
|
let lastUserRouteKey = "";
|
||||||
let adminLanguageMenuOpen = false;
|
let adminLanguageMenuOpen = false;
|
||||||
let adminLanguageClickGuard = false;
|
let adminLanguageClickGuard = false;
|
||||||
let adminLanguageClickGuardArmed = false;
|
let adminLanguageClickGuardArmed = false;
|
||||||
@@ -379,6 +381,7 @@
|
|||||||
const uid = Number(userId);
|
const uid = Number(userId);
|
||||||
// Synthetic email-only users use negative user_id; still a valid admin target.
|
// Synthetic email-only users use negative user_id; still a valid admin target.
|
||||||
if (!Number.isFinite(uid) || uid === 0) return;
|
if (!Number.isFinite(uid) || uid === 0) return;
|
||||||
|
dismissedUserRouteKey = "";
|
||||||
const next = normalizeSection("payments");
|
const next = normalizeSection("payments");
|
||||||
sidebarOpen = false;
|
sidebarOpen = false;
|
||||||
if (active !== next) {
|
if (active !== next) {
|
||||||
@@ -395,6 +398,7 @@
|
|||||||
function openLogsUserCard(userId) {
|
function openLogsUserCard(userId) {
|
||||||
const uid = Number(userId);
|
const uid = Number(userId);
|
||||||
if (!Number.isFinite(uid) || uid === 0) return;
|
if (!Number.isFinite(uid) || uid === 0) return;
|
||||||
|
dismissedUserRouteKey = "";
|
||||||
const next = normalizeSection("logs");
|
const next = normalizeSection("logs");
|
||||||
sidebarOpen = false;
|
sidebarOpen = false;
|
||||||
if (active !== next) {
|
if (active !== next) {
|
||||||
@@ -410,19 +414,20 @@
|
|||||||
function openUserCard(userId) {
|
function openUserCard(userId) {
|
||||||
const uid = Number(userId);
|
const uid = Number(userId);
|
||||||
if (!Number.isFinite(uid) || uid === 0) return;
|
if (!Number.isFinite(uid) || uid === 0) return;
|
||||||
const next = normalizeSection("users");
|
dismissedUserRouteKey = "";
|
||||||
sidebarOpen = false;
|
sidebarOpen = false;
|
||||||
usersStore.setActive(next);
|
usersStore.setActive(active);
|
||||||
if (active !== next) {
|
usersStore.openUser(uid, { skipPush: true, pathContext: active });
|
||||||
active = next;
|
}
|
||||||
paymentsStore.closePayment({ skipPush: true });
|
|
||||||
supportStore.closeTicketView({ skipPush: true });
|
function userRouteKey(section = active) {
|
||||||
onSectionChange(next, uid);
|
if (section === "users" && initialUserId) return `users:${initialUserId}`;
|
||||||
}
|
if (section === "payments" && initialPaymentUserId) return `payments:${initialPaymentUserId}`;
|
||||||
usersStore.openUser(uid);
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeUserCard() {
|
function closeUserCard() {
|
||||||
|
dismissedUserRouteKey = userRouteKey();
|
||||||
usersStore.closeUser({ skipPush: true });
|
usersStore.closeUser({ skipPush: true });
|
||||||
if (active === "users" || active === "payments") {
|
if (active === "users" || active === "payments") {
|
||||||
onSectionChange(active, 0);
|
onSectionChange(active, 0);
|
||||||
@@ -540,9 +545,18 @@
|
|||||||
$: sectionFade = reduceMotion ? { duration: 0 } : { duration: 200 };
|
$: sectionFade = reduceMotion ? { duration: 0 } : { duration: 200 };
|
||||||
$: sidebarBackdropFade = reduceMotion ? { duration: 0 } : { duration: 180 };
|
$: sidebarBackdropFade = reduceMotion ? { duration: 0 } : { duration: 180 };
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const currentUserRouteKey = userRouteKey();
|
||||||
|
if (currentUserRouteKey !== lastUserRouteKey) {
|
||||||
|
if (currentUserRouteKey !== dismissedUserRouteKey) dismissedUserRouteKey = "";
|
||||||
|
lastUserRouteKey = currentUserRouteKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: if (
|
$: if (
|
||||||
active === "users" &&
|
active === "users" &&
|
||||||
initialUserId &&
|
initialUserId &&
|
||||||
|
dismissedUserRouteKey !== `users:${initialUserId}` &&
|
||||||
(!$usersStore.openedUser || $usersStore.openedUser.user_id !== initialUserId)
|
(!$usersStore.openedUser || $usersStore.openedUser.user_id !== initialUserId)
|
||||||
) {
|
) {
|
||||||
usersStore.openUser(initialUserId, { skipPush: true });
|
usersStore.openUser(initialUserId, { skipPush: true });
|
||||||
@@ -559,6 +573,7 @@
|
|||||||
$: if (
|
$: if (
|
||||||
active === "payments" &&
|
active === "payments" &&
|
||||||
initialPaymentUserId &&
|
initialPaymentUserId &&
|
||||||
|
dismissedUserRouteKey !== `payments:${initialPaymentUserId}` &&
|
||||||
(!$usersStore.openedUser || $usersStore.openedUser.user_id !== initialPaymentUserId)
|
(!$usersStore.openedUser || $usersStore.openedUser.user_id !== initialPaymentUserId)
|
||||||
) {
|
) {
|
||||||
usersStore.openUser(initialPaymentUserId, { skipPush: true, pathContext: "payments" });
|
usersStore.openUser(initialPaymentUserId, { skipPush: true, pathContext: "payments" });
|
||||||
|
|||||||
@@ -54,6 +54,58 @@ export function createUsersStore({ api, onToast, at, routePrefix = "" }) {
|
|||||||
|
|
||||||
let _activeRef = "stats"; // fallback if active isn't tracked
|
let _activeRef = "stats"; // fallback if active isn't tracked
|
||||||
let _pathContext = null;
|
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) {
|
function setActive(active) {
|
||||||
_activeRef = active;
|
_activeRef = active;
|
||||||
@@ -124,31 +176,15 @@ export function createUsersStore({ api, onToast, at, routePrefix = "" }) {
|
|||||||
const userId =
|
const userId =
|
||||||
typeof userOrId === "object" && userOrId !== null ? userOrId.user_id : Number(userOrId);
|
typeof userOrId === "object" && userOrId !== null ? userOrId.user_id : Number(userOrId);
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
const requestId = ++_openUserRequestId;
|
||||||
_setPathContext(opts.pathContext);
|
_setPathContext(opts.pathContext);
|
||||||
|
const openedUser =
|
||||||
|
typeof userOrId === "object" && userOrId !== null ? userOrId : { user_id: userId };
|
||||||
|
|
||||||
state.update((s) => ({
|
state.update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
openedUser:
|
..._openingUserModalState(openedUser, userId),
|
||||||
typeof userOrId === "object" && userOrId !== null ? userOrId : { user_id: userId },
|
userActionBusy: s.userActionBusy,
|
||||||
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,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!opts.skipPush) _pushUserPath(userId);
|
if (!opts.skipPush) _pushUserPath(userId);
|
||||||
@@ -161,54 +197,52 @@ export function createUsersStore({ api, onToast, at, routePrefix = "" }) {
|
|||||||
const hasHwidLimit =
|
const hasHwidLimit =
|
||||||
sub?.hwid_device_limit !== null && sub?.hwid_device_limit !== undefined;
|
sub?.hwid_device_limit !== null && sub?.hwid_device_limit !== undefined;
|
||||||
const hwidLimit = hasHwidLimit ? Number(sub?.hwid_device_limit) : null;
|
const hwidLimit = hasHwidLimit ? Number(sub?.hwid_device_limit) : null;
|
||||||
state.update((s) => ({
|
state.update((s) => {
|
||||||
...s,
|
if (!_isCurrentUserRequest(s, requestId, userId)) return s;
|
||||||
openedUserDetail: res,
|
return {
|
||||||
openedUser: res.user ? { ...res.user, ...s.openedUser, ...res.user } : s.openedUser,
|
...s,
|
||||||
premiumUnlimitedDraft: Boolean(sub?.premium_unlimited_override),
|
openedUserDetail: res,
|
||||||
premiumBonusGbDraft: bonusBytes > 0 ? +(bonusBytes / 1024 ** 3).toFixed(2) : "",
|
openedUser: res.user ? { ...res.user, ...s.openedUser, ...res.user } : s.openedUser,
|
||||||
regularUnlimitedDraft: Boolean(sub?.regular_unlimited_override),
|
premiumUnlimitedDraft: Boolean(sub?.premium_unlimited_override),
|
||||||
regularBonusGbDraft:
|
premiumBonusGbDraft: bonusBytes > 0 ? +(bonusBytes / 1024 ** 3).toFixed(2) : "",
|
||||||
regularBonusBytes > 0 ? +(regularBonusBytes / 1024 ** 3).toFixed(2) : "",
|
regularUnlimitedDraft: Boolean(sub?.regular_unlimited_override),
|
||||||
hwidUnlimitedDraft: hasHwidLimit && hwidLimit === 0,
|
regularBonusGbDraft:
|
||||||
hwidDeviceLimitDraft: hasHwidLimit && hwidLimit > 0 ? String(hwidLimit) : "",
|
regularBonusBytes > 0 ? +(regularBonusBytes / 1024 ** 3).toFixed(2) : "",
|
||||||
grantTrafficGbDraft: "",
|
hwidUnlimitedDraft: hasHwidLimit && hwidLimit === 0,
|
||||||
grantTrafficKindDraft: "regular",
|
hwidDeviceLimitDraft: hasHwidLimit && hwidLimit > 0 ? String(hwidLimit) : "",
|
||||||
}));
|
grantTrafficGbDraft: "",
|
||||||
|
grantTrafficKindDraft: "regular",
|
||||||
|
};
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
onToast(res?.error || "load_failed");
|
let shouldClearPath = false;
|
||||||
state.update((s) => ({ ...s, openedUser: null }));
|
let shouldShowError = false;
|
||||||
if (!opts.skipPush) _pushUserPath(null);
|
state.update((s) => {
|
||||||
_pathContext = null;
|
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 {
|
} finally {
|
||||||
state.update((s) => ({ ...s, userDetailLoading: false }));
|
state.update((s) => {
|
||||||
|
if (!_isCurrentUserRequest(s, requestId, userId)) return s;
|
||||||
|
return { ...s, userDetailLoading: false };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeUser(opts = {}) {
|
function closeUser(opts = {}) {
|
||||||
let wasOpen = false;
|
let wasOpen = false;
|
||||||
|
_openUserRequestId += 1;
|
||||||
state.update((s) => {
|
state.update((s) => {
|
||||||
wasOpen = Boolean(s.openedUser);
|
wasOpen = Boolean(s.openedUser);
|
||||||
return {
|
return {
|
||||||
...s,
|
...s,
|
||||||
openedUser: null,
|
..._closedUserModalState(),
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (wasOpen && !opts.skipPush) _pushUserPath(null);
|
if (wasOpen && !opts.skipPush) _pushUserPath(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user