@@ -38,6 +38,7 @@ import {
3838import { useState , useEffect , useCallback , useMemo } from "react"
3939import { useClient , useMetadataSubscriptionCallback } from '@objectstack/client-react' ;
4040import type { InstalledPackage } from '@objectstack/spec/kernel' ;
41+ import { useSidebarGroups , useAllMetadataIcons } from '@/plugins/hooks' ;
4142
4243import {
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 */
130134const 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