From ed2ea23e87eedca147abdf35e38695ef520fc5ee Mon Sep 17 00:00:00 2001
From: TheOnlyMace <0815cracky@gmail.com>
Date: Wed, 22 Jul 2026 21:23:57 +0200
Subject: [PATCH] Refactor channel and role selection in various forms to use
Discord components
- Replaced standard input fields with DiscordChannelSelect and DiscordRoleSelect components in the BirthdaysForm, FeedsManager, GiveawaysManager, LevelingForm, LoggingForm, SchedulerManager, SelfRolesManager, StatsForm, SuggestionsConfigForm, TagsManager, TempVoiceForm, TicketConfigForm, and StarboardForm for improved user experience and consistency.
- Updated state management to accommodate new component structures, enhancing data handling for channel and role selections across the application.
---
.../app/dashboard/[guildId]/commands/page.tsx | 8 +-
.../src/components/modules/birthdays-form.tsx | 20 ++-
.../components/modules/commands-manager.tsx | 20 +--
.../src/components/modules/feeds-manager.tsx | 13 +-
.../components/modules/giveaways-manager.tsx | 13 +-
.../src/components/modules/leveling-form.tsx | 56 ++----
.../src/components/modules/logging-form.tsx | 61 +++----
.../components/modules/scheduler-manager.tsx | 13 +-
.../components/modules/selfroles-manager.tsx | 16 +-
.../src/components/modules/starboard-form.tsx | 64 +++----
.../src/components/modules/stats-form.tsx | 31 +++-
.../modules/suggestions-config-form.tsx | 32 ++--
.../src/components/modules/tags-manager.tsx | 63 ++++---
.../src/components/modules/tempvoice-form.tsx | 21 ++-
.../modules/ticket-categories-manager.tsx | 36 ++--
.../components/modules/ticket-config-form.tsx | 22 ++-
.../components/modules/verification-form.tsx | 20 ++-
.../src/components/modules/welcome-form.tsx | 62 +++----
.../components/settings/access-rules-form.tsx | 24 +--
.../components/ui/discord-channel-select.tsx | 161 ++++++++++++++++++
.../src/components/ui/discord-role-select.tsx | 144 ++++++++++++++++
apps/webui/src/hooks/use-guild-channels.ts | 56 ++++++
apps/webui/src/hooks/use-guild-roles.ts | 56 ++++++
apps/webui/src/lib/discord-guild-resources.ts | 31 +++-
apps/webui/src/messages/de.json | 3 +-
apps/webui/src/messages/en.json | 3 +-
docs/PHASE-TRACKING.md | 22 +++
27 files changed, 749 insertions(+), 322 deletions(-)
create mode 100644 apps/webui/src/components/ui/discord-channel-select.tsx
create mode 100644 apps/webui/src/components/ui/discord-role-select.tsx
create mode 100644 apps/webui/src/hooks/use-guild-channels.ts
create mode 100644 apps/webui/src/hooks/use-guild-roles.ts
diff --git a/apps/webui/src/app/dashboard/[guildId]/commands/page.tsx b/apps/webui/src/app/dashboard/[guildId]/commands/page.tsx
index 01ac972..68d7f21 100644
--- a/apps/webui/src/app/dashboard/[guildId]/commands/page.tsx
+++ b/apps/webui/src/app/dashboard/[guildId]/commands/page.tsx
@@ -1,6 +1,6 @@
import { Suspense } from 'react';
import { CommandsManager } from '@/components/modules/commands-manager';
-import { listGuildChannelsForDashboard, listGuildRolesForDashboard } from '@/lib/discord-guild-resources';
+import { listGuildChannelsForDashboard } from '@/lib/discord-guild-resources';
import { getLocale, t } from '@/lib/i18n';
import { getCommandGlobalRules } from '@/lib/module-configs/command-global-rules';
import { listCommandOverridesDashboard } from '@/lib/module-configs/commands';
@@ -11,12 +11,11 @@ interface CommandsPageProps {
export default async function CommandsPage({ params }: CommandsPageProps) {
const { guildId } = await params;
- const [locale, commands, globalRules, channels, roles] = await Promise.all([
+ const [locale, commands, globalRules, channels] = await Promise.all([
getLocale(),
listCommandOverridesDashboard(guildId),
getCommandGlobalRules(guildId),
- listGuildChannelsForDashboard(guildId).catch(() => []),
- listGuildRolesForDashboard(guildId).catch(() => [])
+ listGuildChannelsForDashboard(guildId).catch(() => [])
]);
return (
@@ -31,7 +30,6 @@ export default async function CommandsPage({ params }: CommandsPageProps) {
initialCommands={commands}
initialGlobalRules={globalRules}
channels={channels}
- roles={roles}
/>
diff --git a/apps/webui/src/components/modules/birthdays-form.tsx b/apps/webui/src/components/modules/birthdays-form.tsx
index c1f3dde..cccadb4 100644
--- a/apps/webui/src/components/modules/birthdays-form.tsx
+++ b/apps/webui/src/components/modules/birthdays-form.tsx
@@ -5,6 +5,8 @@ import { useId } from 'react';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
+import { DiscordRoleSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
@@ -36,8 +38,6 @@ interface BirthdaysFormProps {
export function BirthdaysForm({ guildId, initialValue }: BirthdaysFormProps) {
const t = useTranslations();
- const channelId = useId();
- const roleId = useId();
const timezoneId = useId();
return (
@@ -58,14 +58,22 @@ export function BirthdaysForm({ guildId, initialValue }: BirthdaysFormProps) {
-
- setValue((prev) => ({ ...prev, channelId: event.target.value.trim() }))} placeholder="123456789012345678" />
+
+ setValue((prev) => ({ ...prev, channelId }))}
+ />
-
- setValue((prev) => ({ ...prev, roleId: event.target.value.trim() }))} placeholder="123456789012345678" />
+
+ setValue((prev) => ({ ...prev, roleId }))}
+ />
diff --git a/apps/webui/src/components/modules/commands-manager.tsx b/apps/webui/src/components/modules/commands-manager.tsx
index b9f4866..8d45084 100644
--- a/apps/webui/src/components/modules/commands-manager.tsx
+++ b/apps/webui/src/components/modules/commands-manager.tsx
@@ -3,8 +3,7 @@
import type {
CommandGlobalRules,
CommandOverrideDashboard,
- DiscordChannelOption,
- DiscordRoleOption
+ DiscordChannelOption
} from '@nexumi/shared';
import { RotateCcw } from 'lucide-react';
import { useSearchParams } from 'next/navigation';
@@ -14,6 +13,7 @@ import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { DiscordResourceMultiSelect } from '@/components/ui/discord-resource-multi-select';
+import { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
@@ -26,15 +26,13 @@ interface CommandsManagerProps {
initialCommands: CommandOverrideDashboard[];
initialGlobalRules: CommandGlobalRules;
channels: DiscordChannelOption[];
- roles: DiscordRoleOption[];
}
export function CommandsManager({
guildId,
initialCommands,
initialGlobalRules,
- channels,
- roles
+ channels
}: CommandsManagerProps) {
const t = useTranslations();
const searchParams = useSearchParams();
@@ -48,10 +46,6 @@ export function CommandsManager({
() => channels.map((channel) => ({ id: channel.id, name: channel.name, prefix: '#' })),
[channels]
);
- const roleOptions = useMemo(
- () => roles.map((role) => ({ id: role.id, name: role.name, prefix: '@' })),
- [roles]
- );
useEffect(() => {
const q = searchParams.get('q');
@@ -250,8 +244,8 @@ export function CommandsManager({
{t('modulePages.commands.allowedRoleIds')}
-
patchLocal(entry.commandName, { allowedRoleIds })}
placeholder={t('modulePages.commands.roleSearchPlaceholder')}
@@ -260,8 +254,8 @@ export function CommandsManager({
{t('modulePages.commands.deniedRoleIds')}
-
patchLocal(entry.commandName, { deniedRoleIds })}
placeholder={t('modulePages.commands.roleSearchPlaceholder')}
diff --git a/apps/webui/src/components/modules/feeds-manager.tsx b/apps/webui/src/components/modules/feeds-manager.tsx
index b4a55bd..a8ba0d0 100644
--- a/apps/webui/src/components/modules/feeds-manager.tsx
+++ b/apps/webui/src/components/modules/feeds-manager.tsx
@@ -8,6 +8,8 @@ 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 { DiscordRoleSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
@@ -182,17 +184,18 @@ export function FeedsManager({ guildId, initialFeeds }: FeedsManagerProps) {
- setDraft((prev) => ({ ...prev, channelId: event.target.value }))}
- placeholder="123456789012345678"
+ onChange={(channelId) => setDraft((prev) => ({ ...prev, channelId }))}
/>
- setDraft((prev) => ({ ...prev, rolePingId: event.target.value }))}
+ onChange={(rolePingId) => setDraft((prev) => ({ ...prev, rolePingId }))}
placeholder={t('common.optional')}
/>
diff --git a/apps/webui/src/components/modules/giveaways-manager.tsx b/apps/webui/src/components/modules/giveaways-manager.tsx
index 788418b..2d285b5 100644
--- a/apps/webui/src/components/modules/giveaways-manager.tsx
+++ b/apps/webui/src/components/modules/giveaways-manager.tsx
@@ -9,6 +9,8 @@ import { useTranslations } from '@/components/locale-provider';
import { Badge } from '@/components/ui/badge';
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 { DiscordRoleSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -127,10 +129,10 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
- setDraft((prev) => ({ ...prev, channelId: event.target.value }))}
- placeholder="123456789012345678"
+ onChange={(channelId) => setDraft((prev) => ({ ...prev, channelId }))}
/>
@@ -156,9 +158,10 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
- setDraft((prev) => ({ ...prev, requiredRoleId: event.target.value }))}
+ onChange={(requiredRoleId) => setDraft((prev) => ({ ...prev, requiredRoleId }))}
placeholder={t('common.optional')}
/>
diff --git a/apps/webui/src/components/modules/leveling-form.tsx b/apps/webui/src/components/modules/leveling-form.tsx
index e537e3c..3aab9a2 100644
--- a/apps/webui/src/components/modules/leveling-form.tsx
+++ b/apps/webui/src/components/modules/leveling-form.tsx
@@ -1,10 +1,11 @@
'use client';
import type { LevelingConfigDashboard } from '@nexumi/shared';
-import { useId } from 'react';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { DiscordChannelMultiSelect, DiscordChannelSelect } from '@/components/ui/discord-channel-select';
+import { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
@@ -14,24 +15,11 @@ import type { SettingsSaveResult } from '@/components/settings/settings-form';
import { SettingsForm } from '@/components/settings/settings-form';
interface LocalLevelingValue
- extends Omit
{
- noXpChannelIdsText: string;
- noXpRoleIdsText: string;
+ extends Omit {
roleMultipliersText: string;
channelMultipliersText: string;
}
-function toCsv(ids: string[]): string {
- return ids.join(', ');
-}
-
-function fromCsv(text: string): string[] {
- return text
- .split(',')
- .map((entry) => entry.trim())
- .filter((entry) => entry.length > 0);
-}
-
function toMultiplierText(map: Record): string {
return Object.entries(map)
.map(([id, multiplier]) => `${id}:${multiplier}`)
@@ -58,11 +46,9 @@ function fromMultiplierText(text: string): { value: Record; erro
}
function toLocal(config: LevelingConfigDashboard): LocalLevelingValue {
- const { noXpChannelIds, noXpRoleIds, roleMultipliers, channelMultipliers, ...rest } = config;
+ const { roleMultipliers, channelMultipliers, ...rest } = config;
return {
...rest,
- noXpChannelIdsText: toCsv(noXpChannelIds),
- noXpRoleIdsText: toCsv(noXpRoleIds),
roleMultipliersText: toMultiplierText(roleMultipliers),
channelMultipliersText: toMultiplierText(channelMultipliers)
};
@@ -78,12 +64,10 @@ async function saveLeveling(guildId: string, value: LocalLevelingValue): Promise
return { ok: false, error: channelMultipliers.error };
}
- const { noXpChannelIdsText, noXpRoleIdsText, roleMultipliersText: _roleMultipliersText, channelMultipliersText: _channelMultipliersText, ...rest } = value;
+ const { roleMultipliersText: _roleMultipliersText, channelMultipliersText: _channelMultipliersText, ...rest } = value;
const payload: LevelingConfigDashboard = {
...rest,
- noXpChannelIds: fromCsv(noXpChannelIdsText),
- noXpRoleIds: fromCsv(noXpRoleIdsText),
roleMultipliers: roleMultipliers.value,
channelMultipliers: channelMultipliers.value
};
@@ -111,8 +95,6 @@ interface LevelingFormProps {
export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
const t = useTranslations();
- const noXpChannelsId = useId();
- const noXpRolesId = useId();
return (
@@ -192,23 +174,21 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
-
-
-
-
@@ -271,10 +251,10 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
- setValue((prev) => ({ ...prev, levelUpChannelId: event.target.value.trim() }))}
- placeholder="123456789012345678"
+ onChange={(levelUpChannelId) => setValue((prev) => ({ ...prev, levelUpChannelId }))}
/>
diff --git a/apps/webui/src/components/modules/logging-form.tsx b/apps/webui/src/components/modules/logging-form.tsx
index aa93e16..9d66051 100644
--- a/apps/webui/src/components/modules/logging-form.tsx
+++ b/apps/webui/src/components/modules/logging-form.tsx
@@ -2,16 +2,16 @@
import type { LogChannelMapping, LogEventTypeDashboard, LoggingConfigDashboard } from '@nexumi/shared';
import { Plus, Trash2 } from 'lucide-react';
-import { useId } from 'react';
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 { DiscordChannelMultiSelect, DiscordChannelSelect } from '@/components/ui/discord-channel-select';
+import { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
-import { Textarea } from '@/components/ui/textarea';
import type { SettingsSaveResult } from '@/components/settings/settings-form';
import { SettingsForm } from '@/components/settings/settings-form';
@@ -51,29 +51,18 @@ interface LocalLogChannel extends LogChannelMapping {
interface LocalLoggingValue {
enabled: boolean;
ignoreBots: boolean;
- ignoredChannelIdsText: string;
- ignoredRoleIdsText: string;
+ ignoredChannelIds: string[];
+ ignoredRoleIds: string[];
retentionDays: number;
logChannels: LocalLogChannel[];
}
-function toCsv(ids: string[]): string {
- return ids.join(', ');
-}
-
-function fromCsv(text: string): string[] {
- return text
- .split(',')
- .map((entry) => entry.trim())
- .filter((entry) => entry.length > 0);
-}
-
function toLocal(config: LoggingConfigDashboard): LocalLoggingValue {
return {
enabled: config.enabled,
ignoreBots: config.ignoreBots,
- ignoredChannelIdsText: toCsv(config.ignoredChannelIds),
- ignoredRoleIdsText: toCsv(config.ignoredRoleIds),
+ ignoredChannelIds: config.ignoredChannelIds,
+ ignoredRoleIds: config.ignoredRoleIds,
retentionDays: config.retentionDays,
logChannels: config.logChannels.map((mapping, index) => ({ ...mapping, clientKey: `${mapping.eventType}-${index}` }))
};
@@ -83,8 +72,8 @@ function toRemote(value: LocalLoggingValue): LoggingConfigDashboard {
return {
enabled: value.enabled,
ignoreBots: value.ignoreBots,
- ignoredChannelIds: fromCsv(value.ignoredChannelIdsText),
- ignoredRoleIds: fromCsv(value.ignoredRoleIdsText),
+ ignoredChannelIds: value.ignoredChannelIds,
+ ignoredRoleIds: value.ignoredRoleIds,
retentionDays: value.retentionDays,
logChannels: value.logChannels.map(({ clientKey: _clientKey, ...rest }) => rest)
};
@@ -114,8 +103,6 @@ interface LoggingFormProps {
export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
const t = useTranslations();
- const ignoredChannelsId = useId();
- const ignoredRolesId = useId();
return (
@@ -150,26 +137,22 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
-
-
-
-
@@ -233,17 +216,17 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
-
+ onChange={(channelId) =>
setValue((prev) => ({
...prev,
logChannels: prev.logChannels.map((entry) =>
- entry.clientKey === mapping.clientKey ? { ...entry, channelId: event.target.value.trim() } : entry
+ entry.clientKey === mapping.clientKey ? { ...entry, channelId } : entry
)
}))
}
- placeholder="123456789012345678"
/>