|
|
@@ -1,6 +1,7 @@
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
-import { Alert, Modal, Select, Typography, message } from 'antd';
|
|
|
+import { Alert, Input, Modal, Select, Space, Table, Tag, Typography, message } from 'antd';
|
|
|
+import type { ColumnsType } from 'antd/es/table';
|
|
|
|
|
|
import { HttpUtil } from '@/utils';
|
|
|
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
|
|
@@ -20,10 +21,24 @@ interface BulkAttachResult {
|
|
|
errors?: string[];
|
|
|
}
|
|
|
|
|
|
-function readClientEmails(settings: unknown): string[] {
|
|
|
- const parsed = coerceInboundJsonField(settings) as { clients?: Array<{ email?: string }> };
|
|
|
+interface ClientRow {
|
|
|
+ email: string;
|
|
|
+ comment: string;
|
|
|
+ enable: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+function readClientRows(settings: unknown): ClientRow[] {
|
|
|
+ const parsed = coerceInboundJsonField(settings) as {
|
|
|
+ clients?: Array<{ email?: string; comment?: string; enable?: boolean }>;
|
|
|
+ };
|
|
|
const clients = Array.isArray(parsed?.clients) ? parsed.clients : [];
|
|
|
- return clients.map((c) => (c?.email || '').trim()).filter(Boolean);
|
|
|
+ return clients
|
|
|
+ .map((c) => ({
|
|
|
+ email: (c?.email || '').trim(),
|
|
|
+ comment: (c?.comment || '').trim(),
|
|
|
+ enable: c?.enable !== false,
|
|
|
+ }))
|
|
|
+ .filter((r) => r.email);
|
|
|
}
|
|
|
|
|
|
export default function AttachClientsModal({
|
|
|
@@ -37,12 +52,18 @@ export default function AttachClientsModal({
|
|
|
const [messageApi, messageContextHolder] = message.useMessage();
|
|
|
const [targetIds, setTargetIds] = useState<number[]>([]);
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
+ const [clientRows, setClientRows] = useState<ClientRow[]>([]);
|
|
|
+ const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
|
|
|
+ const [search, setSearch] = useState('');
|
|
|
|
|
|
useEffect(() => {
|
|
|
- if (open) setTargetIds([]);
|
|
|
- }, [open]);
|
|
|
-
|
|
|
- const emails = useMemo(() => (source ? readClientEmails(source.settings) : []), [source]);
|
|
|
+ if (!open) return;
|
|
|
+ const rows = source ? readClientRows(source.settings) : [];
|
|
|
+ setClientRows(rows);
|
|
|
+ setSelectedEmails(rows.map((r) => r.email));
|
|
|
+ setTargetIds([]);
|
|
|
+ setSearch('');
|
|
|
+ }, [open, source]);
|
|
|
|
|
|
const targetOptions = useMemo(() => {
|
|
|
if (!source) return [];
|
|
|
@@ -51,11 +72,53 @@ export default function AttachClientsModal({
|
|
|
.map((ib) => ({ value: ib.id, label: `${ib.remark} (${ib.protocol}@${ib.port})` }));
|
|
|
}, [dbInbounds, source]);
|
|
|
|
|
|
+ const filteredRows = useMemo(() => {
|
|
|
+ const q = search.trim().toLowerCase();
|
|
|
+ if (!q) return clientRows;
|
|
|
+ return clientRows.filter(
|
|
|
+ (r) => r.email.toLowerCase().includes(q) || r.comment.toLowerCase().includes(q),
|
|
|
+ );
|
|
|
+ }, [clientRows, search]);
|
|
|
+
|
|
|
+ const columns: ColumnsType<ClientRow> = useMemo(
|
|
|
+ () => [
|
|
|
+ {
|
|
|
+ title: t('pages.inbounds.email'),
|
|
|
+ dataIndex: 'email',
|
|
|
+ key: 'email',
|
|
|
+ ellipsis: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: t('comment'),
|
|
|
+ dataIndex: 'comment',
|
|
|
+ key: 'comment',
|
|
|
+ ellipsis: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: t('enable'),
|
|
|
+ dataIndex: 'enable',
|
|
|
+ key: 'enable',
|
|
|
+ width: 90,
|
|
|
+ render: (enabled: boolean) =>
|
|
|
+ enabled ? (
|
|
|
+ <Tag color="success">{t('enable')}</Tag>
|
|
|
+ ) : (
|
|
|
+ <Tag>{t('pages.inbounds.attachClientsStatusDisabled')}</Tag>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ [t],
|
|
|
+ );
|
|
|
+
|
|
|
async function submit() {
|
|
|
- if (!source || targetIds.length === 0 || emails.length === 0) return;
|
|
|
+ if (!source || targetIds.length === 0 || selectedEmails.length === 0) return;
|
|
|
setSaving(true);
|
|
|
try {
|
|
|
- const msg = await HttpUtil.post('/panel/api/clients/bulkAttach', { emails, inboundIds: targetIds }, { headers: { 'Content-Type': 'application/json' } });
|
|
|
+ const msg = await HttpUtil.post(
|
|
|
+ '/panel/api/clients/bulkAttach',
|
|
|
+ { emails: selectedEmails, inboundIds: targetIds },
|
|
|
+ { headers: { 'Content-Type': 'application/json' } },
|
|
|
+ );
|
|
|
if (!msg?.success) {
|
|
|
messageApi.error(msg?.msg || t('somethingWentWrong'));
|
|
|
return;
|
|
|
@@ -81,15 +144,52 @@ export default function AttachClientsModal({
|
|
|
open={open}
|
|
|
onCancel={onClose}
|
|
|
onOk={submit}
|
|
|
- okButtonProps={{ disabled: targetIds.length === 0 || emails.length === 0, loading: saving }}
|
|
|
+ okButtonProps={{
|
|
|
+ disabled: targetIds.length === 0 || selectedEmails.length === 0,
|
|
|
+ loading: saving,
|
|
|
+ }}
|
|
|
okText={t('pages.inbounds.attachClients')}
|
|
|
cancelText={t('cancel')}
|
|
|
title={t('pages.inbounds.attachClientsTitle', { remark: source?.remark ?? '' })}
|
|
|
+ width={680}
|
|
|
>
|
|
|
{messageContextHolder}
|
|
|
<Typography.Paragraph type="secondary">
|
|
|
- {t('pages.inbounds.attachClientsDesc', { count: emails.length })}
|
|
|
+ {t('pages.inbounds.attachClientsDesc', { count: clientRows.length })}
|
|
|
</Typography.Paragraph>
|
|
|
+
|
|
|
+ <Space direction="vertical" size="small" style={{ width: '100%', marginBottom: 12 }}>
|
|
|
+ <Typography.Text strong>{t('pages.inbounds.attachClientsSelectLabel')}</Typography.Text>
|
|
|
+ <Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
|
|
|
+ <Input.Search
|
|
|
+ allowClear
|
|
|
+ value={search}
|
|
|
+ onChange={(e) => setSearch(e.target.value)}
|
|
|
+ placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
|
|
+ style={{ maxWidth: 320 }}
|
|
|
+ />
|
|
|
+ <Typography.Text type="secondary">
|
|
|
+ {t('pages.inbounds.attachClientsSelectedCount', {
|
|
|
+ selected: selectedEmails.length,
|
|
|
+ total: clientRows.length,
|
|
|
+ })}
|
|
|
+ </Typography.Text>
|
|
|
+ </Space>
|
|
|
+ <Table<ClientRow>
|
|
|
+ size="small"
|
|
|
+ rowKey="email"
|
|
|
+ columns={columns}
|
|
|
+ dataSource={filteredRows}
|
|
|
+ pagination={false}
|
|
|
+ scroll={{ y: 280 }}
|
|
|
+ rowSelection={{
|
|
|
+ selectedRowKeys: selectedEmails,
|
|
|
+ onChange: (keys) => setSelectedEmails(keys as string[]),
|
|
|
+ preserveSelectedRowKeys: true,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </Space>
|
|
|
+
|
|
|
{targetOptions.length === 0 ? (
|
|
|
<Alert type="info" showIcon message={t('pages.inbounds.attachClientsNoTargets')} />
|
|
|
) : (
|