Skip to content

Commit f9a7744

Browse files
Copilothotlong
andcommitted
feat: enable system objects visibility in Studio sidebar
- Change ObjectFilterSchema.includeSystem default from false to true - Change ObjectManagerConfigSchema.defaultFilter.includeSystem to true - Register system objects (auth/security/audit) in Studio mock kernel - Add dynamic System Objects group in sidebar with collapsible list - Add show/hide system objects toggle on Data group - Update CHANGELOG.md, ROADMAP.md, and documentation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent c5f7eb1 commit f9a7744

10 files changed

Lines changed: 165 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: 105 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,15 @@ 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 object name prefix — objects with this prefix are grouped under "System" */
134+
const SYSTEM_OBJECT_PREFIX = 'sys_';
135+
136+
/** Check if an object item is a system object */
137+
function isSystemObject(item: any): boolean {
138+
const name = item.name || item.id || '';
139+
return item.isSystem === true || name.startsWith(SYSTEM_OBJECT_PREFIX);
140+
}
141+
131142
/** Icon mapping for package types */
132143
const PKG_TYPE_ICONS: Record<string, LucideIcon> = {
133144
app: AppWindow, plugin: Layers, driver: Database, server: Globe,
@@ -161,6 +172,9 @@ export function AppSidebar({
161172
// Track which metadata *types* are expanded (show individual items)
162173
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(new Set(['object', 'objects']));
163174

175+
// Toggle to show/hide system objects in the Data protocol group
176+
const [showSystemInData, setShowSystemInData] = useState(true);
177+
164178
const toggleTypeExpanded = (type: string) => {
165179
setExpandedTypes(prev => {
166180
const next = new Set(prev);
@@ -216,12 +230,35 @@ export function AppSidebar({
216230
label.toLowerCase().includes(searchQuery.toLowerCase()) ||
217231
name.toLowerCase().includes(searchQuery.toLowerCase());
218232

233+
// Extract system objects from loaded metadata (object/objects types)
234+
const systemObjects = useMemo(() => {
235+
const objectTypes = ['object', 'objects'];
236+
const sysItems: any[] = [];
237+
for (const type of objectTypes) {
238+
const items = metaItems[type] || [];
239+
sysItems.push(...items.filter(isSystemObject));
240+
}
241+
return sysItems;
242+
}, [metaItems]);
243+
244+
// Filter system objects out of the Data protocol group when toggled off
245+
const filteredMetaItems = useMemo(() => {
246+
if (showSystemInData) return metaItems;
247+
const result = { ...metaItems };
248+
for (const type of ['object', 'objects']) {
249+
if (result[type]) {
250+
result[type] = result[type].filter((item: any) => !isSystemObject(item));
251+
}
252+
}
253+
return result;
254+
}, [metaItems, showSystemInData]);
255+
219256
// Compute visible groups: only show groups that have at least one type with items
220257
const visibleGroups = PROTOCOL_GROUPS.map(group => {
221258
const visibleTypes = group.types.filter(t =>
222-
metaTypes.includes(t) && !HIDDEN_TYPES.has(t) && (metaItems[t]?.length ?? 0) > 0
259+
metaTypes.includes(t) && !HIDDEN_TYPES.has(t) && (filteredMetaItems[t]?.length ?? 0) > 0
223260
);
224-
const totalItems = visibleTypes.reduce((sum, t) => sum + (metaItems[t]?.length ?? 0), 0);
261+
const totalItems = visibleTypes.reduce((sum, t) => sum + (filteredMetaItems[t]?.length ?? 0), 0);
225262
return { ...group, visibleTypes, totalItems };
226263
}).filter(g => g.totalItems > 0);
227264

@@ -328,11 +365,23 @@ export function AppSidebar({
328365
<group.icon className="mr-1.5 h-3.5 w-3.5" />
329366
<span className="flex-1 min-w-0 truncate">{group.label}</span>
330367
<span className="shrink-0 text-xs tabular-nums text-sidebar-foreground/50">{group.totalItems}</span>
368+
{/* System objects filter toggle for Data group */}
369+
{group.key === 'data' && systemObjects.length > 0 && (
370+
<button
371+
title={showSystemInData ? 'Hide system objects' : 'Show system objects'}
372+
onClick={(e) => { e.stopPropagation(); setShowSystemInData(!showSystemInData); }}
373+
className="ml-1 shrink-0 rounded p-0.5 text-sidebar-foreground/50 hover:text-sidebar-foreground hover:bg-sidebar-accent transition-colors"
374+
>
375+
{showSystemInData
376+
? <Eye className="h-3 w-3" />
377+
: <EyeOff className="h-3 w-3" />}
378+
</button>
379+
)}
331380
</SidebarGroupLabel>
332381
<SidebarGroupContent>
333382
<SidebarMenu>
334383
{group.visibleTypes.map(type => {
335-
const items = metaItems[type] || [];
384+
const items = filteredMetaItems[type] || [];
336385
const TypeIcon = getTypeIcon(type);
337386
const typeLabel = getTypeLabel(type);
338387
const isObjectType = type === 'object' || type === 'objects';
@@ -408,9 +457,60 @@ export function AppSidebar({
408457

409458
{/* ── System ── */}
410459
<SidebarGroup>
411-
<SidebarGroupLabel>System</SidebarGroupLabel>
460+
<SidebarGroupLabel>
461+
<Settings className="mr-1.5 h-3.5 w-3.5" />
462+
<span className="flex-1 min-w-0 truncate">System</span>
463+
{systemObjects.length > 0 && (
464+
<span className="shrink-0 text-xs tabular-nums text-sidebar-foreground/50">{systemObjects.length}</span>
465+
)}
466+
</SidebarGroupLabel>
412467
<SidebarGroupContent>
413468
<SidebarMenu>
469+
{/* Dynamic system objects */}
470+
{systemObjects.length > 0 && (
471+
<Collapsible
472+
open={expandedTypes.has('_system_objects') || !!searchQuery}
473+
onOpenChange={() => toggleTypeExpanded('_system_objects')}
474+
asChild
475+
>
476+
<SidebarMenuItem>
477+
<CollapsibleTrigger asChild>
478+
<SidebarMenuButton tooltip={`System Objects (${systemObjects.length})`}>
479+
<Database className="h-4 w-4" />
480+
<span className="flex-1 min-w-0 truncate">System Objects</span>
481+
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">{systemObjects.length}</span>
482+
<ChevronRight className={`h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ${(expandedTypes.has('_system_objects') || !!searchQuery) ? 'rotate-90' : ''}`} />
483+
</SidebarMenuButton>
484+
</CollapsibleTrigger>
485+
<CollapsibleContent>
486+
<SidebarMenuSub>
487+
{systemObjects
488+
.filter((item: any) => matchesSearch(item.label || item.name || '', item.name || ''))
489+
.map((item: any) => {
490+
const itemName = item.name || item.id || 'unknown';
491+
const itemLabel = item.label || item.name || 'Untitled';
492+
493+
return (
494+
<SidebarMenuSubItem key={itemName}>
495+
<SidebarMenuSubButton
496+
isActive={selectedObject === itemName}
497+
onClick={() => onSelectObject(itemName)}
498+
>
499+
<span className="truncate">
500+
<span className="text-muted-foreground font-mono text-xs">sys_</span>
501+
{itemLabel}
502+
</span>
503+
</SidebarMenuSubButton>
504+
</SidebarMenuSubItem>
505+
);
506+
})}
507+
</SidebarMenuSub>
508+
</CollapsibleContent>
509+
</SidebarMenuItem>
510+
</Collapsible>
511+
)}
512+
513+
{/* Static system items */}
414514
<SidebarMenuItem>
415515
<SidebarMenuButton
416516
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/tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
"noFallthroughCasesInSwitch": true,
2222
"baseUrl": ".",
2323
"paths": {
24-
"@/*": ["./src/*"]
24+
"@/*": ["./src/*"],
25+
"@objectstack/plugin-auth/objects": ["../../packages/plugins/plugin-auth/src/objects/index.ts"],
26+
"@objectstack/plugin-security/objects": ["../../packages/plugins/plugin-security/src/objects/index.ts"],
27+
"@objectstack/plugin-audit/objects": ["../../packages/plugins/plugin-audit/src/objects/index.ts"]
2528
}
2629
},
2730
"include": ["src", "test"],

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: 1 addition & 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();

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

Lines changed: 2 additions & 2 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

0 commit comments

Comments
 (0)