added features and fixes
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../lib/api.ts';
|
||||
import { useServerStore } from '../stores/server.ts';
|
||||
import { usePermissions } from '../lib/usePermissions.ts';
|
||||
import { RoleManager } from './RoleManager.tsx';
|
||||
|
||||
interface ServerGroup {
|
||||
id: string;
|
||||
server_id: string;
|
||||
name: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
interface ServerSettingsModalProps {
|
||||
serverId: string;
|
||||
@@ -47,13 +56,15 @@ const ACTION_LABELS: Record<string, string> = {
|
||||
const DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
|
||||
export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalProps) {
|
||||
const [tab, setTab] = useState<'audit' | 'availability'>('audit');
|
||||
const [tab, setTab] = useState<'audit' | 'availability' | 'roles' | 'groups'>('audit');
|
||||
const [entries, setEntries] = useState<AuditEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const servers = useServerStore((s) => s.servers);
|
||||
const server = servers.find((s) => s.id === serverId);
|
||||
|
||||
const { isOwner, canManageRoles, canManageChannels, canManageServer } = usePermissions(serverId);
|
||||
|
||||
// Availability state
|
||||
const [slots, setSlots] = useState<AvailabilitySlot[]>([]);
|
||||
const [availLoading, setAvailLoading] = useState(false);
|
||||
@@ -62,11 +73,23 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
const [editStart, setEditStart] = useState('09:00');
|
||||
const [editEnd, setEditEnd] = useState('17:00');
|
||||
|
||||
// Groups state
|
||||
const [groups, setGroups] = useState<ServerGroup[]>([]);
|
||||
const [groupName, setGroupName] = useState('');
|
||||
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
|
||||
const [editingGroupName, setEditingGroupName] = useState('');
|
||||
const [groupsLoading, setGroupsLoading] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
window.dispatchEvent(new CustomEvent('refresh-server-groups'));
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
@@ -74,7 +97,7 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === 'audit') {
|
||||
if (tab === 'audit' && (isOwner || canManageServer)) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
api.get<AuditEntry[]>(`/servers/${serverId}/audit-log`)
|
||||
@@ -87,8 +110,15 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
.then((data) => setSlots(Array.isArray(data) ? data : []))
|
||||
.catch(() => {})
|
||||
.finally(() => setAvailLoading(false));
|
||||
} else if (tab === 'groups' && (isOwner || canManageChannels)) {
|
||||
setGroupsLoading(true);
|
||||
setError(null);
|
||||
api.get<ServerGroup[]>(`/servers/${serverId}/groups`)
|
||||
.then((data) => setGroups(Array.isArray(data) ? data : []))
|
||||
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load groups'))
|
||||
.finally(() => setGroupsLoading(false));
|
||||
}
|
||||
}, [tab, serverId]);
|
||||
}, [tab, serverId, isOwner, canManageServer, canManageChannels]);
|
||||
|
||||
const formatTime = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
@@ -116,39 +146,116 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
|
||||
const slotsForDay = (day: string) => slots.filter((s) => s.day_of_week === day);
|
||||
|
||||
const handleCreateGroup = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!groupName.trim()) return;
|
||||
setError(null);
|
||||
try {
|
||||
await api.post(`/servers/${serverId}/groups`, { name: groupName.trim() });
|
||||
setGroupName('');
|
||||
const data = await api.get<ServerGroup[]>(`/servers/${serverId}/groups`);
|
||||
setGroups(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create group');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameGroup = async (groupId: string, name: string) => {
|
||||
if (!name.trim()) return;
|
||||
setError(null);
|
||||
try {
|
||||
await api.patch(`/servers/${serverId}/groups/${groupId}`, { name: name.trim() });
|
||||
setEditingGroupId(null);
|
||||
const data = await api.get<ServerGroup[]>(`/servers/${serverId}/groups`);
|
||||
setGroups(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to rename group');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteGroup = async (groupId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this group? Channels inside will be moved to ungrouped.')) return;
|
||||
setError(null);
|
||||
try {
|
||||
await api.delete(`/servers/${serverId}/groups/${groupId}`);
|
||||
const data = await api.get<ServerGroup[]>(`/servers/${serverId}/groups`);
|
||||
setGroups(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete group');
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveGroup = async (groupId: string, direction: 'up' | 'down') => {
|
||||
const idx = groups.findIndex((g) => g.id === groupId);
|
||||
if (idx === -1) return;
|
||||
const targetIdx = direction === 'up' ? idx - 1 : idx + 1;
|
||||
if (targetIdx < 0 || targetIdx >= groups.length) return;
|
||||
|
||||
setError(null);
|
||||
const g1 = groups[idx];
|
||||
const g2 = groups[targetIdx];
|
||||
try {
|
||||
await api.patch(`/servers/${serverId}/groups/${g1.id}`, { position: g2.position });
|
||||
await api.patch(`/servers/${serverId}/groups/${g2.id}`, { position: g1.position });
|
||||
const data = await api.get<ServerGroup[]>(`/servers/${serverId}/groups`);
|
||||
setGroups(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to reorder group');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
onClick={onClose}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<div
|
||||
className="bg-gb-bg border border-gb-bg-t w-[700px] max-h-[80vh] flex flex-col font-mono"
|
||||
className="bg-gb-bg border border-gb-bg-t w-[750px] max-h-[85vh] flex flex-col font-mono"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gb-bg-t">
|
||||
<span className="text-sm text-gb-orange">SERVER SETTINGS: {server?.name ?? serverId}</span>
|
||||
<button onClick={onClose} className="text-xs text-gb-red">[x]</button>
|
||||
<button onClick={handleClose} className="text-xs text-gb-red">[x]</button>
|
||||
</div>
|
||||
<div className="flex border-b border-gb-bg-t">
|
||||
<button
|
||||
onClick={() => setTab('audit')}
|
||||
className={`px-4 py-2 text-xs ${tab === 'audit' ? 'text-gb-orange bg-gb-bg-s' : 'text-gb-fg-f hover:text-gb-orange'}`}
|
||||
>
|
||||
[AUDIT LOG]
|
||||
</button>
|
||||
{(isOwner || canManageServer) && (
|
||||
<button
|
||||
onClick={() => setTab('audit')}
|
||||
className={`px-4 py-2 text-xs ${tab === 'audit' ? 'text-gb-orange bg-gb-bg-s' : 'text-gb-fg-f hover:text-gb-orange'}`}
|
||||
>
|
||||
[AUDIT LOG]
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setTab('availability')}
|
||||
className={`px-4 py-2 text-xs ${tab === 'availability' ? 'text-gb-orange bg-gb-bg-s' : 'text-gb-fg-f hover:text-gb-orange'}`}
|
||||
>
|
||||
[AVAILABILITY]
|
||||
</button>
|
||||
{(isOwner || canManageRoles) && (
|
||||
<button
|
||||
onClick={() => setTab('roles')}
|
||||
className={`px-4 py-2 text-xs ${tab === 'roles' ? 'text-gb-orange bg-gb-bg-s' : 'text-gb-fg-f hover:text-gb-orange'}`}
|
||||
>
|
||||
[ROLES]
|
||||
</button>
|
||||
)}
|
||||
{(isOwner || canManageChannels) && (
|
||||
<button
|
||||
onClick={() => setTab('groups')}
|
||||
className={`px-4 py-2 text-xs ${tab === 'groups' ? 'text-gb-orange bg-gb-bg-s' : 'text-gb-fg-f hover:text-gb-orange'}`}
|
||||
>
|
||||
[GROUPS]
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{error && <p className="text-gb-red text-xs mb-3">ERR: {error}</p>}
|
||||
|
||||
{tab === 'audit' && (
|
||||
<>
|
||||
{loading && <p className="text-gb-fg-f text-xs">[loading...]</p>}
|
||||
{error && <p className="text-gb-red text-xs">ERR: {error}</p>}
|
||||
{!loading && !error && entries.length === 0 && (
|
||||
{!loading && entries.length === 0 && (
|
||||
<p className="text-gb-fg-f text-xs">[no audit log entries]</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
@@ -173,6 +280,7 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'availability' && (
|
||||
<div className="space-y-4">
|
||||
{availLoading && <p className="text-gb-fg-f text-xs">[loading...]</p>}
|
||||
@@ -232,6 +340,110 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'roles' && (
|
||||
<div className="h-[60vh] overflow-y-auto">
|
||||
<RoleManager serverId={serverId} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'groups' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-gb-fg-s">manage channel groups for layout organization</p>
|
||||
|
||||
{/* Create group form */}
|
||||
<form onSubmit={handleCreateGroup} className="flex gap-2 items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="group name..."
|
||||
value={groupName}
|
||||
onChange={(e) => setGroupName(e.target.value)}
|
||||
className="terminal-input text-xs px-2 py-1 flex-1"
|
||||
/>
|
||||
<button type="submit" className="px-3 py-1 bg-gb-orange text-gb-bg text-xs">[CREATE GROUP]</button>
|
||||
</form>
|
||||
|
||||
{groupsLoading && <p className="text-gb-fg-f text-xs">[loading...]</p>}
|
||||
{!groupsLoading && groups.length === 0 && (
|
||||
<p className="text-gb-fg-f text-xs">[no groups created yet]</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 mt-2">
|
||||
{groups.map((g, index) => {
|
||||
const isEditing = editingGroupId === g.id;
|
||||
return (
|
||||
<div key={g.id} className="flex items-center justify-between border border-gb-bg-t p-2 bg-gb-bg-s text-xs">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<input
|
||||
autoFocus
|
||||
value={editingGroupName}
|
||||
onChange={(e) => setEditingGroupName(e.target.value)}
|
||||
className="bg-gb-bg-t text-gb-fg px-1 py-0.5 text-xs outline-none border border-gb-orange flex-1"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRenameGroup(g.id, editingGroupName)}
|
||||
className="text-gb-green hover:underline font-bold"
|
||||
>
|
||||
[SAVE]
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingGroupId(null)}
|
||||
className="text-gb-fg-f hover:underline"
|
||||
>
|
||||
[CANCEL]
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-gb-fg-s font-bold uppercase">{g.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={index === 0}
|
||||
onClick={() => handleMoveGroup(g.id, 'up')}
|
||||
className="text-gb-fg-f hover:text-gb-orange disabled:opacity-30"
|
||||
title="Move Up"
|
||||
>
|
||||
▲
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={index === groups.length - 1}
|
||||
onClick={() => handleMoveGroup(g.id, 'down')}
|
||||
className="text-gb-fg-f hover:text-gb-orange disabled:opacity-30"
|
||||
title="Move Down"
|
||||
>
|
||||
▼
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingGroupId(g.id);
|
||||
setEditingGroupName(g.name);
|
||||
}}
|
||||
className="text-gb-aqua hover:underline"
|
||||
>
|
||||
[RENAME]
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteGroup(g.id)}
|
||||
className="text-gb-red hover:underline"
|
||||
>
|
||||
[DELETE]
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user