/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '@/stores/authStore'; import { useSchemaStore } from '@/stores/schemaStore'; import { useAccountStore } from '@/stores/accountStore'; import { useUIStore } from '@/stores/uiStore'; import { fetchSession, fetchSchema, fetchAccountInfo } from '@/services/jmap/client'; import { setLocale } from '@/i18n'; import { TopBar } from '@/components/layout/TopBar'; import { Sidebar } from '@/components/layout/Sidebar'; import { MainContent } from '@/components/layout/MainContent'; import { ErrorBoundary } from '@/components/layout/ErrorBoundary'; import { LoadingFallback } from '@/components/common/LoadingFallback'; import { findFirstAccessibleLinkInLayout, findFirstVisibleLinkInLayout, isLinkAccessible, visibleLayouts, } from '@/lib/layout'; import { usePermissions } from '@/hooks/usePermissions'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; import { friendlyName } from '@/hooks/useGlobalSearch'; const BootstrapWizard = lazy(() => import('@/components/bootstrap/BootstrapWizard').then((m) => ({ default: m.BootstrapWizard })), ); export default function AdminPanel() { const { t } = useTranslation(); const navigate = useNavigate(); const { section, '*': splat } = useParams<{ section?: string; '*'?: string; }>(); let viewName: string | undefined; let id: string | undefined; if (splat) { const parts = splat.split('/'); if (parts[parts.length - 1] === 'new') { id = 'new'; viewName = parts.slice(0, -1).join('/'); } else if (parts[parts.length - 1] === 'singleton') { id = 'singleton'; viewName = parts.slice(0, -1).join('/'); } else { const schemaStore = useSchemaStore.getState(); const fullAsView = parts.join('/'); if (schemaStore.schema?.objects[fullAsView]) { viewName = fullAsView; } else if (parts.length > 1) { const candidateView = parts.slice(0, -1).join('/'); if (schemaStore.schema?.objects[candidateView]) { viewName = candidateView; id = parts[parts.length - 1]; } else { viewName = fullAsView; } } else { viewName = fullAsView; } } } const isSchemaLoaded = useSchemaStore((s) => s.isLoaded); const schema = useSchemaStore((s) => s.schema); const setSchema = useSchemaStore((s) => s.setSchema); const setAccountInfo = useAccountStore((s) => s.setAccountInfo); const edition = useAccountStore((s) => s.edition); const permissions = useAccountStore((s) => s.permissions); const setSession = useAuthStore((s) => s.setSession); const accessToken = useAuthStore((s) => s.accessToken); const activeAccountId = useAuthStore((s) => s.activeAccountId); const setActiveSection = useUIStore((s) => s.setActiveSection); const sidebarOpen = useUIStore((s) => s.sidebarOpen); const { canViewObject } = usePermissions(); const [initError, setInitError] = useState(null); const [initializing, setInitializing] = useState(!isSchemaLoaded); const searchIndex = useSchemaStore((s) => s.searchIndex); const pageTitle = useMemo(() => { if (!section) return t('dashboard.title', 'Dashboard'); if (!viewName) return section; let label: string | undefined; for (const entry of searchIndex) { if (entry.type !== 'link' || entry.viewName !== viewName) continue; if (entry.section === section) { label = entry.text; break; } label ??= entry.text; } const name = label ?? friendlyName(viewName); const title = id === 'new' ? t('form.createTitle', 'Create {{name}}', { name }) : name; return `${title} ยท ${section}`; }, [section, viewName, id, searchIndex, t]); useDocumentTitle(pageTitle); useEffect(() => { const bypassToken = import.meta.env.VITE_ACCESS_TOKEN; if (bypassToken && !accessToken) { useAuthStore.getState().setTokens(bypassToken, '', 86400, ''); } }, [accessToken]); useEffect(() => { if (isSchemaLoaded) return; let cancelled = false; async function init() { try { const session = await fetchSession(); const accounts: Record = {}; for (const [accountId, info] of Object.entries( session.accounts as Record, )) { accounts[accountId] = { name: info.name, isPersonal: info.isPersonal }; } const primaryAccounts = session.primaryAccounts as Record; const primaryAccountId = primaryAccounts['urn:ietf:params:jmap:core'] ?? Object.values(primaryAccounts)[0] ?? Object.keys(accounts)[0]; const apiUrl = (session.apiUrl as string) || '/jmap'; const capabilities = session.capabilities as Record> | undefined; const coreCapability = capabilities?.['urn:ietf:params:jmap:core']; const maxObjectsInGet = typeof coreCapability?.maxObjectsInGet === 'number' ? coreCapability.maxObjectsInGet : undefined; const maxObjectsInSet = typeof coreCapability?.maxObjectsInSet === 'number' ? coreCapability.maxObjectsInSet : undefined; if (cancelled) return; setSession(accounts, primaryAccountId, apiUrl, maxObjectsInGet, maxObjectsInSet); const [schemaData, accountData] = await Promise.all([fetchSchema(), fetchAccountInfo()]); if (cancelled) return; setSchema(schemaData); setAccountInfo(accountData.permissions, accountData.edition, accountData.locale); setLocale(accountData.locale); setInitializing(false); } catch (err) { if (!cancelled) { console.error('Initialization failed:', err); setInitError(err instanceof Error ? err.message : t('errors.unexpectedError')); setInitializing(false); } } } init(); return () => { cancelled = true; }; }, [isSchemaLoaded, setSession, setSchema, setAccountInfo, t]); const isBootstrapMode = useMemo(() => { if (!schema) return false; if (!canViewObject('x:Bootstrap')) return false; const canGet = (prefix: string) => permissions.includes(`${prefix}Get`); const hasPerm = (perm: string) => permissions.includes(perm); return visibleLayouts(schema, edition, canGet, hasPerm).length === 0; }, [schema, canViewObject, edition, permissions]); useEffect(() => { if (!schema) return; if (isBootstrapMode) return; const canGet = (prefix: string) => permissions.includes(`${prefix}Get`); const hasPerm = (perm: string) => permissions.includes(perm); const layouts = visibleLayouts(schema, edition, canGet, hasPerm); const pickDefault = (): { layoutName: string; link: string | null } | null => { for (const layout of layouts) { const link = findFirstAccessibleLinkInLayout(schema, layout, edition, canGet, hasPerm); if (link) return { layoutName: layout.name, link }; } if (layouts[0]) { return { layoutName: layouts[0].name, link: findFirstVisibleLinkInLayout(schema, layouts[0], edition, canGet, hasPerm), }; } return null; }; const redirectTo = (layoutName: string, link: string | null) => { setActiveSection(layoutName); if (link) navigate(`/${layoutName}/${link}`, { replace: true }); }; if (section && viewName && !isLinkAccessible(schema, viewName, edition, canGet, hasPerm)) { const fallback = pickDefault(); if (fallback && (fallback.layoutName !== section || fallback.link !== viewName)) { redirectTo(fallback.layoutName, fallback.link); return; } } if (section && viewName) { const ownLayout = layouts.find((l) => l.name.toLowerCase() === section.toLowerCase()); setActiveSection(ownLayout?.name ?? layouts[0]?.name ?? section); return; } if (section) { const ownLayout = layouts.find((l) => l.name.toLowerCase() === section.toLowerCase()); const ownLink = ownLayout ? (findFirstAccessibleLinkInLayout(schema, ownLayout, edition, canGet, hasPerm) ?? findFirstVisibleLinkInLayout(schema, ownLayout, edition, canGet, hasPerm)) : null; if (ownLayout && ownLink) { redirectTo(ownLayout.name, ownLink); } else { const target = pickDefault(); if (target) redirectTo(target.layoutName, target.link); } return; } const target = pickDefault(); if (target) redirectTo(target.layoutName, target.link); }, [section, viewName, schema, setActiveSection, navigate, edition, permissions, isBootstrapMode]); if (initializing) { return ; } if (initError) { return (

{t('errors.loadSchemaFailed')}

{initError}

); } if (!schema) return null; if (isBootstrapMode) { return ( }> ); } return (
); }