Skip to content

Commit 89c0f97

Browse files
Claudehotlong
andauthored
feat(studio): replace hardcoded sidebar groups with plugin-contributed groups
- Remove hardcoded PROTOCOL_GROUPS array - Add useSidebarGroups() and useAllMetadataIcons() hooks - Create icon string to Lucide component mapping - Update sidebar to dynamically render groups from plugins - Metadata type labels and icons now sourced from plugin registry - Part of Phase 1.3: Plugin Sidebar Groups task Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/6c441201-fefb-4ec4-90de-2d3cd9f043db Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent cc9e8c7 commit 89c0f97

1 file changed

Lines changed: 68 additions & 58 deletions

File tree

apps/studio/src/components/app-sidebar.tsx

Lines changed: 68 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import { useState, useEffect, useCallback, useMemo } from "react"
3939
import { useClient, useMetadataSubscriptionCallback } from '@objectstack/client-react';
4040
import type { InstalledPackage } from '@objectstack/spec/kernel';
41+
import { useSidebarGroups, useAllMetadataIcons } from '@/plugins/hooks';
4142

4243
import {
4344
Sidebar,
@@ -70,61 +71,64 @@ import {
7071
DropdownMenuTrigger,
7172
} from "@/components/ui/dropdown-menu"
7273

73-
// ─── Icon & label hints ──────────────────────────────────────────────
74-
const META_TYPE_HINTS: Record<string, { label: string; icon: LucideIcon }> = {
75-
object: { label: 'Objects', icon: Package },
76-
hook: { label: 'Hooks', icon: Anchor },
77-
mapping: { label: 'Mappings', icon: Map },
78-
analyticsCube: { label: 'Analytics Cubes', icon: PieChart },
79-
data: { label: 'Seed Data', icon: Database },
80-
app: { label: 'Apps', icon: AppWindow },
81-
action: { label: 'Actions', icon: Zap },
82-
view: { label: 'Views', icon: Eye },
83-
page: { label: 'Pages', icon: FileCode },
84-
dashboard: { label: 'Dashboards', icon: BarChart3 },
85-
report: { label: 'Reports', icon: FileText },
86-
theme: { label: 'Themes', icon: Palette },
87-
flow: { label: 'Flows', icon: Workflow },
88-
workflow: { label: 'Workflows', icon: Workflow },
89-
approval: { label: 'Approvals', icon: CheckSquare },
90-
webhook: { label: 'Webhooks', icon: Webhook },
91-
role: { label: 'Roles', icon: UserCog },
92-
permission: { label: 'Permissions', icon: Lock },
93-
profile: { label: 'Profiles', icon: Shield },
94-
sharingRule: { label: 'Sharing Rules', icon: Shield },
95-
policy: { label: 'Policies', icon: Shield },
96-
agent: { label: 'Agents', icon: Bot },
97-
tool: { label: 'Tools', icon: Wrench },
98-
ragPipeline: { label: 'RAG Pipelines', icon: BookOpen },
99-
api: { label: 'APIs', icon: Globe },
100-
connector: { label: 'Connectors', icon: Link2 },
101-
plugin: { label: 'Plugins', icon: Layers },
102-
kind: { label: 'Kinds', icon: Database },
74+
// ─── Icon string to Lucide component mapping ─────────────────────────
75+
const ICON_MAP: Record<string, LucideIcon> = {
76+
'database': Database,
77+
'package': Package,
78+
'anchor': Anchor,
79+
'map': Map,
80+
'pie-chart': PieChart,
81+
'app-window': AppWindow,
82+
'zap': Zap,
83+
'eye': Eye,
84+
'file-code': FileCode,
85+
'bar-chart-3': BarChart3,
86+
'file-text': FileText,
87+
'palette': Palette,
88+
'workflow': Workflow,
89+
'check-square': CheckSquare,
90+
'webhook': Webhook,
91+
'shield': Shield,
92+
'user-cog': UserCog,
93+
'lock': Lock,
94+
'bot': Bot,
95+
'wrench': Wrench,
96+
'book-open': BookOpen,
97+
'globe': Globe,
98+
'link-2': Link2,
99+
'layers': Layers,
103100
};
104101

105-
function getTypeLabel(type: string): string {
106-
return META_TYPE_HINTS[type]?.label || type.charAt(0).toUpperCase() + type.slice(1);
107-
}
108-
function getTypeIcon(type: string): LucideIcon {
109-
return META_TYPE_HINTS[type]?.icon || Layers;
102+
/**
103+
* Map an icon string (from plugin manifest) to a Lucide icon component.
104+
* Fallback to Layers if not found.
105+
*/
106+
function getIconComponent(iconName?: string): LucideIcon {
107+
if (!iconName) return Layers;
108+
return ICON_MAP[iconName] || Layers;
110109
}
111110

112-
// ─── Protocol groups ─────────────────────────────────────────────────
113-
interface ProtocolGroup {
114-
key: string;
115-
label: string;
116-
icon: LucideIcon;
117-
types: string[];
111+
// ─── Metadata type label/icon helpers ───────────────────────────────
112+
113+
/**
114+
* Get label for a metadata type.
115+
* Uses plugin-registered icons if available, otherwise generates from type name.
116+
*/
117+
function getTypeLabel(type: string, metadataIcons: Map<string, any>): string {
118+
const icon = metadataIcons.get(type);
119+
if (icon?.label) return icon.label;
120+
return type.charAt(0).toUpperCase() + type.slice(1);
118121
}
119122

120-
const PROTOCOL_GROUPS: ProtocolGroup[] = [
121-
{ key: 'data', label: 'Data', icon: Database, types: ['object', 'hook', 'mapping', 'analyticsCube', 'data'] },
122-
{ key: 'ui', label: 'UI', icon: AppWindow, types: ['app', 'action', 'view', 'page', 'dashboard', 'report', 'theme'] },
123-
{ key: 'automation', label: 'Automation', icon: Workflow, types: ['flow', 'workflow', 'approval', 'webhook'] },
124-
{ key: 'security', label: 'Security', icon: Shield, types: ['role', 'permission', 'profile', 'sharingRule', 'policy'] },
125-
{ key: 'ai', label: 'AI', icon: Bot, types: ['agent', 'tool', 'ragPipeline'] },
126-
{ key: 'api', label: 'API', icon: Globe, types: ['api', 'connector'] },
127-
];
123+
/**
124+
* Get Lucide icon component for a metadata type.
125+
* Uses plugin-registered icons if available, otherwise falls back to Layers.
126+
*/
127+
function getTypeIcon(type: string, metadataIcons: Map<string, any>): LucideIcon {
128+
const icon = metadataIcons.get(type);
129+
if (icon?.icon) return icon.icon;
130+
return Layers;
131+
}
128132

129133
/** Types that are internal / should be hidden from the sidebar */
130134
const HIDDEN_TYPES = new Set(['plugin', 'kind']);
@@ -185,6 +189,10 @@ export function AppSidebar({
185189
const [metaTypes, setMetaTypes] = useState<string[]>([]);
186190
const [metaItems, setMetaItems] = useState<Record<string, any[]>>({});
187191

192+
// Get sidebar groups and metadata icons from plugins
193+
const sidebarGroups = useSidebarGroups();
194+
const metadataIcons = useAllMetadataIcons();
195+
188196
// Track which metadata *types* are expanded (show individual items)
189197
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(new Set(['object']));
190198

@@ -212,17 +220,19 @@ export function AppSidebar({
212220
}
213221

214222
// Normalize types: prefer singular form (agent, tool) over plural (agents, tools)
215-
// when both exist in PROTOCOL_GROUPS, since the singular REST endpoint merges
223+
// when both exist in sidebarGroups, since the singular REST endpoint merges
216224
// SchemaRegistry items with MetadataService runtime items.
217-
const groupSingulars = new Set(PROTOCOL_GROUPS.flatMap(g => g.types).filter(t => !t.endsWith('s')));
225+
const groupSingulars = new Set(
226+
sidebarGroups.flatMap(g => g.metadataTypes).filter(t => !t.endsWith('s'))
227+
);
218228
const normalized = types.map(t => {
219229
if (t.endsWith('s') && groupSingulars.has(t.slice(0, -1))) {
220230
return t.slice(0, -1); // agents → agent, tools → tool
221231
}
222232
return t;
223233
});
224234
// Also add group types that aren't covered at all by the server types
225-
const groupTypes = PROTOCOL_GROUPS.flatMap(g => g.types);
235+
const groupTypes = sidebarGroups.flatMap(g => g.metadataTypes);
226236
const coveredSet = new Set(normalized);
227237
const extraTypes = groupTypes.filter(t => {
228238
if (coveredSet.has(t)) return false;
@@ -256,7 +266,7 @@ export function AppSidebar({
256266
} finally {
257267
setLoading(false);
258268
}
259-
}, [client, selectedPackage]);
269+
}, [client, selectedPackage, sidebarGroups]);
260270

261271
useEffect(() => { loadMetadata(); }, [loadMetadata]);
262272

@@ -294,12 +304,12 @@ export function AppSidebar({
294304
}, [metaItems, showSystemInData]);
295305

296306
// Compute visible groups: only show groups that have at least one type with items
297-
const visibleGroups = PROTOCOL_GROUPS.map(group => {
298-
const visibleTypes = group.types.filter(t =>
307+
const visibleGroups = sidebarGroups.map(group => {
308+
const visibleTypes = group.metadataTypes.filter(t =>
299309
metaTypes.includes(t) && !HIDDEN_TYPES.has(t) && (filteredMetaItems[t]?.length ?? 0) > 0
300310
);
301311
const totalItems = visibleTypes.reduce((sum, t) => sum + (filteredMetaItems[t]?.length ?? 0), 0);
302-
return { ...group, visibleTypes, totalItems };
312+
return { ...group, visibleTypes, totalItems, icon: getIconComponent(group.icon) };
303313
}).filter(g => g.totalItems > 0);
304314

305315
// Package switcher state
@@ -424,8 +434,8 @@ export function AppSidebar({
424434
<SidebarMenu>
425435
{group.visibleTypes.map(type => {
426436
const items = filteredMetaItems[type] || [];
427-
const TypeIcon = getTypeIcon(type);
428-
const typeLabel = getTypeLabel(type);
437+
const TypeIcon = getTypeIcon(type, metadataIcons);
438+
const typeLabel = getTypeLabel(type, metadataIcons);
429439
const isObjectType = type === 'object';
430440
const isExpanded = expandedTypes.has(type) || !!searchQuery;
431441

0 commit comments

Comments
 (0)