diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4c30097d..02f07faeee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Studio system objects visibility** — Studio now auto-registers all system objects (sys_user, + sys_role, sys_audit_log, etc.) from plugin-auth, plugin-security, and plugin-audit at kernel + initialization. The sidebar "System" group dynamically lists all `isSystem=true` objects + with a collapsible "System Objects" section. A filter toggle on the Data group allows + showing/hiding system objects from the main object list. - **ObjectSchema `namespace` property** — New optional `namespace` field on `ObjectSchema` for logical domain classification (e.g., `'sys'`, `'crm'`). When set, `tableName` is auto-derived as `{namespace}_{name}` by `ObjectSchema.create()` unless an explicit `tableName` is provided. This decouples the logical object name @@ -24,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (`{namespace}_{name}` fallback when no explicit `tableName` is set). ### Changed +- **ObjectFilterSchema `includeSystem` default** — Changed from `false` to `true`. Studio + ObjectManager now includes system objects by default. Users can toggle visibility via the + Data group filter control. - **System object naming convention** — All system objects now use `namespace: 'sys'` with short `name` (e.g., `name: 'user'` instead of `name: 'sys_user'`). The `sys_` prefix is auto-derived via `tableName` = `{namespace}_{name}`. File naming follows `sys-{name}.object.ts` pattern. diff --git a/ROADMAP.md b/ROADMAP.md index b936e70bf7..190cc5edad 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -779,6 +779,7 @@ Final polish and advanced features. ### 8.3 Studio IDE - [x] Object Designer Protocol — field editor, relationship mapper, ER diagram, object manager schemas defined (`studio/object-designer.zod.ts`) +- [x] System Objects Visibility — Studio sidebar dynamically lists all system objects (sys_user, sys_role, sys_audit_log, etc.) under a "System Objects" collapsible group. Mock kernel auto-registers auth/security/audit system objects. Data group filter toggle to show/hide system objects. - [ ] Object Designer Runtime — visual field editor with inline editing, drag-reorder, type-aware property panels - [ ] Relationship Mapper — visual lookup/master-detail/tree creation with drag-to-connect - [ ] ER Diagram — interactive entity-relationship diagram with force/hierarchy/grid layouts, minimap, zoom, export (PNG/SVG) diff --git a/apps/studio/ROADMAP.md b/apps/studio/ROADMAP.md index ccc8803523..2457f6e161 100644 --- a/apps/studio/ROADMAP.md +++ b/apps/studio/ROADMAP.md @@ -39,6 +39,7 @@ | No component-level tests | Regression risk | 🟡 Medium | | Data refresh via `setTimeout` hack | Race conditions | 🟡 Medium | | Sidebar groups hardcoded (not reading from plugins) | Plugin contributions ignored | 🟡 Medium | +| System objects not visible in Studio | Cannot manage users, roles, audit logs | ✅ Fixed | ### Protocol Coverage Gap diff --git a/apps/studio/src/components/app-sidebar.tsx b/apps/studio/src/components/app-sidebar.tsx index 1cb88f9e9f..7dc1f931ff 100644 --- a/apps/studio/src/components/app-sidebar.tsx +++ b/apps/studio/src/components/app-sidebar.tsx @@ -19,6 +19,7 @@ import { AppWindow, Layers, Eye, + EyeOff, FileCode, Palette, CheckSquare, @@ -30,9 +31,10 @@ import { Anchor, UserCog, ChevronRight, + Settings, type LucideIcon, } from "lucide-react" -import { useState, useEffect, useCallback } from "react" +import { useState, useEffect, useCallback, useMemo } from "react" import { useClient } from '@objectstack/client-react'; import type { InstalledPackage } from '@objectstack/spec/kernel'; @@ -128,6 +130,24 @@ const PROTOCOL_GROUPS: ProtocolGroup[] = [ /** Types that are internal / should be hidden from the sidebar */ const HIDDEN_TYPES = new Set(['plugin', 'plugins', 'kind', 'app', 'apps', 'package']); +/** System namespace used for FQN-based names (e.g., sys__user) */ +const SYSTEM_NAMESPACE = 'sys'; + +/** System object FQN prefix (namespace + double underscore separator) */ +const SYSTEM_FQN_PREFIX = `${SYSTEM_NAMESPACE}__`; + +/** Legacy system object name prefix (namespace + single underscore) */ +const SYSTEM_LEGACY_PREFIX = `${SYSTEM_NAMESPACE}_`; + +/** Check if an object item is a system object */ +function isSystemObject(item: any): boolean { + if (item.isSystem === true) return true; + if (item.namespace === SYSTEM_NAMESPACE) return true; + const name = item.name || item.id || ''; + // Match FQN format (sys__user) or legacy format (sys_user) + return name.startsWith(SYSTEM_FQN_PREFIX) || name.startsWith(SYSTEM_LEGACY_PREFIX); +} + /** Icon mapping for package types */ const PKG_TYPE_ICONS: Record = { app: AppWindow, plugin: Layers, driver: Database, server: Globe, @@ -161,6 +181,9 @@ export function AppSidebar({ // Track which metadata *types* are expanded (show individual items) const [expandedTypes, setExpandedTypes] = useState>(new Set(['object', 'objects'])); + // Toggle to show/hide system objects in the Data protocol group + const [showSystemInData, setShowSystemInData] = useState(true); + const toggleTypeExpanded = (type: string) => { setExpandedTypes(prev => { const next = new Set(prev); @@ -216,12 +239,35 @@ export function AppSidebar({ label.toLowerCase().includes(searchQuery.toLowerCase()) || name.toLowerCase().includes(searchQuery.toLowerCase()); + // Extract system objects from loaded metadata (object/objects types) + const systemObjects = useMemo(() => { + const objectTypes = ['object', 'objects']; + const sysItems: any[] = []; + for (const type of objectTypes) { + const items = metaItems[type] || []; + sysItems.push(...items.filter(isSystemObject)); + } + return sysItems; + }, [metaItems]); + + // Filter system objects out of the Data protocol group when toggled off + const filteredMetaItems = useMemo(() => { + if (showSystemInData) return metaItems; + const result = { ...metaItems }; + for (const type of ['object', 'objects']) { + if (result[type]) { + result[type] = result[type].filter((item: any) => !isSystemObject(item)); + } + } + return result; + }, [metaItems, showSystemInData]); + // Compute visible groups: only show groups that have at least one type with items const visibleGroups = PROTOCOL_GROUPS.map(group => { const visibleTypes = group.types.filter(t => - metaTypes.includes(t) && !HIDDEN_TYPES.has(t) && (metaItems[t]?.length ?? 0) > 0 + metaTypes.includes(t) && !HIDDEN_TYPES.has(t) && (filteredMetaItems[t]?.length ?? 0) > 0 ); - const totalItems = visibleTypes.reduce((sum, t) => sum + (metaItems[t]?.length ?? 0), 0); + const totalItems = visibleTypes.reduce((sum, t) => sum + (filteredMetaItems[t]?.length ?? 0), 0); return { ...group, visibleTypes, totalItems }; }).filter(g => g.totalItems > 0); @@ -328,11 +374,25 @@ export function AppSidebar({ {group.label} {group.totalItems} + {/* System objects filter toggle for Data group */} + {group.key === 'data' && systemObjects.length > 0 && ( + + )} {group.visibleTypes.map(type => { - const items = metaItems[type] || []; + const items = filteredMetaItems[type] || []; const TypeIcon = getTypeIcon(type); const typeLabel = getTypeLabel(type); const isObjectType = type === 'object' || type === 'objects'; @@ -408,9 +468,66 @@ export function AppSidebar({ {/* ── System ── */} - System + + + System + {systemObjects.length > 0 && ( + {systemObjects.length} + )} + + {/* Dynamic system objects */} + {systemObjects.length > 0 && ( + { + const isExpanded = expandedTypes.has('_system_objects'); + if (open && !isExpanded) toggleTypeExpanded('_system_objects'); + if (!open && isExpanded) toggleTypeExpanded('_system_objects'); + }} + asChild + > + + + + + System Objects + {systemObjects.length} + + + + + + {systemObjects + .filter((item: any) => matchesSearch(item.label || item.name || '', item.name || '')) + .map((item: any) => { + const itemName = item.name || item.id || 'unknown'; + const itemLabel = item.label || item.name || 'Untitled'; + + return ( + + onSelectObject(itemName)} + > + + {isSystemObject(item) && ( + {SYSTEM_NAMESPACE}: + )} + {itemLabel} + + + + ); + })} + + + + + )} + + {/* Static system items */} { describe('ObjectFilterSchema', () => { it('should accept empty object with defaults', () => { const result = ObjectFilterSchema.parse({}); - expect(result.includeSystem).toBe(false); + expect(result.includeSystem).toBe(true); expect(result.includeAbstract).toBe(false); expect(result.package).toBeUndefined(); expect(result.tags).toBeUndefined(); @@ -448,6 +448,7 @@ describe('ObjectDesignerConfigSchema', () => { expect(result.erDiagram.enabled).toBe(true); expect(result.objectManager).toBeDefined(); expect(result.objectManager.defaultDisplayMode).toBe('table'); + expect(result.objectManager.defaultFilter.includeSystem).toBe(true); expect(result.objectPreview).toBeDefined(); expect(result.objectPreview.tabs.length).toBe(8); }); diff --git a/packages/spec/src/studio/object-designer.zod.ts b/packages/spec/src/studio/object-designer.zod.ts index a34f220196..10063608a1 100644 --- a/packages/spec/src/studio/object-designer.zod.ts +++ b/packages/spec/src/studio/object-designer.zod.ts @@ -328,7 +328,7 @@ export const ObjectFilterSchema = z.object({ tags: z.array(z.string()).optional().describe('Filter by object tags'), /** Show system objects */ - includeSystem: z.boolean().default(false).describe('Include system-level objects'), + includeSystem: z.boolean().default(true).describe('Include system-level objects'), /** Show abstract objects */ includeAbstract: z.boolean().default(false).describe('Include abstract base objects'), @@ -361,7 +361,7 @@ export const ObjectManagerConfigSchema = z.object({ /** Default filters */ defaultFilter: ObjectFilterSchema.default({ - includeSystem: false, + includeSystem: true, includeAbstract: false, }).describe('Default filter configuration'), @@ -547,7 +547,7 @@ export const ObjectDesignerConfigSchema = z.object({ defaultSortField: 'label', defaultSortDirection: 'asc', defaultFilter: { - includeSystem: false, + includeSystem: true, includeAbstract: false, }, showFieldCount: true,