버그수정
This commit is contained in:
@@ -1,14 +1,32 @@
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
matchPath,
|
||||
NavLink,
|
||||
Outlet,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { LayoutDashboard, UploadCloud, Settings2, Activity, Brain, Network, ListChecks, Menu } from "lucide-react";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
UploadCloud,
|
||||
Settings2,
|
||||
Activity,
|
||||
Brain,
|
||||
Network,
|
||||
ListChecks,
|
||||
Menu,
|
||||
} from "lucide-react";
|
||||
import { RootState } from "@/stores";
|
||||
import { toggleSidebar } from "@/stores/slices/uiSlice";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProjects } from "@/hooks/useProjects";
|
||||
|
||||
interface NavItem {
|
||||
to: string;
|
||||
to?: string;
|
||||
projectPath?: string;
|
||||
labelKey: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
}
|
||||
@@ -16,17 +34,71 @@ interface NavItem {
|
||||
const navItems: NavItem[] = [
|
||||
{ to: "/", labelKey: "nav.dashboard", icon: LayoutDashboard },
|
||||
{ to: "/onboard", labelKey: "nav.onboard", icon: UploadCloud },
|
||||
{ to: "/sources/demo-project", labelKey: "nav.sources", icon: Settings2 },
|
||||
{ to: "/crawl/demo-project", labelKey: "nav.crawl", icon: Activity },
|
||||
{ to: "/research/demo-project", labelKey: "nav.research", icon: Brain },
|
||||
{ to: "/editor/demo-project", labelKey: "nav.editor", icon: Network },
|
||||
{ to: "/review/demo-project", labelKey: "nav.review", icon: ListChecks },
|
||||
{ projectPath: "sources", labelKey: "nav.sources", icon: Settings2 },
|
||||
{ projectPath: "crawl", labelKey: "nav.crawl", icon: Activity },
|
||||
{ projectPath: "research", labelKey: "nav.research", icon: Brain },
|
||||
{ projectPath: "editor", labelKey: "nav.editor", icon: Network },
|
||||
{ projectPath: "review", labelKey: "nav.review", icon: ListChecks },
|
||||
];
|
||||
|
||||
const projectRoutePatterns = [
|
||||
"/sources/:projectId",
|
||||
"/crawl/:projectId",
|
||||
"/research/:projectId",
|
||||
"/editor/:projectId",
|
||||
"/review/:projectId",
|
||||
];
|
||||
|
||||
function projectIdFromPathname(pathname: string): string | undefined {
|
||||
for (const pattern of projectRoutePatterns) {
|
||||
const match = matchPath({ path: pattern, end: false }, pathname);
|
||||
if (match?.params.projectId) return match.params.projectId;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function workspaceSectionFromPathname(pathname: string): string | undefined {
|
||||
for (const pattern of projectRoutePatterns) {
|
||||
const match = matchPath({ path: pattern, end: false }, pathname);
|
||||
if (match?.params.projectId) return pattern.split("/")[1];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default function AppShell() {
|
||||
const { t } = useTranslation();
|
||||
const sidebarOpen = useSelector((s: RootState) => s.ui.sidebarOpen);
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const routeProjectId = projectIdFromPathname(location.pathname);
|
||||
const workspaceSection = workspaceSectionFromPathname(location.pathname);
|
||||
const { data: projects } = useProjects();
|
||||
const fallbackProjectId = projects?.[0]?.name;
|
||||
const routeProjectExists =
|
||||
!routeProjectId ||
|
||||
!projects ||
|
||||
projects.some((project) => project.name === routeProjectId);
|
||||
const currentProjectId = routeProjectExists
|
||||
? routeProjectId ?? fallbackProjectId
|
||||
: fallbackProjectId;
|
||||
|
||||
useEffect(() => {
|
||||
if (routeProjectId && projects && !routeProjectExists) {
|
||||
const nextPath =
|
||||
workspaceSection && fallbackProjectId
|
||||
? `/${workspaceSection}/${encodeURIComponent(fallbackProjectId)}`
|
||||
: "/";
|
||||
navigate(nextPath, { replace: true });
|
||||
}
|
||||
}, [
|
||||
fallbackProjectId,
|
||||
navigate,
|
||||
projects,
|
||||
routeProjectExists,
|
||||
routeProjectId,
|
||||
workspaceSection,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background text-foreground">
|
||||
@@ -51,25 +123,62 @@ export default function AppShell() {
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{sidebarOpen && (
|
||||
<div className="border-b px-4 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("nav.currentProject", "Current project")}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-sm font-medium">
|
||||
{currentProjectId ??
|
||||
t("nav.noProjectSelected", "Select a project")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="flex flex-col gap-1 p-2">
|
||||
{navItems.map(({ to, labelKey, icon: Icon }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={to === "/"}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
||||
isActive
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{sidebarOpen && <span>{t(labelKey)}</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
{navItems.map(({ to, projectPath, labelKey, icon: Icon }) => {
|
||||
const href =
|
||||
to ??
|
||||
(currentProjectId
|
||||
? `/${projectPath}/${encodeURIComponent(currentProjectId)}`
|
||||
: undefined);
|
||||
|
||||
if (!href) {
|
||||
return (
|
||||
<div
|
||||
key={labelKey}
|
||||
className="flex cursor-not-allowed items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground/50"
|
||||
title={t(
|
||||
"nav.selectProjectFirst",
|
||||
"Select a project from the dashboard first",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{sidebarOpen && <span>{t(labelKey)}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={labelKey}
|
||||
to={href}
|
||||
end={href === "/"}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
||||
isActive
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{sidebarOpen && <span>{t(labelKey)}</span>}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const projectSourceSchema = z.object({
|
||||
id: z.union([z.string(), z.number()]).transform(String),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
base_url: z.string().nullable().optional(),
|
||||
trust_level: z.union([z.string(), z.number()]).nullable().optional(),
|
||||
respect_robots_txt: z.boolean().nullable().optional(),
|
||||
rate_limit_per_minute: z.number().nullable().optional(),
|
||||
|
||||
Reference in New Issue
Block a user