Skip to content

Commit d54568e

Browse files
committed
Add application support and app object definition
Introduces an 'app' object definition to the platform and updates the client to support application context in navigation, sidebar, and dashboard. The dashboard now lists available applications, and navigation logic is updated to handle app-specific object views.
1 parent 1cd62b2 commit d54568e

File tree

5 files changed

+139
-24
lines changed

5 files changed

+139
-24
lines changed

packages/client/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,16 @@ function AppContent() {
8484
<BreadcrumbList>
8585
<BreadcrumbItem>
8686
<BreadcrumbPage>
87-
{currentPath === '/settings' ? 'Settings' : 'Dashboard'}
87+
{(() => {
88+
if (currentPath === '/settings') return 'Settings';
89+
if (currentPath === '/organization') return 'Organization';
90+
91+
const parts = currentPath.split('/');
92+
if (parts[1] === 'app') {
93+
return `App: ${parts[2]}`;
94+
}
95+
return 'Dashboard';
96+
})()}
8897
</BreadcrumbPage>
8998
</BreadcrumbItem>
9099
</BreadcrumbList>

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import { useAuth } from "../context/AuthContext"
2020
export function AppSidebar({ objects, ...props }: React.ComponentProps<typeof Sidebar> & { objects: Record<string, any> }) {
2121
const { path, navigate } = useRouter()
2222
const { user } = useAuth()
23+
24+
// Parse current app context
25+
const parts = path.split('/');
26+
const currentApp = parts[1] === 'app' ? parts[2] : null;
27+
const getObjectPath = (name: string) => currentApp ? `/app/${currentApp}/object/${name}` : `/object/${name}`;
2328

2429
return (
2530
<Sidebar collapsible="icon" {...props}>
@@ -46,8 +51,8 @@ export function AppSidebar({ objects, ...props }: React.ComponentProps<typeof Si
4651
{Object.entries(objects).map(([name, schema]) => (
4752
<SidebarMenuItem key={name}>
4853
<SidebarMenuButton
49-
isActive={path.startsWith(`/object/${name}`)}
50-
onClick={() => navigate(`/object/${name}`)}
54+
isActive={path.includes(`/object/${name}`)}
55+
onClick={() => navigate(getObjectPath(name))}
5156
>
5257
<FileText />
5358
<span>{schema.label || schema.title || name}</span>

packages/client/src/pages/Dashboard.tsx

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { useState, useEffect } from 'react';
22
import {
33
Spinner,
4+
Card,
5+
CardHeader,
6+
CardTitle,
7+
CardDescription
48
} from '@objectql/ui';
59
import { useAuth } from '../context/AuthContext';
610
import { useRouter } from '../hooks/useRouter';
@@ -13,17 +17,47 @@ export default function Dashboard() {
1317
const { user } = useAuth();
1418
const [loading, setLoading] = useState(true);
1519
const [objects, setObjects] = useState<Record<string, any>>({});
20+
const [apps, setApps] = useState<any[]>([]);
1621
const { path, navigate } = useRouter();
1722

1823
// Parse path
19-
// /object/project/123 -> parts ['', 'object', 'project', '123']
24+
// Support patterns:
25+
// 1. /object/:objectName/:recordId?
26+
// 2. /app/:appName/object/:objectName/:recordId?
2027
const parts = path.split('/');
21-
const objectName = parts[1] === 'object' ? parts[2] : null;
22-
const isObjectView = parts[1] === 'object';
23-
const recordId = isObjectView ? parts[3] : null; // Index 3 is ID because parts[2] is name
28+
29+
let objectName: string | null = null;
30+
let recordId: string | undefined = undefined;
31+
let appName: string | null = null;
32+
33+
if (parts[1] === 'object') {
34+
objectName = parts[2];
35+
recordId = parts[3];
36+
} else if (parts[1] === 'app' && parts[3] === 'object') {
37+
appName = parts[2];
38+
objectName = parts[4];
39+
recordId = parts[5];
40+
}
41+
42+
// Wrap navigate to preserve app context
43+
const wrappedNavigate = (to: string) => {
44+
if (appName && to.startsWith('/object/')) {
45+
navigate(to.replace('/object/', `/app/${appName}/object/`));
46+
} else {
47+
navigate(to);
48+
}
49+
};
2450

2551
useEffect(() => {
2652
if (user) {
53+
fetch('/api/v6/data/app?limit=100', { headers: getHeaders() })
54+
.then(res => res.json())
55+
.then(result => {
56+
const data = Array.isArray(result) ? result : (result.data || []);
57+
setApps(data);
58+
})
59+
.catch(console.error);
60+
2761
// Fetch objects
2862
fetch('/api/v6/metadata/object', { headers: getHeaders() })
2963
.then(res => res.json())
@@ -38,12 +72,7 @@ export default function Dashboard() {
3872

3973
const objNames = Object.keys(objectsMap);
4074
setObjects(objectsMap);
41-
42-
const currentPath = window.location.pathname;
43-
if ((currentPath === '/' || currentPath === '/object') && objNames.length > 0) {
44-
navigate(`/object/${objNames[0]}`);
45-
}
46-
75+
// Auto-redirect removed to show App List
4776
setLoading(false);
4877
})
4978
.catch(err => {
@@ -74,7 +103,7 @@ export default function Dashboard() {
74103
<ObjectDetailView
75104
objectName={objectName}
76105
recordId={recordId}
77-
navigate={navigate}
106+
navigate={wrappedNavigate}
78107
objectSchema={objects[objectName]}
79108
/>
80109
);
@@ -84,21 +113,60 @@ export default function Dashboard() {
84113
objectName={objectName}
85114
user={user}
86115
isCreating={false}
87-
navigate={navigate}
116+
navigate={wrappedNavigate}
88117
objectSchema={objects[objectName]}
89118
/>
90119
);
91120
}
92121

93122
return (
94-
<div className="flex flex-1 items-center justify-center rounded-lg border border-dashed shadow-sm">
95-
<div className="flex flex-col items-center gap-1 text-center">
96-
<h3 className="text-2xl font-bold tracking-tight">
97-
Welcome to ObjectQL
98-
</h3>
99-
<p className="text-sm text-muted-foreground">
100-
Select an object from the sidebar to get started.
101-
</p>
123+
<div className="p-6 max-w-7xl mx-auto w-full">
124+
<div className="flex items-center justify-between mb-8">
125+
<div>
126+
<h2 className="text-3xl font-bold tracking-tight">Applications</h2>
127+
<p className="text-muted-foreground mt-2">
128+
Select an application to start working.
129+
</p>
130+
</div>
131+
</div>
132+
133+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
134+
{apps.map(app => (
135+
<Card
136+
key={app._id || app.id}
137+
className="hover:shadow-md transition-all cursor-pointer border-2 hover:border-primary/20"
138+
onClick={() => {
139+
// Navigate to first object or app root
140+
if (app.objects && Array.isArray(app.objects) && app.objects.length > 0) {
141+
navigate(`/app/${app.slug}/object/${app.objects[0]}`);
142+
} else {
143+
navigate(`/app/${app.slug}`);
144+
}
145+
}}
146+
>
147+
<CardHeader className="flex flex-row items-start gap-4 space-y-0">
148+
<div className="p-3 rounded-lg bg-primary/10 text-primary">
149+
<i className={`${app.icon || 'ri-apps-line'} text-xl`}></i>
150+
</div>
151+
<div className="space-y-1">
152+
<CardTitle className="text-lg">{app.name}</CardTitle>
153+
<CardDescription className="line-clamp-2">
154+
{app.description || 'No description provided'}
155+
</CardDescription>
156+
</div>
157+
</CardHeader>
158+
</Card>
159+
))}
160+
161+
{apps.length === 0 && (
162+
<div className="col-span-full flex flex-col items-center justify-center p-12 border-2 border-dashed rounded-lg">
163+
<div className="p-4 rounded-full bg-muted mb-4">
164+
<i className="ri-apps-line text-2xl text-muted-foreground"></i>
165+
</div>
166+
<h3 className="text-lg font-semibold">No Applications Found</h3>
167+
<p className="text-muted-foreground">Admin needs to create an application first.</p>
168+
</div>
169+
)}
102170
</div>
103171
</div>
104172
);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: app
2+
label: Application
3+
description: A lightweight container for objects
4+
icon: ri-apps-line
5+
6+
fields:
7+
name:
8+
type: text
9+
label: App Name
10+
required: true
11+
searchable: true
12+
13+
slug:
14+
type: text
15+
label: URL Slug
16+
unique: true
17+
required: true
18+
regex: ^[a-z0-9][a-z0-9_-]*[a-z0-9]$
19+
20+
description:
21+
type: textarea
22+
label: Description
23+
24+
icon:
25+
type: text
26+
label: Icon
27+
default: ri-apps-line
28+
29+
objects:
30+
type: json
31+
label: Details
32+
description: List of object names included in this app

packages/platform/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export const objectDefinitions = [
4141
'member.object.yml',
4242
'invitation.object.yml',
4343
'team.object.yml',
44-
'teamMember.object.yml'
44+
'teamMember.object.yml',
45+
'app.object.yml'
4546
];
4647

4748
/**

0 commit comments

Comments
 (0)