feat: add plugin extension pages (iframe rendering, Page SDK, security hardening, i18n)

Co-Authored-By: Typer_Body <mcjiekejiemi@163.com>
This commit is contained in:
Junyan Qin
2026-04-24 17:19:45 +08:00
parent 195f6efeff
commit 12df9d6ee9
17 changed files with 572 additions and 95 deletions
@@ -92,6 +92,7 @@ import {
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { useSidebarData, SidebarEntityItem } from './SidebarDataContext';
import { LayoutDashboard } from 'lucide-react';
// Compare two version strings, returns true if v1 > v2
function compareVersions(v1: string, v2: string): boolean {
@@ -699,87 +700,103 @@ function NavItems({
>
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={false}
onClick={() => {
if (isCollapseOnly) {
onSectionToggle(config.id, !isOpen);
} else {
onChildClick(config);
}
}}
tooltip={config.name}
className="group/category-header"
>
{config.icon}
<span>{config.name}</span>
<div className="ml-auto flex items-center gap-0.5 -mr-1">
{canCreate &&
(isPlugin ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all"
onClick={(e) => e.stopPropagation()}
>
<Plus className="size-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{systemInfo.enable_marketplace && (
<div
role="button"
tabIndex={0}
onClick={() => {
if (isCollapseOnly) {
onSectionToggle(config.id, !isOpen);
} else {
onChildClick(config);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (isCollapseOnly) {
onSectionToggle(config.id, !isOpen);
} else {
onChildClick(config);
}
}
}}
>
{config.icon}
<span>{config.name}</span>
<div className="ml-auto flex items-center gap-0.5 -mr-1">
{canCreate &&
(isPlugin ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all"
onClick={(e) => e.stopPropagation()}
>
<Plus className="size-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{systemInfo.enable_marketplace && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
navigate('/home/market');
}}
>
<Store className="size-4" />
{t('plugins.goToMarketplace')}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
navigate('/home/market');
setPendingPluginInstallAction('local');
navigate('/home/plugins');
}}
>
<Store className="size-4" />
{t('plugins.goToMarketplace')}
<Upload className="size-4" />
{t('plugins.uploadLocal')}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setPendingPluginInstallAction('local');
navigate('/home/plugins');
}}
>
<Upload className="size-4" />
{t('plugins.uploadLocal')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setPendingPluginInstallAction('github');
navigate('/home/plugins');
}}
>
<Github className="size-4" />
{t('plugins.installFromGithub')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setPendingPluginInstallAction('github');
navigate('/home/plugins');
}}
>
<Github className="size-4" />
{t('plugins.installFromGithub')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<button
type="button"
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all"
onClick={(e) => {
e.stopPropagation();
navigate(`${routePrefix}?id=new`);
}}
>
<Plus className="size-3.5" />
</button>
))}
<CollapsibleTrigger asChild>
<button
type="button"
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all"
onClick={(e) => {
e.stopPropagation();
navigate(`${routePrefix}?id=new`);
}}
className="p-1 rounded-sm hover:bg-sidebar-accent"
onClick={(e) => e.stopPropagation()}
>
<Plus className="size-3.5" />
<ChevronRight className="size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</button>
))}
<CollapsibleTrigger asChild>
<button
type="button"
className="p-1 rounded-sm hover:bg-sidebar-accent"
onClick={(e) => e.stopPropagation()}
>
<ChevronRight className="size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</button>
</CollapsibleTrigger>
</CollapsibleTrigger>
</div>
</div>
</SidebarMenuButton>
<CollapsibleContent>
@@ -1023,6 +1040,47 @@ function PluginItemMenu({
);
}
// Plugin pages navigation section
function PluginPagesNav() {
const { pluginPages } = useSidebarData();
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const { t } = useTranslation();
if (pluginPages.length === 0) return null;
const pathname = location.pathname;
const currentId =
pathname === '/home/plugin-pages' ? searchParams.get('id') : null;
return (
<SidebarGroup>
<SidebarGroupLabel>{t('sidebar.pluginPages')}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{pluginPages.map((page) => {
const isActive = currentId === page.id;
const route = `/home/plugin-pages?id=${encodeURIComponent(page.id)}`;
return (
<SidebarMenuItem key={page.id}>
<SidebarMenuButton
isActive={isActive}
tooltip={page.name}
onClick={() => navigate(route)}
>
<LayoutDashboard className="size-4 text-blue-500" />
<span>{page.name}</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}
export default function HomeSidebar({
onSelectedChangeAction,
}: {
@@ -1285,6 +1343,7 @@ export default function HomeSidebar({
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<PluginPagesNav />
</SidebarContent>
{/* Footer */}