refactor: support prefixed webapp routes
This commit is contained in:
+98
-49
@@ -101,6 +101,7 @@
|
|||||||
sectionFromPath,
|
sectionFromPath,
|
||||||
supportTicketIdFromPath,
|
supportTicketIdFromPath,
|
||||||
syncSectionPath,
|
syncSectionPath,
|
||||||
|
withRoutePrefix,
|
||||||
} from "./lib/webapp/routes.js";
|
} from "./lib/webapp/routes.js";
|
||||||
|
|
||||||
export let mockRuntime = null;
|
export let mockRuntime = null;
|
||||||
@@ -125,6 +126,8 @@
|
|||||||
const MOCK_SOURCE = mockRuntime?.source || EMPTY_MOCK;
|
const MOCK_SOURCE = mockRuntime?.source || EMPTY_MOCK;
|
||||||
const previewBoardComponent = mockRuntime?.PreviewBoard || null;
|
const previewBoardComponent = mockRuntime?.PreviewBoard || null;
|
||||||
const isDocsDemo = mockRuntime?.docsDemo === true;
|
const isDocsDemo = mockRuntime?.docsDemo === true;
|
||||||
|
const routePrefix = isDocsDemo ? "/demo/runtime" : "";
|
||||||
|
let docsDemoParentRouteConsumed = false;
|
||||||
const query = new URLSearchParams(window.location.search);
|
const query = new URLSearchParams(window.location.search);
|
||||||
const isAppLaunchRoute = isExternalAppLaunchPath(window.location.pathname);
|
const isAppLaunchRoute = isExternalAppLaunchPath(window.location.pathname);
|
||||||
mockRuntime?.applyPreviewMock?.(query.get("mock"));
|
mockRuntime?.applyPreviewMock?.(query.get("mock"));
|
||||||
@@ -257,7 +260,7 @@
|
|||||||
telegramSdk,
|
telegramSdk,
|
||||||
});
|
});
|
||||||
const devicesStore = createDevicesStore({ api, t, showToast });
|
const devicesStore = createDevicesStore({ api, t, showToast });
|
||||||
const supportStore = createSupportStore({ api, t, showToast });
|
const supportStore = createSupportStore({ api, t, showToast, routePrefix });
|
||||||
const installGuidesStore = createInstallGuidesStore({ api, t, showToast });
|
const installGuidesStore = createInstallGuidesStore({ api, t, showToast });
|
||||||
const accountStore = createAccountStore({
|
const accountStore = createAccountStore({
|
||||||
api,
|
api,
|
||||||
@@ -773,7 +776,7 @@
|
|||||||
const section =
|
const section =
|
||||||
isDocsDemo && currentQuery.get("screen")
|
isDocsDemo && currentQuery.get("screen")
|
||||||
? normalizeSection(currentQuery.get("screen"))
|
? normalizeSection(currentQuery.get("screen"))
|
||||||
: sectionFromPath(window.location.pathname);
|
: sectionFromPath(routePathnameFromLocation(), routePrefix);
|
||||||
if (mode === "login") {
|
if (mode === "login") {
|
||||||
setPasswordLoginMode(isPasswordLoginPath(), true);
|
setPasswordLoginMode(isPasswordLoginPath(), true);
|
||||||
screen = "login";
|
screen = "login";
|
||||||
@@ -783,11 +786,11 @@
|
|||||||
if (section === "admin" && isAdmin) {
|
if (section === "admin" && isAdmin) {
|
||||||
adminActiveSection = isDocsDemo
|
adminActiveSection = isDocsDemo
|
||||||
? initialAdminSectionFromLocation()
|
? initialAdminSectionFromLocation()
|
||||||
: adminSectionFromPath(window.location.pathname);
|
: adminSectionFromPath(routePathnameFromLocation(), routePrefix);
|
||||||
const pathAtStart = window.location.pathname;
|
const pathAtStart = window.location.pathname;
|
||||||
void Promise.all([ensureI18nScope("admin"), ensureAdminBundle()])
|
void Promise.all([ensureI18nScope("admin"), ensureAdminBundle()])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (sectionFromPath(window.location.pathname) !== "admin") return;
|
if (sectionFromPath(routePathnameFromLocation(), routePrefix) !== "admin") return;
|
||||||
if (window.location.pathname !== pathAtStart) return;
|
if (window.location.pathname !== pathAtStart) return;
|
||||||
activeTab = "settings";
|
activeTab = "settings";
|
||||||
screen = "admin";
|
screen = "admin";
|
||||||
@@ -1038,39 +1041,95 @@
|
|||||||
return new URLSearchParams(window.location.search);
|
return new URLSearchParams(window.location.search);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function docsDemoParentSearchParams() {
|
||||||
|
if (!isDocsDemo) return null;
|
||||||
|
try {
|
||||||
|
if (window.parent === window) return null;
|
||||||
|
return new URLSearchParams(window.parent.location.search);
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDemoRoutePath(value) {
|
||||||
|
const raw = String(value || "").trim();
|
||||||
|
if (!raw) return "";
|
||||||
|
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
||||||
|
return withSlash.replace(/\/{2,}/g, "/").replace(/\/+$/, "") || "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function docsDemoRouteParams() {
|
||||||
|
if (!isDocsDemo) return null;
|
||||||
|
const currentQuery = currentSearchParams();
|
||||||
|
const currentParams = {
|
||||||
|
path: currentQuery.get("path") || "",
|
||||||
|
screen: currentQuery.get("screen") || "",
|
||||||
|
adminSection: currentQuery.get("admin_section") || "",
|
||||||
|
};
|
||||||
|
if (currentParams.path || currentParams.screen || currentParams.adminSection) {
|
||||||
|
return currentParams;
|
||||||
|
}
|
||||||
|
if (docsDemoParentRouteConsumed) return currentParams;
|
||||||
|
const parentQuery = docsDemoParentSearchParams();
|
||||||
|
return {
|
||||||
|
path: parentQuery?.get("path") || "",
|
||||||
|
screen: parentQuery?.get("screen") || "",
|
||||||
|
adminSection: parentQuery?.get("admin_section") || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function docsDemoRoutePathFromParams() {
|
||||||
|
const params = docsDemoRouteParams();
|
||||||
|
if (!params) return "";
|
||||||
|
const explicitPath = normalizeDemoRoutePath(params.path);
|
||||||
|
if (explicitPath) return explicitPath;
|
||||||
|
const section = normalizeSection(params.screen);
|
||||||
|
if (section === "admin") {
|
||||||
|
return `/admin/${normalizeAdminSection(params.adminSection || "stats")}`;
|
||||||
|
}
|
||||||
|
return params.screen ? `/${section}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function routePathnameFromLocation() {
|
||||||
|
return docsDemoRoutePathFromParams() || window.location.pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanDocsDemoRouteQuery() {
|
||||||
|
if (!isDocsDemo || window.location.protocol === "file:") return;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const routeKeys = ["path", "screen", "admin_section"];
|
||||||
|
const changed = routeKeys.some((key) => url.searchParams.has(key));
|
||||||
|
if (!changed) return;
|
||||||
|
for (const key of routeKeys) url.searchParams.delete(key);
|
||||||
|
const search = url.searchParams.toString();
|
||||||
|
window.history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
`${url.pathname}${search ? `?${search}` : ""}${url.hash}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function initialAdminSectionFromLocation() {
|
function initialAdminSectionFromLocation() {
|
||||||
const currentQuery = currentSearchParams();
|
const currentQuery = currentSearchParams();
|
||||||
if (MOCK && currentQuery.get("admin_section")) {
|
if (MOCK && currentQuery.get("admin_section")) {
|
||||||
return normalizeAdminSection(currentQuery.get("admin_section"));
|
return normalizeAdminSection(currentQuery.get("admin_section"));
|
||||||
}
|
}
|
||||||
return adminSectionFromPath(window.location.pathname);
|
const demoRouteParams = docsDemoRouteParams();
|
||||||
|
if (MOCK && demoRouteParams?.adminSection) {
|
||||||
|
return normalizeAdminSection(demoRouteParams.adminSection);
|
||||||
|
}
|
||||||
|
return adminSectionFromPath(routePathnameFromLocation(), routePrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncDocsDemoSection(section, replace = false, adminSection = null) {
|
function syncDocsDemoSection(section, replace = false, adminSection = null, adminUserId = null) {
|
||||||
if (!isDocsDemo || window.location.protocol === "file:") return false;
|
if (!isDocsDemo || window.location.protocol === "file:") return false;
|
||||||
const normalized = normalizeSection(section);
|
syncSectionPath(section, replace, adminSection, adminUserId, routePrefix);
|
||||||
const currentQuery = currentSearchParams();
|
cleanDocsDemoRouteQuery();
|
||||||
currentQuery.set("screen", normalized);
|
|
||||||
if (normalized === "admin") {
|
|
||||||
currentQuery.set(
|
|
||||||
"admin_section",
|
|
||||||
normalizeAdminSection(
|
|
||||||
adminSection || adminActiveSection || initialAdminSectionFromLocation()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
currentQuery.delete("admin_section");
|
|
||||||
}
|
|
||||||
const nextSearch = currentQuery.toString();
|
|
||||||
const nextUrl = `${window.location.pathname}${nextSearch ? `?${nextSearch}` : ""}${window.location.hash}`;
|
|
||||||
if (nextUrl !== `${window.location.pathname}${window.location.search}${window.location.hash}`) {
|
|
||||||
window.history[replace ? "replaceState" : "pushState"](null, "", nextUrl);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncAppSectionPath(section, replace = false, adminSection = null, adminUserId = null) {
|
function syncAppSectionPath(section, replace = false, adminSection = null, adminUserId = null) {
|
||||||
if (syncDocsDemoSection(section, replace, adminSection)) return;
|
if (syncDocsDemoSection(section, replace, adminSection, adminUserId)) return;
|
||||||
syncSectionPath(section, replace, adminSection, adminUserId);
|
syncSectionPath(section, replace, adminSection, adminUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,14 +1138,15 @@
|
|||||||
onClose: closeAdminPanel,
|
onClose: closeAdminPanel,
|
||||||
onToast: (text) => showToast(text),
|
onToast: (text) => showToast(text),
|
||||||
initialSection: screen === "admin" ? adminActiveSection : initialAdminSectionFromLocation(),
|
initialSection: screen === "admin" ? adminActiveSection : initialAdminSectionFromLocation(),
|
||||||
initialPaymentId: adminPaymentIdFromPath(window.location.pathname),
|
initialPaymentId: adminPaymentIdFromPath(routePathnameFromLocation(), routePrefix),
|
||||||
initialPaymentUserId: adminPaymentsUserIdFromPath(window.location.pathname),
|
initialPaymentUserId: adminPaymentsUserIdFromPath(routePathnameFromLocation(), routePrefix),
|
||||||
initialUserId: adminUserIdFromPath(window.location.pathname),
|
initialUserId: adminUserIdFromPath(routePathnameFromLocation(), routePrefix),
|
||||||
onSectionChange: handleAdminSectionChange,
|
onSectionChange: handleAdminSectionChange,
|
||||||
onSettingsSaved: handleAdminPersistedSaved,
|
onSettingsSaved: handleAdminPersistedSaved,
|
||||||
onTariffsSaved: handleAdminPersistedSaved,
|
onTariffsSaved: handleAdminPersistedSaved,
|
||||||
onThemesSaved: handleAdminPersistedSaved,
|
onThemesSaved: handleAdminPersistedSaved,
|
||||||
onTranslationsSaved: handleAdminTranslationsSaved,
|
onTranslationsSaved: handleAdminTranslationsSaved,
|
||||||
|
routePrefix,
|
||||||
brandTitle,
|
brandTitle,
|
||||||
brand,
|
brand,
|
||||||
appFaviconUrl: CFG.faviconUrl,
|
appFaviconUrl: CFG.faviconUrl,
|
||||||
@@ -1231,7 +1291,7 @@
|
|||||||
? preservedSection
|
? preservedSection
|
||||||
: MOCK && currentQuery.get("screen")
|
: MOCK && currentQuery.get("screen")
|
||||||
? normalizeSection(currentQuery.get("screen"))
|
? normalizeSection(currentQuery.get("screen"))
|
||||||
: sectionFromPath(window.location.pathname);
|
: sectionFromPath(routePathnameFromLocation(), routePrefix);
|
||||||
if (section === "admin" && !payload.user?.is_admin) section = "settings";
|
if (section === "admin" && !payload.user?.is_admin) section = "settings";
|
||||||
if (section === "devices" && !payload.settings?.my_devices_enabled) section = "home";
|
if (section === "devices" && !payload.settings?.my_devices_enabled) section = "home";
|
||||||
if (section === "support" && payload.settings?.support_tickets_enabled === false) {
|
if (section === "support" && payload.settings?.support_tickets_enabled === false) {
|
||||||
@@ -1258,7 +1318,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const initialSupportTicketId =
|
const initialSupportTicketId =
|
||||||
section === "support" ? supportTicketIdFromPath(window.location.pathname) : null;
|
section === "support"
|
||||||
|
? supportTicketIdFromPath(routePathnameFromLocation(), routePrefix)
|
||||||
|
: null;
|
||||||
|
if (isDocsDemo) docsDemoParentRouteConsumed = true;
|
||||||
activeTab =
|
activeTab =
|
||||||
section === "admin"
|
section === "admin"
|
||||||
? "settings"
|
? "settings"
|
||||||
@@ -1275,10 +1338,8 @@
|
|||||||
}
|
}
|
||||||
supportStore.startPolling({ includeList: false });
|
supportStore.startPolling({ includeList: false });
|
||||||
}
|
}
|
||||||
if (isDocsDemo) {
|
if (section === "support" && initialSupportTicketId) {
|
||||||
// The docs demo is a static iframe route; keep query params as its navigation contract.
|
const targetPath = withRoutePrefix(`/support/${initialSupportTicketId}`, routePrefix);
|
||||||
} else if (section === "support" && initialSupportTicketId) {
|
|
||||||
const targetPath = `/support/${initialSupportTicketId}`;
|
|
||||||
if (window.location.protocol !== "file:" && window.location.pathname !== targetPath) {
|
if (window.location.protocol !== "file:" && window.location.pathname !== targetPath) {
|
||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
null,
|
null,
|
||||||
@@ -1286,6 +1347,7 @@
|
|||||||
`${targetPath}${window.location.search}${window.location.hash}`
|
`${targetPath}${window.location.search}${window.location.hash}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
cleanDocsDemoRouteQuery();
|
||||||
} else {
|
} else {
|
||||||
syncAppSectionPath(section, true, initialAdminSection);
|
syncAppSectionPath(section, true, initialAdminSection);
|
||||||
}
|
}
|
||||||
@@ -1710,7 +1772,7 @@
|
|||||||
clearLanguageClickGuard();
|
clearLanguageClickGuard();
|
||||||
billingStore.closePaymentModal();
|
billingStore.closePaymentModal();
|
||||||
const nextAdminSection = normalizeAdminSection(
|
const nextAdminSection = normalizeAdminSection(
|
||||||
adminActiveSection || adminSectionFromPath(window.location.pathname)
|
adminActiveSection || adminSectionFromPath(routePathnameFromLocation(), routePrefix)
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await ensureI18nScope("admin");
|
await ensureI18nScope("admin");
|
||||||
@@ -1737,20 +1799,7 @@
|
|||||||
const nextAdminSection = normalizeAdminSection(adminSection);
|
const nextAdminSection = normalizeAdminSection(adminSection);
|
||||||
adminActiveSection = nextAdminSection;
|
adminActiveSection = nextAdminSection;
|
||||||
if (window.location.protocol === "file:") return;
|
if (window.location.protocol === "file:") return;
|
||||||
if (isDocsDemo) {
|
syncAppSectionPath("admin", false, nextAdminSection, adminUserId);
|
||||||
syncDocsDemoSection("admin", false, nextAdminSection);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const targetPath =
|
|
||||||
nextAdminSection === "users" && adminUserId
|
|
||||||
? `/admin/users/${adminUserId}`
|
|
||||||
: `/admin/${nextAdminSection}`;
|
|
||||||
if (window.location.pathname === targetPath) return;
|
|
||||||
window.history.pushState(
|
|
||||||
null,
|
|
||||||
"",
|
|
||||||
`${targetPath}${window.location.search}${window.location.hash}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function adminPayloadHasLogoChange(options = {}) {
|
function adminPayloadHasLogoChange(options = {}) {
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
userTelegramProfileLink,
|
userTelegramProfileLink,
|
||||||
userTelegramProfileLinkKind,
|
userTelegramProfileLinkKind,
|
||||||
} from "../lib/admin/users.js";
|
} from "../lib/admin/users.js";
|
||||||
|
import { stripRoutePrefix } from "../lib/webapp/routes.js";
|
||||||
|
|
||||||
export let api;
|
export let api;
|
||||||
export let onClose = () => {};
|
export let onClose = () => {};
|
||||||
@@ -90,6 +91,7 @@
|
|||||||
export let onTariffsSaved = () => {};
|
export let onTariffsSaved = () => {};
|
||||||
export let onThemesSaved = () => {};
|
export let onThemesSaved = () => {};
|
||||||
export let onTranslationsSaved = () => {};
|
export let onTranslationsSaved = () => {};
|
||||||
|
export let routePrefix = "";
|
||||||
export let brand = {};
|
export let brand = {};
|
||||||
export let brandTitle = "/minishop";
|
export let brandTitle = "/minishop";
|
||||||
export let appFaviconUrl = "";
|
export let appFaviconUrl = "";
|
||||||
@@ -243,15 +245,15 @@
|
|||||||
const backupsStore = createBackupsStore({ api, onToast: flash, at });
|
const backupsStore = createBackupsStore({ api, onToast: flash, at });
|
||||||
const broadcastStore = createBroadcastStore({ api, onToast: flash, at });
|
const broadcastStore = createBroadcastStore({ api, onToast: flash, at });
|
||||||
const logsStore = createLogsStore({ api, at });
|
const logsStore = createLogsStore({ api, at });
|
||||||
const paymentsStore = createPaymentsStore({ api, onToast: flash, at });
|
const paymentsStore = createPaymentsStore({ api, onToast: flash, at, routePrefix });
|
||||||
const promosStore = createPromosStore({ api, onToast: flash, at });
|
const promosStore = createPromosStore({ api, onToast: flash, at });
|
||||||
const settingsStore = createSettingsStore({ api, onToast: flash, at });
|
const settingsStore = createSettingsStore({ api, onToast: flash, at });
|
||||||
const statsStore = createStatsStore({ api, onToast: flash, at });
|
const statsStore = createStatsStore({ api, onToast: flash, at });
|
||||||
const supportStore = createAdminSupportStore({ api, onToast: flash, at });
|
const supportStore = createAdminSupportStore({ api, onToast: flash, at, routePrefix });
|
||||||
const tariffsStore = createTariffsStore({ api, onToast: flash, onTariffsSaved, flash, at });
|
const tariffsStore = createTariffsStore({ api, onToast: flash, onTariffsSaved, flash, at });
|
||||||
const themesStore = createThemesStore({ api, onThemesSaved, flash, at });
|
const themesStore = createThemesStore({ api, onThemesSaved, flash, at });
|
||||||
const translationsStore = createTranslationsStore({ api, onToast: flash, at });
|
const translationsStore = createTranslationsStore({ api, onToast: flash, at });
|
||||||
const usersStore = createUsersStore({ api, onToast: flash, at });
|
const usersStore = createUsersStore({ api, onToast: flash, at, routePrefix });
|
||||||
|
|
||||||
setContext("promosStore", promosStore);
|
setContext("promosStore", promosStore);
|
||||||
setContext("adsStore", adsStore);
|
setContext("adsStore", adsStore);
|
||||||
@@ -296,33 +298,38 @@
|
|||||||
onLanguageChange(value, { section: "admin", adminSection: active });
|
onLanguageChange(value, { section: "admin", adminSection: active });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentRoutePathname() {
|
||||||
|
if (typeof window === "undefined") return "/";
|
||||||
|
return stripRoutePrefix(window.location.pathname, routePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
function readSectionFromPath() {
|
function readSectionFromPath() {
|
||||||
if (typeof window === "undefined") return "stats";
|
if (typeof window === "undefined") return "stats";
|
||||||
const match = window.location.pathname.match(/^\/admin\/([a-z0-9_-]+)(?:\/.*)?$/i);
|
const match = currentRoutePathname().match(/^\/admin\/([a-z0-9_-]+)(?:\/.*)?$/i);
|
||||||
return normalizeSection(match ? match[1].toLowerCase() : "stats");
|
return normalizeSection(match ? match[1].toLowerCase() : "stats");
|
||||||
}
|
}
|
||||||
|
|
||||||
function readUserIdFromPath() {
|
function readUserIdFromPath() {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
const match = window.location.pathname.match(/^\/admin\/users\/(-?\d+)$/);
|
const match = currentRoutePathname().match(/^\/admin\/users\/(-?\d+)$/);
|
||||||
return match ? Number(match[1]) : null;
|
return match ? Number(match[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSupportTicketIdFromPath() {
|
function readSupportTicketIdFromPath() {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
const match = window.location.pathname.match(/^\/admin\/support\/(\d+)$/);
|
const match = currentRoutePathname().match(/^\/admin\/support\/(\d+)$/);
|
||||||
return match ? Number(match[1]) : null;
|
return match ? Number(match[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPaymentIdFromPath() {
|
function readPaymentIdFromPath() {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
const match = window.location.pathname.match(/^\/admin\/payments\/(\d+)$/);
|
const match = currentRoutePathname().match(/^\/admin\/payments\/(\d+)$/);
|
||||||
return match ? Number(match[1]) : null;
|
return match ? Number(match[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPaymentUserIdFromPath() {
|
function readPaymentUserIdFromPath() {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
const match = window.location.pathname.match(/^\/admin\/payments\/users\/(-?\d+)$/);
|
const match = currentRoutePathname().match(/^\/admin\/payments\/users\/(-?\d+)$/);
|
||||||
return match ? Number(match[1]) : null;
|
return match ? Number(match[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,10 +384,26 @@
|
|||||||
paymentsStore.closePayment({ skipPush: true });
|
paymentsStore.closePayment({ skipPush: true });
|
||||||
onSectionChange(next);
|
onSectionChange(next);
|
||||||
}
|
}
|
||||||
|
usersStore.setActive(next);
|
||||||
paymentsStore.closePayment({ skipPush: true });
|
paymentsStore.closePayment({ skipPush: true });
|
||||||
usersStore.openUser(uid, { pathContext: "payments" });
|
usersStore.openUser(uid, { pathContext: "payments" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openUserCard(userId) {
|
||||||
|
const uid = Number(userId);
|
||||||
|
if (!Number.isFinite(uid) || uid === 0) return;
|
||||||
|
const next = normalizeSection("users");
|
||||||
|
sidebarOpen = false;
|
||||||
|
if (active !== next) {
|
||||||
|
active = next;
|
||||||
|
paymentsStore.closePayment({ skipPush: true });
|
||||||
|
supportStore.closeTicketView({ skipPush: true });
|
||||||
|
onSectionChange(next, uid);
|
||||||
|
}
|
||||||
|
usersStore.setActive(next);
|
||||||
|
usersStore.openUser(uid);
|
||||||
|
}
|
||||||
|
|
||||||
function resolvedAvatarUrl(user) {
|
function resolvedAvatarUrl(user) {
|
||||||
return userAvatarUrl(user) || (user?.email ? gravatarCache.gravatarUrl(user.email) : "");
|
return userAvatarUrl(user) || (user?.email ? gravatarCache.gravatarUrl(user.email) : "");
|
||||||
}
|
}
|
||||||
@@ -786,6 +809,7 @@
|
|||||||
{at}
|
{at}
|
||||||
{brand}
|
{brand}
|
||||||
{resolvedAvatarUrl}
|
{resolvedAvatarUrl}
|
||||||
|
onOpenUserCard={openUserCard}
|
||||||
initialTicketId={readSupportTicketIdFromPath()}
|
initialTicketId={readSupportTicketIdFromPath()}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
export let initialTicketId = null;
|
export let initialTicketId = null;
|
||||||
export let brand = {};
|
export let brand = {};
|
||||||
export let resolvedAvatarUrl = () => "";
|
export let resolvedAvatarUrl = () => "";
|
||||||
|
export let onOpenUserCard = () => {};
|
||||||
|
|
||||||
const supportStore = getContext("adminSupportStore");
|
const supportStore = getContext("adminSupportStore");
|
||||||
let reply = "";
|
let reply = "";
|
||||||
@@ -306,7 +307,12 @@
|
|||||||
onPatch={(updates) => supportStore.patchTicket(updates)}
|
onPatch={(updates) => supportStore.patchTicket(updates)}
|
||||||
onClose={() => supportStore.closeTicket()}
|
onClose={() => supportStore.closeTicket()}
|
||||||
/>
|
/>
|
||||||
<SupportUserContextPanel ticket={openedTicket} snapshot={userSnapshot} {at} />
|
<SupportUserContextPanel
|
||||||
|
ticket={openedTicket}
|
||||||
|
snapshot={userSnapshot}
|
||||||
|
{at}
|
||||||
|
onOpenUser={onOpenUserCard}
|
||||||
|
/>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
bind:element={messagesScrollEl}
|
bind:element={messagesScrollEl}
|
||||||
maxHeight="none"
|
maxHeight="none"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import { withRoutePrefix } from "../../webapp/routes.js";
|
||||||
|
|
||||||
export function createPaymentsStore({
|
export function createPaymentsStore({
|
||||||
api,
|
api,
|
||||||
onToast = () => {},
|
onToast = () => {},
|
||||||
at = (key, _params, fallback) => fallback || key,
|
at = (key, _params, fallback) => fallback || key,
|
||||||
|
routePrefix = "",
|
||||||
}) {
|
}) {
|
||||||
const state = writable({
|
const state = writable({
|
||||||
payments: [],
|
payments: [],
|
||||||
@@ -25,7 +27,10 @@ export function createPaymentsStore({
|
|||||||
function pushPaymentPath(paymentId) {
|
function pushPaymentPath(paymentId) {
|
||||||
if (typeof window === "undefined" || window.location.protocol === "file:") return;
|
if (typeof window === "undefined" || window.location.protocol === "file:") return;
|
||||||
if (active !== "payments") return;
|
if (active !== "payments") return;
|
||||||
const target = paymentId ? `/admin/payments/${paymentId}` : "/admin/payments";
|
const target = withRoutePrefix(
|
||||||
|
paymentId ? `/admin/payments/${paymentId}` : "/admin/payments",
|
||||||
|
routePrefix
|
||||||
|
);
|
||||||
if (window.location.pathname === target) return;
|
if (window.location.pathname === target) return;
|
||||||
window.history.pushState(null, "", `${target}${window.location.search}${window.location.hash}`);
|
window.history.pushState(null, "", `${target}${window.location.search}${window.location.hash}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import { withRoutePrefix } from "../../webapp/routes.js";
|
||||||
|
|
||||||
export function createAdminSupportStore({ api, onToast, at }) {
|
export function createAdminSupportStore({ api, onToast, at, routePrefix = "" }) {
|
||||||
const OPEN_TICKET_POLL_MS = 3_000;
|
const OPEN_TICKET_POLL_MS = 3_000;
|
||||||
const STATS_POLL_MS = 30_000;
|
const STATS_POLL_MS = 30_000;
|
||||||
const HIDDEN_POLL_MS = 300_000;
|
const HIDDEN_POLL_MS = 300_000;
|
||||||
@@ -58,7 +59,10 @@ export function createAdminSupportStore({ api, onToast, at }) {
|
|||||||
function pushTicketPath(ticketId) {
|
function pushTicketPath(ticketId) {
|
||||||
if (typeof window === "undefined" || window.location.protocol === "file:") return;
|
if (typeof window === "undefined" || window.location.protocol === "file:") return;
|
||||||
if (active !== "support") return;
|
if (active !== "support") return;
|
||||||
const target = ticketId ? `/admin/support/${ticketId}` : "/admin/support";
|
const target = withRoutePrefix(
|
||||||
|
ticketId ? `/admin/support/${ticketId}` : "/admin/support",
|
||||||
|
routePrefix
|
||||||
|
);
|
||||||
if (window.location.pathname !== target) {
|
if (window.location.pathname !== target) {
|
||||||
window.history.pushState(
|
window.history.pushState(
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import { withRoutePrefix } from "../../webapp/routes.js";
|
||||||
|
|
||||||
export function createUsersStore({ api, onToast, at }) {
|
export function createUsersStore({ api, onToast, at, routePrefix = "" }) {
|
||||||
const USERS_PAGE_SIZE = 25;
|
const USERS_PAGE_SIZE = 25;
|
||||||
const USER_LOGS_PAGE_SIZE = 20;
|
const USER_LOGS_PAGE_SIZE = 20;
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ export function createUsersStore({ api, onToast, at }) {
|
|||||||
target = userId ? `/admin/payments/users/${userId}` : `/admin/payments`;
|
target = userId ? `/admin/payments/users/${userId}` : `/admin/payments`;
|
||||||
}
|
}
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
target = withRoutePrefix(target, routePrefix);
|
||||||
if (window.location.pathname === target) return;
|
if (window.location.pathname === target) return;
|
||||||
window.history.pushState(null, "", `${target}${window.location.search}${window.location.hash}`);
|
window.history.pushState(null, "", `${target}${window.location.search}${window.location.hash}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
export let ticket;
|
export let ticket;
|
||||||
export let snapshot = {};
|
export let snapshot = {};
|
||||||
export let at = (key) => key;
|
export let at = (key) => key;
|
||||||
|
export let onOpenUser = () => {};
|
||||||
|
|
||||||
$: user = ticket?.user || {};
|
$: user = ticket?.user || {};
|
||||||
$: displayName = snapshot?.name || user.username || user.email || user.user_id || "-";
|
$: displayName = snapshot?.name || user.username || user.email || user.user_id || "-";
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
disabled={!canOpenUser}
|
disabled={!canOpenUser}
|
||||||
onclick={() => (window.location.href = `/admin/users/${user.user_id}`)}
|
onclick={() => onOpenUser(user.user_id)}
|
||||||
aria-label={at("support_open_user", {}, "Карточка")}
|
aria-label={at("support_open_user", {}, "Карточка")}
|
||||||
title={at("support_open_user", {}, "Карточка")}
|
title={at("support_open_user", {}, "Карточка")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,15 +25,41 @@ export function normalizeAdminSection(value) {
|
|||||||
return ADMIN_SECTIONS.has(section) ? section : "stats";
|
return ADMIN_SECTIONS.has(section) ? section : "stats";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sectionFromPath(pathname) {
|
function normalizePathname(pathname) {
|
||||||
|
const normalized = String(pathname || "")
|
||||||
|
.trim()
|
||||||
|
.replace(/\/+$/, "");
|
||||||
|
return normalized || "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripRoutePrefix(pathname, routePrefix = "") {
|
||||||
|
const path = normalizePathname(pathname);
|
||||||
|
const prefix = normalizePathname(routePrefix);
|
||||||
|
if (prefix === "/") return path;
|
||||||
|
if (path.toLowerCase() === prefix.toLowerCase()) return "/";
|
||||||
|
if (path.toLowerCase().startsWith(`${prefix.toLowerCase()}/`)) {
|
||||||
|
return path.slice(prefix.length) || "/";
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withRoutePrefix(pathname, routePrefix = "") {
|
||||||
|
const path = normalizePathname(pathname);
|
||||||
|
const prefix = normalizePathname(routePrefix);
|
||||||
|
if (prefix === "/") return path;
|
||||||
|
if (path === "/") return prefix;
|
||||||
|
return `${prefix}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sectionFromPath(pathname, routePrefix = "") {
|
||||||
const normalizedPath = String(pathname || "")
|
const normalizedPath = String(pathname || "")
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\/+$/, "");
|
.replace(/\/+$/, "");
|
||||||
if (!normalizedPath || normalizedPath === "/") return "home";
|
const routePath = stripRoutePrefix(normalizedPath, routePrefix).toLowerCase().replace(/\/+$/, "");
|
||||||
if (normalizedPath === "/admin" || normalizedPath.startsWith("/admin/")) return "admin";
|
if (!routePath || routePath === "/") return "home";
|
||||||
if (normalizedPath === "/support" || normalizedPath.startsWith("/support/")) return "support";
|
if (routePath === "/admin" || routePath.startsWith("/admin/")) return "admin";
|
||||||
const section = normalizedPath.startsWith("/") ? normalizedPath.slice(1) : normalizedPath;
|
if (routePath === "/support" || routePath.startsWith("/support/")) return "support";
|
||||||
|
const section = routePath.startsWith("/") ? routePath.slice(1) : routePath;
|
||||||
return normalizeSection(section);
|
return normalizeSection(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,67 +71,68 @@ export function publicInstallTokenFromPath(pathname) {
|
|||||||
return match ? match[1].toLowerCase() : "";
|
return match ? match[1].toLowerCase() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adminSectionFromPath(pathname) {
|
export function adminSectionFromPath(pathname, routePrefix = "") {
|
||||||
const normalized = String(pathname || "")
|
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\/+$/, "");
|
|
||||||
const m = normalized.match(/^\/admin\/([a-z0-9_-]+)(?:\/.*)?$/);
|
const m = normalized.match(/^\/admin\/([a-z0-9_-]+)(?:\/.*)?$/);
|
||||||
return normalizeAdminSection(m ? m[1] : "");
|
return normalizeAdminSection(m ? m[1] : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adminUserIdFromPath(pathname) {
|
export function adminUserIdFromPath(pathname, routePrefix = "") {
|
||||||
const normalized = String(pathname || "")
|
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\/+$/, "");
|
|
||||||
const m = normalized.match(/^\/admin\/users\/(-?\d+)$/);
|
const m = normalized.match(/^\/admin\/users\/(-?\d+)$/);
|
||||||
return m ? Number(m[1]) : null;
|
return m ? Number(m[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adminPaymentIdFromPath(pathname) {
|
export function adminPaymentIdFromPath(pathname, routePrefix = "") {
|
||||||
const normalized = String(pathname || "")
|
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\/+$/, "");
|
|
||||||
const m = normalized.match(/^\/admin\/payments\/(\d+)$/);
|
const m = normalized.match(/^\/admin\/payments\/(\d+)$/);
|
||||||
return m ? Number(m[1]) : null;
|
return m ? Number(m[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adminPaymentsUserIdFromPath(pathname) {
|
export function adminPaymentsUserIdFromPath(pathname, routePrefix = "") {
|
||||||
const normalized = String(pathname || "")
|
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\/+$/, "");
|
|
||||||
const m = normalized.match(/^\/admin\/payments\/users\/(-?\d+)$/);
|
const m = normalized.match(/^\/admin\/payments\/users\/(-?\d+)$/);
|
||||||
return m ? Number(m[1]) : null;
|
return m ? Number(m[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function supportTicketIdFromPath(pathname) {
|
export function supportTicketIdFromPath(pathname, routePrefix = "") {
|
||||||
const normalized = String(pathname || "")
|
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\/+$/, "");
|
|
||||||
const m = normalized.match(/^\/support\/(\d+)$/);
|
const m = normalized.match(/^\/support\/(\d+)$/);
|
||||||
return m ? Number(m[1]) : null;
|
return m ? Number(m[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adminSupportTicketIdFromPath(pathname) {
|
export function adminSupportTicketIdFromPath(pathname, routePrefix = "") {
|
||||||
const normalized = String(pathname || "")
|
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\/+$/, "");
|
|
||||||
const m = normalized.match(/^\/admin\/support\/(\d+)$/);
|
const m = normalized.match(/^\/admin\/support\/(\d+)$/);
|
||||||
return m ? Number(m[1]) : null;
|
return m ? Number(m[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncSectionPath(section, replace = false, adminSection = null, adminUserId = null) {
|
export function syncSectionPath(
|
||||||
|
section,
|
||||||
|
replace = false,
|
||||||
|
adminSection = null,
|
||||||
|
adminUserId = null,
|
||||||
|
routePrefix = ""
|
||||||
|
) {
|
||||||
if (window.location.protocol === "file:") return;
|
if (window.location.protocol === "file:") return;
|
||||||
const normalized = normalizeSection(section);
|
const normalized = normalizeSection(section);
|
||||||
let targetPath = APP_SECTION_PATHS[normalized] || APP_SECTION_PATHS.home;
|
let targetPath = APP_SECTION_PATHS[normalized] || APP_SECTION_PATHS.home;
|
||||||
if (normalized === "admin") {
|
if (normalized === "admin") {
|
||||||
const adm = adminSection || adminSectionFromPath(window.location.pathname) || "stats";
|
const adm =
|
||||||
|
adminSection || adminSectionFromPath(window.location.pathname, routePrefix) || "stats";
|
||||||
const uid =
|
const uid =
|
||||||
adminUserId ?? (adm === "users" ? adminUserIdFromPath(window.location.pathname) : null);
|
adminUserId ??
|
||||||
|
(adm === "users" ? adminUserIdFromPath(window.location.pathname, routePrefix) : null);
|
||||||
const supportTicketId =
|
const supportTicketId =
|
||||||
adm === "support" ? adminSupportTicketIdFromPath(window.location.pathname) : null;
|
adm === "support"
|
||||||
const paymentId = adm === "payments" ? adminPaymentIdFromPath(window.location.pathname) : null;
|
? adminSupportTicketIdFromPath(window.location.pathname, routePrefix)
|
||||||
|
: null;
|
||||||
|
const paymentId =
|
||||||
|
adm === "payments" ? adminPaymentIdFromPath(window.location.pathname, routePrefix) : null;
|
||||||
const paymentUserId =
|
const paymentUserId =
|
||||||
adm === "payments" ? adminPaymentsUserIdFromPath(window.location.pathname) : null;
|
adm === "payments"
|
||||||
|
? adminPaymentsUserIdFromPath(window.location.pathname, routePrefix)
|
||||||
|
: null;
|
||||||
if (adm === "users" && uid) targetPath = `/admin/users/${uid}`;
|
if (adm === "users" && uid) targetPath = `/admin/users/${uid}`;
|
||||||
else if (adm === "support" && supportTicketId) targetPath = `/admin/support/${supportTicketId}`;
|
else if (adm === "support" && supportTicketId) targetPath = `/admin/support/${supportTicketId}`;
|
||||||
else if (adm === "payments" && paymentUserId)
|
else if (adm === "payments" && paymentUserId)
|
||||||
@@ -113,6 +140,7 @@ export function syncSectionPath(section, replace = false, adminSection = null, a
|
|||||||
else if (adm === "payments" && paymentId) targetPath = `/admin/payments/${paymentId}`;
|
else if (adm === "payments" && paymentId) targetPath = `/admin/payments/${paymentId}`;
|
||||||
else targetPath = `/admin/${adm}`;
|
else targetPath = `/admin/${adm}`;
|
||||||
}
|
}
|
||||||
|
targetPath = withRoutePrefix(targetPath, routePrefix);
|
||||||
if (window.location.pathname === targetPath) return;
|
if (window.location.pathname === targetPath) return;
|
||||||
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
|
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
|
||||||
window.history[replace ? "replaceState" : "pushState"](null, "", nextUrl);
|
window.history[replace ? "replaceState" : "pushState"](null, "", nextUrl);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import { withRoutePrefix } from "../routes.js";
|
||||||
|
|
||||||
export function createSupportStore({ api, t, showToast }) {
|
export function createSupportStore({ api, t, showToast, routePrefix = "" }) {
|
||||||
const OPEN_TICKET_POLL_MS = 3_000;
|
const OPEN_TICKET_POLL_MS = 3_000;
|
||||||
const ACTIVE_POLL_MS = 8_000;
|
const ACTIVE_POLL_MS = 8_000;
|
||||||
const BACKGROUND_POLL_MS = 45_000;
|
const BACKGROUND_POLL_MS = 45_000;
|
||||||
@@ -198,7 +199,7 @@ export function createSupportStore({ api, t, showToast }) {
|
|||||||
detailLoading: true,
|
detailLoading: true,
|
||||||
}));
|
}));
|
||||||
if (!opts.skipPush && typeof window !== "undefined" && window.location.protocol !== "file:") {
|
if (!opts.skipPush && typeof window !== "undefined" && window.location.protocol !== "file:") {
|
||||||
const target = `/support/${id}`;
|
const target = withRoutePrefix(`/support/${id}`, routePrefix);
|
||||||
if (window.location.pathname !== target) {
|
if (window.location.pathname !== target) {
|
||||||
window.history.pushState(
|
window.history.pushState(
|
||||||
null,
|
null,
|
||||||
@@ -232,11 +233,12 @@ export function createSupportStore({ api, t, showToast }) {
|
|||||||
function closeTicketView(opts = {}) {
|
function closeTicketView(opts = {}) {
|
||||||
state.update((s) => ({ ...s, openedTicketId: null, openedTicket: null, messages: [] }));
|
state.update((s) => ({ ...s, openedTicketId: null, openedTicket: null, messages: [] }));
|
||||||
if (!opts.skipPush && typeof window !== "undefined" && window.location.protocol !== "file:") {
|
if (!opts.skipPush && typeof window !== "undefined" && window.location.protocol !== "file:") {
|
||||||
if (window.location.pathname.startsWith("/support/")) {
|
const supportPath = withRoutePrefix("/support", routePrefix);
|
||||||
|
if (window.location.pathname.startsWith(`${supportPath}/`)) {
|
||||||
window.history.pushState(
|
window.history.pushState(
|
||||||
null,
|
null,
|
||||||
"",
|
"",
|
||||||
`/support${window.location.search}${window.location.hash}`
|
`${supportPath}${window.location.search}${window.location.hash}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user