Skip to content

Commit 6519b8b

Browse files
authored
Merge pull request #918 from objectstack-ai/copilot/fix-studio-system-objects-display
2 parents f00a3e2 + c2fac1c commit 6519b8b

9 files changed

Lines changed: 180 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **Studio system objects visibility** — Studio now auto-registers all system objects (sys_user,
12+
sys_role, sys_audit_log, etc.) from plugin-auth, plugin-security, and plugin-audit at kernel
13+
initialization. The sidebar "System" group dynamically lists all `isSystem=true` objects
14+
with a collapsible "System Objects" section. A filter toggle on the Data group allows
15+
showing/hiding system objects from the main object list.
1116
- **ObjectSchema `namespace` property** — New optional `namespace` field on `ObjectSchema` for logical domain
1217
classification (e.g., `'sys'`, `'crm'`). When set, `tableName` is auto-derived as `{namespace}_{name}` by
1318
`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
2429
(`{namespace}_{name}` fallback when no explicit `tableName` is set).
2530

2631
### Changed
32+
- **ObjectFilterSchema `includeSystem` default** — Changed from `false` to `true`. Studio
33+
ObjectManager now includes system objects by default. Users can toggle visibility via the
34+
Data group filter control.
2735
- **System object naming convention** — All system objects now use `namespace: 'sys'` with short `name`
2836
(e.g., `name: 'user'` instead of `name: 'sys_user'`). The `sys_` prefix is auto-derived via
2937
`tableName` = `{namespace}_{name}`. File naming follows `sys-{name}.object.ts` pattern.

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,7 @@ Final polish and advanced features.
779779
### 8.3 Studio IDE
780780

781781
- [x] Object Designer Protocol — field editor, relationship mapper, ER diagram, object manager schemas defined (`studio/object-designer.zod.ts`)
782+
- [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.
782783
- [ ] Object Designer Runtime — visual field editor with inline editing, drag-reorder, type-aware property panels
783784
- [ ] Relationship Mapper — visual lookup/master-detail/tree creation with drag-to-connect
784785
- [ ] ER Diagram — interactive entity-relationship diagram with force/hierarchy/grid layouts, minimap, zoom, export (PNG/SVG)

apps/studio/ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
| No component-level tests | Regression risk | 🟡 Medium |
4040
| Data refresh via `setTimeout` hack | Race conditions | 🟡 Medium |
4141
| Sidebar groups hardcoded (not reading from plugins) | Plugin contributions ignored | 🟡 Medium |
42+
| System objects not visible in Studio | Cannot manage users, roles, audit logs | ✅ Fixed |
4243

4344
### Protocol Coverage Gap
4445

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

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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"
3638
import { useClient } from '@objectstack/client-react';
3739
import 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 */
129131
const 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 */
132152
const 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"

apps/studio/src/mocks/createKernel.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@ import { ObjectQLPlugin, SchemaRegistry } from '@objectstack/objectql';
55
import { InMemoryDriver } from '@objectstack/driver-memory';
66
import { MSWPlugin } from '@objectstack/plugin-msw';
77

8+
// System object definitions — resolved via Vite aliases to plugin source (no runtime deps)
9+
import {
10+
SysUser, SysSession, SysAccount, SysVerification,
11+
SysOrganization, SysMember, SysInvitation,
12+
SysTeam, SysTeamMember,
13+
SysApiKey, SysTwoFactor,
14+
} from '@objectstack/plugin-auth/objects';
15+
import { SysRole, SysPermissionSet } from '@objectstack/plugin-security/objects';
16+
import { SysAuditLog } from '@objectstack/plugin-audit/objects';
17+
18+
/** All system objects from auth, security, and audit plugins */
19+
const SYSTEM_OBJECTS = [
20+
// Auth
21+
SysUser, SysSession, SysAccount, SysVerification,
22+
SysOrganization, SysMember, SysInvitation,
23+
SysTeam, SysTeamMember,
24+
SysApiKey, SysTwoFactor,
25+
// Security
26+
SysRole, SysPermissionSet,
27+
// Audit
28+
SysAuditLog,
29+
];
30+
831
export interface KernelOptions {
932
appConfigs?: any[]; // Multiple app configs
1033
appConfig?: any; // Legacy single app config (backward compat)
@@ -30,6 +53,21 @@ export async function createKernel(options: KernelOptions) {
3053
// Register the driver
3154
await kernel.use(new DriverPlugin(driver, 'memory'));
3255

56+
// Register system objects (auth, security, audit) as a built-in system package
57+
const systemConfig = {
58+
name: 'system',
59+
manifest: {
60+
id: 'com.objectstack.system',
61+
name: 'System',
62+
version: '1.0.0',
63+
type: 'plugin',
64+
namespace: 'sys',
65+
},
66+
objects: SYSTEM_OBJECTS,
67+
};
68+
console.log('[KernelFactory] Loading system objects:', SYSTEM_OBJECTS.length);
69+
await kernel.use(new AppPlugin(systemConfig));
70+
3371
// Load all app configs as plugins (handles object registration & seeding)
3472
for (const appConfig of allConfigs) {
3573
console.log('[KernelFactory] Loading app:', appConfig.manifest?.id || appConfig.name || 'unknown');

apps/studio/vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export default defineConfig({
1616
'react': path.resolve(__dirname, './node_modules/react'),
1717
'react-dom': path.resolve(__dirname, './node_modules/react-dom'),
1818
'@': path.resolve(__dirname, './src'),
19+
// System object definitions — resolve to plugin source (no runtime deps)
20+
'@objectstack/plugin-auth/objects': path.resolve(__dirname, '../../packages/plugins/plugin-auth/src/objects/index.ts'),
21+
'@objectstack/plugin-security/objects': path.resolve(__dirname, '../../packages/plugins/plugin-security/src/objects/index.ts'),
22+
'@objectstack/plugin-audit/objects': path.resolve(__dirname, '../../packages/plugins/plugin-audit/src/objects/index.ts'),
1923
'node:fs/promises': path.resolve(__dirname, './mocks/node-polyfills.ts'),
2024
'node:fs': path.resolve(__dirname, './mocks/node-polyfills.ts'),
2125
'node:events': path.resolve(__dirname, './mocks/node-polyfills.ts'),

content/docs/references/studio/object-designer.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Default view when entering the Object Designer
261261
| :--- | :--- | :--- | :--- |
262262
| **package** | `string` | optional | Filter by owning package |
263263
| **tags** | `string[]` | optional | Filter by object tags |
264-
| **includeSystem** | `boolean` || Include system-level objects |
264+
| **includeSystem** | `boolean` || Include system-level objects (default: `true`) |
265265
| **includeAbstract** | `boolean` || Include abstract base objects |
266266
| **hasFieldType** | `string` | optional | Filter to objects containing a specific field type |
267267
| **hasRelationships** | `boolean` | optional | Filter to objects with lookup/master_detail fields |

packages/spec/src/studio/object-designer.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ describe('ObjectSortFieldSchema', () => {
307307
describe('ObjectFilterSchema', () => {
308308
it('should accept empty object with defaults', () => {
309309
const result = ObjectFilterSchema.parse({});
310-
expect(result.includeSystem).toBe(false);
310+
expect(result.includeSystem).toBe(true);
311311
expect(result.includeAbstract).toBe(false);
312312
expect(result.package).toBeUndefined();
313313
expect(result.tags).toBeUndefined();
@@ -448,6 +448,7 @@ describe('ObjectDesignerConfigSchema', () => {
448448
expect(result.erDiagram.enabled).toBe(true);
449449
expect(result.objectManager).toBeDefined();
450450
expect(result.objectManager.defaultDisplayMode).toBe('table');
451+
expect(result.objectManager.defaultFilter.includeSystem).toBe(true);
451452
expect(result.objectPreview).toBeDefined();
452453
expect(result.objectPreview.tabs.length).toBe(8);
453454
});

packages/spec/src/studio/object-designer.zod.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ export const ObjectFilterSchema = z.object({
328328
tags: z.array(z.string()).optional().describe('Filter by object tags'),
329329

330330
/** Show system objects */
331-
includeSystem: z.boolean().default(false).describe('Include system-level objects'),
331+
includeSystem: z.boolean().default(true).describe('Include system-level objects'),
332332

333333
/** Show abstract objects */
334334
includeAbstract: z.boolean().default(false).describe('Include abstract base objects'),
@@ -361,7 +361,7 @@ export const ObjectManagerConfigSchema = z.object({
361361

362362
/** Default filters */
363363
defaultFilter: ObjectFilterSchema.default({
364-
includeSystem: false,
364+
includeSystem: true,
365365
includeAbstract: false,
366366
}).describe('Default filter configuration'),
367367

@@ -547,7 +547,7 @@ export const ObjectDesignerConfigSchema = z.object({
547547
defaultSortField: 'label',
548548
defaultSortDirection: 'asc',
549549
defaultFilter: {
550-
includeSystem: false,
550+
includeSystem: true,
551551
includeAbstract: false,
552552
},
553553
showFieldCount: true,

0 commit comments

Comments
 (0)