Skip to content

Commit 97ea887

Browse files
author
Bill
committed
fix: Complete remaining PR #244 review comments
UX Regressions (CRITICAL): - Restore ChannelSelector in StarboardSection for starboard channel Mobile Responsive: - ChallengesSection: Use responsive col-span (col-span-1 md:col-span-2) - ModerationSection: Use grid-cols-1 md:grid-cols-2 for rate limit grid - ModerationSection: Use grid-cols-1 md:grid-cols-3 for mute settings grid - ReputationSection: Use grid-cols-1 md:grid-cols-2 for XP settings Input Normalization: - StarboardSection: Add raw buffer + blur pattern for ignored channels - TriageSection: Trim whitespace for moderation log channel (already done) Code Quality: - EngagementSection: Refactor activityBadges updater with updateBadge helper - EngagementSection: Add accessible names to all form controls - EngagementSection: Fix badge row key collision (use days-label composite key) - AiAutoModSection: Handle partial config sibling categories with defaults - config-updates.ts: Guard array cloning against non-array values - config-updates.ts: Validate array index bounds in update/remove/append All tests pass (3628 passed, 2 skipped).
1 parent 0c72554 commit 97ea887

8 files changed

Lines changed: 92 additions & 42 deletions

File tree

web/src/components/dashboard/config-sections/AiAutoModSection.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,19 @@ export function AiAutoModSection({ draftConfig, saving, onFieldChange }: AiAutoM
3131
flagChannelId: null,
3232
autoDelete: true,
3333
} as const);
34-
const thresholds = (draftConfig.aiAutoMod?.thresholds as Record<string, number>) ?? {};
35-
const actions = (draftConfig.aiAutoMod?.actions as Record<string, string>) ?? {};
34+
// Ensure all categories are present even with partial config
35+
const thresholds: Record<string, number> = {
36+
toxicity: 0.7,
37+
spam: 0.7,
38+
harassment: 0.7,
39+
...((draftConfig.aiAutoMod?.thresholds as Record<string, number>) ?? {}),
40+
};
41+
const actions: Record<string, string> = {
42+
toxicity: 'flag',
43+
spam: 'flag',
44+
harassment: 'flag',
45+
...((draftConfig.aiAutoMod?.actions as Record<string, string>) ?? {}),
46+
};
3647

