v1.0.7 (fixes #17)

This commit is contained in:
Maurus Decimus
2026-07-30 17:16:05 +02:00
parent f18f3012aa
commit 189e270785
31 changed files with 1478 additions and 695 deletions
+65 -42
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
import { useEffect, useMemo, useState } from 'react';
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';
@@ -17,7 +17,7 @@ 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 { BootstrapWizard } from '@/components/bootstrap/BootstrapWizard';
import { LoadingFallback } from '@/components/common/LoadingFallback';
import {
findFirstAccessibleLinkInLayout,
findFirstVisibleLinkInLayout,
@@ -25,7 +25,12 @@ import {
visibleLayouts,
} from '@/lib/layout';
import { usePermissions } from '@/hooks/usePermissions';
import { Loader2 } from 'lucide-react';
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();
@@ -69,9 +74,10 @@ export default function AdminPanel() {
const setSchema = useSchemaStore((s) => s.setSchema);
const setAccountInfo = useAccountStore((s) => s.setAccountInfo);
const edition = useAccountStore((s) => s.edition);
const hasObjectPermission = useAccountStore((s) => s.hasObjectPermission);
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();
@@ -79,6 +85,26 @@ export default function AdminPanel() {
const [initError, setInitError] = useState<string | null>(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) {
@@ -140,20 +166,19 @@ export default function AdminPanel() {
};
}, [isSchemaLoaded, setSession, setSchema, setAccountInfo, t]);
const hasPermission = useAccountStore((s) => s.hasPermission);
const isBootstrapMode = useMemo(() => {
if (!schema) return false;
if (!canViewObject('x:Bootstrap')) return false;
const canGet = (prefix: string) => hasObjectPermission(prefix, 'Get');
const hasPerm = (perm: string) => hasPermission(perm);
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, hasObjectPermission, hasPermission]);
}, [schema, canViewObject, edition, permissions]);
useEffect(() => {
if (!schema) return;
if (isBootstrapMode) return;
const canGet = (prefix: string) => hasObjectPermission(prefix, 'Get');
const hasPerm = (perm: string) => hasPermission(perm);
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) {
@@ -169,50 +194,46 @@ export default function AdminPanel() {
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)) {
setActiveSection(fallback.layoutName);
if (fallback.link) {
navigate(`/${fallback.layoutName}/${fallback.link}`, { replace: true });
}
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) {
setActiveSection(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) {
setActiveSection(target.layoutName);
if (target.link) {
navigate(`/${target.layoutName}/${target.link}`, { replace: true });
}
}
}, [
section,
viewName,
schema,
setActiveSection,
navigate,
edition,
hasObjectPermission,
hasPermission,
isBootstrapMode,
]);
if (target) redirectTo(target.layoutName, target.link);
}, [section, viewName, schema, setActiveSection, navigate, edition, permissions, isBootstrapMode]);
if (initializing) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground">{t('common.loading')}</p>
</div>
</div>
);
return <LoadingFallback fullScreen />;
}
if (initError) {
@@ -237,7 +258,9 @@ export default function AdminPanel() {
if (isBootstrapMode) {
return (
<ErrorBoundary>
<BootstrapWizard />
<Suspense fallback={<LoadingFallback fullScreen />}>
<BootstrapWizard />
</Suspense>
</ErrorBoundary>
);
}
@@ -250,7 +273,7 @@ export default function AdminPanel() {
<main
className={`flex-1 overflow-auto bg-content-background p-6 transition-[margin] ${sidebarOpen ? 'md:ml-64' : ''}`}
>
<ErrorBoundary>
<ErrorBoundary key={activeAccountId ?? 'none'}>
<MainContent viewName={viewName} id={id} section={section} />
</ErrorBoundary>
</main>