fix(web): hide resolved comments from preview overlays (#1762)

This commit is contained in:
Yuhao Chen 2026-05-15 15:46:03 +08:00 committed by GitHub
parent bceca66bb4
commit b2d2635360
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 159 additions and 6 deletions

View file

@ -3685,6 +3685,7 @@ function HtmlViewer({
);
const [activeCommentTarget, setActiveCommentTarget] = useState<PreviewCommentSnapshot | null>(null);
const [hoveredCommentTarget, setHoveredCommentTarget] = useState<PreviewCommentSnapshot | null>(null);
const [activePreviewCommentId, setActivePreviewCommentId] = useState<string | null>(null);
const [liveCommentTargets, setLiveCommentTargets] = useState<Map<string, PreviewCommentSnapshot>>(() => new Map());
const liveCommentTargetsRef = useRef(liveCommentTargets);
const [commentDraft, setCommentDraft] = useState('');
@ -4178,6 +4179,7 @@ function HtmlViewer({
if (!selectionMode) {
setActiveCommentTarget((current) => (current ? null : current));
setHoveredCommentTarget((current) => (current ? null : current));
setActivePreviewCommentId((current) => (current ? null : current));
setLiveCommentTargets((current) => (current.size > 0 ? new Map() : current));
setQueuedBoardNotes((current) => (current.length > 0 ? [] : current));
setStrokePoints((current) => (current.length > 0 ? [] : current));
@ -4245,11 +4247,16 @@ function HtmlViewer({
if (data.type === 'od:comment-target') {
const snapshot = snapshotFromData(data);
if (!snapshot.elementId) return;
const existing = previewComments.find((comment) => comment.elementId === snapshot.elementId);
const existing = previewComments.find((comment) =>
comment.filePath === file.name &&
comment.status === 'open' &&
comment.elementId === snapshot.elementId,
);
setActiveCommentTarget(snapshot);
setHoveredCommentTarget(snapshot);
setLiveCommentTargets((current) => new Map(current).set(snapshot.elementId, snapshot));
if (boardMode) {
setActivePreviewCommentId(existing?.id ?? null);
setCommentDraft(existing?.note ?? '');
setQueuedBoardNotes([]);
}
@ -4285,6 +4292,7 @@ function HtmlViewer({
}
setActiveCommentTarget(nextTarget);
setHoveredCommentTarget(nextTarget);
setActivePreviewCommentId(null);
setQueuedBoardNotes([]);
setCommentDraft('');
setStrokePoints([]);
@ -5034,6 +5042,7 @@ function HtmlViewer({
function clearBoardComposer() {
setActiveCommentTarget(null);
setHoveredCommentTarget(null);
setActivePreviewCommentId(null);
setCommentDraft('');
setQueuedBoardNotes([]);
setStrokePoints([]);
@ -5083,9 +5092,15 @@ function HtmlViewer({
const canShare = source !== null;
const exportTitle = file.name.replace(/\.html?$/i, '') || file.name;
const canPptx = canShare && Boolean(onExportAsPptx) && !streaming;
const visibleSideComments = previewComments.filter(
(comment) => comment.filePath === file.name && comment.status === 'open',
const visibleSideComments = useMemo(
() => previewComments.filter((comment) => comment.filePath === file.name && comment.status === 'open'),
[file.name, previewComments],
);
useEffect(() => {
if (!boardMode || !activePreviewCommentId) return;
const stillOpen = visibleSideComments.some((comment) => comment.id === activePreviewCommentId);
if (!stillOpen) clearBoardComposer();
}, [activePreviewCommentId, boardMode, visibleSideComments]);
const activeDeployment = deployResult || deployment;
const activeDeployedUrl = activeDeployment?.url?.trim() || '';
const activeDeploymentDelayed = activeDeployment?.status === 'link-delayed';
@ -5798,7 +5813,7 @@ function HtmlViewer({
</div>
{(boardMode || drawClickSelectionMode) ? (
<CommentPreviewOverlays
comments={boardMode ? previewComments : []}
comments={boardMode ? visibleSideComments : []}
liveTargets={liveCommentTargets}
hoveredTarget={hoveredCommentTarget}
activeTarget={activeCommentTarget}
@ -5808,6 +5823,7 @@ function HtmlViewer({
onOpenComment={(comment, snapshot) => {
setActiveCommentTarget(snapshot);
setHoveredCommentTarget(snapshot);
setActivePreviewCommentId(comment.id);
setCommentDraft(comment.note);
setQueuedBoardNotes([]);
}}
@ -5834,7 +5850,7 @@ function HtmlViewer({
{boardMode && activeCommentTarget ? (
<BoardComposerPopover
target={activeCommentTarget}
existing={previewComments.find((comment) => comment.elementId === activeCommentTarget.elementId) ?? null}
existing={visibleSideComments.find((comment) => comment.elementId === activeCommentTarget.elementId) ?? null}
draft={commentDraft}
notes={queuedBoardNotes}
onDraft={setCommentDraft}
@ -5889,6 +5905,7 @@ function HtmlViewer({
};
setActiveCommentTarget(snapshot);
setHoveredCommentTarget(snapshot);
setActivePreviewCommentId(comment.id);
setCommentDraft(comment.note);
setQueuedBoardNotes([]);
}}

View file

@ -34,7 +34,7 @@ import {
updateInspectOverride,
} from '../../src/components/FileViewer';
import type { InspectOverrideMap } from '../../src/components/FileViewer';
import type { LiveArtifact, LiveArtifactWorkspaceEntry, ProjectFile } from '../../src/types';
import type { LiveArtifact, LiveArtifactWorkspaceEntry, PreviewComment, ProjectFile } from '../../src/types';
import { I18nProvider } from '../../src/i18n';
import type { Dict } from '../../src/i18n/types';
@ -1141,6 +1141,142 @@ describe('FileViewer tweaks toolbar', () => {
expect(screen.queryByText('Queues while working')).toBeNull();
});
it('hides non-open saved comments from preview markers when the side panel is empty', () => {
const resolvedComment: PreviewComment = {
id: 'comment-applying',
projectId: 'project-1',
conversationId: 'conversation-1',
filePath: 'preview.html',
elementId: 'pin-applying',
selector: '[data-od-pin="pin-applying"]',
label: 'pin-applying',
text: '',
htmlHint: '',
position: { x: 24, y: 32, width: 18, height: 18 },
note: 'Already sent to Claude',
status: 'applying',
createdAt: Date.now(),
updatedAt: Date.now(),
};
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
previewComments={[resolvedComment]}
/>,
);
fireEvent.click(screen.getByTestId('board-mode-toggle'));
expect(screen.getByTestId('comment-side-panel')).toBeTruthy();
expect(screen.queryByTestId('comment-saved-marker-pin-applying')).toBeNull();
expect(screen.queryByText('Already sent to Claude')).toBeNull();
});
it('does not preload non-open element comments into the picker composer', async () => {
const applyingElementComment: PreviewComment = {
id: 'comment-element-applying',
projectId: 'project-1',
conversationId: 'conversation-1',
filePath: 'preview.html',
elementId: 'hero',
selector: '[data-od-id="hero"]',
label: 'Hero',
text: 'Hero',
htmlHint: '<main data-od-id="hero">Hero</main>',
position: { x: 8, y: 12, width: 120, height: 48 },
note: 'Do not resurrect this note',
status: 'applying',
createdAt: Date.now(),
updatedAt: Date.now(),
};
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
previewComments={[applyingElementComment]}
/>,
);
const frame = screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement;
fireEvent.click(screen.getByTestId('board-mode-toggle'));
window.dispatchEvent(new MessageEvent('message', {
source: frame.contentWindow,
data: {
type: 'od:comment-target',
elementId: 'hero',
selector: '[data-od-id="hero"]',
label: 'Hero',
text: 'Hero',
position: { x: 8, y: 12, width: 120, height: 48 },
htmlHint: '<main data-od-id="hero">Hero</main>',
},
}));
const input = await screen.findByTestId('comment-popover-input') as HTMLTextAreaElement;
expect(input.value).toBe('');
expect(screen.queryByText('Remove')).toBeNull();
expect(screen.queryByText('Do not resurrect this note')).toBeNull();
});
it('closes an open saved-comment composer when that comment leaves the open state', async () => {
const openComment: PreviewComment = {
id: 'comment-status-transition',
projectId: 'project-1',
conversationId: 'conversation-1',
filePath: 'preview.html',
elementId: 'pin-transition',
selector: '[data-od-pin="pin-transition"]',
label: 'pin-transition',
text: '',
htmlHint: '',
position: { x: 40, y: 52, width: 18, height: 18 },
note: 'Do not recreate this stale comment',
status: 'open',
createdAt: Date.now(),
updatedAt: Date.now(),
};
const { rerender } = render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
previewComments={[openComment]}
/>,
);
fireEvent.click(screen.getByTestId('board-mode-toggle'));
fireEvent.click(screen.getByRole('button', { name: 'Open comment for pin-transition' }));
expect((await screen.findByTestId('comment-popover-input') as HTMLTextAreaElement).value)
.toBe('Do not recreate this stale comment');
rerender(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
previewComments={[{ ...openComment, status: 'applying' }]}
/>,
);
await waitFor(() => {
expect(screen.queryByTestId('comment-popover-input')).toBeNull();
});
expect(screen.queryByTestId('comment-saved-marker-pin-transition')).toBeNull();
expect(screen.queryByText('Do not recreate this stale comment')).toBeNull();
});
it('collapses the comment side panel into a narrow reopen rail', () => {
const onCollapseChange = vi.fn();