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:
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user