@@ -19,6 +19,7 @@ import {
1919 AppWindow ,
2020 Layers ,
2121 Eye ,
22+ EyeOff ,
2223 FileCode ,
2324 Palette ,
2425 CheckSquare ,
@@ -30,9 +31,10 @@ import {
3031 Anchor ,
3132 UserCog ,
3233 ChevronRight ,
34+ Settings ,
3335 type LucideIcon ,
3436} from "lucide-react"
35- import { useState , useEffect , useCallback } from "react"
37+ import { useState , useEffect , useCallback , useMemo } from "react"
3638import { useClient } from '@objectstack/client-react' ;
3739import type { InstalledPackage } from '@objectstack/spec/kernel' ;
3840
@@ -128,6 +130,24 @@ const PROTOCOL_GROUPS: ProtocolGroup[] = [
128130/** Types that are internal / should be hidden from the sidebar */
129131const HIDDEN_TYPES = new Set ( [ 'plugin' , 'plugins' , 'kind' , 'app' , 'apps' , 'package' ] ) ;
130132
133+ /** System namespace used for FQN-based names (e.g., sys__user) */
134+ const SYSTEM_NAMESPACE = 'sys' ;
135+
136+ /** System object FQN prefix (namespace + double underscore separator) */
137+ const SYSTEM_FQN_PREFIX = `${ SYSTEM_NAMESPACE } __` ;
138+
139+ /** Legacy system object name prefix (namespace + single underscore) */
140+ const SYSTEM_LEGACY_PREFIX = `${ SYSTEM_NAMESPACE } _` ;
141+
142+ /** Check if an object item is a system object */
143+ function isSystemObject ( item : any ) : boolean {
144+ if ( item . isSystem === true ) return true ;
145+ if ( item . namespace === SYSTEM_NAMESPACE ) return true ;
146+ const name = item . name || item . id || '' ;
147+ // Match FQN format (sys__user) or legacy format (sys_user)
148+ return name . startsWith ( SYSTEM_FQN_PREFIX ) || name . startsWith ( SYSTEM_LEGACY_PREFIX ) ;
149+ }
150+
131151/** Icon mapping for package types */
132152const PKG_TYPE_ICONS : Record < string , LucideIcon > = {
133153 app : AppWindow , plugin : Layers , driver : Database , server : Globe ,
@@ -161,6 +181,9 @@ export function AppSidebar({
161181 // Track which metadata *types* are expanded (show individual items)
162182 const [ expandedTypes , setExpandedTypes ] = useState < Set < string > > ( new Set ( [ 'object' , 'objects' ] ) ) ;
163183
184+ // Toggle to show/hide system objects in the Data protocol group
185+ const [ showSystemInData , setShowSystemInData ] = useState ( true ) ;
186+
164187 const toggleTypeExpanded = ( type : string ) => {
165188 setExpandedTypes ( prev => {
166189 const next = new Set ( prev ) ;
@@ -216,12 +239,35 @@ export function AppSidebar({
216239 label . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ||
217240 name . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ;
218241
242+ // Extract system objects from loaded metadata (object/objects types)
243+ const systemObjects = useMemo ( ( ) => {
244+ const objectTypes = [ 'object' , 'objects' ] ;
245+ const sysItems : any [ ] = [ ] ;
246+ for ( const type of objectTypes ) {
247+ const items = metaItems [ type ] || [ ] ;
248+ sysItems . push ( ...items . filter ( isSystemObject ) ) ;
249+ }
250+ return sysItems ;
251+ } , [ metaItems ] ) ;
252+
253+ // Filter system objects out of the Data protocol group when toggled off
254+ const filteredMetaItems = useMemo ( ( ) => {
255+ if ( showSystemInData ) return metaItems ;
256+ const result = { ...metaItems } ;
257+ for ( const type of [ 'object' , 'objects' ] ) {
258+ if ( result [ type ] ) {
259+ result [ type ] = result [ type ] . filter ( ( item : any ) => ! isSystemObject ( item ) ) ;
260+ }
261+ }
262+ return result ;
263+ } , [ metaItems , showSystemInData ] ) ;
264+
219265 // Compute visible groups: only show groups that have at least one type with items
220266 const visibleGroups = PROTOCOL_GROUPS . map ( group => {
221267 const visibleTypes = group . types . filter ( t =>
222- metaTypes . includes ( t ) && ! HIDDEN_TYPES . has ( t ) && ( metaItems [ t ] ?. length ?? 0 ) > 0
268+ metaTypes . includes ( t ) && ! HIDDEN_TYPES . has ( t ) && ( filteredMetaItems [ t ] ?. length ?? 0 ) > 0
223269 ) ;
224- const totalItems = visibleTypes . reduce ( ( sum , t ) => sum + ( metaItems [ t ] ?. length ?? 0 ) , 0 ) ;
270+ const totalItems = visibleTypes . reduce ( ( sum , t ) => sum + ( filteredMetaItems [ t ] ?. length ?? 0 ) , 0 ) ;
225271 return { ...group , visibleTypes, totalItems } ;
226272 } ) . filter ( g => g . totalItems > 0 ) ;
227273
@@ -328,11 +374,25 @@ export function AppSidebar({
328374 < group . icon className = "mr-1.5 h-3.5 w-3.5" />
329375 < span className = "flex-1 min-w-0 truncate" > { group . label } </ span >
330376 < span className = "shrink-0 text-xs tabular-nums text-sidebar-foreground/50" > { group . totalItems } </ span >
377+ { /* System objects filter toggle for Data group */ }
378+ { group . key === 'data' && systemObjects . length > 0 && (
379+ < button
380+ type = "button"
381+ title = { showSystemInData ? 'Hide system objects' : 'Show system objects' }
382+ aria-label = { showSystemInData ? 'Hide system objects' : 'Show system objects' }
383+ onClick = { ( e ) => { e . stopPropagation ( ) ; setShowSystemInData ( ! showSystemInData ) ; } }
384+ className = "ml-1 shrink-0 rounded p-0.5 text-sidebar-foreground/50 hover:text-sidebar-foreground hover:bg-sidebar-accent transition-colors"
385+ >
386+ { showSystemInData
387+ ? < Eye className = "h-3 w-3" />
388+ : < EyeOff className = "h-3 w-3" /> }
389+ </ button >
390+ ) }
331391 </ SidebarGroupLabel >
332392 < SidebarGroupContent >
333393 < SidebarMenu >
334394 { group . visibleTypes . map ( type => {
335- const items = metaItems [ type ] || [ ] ;
395+ const items = filteredMetaItems [ type ] || [ ] ;
336396 const TypeIcon = getTypeIcon ( type ) ;
337397 const typeLabel = getTypeLabel ( type ) ;
338398 const isObjectType = type === 'object' || type === 'objects' ;
@@ -408,9 +468,66 @@ export function AppSidebar({
408468
409469 { /* ── System ── */ }
410470 < SidebarGroup >
411- < SidebarGroupLabel > System</ SidebarGroupLabel >
471+ < SidebarGroupLabel >
472+ < Settings className = "mr-1.5 h-3.5 w-3.5" />
473+ < span className = "flex-1 min-w-0 truncate" > System</ span >
474+ { systemObjects . length > 0 && (
475+ < span className = "shrink-0 text-xs tabular-nums text-sidebar-foreground/50" > { systemObjects . length } </ span >
476+ ) }
477+ </ SidebarGroupLabel >
412478 < SidebarGroupContent >
413479 < SidebarMenu >
480+ { /* Dynamic system objects */ }
481+ { systemObjects . length > 0 && (
482+ < Collapsible
483+ open = { expandedTypes . has ( '_system_objects' ) || ! ! searchQuery }
484+ onOpenChange = { ( open ) => {
485+ const isExpanded = expandedTypes . has ( '_system_objects' ) ;
486+ if ( open && ! isExpanded ) toggleTypeExpanded ( '_system_objects' ) ;
487+ if ( ! open && isExpanded ) toggleTypeExpanded ( '_system_objects' ) ;
488+ } }
489+ asChild
490+ >
491+ < SidebarMenuItem >
492+ < CollapsibleTrigger asChild >
493+ < SidebarMenuButton tooltip = { `System Objects (${ systemObjects . length } )` } >
494+ < Database className = "h-4 w-4" />
495+ < span className = "flex-1 min-w-0 truncate" > System Objects</ span >
496+ < span className = "shrink-0 text-xs tabular-nums text-muted-foreground" > { systemObjects . length } </ span >
497+ < ChevronRight className = { `h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ${ ( expandedTypes . has ( '_system_objects' ) || ! ! searchQuery ) ? 'rotate-90' : '' } ` } />
498+ </ SidebarMenuButton >
499+ </ CollapsibleTrigger >
500+ < CollapsibleContent >
501+ < SidebarMenuSub >
502+ { systemObjects
503+ . filter ( ( item : any ) => matchesSearch ( item . label || item . name || '' , item . name || '' ) )
504+ . map ( ( item : any ) => {
505+ const itemName = item . name || item . id || 'unknown' ;
506+ const itemLabel = item . label || item . name || 'Untitled' ;
507+
508+ return (
509+ < SidebarMenuSubItem key = { itemName } >
510+ < SidebarMenuSubButton
511+ isActive = { selectedObject === itemName }
512+ onClick = { ( ) => onSelectObject ( itemName ) }
513+ >
514+ < span className = "truncate" >
515+ { isSystemObject ( item ) && (
516+ < span className = "text-muted-foreground font-mono text-xs" > { SYSTEM_NAMESPACE } :</ span >
517+ ) }
518+ { itemLabel }
519+ </ span >
520+ </ SidebarMenuSubButton >
521+ </ SidebarMenuSubItem >
522+ ) ;
523+ } ) }
524+ </ SidebarMenuSub >
525+ </ CollapsibleContent >
526+ </ SidebarMenuItem >
527+ </ Collapsible >
528+ ) }
529+
530+ { /* Static system items */ }
414531 < SidebarMenuItem >
415532 < SidebarMenuButton
416533 tooltip = "API Console"
0 commit comments