Replace react-markdown in comment/reply display with Tiptap readonly mode, ensuring visual consistency between editing and viewing. Extract shared BaseMentionExtension with MentionView NodeView used in both modes — issue mentions render as inline cards with StatusIcon, clickable to open in new tab. Redesign mention suggestion popup with grouped sections (Users/Issues), agent badges, and StatusIcon for issues. - New: mention-extension.ts (shared mention core) - New: mention-view.tsx (shared NodeView for both modes) - New: readonly-editor.tsx (lightweight Tiptap readonly wrapper) - Modified: rich-text-editor.tsx (import from shared mention-extension) - Modified: rich-text-editor.css (readonly + issue-mention overrides) - Modified: comment-card.tsx (Markdown → ReadonlyEditor) - Modified: mention-suggestion.tsx (grouped UI, StatusIcon, agent badge) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
"use client";
|
|
|
|
import { NodeViewWrapper } from "@tiptap/react";
|
|
import type { NodeViewProps } from "@tiptap/react";
|
|
import { useIssueStore } from "@/features/issues/store";
|
|
import { StatusIcon } from "@/features/issues/components/status-icon";
|
|
|
|
/**
|
|
* MentionView — shared NodeView for mention nodes (both editing and readonly).
|
|
*
|
|
* Rendering and behavior are identical in both modes.
|
|
* Issue mentions are always clickable (open in new tab).
|
|
*/
|
|
export function MentionView({ node }: NodeViewProps) {
|
|
const { type, id, label } = node.attrs;
|
|
|
|
if (type === "issue") {
|
|
return (
|
|
<NodeViewWrapper as="span" className="inline">
|
|
<IssueMention issueId={id} fallbackLabel={label} />
|
|
</NodeViewWrapper>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<NodeViewWrapper as="span" className="inline">
|
|
<span className="mention">@{label ?? id}</span>
|
|
</NodeViewWrapper>
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// IssueMention — inline card, always opens in new tab
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function IssueMention({
|
|
issueId,
|
|
fallbackLabel,
|
|
}: {
|
|
issueId: string;
|
|
fallbackLabel?: string;
|
|
}) {
|
|
const issue = useIssueStore((s) => s.issues.find((i) => i.id === issueId));
|
|
|
|
const handleClick = (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
window.open(`/issues/${issueId}`, "_blank", "noopener,noreferrer");
|
|
};
|
|
|
|
if (!issue) {
|
|
return (
|
|
<a
|
|
href={`/issues/${issueId}`}
|
|
onClick={handleClick}
|
|
className="issue-mention text-primary font-medium cursor-pointer hover:underline"
|
|
>
|
|
{fallbackLabel ?? issueId.slice(0, 8)}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<a
|
|
href={`/issues/${issueId}`}
|
|
onClick={handleClick}
|
|
className="issue-mention inline-flex items-center align-middle gap-1.5 rounded-md border px-2 py-0.5 text-sm hover:bg-accent transition-colors cursor-pointer"
|
|
>
|
|
<StatusIcon status={issue.status} className="h-3.5 w-3.5" />
|
|
<span className="font-medium text-muted-foreground">{issue.identifier}</span>
|
|
<span className="text-foreground">{issue.title}</span>
|
|
</a>
|
|
);
|
|
}
|