3748
return (
3849
<Card>

web/src/components/dashboard/config-sections/ChallengesSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function ChallengesSection({
6666
placeholder="09:00"
6767
/>
6868
</label>
69-
<label className="space-y-2 col-span-2">
69+
<label className="space-y-2 col-span-1 md:col-span-2">
7070
<span className="text-sm font-medium">Timezone</span>
7171
<input
7272
type="text"

web/src/components/dashboard/config-sections/EngagementSection.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export function EngagementSection({
3030
}: EngagementSectionProps) {
3131
const badges = draftConfig.engagement?.activityBadges ?? [...DEFAULT_ACTIVITY_BADGES];
3232

33+
const updateBadge = (index: number, updates: Partial<{ days: number; label: string }>) => {
34+
const newBadges = [...badges];
35+
newBadges[index] = { ...newBadges[index], ...updates };
36+
onActivityBadgesChange(newBadges);
37+
};
38+
3339
return (
3440
<Card>
3541
<CardContent className="space-y-4 pt-6">
@@ -39,32 +45,27 @@ export function EngagementSection({
3945
active days.
4046
</p>
4147
{badges.map((badge, i) => (
42-
<div key={`badge-${i}`} className="flex items-center gap-2">
48+
<div key={`${badge.days}-${badge.label}`} className="flex items-center gap-2">
4349
<Input
4450
className="w-20"
4551
type="number"
4652
min={0}
4753
value={badge.days ?? 0}
4854
onChange={(e) => {
49-
const newBadges = [...badges];
50-
newBadges[i] = {
51-
...newBadges[i],
52-
days: Math.max(0, parseInt(e.target.value, 10) || 0),
53-
};
54-
onActivityBadgesChange(newBadges);
55+
updateBadge(i, { days: Math.max(0, parseInt(e.target.value, 10) || 0) });
5556
}}
5657
disabled={saving}
58+
aria-label={`Badge ${i + 1} minimum days`}
5759
/>
5860
<span className="text-xs text-muted-foreground">days →</span>
5961
<Input
6062
className="flex-1"
6163
value={badge.label ?? ''}
6264
onChange={(e) => {
63-
const newBadges = [...badges];
64-
newBadges[i] = { ...newBadges[i], label: e.target.value };
65-
onActivityBadgesChange(newBadges);
65+
updateBadge(i, { label: e.target.value });
6666
}}
6767
disabled={saving}
68+
aria-label={`Badge ${i + 1} label`}
6869
/>
6970
<Button
7071
variant="ghost"
@@ -74,6 +75,7 @@ export function EngagementSection({
7475
onActivityBadgesChange(newBadges);
7576
}}
7677
disabled={saving || badges.length <= 1}
78+
aria-label={`Remove badge ${i + 1}`}
7779
>
7880
7981
</Button>

web/src/components/dashboard/config-sections/ModerationSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export function ModerationSection({
148148
label="Rate Limiting"
149149
/>
150150
</div>
151-
<div className="grid grid-cols-2 gap-4">
151+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
152152
<label htmlFor="max-messages" className="space-y-2">
153153
<span className="text-sm text-muted-foreground">Max Messages</span>
154154
<input
@@ -180,7 +180,7 @@ export function ModerationSection({
180180
/>
181181
</label>
182182
</div>
183-
<div className="grid grid-cols-3 gap-4">
183+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
184184
<label htmlFor="mute-after-triggers" className="space-y-2">
185185
<span className="text-sm text-muted-foreground">Mute After Triggers</span>
186186
<input

web/src/components/dashboard/config-sections/ReputationSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function ReputationSection({
5252
label="Reputation"
5353
/>
5454
</div>
55-
<div className="grid grid-cols-2 gap-4">
55+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
5656
<label htmlFor="xp-per-message-min" className="space-y-2">
5757
<span className="text-sm font-medium">XP per Message (min)</span>
5858
<input

web/src/components/dashboard/config-sections/StarboardSection.tsx

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,19 @@ const inputClasses =
2424
* Provides controls for pinning popular messages to a starboard channel,
2525
* including threshold, emoji settings, and ignored channels.
2626
*/
27-
export function StarboardSection({ draftConfig, guildId, saving, onFieldChange }: StarboardSectionProps) {
27+
export function StarboardSection({
28+
draftConfig,
29+
guildId,
30+
saving,
31+
onFieldChange,
32+
}: StarboardSectionProps) {
33+
// Local state for ignored channels raw input (parsed on blur)
34+
const ignoredChannelsDisplay = (draftConfig.starboard?.ignoredChannels ?? []).join(', ');
35+
const [ignoredChannelsRaw, setIgnoredChannelsRaw] = useState(ignoredChannelsDisplay);
36+
useEffect(() => {
37+
setIgnoredChannelsRaw(ignoredChannelsDisplay);
38+
}, [ignoredChannelsDisplay]);
39+
2840
return (
2941
<Card>
3042
<CardHeader>
@@ -42,18 +54,21 @@ export function StarboardSection({ draftConfig, guildId, saving, onFieldChange }
4254
</div>
4355
</CardHeader>
4456
<CardContent className="space-y-4">
45-
<label htmlFor="channel-id" className="space-y-2">
46-
<span className="text-sm font-medium">Channel ID</span>
47-
<input
48-
id="channel-id"
49-
type="text"
50-
value={draftConfig.starboard?.channelId ?? ''}
51-
onChange={(e) => onFieldChange('channelId', e.target.value)}
52-
disabled={saving}
53-
className={inputClasses}
54-
placeholder="Starboard channel ID"
55-
/>
56-
</label>
57+
<div className="space-y-2">
58+
<span className="text-sm font-medium">Starboard Channel</span>
59+
{guildId ? (
60+
<ChannelSelector
61+
guildId={guildId}
62+
selected={draftConfig.starboard?.channelId ? [draftConfig.starboard.channelId] : []}
63+
onChange={(selected) => onFieldChange('channelId', selected[0] ?? null)}
64+
placeholder="Select starboard channel..."
65+
disabled={saving}
66+
maxSelections={1}
67+
/>
68+
) : (
69+
<p className="text-muted-foreground text-sm">Select a server first</p>
70+
)}
71+
</div>
5772
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
5873
<label htmlFor="threshold" className="space-y-2">
5974
<span className="text-sm font-medium">Threshold</span>
@@ -115,16 +130,28 @@ export function StarboardSection({ draftConfig, guildId, saving, onFieldChange }
115130
<input
116131
id="ignored-channels"
117132
type="text"
118-
value={(draftConfig.starboard?.ignoredChannels ?? []).join(', ')}
119-
onChange={(e) =>
133+
value={ignoredChannelsRaw}
134+
onChange={(e) => {
135+
const raw = e.target.value;
136+
setIgnoredChannelsRaw(raw);
137+
// Call onFieldChange on every change to prevent Ctrl+S data loss
120138
onFieldChange(
121139
'ignoredChannels',
122-
e.target.value
140+
raw
123141
.split(',')
124142
.map((s) => s.trim())
125143
.filter(Boolean),
126-
)
127-
}
144+
);
145+
}}
146+
onBlur={() => {
147+
// Normalize on blur
148+
const normalized = ignoredChannelsRaw
149+
.split(',')
150+
.map((s) => s.trim())
151+
.filter(Boolean)
152+
.join(', ');
153+
setIgnoredChannelsRaw(normalized);
154+
}}
128155
disabled={saving}
129156
className={inputClasses}
130157
placeholder="Comma-separated channel IDs"

web/src/components/dashboard/config-sections/WelcomeSection.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ export function WelcomeSection({
9797
{guildId ? (
9898
<ChannelSelector
9999
guildId={guildId}
100-
selected={draftConfig.welcome?.rulesChannel ? [draftConfig.welcome.rulesChannel] : []}
100+
selected={
101+
draftConfig.welcome?.rulesChannel ? [draftConfig.welcome.rulesChannel] : []
102+
}
101103
onChange={(selected) => onFieldChange('rulesChannel', selected[0] ?? null)}
102104
placeholder="Select rules channel..."
103105
disabled={saving}
@@ -112,7 +114,9 @@ export function WelcomeSection({
112114
{guildId ? (
113115
<RoleSelector
114116
guildId={guildId}
115-
selected={draftConfig.welcome?.verifiedRole ? [draftConfig.welcome.verifiedRole] : []}
117+
selected={
118+
draftConfig.welcome?.verifiedRole ? [draftConfig.welcome.verifiedRole] : []
119+
}
116120
onChange={(selected) => onFieldChange('verifiedRole', selected[0] ?? null)}
117121
placeholder="Select verified role..."
118122
disabled={saving}
@@ -127,7 +131,9 @@ export function WelcomeSection({
127131
{guildId ? (
128132
<ChannelSelector
129133
guildId={guildId}
130-
selected={draftConfig.welcome?.introChannel ? [draftConfig.welcome.introChannel] : []}
134+
selected={
135+
draftConfig.welcome?.introChannel ? [draftConfig.welcome.introChannel] : []
136+
}
131137
onChange={(selected) => onFieldChange('introChannel', selected[0] ?? null)}
132138
placeholder="Select intro channel..."
133139
disabled={saving}

web/src/lib/config-updates.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,9 @@ export function updateArrayItem<T>(
117117
}
118118

119119
const lastKey = path[path.length - 1];
120-
const existing = (cursor[lastKey] as T[]) || [];
121-
const arr = [...existing];
120+
const existing = cursor[lastKey];
121+
// Guard against non-array values
122+
const arr = Array.isArray(existing) ? [...existing] : [];
122123
if (index < 0 || index >= arr.length) {
123124
return config;
124125
}
@@ -166,8 +167,9 @@ export function removeArrayItem(
166167
}
167168

168169
const lastKey = path[path.length - 1];
169-
const existing = (cursor[lastKey] as unknown[]) || [];
170-
const arr = [...existing];
170+
const existing = cursor[lastKey];
171+
// Guard against non-array values
172+
const arr = Array.isArray(existing) ? [...existing] : [];
171173
if (index < 0 || index >= arr.length) {
172174
return config;
173175
}
@@ -215,7 +217,9 @@ export function appendArrayItem<T>(
215217
}
216218

217219
const lastKey = path[path.length - 1];
218-
const arr = [...((cursor[lastKey] as T[]) || []), item];
220+
const existing = cursor[lastKey];
221+
// Guard against non-array values
222+
const arr = [...(Array.isArray(existing) ? existing : []), item];
219223

220224
// Rebuild from bottom up using tracked levels
221225
let rebuilt: Record<string, unknown> = { ...cursor, [lastKey]: arr };

0 commit comments

Comments
 (0)