Files
glchat/web/src/components/instance/InstanceGuildsPanel.tsx
T

161 lines
5.5 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
deleteInstanceGuild,
instanceGuildsQueryKey,
instanceUsersQueryKey,
updateInstanceGuild,
} from '@/api/instance';
import { ErrorNotice } from '@/components/ui/ErrorNotice';
import { Field } from '@/components/ui/Field';
import { Button } from '@/components/ui/primitives';
import type { InstanceGuild } from '@/api/types';
interface InstanceGuildsPanelProps {
guilds: InstanceGuild[];
isPending: boolean;
error: unknown;
onRetry: () => void;
/** Открыть настройки сервера: админ правит комнаты, роли и ники там. */
onOpenSettings: (guildId: string) => void;
}
/**
* Серверы инстанса (AGENT.md 7.19): администратор переименовывает и удаляет
* любой сервер и открывает его настройки — комнаты, роли и ники правятся
* обычными разделами, права администратора инстанса действуют везде.
*/
export function InstanceGuildsPanel({
guilds,
isPending,
error,
onRetry,
onOpenSettings,
}: InstanceGuildsPanelProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [renaming, setRenaming] = useState<InstanceGuild | null>(null);
const [name, setName] = useState('');
const [actionError, setActionError] = useState<unknown>(null);
const invalidate = (): void => {
void queryClient.invalidateQueries({ queryKey: instanceGuildsQueryKey });
void queryClient.invalidateQueries({ queryKey: instanceUsersQueryKey });
};
const rename = useMutation({
mutationFn: (input: { id: string; name: string }) =>
updateInstanceGuild(input.id, { name: input.name }),
onSuccess: () => {
setActionError(null);
setRenaming(null);
invalidate();
},
onError: (cause: unknown) => setActionError(cause),
});
const remove = useMutation({
mutationFn: (guildId: string) => deleteInstanceGuild(guildId),
onSuccess: () => {
setActionError(null);
invalidate();
},
onError: (cause: unknown) => setActionError(cause),
});
const pending = rename.isPending || remove.isPending;
if (isPending) {
return <p className="mt-2 text-sm text-fg-muted">{t('common.loading')}</p>;
}
if (error !== null && error !== undefined) {
return <ErrorNotice className="mt-3" error={error} onRetry={onRetry} />;
}
if (guilds.length === 0) {
return <p className="mt-2 text-sm text-fg-muted">{t('settings.instance.guildsEmpty')}</p>;
}
return (
<div className="mt-3 flex flex-col gap-2" data-testid="instance-guilds-actions">
{actionError === null ? null : <ErrorNotice error={actionError} />}
<ul className="flex flex-col gap-1 text-sm" data-testid="instance-guilds">
{guilds.map((guild) => (
<li
key={guild.id}
data-testid={`instance-guild-${guild.id}`}
className="flex flex-wrap items-center gap-2 border-b border-border/40 py-2 last:border-b-0"
>
<span className="min-w-0 flex-1 truncate">{guild.name}</span>
<span className="shrink-0 text-xs text-fg-muted">
{guild.is_main ? `${t('settings.instance.mainBadge')} · ` : ''}
{t('settings.instance.memberCount', { count: guild.member_count })}
</span>
<Button
variant="ghost"
data-testid={`instance-guild-open-${guild.id}`}
onClick={() => onOpenSettings(guild.id)}
>
{t('settings.instance.actions.openSettings')}
</Button>
<Button
variant="ghost"
data-testid={`instance-guild-rename-${guild.id}`}
disabled={pending}
onClick={() => {
setRenaming(guild);
setName(guild.name);
}}
>
{t('settings.instance.actions.rename')}
</Button>
<Button
variant="ghost"
className="border border-danger/50 text-danger"
data-testid={`instance-guild-delete-${guild.id}`}
disabled={pending || guild.is_main}
onClick={() => {
if (
window.confirm(
t('settings.instance.actions.deleteGuildConfirm', { name: guild.name }),
)
) {
remove.mutate(guild.id);
}
}}
>
{t('common.delete')}
</Button>
</li>
))}
</ul>
{renaming === null ? null : (
<form
className="flex items-end gap-2"
onSubmit={(event) => {
event.preventDefault();
if (renaming !== null && name.trim() !== '') {
rename.mutate({ id: renaming.id, name: name.trim() });
}
}}
>
<Field
label={t('settings.instance.actions.newName')}
value={name}
maxLength={64}
onChange={(event) => setName(event.target.value)}
/>
<Button type="submit" disabled={rename.isPending}>
{t('common.save')}
</Button>
<Button variant="ghost" onClick={() => setRenaming(null)}>
{t('common.cancel')}
</Button>
</form>
)}
</div>
);
}