Skip to content

Commit d649d84

Browse files
Copilothotlong
andcommitted
Add Console App Creation Wizard integration: routes, pages, sidebar, command palette entries
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent fe7722e commit d649d84

15 files changed

Lines changed: 250 additions & 1 deletion

File tree

apps/console/src/App.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ const ReportView = lazy(() => import('./components/ReportView').then(m => ({ def
3030
const ViewDesignerPage = lazy(() => import('./components/ViewDesignerPage').then(m => ({ default: m.ViewDesignerPage })));
3131
const SearchResultsPage = lazy(() => import('./components/SearchResultsPage').then(m => ({ default: m.SearchResultsPage })));
3232

33+
// App Creation / Edit Pages (lazy — only needed during app management)
34+
const CreateAppPage = lazy(() => import('./pages/CreateAppPage').then(m => ({ default: m.CreateAppPage })));
35+
const EditAppPage = lazy(() => import('./pages/EditAppPage').then(m => ({ default: m.EditAppPage })));
36+
3337
// Auth Pages (lazy — only needed before login)
3438
const LoginPage = lazy(() => import('./pages/LoginPage').then(m => ({ default: m.LoginPage })));
3539
const RegisterPage = lazy(() => import('./pages/RegisterPage').then(m => ({ default: m.RegisterPage })));
@@ -250,6 +254,14 @@ export function AppContent() {
250254
<div className="h-screen flex items-center justify-center">
251255
<Empty>
252256
<EmptyTitle>No Apps Configured</EmptyTitle>
257+
<EmptyDescription>No applications have been registered.</EmptyDescription>
258+
<button
259+
className="mt-4 inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
260+
onClick={() => navigate('/apps/_/create-app')}
261+
data-testid="create-first-app-btn"
262+
>
263+
Create Your First App
264+
</button>
253265
</Empty>
254266
</div>
255267
);
@@ -331,6 +343,10 @@ export function AppContent() {
331343
<SearchResultsPage />
332344
} />
333345

346+
{/* App Creation & Editing */}
347+
<Route path="create-app" element={<CreateAppPage />} />
348+
<Route path="edit-app/:editAppName" element={<EditAppPage />} />
349+
334350
{/* System Administration Routes */}
335351
<Route path="system/users" element={<UserManagementPage />} />
336352
<Route path="system/organizations" element={<OrgManagementPage />} />
@@ -402,6 +418,7 @@ function findFirstRoute(items: any[]): string {
402418
// Redirect root to default app
403419
function RootRedirect() {
404420
const { apps, loading, error } = useMetadata();
421+
const navigate = useNavigate();
405422
const activeApps = apps.filter((a: any) => a.active !== false);
406423
const defaultApp = activeApps.find((a: any) => a.isDefault === true) || activeApps[0];
407424

@@ -416,6 +433,15 @@ function RootRedirect() {
416433
<EmptyDescription>
417434
{error ? error.message : 'No applications have been registered. Check your ObjectStack configuration.'}
418435
</EmptyDescription>
436+
{!error && (
437+
<button
438+
className="mt-4 inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
439+
onClick={() => navigate('/apps/_/create-app')}
440+
data-testid="create-first-app-btn"
441+
>
442+
Create Your First App
443+
</button>
444+
)}
419445
</Empty>
420446
</div>
421447
);

apps/console/src/components/AppSidebar.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
Star,
4848
StarOff,
4949
Search,
50+
Pencil,
5051
} from 'lucide-react';
5152
import { NavigationRenderer } from '@object-ui/layout';
5253
import type { NavigationItem } from '@object-ui/types';
@@ -299,12 +300,18 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
299300
</DropdownMenuItem>
300301
))}
301302
<DropdownMenuSeparator />
302-
<DropdownMenuItem className="gap-2 p-2">
303+
<DropdownMenuItem className="gap-2 p-2" onClick={() => navigate(`/apps/${activeAppName}/create-app`)} data-testid="add-app-btn">
303304
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
304305
<Plus className="size-4" />
305306
</div>
306307
<div className="font-medium text-muted-foreground">Add App</div>
307308
</DropdownMenuItem>
309+
<DropdownMenuItem className="gap-2 p-2" onClick={() => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`)} data-testid="edit-app-btn">
310+
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
311+
<Pencil className="size-4" />
312+
</div>
313+
<div className="font-medium text-muted-foreground">Edit App</div>
314+
</DropdownMenuItem>
308315
</DropdownMenuContent>
309316
</DropdownMenu>
310317
</SidebarMenuItem>

apps/console/src/components/CommandPalette.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
Sun,
2929
Monitor,
3030
Search,
31+
Plus,
3132
} from 'lucide-react';
3233
import { useTheme } from './theme-provider';
3334
import { useExpressionContext, evaluateVisibility } from '../context/ExpressionProvider';
@@ -212,6 +213,13 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
212213
{/* Full Search Page */}
213214
<CommandSeparator />
214215
<CommandGroup heading={t('console.commandPalette.actions')}>
216+
<CommandItem
217+
value="create new app application"
218+
onSelect={() => runCommand(() => navigate(`${baseUrl}/create-app`))}
219+
>
220+
<Plus className="mr-2 h-4 w-4" />
221+
<span>{t('console.commandPalette.createApp')}</span>
222+
</CommandItem>
215223
<CommandItem
216224
value="search all results full page"
217225
onSelect={() => runCommand(() => navigate(`${baseUrl}/search`))}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* CreateAppPage
3+
*
4+
* Console page that renders the AppCreationWizard from @object-ui/plugin-designer.
5+
* Handles wizard callbacks: onComplete (create app), onCancel (navigate back),
6+
* onSaveDraft (persist to localStorage).
7+
* @module
8+
*/
9+
10+
import { useCallback } from 'react';
11+
import { useNavigate, useParams } from 'react-router-dom';
12+
import { AppCreationWizard } from '@object-ui/plugin-designer';
13+
import { wizardDraftToAppSchema } from '@object-ui/types';
14+
import type { AppWizardDraft, ObjectSelection } from '@object-ui/types';
15+
import { useMetadata } from '../context/MetadataProvider';
16+
import { toast } from 'sonner';
17+
18+
const DRAFT_STORAGE_KEY = 'objectui-app-wizard-draft';
19+
20+
export function CreateAppPage() {
21+
const navigate = useNavigate();
22+
const { appName } = useParams();
23+
const { objects, refresh } = useMetadata();
24+
25+
// Map metadata objects to ObjectSelection format
26+
const availableObjects: ObjectSelection[] = (objects || []).map((obj: any) => ({
27+
name: obj.name,
28+
label: obj.label || obj.name,
29+
icon: obj.icon,
30+
selected: false,
31+
}));
32+
33+
// Load saved draft from localStorage (if any)
34+
const loadDraft = useCallback((): Partial<AppWizardDraft> | undefined => {
35+
try {
36+
const raw = localStorage.getItem(DRAFT_STORAGE_KEY);
37+
if (raw) {
38+
return JSON.parse(raw) as Partial<AppWizardDraft>;
39+
}
40+
} catch {
41+
// ignore
42+
}
43+
return undefined;
44+
}, []);
45+
46+
const handleComplete = useCallback(
47+
async (draft: AppWizardDraft) => {
48+
try {
49+
const _appSchema = wizardDraftToAppSchema(draft);
50+
// Clear draft from localStorage on successful creation
51+
localStorage.removeItem(DRAFT_STORAGE_KEY);
52+
toast.success(`Application "${draft.title}" created successfully`);
53+
// Refresh metadata so the new app shows up
54+
await refresh?.();
55+
// Navigate to the new app
56+
navigate(`/apps/${draft.name}`);
57+
} catch (err: any) {
58+
toast.error(err?.message || 'Failed to create application');
59+
}
60+
},
61+
[navigate, refresh],
62+
);
63+
64+
const handleCancel = useCallback(() => {
65+
if (appName) {
66+
navigate(`/apps/${appName}`);
67+
} else {
68+
navigate('/');
69+
}
70+
}, [navigate, appName]);
71+
72+
const handleSaveDraft = useCallback((draft: AppWizardDraft) => {
73+
try {
74+
localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft));
75+
toast.info('Draft saved');
76+
} catch {
77+
// localStorage full — ignore
78+
}
79+
}, []);
80+
81+
return (
82+
<div className="mx-auto max-w-4xl py-8 px-4" data-testid="create-app-page">
83+
<AppCreationWizard
84+
availableObjects={availableObjects}
85+
initialDraft={loadDraft()}
86+
onComplete={handleComplete}
87+
onCancel={handleCancel}
88+
onSaveDraft={handleSaveDraft}
89+
/>
90+
</div>
91+
);
92+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* EditAppPage
3+
*
4+
* Console page that reuses AppCreationWizard in edit mode.
5+
* Loads the existing app configuration as `initialDraft` and
6+
* updates the app on completion.
7+
* @module
8+
*/
9+
10+
import { useCallback, useMemo } from 'react';
11+
import { useNavigate, useParams } from 'react-router-dom';
12+
import { AppCreationWizard } from '@object-ui/plugin-designer';
13+
import { wizardDraftToAppSchema } from '@object-ui/types';
14+
import type { AppWizardDraft, ObjectSelection } from '@object-ui/types';
15+
import { useMetadata } from '../context/MetadataProvider';
16+
import { toast } from 'sonner';
17+
18+
export function EditAppPage() {
19+
const navigate = useNavigate();
20+
const { appName, editAppName } = useParams();
21+
const { apps, objects, refresh } = useMetadata();
22+
23+
const targetAppName = editAppName || appName;
24+
25+
// Find the app to edit
26+
const appToEdit = apps.find((a: any) => a.name === targetAppName);
27+
28+
// Map metadata objects to ObjectSelection format
29+
const availableObjects: ObjectSelection[] = (objects || []).map((obj: any) => ({
30+
name: obj.name,
31+
label: obj.label || obj.name,
32+
icon: obj.icon,
33+
selected: appToEdit?.navigation?.some(
34+
(nav: any) => nav.type === 'object' && nav.objectName === obj.name,
35+
) ?? false,
36+
}));
37+
38+
// Convert existing app to wizard draft
39+
const initialDraft = useMemo((): Partial<AppWizardDraft> | undefined => {
40+
if (!appToEdit) return undefined;
41+
return {
42+
name: appToEdit.name,
43+
title: appToEdit.title || appToEdit.label || '',
44+
description: appToEdit.description || '',
45+
icon: appToEdit.icon || '',
46+
layout: appToEdit.layout || 'sidebar',
47+
navigation: appToEdit.navigation || [],
48+
branding: {
49+
logo: appToEdit.branding?.logo || appToEdit.logo || '',
50+
primaryColor: appToEdit.branding?.primaryColor || '#3b82f6',
51+
favicon: appToEdit.branding?.favicon || appToEdit.favicon || '',
52+
},
53+
};
54+
}, [appToEdit]);
55+
56+
const handleComplete = useCallback(
57+
async (draft: AppWizardDraft) => {
58+
try {
59+
const _appSchema = wizardDraftToAppSchema(draft);
60+
toast.success(`Application "${draft.title}" updated successfully`);
61+
await refresh?.();
62+
navigate(`/apps/${draft.name}`);
63+
} catch (err: any) {
64+
toast.error(err?.message || 'Failed to update application');
65+
}
66+
},
67+
[navigate, refresh],
68+
);
69+
70+
const handleCancel = useCallback(() => {
71+
if (appName) {
72+
navigate(`/apps/${appName}`);
73+
} else {
74+
navigate('/');
75+
}
76+
}, [navigate, appName]);
77+
78+
const handleSaveDraft = useCallback((draft: AppWizardDraft) => {
79+
try {
80+
localStorage.setItem(`objectui-edit-draft-${targetAppName}`, JSON.stringify(draft));
81+
toast.info('Draft saved');
82+
} catch {
83+
// localStorage full
84+
}
85+
}, [targetAppName]);
86+
87+
if (!appToEdit) {
88+
return (
89+
<div className="mx-auto max-w-4xl py-8 px-4 text-center" data-testid="edit-app-not-found">
90+
<p className="text-muted-foreground">Application &quot;{targetAppName}&quot; not found.</p>
91+
</div>
92+
);
93+
}
94+
95+
return (
96+
<div className="mx-auto max-w-4xl py-8 px-4" data-testid="edit-app-page">
97+
<AppCreationWizard
98+
availableObjects={availableObjects}
99+
initialDraft={initialDraft}
100+
onComplete={handleComplete}
101+
onCancel={handleCancel}
102+
onSaveDraft={handleSaveDraft}
103+
/>
104+
</div>
105+
);
106+
}

packages/i18n/src/locales/ar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ const ar = {
268268
systemTheme: 'مظهر النظام',
269269
actions: 'الإجراءات',
270270
openFullSearch: 'فتح صفحة البحث الكاملة',
271+
createApp: 'إنشاء تطبيق جديد',
271272
},
272273
errors: {
273274
somethingWentWrong: 'حدث خطأ ما',

packages/i18n/src/locales/de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ const de = {
272272
systemTheme: 'Systemdesign',
273273
actions: 'Aktionen',
274274
openFullSearch: 'Vollständige Suchseite öffnen',
275+
createApp: 'Neue App erstellen',
275276
},
276277
errors: {
277278
somethingWentWrong: 'Etwas ist schiefgelaufen',

packages/i18n/src/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ const en = {
272272
systemTheme: 'System Theme',
273273
actions: 'Actions',
274274
openFullSearch: 'Open Full Search Page',
275+
createApp: 'Create New App',
275276
},
276277
errors: {
277278
somethingWentWrong: 'Something went wrong',

packages/i18n/src/locales/es.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ const es = {
267267
systemTheme: 'Tema del sistema',
268268
actions: 'Acciones',
269269
openFullSearch: 'Abrir página de búsqueda completa',
270+
createApp: 'Crear nueva aplicación',
270271
},
271272
errors: {
272273
somethingWentWrong: 'Algo salió mal',

packages/i18n/src/locales/fr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ const fr = {
272272
systemTheme: 'Thème système',
273273
actions: 'Actions',
274274
openFullSearch: 'Ouvrir la page de recherche complète',
275+
createApp: 'Créer une nouvelle application',
275276
},
276277
errors: {
277278
somethingWentWrong: "Quelque chose s'est mal passé",

0 commit comments

Comments
 (0)