Skip to content

Commit e3af727

Browse files
Copilothotlong
andcommitted
fix: make DrawerDetailContent a proper component with full data fetching and comment handlers
Extract drawer content into DrawerDetailContent component that uses hooks for: - Fetching persisted comments from sys_comment via dataSource - handleAddComment with backend persistence - handleAddReply with threading support - handleToggleReaction with backend persistence This matches the full functionality of RecordDetailView, ensuring consistent experience between drawer and page mode as required by the issue. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 445aa82 commit e3af727

File tree

1 file changed

+196
-42
lines changed

1 file changed

+196
-42
lines changed

apps/console/src/components/ObjectView.tsx

Lines changed: 196 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import '@object-ui/plugin-kanban';
2222
import '@object-ui/plugin-calendar';
2323
import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@object-ui/components';
2424
import { 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';
2626
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
2727
import { ViewConfigPanel } from './ViewConfigPanel';
2828
import { 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+
59248
export 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

Comments
 (0)