Skip to content

Commit b8d09b3

Browse files
author
Pip Build
committed
fix: address CodeRabbit review comments
- Add Zustand discord-entities store for caching channels/roles - Update ChannelSelector and RoleSelector to use store caching - Add stable UUIDs for role menu options - Add validation for empty role option label/roleId - Add aria-label to remove button for accessibility
1 parent 05978bc commit b8d09b3

5 files changed

Lines changed: 137 additions & 9 deletions

File tree

pnpm-lock.yaml

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@
2525
"lucide-react": "^0.525.0",
2626
"next": "^16.1.6",
2727
"next-auth": "^4.24.13",
28+
"next-themes": "^0.4.6",
2829
"radix-ui": "^1.4.3",
2930
"react": "^19.2.4",
3031
"react-dom": "^19.2.4",
32+
"react-hook-form": "^7.56.4",
3133
"recharts": "^3.7.0",
3234
"server-only": "^0.0.1",
3335
"sonner": "^2.0.7",
3436
"tailwind-merge": "^3.5.0",
35-
"next-themes": "^0.4.6",
36-
"react-hook-form": "^7.56.4"
37+
"zustand": "^5.0.11"
3738
},
3839
"devDependencies": {
3940
"@tailwindcss/postcss": "^4.2.1",

web/src/components/dashboard/config-editor.tsx

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@ export function ConfigEditor() {
180180
throw new Error('Invalid config response');
181181
}
182182

183+
// Ensure role menu options have stable IDs
184+
if (data.welcome?.roleMenu?.options) {
185+
data.welcome.roleMenu.options = data.welcome.roleMenu.options.map((opt) => ({
186+
...opt,
187+
id: opt.id || crypto.randomUUID(),
188+
}));
189+
}
183190
setSavedConfig(data);
184191
setDraftConfig(structuredClone(data));
185192
setDmStepsRaw((data.welcome?.dmSequence?.steps ?? []).join('\n'));
@@ -208,6 +215,12 @@ export function ConfigEditor() {
208215
// Currently only validates system prompt length; extend with additional checks as needed.
209216
const hasValidationErrors = useMemo(() => {
210217
if (!draftConfig) return false;
218+
// Role menu validation: all options must have non-empty label and roleId
219+
const roleMenuOptions = draftConfig.welcome?.roleMenu?.options ?? [];
220+
const hasRoleMenuErrors = roleMenuOptions.some(
221+
(opt) => !opt.label?.trim() || !opt.roleId?.trim(),
222+
);
223+
if (hasRoleMenuErrors) return true;
211224
const promptLength = draftConfig.ai?.systemPrompt?.length ?? 0;
212225
return promptLength > SYSTEM_PROMPT_MAX_LENGTH;
213226
}, [draftConfig]);
@@ -773,7 +786,7 @@ export function ConfigEditor() {
773786
</div>
774787
<div className="space-y-3">
775788
{(draftConfig.welcome?.roleMenu?.options ?? []).map((opt, i) => (
776-
<div key={i} className="flex flex-col gap-2 rounded-md border p-2">
789+
<div key={opt.id} className="flex flex-col gap-2 rounded-md border p-2">
777790
<div className="flex items-center gap-2">
778791
<input
779792
type="text"
@@ -791,10 +804,15 @@ export function ConfigEditor() {
791804
variant="ghost"
792805
size="sm"
793806
onClick={() => {
794-
const opts = [...(draftConfig.welcome?.roleMenu?.options ?? [])].filter((_, idx) => idx !== i);
807+
const opts = [...(draftConfig.welcome?.roleMenu?.options ?? [])].filter(
808+
(o) => o.id !== opt.id,
809+
);
795810
updateWelcomeRoleMenu('options', opts);
796811
}}
797-
disabled={saving || (draftConfig.welcome?.roleMenu?.options ?? []).length <= 1}
812+
disabled={
813+
saving || (draftConfig.welcome?.roleMenu?.options ?? []).length <= 1
814+
}
815+
aria-label={`Remove role option ${opt.label || i + 1}`}
798816
>
799817
800818
</Button>
@@ -829,7 +847,10 @@ export function ConfigEditor() {
829847
variant="outline"
830848
size="sm"
831849
onClick={() => {
832-
const opts = [...(draftConfig.welcome?.roleMenu?.options ?? []), { label: '', roleId: '' }];
850+
const opts = [
851+
...(draftConfig.welcome?.roleMenu?.options ?? []),
852+
{ id: crypto.randomUUID(), label: '', roleId: '' },
853+
];
833854
updateWelcomeRoleMenu('options', opts);
834855
}}
835856
disabled={saving || (draftConfig.welcome?.roleMenu?.options ?? []).length >= 25}
@@ -1370,7 +1391,9 @@ export function ConfigEditor() {
13701391
<span className="text-sm font-medium">Admin Role ID</span>
13711392
<RoleSelector
13721393
guildId={guildId}
1373-
selected={draftConfig.permissions?.adminRoleId ? [draftConfig.permissions.adminRoleId] : []}
1394+
selected={
1395+
draftConfig.permissions?.adminRoleId ? [draftConfig.permissions.adminRoleId] : []
1396+
}
13741397
onChange={(selected) => updatePermissionsField('adminRoleId', selected[0] ?? null)}
13751398
placeholder="Select admin role"
13761399
disabled={saving}
@@ -1381,8 +1404,14 @@ export function ConfigEditor() {
13811404
<span className="text-sm font-medium">Moderator Role ID</span>
13821405
<RoleSelector
13831406
guildId={guildId}
1384-
selected={draftConfig.permissions?.moderatorRoleId ? [draftConfig.permissions.moderatorRoleId] : []}
1385-
onChange={(selected) => updatePermissionsField('moderatorRoleId', selected[0] ?? null)}
1407+
selected={
1408+
draftConfig.permissions?.moderatorRoleId
1409+
? [draftConfig.permissions.moderatorRoleId]
1410+
: []
1411+
}
1412+
onChange={(selected) =>
1413+
updatePermissionsField('moderatorRoleId', selected[0] ?? null)
1414+
}
13861415
placeholder="Select moderator role"
13871416
disabled={saving}
13881417
maxSelections={1}

web/src/stores/discord-entities.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Zustand store for caching Discord entities (channels and roles) per guild.
3+
* Reduces redundant API calls when reopening selectors and shares data across instances.
4+
*/
5+
import { create } from 'zustand';
6+
7+
export interface DiscordChannel {
8+
id: string;
9+
name: string;
10+
type: number;
11+
}
12+
13+
export interface DiscordRole {
14+
id: string;
15+
name: string;
16+
color: number;
17+
}
18+
19+
interface DiscordEntitiesState {
20+
// Channels cache: guildId -> channels
21+
channelsByGuild: Record<string, DiscordChannel[]>;
22+
// Roles cache: guildId -> roles
23+
rolesByGuild: Record<string, DiscordRole[]>;
24+
25+
// Get cached channels for a guild
26+
getChannels: (guildId: string) => DiscordChannel[] | undefined;
27+
// Set channels for a guild
28+
setChannels: (guildId: string, channels: DiscordChannel[]) => void;
29+
// Clear channels for a guild (optional invalidation)
30+
clearChannels: (guildId: string) => void;
31+
32+
// Get cached roles for a guild
33+
getRoles: (guildId: string) => DiscordRole[] | undefined;
34+
// Set roles for a guild
35+
setRoles: (guildId: string, roles: DiscordRole[]) => void;
36+
// Clear roles for a guild (optional invalidation)
37+
clearRoles: (guildId: string) => void;
38+
}
39+
40+
export const useDiscordEntitiesStore = create<DiscordEntitiesState>((set, get) => ({
41+
channelsByGuild: {},
42+
rolesByGuild: {},
43+
44+
getChannels: (guildId: string) => get().channelsByGuild[guildId],
45+
46+
setChannels: (guildId: string, channels: DiscordChannel[]) =>
47+
set((state) => ({
48+
channelsByGuild: { ...state.channelsByGuild, [guildId]: channels },
49+
})),
50+
51+
clearChannels: (guildId: string) =>
52+
set((state) => {
53+
const { [guildId]: _, ...rest } = state.channelsByGuild;
54+
return { channelsByGuild: rest };
55+
}),
56+
57+
getRoles: (guildId: string) => get().rolesByGuild[guildId],
58+
59+
setRoles: (guildId: string, roles: DiscordRole[]) =>
60+
set((state) => ({
61+
rolesByGuild: { ...state.rolesByGuild, [guildId]: roles },
62+
})),
63+
64+
clearRoles: (guildId: string) =>
65+
set((state) => {
66+
const { [guildId]: _, ...rest } = state.rolesByGuild;
67+
return { rolesByGuild: rest };
68+
}),
69+
}));

web/src/types/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface WelcomeDynamic {
2727

2828
/** Self-assignable role menu option. */
2929
export interface WelcomeRoleOption {
30+
id: string;
3031
label: string;
3132
roleId: string;
3233
description?: string;

0 commit comments

Comments
 (0)