feat(review): update FeedbackPanel with expandRequest, draftPin input, and lifted thread detail state

- Replace expandThreadId with expandRequest pulse pattern (seq counter)
- Add draftPin, onCreatePointThread, focusedThreadId, onThreadDetailChange props
- Lift expandedThreadId changes to parent via onThreadDetailChange callback
- Draft pin comment input section shown above thread list
- Thread detail collapse signal via expandRequest with null threadId
- Pass thread object to onJumpToObject for point-aware navigation
This commit is contained in:
Hiren Kangad
2026-03-12 22:09:12 +05:30
parent 3b15166c32
commit 9bd2810f5a
@@ -2,7 +2,9 @@ import React, { useState, useCallback, useEffect, useSyncExternalStore } from 'r
import { AnnotationStore } from '../../stores/annotationStore'; import { AnnotationStore } from '../../stores/annotationStore';
import ThreadList, { FilterType } from './ThreadList'; import ThreadList, { FilterType } from './ThreadList';
import ThreadDetail from './ThreadDetail'; import ThreadDetail from './ThreadDetail';
import { PANEL_BG, BORDER, TEXT_MUTED, STATUS_OPEN } from './feedbackStyles'; import CommentInput from './CommentInput';
import { PANEL_BG, BORDER, TEXT_MUTED, TEXT_PRIMARY, STATUS_OPEN } from './feedbackStyles';
import type { DraftPin } from '../../pages/Editor';
interface FeedbackPanelProps { interface FeedbackPanelProps {
annotationStore: AnnotationStore; annotationStore: AnnotationStore;
@@ -11,10 +13,18 @@ interface FeedbackPanelProps {
boardId: string; boardId: string;
token: string; token: string;
canvasObjects: Map<string, { id: string; name?: string; type: string }>; canvasObjects: Map<string, { id: string; name?: string; type: string }>;
onJumpToObject?: (objectId: string) => void; onJumpToObject?: (objectId: string, thread?: any) => void;
onError?: (msg: string) => void; onError?: (msg: string) => void;
/** Set externally to expand a specific thread (e.g. from pin click) */ /** Pulse signal to expand a specific thread */
expandThreadId?: string | null; expandRequest?: { threadId: string | null; seq: number } | null;
/** Focused thread ID for highlighting */
focusedThreadId?: string | null;
/** Draft pin for point-comment creation */
draftPin?: DraftPin | null;
/** Callback to create a point-pinned thread */
onCreatePointThread?: (draftPin: DraftPin, content: string) => Promise<void>;
/** Callback when expanded thread detail changes */
onThreadDetailChange?: (threadId: string | null) => void;
} }
export default function FeedbackPanel({ export default function FeedbackPanel({
@@ -26,7 +36,11 @@ export default function FeedbackPanel({
canvasObjects, canvasObjects,
onJumpToObject, onJumpToObject,
onError, onError,
expandThreadId, expandRequest,
focusedThreadId,
draftPin,
onCreatePointThread,
onThreadDetailChange,
}: FeedbackPanelProps) { }: FeedbackPanelProps) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [expandedThreadId, setExpandedThreadId] = useState<string | null>(null); const [expandedThreadId, setExpandedThreadId] = useState<string | null>(null);
@@ -39,13 +53,25 @@ export default function FeedbackPanel({
() => annotationStore.version, () => annotationStore.version,
); );
// External expand trigger (from pin click) // Wrap setExpandedThreadId to notify parent
const updateExpandedThread = useCallback((id: string | null) => {
setExpandedThreadId(id);
onThreadDetailChange?.(id);
}, [onThreadDetailChange]);
// External expand trigger (from pin click or submit)
useEffect(() => { useEffect(() => {
if (expandThreadId) { if (!expandRequest) return;
setExpandedThreadId(expandThreadId); if (expandRequest.threadId) {
// Open/expand a specific thread
updateExpandedThread(expandRequest.threadId);
setCollapsed(false); setCollapsed(false);
} else {
// Collapse signal (threadId: null) — close thread detail
updateExpandedThread(null);
} }
}, [expandThreadId]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [expandRequest?.seq]);
const allThreads = Array.from(annotationStore.threads.values()); const allThreads = Array.from(annotationStore.threads.values());
const openCount = allThreads.filter((t) => t.status === 'open').length; const openCount = allThreads.filter((t) => t.status === 'open').length;
@@ -147,12 +173,12 @@ export default function FeedbackPanel({
method: 'DELETE', method: 'DELETE',
headers: headers(false), headers: headers(false),
}); });
setExpandedThreadId(null); updateExpandedThread(null);
} catch { } catch {
// Error surfaced via onError // Error surfaced via onError
} }
}, },
[boardId, headers, apiFetch], [boardId, headers, apiFetch, updateExpandedThread],
); );
const handleCreateThread = useCallback(async () => { const handleCreateThread = useCallback(async () => {
@@ -245,12 +271,12 @@ export default function FeedbackPanel({
thread={expandedThread} thread={expandedThread}
store={annotationStore} store={annotationStore}
userId={userId} userId={userId}
onBack={() => setExpandedThreadId(null)} onBack={() => updateExpandedThread(null)}
onReply={handleReply} onReply={handleReply}
onResolve={handleResolve} onResolve={handleResolve}
onDeleteComment={handleDeleteComment} onDeleteComment={handleDeleteComment}
onDeleteThread={handleDeleteThread} onDeleteThread={handleDeleteThread}
onJumpToObject={onJumpToObject} onJumpToObject={onJumpToObject ? (objectId) => onJumpToObject(objectId, expandedThread) : undefined}
/> />
); );
} }
@@ -263,6 +289,30 @@ export default function FeedbackPanel({
selectedObjectId?.slice(0, 8) || selectedObjectId?.slice(0, 8) ||
''; '';
// Draft pin comment input (shown above thread list when draft is active)
const draftCommentSection = draftPin ? (
<div style={{ padding: '12px 16px', borderBottom: `1px solid ${BORDER}`, background: 'rgba(249, 115, 22, 0.03)' }}>
<div style={{ color: TEXT_MUTED, fontSize: '10px', marginBottom: '8px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
Comment on point
</div>
<div style={{ color: TEXT_PRIMARY, fontSize: '12px', marginBottom: '10px', fontWeight: 500 }}>
{canvasObjects.get(draftPin.objectId)?.name || canvasObjects.get(draftPin.objectId)?.type || 'Object'}
</div>
<CommentInput
value={newCommentText}
onChange={setNewCommentText}
onSubmit={async () => {
if (!newCommentText.trim() || !onCreatePointThread || !draftPin) return;
await onCreatePointThread(draftPin, newCommentText.trim());
setNewCommentText('');
}}
placeholder="Add a point comment..."
submitLabel="Comment"
autoFocus
/>
</div>
) : null;
return ( return (
<ThreadList <ThreadList
threads={threads} threads={threads}
@@ -271,13 +321,15 @@ export default function FeedbackPanel({
openCount={openCount} openCount={openCount}
filter={filter} filter={filter}
onFilterChange={setFilter} onFilterChange={setFilter}
onSelectThread={setExpandedThreadId} onSelectThread={updateExpandedThread}
onCollapse={() => setCollapsed(true)} onCollapse={() => setCollapsed(true)}
selectedObjectId={selectedObjectId} selectedObjectId={selectedObjectId}
selectedObjectLabel={selectedLabel} selectedObjectLabel={selectedLabel}
newCommentText={newCommentText} newCommentText={newCommentText}
onNewCommentChange={setNewCommentText} onNewCommentChange={setNewCommentText}
onCreateThread={handleCreateThread} onCreateThread={handleCreateThread}
headerSlot={draftCommentSection}
focusedThreadId={focusedThreadId}
/> />
); );
} }