Skip to content

Commit f933541

Browse files
committed
Add dynamic app menu and app list page to client
Introduces a new AppList page for displaying available apps and implements a dynamic sidebar menu based on app metadata. Adds a DynamicIcon component for flexible icon rendering, updates the metadata spec and example YAML to clarify app and menu structure, and enhances the sidebar to support grouped, nested, and custom menu items.
1 parent 2729d83 commit f933541

File tree

6 files changed

+460
-73
lines changed

6 files changed

+460
-73
lines changed

docs/spec/metadata-format.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +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. |
215216
| `name` | `string` | **Required.** Display name of the app. |
216-
| `id` | `string` | Unique identifier for the app. Defaults to `name` if not provided. |
217-
| `code` | `string` | URL-friendly code/slug for the app. |
218217
| `description` | `string` | Description of the app's purpose. |
219218
| `icon` | `string` | Icon identifier (e.g., `ri-dashboard-line`). |
220219
| `color` | `string` | Color theme for the app (e.g., `blue`, `gray`). |
@@ -231,8 +230,8 @@ The `menu` property defines the left-side navigation structure. It can be either
231230

232231
| Property | Type | Description |
233232
| :--- | :--- | :--- |
234-
| `label` | `string` | **Required.** Display label for the menu item. |
235233
| `id` | `string` | Unique identifier for the menu item. |
234+
| `label` | `string` | **Required.** Display label for the menu item. |
236235
| `icon` | `string` | Icon identifier (e.g., `ri-home-line`, `ri-dashboard-line`). |
237236
| `type` | `string` | Type: `object`, `page`, `url`, or `divider`. Default: `page`. |
238237
| `object` | `string` | Object name to link to (when `type: object`). |
@@ -284,9 +283,9 @@ The `menu` property defines the left-side navigation structure. It can be either
284283
### 6.4 Complete App Example
285284

