Enhance component message handling and introduce new features
- Added `ComponentMessageBinding` model to manage message components in the database. - Updated `WelcomeConfig`, `Tag`, and `ScheduledMessage` models to support new component fields. - Integrated component handling in various modules, including Scheduler and Tags, to improve message customization. - Enhanced user experience by implementing new commands for creating and managing component messages in the utility module. - Updated localization files to reflect new component-related features and improve user guidance.
This commit is contained in:
147
apps/webui/src/components/modules/messages-manager.tsx
Normal file
147
apps/webui/src/components/modules/messages-manager.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
componentsV2HasContent,
|
||||
embedHasContent,
|
||||
type DashboardMessageMode,
|
||||
type MessageComponentsV2,
|
||||
type WelcomeEmbed
|
||||
} from '@nexumi/shared';
|
||||
import { Send } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { FieldAnchor } from '@/components/layout/field-anchor';
|
||||
import { useTranslations } from '@/components/locale-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
|
||||
import {
|
||||
DiscordComponentsV2Builder,
|
||||
emptyComponentsV2,
|
||||
normalizeComponentsV2
|
||||
} from '@/components/ui/discord-components-v2-builder';
|
||||
import { DiscordEmbedBuilder, emptyEmbed, normalizeEmbed } from '@/components/ui/discord-embed-builder';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface MessagesManagerProps {
|
||||
guildId: string;
|
||||
}
|
||||
|
||||
export function MessagesManager({ guildId }: MessagesManagerProps) {
|
||||
const t = useTranslations();
|
||||
const [channelId, setChannelId] = useState('');
|
||||
const [mode, setMode] = useState<DashboardMessageMode>('EMBED');
|
||||
const [embed, setEmbed] = useState<WelcomeEmbed | null>(emptyEmbed());
|
||||
const [components, setComponents] = useState<MessageComponentsV2 | null>(emptyComponentsV2());
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
async function handleSend() {
|
||||
if (!channelId.trim()) {
|
||||
toast.error(t('modulePages.messages.channelRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedEmbed = mode === 'EMBED' ? normalizeEmbed(embed) : null;
|
||||
const normalizedComponents = mode === 'COMPONENTS_V2' ? normalizeComponentsV2(components) : null;
|
||||
|
||||
if (
|
||||
mode === 'EMBED' &&
|
||||
!embedHasContent(normalizedEmbed) &&
|
||||
normalizedEmbed?.color === undefined
|
||||
) {
|
||||
toast.error(t('modulePages.messages.embedRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'COMPONENTS_V2' && !componentsV2HasContent(normalizedComponents)) {
|
||||
toast.error(t('modulePages.messages.componentsRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
setSending(true);
|
||||
try {
|
||||
const response = await fetch(`/api/guilds/${guildId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
channelId: channelId.trim(),
|
||||
mode,
|
||||
embed: normalizedEmbed,
|
||||
components: normalizedComponents
|
||||
})
|
||||
});
|
||||
const body = (await response.json().catch(() => null)) as { error?: string; messageId?: string | null } | null;
|
||||
if (!response.ok || !body) {
|
||||
toast.error(body?.error ?? t('common.saveError'));
|
||||
return;
|
||||
}
|
||||
toast.success(
|
||||
body.messageId
|
||||
? t('modulePages.messages.sentWithId', { id: body.messageId })
|
||||
: t('modulePages.messages.sent')
|
||||
);
|
||||
} catch {
|
||||
toast.error(t('common.saveError'));
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldAnchor id="field-messages-send">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('modulePages.messages.sendTitle')}</CardTitle>
|
||||
<CardDescription>{t('modulePages.messages.sendDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.messages.channelId')}</Label>
|
||||
<DiscordChannelSelect guildId={guildId} value={channelId} onChange={setChannelId} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.messages.mode')}</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['EMBED', 'COMPONENTS_V2'] as DashboardMessageMode[]).map((entry) => (
|
||||
<Button
|
||||
key={entry}
|
||||
type="button"
|
||||
variant={mode === entry ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(mode !== entry && 'text-muted-foreground')}
|
||||
onClick={() => setMode(entry)}
|
||||
>
|
||||
{t(`modulePages.messages.modes.${entry}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mode === 'EMBED' ? (
|
||||
<DiscordEmbedBuilder
|
||||
value={embed}
|
||||
onChange={setEmbed}
|
||||
placeholderPreset="scheduler"
|
||||
showClearHint={false}
|
||||
/>
|
||||
) : (
|
||||
<DiscordComponentsV2Builder
|
||||
guildId={guildId}
|
||||
value={components}
|
||||
onChange={setComponents}
|
||||
placeholderPreset="scheduler"
|
||||
showClearHint={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button type="button" onClick={handleSend} disabled={sending}>
|
||||
<Send className="size-4" />
|
||||
{sending ? t('modulePages.messages.sending') : t('modulePages.messages.send')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FieldAnchor>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user