refactor: permission

This commit is contained in:
tao.chen
2026-08-25 21:21:28 +08:00
parent e186e5e541
commit 38e763d08e
11 changed files with 171 additions and 1037 deletions
+91 -79
View File
@@ -1,7 +1,9 @@
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router";
import { Home, Code, Calendar, Settings, Users, Folder, PanelLeft, ChevronRight } from "lucide-react";
import { PanelLeft, ChevronRight } from "lucide-react";
import { navigation } from "./platform.navigation";
import type { ActivePage, NavigationItem } from "./platform.navigation";
import { Collapsible } from "@base-ui/react/collapsible";
import type { Route } from "./+types/platform";
@@ -23,7 +25,7 @@ import {
SidebarTrigger,
SidebarInset,
} from "~/components/ui/sidebar";
import { useApi, useAuth } from "~/context/AuthContext";
import { useApi, useAuth, usePermission } from "~/context/AuthContext";
import { useEditSessionLifecycle } from "~/features/platform/hooks/useEditSessionLifecycle";
import {
bindScriptWorkspaceApi,
@@ -36,8 +38,6 @@ import { useUiStore } from "~/features/platform/state/uiStore";
import { bindAdminApi } from "~/features/admin/state/adminStore";
import { bindSchedulesApi } from "~/features/schedules/state/helpers";
type ActivePage = "home" | "scripts" | "schedules" | "system";
function pageFromPath(pathname: string): ActivePage {
const first = pathname.replace(/^\/+/, "").split("/")[0];
if (first === "workbench" || first === "") return "home";
@@ -59,6 +59,80 @@ export function meta({}: Route.MetaArgs) {
];
}
function NavRow({
item,
can,
activePage,
pathname,
onNavigate,
}: {
item: NavigationItem;
can: (code: string) => boolean;
activePage: ActivePage;
pathname: string;
onNavigate: (sub?: "users" | "projects") => void;
}) {
const itemIcon = <item.icon />;
const childActivePath = (child: NavigationItem) =>
child.activePath ?? pathForPage(child.page);
if (item.children && item.children.length > 0) {
return (
<Collapsible.Root
defaultOpen={activePage === item.page}
className="group/collapsible"
>
<SidebarMenuItem>
<Collapsible.Trigger
render={
<SidebarMenuButton
className="py-3 text-sm"
isActive={activePage === item.page}
data-active={activePage === item.page ? "true" : undefined}
/>
}
>
{itemIcon}
<span>{item.label}</span>
<ChevronRight className="ml-auto size-4 transition-transform group-data-[open]/collapsible:rotate-90" />
</Collapsible.Trigger>
<Collapsible.Panel>
<SidebarMenuSub className="gap-1.5 mt-2">
{item.children
.filter((child) => !child.permission || can(child.permission))
.map((child) => (
<SidebarMenuSubItem key={child.label}>
<SidebarMenuSubButton
className="h-9 text-xs"
onClick={() => onNavigate(child.sub)}
isActive={pathname === childActivePath(child)}
>
<child.icon /> <span>{child.label}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</Collapsible.Panel>
</SidebarMenuItem>
</Collapsible.Root>
);
}
return (
<SidebarMenuItem>
<SidebarMenuButton
className="py-3 text-sm"
onClick={() => onNavigate()}
isActive={activePage === item.page}
data-active={activePage === item.page ? "true" : undefined}
>
{itemIcon}
<span>{item.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
export default function PlatformLayout() {
const { currentWorkspace } = useAuth();
if (!currentWorkspace) {
@@ -98,7 +172,7 @@ function AuthenticatedLayout() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const isSystemAdmin = user?.is_system_admin ?? false;
const can = usePermission;
// 绑定 api 到 script workspace store。
// 必须放在 render body(同步),不能用 useEffect([api]) + cleanup —
@@ -168,80 +242,18 @@ function AuthenticatedLayout() {
</SidebarHeader>
<SidebarContent>
<SidebarMenu className="gap-1.5">
<SidebarMenuItem>
<SidebarMenuButton
className="py-3 text-sm"
onClick={() => guardedNavigate("home")}
isActive={activePage === "home"}
data-active={activePage === "home" ? "true" : undefined}
>
<Home /> <span></span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
className="py-3 text-sm"
onClick={() => guardedNavigate("scripts")}
isActive={activePage === "scripts"}
data-active={activePage === "scripts" ? "true" : undefined}
>
<Code /> <span></span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
className="py-3 text-sm"
onClick={() => guardedNavigate("schedules")}
isActive={activePage === "schedules"}
data-active={activePage === "schedules" ? "true" : undefined}
>
<Calendar /> <span></span>
</SidebarMenuButton>
</SidebarMenuItem>
{isSystemAdmin && (
<Collapsible.Root
defaultOpen={activePage === "system"}
className="group/collapsible"
>
<SidebarMenuItem>
<Collapsible.Trigger
render={
<SidebarMenuButton
className="py-3 text-sm"
isActive={activePage === "system"}
data-active={activePage === "system" ? "true" : undefined}
/>
}
>
<Settings />
<span></span>
<ChevronRight className="ml-auto size-4 transition-transform group-data-[open]/collapsible:rotate-90" />
</Collapsible.Trigger>
<Collapsible.Panel>
<SidebarMenuSub className="gap-1.5 mt-2">
<SidebarMenuSubItem>
<SidebarMenuSubButton
className="h-9 text-xs"
onClick={() => guardedNavigate("system", "users")}
isActive={location.pathname === "/system/users"}
>
<Users /> <span></span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton
className="h-9 text-xs"
onClick={() => guardedNavigate("system", "projects")}
isActive={location.pathname === "/system/projects"}
>
<Folder /> <span></span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</Collapsible.Panel>
</SidebarMenuItem>
</Collapsible.Root>
)}
{navigation
.filter((item) => !item.permission || can(item.permission))
.map((item) => (
<NavRow
key={item.page}
item={item}
can={can}
activePage={activePage}
pathname={location.pathname}
onNavigate={(sub) => guardedNavigate(item.page, sub)}
/>
))}
</SidebarMenu>
</SidebarContent>
<SidebarFooter>