@@ -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 ( ) ;
@@ -635,47 +824,12 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
635824 { ( record : Record < string , unknown > ) => {
636825 const recordId = ( record . _id || record . id ) as string ;
637826 return (
638- < div className = "h-full bg-background overflow-auto p-3 sm:p-4 lg:p-6" >
639- < DetailView
640- schema = { {
641- type : 'detail-view' ,
642- objectName : objectDef . name ,
643- resourceId : recordId ,
644- showBack : false ,
645- showEdit : true ,
646- title : objectDef . label ,
647- sections : [
648- {
649- title : 'Details' ,
650- fields : Object . keys ( objectDef . fields || { } ) . map ( ( key : string ) => ( {
651- name : key ,
652- label : objectDef . fields [ key ] . label || key ,
653- type : objectDef . fields [ key ] . type || 'text'
654- } ) ) ,
655- }
656- ]
657- } }
658- dataSource = { dataSource }
659- onEdit = { ( ) => onEdit ( { _id : recordId , id : recordId } ) }
660- />
661- { /* Discussion panel — collapsible in drawer/overlay mode.
662- Items start empty; full data fetching is in RecordDetailView (page mode). */ }
663- < div className = "mt-6 border-t pt-6" >
664- < RecordChatterPanel
665- config = { {
666- position : 'bottom' ,
667- collapsible : true ,
668- defaultCollapsed : true ,
669- feed : {
670- enableReactions : true ,
671- enableThreading : true ,
672- showCommentInput : true ,
673- } ,
674- } }
675- items = { [ ] }
676- />
677- </ div >
678- </ div >
827+ < DrawerDetailContent
828+ objectDef = { objectDef }
829+ recordId = { recordId }
830+ dataSource = { dataSource }
831+ onEdit = { onEdit }
832+ />
679833 ) ;
680834 } }
681835 </ NavigationOverlay >
0 commit comments