77 */
88
99import { useState , useEffect , useCallback } from 'react' ;
10- import { useParams } from 'react-router-dom' ;
10+ import { useParams , useNavigate } from 'react-router-dom' ;
1111import { DetailView , RecordChatterPanel } from '@object-ui/plugin-detail' ;
1212import { Empty , EmptyTitle , EmptyDescription } from '@object-ui/components' ;
1313import { PresenceAvatars , type PresenceUser } from '@object-ui/collaboration' ;
1414import { useAuth } from '@object-ui/auth' ;
15+ import { ActionProvider } from '@object-ui/react' ;
16+ import { toast } from 'sonner' ;
1517import { Database , Users } from 'lucide-react' ;
1618import { MetadataPanel , useMetadataInspector } from './MetadataInspector' ;
1719import { SkeletonDetail } from './skeletons' ;
20+ import { ActionConfirmDialog , type ConfirmDialogState } from './ActionConfirmDialog' ;
21+ import { ActionParamDialog , type ParamDialogState } from './ActionParamDialog' ;
1822import type { DetailViewSchema , FeedItem } from '@object-ui/types' ;
23+ import type { ActionDef , ActionContext as ActionCtx , ActionParamDef } from '@object-ui/core' ;
1924
2025interface RecordDetailViewProps {
2126 dataSource : any ;
@@ -29,16 +34,82 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
2934 const { objectName, recordId } = useParams ( ) ;
3035 const { showDebug } = useMetadataInspector ( ) ;
3136 const { user } = useAuth ( ) ;
37+ const navigate = useNavigate ( ) ;
3238 const [ isLoading , setIsLoading ] = useState ( true ) ;
3339 const [ feedItems , setFeedItems ] = useState < FeedItem [ ] > ( [ ] ) ;
3440 const [ recordViewers , setRecordViewers ] = useState < PresenceUser [ ] > ( [ ] ) ;
41+ const [ actionRefreshKey , setActionRefreshKey ] = useState ( 0 ) ;
3542 const objectDef = objects . find ( ( o : any ) => o . name === objectName ) ;
3643
3744 // Use the URL recordId as-is — it contains the actual record _id.
3845 // Navigation code passes `record._id || record.id` directly into the URL
3946 // without adding any prefix, so no stripping is needed.
4047 const pureRecordId = recordId ;
4148
49+ // ─── Action Provider Handlers ───────────────────────────────────────
50+
51+ // Confirm dialog state (promise-based)
52+ const [ confirmState , setConfirmState ] = useState < ConfirmDialogState > ( { open : false , message : '' } ) ;
53+
54+ // Param collection dialog state (promise-based)
55+ const [ paramState , setParamState ] = useState < ParamDialogState > ( { open : false , params : [ ] } ) ;
56+
57+ const confirmHandler = useCallback ( ( message : string , options ?: { title ?: string ; confirmText ?: string ; cancelText ?: string } ) => {
58+ return new Promise < boolean > ( ( resolve ) => {
59+ setConfirmState ( { open : true , message, options, resolve } ) ;
60+ } ) ;
61+ } , [ ] ) ;
62+
63+ const paramCollectionHandler = useCallback ( ( params : ActionParamDef [ ] ) => {
64+ return new Promise < Record < string , any > | null > ( ( resolve ) => {
65+ setParamState ( { open : true , params, resolve } ) ;
66+ } ) ;
67+ } , [ ] ) ;
68+
69+ const toastHandler = useCallback ( ( message : string , options ?: { type ?: string } ) => {
70+ if ( options ?. type === 'error' ) toast . error ( message ) ;
71+ else toast . success ( message ) ;
72+ } , [ ] ) ;
73+
74+ const navigateHandler = useCallback ( ( url : string , options ?: { external ?: boolean ; newTab ?: boolean } ) => {
75+ if ( options ?. external || options ?. newTab ) {
76+ window . open ( url , '_blank' , 'noopener,noreferrer' ) ;
77+ } else {
78+ navigate ( url ) ;
79+ }
80+ } , [ navigate ] ) ;
81+
82+ // API action handler — maps logical action targets to dataSource operations
83+ const apiHandler = useCallback ( async ( action : ActionDef , _context : ActionCtx ) => {
84+ try {
85+ const target = action . target || action . name ;
86+ const params = action . params || { } ;
87+
88+ switch ( target ) {
89+ case 'opportunity_change_stage' :
90+ await dataSource . update ( objectName ! , pureRecordId ! , { stage : params . new_stage } ) ;
91+ break ;
92+ case 'opportunity_mark_won' :
93+ await dataSource . update ( objectName ! , pureRecordId ! , { stage : 'closed_won' } ) ;
94+ break ;
95+ case 'opportunity_mark_lost' :
96+ await dataSource . update ( objectName ! , pureRecordId ! , { stage : 'closed_lost' , loss_reason : params . loss_reason } ) ;
97+ break ;
98+ default :
99+ // Generic: update record with collected params
100+ if ( Object . keys ( params ) . length > 0 ) {
101+ await dataSource . update ( objectName ! , pureRecordId ! , params ) ;
102+ }
103+ break ;
104+ }
105+
106+ setActionRefreshKey ( k => k + 1 ) ;
107+ return { success : true , reload : true } ;
108+ } catch ( error ) {
109+ return { success : false , error : ( error as Error ) . message } ;
110+ }
111+ } , [ dataSource , objectName , pureRecordId ] ) ;
112+
42113 const currentUser = user
43114 ? { id : user . id , name : user . name , avatar : user . image }
44115 : FALLBACK_USER ;
@@ -290,13 +361,23 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
290361
291362 < div className = "flex-1 overflow-hidden flex flex-row" >
292363 < div className = "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48" >
293- < DetailView
294- schema = { detailSchema }
295- dataSource = { dataSource }
296- onEdit = { ( ) => {
297- onEdit ( { _id : pureRecordId , id : pureRecordId } ) ;
298- } }
299- />
364+ < ActionProvider
365+ context = { { record : { } , objectName, user : currentUser } }
366+ onConfirm = { confirmHandler }
367+ onToast = { toastHandler }
368+ onNavigate = { navigateHandler }
369+ onParamCollection = { paramCollectionHandler }
370+ handlers = { { api : apiHandler } }
371+ >
372+ < DetailView
373+ key = { actionRefreshKey }
374+ schema = { detailSchema }
375+ dataSource = { dataSource }
376+ onEdit = { ( ) => {
377+ onEdit ( { _id : pureRecordId , id : pureRecordId } ) ;
378+ } }
379+ />
380+ </ ActionProvider >
300381
301382 { /* Comments & Discussion */ }
302383 < div className = "mt-6 border-t pt-6" >
@@ -322,6 +403,22 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
322403 sections = { [ { title : 'View Schema' , data : detailSchema } ] }
323404 />
324405 </ div >
406+
407+ { /* Action Confirm Dialog */ }
408+ < ActionConfirmDialog
409+ state = { confirmState }
410+ onOpenChange = { ( open ) => {
411+ if ( ! open ) setConfirmState ( s => ( { ...s , open : false } ) ) ;
412+ } }
413+ />
414+
415+ { /* Action Param Collection Dialog */ }
416+ < ActionParamDialog
417+ state = { paramState }
418+ onOpenChange = { ( open ) => {
419+ if ( ! open ) setParamState ( s => ( { ...s , open : false } ) ) ;
420+ } }
421+ />
325422 </ div >
326423 ) ;
327424}
0 commit comments