Skip to content

Commit fb4998c

Browse files
authored
Merge pull request #765 from objectstack-ai/copilot/add-defineapp-api-integration
2 parents 8ad9e41 + 530ad02 commit fb4998c

File tree

5 files changed

+100
-5
lines changed

5 files changed

+100
-5
lines changed

ROADMAP.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,11 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
498498
- [x] CommandPalette "Create New App" command (⌘+K → Actions group)
499499
- [x] Empty state CTA "Create Your First App" when no apps configured
500500
- [x] `wizardDraftToAppSchema()` conversion on completion
501+
- [x] `client.meta.saveItem('app', name, schema)` — persists app metadata to backend on create/edit
502+
- [x] MSW PUT handler for `/meta/:type/:name` — dev/mock mode metadata persistence
501503
- [x] Draft persistence to localStorage with auto-clear on success
502504
- [x] `createApp` i18n key added to all 10 locales
503-
- [x] 11 console integration tests (routes, wizard callbacks, draft persistence, CommandPalette)
505+
- [x] 13 console integration tests (routes, wizard callbacks, draft persistence, saveItem, CommandPalette)
504506

505507
---
506508

apps/console/src/__tests__/app-creation-integration.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ const MockAdapterInstance = {
8383
connect: vi.fn().mockResolvedValue(true),
8484
onConnectionStateChange: vi.fn().mockReturnValue(() => {}),
8585
getConnectionState: vi.fn().mockReturnValue('connected'),
86+
getClient: vi.fn().mockReturnValue({
87+
meta: {
88+
saveItem: vi.fn().mockResolvedValue({ ok: true }),
89+
getItems: vi.fn().mockResolvedValue({ items: [] }),
90+
getItem: vi.fn().mockResolvedValue(null),
91+
},
92+
}),
8693
discovery: {},
8794
};
8895

@@ -349,6 +356,25 @@ describe('Console App Creation Integration', () => {
349356
expect(localStorage.getItem('objectui-app-wizard-draft')).toBeNull();
350357
});
351358
});
359+
360+
it('calls client.meta.saveItem on wizard completion', async () => {
361+
renderApp('/apps/sales/create-app');
362+
363+
await waitFor(() => {
364+
expect(screen.getByTestId('wizard-complete')).toBeInTheDocument();
365+
}, { timeout: 10000 });
366+
367+
fireEvent.click(screen.getByTestId('wizard-complete'));
368+
369+
await waitFor(() => {
370+
const client = MockAdapterInstance.getClient();
371+
expect(client.meta.saveItem).toHaveBeenCalledWith(
372+
'app',
373+
'my_app',
374+
expect.objectContaining({ name: 'my_app' }),
375+
);
376+
});
377+
});
352378
});
353379

354380
// --- Route: Edit App ---
@@ -379,6 +405,25 @@ describe('Console App Creation Integration', () => {
379405
expect(screen.getByTestId('edit-app-not-found')).toBeInTheDocument();
380406
}, { timeout: 10000 });
381407
});
408+
409+
it('calls client.meta.saveItem on wizard completion (edit)', async () => {
410+
renderApp('/apps/sales/edit-app/sales');
411+
412+
await waitFor(() => {
413+
expect(screen.getByTestId('wizard-complete')).toBeInTheDocument();
414+
}, { timeout: 10000 });
415+
416+
fireEvent.click(screen.getByTestId('wizard-complete'));
417+
418+
await waitFor(() => {
419+
const client = MockAdapterInstance.getClient();
420+
expect(client.meta.saveItem).toHaveBeenCalledWith(
421+
'app',
422+
'my_app',
423+
expect.objectContaining({ name: 'my_app' }),
424+
);
425+
});
426+
});
382427
});
383428

384429
// --- CommandPalette: Create App Command ---

apps/console/src/mocks/handlers.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,40 @@ export function createHandlers(baseUrl: string, kernel: ObjectKernel, driver: In
127127
}
128128
}),
129129

