mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-24 05:16:09 +00:00
feat(web): group plugin pages by plugin in sidebar with collapsible sections
- Group pages by plugin when a plugin has multiple pages, collapse under the plugin label; single-page plugins render directly without nesting - Rename "Extension Pages" to "Plugin Pages" with tooltip explaining these are visual pages provided by installed plugins - Add pluginLabel to PluginPageItem for display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,7 +92,7 @@ import {
|
|||||||
} from '@/components/ui/popover';
|
} from '@/components/ui/popover';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useSidebarData, SidebarEntityItem } from './SidebarDataContext';
|
import { useSidebarData, SidebarEntityItem } from './SidebarDataContext';
|
||||||
import { LayoutDashboard } from 'lucide-react';
|
import { LayoutDashboard, Puzzle } from 'lucide-react';
|
||||||
|
|
||||||
// Compare two version strings, returns true if v1 > v2
|
// Compare two version strings, returns true if v1 > v2
|
||||||
function compareVersions(v1: string, v2: string): boolean {
|
function compareVersions(v1: string, v2: string): boolean {
|
||||||
@@ -1040,7 +1040,7 @@ function PluginItemMenu({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin pages navigation section
|
// Plugin pages navigation section — grouped by plugin
|
||||||
function PluginPagesNav() {
|
function PluginPagesNav() {
|
||||||
const { pluginPages } = useSidebarData();
|
const { pluginPages } = useSidebarData();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -1054,27 +1054,87 @@ function PluginPagesNav() {
|
|||||||
const currentId =
|
const currentId =
|
||||||
pathname === '/home/plugin-pages' ? searchParams.get('id') : null;
|
pathname === '/home/plugin-pages' ? searchParams.get('id') : null;
|
||||||
|
|
||||||
|
// Group pages by plugin (author/name)
|
||||||
|
const grouped = new Map<
|
||||||
|
string,
|
||||||
|
{ label: string; pages: typeof pluginPages }
|
||||||
|
>();
|
||||||
|
for (const page of pluginPages) {
|
||||||
|
const key = `${page.pluginAuthor}/${page.pluginName}`;
|
||||||
|
if (!grouped.has(key)) {
|
||||||
|
grouped.set(key, { label: page.pluginLabel, pages: [] });
|
||||||
|
}
|
||||||
|
grouped.get(key)!.pages.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>{t('sidebar.pluginPages')}</SidebarGroupLabel>
|
<SidebarGroupLabel title={t('sidebar.pluginPagesTooltip')}>
|
||||||
|
{t('sidebar.pluginPages')}
|
||||||
|
</SidebarGroupLabel>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{pluginPages.map((page) => {
|
{Array.from(grouped.entries()).map(
|
||||||
const isActive = currentId === page.id;
|
([pluginKey, { label, pages }]) => {
|
||||||
const route = `/home/plugin-pages?id=${encodeURIComponent(page.id)}`;
|
const hasActivePage = pages.some((p) => p.id === currentId);
|
||||||
return (
|
|
||||||
<SidebarMenuItem key={page.id}>
|
// Single page — render directly without nesting
|
||||||
<SidebarMenuButton
|
if (pages.length === 1) {
|
||||||
isActive={isActive}
|
const page = pages[0];
|
||||||
tooltip={page.name}
|
const isActive = currentId === page.id;
|
||||||
onClick={() => navigate(route)}
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple pages — collapsible group
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
key={pluginKey}
|
||||||
|
defaultOpen={hasActivePage}
|
||||||
|
className="group/collapsible"
|
||||||
>
|
>
|
||||||
<LayoutDashboard className="size-4 text-blue-500" />
|
<SidebarMenuItem>
|
||||||
<span>{page.name}</span>
|
<CollapsibleTrigger asChild>
|
||||||
</SidebarMenuButton>
|
<SidebarMenuButton tooltip={label}>
|
||||||
</SidebarMenuItem>
|
<Puzzle className="size-4 text-blue-500" />
|
||||||
);
|
<span>{label}</span>
|
||||||
})}
|
<ChevronRight className="ml-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{pages.map((page) => {
|
||||||
|
const isActive = currentId === page.id;
|
||||||
|
const route = `/home/plugin-pages?id=${encodeURIComponent(page.id)}`;
|
||||||
|
return (
|
||||||
|
<SidebarMenuSubItem key={page.id}>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
isActive={isActive}
|
||||||
|
onClick={() => navigate(route)}
|
||||||
|
>
|
||||||
|
<span>{page.name}</span>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface PluginPageItem {
|
|||||||
name: string; // display label
|
name: string; // display label
|
||||||
pluginAuthor: string;
|
pluginAuthor: string;
|
||||||
pluginName: string;
|
pluginName: string;
|
||||||
|
pluginLabel: string; // human-readable plugin display name
|
||||||
pageId: string;
|
pageId: string;
|
||||||
path: string; // asset path (HTML file)
|
path: string; // asset path (HTML file)
|
||||||
icon?: string; // optional icon name
|
icon?: string; // optional icon name
|
||||||
@@ -191,6 +192,7 @@ export function SidebarDataProvider({
|
|||||||
const meta = plugin.manifest.manifest.metadata;
|
const meta = plugin.manifest.manifest.metadata;
|
||||||
const author = meta.author ?? '';
|
const author = meta.author ?? '';
|
||||||
const name = meta.name;
|
const name = meta.name;
|
||||||
|
const label = meta.label ? extractI18nObject(meta.label) : name;
|
||||||
const spec = plugin.manifest.manifest.spec;
|
const spec = plugin.manifest.manifest.spec;
|
||||||
if (spec?.pages && Array.isArray(spec.pages)) {
|
if (spec?.pages && Array.isArray(spec.pages)) {
|
||||||
for (const page of spec.pages) {
|
for (const page of spec.pages) {
|
||||||
@@ -202,6 +204,7 @@ export function SidebarDataProvider({
|
|||||||
name: page.label ? extractI18nObject(page.label) : page.id,
|
name: page.label ? extractI18nObject(page.label) : page.id,
|
||||||
pluginAuthor: author,
|
pluginAuthor: author,
|
||||||
pluginName: name,
|
pluginName: name,
|
||||||
|
pluginLabel: label,
|
||||||
pageId: page.id,
|
pageId: page.id,
|
||||||
path: page.path,
|
path: page.path,
|
||||||
icon: page.icon,
|
icon: page.icon,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const enUS = {
|
|||||||
installedPlugins: 'Installed Plugins',
|
installedPlugins: 'Installed Plugins',
|
||||||
pluginMarket: 'Marketplace',
|
pluginMarket: 'Marketplace',
|
||||||
mcpServers: 'MCP Servers',
|
mcpServers: 'MCP Servers',
|
||||||
pluginPages: 'Extension Pages',
|
pluginPages: 'Plugin Pages',
|
||||||
|
pluginPagesTooltip: 'Visual pages provided by installed plugins',
|
||||||
quickStart: 'Quick Start',
|
quickStart: 'Quick Start',
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
@@ -1294,8 +1295,8 @@ const enUS = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: 'Select an extension page from the sidebar',
|
selectFromSidebar: 'Select a plugin page from the sidebar',
|
||||||
invalidPage: 'Invalid extension page',
|
invalidPage: 'Invalid plugin page',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const zhHans = {
|
|||||||
installedPlugins: '已安装插件',
|
installedPlugins: '已安装插件',
|
||||||
pluginMarket: '插件市场',
|
pluginMarket: '插件市场',
|
||||||
mcpServers: 'MCP 服务器',
|
mcpServers: 'MCP 服务器',
|
||||||
pluginPages: '扩展页',
|
pluginPages: '插件页面',
|
||||||
|
pluginPagesTooltip: '由已安装的插件提供的可视化页面',
|
||||||
quickStart: '快速开始向导',
|
quickStart: '快速开始向导',
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
@@ -1236,8 +1237,8 @@ const zhHans = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: '从侧边栏选择一个扩展页',
|
selectFromSidebar: '从侧边栏选择一个插件页面',
|
||||||
invalidPage: '无效的扩展页',
|
invalidPage: '无效的插件页面',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user