diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index b7baf759f..58dd42ad2 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -53,4 +53,37 @@ export default [ 'jsx-a11y/no-autofocus': 'off', }, }, + { + // The settings and xray pages write numeric InputNumber changes straight + // into state, so a null-collapsing handler (`Number(v) || N`, or the + // ternary `typeof v === 'number' ? v : N`) turns a cleared field into a + // stored N — the cleared-port bug, #6121. Handlers here go through + // onNumber() (src/utils/onNumber.ts) instead. Known limit: a handler + // extracted into a variable and passed as onChange={handler} is not + // matched; the inline shapes below are the ones that drift in practice. + files: ['src/pages/settings/**/*.tsx', 'src/pages/xray/**/*.tsx'], + rules: { + 'no-restricted-syntax': ['error', { + selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] LogicalExpression[operator="||"] > CallExpression[callee.name="Number"]', + message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).', + }, { + selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] ConditionalExpression[test.left.operator="typeof"][alternate.type="Literal"]', + message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).', + }, { + selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] LogicalExpression[operator="??"][right.type="Literal"]', + message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).', + }], + }, + }, + { + // The xray form modals (OutboundFormModal, BalancerFormModal, + // DnsServerModal, WarpModal, …) stage values behind Zod validation like + // the clients/inbounds modals do, and some of their fields carry a + // deliberate clear-means-zero semantic — the direct-write rule above + // does not apply to them. + files: ['src/pages/xray/**/*Modal.tsx'], + rules: { + 'no-restricted-syntax': 'off', + }, + }, ]; diff --git a/frontend/src/pages/settings/TelegramTab.tsx b/frontend/src/pages/settings/TelegramTab.tsx index 349435da1..6f3600886 100644 --- a/frontend/src/pages/settings/TelegramTab.tsx +++ b/frontend/src/pages/settings/TelegramTab.tsx @@ -4,6 +4,7 @@ import { Alert, Button, Input, InputNumber, Select, Space, Switch, Tabs } from ' import { BellOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons'; import { LanguageManager } from '@/utils'; import { HttpUtil } from '@/utils'; +import { onNumber } from '@/utils/onNumber'; import type { AllSetting } from '@/models/setting'; import { SettingListItem } from '@/components/ui'; import { TelegramNotifications } from '@/components/ui/notifications/TelegramNotifications'; @@ -122,9 +123,10 @@ function NotifyTimeField({ value, onChange }: { value: string; onChange: (v: str update({ num: Math.max(1, Number(v) || 1) })} + onChange={onNumber((v) => update({ num: Math.max(1, v) }))} aria-label={t('pages.settings.notifyTime.interval')} /> diff --git a/frontend/src/pages/xray/basics/BasicsTab.tsx b/frontend/src/pages/xray/basics/BasicsTab.tsx index d29a67f79..c31727f90 100644 --- a/frontend/src/pages/xray/basics/BasicsTab.tsx +++ b/frontend/src/pages/xray/basics/BasicsTab.tsx @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { onNumber } from '@/utils/onNumber'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Input, InputNumber, Modal, Select, Space, Switch, Tabs } from 'antd'; import { @@ -216,10 +217,10 @@ export default function BasicsTab({ style={{ width: '100%' }} value={directHappyEyeballs.tryDelayMs} placeholder="150" - onChange={(v) => setDirectHappyEyeballs({ + onChange={onNumber((v) => setDirectHappyEyeballs({ ...directHappyEyeballs, - tryDelayMs: typeof v === 'number' ? v : 0, - })} + tryDelayMs: v, + }))} /> } />