From 51ffba5961dbad2ede0fb6ba2c6693548f5e4000 Mon Sep 17 00:00:00 2001 From: nima1024m <114405577+nima1024m@users.noreply.github.com> Date: Sun, 28 Jun 2026 15:01:53 +0200 Subject: [PATCH] fix(balancers): defer validation errors until touched or save (#5626) The Add Balancer modal parsed its empty initial state through BalancerFormSchema on mount and bound Form.Item validateStatus/help directly to the result, so "Tag is required" and "Pick at least one outbound" rendered the moment the modal opened, before any user input. Gate the inline errors behind per-field touched tracking plus a submit-attempted flag, and drop the disabled Create button so a save attempt surfaces the errors (matching RuleFormModal). The existing key-based remount in BalancersTab resets the flags on each open. Add a regression test asserting no errors on open and errors only after a save attempt. --- .../xray/balancers/BalancerFormModal.tsx | 26 ++++++--- .../src/test/balancer-form-modal.test.tsx | 58 +++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 frontend/src/test/balancer-form-modal.test.tsx diff --git a/frontend/src/pages/xray/balancers/BalancerFormModal.tsx b/frontend/src/pages/xray/balancers/BalancerFormModal.tsx index 446364c93..93d6ff250 100644 --- a/frontend/src/pages/xray/balancers/BalancerFormModal.tsx +++ b/frontend/src/pages/xray/balancers/BalancerFormModal.tsx @@ -68,10 +68,14 @@ export default function BalancerFormModal({ }: BalancerFormModalProps) { const { t } = useTranslation(); const [state, setState] = useState(() => initialState(balancer)); + const [touched, setTouched] = useState>>({}); + const [submitAttempted, setSubmitAttempted] = useState(false); const isEdit = balancer != null; - const update = (key: K, value: FormState[K]) => + const update = (key: K, value: FormState[K]) => { + setTouched((prev) => (prev[key] ? prev : { ...prev, [key]: true })); setState((prev) => ({ ...prev, [key]: value })); + }; const parsed = useMemo( () => BalancerFormSchema.safeParse(state), @@ -89,8 +93,17 @@ export default function BalancerFormModal({ return map; }, [parsed, t]); + const showTagIssue = submitAttempted || !!touched.tag; + const showSelectorIssue = submitAttempted || !!touched.selector; + const tagError = showTagIssue ? issues.tag : ''; + const selectorError = showSelectorIssue ? issues.selector : ''; + const showDuplicate = showTagIssue && duplicateTag; + function submit() { - if (!parsed.success || duplicateTag) return; + if (!parsed.success || duplicateTag) { + setSubmitAttempted(true); + return; + } const values = { ...parsed.data }; if (values.strategy !== 'leastLoad') delete values.settings; onConfirm(values); @@ -128,7 +141,6 @@ export default function BalancerFormModal({ title={title} okText={okText} cancelText={t('close')} - okButtonProps={{ disabled: !parsed.success || duplicateTag }} mask={{ closable: false }} onOk={submit} onCancel={onClose} @@ -137,8 +149,8 @@ export default function BalancerFormModal({