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:
MHSanaei
2026-05-26 02:01:31 +02:00
parent e2784fcf3f
commit b10e0d0acd

View 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>
</>
);
}