mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-30 08:59:34 +00:00
feat(frontend): InboundFormModal.new.tsx skeleton (Pattern A)
First commit of the sibling-file modal rewrite. The new modal mounts Form.useForm<InboundFormValues>, hydrates via rawInboundToFormValues on open (edit) or buildAddModeValues (add), runs validateFields + safeParse on submit, and posts the formValuesToWirePayload result. No tabs yet — the modal body shows a WIP placeholder. The file is not imported anywhere; the existing InboundFormModal.tsx remains the one InboundsPage renders. Build, lint, and 280 tests stay green. Subsequent commits add the basic / sniffing / protocol / stream / security / advanced / fallbacks sections; the atomic import swap in InboundsPage.tsx lands last.
This commit is contained in:
142
frontend/src/pages/inbounds/InboundFormModal.new.tsx
Normal file
142
frontend/src/pages/inbounds/InboundFormModal.new.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Modal, Typography, message } from 'antd';
|
||||
|
||||
import { HttpUtil, RandomUtil } from '@/utils';
|
||||
import {
|
||||
rawInboundToFormValues,
|
||||
formValuesToWirePayload,
|
||||
} from '@/lib/xray/inbound-form-adapter';
|
||||
import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
|
||||
import { InboundFormSchema, type InboundFormValues } from '@/schemas/forms/inbound-form';
|
||||
import type { DBInbound } from '@/models/dbinbound';
|
||||
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||
|
||||
// Pattern A rewrite of InboundFormModal. Built as a sibling file so the
|
||||
// build stays green while the rewrite progresses section by section. The
|
||||
// old InboundFormModal.tsx continues to be the one InboundsPage renders
|
||||
// until the atomic swap at the end of the rewrite (per Core Decision 7 in
|
||||
// the architecture spec).
|
||||
//
|
||||
// Current state: skeleton only. The form holds the full InboundFormValues
|
||||
// shape via setFieldsValue on open; validateFields + safeParse + adapter
|
||||
// produce the wire payload on submit. Tabs are not yet wired — the modal
|
||||
// body shows a WIP placeholder.
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface InboundFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
mode: 'add' | 'edit';
|
||||
dbInbound: DBInbound | null;
|
||||
dbInbounds: DBInbound[];
|
||||
availableNodes?: NodeRecord[];
|
||||
}
|
||||
|
||||
function buildAddModeValues(): InboundFormValues {
|
||||
const settings = createDefaultInboundSettings('vless') ?? undefined;
|
||||
return rawInboundToFormValues({
|
||||
protocol: 'vless',
|
||||
settings,
|
||||
streamSettings: { network: 'tcp', security: 'none' },
|
||||
sniffing: {},
|
||||
port: RandomUtil.randomInteger(10000, 60000),
|
||||
listen: '',
|
||||
tag: '',
|
||||
enable: true,
|
||||
trafficReset: 'never',
|
||||
});
|
||||
}
|
||||
|
||||
export default function InboundFormModalNew({
|
||||
open,
|
||||
onClose,
|
||||
onSaved,
|
||||
mode,
|
||||
dbInbound,
|
||||
}: InboundFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [form] = Form.useForm<InboundFormValues>();
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const initial = mode === 'edit' && dbInbound
|
||||
? rawInboundToFormValues(dbInbound)
|
||||
: buildAddModeValues();
|
||||
form.setFieldsValue(initial);
|
||||
}, [open, mode, dbInbound, form]);
|
||||
|
||||
const submit = async () => {
|
||||
let values: InboundFormValues;
|
||||
try {
|
||||
values = await form.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const parsed = InboundFormSchema.safeParse(values);
|
||||
if (!parsed.success) {
|
||||
const issue = parsed.error.issues[0];
|
||||
messageApi.error(
|
||||
t(issue?.message ?? 'somethingWentWrong', {
|
||||
defaultValue: issue?.message ?? 'invalid',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = formValuesToWirePayload(parsed.data);
|
||||
const url = mode === 'edit' && dbInbound
|
||||
? `/panel/api/inbounds/update/${dbInbound.id}`
|
||||
: '/panel/api/inbounds/add';
|
||||
const msg = await HttpUtil.post(url, payload);
|
||||
if (msg?.success) {
|
||||
onSaved();
|
||||
onClose();
|
||||
}
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const title = mode === 'edit'
|
||||
? t('pages.inbounds.modifyInbound')
|
||||
: t('pages.inbounds.addInbound');
|
||||
|
||||
const okText = mode === 'edit'
|
||||
? t('pages.clients.submitEdit')
|
||||
: t('create');
|
||||
|
||||
return (
|
||||
<>
|
||||
{messageContextHolder}
|
||||
<Modal
|
||||
open={open}
|
||||
title={title}
|
||||
okText={okText}
|
||||
cancelText={t('close')}
|
||||
confirmLoading={saving}
|
||||
mask={{ closable: false }}
|
||||
width={780}
|
||||
onOk={submit}
|
||||
onCancel={onClose}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
colon={false}
|
||||
labelCol={{ sm: { span: 8 } }}
|
||||
wrapperCol={{ sm: { span: 14 } }}
|
||||
>
|
||||
<Text type="secondary">
|
||||
WIP — Pattern A rewrite. Tabs are not yet wired into this skeleton.
|
||||
</Text>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user