@@ -13,7 +13,7 @@ import { useMemo, useState, useCallback, useEffect, type ComponentType } from 'r
1313import { useParams , useSearchParams , useNavigate } from 'react-router-dom' ;
1414import { ObjectChart } from '@object-ui/plugin-charts' ;
1515import { ListView } from '@object-ui/plugin-list' ;
16- import { DetailView } from '@object-ui/plugin-detail' ;
16+ import { DetailView , RecordChatterPanel } from '@object-ui/plugin-detail' ;
1717import { ObjectView as PluginObjectView , ViewTabBar } from '@object-ui/plugin-view' ;
1818import type { ViewTabItem , AvailableViewType } from '@object-ui/plugin-view' ;
1919// Import plugins for side-effects (registration)
@@ -22,7 +22,7 @@ import '@object-ui/plugin-kanban';
2222import '@object-ui/plugin-calendar' ;
2323import { Button , Empty , EmptyTitle , EmptyDescription , NavigationOverlay , DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuTrigger , DropdownMenuSeparator } from '@object-ui/components' ;
2424import { Plus , Table as TableIcon , Settings2 , Wrench , KanbanSquare , Calendar , LayoutGrid , Activity , GanttChart , MapPin , BarChart3 , ChevronRight } from 'lucide-react' ;
25- import type { ListViewSchema , ViewNavigationConfig } from '@object-ui/types' ;
25+ import type { ListViewSchema , ViewNavigationConfig , FeedItem } from '@object-ui/types' ;
2626import { MetadataToggle , MetadataPanel , useMetadataInspector } from './MetadataInspector' ;
2727import { ViewConfigPanel } from './ViewConfigPanel' ;
2828import { useObjectActions } from '../hooks/useObjectActions' ;
@@ -56,6 +56,195 @@ const AVAILABLE_VIEW_TYPES: AvailableViewType[] = [
5656 { type : 'chart' , label : 'Chart' , description : 'Data visualization' } ,
5757] ;
5858
59+ const FALLBACK_USER = { id : 'current-user' , name : 'Demo User' } ;
60+
61+ /**
62+ * DrawerDetailContent — extracted component for NavigationOverlay content.
63+ * Needs to be a proper component (not a render prop) so it can use hooks
64+ * for data fetching, comment handling, etc.
65+ */
66+ function DrawerDetailContent ( { objectDef, recordId, dataSource, onEdit } : {
67+ objectDef : any ;
68+ recordId : string ;
69+ dataSource : any ;
70+ onEdit : ( record : any ) => void ;
71+ } ) {
72+ const { user } = useAuth ( ) ;
73+ const currentUser = user
74+ ? { id : user . id , name : user . name , avatar : user . image }
75+ : FALLBACK_USER ;
76+
77+ const [ feedItems , setFeedItems ] = useState < FeedItem [ ] > ( [ ] ) ;
78+
79+ // Fetch persisted comments from API
80+ useEffect ( ( ) => {
81+ if ( ! dataSource || ! objectDef ?. name || ! recordId ) return ;
82+ const threadId = `${ objectDef . name } :${ recordId } ` ;
83+ dataSource . find ( 'sys_comment' , { $filter : `threadId eq '${ threadId } '` , $orderby : 'createdAt asc' } )
84+ . then ( ( res : any ) => {
85+ if ( res . data ?. length ) {
86+ setFeedItems ( res . data . map ( ( c : any ) => ( {
87+ id : c . id ,
88+ type : 'comment' as const ,
89+ actor : c . author ?. name ?? 'Unknown' ,
90+ actorAvatarUrl : c . author ?. avatar ,
91+ body : c . content ,
92+ createdAt : c . createdAt ,
93+ updatedAt : c . updatedAt ,
94+ parentId : c . parentId ,
95+ reactions : c . reactions
96+ ? Object . entries ( c . reactions as Record < string , string [ ] > ) . map ( ( [ emoji , userIds ] ) => ( {
97+ emoji,
98+ count : userIds . length ,
99+ reacted : userIds . includes ( currentUser . id ) ,
100+ } ) )
101+ : undefined ,
102+ } ) ) ) ;
103+ }
104+ } )
105+ . catch ( ( ) => { } ) ;
106+ } , [ dataSource , objectDef ?. name , recordId , currentUser . id ] ) ;
107+
108+ const handleAddComment = useCallback (
109+ async ( text : string ) => {
110+ const newItem : FeedItem = {
111+ id : crypto . randomUUID ( ) ,
112+ type : 'comment' ,
113+ actor : currentUser . name ,
114+ actorAvatarUrl : 'avatar' in currentUser ? ( currentUser as any ) . avatar : undefined ,
115+ body : text ,
116+ createdAt : new Date ( ) . toISOString ( ) ,
117+ } ;
118+ setFeedItems ( prev => [ ...prev , newItem ] ) ;
119+ if ( dataSource ) {
120+ const threadId = `${ objectDef . name } :${ recordId } ` ;
121+ dataSource . create ( 'sys_comment' , {
122+ id : newItem . id ,
123+ threadId,
124+ author : currentUser ,
125+ content : text ,
126+ mentions : [ ] ,
127+ createdAt : newItem . createdAt ,
128+ } ) . catch ( ( ) => { } ) ;
129+ }
130+ } ,
131+ [ currentUser , dataSource , objectDef ?. name , recordId ] ,
132+ ) ;
133+
134+ const handleAddReply = useCallback (
135+ async ( parentId : string | number , text : string ) => {
136+ const newItem : FeedItem = {
137+ id : crypto . randomUUID ( ) ,
138+ type : 'comment' ,
139+ actor : currentUser . name ,
140+ actorAvatarUrl : 'avatar' in currentUser ? ( currentUser as any ) . avatar : undefined ,
141+ body : text ,
142+ createdAt : new Date ( ) . toISOString ( ) ,
143+ parentId,
144+ } ;
145+ setFeedItems ( prev => {
146+ const updated = [ ...prev , newItem ] ;
147+ return updated . map ( item =>
148+ item . id === parentId
149+ ? { ...item , replyCount : ( item . replyCount ?? 0 ) + 1 }
150+ : item
151+ ) ;
152+ } ) ;
153+ if ( dataSource ) {
154+ const threadId = `${ objectDef . name } :${ recordId } ` ;
155+ dataSource . create ( 'sys_comment' , {
156+ id : newItem . id ,
157+ threadId,
158+ author : currentUser ,
159+ content : text ,
160+ mentions : [ ] ,
161+ createdAt : newItem . createdAt ,
162+ parentId,
163+ } ) . catch ( ( ) => { } ) ;
164+ }
165+ } ,
166+ [ currentUser , dataSource , objectDef ?. name , recordId ] ,
167+ ) ;
168+
169+ const handleToggleReaction = useCallback (
170+ ( itemId : string | number , emoji : string ) => {
171+ setFeedItems ( prev => prev . map ( item => {
172+ if ( item . id !== itemId ) return item ;
173+ const reactions = [ ...( item . reactions ?? [ ] ) ] ;
174+ const idx = reactions . findIndex ( r => r . emoji === emoji ) ;
175+ if ( idx >= 0 ) {
176+ const r = reactions [ idx ] ;
177+ if ( r . reacted ) {
178+ if ( r . count <= 1 ) {
179+ reactions . splice ( idx , 1 ) ;
180+ } else {
181+ reactions [ idx ] = { ...r , count : r . count - 1 , reacted : false } ;
182+ }
183+ } else {
184+ reactions [ idx ] = { ...r , count : r . count + 1 , reacted : true } ;
185+ }
186+ } else {
187+ reactions . push ( { emoji, count : 1 , reacted : true } ) ;
188+ }
189+ const updated = { ...item , reactions } ;
190+ if ( dataSource ) {
191+ dataSource . update ( 'sys_comment' , String ( itemId ) , {
192+ $toggleReaction : { emoji, userId : currentUser . id } ,
193+ } ) . catch ( ( ) => { } ) ;
194+ }
195+ return updated ;
196+ } ) ) ;
197+ } ,
198+ [ currentUser . id , dataSource ] ,
199+ ) ;
200+
201+ return (
202+ < div className = "h-full bg-background overflow-auto p-3 sm:p-4 lg:p-6" >
203+ < DetailView
204+ schema = { {
205+ type : 'detail-view' ,
206+ objectName : objectDef . name ,
207+ resourceId : recordId ,
208+ showBack : false ,
209+ showEdit : true ,
210+ title : objectDef . label ,
211+ sections : [
212+ {
213+ title : 'Details' ,
214+ fields : Object . keys ( objectDef . fields || { } ) . map ( ( key : string ) => ( {
215+ name : key ,
216+ label : objectDef . fields [ key ] . label || key ,
217+ type : objectDef . fields [ key ] . type || 'text'
218+ } ) ) ,
219+ }
220+ ]
221+ } }
222+ dataSource = { dataSource }
223+ onEdit = { ( ) => onEdit ( { _id : recordId , id : recordId } ) }
224+ />
225+ { /* Discussion panel — collapsible in drawer/overlay mode */ }
226+ < div className = "mt-6 border-t pt-6" >
227+ < RecordChatterPanel
228+ config = { {
229+ position : 'bottom' ,
230+ collapsible : true ,
231+ defaultCollapsed : true ,
232+ feed : {
233+ enableReactions : true ,
234+ enableThreading : true ,
235+ showCommentInput : true ,
236+ } ,
237+ } }
238+ items = { feedItems }
239+ onAddComment = { handleAddComment }
240+ onAddReply = { handleAddReply }
241+ onToggleReaction = { handleToggleReaction }
242+ />
243+ </ div >
244+ </ div >
245+ ) ;
246+ }
247+
59248export function ObjectView ( { dataSource, objects, onEdit, onRowClick } : any ) {
60249 const navigate = useNavigate ( ) ;
61250 const { objectName, viewId } = useParams ( ) ;
@@ -230,8 +419,8 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
230419 } , [ dataSource , objectDef . name , refreshKey ] ) ;
231420
232421 // Navigation overlay for record detail (supports drawer/modal/split/popover via config)
233- // Priority: activeView.navigation > objectDef.navigation > default drawer
234- const detailNavigation : ViewNavigationConfig = activeView ?. navigation ?? objectDef . navigation ?? { mode : 'drawer ' } ;
422+ // Priority: activeView.navigation > objectDef.navigation > default page
423+ const detailNavigation : ViewNavigationConfig = activeView ?. navigation ?? objectDef . navigation ?? { mode : 'page ' } ;
235424 const drawerRecordId = searchParams . get ( 'recordId' ) ;
236425 const navOverlay = useNavigationOverlay ( {
237426 navigation : detailNavigation ,
@@ -630,30 +819,12 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
630819 { ( record : Record < string , unknown > ) => {
631820 const recordId = ( record . _id || record . id ) as string ;
632821 return (
633- < div className = "h-full bg-background overflow-auto p-3 sm:p-4 lg:p-6" >
634- < DetailView
635- schema = { {
636- type : 'detail-view' ,
637- objectName : objectDef . name ,
638- resourceId : recordId ,
639- showBack : false ,
640- showEdit : true ,
641- title : objectDef . label ,
642- sections : [
643- {
644- title : 'Details' ,
645- fields : Object . keys ( objectDef . fields || { } ) . map ( ( key : string ) => ( {
646- name : key ,
647- label : objectDef . fields [ key ] . label || key ,
648- type : objectDef . fields [ key ] . type || 'text'
649- } ) ) ,
650- }
651- ]
652- } }
653- dataSource = { dataSource }
654- onEdit = { ( ) => onEdit ( { _id : recordId , id : recordId } ) }
655- />
656- </ div >
822+ < DrawerDetailContent
823+ objectDef = { objectDef }
824+ recordId = { recordId }
825+ dataSource = { dataSource }
826+ onEdit = { onEdit }
827+ />
657828 ) ;
658829 } }
659830 </ NavigationOverlay >
0 commit comments