diff --git a/frontend/src/pages/inbounds/InboundFormModal.new.tsx b/frontend/src/pages/inbounds/InboundFormModal.new.tsx new file mode 100644 index 00000000..030c287a --- /dev/null +++ b/frontend/src/pages/inbounds/InboundFormModal.new.tsx @@ -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(); + 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} + +
+ + WIP — Pattern A rewrite. Tabs are not yet wired into this skeleton. + +
+
+ + ); +}