bug fixes

This commit is contained in:
2025-10-10 18:51:23 -04:00
parent 8236c1e0e7
commit 61ab1e1d9e
15 changed files with 4463 additions and 7 deletions

View File

@@ -0,0 +1,196 @@
import React, { useState, useEffect } from 'react';
import { Box, Button, TextField, Select, MenuItem, FormControl, InputLabel, Accordion, AccordionSummary, AccordionDetails, Typography, IconButton, List, ListItem, ListItemText, Chip } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import DeleteIcon from '@mui/icons-material/Delete';
import * as api from '../../lib/api';
import { useBackend } from '../../contexts/BackendContext';
import ConfirmDialog from '../common/ConfirmDialog';
export default function ReactionRoles({ guildId, channels, roles = [] }) {
const { eventTarget } = useBackend() || {};
const [list, setList] = useState([]);
const [name, setName] = useState('');
const [channelId, setChannelId] = useState('');
const [embed, setEmbed] = useState('');
const [embedTitle, setEmbedTitle] = useState('');
const [embedColor, setEmbedColor] = useState('#2f3136');
const [embedThumbnail, setEmbedThumbnail] = useState('');
const [embedFields, setEmbedFields] = useState([]);
const [buttons, setButtons] = useState([]);
const [newBtnLabel, setNewBtnLabel] = useState('');
const [newBtnRole, setNewBtnRole] = useState('');
const [confirmOpen, setConfirmOpen] = useState(false);
const [pendingDeleteId, setPendingDeleteId] = useState(null);
const [editingId, setEditingId] = useState(null);
useEffect(() => {
let mounted = true;
async function load() {
const rows = await api.listReactionRoles(guildId) || [];
if (!mounted) return;
setList(rows);
}
load();
const onRRUpdate = (e) => {
const d = e.detail || {};
if (d.guildId && d.guildId !== guildId) return;
// reload
api.listReactionRoles(guildId).then(rows => setList(rows || []));
};
eventTarget && eventTarget.addEventListener('reactionRolesUpdate', onRRUpdate);
return () => { mounted = false; eventTarget && eventTarget.removeEventListener('reactionRolesUpdate', onRRUpdate); };
}, [guildId, eventTarget]);
const addButton = () => {
if (!newBtnLabel || !newBtnRole) return;
setButtons(prev => [...prev, { label: newBtnLabel, roleId: newBtnRole }]);
setNewBtnLabel(''); setNewBtnRole('');
};
const addEmbedField = () => {
setEmbedFields(prev => [...prev, { name: '', value: '' }]);
};
const updateEmbedField = (idx, k, v) => {
setEmbedFields(prev => prev.map((f,i) => i===idx ? { ...f, [k]: v } : f));
};
const removeEmbedField = (idx) => {
setEmbedFields(prev => prev.filter((_,i)=>i!==idx));
};
const createRR = async () => {
if (editingId) return updateRR(); // if editing, update instead
if (!channelId || !name || (!embed && !embedTitle) || buttons.length === 0) return alert('channel, name, embed (title or description), and at least one button required');
const emb = { title: embedTitle, description: embed, color: embedColor, thumbnail: embedThumbnail, fields: embedFields };
const res = await api.createReactionRole(guildId, { channelId, name, embed: emb, buttons });
if (res && res.reactionRole) {
setList(prev => [res.reactionRole, ...prev]);
setName(''); setEmbed(''); setEmbedTitle(''); setEmbedColor('#2f3136'); setEmbedThumbnail(''); setEmbedFields([]); setButtons([]); setChannelId('');
} else {
alert('Failed to create reaction role');
}
};
const confirmDelete = (id) => {
setPendingDeleteId(id);
setConfirmOpen(true);
};
const deleteRR = async (id) => {
const ok = await api.deleteReactionRole(guildId, id);
if (ok) setList(prev => prev.filter(r => r.id !== id));
setConfirmOpen(false);
setPendingDeleteId(null);
};
const startEdit = (rr) => {
setEditingId(rr.id);
setName(rr.name);
setChannelId(rr.channel_id);
setEmbed(rr.embed?.description || '');
setEmbedTitle(rr.embed?.title || '');
setEmbedColor(rr.embed?.color || '#2f3136');
setEmbedThumbnail(rr.embed?.thumbnail || '');
setEmbedFields(rr.embed?.fields || []);
setButtons(rr.buttons || []);
};
const cancelEdit = () => {
setEditingId(null);
setName(''); setChannelId(''); setEmbed(''); setEmbedTitle(''); setEmbedColor('#2f3136'); setEmbedThumbnail(''); setEmbedFields([]); setButtons([]);
};
const updateRR = async () => {
if (!channelId || !name || (!embed && !embedTitle) || buttons.length === 0) return alert('channel, name, embed (title or description), and at least one button required');
const emb = { title: embedTitle, description: embed, color: embedColor, thumbnail: embedThumbnail, fields: embedFields };
const res = await api.updateReactionRole(guildId, editingId, { channelId, name, embed: emb, buttons });
if (res && res.reactionRole) {
setList(prev => prev.map(r => r.id === editingId ? res.reactionRole : r));
cancelEdit();
} else {
alert('Failed to update reaction role');
}
};
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon/>}>
<Typography>Reaction Roles</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ mb: 2 }}>
<FormControl fullWidth sx={{ mb: 1 }}>
<InputLabel id="rr-channel-label">Channel</InputLabel>
<Select labelId="rr-channel-label" value={channelId} label="Channel" onChange={e => setChannelId(e.target.value)}>
<MenuItem value="">Select channel</MenuItem>
{channels.map(c => <MenuItem key={c.id} value={c.id}>{c.name}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Name" fullWidth value={name} onChange={e=>setName(e.target.value)} sx={{ mb:1 }} />
<TextField label="Embed (description)" fullWidth multiline rows={3} value={embed} onChange={e=>setEmbed(e.target.value)} sx={{ mb:1 }} />
<Box sx={{ display: 'flex', gap: 1, mb:1 }}>
<TextField label="Embed Title" value={embedTitle} onChange={e=>setEmbedTitle(e.target.value)} sx={{ flex: 1 }} />
<TextField label="Color" value={embedColor} onChange={e=>setEmbedColor(e.target.value)} sx={{ width: 120 }} />
</Box>
<TextField label="Thumbnail URL" fullWidth value={embedThumbnail} onChange={e=>setEmbedThumbnail(e.target.value)} sx={{ mb:1 }} />
<Box sx={{ mb:1 }}>
<Typography variant="subtitle2">Fields</Typography>
{embedFields.map((f,i)=> (
<Box key={i} sx={{ display: 'flex', gap: 1, mb: 1 }}>
<TextField placeholder="Name" value={f.name} onChange={e=>updateEmbedField(i, 'name', e.target.value)} sx={{ flex: 1 }} />
<TextField placeholder="Value" value={f.value} onChange={e=>updateEmbedField(i, 'value', e.target.value)} sx={{ flex: 2 }} />
<IconButton onClick={()=>removeEmbedField(i)}><DeleteIcon/></IconButton>
</Box>
))}
<Button onClick={addEmbedField} size="small">Add Field</Button>
</Box>
<Box sx={{ display: 'flex', gap: 1, mb:1 }}>
<TextField label="Button label" value={newBtnLabel} onChange={e=>setNewBtnLabel(e.target.value)} />
<FormControl sx={{ minWidth: 220 }}>
<InputLabel id="rr-role-label">Role</InputLabel>
<Select labelId="rr-role-label" value={newBtnRole} label="Role" onChange={e=>setNewBtnRole(e.target.value)}>
<MenuItem value="">Select role</MenuItem>
{roles.map(role => (
<MenuItem key={role.id} value={role.id}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip size="small" label={role.name} sx={{ bgcolor: role.color || undefined, color: role.color ? '#fff' : undefined }} />
<Typography variant="caption" sx={{ color: 'text.secondary' }}>{role.permissions || ''}</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
<Button variant="outlined" onClick={addButton}>Add Button</Button>
</Box>
<List>
{buttons.map((b,i)=>(
<ListItem key={i} secondaryAction={<IconButton edge="end" onClick={()=>setButtons(bs=>bs.filter((_,idx)=>idx!==i))}><DeleteIcon/></IconButton>}>
<ListItemText primary={b.label} secondary={roles.find(r=>r.id===b.roleId)?.name || b.roleId} />
</ListItem>
))}
</List>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button variant="contained" onClick={createRR}>{editingId ? 'Update Reaction Role' : 'Create Reaction Role'}</Button>
{editingId && <Button variant="outlined" onClick={cancelEdit}>Cancel</Button>}
</Box>
</Box>
<Typography variant="h6">Existing</Typography>
{list.map(r => (
<Box key={r.id} sx={{ border: '1px solid #ddd', p:1, mb:1 }}>
<Typography>{r.name}</Typography>
<Typography variant="body2">Channel: {r.channel_id || r.channelId}</Typography>
<Typography variant="body2">Message: {r.message_id || r.messageId || 'Not posted'}</Typography>
<Button variant="outlined" onClick={async ()=>{ const res = await api.postReactionRoleMessage(guildId, r); if (!res || !res.success) alert('Failed to post message'); }}>Post Message</Button>
<Button variant="text" color="error" onClick={()=>confirmDelete(r.id)}>Delete</Button>
<Button variant="text" onClick={() => startEdit(r)}>Edit</Button>
</Box>
))}
<ConfirmDialog open={confirmOpen} title="Delete Reaction Role" description="Delete this reaction role configuration? This will remove it from the database." onClose={() => { setConfirmOpen(false); setPendingDeleteId(null); }} onConfirm={() => deleteRR(pendingDeleteId)} />
</AccordionDetails>
</Accordion>
);
}

View File

@@ -10,6 +10,7 @@ import ConfirmDialog from '../common/ConfirmDialog';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DeleteIcon from '@mui/icons-material/Delete';
import { UserContext } from '../../contexts/UserContext';
import ReactionRoles from './ReactionRoles';
// Use a relative API base by default so the frontend talks to the same origin that served it.
// In development you can set REACT_APP_API_BASE to a full URL if needed.
@@ -674,6 +675,10 @@ const ServerSettings = () => {
</Box>
</AccordionDetails>
</Accordion>
{/* Reaction Roles Accordion */}
<Box sx={{ marginTop: '20px' }}>
<ReactionRoles guildId={guildId} channels={channels} roles={roles} />
</Box>
{/* Live Notifications dialog */}
{/* header live dialog removed; Live Notifications is managed in its own accordion below */}
{/* Invite creation and list */}

View File

@@ -20,4 +20,30 @@ export async function del(path, config) {
return client.delete(path, config);
}
export async function listReactionRoles(guildId) {
const res = await client.get(`/api/servers/${guildId}/reaction-roles`);
return res.data;
}
export async function createReactionRole(guildId, body) {
const res = await client.post(`/api/servers/${guildId}/reaction-roles`, body);
return res.data;
}
export async function deleteReactionRole(guildId, id) {
const res = await client.delete(`/api/servers/${guildId}/reaction-roles/${id}`);
return res.data && res.data.success;
}
export async function postReactionRoleMessage(guildId, rr) {
// instruct backend to have bot post message by asking bot module via internal call
const res = await client.post(`/internal/publish-reaction-role`, { guildId, id: rr.id });
return res.data;
}
export async function updateReactionRole(guildId, id, body) {
const res = await client.put(`/api/servers/${guildId}/reaction-roles/${id}`, body);
return res.data;
}
export default client;