Добавить UI настроек и inbox автомодерации.
CI / test (push) Failing after 3m46s
CI / push-image (push) Has been skipped
CI / deploy-ift (push) Has been skipped
CI / ci-done (push) Failing after 0s
CI / e2e-ift (push) Has been skipped
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped

Refs EventHub/EventHubFrontAdmin#40
Refs EventHub/EventHubFrontAdmin#41
This commit is contained in:
2026-07-17 18:09:38 +03:00
parent b39a676e1a
commit 63323c1ca4
10 changed files with 523 additions and 2 deletions
+150
View File
@@ -0,0 +1,150 @@
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ListPageHeader } from '@/components/ListPageHeader';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useAutomodHits, useResolveAutomodHit } from '@/hooks/useAutomod';
import type { AutomodHit, AutomodHitStatus } from '@/api/automodApi';
import { formatDateTime } from '@/utils/datetime';
import { notify } from '@/lib/notify';
function entityLink(hit: AutomodHit): string {
if (hit.entity_type === 'event') return `/events/${hit.entity_id}`;
if (hit.entity_type === 'calendar') return `/calendars/${hit.entity_id}`;
return `/reviews/${hit.entity_id}`;
}
const AutomodInboxPage: React.FC = () => {
const { t } = useTranslation();
const [status, setStatus] = useState<AutomodHitStatus | 'all'>('open');
const params = useMemo(
() => (status === 'all' ? undefined : { status }),
[status]
);
const { data, isLoading } = useAutomodHits(params);
const resolve = useResolveAutomodHit();
const rows = data ?? [];
const onResolve = (id: string, next: 'approved' | 'rejected') => {
resolve.mutate(
{ id, status: next },
{
onSuccess: () => notify.success(t('automodInbox.resolved')),
onError: () => notify.error(t('automodInbox.resolveFailed')),
}
);
};
return (
<div className="space-y-6" data-testid="page-automod-inbox">
<ListPageHeader
title={t('automodInbox.title')}
description={t('automodInbox.description')}
/>
<div className="flex items-center gap-3">
<Select value={status} onValueChange={(v) => setStatus(v as AutomodHitStatus | 'all')}>
<SelectTrigger className="w-48" data-testid="filter-automod-status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">{t('automodInbox.filterOpen')}</SelectItem>
<SelectItem value="all">{t('automodInbox.filterAll')}</SelectItem>
<SelectItem value="approved">{t('automodInbox.filterApproved')}</SelectItem>
<SelectItem value="rejected">{t('automodInbox.filterRejected')}</SelectItem>
</SelectContent>
</Select>
</div>
{isLoading ? (
<Skeleton className="h-40 w-full" />
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('automodInbox.createdAt')}</TableHead>
<TableHead>{t('automodInbox.trigger')}</TableHead>
<TableHead>{t('automodInbox.entity')}</TableHead>
<TableHead>{t('automodInbox.words')}</TableHead>
<TableHead>{t('automodInbox.action')}</TableHead>
<TableHead>{t('automodInbox.status')}</TableHead>
<TableHead>{t('automodInbox.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-muted-foreground">
{t('common.noData')}
</TableCell>
</TableRow>
) : (
rows.map((hit) => (
<TableRow key={hit.id} data-testid={`row-automod-hit-${hit.id}`}>
<TableCell>{formatDateTime(hit.created_at)}</TableCell>
<TableCell>
<Badge variant="secondary">{hit.trigger}</Badge>
</TableCell>
<TableCell>
<Link className="underline" to={entityLink(hit)}>
{hit.entity_type}:{hit.entity_id.slice(0, 8)}
</Link>
</TableCell>
<TableCell>{hit.matched_words.join(', ') || '—'}</TableCell>
<TableCell>{hit.action_taken}</TableCell>
<TableCell>
<Badge>{hit.status}</Badge>
</TableCell>
<TableCell>
{hit.status === 'open' ? (
<div className="flex gap-2">
<Button
size="sm"
data-testid={`btn-approve-hit-${hit.id}`}
onClick={() => onResolve(hit.id, 'approved')}
disabled={resolve.isPending}
>
{t('automodInbox.approve')}
</Button>
<Button
size="sm"
variant="outline"
data-testid={`btn-reject-hit-${hit.id}`}
onClick={() => onResolve(hit.id, 'rejected')}
disabled={resolve.isPending}
>
{t('automodInbox.reject')}
</Button>
</div>
) : (
'—'
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)}
</div>
);
};
export default AutomodInboxPage;