286285
```yaml
286+
code: projects
287287
name: Project Management
288288
description: Manage and track your projects efficiently.
289-
code: projects
290289
icon: ri-dashboard-line
291290
color: blue
292291
dark: false
Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,80 @@
1+
# ==========================================
2+
# 1. 应用基础信息 (App Identity)
3+
# ==========================================
4+
code: projects
15
name: Projects Management
26
description: Manage and track your projects efficiently.
3-
code: projects
47
dark: true
58
color: gray
69
icon: ri-settings-line
10+
sort_no: 10 # 在应用启动器中的排序
711

8-
# Left-side menu configuration (similar to Airtable interfaces)
12+
# ==========================================
13+
# 3. 导航菜单定义 (Navigation Tree)
14+
# ==========================================
15+
# 结构说明:支持 Group(分组), Page(自定义页), Object(对象列表), URL(外链)
916
menu:
10-
# Main section
11-
- label: Main
12-
items:
13-
- label: Dashboard
14-
icon: ri-dashboard-line
15-
type: page
16-
url: /dashboard
17-
- label: All Projects
18-
icon: ri-building-line
17+
# --- 模块 A: 仪表盘 ---
18+
- id: home_dashboard
19+
type: page
20+
label: 项目概览
21+
icon: layout-dashboard
22+
page_id: pm_overview_dashboard # 指向 pages 表中的一个 Amis/Liquid 页面
23+
24+
# --- 模块 B: 核心业务 (分组) ---
25+
- id: group_core
26+
type: group
27+
label: 项目执行
28+
children:
29+
# 1. 项目列表 (标准对象)
30+
- id: nav_projects
1931
type: object
20-
object: projects
21-
- label: Active Tasks
22-
icon: ri-checkbox-circle-line
32+
label: 所有项目
33+
icon: folder-kanban
34+
object_name: projects
35+
default_view: all # 点击后默认打开 "全部" 视图
36+
37+
# 2. 任务 (带过滤器的菜单 - 类似 ServiceNow 模式)
38+
- id: nav_my_tasks
2339
type: object
24-
object: tasks
25-
badge: 12
26-
27-
# Views section
28-
- label: Views
29-
collapsible: true
30-
items:
31-
- label: My Projects
32-
icon: ri-user-line
33-
type: page
34-
url: /my-projects
35-
- label: Team Calendar
36-
icon: ri-calendar-line
37-
type: page
38-
url: /calendar
39-
- label: Reports
40-
icon: ri-bar-chart-line
41-
type: page
42-
url: /reports
43-
44-
# Settings section
45-
- label: Settings
46-
collapsible: true
47-
collapsed: true
48-
items:
49-
- label: App Settings
50-
icon: ri-settings-3-line
40+
label: 我的待办任务
41+
icon: check-circle-2
42+
object_name: tasks
43+
# 关键:菜单携带预设过滤条件
44+
filters:
45+
- [ "owner", "=", "{currentUser}" ]
46+
- [ "status", "!=", "completed" ]
47+
48+
# 3. 任务 (普通入口)
49+
- id: nav_all_tasks
50+
type: object
51+
label: 所有任务
52+
icon: list-todo
53+
object_name: tasks
54+
55+
# --- 模块 C: 高级视图 (自定义页面) ---
56+
- id: group_views
57+
type: group
58+
label: 视图与分析
59+
children:
60+
# 1. 甘特图 (这是用 React/UMD 开发的特殊页面)
61+
- id: nav_gantt
5162
type: page
52-
url: /settings
53-
- label: Help & Support
54-
icon: ri-question-line
55-
type: url
56-
url: https://docs.objectql.org
63+
label: 全局甘特图
64+
icon: calendar-range
65+
page_id: global_gantt_view
66+
67+
# 2. 工时表
68+
- id: nav_timesheets
69+
type: object
70+
label: 工时填报
71+
icon: clock
72+
object_name: timesheets
73+
74+
# --- 模块 D: 外部资源 ---
75+
- id: nav_help
76+
type: url
77+
label: 项目管理规范手册
78+
icon: book-open
79+
url: https://wiki.yourcompany.com/pm-guide
80+
target: _blank

packages/client/src/App.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useEffect } from 'react';
2+
import AppList from './pages/AppList';
23
import Login from './pages/Login';
34
import Dashboard from './pages/Dashboard';
45
import Settings from './pages/Settings';
@@ -17,6 +18,32 @@ function AppContent() {
1718
// Let's modify AppContent to render the Main Layout if logged in.
1819

1920
const [objects, setObjects] = useState<Record<string, any>>({});
21+
const [currentAppMetadata, setCurrentAppMetadata] = useState<any>(null);
22+
23+
// Fetch App Metadata when entering an app
24+
useEffect(() => {
25+
const parts = currentPath.split('/');
26+
if (parts[1] === 'app' && parts[2]) {
27+
const appName = parts[2];
28+
// Simple cache check/avoid refetch if same app
29+
if (currentAppMetadata && (currentAppMetadata.id === appName || currentAppMetadata.content?.code === appName)) {
30+
return;
31+
}
32+
33+
fetch(`/api/v6/metadata/app/${appName}`)
34+
.then(res => {
35+
if (!res.ok) throw new Error('App not found');
36+
return res.json();
37+
})
38+
.then(data => setCurrentAppMetadata(data))
39+
.catch(err => {
40+
console.error(err);
41+
setCurrentAppMetadata(null);
42+
});
43+
} else if (currentAppMetadata) {
44+
setCurrentAppMetadata(null);
45+
}
46+
}, [currentPath, currentAppMetadata]);
2047

2148
// We need to fetch objects for the sidebar if we are not in dashboard
2249
useEffect(() => {
@@ -73,9 +100,33 @@ function AppContent() {
73100
}
74101

75102
// Main Layout
103+
if (currentPath === '/' || currentPath === '/apps') {
104+
return (
105+
<SidebarProvider>
106+
<AppSidebar objects={objects} />
107+
<SidebarInset>
108+
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
109+
<SidebarTrigger className="-ml-1" />
110+
<Separator orientation="vertical" className="mr-2 h-4" />
111+
<Breadcrumb>
112+
<BreadcrumbList>
113+
<BreadcrumbItem>
114+
<BreadcrumbPage>Apps</BreadcrumbPage>
115+
</BreadcrumbItem>
116+
</BreadcrumbList>
117+
</Breadcrumb>
118+
</header>
119+
<div className="flex flex-1 flex-col gap-4 p-4 overflow-y-auto">
120+
<AppList />
121+
</div>
122+
</SidebarInset>
123+
</SidebarProvider>
124+
);
125+
}
126+
76127
return (
77128
<SidebarProvider>
78-
<AppSidebar objects={objects} />
129+
<AppSidebar objects={objects} appMetadata={currentAppMetadata} />
79130
<SidebarInset>
80131
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
81132
<SidebarTrigger className="-ml-1" />
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import * as LucideIcons from 'lucide-react';
3+
4+
interface DynamicIconProps extends React.ComponentProps<'svg'> {
5+
name?: string;
6+
fallback?: React.ElementType;
7+
className?: string;
8+
}
9+
10+
export function DynamicIcon({ name, fallback, className, ...props }: DynamicIconProps) {
11+
const Fallback = fallback || LucideIcons.FileText;
12+
13+
if (!name) {
14+
return <Fallback className={className} {...props} />;
15+
}
16+
17+
let iconName = name;
18+
19+
// Handle Remix Icon names (ri-dashboard-line -> Dashboard)
20+
// Common mappings if needed, or just stripping prefixes
21+
if (name.startsWith('ri-')) {
22+
iconName = name
23+
.replace(/^ri-/, '')
24+
.replace(/-line$/, '')
25+
.replace(/-fill$/, '');
26+
}
27+
28+
// Convert kebab-case to PascalCase (dashboard-layout -> DashboardLayout)
29+
const pascalName = iconName
30+
.split('-')
31+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
32+
.join('');
33+
34+
// Try to find the icon in Lucide
35+
// 1. Exact PascalCase match
36+
// 2. Case-insensitive match (less likely needed if normalization is good)
37+
const IconComponent = (LucideIcons as any)[pascalName] || (LucideIcons as any)[iconName];
38+
39+
if (IconComponent) {
40+
return <IconComponent className={className} {...props} />;
41+
}
42+
43+
// If not found, return fallback
44+
return <Fallback className={className} {...props} />;
45+
}

0 commit comments

Comments
 (0)