|
1 | 1 | import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; |
2 | | -import { useState, useEffect, lazy, Suspense, useMemo, type ReactNode } from 'react'; |
| 2 | +import { useState, useEffect, useCallback, lazy, Suspense, useMemo, type ReactNode } from 'react'; |
3 | 3 | import { ObjectForm } from '@object-ui/plugin-form'; |
4 | 4 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components'; |
5 | 5 | import { toast } from 'sonner'; |
6 | | -import { SchemaRendererProvider } from '@object-ui/react'; |
| 6 | +import { SchemaRendererProvider, useActionRunner } from '@object-ui/react'; |
7 | 7 | import type { ConnectionState } from './dataSource'; |
8 | 8 | import { AuthGuard, useAuth, PreviewBanner } from '@object-ui/auth'; |
9 | 9 | import { MetadataProvider, useMetadata } from './context/MetadataProvider'; |
@@ -92,6 +92,23 @@ export function AppContent() { |
92 | 92 | const [refreshKey, setRefreshKey] = useState(0); |
93 | 93 | const { addRecentItem } = useRecentItems(); |
94 | 94 |
|
| 95 | + // ActionRunner for CRUD dialog callbacks (Phase 2.9) |
| 96 | + const { execute: executeAction, runner } = useActionRunner(); |
| 97 | + |
| 98 | + useEffect(() => { |
| 99 | + runner.registerHandler('crud_success', async (action) => { |
| 100 | + setIsDialogOpen(false); |
| 101 | + setRefreshKey(k => k + 1); |
| 102 | + toast.success(action.params?.message ?? 'Record saved successfully'); |
| 103 | + return { success: true, reload: true }; |
| 104 | + }); |
| 105 | + |
| 106 | + runner.registerHandler('dialog_cancel', async () => { |
| 107 | + setIsDialogOpen(false); |
| 108 | + return { success: true }; |
| 109 | + }); |
| 110 | + }, [runner]); |
| 111 | + |
95 | 112 | // Branding is now applied by AppShell via ConsoleLayout |
96 | 113 |
|
97 | 114 | useEffect(() => { |
@@ -123,6 +140,22 @@ export function AppContent() { |
123 | 140 |
|
124 | 141 | const currentObjectDef = allObjects.find((o: any) => o.name === objectNameFromPath); |
125 | 142 |
|
| 143 | + const handleCrudSuccess = useCallback(() => { |
| 144 | + const label = currentObjectDef?.label || 'Record'; |
| 145 | + executeAction({ |
| 146 | + type: 'crud_success', |
| 147 | + params: { |
| 148 | + message: editingRecord |
| 149 | + ? `${label} updated successfully` |
| 150 | + : `${label} created successfully`, |
| 151 | + }, |
| 152 | + }); |
| 153 | + }, [executeAction, editingRecord, currentObjectDef?.label]); |
| 154 | + |
| 155 | + const handleDialogCancel = useCallback(() => { |
| 156 | + executeAction({ type: 'dialog_cancel' }); |
| 157 | + }, [executeAction]); |
| 158 | + |
126 | 159 | // Track recent items on route change |
127 | 160 | // Only depend on location.pathname — the sole external trigger. |
128 | 161 | // All other values (activeApp, allObjects, cleanParts) are derived from |
@@ -323,16 +356,8 @@ export function AppContent() { |
323 | 356 | .filter(([_, f]: [string, any]) => evaluateVisibility(f.visible, expressionEvaluator)) |
324 | 357 | .map(([key]: [string, any]) => key)) |
325 | 358 | : [], |
326 | | - onSuccess: () => { |
327 | | - setIsDialogOpen(false); |
328 | | - setRefreshKey(k => k + 1); |
329 | | - toast.success( |
330 | | - editingRecord |
331 | | - ? `${currentObjectDef?.label} updated successfully` |
332 | | - : `${currentObjectDef?.label} created successfully` |
333 | | - ); |
334 | | - }, |
335 | | - onCancel: () => setIsDialogOpen(false), |
| 359 | + onSuccess: handleCrudSuccess, |
| 360 | + onCancel: handleDialogCancel, |
336 | 361 | showSubmit: true, |
337 | 362 | showCancel: true, |
338 | 363 | submitText: 'Save Record', |
|
0 commit comments