gitdataai/src/components/room/RoomMessageActions.tsx
ZhenYi 99bc4eeb80 chore: API and frontend UI adjustments
- API: issue label bulk add, search messages, room WS push, openapi
- Frontend: notify page, issue detail AI triage banner, search page,
  repository settings, preferences, PR components, file browser
- Room: DiscordChannelSidebar, RoomPinPanel, RoomMessageActions,
  RoomThreadPanel, MessageContent, repository-context
- Frontend SDK regenerated from openapi.json
2026-04-25 09:54:05 +08:00

169 lines
6.1 KiB
TypeScript

import type { MessageWithMeta } from '@/contexts';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useUser } from '@/contexts';
import { cn } from '@/lib/utils';
import { AlertCircle, Copy, Edit, GitPullRequest, LayoutDashboard, MoreHorizontal, Reply, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { getSenderUserUid } from './sender';
interface RoomMessageActionsProps {
message: MessageWithMeta;
onEdit?: () => void;
onRevoke?: () => void;
onReply?: () => void;
}
export function RoomMessageActions({ message, onEdit, onRevoke, onReply }: RoomMessageActionsProps) {
const [isOpen, setIsOpen] = useState(false);
const { user } = useUser();
const isOwner = user?.uid === getSenderUserUid(message);
const handleCopy = async () => {
if (message.content_type !== 'text') return;
try {
await navigator.clipboard.writeText(message.content);
toast.success('Message copied');
} catch {
toast.error('Failed to copy message');
} finally {
setIsOpen(false);
}
};
return (
<div
className={cn(
'absolute -top-3 right-4 z-20 items-center gap-1 rounded-md border border-border/70 bg-popover/95 p-1 shadow-sm',
isOpen ? 'flex' : 'hidden group-hover:flex',
)}
>
{onReply && (
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={onReply} title="Reply">
<Reply className="h-3.5 w-3.5" />
</Button>
)}
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger className="inline-flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground">
<MoreHorizontal className="h-3.5 w-3.5" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" sideOffset={6}>
{message.content_type === 'text' && (
<DropdownMenuItem onClick={handleCopy}>
<Copy className="mr-2 h-4 w-4" />
Copy
</DropdownMenuItem>
)}
{message.content_type === 'text' && (
<DropdownMenuItem
onClick={() => {
toast.info('Creating issue from message…', {
description: 'This will open the issue creation form with the message content pre-filled.',
action: {
label: 'Create',
onClick: () => {
// TODO: wire to POST /api/issue/{project}/issues/from-message
const title = message.content.split('\n')[0].slice(0, 80);
const body = `Converted from room message (${message.id})\n\n${message.content}`;
const params = new URLSearchParams({ title, body });
window.open(`/project/-/issues/new?${params}`, '_blank');
},
},
});
setIsOpen(false);
}}
>
<AlertCircle className="mr-2 h-4 w-4" />
Create Issue
</DropdownMenuItem>
)}
{message.content_type === 'text' && /\n```[\s\S]*?\n```/.test(message.content) && (
<DropdownMenuItem
onClick={() => {
setIsOpen(false);
toast.info('Creating PR from message…', {
description: 'Open the PR creation form with the code snippet pre-filled.',
action: {
label: 'Open',
onClick: () => {
// TODO: wire to POST /api/repo_pr/{ns}/{repo}/pulls/from-message
const params = new URLSearchParams({
body: `Converted from room message (${message.id})\n\n${message.content}`,
});
window.open(`/pulls/new?${params}`, '_blank');
},
},
});
}}
>
<GitPullRequest className="mr-2 h-4 w-4" />
Create PR
</DropdownMenuItem>
)}
{message.content_type === 'text' && (
<DropdownMenuItem
onClick={() => {
setIsOpen(false);
toast.info('Adding to board…', {
description: 'Open the kanban board with this message as the card description.',
action: {
label: 'Open Board',
onClick: () => {
// TODO: wire to POST /api/board/cards
const params = new URLSearchParams({
title: message.content.split('\n')[0].slice(0, 80),
description: message.content,
});
window.open(`/boards/new?${params}`, '_blank');
},
},
});
}}
>
<LayoutDashboard className="mr-2 h-4 w-4" />
Add to Board
</DropdownMenuItem>
)}
{isOwner && onEdit && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
onEdit();
setIsOpen(false);
}}
>
<Edit className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
</>
)}
{isOwner && onRevoke && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => {
onRevoke();
setIsOpen(false);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}