Files
3x-ui/frontend/src/pages/clients/ClientBulkAdjustModal.tsx
T
Cheng Ho Ming, Eric b54325dcc9 fix(i18n): replace raw translation keys on panel screens (#7057)
Use valid translation keys on six panel screens, add the missing copy-failure and empty-value strings across all locales, and reject unresolved literal translation keys in the regression test.
2026-11-05 18:10:09 +01:00

155 lines
5.0 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Form, Input, InputNumber, Modal, Select, message } from 'antd';
import { FormProvider, useForm } from 'react-hook-form';
import { ClientBulkAdjustFormSchema, type ClientBulkAdjustFormValues } from '@/schemas/client';
import { TLS_FLOW_CONTROL } from '@/schemas/primitives/flow';
import { FormField } from '@/components/form/rhf';
const GB = 1024 * 1024 * 1024;
const FLOW_CLEAR = 'none';
const EMPTY: ClientBulkAdjustFormValues = {
addDays: 0,
addGB: 0,
flow: '',
limitHwid: null,
adTag: '',
};
interface ClientBulkAdjustModalProps {
open: boolean;
count: number;
onOpenChange: (open: boolean) => void;
onSubmit: (
addDays: number,
addBytes: number,
flow: string,
limitHwid?: number | null,
adTag?: string,
) => Promise<{ adjusted: number; skipped?: { email: string; reason: string }[] } | null>;
}
export default function ClientBulkAdjustModal({
open,
count,
onOpenChange,
onSubmit,
}: ClientBulkAdjustModalProps) {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
const [submitting, setSubmitting] = useState(false);
const methods = useForm<ClientBulkAdjustFormValues>({ defaultValues: EMPTY });
useEffect(() => {
if (open) methods.reset(EMPTY);
}, [open, methods]);
async function handleOk() {
const values = methods.getValues();
const validated = ClientBulkAdjustFormSchema.safeParse({
addDays: Math.trunc(Number(values.addDays) || 0),
addGB: Number(values.addGB) || 0,
flow: values.flow,
limitHwid:
values.limitHwid !== null &&
values.limitHwid !== undefined &&
(values.limitHwid as unknown) !== ''
? Math.trunc(Number(values.limitHwid))
: null,
adTag: values.adTag?.trim() ?? '',
});
if (!validated.success) {
messageApi.warning(t(validated.error.issues[0]?.message ?? 'somethingWentWrong'));
return;
}
const { addDays: days, addGB: gb, flow: flowValue, limitHwid, adTag } = validated.data;
setSubmitting(true);
try {
const bytes = Math.trunc(gb * GB);
const result = await onSubmit(days, bytes, flowValue, limitHwid, adTag);
if (!result) return;
const ok = result.adjusted ?? 0;
const skipped = result.skipped?.length ?? 0;
if (skipped === 0) {
messageApi.success(t('pages.clients.toasts.bulkAdjusted', { count: ok }));
} else {
const firstReason = result.skipped?.[0]?.reason ?? '';
messageApi.warning(
firstReason
? `${t('pages.clients.toasts.bulkAdjustedMixed', { ok, skipped })} — ${firstReason}`
: t('pages.clients.toasts.bulkAdjustedMixed', { ok, skipped }),
);
}
onOpenChange(false);
} finally {
setSubmitting(false);
}
}
return (
<>
{messageContextHolder}
<Modal
open={open}
title={t('pages.clients.bulkAdjustTitle', { count })}
okText={t('confirm')}
cancelText={t('cancel')}
confirmLoading={submitting}
onOk={handleOk}
onCancel={() => onOpenChange(false)}
destroyOnHidden
>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
title={t('pages.clients.bulkAdjustHint')}
/>
<FormProvider {...methods}>
<Form layout="vertical">
<FormField name="addDays" label={t('pages.clients.addDays')}>
<InputNumber style={{ width: '100%' }} step={1} precision={0} />
</FormField>
<FormField name="addGB" label={t('pages.clients.addTrafficGB')}>
<InputNumber style={{ width: '100%' }} step={1} />
</FormField>
<FormField name="flow" label={t('pages.clients.bulkFlow')}>
<Select
style={{ width: '100%' }}
options={[
{ value: '', label: t('pages.clients.bulkFlowNoChange') },
{ value: FLOW_CLEAR, label: t('pages.clients.bulkFlowDisable') },
...Object.values(TLS_FLOW_CONTROL).map((k) => ({ value: k, label: k })),
]}
/>
</FormField>
<FormField
name="limitHwid"
label={t('pages.clients.limitHwid')}
tooltip={t('pages.clients.limitHwidDesc')}
>
<InputNumber
style={{ width: '100%' }}
min={0}
step={1}
precision={0}
placeholder={t('pages.clients.bulkFlowNoChange')}
/>
</FormField>
<FormField
name="adTag"
label={t('pages.clients.mtprotoAdTag')}
extra={t('pages.clients.bulkAdTagHint')}
>
<Input placeholder={t('pages.clients.bulkFlowNoChange')} allowClear />
</FormField>
</Form>
</FormProvider>
</Modal>
</>
);
}