fix: clear selected preview comments (#3144)

* fix: The "clear" button for comments is not functioning; the comments no longer have serial numbers.

* fix: The active pin always renders {visibleComments.length + 1}, but showActivePin (= commentCreateMode) is also true while editing an existing comment: onOpenComment at line 6821 calls setCommentCreateMode(true) and setActiveCommentTarget(snapshot) against the saved comment the user just clicked. In that path the overlay now stamps a stale number on top of an existing saved marker (e.g. clicking the pin showing 2 paints an additional 3 at the same position), which contradicts the invariant this PR is restoring — that preview-area numbers match the side-panel numbers.

---------

Co-authored-by: 郑惠 <14549727+felicia-study@user.noreply.gitee.com>
This commit is contained in:
feliciaZH 2026-05-28 18:56:21 +08:00 committed by GitHub
parent ed16de6f92
commit b746efefe2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 103 additions and 9 deletions

View file

@ -1996,7 +1996,7 @@ export function CommentSidePanel({
<div className="comment-side-empty">
{t('chat.comments.emptySaved')}
</div>
) : sorted.map((comment) => {
) : sorted.map((comment, index) => {
const selected = visibleSelectedIds.has(comment.id);
const active = comment.id === activeCommentId;
return (
@ -2009,7 +2009,7 @@ export function CommentSidePanel({
>
<div className="comment-side-item-head">
<span className="comment-side-author">
<strong>{commentDisplayLabel(comment, t)}</strong>
<strong>{`${index + 1}. ${commentDisplayLabel(comment, t)}`}</strong>
</span>
<span className="comment-side-time">{formatCommentTime(comment.createdAt, t)}</span>
<button
@ -2834,6 +2834,12 @@ function CommentPreviewOverlays({
.filter((item): item is { comment: PreviewComment; index: number; snapshot: PreviewCommentSnapshot } =>
Boolean(item.snapshot),
);
const activeSavedIndex = activeTarget
? visibleComments.findIndex(({ snapshot }) => snapshot.elementId === activeTarget.elementId)
: -1;
const activePinNumber = activeSavedIndex >= 0
? activeSavedIndex + 1
: visibleComments.length + 1;
const targetOverlay = activeTarget ?? hoveredTarget;
return (
<div className="comment-overlay-layer" aria-hidden={false}>
@ -2861,10 +2867,10 @@ function CommentPreviewOverlays({
event.stopPropagation();
onOpenComment(comment, snapshot);
}}
title={`${label}: ${comment.note}`}
title={`${index + 1}. ${label}: ${comment.note}`}
aria-label={`Open comment for ${label}`}
>
C
{index + 1}
</button>
</div>
);
@ -2884,7 +2890,7 @@ function CommentPreviewOverlays({
data-testid="comment-active-pin"
aria-hidden="true"
>
C
{activePinNumber}
</div>
) : null}
{boardTool === 'pod' && strokePoints.length > 1 ? (
@ -6181,7 +6187,25 @@ function HtmlViewer({
return next;
});
}}
onClearSelection={() => setSelectedSideCommentIds(new Set())}
onClearSelection={() => {
if (selectedSideCommentIds.size === 0) return;
if (!onRemovePreviewComment) {
setSelectedSideCommentIds(new Set());
return;
}
const selectedIds = new Set(selectedSideCommentIds);
const targets = visibleSideComments
.filter((comment) => selectedIds.has(comment.id))
.map((comment) => comment.id);
if (targets.length === 0) {
setSelectedSideCommentIds(new Set());
return;
}
void (async () => {
await Promise.allSettled(targets.map((id) => onRemovePreviewComment(id)));
setSelectedSideCommentIds(new Set());
})();
}}
onReply={(comment) => {
// Reply == edit on a flat-thread model: prefill the
// popover with the existing note so the user sees and

View file

@ -1696,8 +1696,8 @@ describe('FileViewer tweaks toolbar', () => {
fireEvent.click(screen.getByTestId('comment-panel-toggle'));
expect(screen.getByTestId('comment-side-panel')).toBeTruthy();
expect(screen.getByTestId('comment-saved-marker-pin-newer').textContent).toBe('C');
expect(screen.getByTestId('comment-saved-marker-pin-older').textContent).toBe('C');
expect(screen.getByTestId('comment-saved-marker-pin-newer').textContent).toBe('1');
expect(screen.getByTestId('comment-saved-marker-pin-older').textContent).toBe('2');
clickAgentTool('board-mode-toggle');
@ -1722,7 +1722,7 @@ describe('FileViewer tweaks toolbar', () => {
},
}));
expect((await screen.findByTestId('comment-active-pin')).textContent).toBe('C');
expect((await screen.findByTestId('comment-active-pin')).textContent).toBe('3');
expect(screen.getByTestId('comment-saved-marker-pin-newer')).toBeTruthy();
expect(screen.getByTestId('comment-saved-marker-pin-older')).toBeTruthy();
@ -1732,6 +1732,7 @@ describe('FileViewer tweaks toolbar', () => {
expect(activeItem?.className).toContain('active');
expect(activeItem?.getAttribute('aria-current')).toBe('true');
});
expect(screen.getByTestId('comment-active-pin').textContent).toBe('1');
expect(document.querySelector('[data-comment-id="comment-older"]')?.className).not.toContain('active');
});
@ -2076,6 +2077,75 @@ describe('FileViewer tweaks toolbar', () => {
expect(screen.queryByTestId('comment-side-selectbar')).toBeNull();
expect(screen.queryByTestId('comment-side-collapsed-rail')).toBeNull();
});
it('deletes selected comments when clear is clicked', async () => {
const removed: string[] = [];
function Harness() {
const [comments, setComments] = useState<PreviewComment[]>([
{
id: 'comment-1',
projectId: 'project-1',
conversationId: 'conversation-1',
filePath: 'preview.html',
elementId: 'pin-1',
selector: '[data-od-pin="pin-1"]',
label: 'pin-1',
text: '',
htmlHint: '',
position: { x: 16, y: 20, width: 18, height: 18 },
note: 'First',
status: 'open',
createdAt: 10,
updatedAt: 10,
},
{
id: 'comment-2',
projectId: 'project-1',
conversationId: 'conversation-1',
filePath: 'preview.html',
elementId: 'pin-2',
selector: '[data-od-pin="pin-2"]',
label: 'pin-2',
text: '',
htmlHint: '',
position: { x: 48, y: 20, width: 18, height: 18 },
note: 'Second',
status: 'open',
createdAt: 20,
updatedAt: 20,
},
]);
return (
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
previewComments={comments}
onRemovePreviewComment={async (commentId) => {
removed.push(commentId);
setComments((current) => current.filter((comment) => comment.id !== commentId));
}}
/>
);
}
render(<Harness />);
fireEvent.click(screen.getByTestId('comment-panel-toggle'));
const selectButtons = screen.getAllByRole('button', { name: /select/i });
const firstSelectButton = selectButtons[0];
expect(firstSelectButton).toBeTruthy();
if (!firstSelectButton) return;
fireEvent.click(firstSelectButton);
fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
await waitFor(() => {
expect(screen.queryByText('Second')).toBeNull();
});
expect(removed).toEqual(['comment-2']);
});
});
describe('applyInspectOverridesToSource', () => {