Skip to content

Commit 782641e

Browse files
committed
Refactor app metadata: rename 'name' to 'label', update menu types
Standardizes app metadata by renaming the display property from 'name' to 'label' and making 'name' the unique identifier. Updates documentation, example YAML, and TypeScript types accordingly. Improves menu item and section typing, adds default values, and refines sidebar menu rendering logic for robustness and consistency.
1 parent f933541 commit 782641e

File tree

7 files changed

+54
-68
lines changed

7 files changed

+54
-68
lines changed

docs/spec/metadata-format.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ App files define application interfaces with custom navigation menus, similar to
212212

213213
| Property | Type | Description |
214214
| :--- | :--- | :--- |
215-
| `code` | `string` | Unique identifier for the app, default to id if not specified. |
216-
| `name` | `string` | **Required.** Display name of the app. |
215+
| `name` | `string` | Unique identifier for the app, default to id if not specified. |
216+
| `label` | `string` | **Required.** Display name of the app. |
217217
| `description` | `string` | Description of the app's purpose. |
218218
| `icon` | `string` | Icon identifier (e.g., `ri-dashboard-line`). |
219219
| `color` | `string` | Color theme for the app (e.g., `blue`, `gray`). |
@@ -283,8 +283,8 @@ The `menu` property defines the left-side navigation structure. It can be either
283283
### 6.4 Complete App Example
284284

