63323c1ca4
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
151 lines
7.2 KiB
TypeScript
151 lines
7.2 KiB
TypeScript
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;
|