130+
// ── Metadata: save item by type + name ───────────────────────────────
131+
http.put(`${prefix}${baseUrl}/meta/:type/:name`, async ({ params, request }) => {
132+
try {
133+
const body = await request.json();
134+
if (typeof protocol.saveMetaItem === 'function') {
135+
const result = await protocol.saveMetaItem({
136+
type: params.type as string,
137+
name: params.name as string,
138+
item: body,
139+
});
140+
return HttpResponse.json(result, { status: 200 });
141+
}
142+
return HttpResponse.json({ error: 'Save not supported' }, { status: 501 });
143+
} catch (e) {
144+
return HttpResponse.json({ error: String(e) }, { status: 500 });
145+
}
146+
}),
147+
http.put(`${prefix}${baseUrl}/metadata/:type/:name`, async ({ params, request }) => {
148+
try {
149+
const body = await request.json();
150+
if (typeof protocol.saveMetaItem === 'function') {
151+
const result = await protocol.saveMetaItem({
152+
type: params.type as string,
153+
name: params.name as string,
154+
item: body,
155+
});
156+
return HttpResponse.json(result, { status: 200 });
157+
}
158+
return HttpResponse.json({ error: 'Save not supported' }, { status: 501 });
159+
} catch (e) {
160+
return HttpResponse.json({ error: String(e) }, { status: 500 });
161+
}
162+
}),
163+
130164
// ── Data: find all ──────────────────────────────────────────────────
131165
http.get(`${prefix}${baseUrl}/data/:objectName`, async ({ params, request }) => {
132166
const url = new URL(request.url);

apps/console/src/pages/CreateAppPage.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { AppCreationWizard } from '@object-ui/plugin-designer';
1313
import { wizardDraftToAppSchema } from '@object-ui/types';
1414
import type { AppWizardDraft, ObjectSelection } from '@object-ui/types';
1515
import { useMetadata } from '../context/MetadataProvider';
16+
import { useAdapter } from '../context/AdapterProvider';
1617
import { toast } from 'sonner';
1718

1819
const DRAFT_STORAGE_KEY = 'objectui-app-wizard-draft';
@@ -21,6 +22,7 @@ export function CreateAppPage() {
2122
const navigate = useNavigate();
2223
const { appName } = useParams();
2324
const { objects, refresh } = useMetadata();
25+
const adapter = useAdapter();
2426

2527
// Map metadata objects to ObjectSelection format
2628
const availableObjects: ObjectSelection[] = (objects || []).map((obj: any) => ({
@@ -46,7 +48,12 @@ export function CreateAppPage() {
4648
const handleComplete = useCallback(
4749
async (draft: AppWizardDraft) => {
4850
try {
49-
const _appSchema = wizardDraftToAppSchema(draft);
51+
const appSchema = wizardDraftToAppSchema(draft);
52+
// Persist app metadata to backend
53+
const client = adapter?.getClient();
54+
if (client) {
55+
await client.meta.saveItem('app', draft.name, appSchema);
56+
}
5057
// Clear draft from localStorage on successful creation
5158
localStorage.removeItem(DRAFT_STORAGE_KEY);
5259
toast.success(`Application "${draft.title}" created successfully`);
@@ -58,7 +65,7 @@ export function CreateAppPage() {
5865
toast.error(err?.message || 'Failed to create application');
5966
}
6067
},
61-
[navigate, refresh],
68+
[navigate, refresh, adapter],
6269
);
6370

6471
const handleCancel = useCallback(() => {

apps/console/src/pages/EditAppPage.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import { AppCreationWizard } from '@object-ui/plugin-designer';
1313
import { wizardDraftToAppSchema } from '@object-ui/types';
1414
import type { AppWizardDraft, ObjectSelection } from '@object-ui/types';
1515
import { useMetadata } from '../context/MetadataProvider';
16+
import { useAdapter } from '../context/AdapterProvider';
1617
import { toast } from 'sonner';
1718

1819
export function EditAppPage() {
1920
const navigate = useNavigate();
2021
const { appName, editAppName } = useParams();
2122
const { apps, objects, refresh } = useMetadata();
23+
const adapter = useAdapter();
2224

2325
const targetAppName = editAppName || appName;
2426

@@ -56,15 +58,20 @@ export function EditAppPage() {
5658
const handleComplete = useCallback(
5759
async (draft: AppWizardDraft) => {
5860
try {
59-
const _appSchema = wizardDraftToAppSchema(draft);
61+
const appSchema = wizardDraftToAppSchema(draft);
62+
// Persist app metadata to backend
63+
const client = adapter?.getClient();
64+
if (client) {
65+
await client.meta.saveItem('app', draft.name, appSchema);
66+
}
6067
toast.success(`Application "${draft.title}" updated successfully`);
6168
await refresh?.();
6269
navigate(`/apps/${draft.name}`);
6370
} catch (err: any) {
6471
toast.error(err?.message || 'Failed to update application');
6572
}
6673
},
67-
[navigate, refresh],
74+
[navigate, refresh, adapter],
6875
);
6976

7077
const handleCancel = useCallback(() => {

0 commit comments

Comments
 (0)