285285
```yaml
286-
code: projects
287-
name: Project Management
286+
name: projects
287+
label: Project Management
288288
description: Manage and track your projects efficiently.
289289
icon: ri-dashboard-line
290290
color: blue

examples/project-management/src/projects.app.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# ==========================================
22
# 1. 应用基础信息 (App Identity)
33
# ==========================================
4-
code: projects
5-
name: Projects Management
4+
5+
name: projects
6+
label: Projects Management
67
description: Manage and track your projects efficiently.
78
dark: true
89
color: gray

packages/client/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react';
1+
import { useState, useEffect, useRef } from 'react';
22
import AppList from './pages/AppList';
33
import Login from './pages/Login';
44
import Dashboard from './pages/Dashboard';
@@ -26,7 +26,7 @@ function AppContent() {
2626
if (parts[1] === 'app' && parts[2]) {
2727
const appName = parts[2];
2828
// Simple cache check/avoid refetch if same app
29-
if (currentAppMetadata && (currentAppMetadata.id === appName || currentAppMetadata.content?.code === appName)) {
29+
if (currentAppMetadata && (currentAppMetadata.name === appName || currentAppMetadata.id === appName)) {
3030
return;
3131
}
3232

packages/client/src/components/app-sidebar.tsx

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ export function AppSidebar({ objects, appMetadata, ...props }: React.ComponentPr
3434
const currentApp = parts[1] === 'app' ? parts[2] : null;
3535
const getObjectPath = (name: string) => currentApp ? `/app/${currentApp}/object/${name}` : `/object/${name}`;
3636

37-
const rawMenu = appMetadata?.content?.menu;
37+
const rawMenu = appMetadata?.menu;
3838

39-
// Normalize menu structure to always be a list of sections (groups)
40-
// If the first item has a 'type' or doesn't look like a section container, wrap it in a default section.
41-
const isGrouped = Array.isArray(rawMenu) && rawMenu.length > 0 &&
42-
(rawMenu[0].items && !rawMenu[0].type);
39+
// Robust check for grouped vs flat menu structure
40+
// A section has 'items' but NO 'type', 'object', or 'url'
41+
const isSection = (item: any) => item && item.items && Array.isArray(item.items) && !item.type && !item.object && !item.url;
42+
43+
const isGrouped = Array.isArray(rawMenu) && rawMenu.length > 0 && isSection(rawMenu[0]);
4344

4445
const menuSections = rawMenu ? (isGrouped ? rawMenu : [{ label: 'Menu', items: rawMenu }]) : [];
4546

@@ -51,48 +52,53 @@ export function AppSidebar({ objects, appMetadata, ...props }: React.ComponentPr
5152
return <div key={idx} className="my-2 h-px bg-border" />;
5253
}
5354

55+
// Default label if missing (e.g. for dividers context or malformed data)
56+
const label = item.label || '';
57+
const itemType = item.type || 'page';
58+
5459
// Determine active state
5560
let isActive = false;
56-
if (item.type === 'object') {
61+
if (itemType === 'object') {
5762
isActive = path.includes(`/object/${item.object}`);
58-
} else if (item.type === 'page' || item.type === 'url') {
63+
} else if (itemType === 'page' || itemType === 'url') {
5964
isActive = item.url ? path.endsWith(item.url) : false;
6065
}
6166

6267
const handleClick = () => {
63-
if (item.type === 'object' && item.object) {
68+
if (itemType === 'object' && item.object) {
6469
navigate(getObjectPath(item.object));
65-
} else if (item.type === 'page' && item.url) {
70+
} else if (itemType === 'page' && item.url) {
6671
navigate(item.url);
67-
} else if (item.type === 'url' && item.url) {
72+
} else if (itemType === 'url' && item.url) {
6873
window.open(item.url, '_blank');
6974
}
7075
};
7176

7277
// Handle Nested Items (Submenus)
7378
if (item.items && item.items.length > 0) {
7479
return (
75-
<Collapsible key={idx} asChild defaultOpen={item.collapsed === false} className="group/collapsible">
80+
<Collapsible key={idx} asChild defaultOpen={false} className="group/collapsible">
7681
<SidebarMenuItem>
7782
<CollapsibleTrigger asChild>
78-
<SidebarMenuButton tooltip={item.label}>
79-
<DynamicIcon name={item.icon} fallback={item.type === 'object' ? LucideIcons.FileText : LucideIcons.Layout} />
80-
<span>{item.label}</span>
83+
<SidebarMenuButton tooltip={label}>
84+
<DynamicIcon name={item.icon} fallback={itemType === 'object' ? LucideIcons.FileText : LucideIcons.Layout} />
85+
<span>{label}</span>
8186
<LucideIcons.ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
8287
</SidebarMenuButton>
8388
</CollapsibleTrigger>
8489
<CollapsibleContent>
8590
<SidebarMenuSub>
8691
{item.items.map((subItem: any, subIdx: number) => {
8792
if (subItem.visible === false) return null;
93+
const subItemType = subItem.type || 'page';
8894
return (
8995
<SidebarMenuSubItem key={subIdx}>
9096
<SidebarMenuSubButton
91-
isActive={subItem.type === 'object' ? path.includes(`/object/${subItem.object}`) : path.endsWith(subItem.url)}
97+
isActive={subItemType === 'object' ? path.includes(`/object/${subItem.object}`) : path.endsWith(subItem.url)}
9298
onClick={() => {
93-
if (subItem.type === 'object') navigate(getObjectPath(subItem.object));
94-
else if (subItem.type === 'page') navigate(subItem.url);
95-
else if (subItem.type === 'url') window.open(subItem.url, '_blank');
99+
if (subItemType === 'object') navigate(getObjectPath(subItem.object));
100+
else if (subItemType === 'page') navigate(subItem.url);
101+
else if (subItemType === 'url') window.open(subItem.url, '_blank');
96102
}}
97103
>
98104
<span>{subItem.label}</span>
@@ -114,8 +120,8 @@ export function AppSidebar({ objects, appMetadata, ...props }: React.ComponentPr
114120
isActive={isActive}
115121
onClick={handleClick}
116122
>
117-
<DynamicIcon name={item.icon} fallback={item.type === 'object' ? LucideIcons.FileText : LucideIcons.Layout} />
118-
<span>{item.label}</span>
123+
<DynamicIcon name={item.icon} fallback={itemType === 'object' ? LucideIcons.FileText : LucideIcons.Layout} />
124+
<span>{label}</span>
119125
{item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
120126
</SidebarMenuButton>
121127
</SidebarMenuItem>
@@ -132,16 +138,15 @@ export function AppSidebar({ objects, appMetadata, ...props }: React.ComponentPr
132138
<LucideIcons.Database className="size-4" />
133139
</div>
134140
<div className="grid flex-1 text-left text-sm leading-tight">
135-
<span className="truncate font-semibold">{appMetadata?.content?.name || 'ObjectQL'}</span>
141+
<span className="truncate font-semibold">{appMetadata?.name || 'ObjectQL'}</span>
136142
<span className="truncate text-xs">Data Browser</span>
137143
</div>
138144
</SidebarMenuButton>
139145
</SidebarMenuItem>
140146
</SidebarMenu>
141147
</SidebarHeader>
142148
<SidebarContent>
143-
{menuSections.length > 0 ? (
144-
menuSections.map((section: any, idx: number) => {
149+
{menuSections.map((section: any, idx: number) => {
145150
const isCollapsible = section.collapsible === true;
146151
const isCollapsed = section.collapsed === true;
147152

@@ -184,28 +189,7 @@ export function AppSidebar({ objects, appMetadata, ...props }: React.ComponentPr
184189
</SidebarGroupContent>
185190
</SidebarGroup>
186191
);
187-
})
188-
) : (
189-
// Render Default Object List
190-
<SidebarGroup>
191-
<SidebarGroupLabel>Collections</SidebarGroupLabel>
192-
<SidebarGroupContent>
193-
<SidebarMenu>
194-
{Object.entries(objects).map(([name, schema]) => (
195-
<SidebarMenuItem key={name}>
196-
<SidebarMenuButton
197-
isActive={path.includes(`/object/${name}`)}
198-
onClick={() => navigate(getObjectPath(name))}
199-
>
200-
<LucideIcons.FileText />
201-
<span>{schema.label || schema.title || name}</span>
202-
</SidebarMenuButton>
203-
</SidebarMenuItem>
204-
))}
205-
</SidebarMenu>
206-
</SidebarGroupContent>
207-
</SidebarGroup>
208-
)}
192+
})}
209193
</SidebarContent>
210194
<SidebarFooter>
211195
<NavUser user={{

packages/client/src/pages/Dashboard.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ export default function Dashboard() {
7070
});
7171
}
7272

73-
const objNames = Object.keys(objectsMap);
7473
setObjects(objectsMap);
7574
// Auto-redirect removed to show App List
7675
setLoading(false);

packages/metadata/src/plugins/objectql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function registerObjectQLPlugins(loader: MetadataLoader) {
3737
handler: (ctx) => {
3838
try {
3939
const doc = yaml.load(ctx.content) as any;
40-
const id = doc.id || doc.name;
40+
const id = doc.code || doc.id || doc.name;
4141
if (id) {
4242
ctx.registry.register('app', {
4343
type: 'app',

packages/metadata/src/types.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,19 +119,19 @@ export interface ObjectConfig {
119119
export interface AppMenuItem {
120120
/** Unique identifier for the menu item */
121121
id?: string;
122-
/** Display label for the menu item */
123-
label?: string;
122+
/** **Required.** Display label for the menu item */
123+
label: string;
124124
/** Icon identifier (e.g., remixicon class name like 'ri-home-line') */
125125
icon?: string;
126-
/** Type of menu item */
126+
/** Type of menu item. Default: 'page' */
127127
type?: 'object' | 'page' | 'url' | 'divider';
128128
/** Reference to an object name (for type: 'object') */
129129
object?: string;
130130
/** URL path (for type: 'url' or 'page') */
131131
url?: string;
132132
/** Badge text or count to display */
133133
badge?: string | number;
134-
/** Whether this item is visible */
134+
/** Whether this item is visible. Default: true */
135135
visible?: boolean;
136136
/** Nested sub-menu items */
137137
items?: AppMenuItem[];
@@ -145,9 +145,9 @@ export interface AppMenuSection {
145145
label?: string;
146146
/** **Required.** Menu items in this section. */
147147
items: AppMenuItem[];
148-
/** Whether this section is collapsible */
148+
/** Whether this section is collapsible. Default: false */
149149
collapsible?: boolean;
150-
/** Whether this section is collapsed by default */
150+
/** Whether this section is collapsed by default. Default: false */
151151
collapsed?: boolean;
152152
}
153153

@@ -171,22 +171,24 @@ export function isAppMenuSection(entry: AppMenuSection | AppMenuItem): entry is
171171
export interface AppConfig {
172172
/** Unique identifier or code for the app */
173173
id?: string;
174-
/** App name */
174+
/** App name. Unique identifier for the app, default to id if not specified. */
175175
name: string;
176-
/** App code/slug */
176+
/** App code/slug (Legacy, use name or id) */
177177
code?: string;
178-
/** Description of the app */
178+
/** **Required.** Display name of the app. */
179+
label: string;
180+
/** Description of the app's purpose. */
179181
description?: string;
180-
/** App icon identifier */
182+
/** Icon identifier (e.g., `ri-dashboard-line`). */
181183
icon?: string;
182-
/** Color theme for the app */
184+
/** Color theme for the app (e.g., `blue`, `gray`). */
183185
color?: string;
184-
/** Dark mode preference */
186+
/** Whether to use dark mode by default. */
185187
dark?: boolean;
186188
/** Base ID this app belongs to (optional, for Base layer support) */
187189
baseId?: string;
188190
/**
189-
* Left-side menu configuration.
191+
* Left-side navigation menu configuration.
190192
* Can be either:
191193
* - An array of menu sections (recommended for organized menus with groups)
192194
* - An array of menu items (for simple flat menus)

0 commit comments

Comments
 (0)