From a4fd955c1b6738ff3ec440eb6a7bb9c5c328747b Mon Sep 17 00:00:00 2001
From: Denozordec
Date: Fri, 17 Jul 2026 13:53:54 +0700
Subject: [PATCH] =?UTF-8?q?refactor(forms):=20=D0=BF=D0=B5=D1=80=D0=B5?=
=?UTF-8?q?=D0=B2=D0=B5=D1=81=D1=82=D0=B8=20service=20sheets=20=D0=BD?=
=?UTF-8?q?=D0=B0=20FormSheet=20=D0=B8=20RHF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
SubdomainEditSheet на FormSheet+RHF+Zod; FormField hint через FieldDescription; service-edit на Field/Item примитивы.
Co-authored-by: Cursor
---
apps/web/src/components/form-field.tsx | 10 +-
.../web/src/components/service-edit-sheet.tsx | 123 +++++------
.../src/components/subdomain-edit-sheet.tsx | 208 ++++++++++--------
3 files changed, 182 insertions(+), 159 deletions(-)
diff --git a/apps/web/src/components/form-field.tsx b/apps/web/src/components/form-field.tsx
index 061a075..94c47c1 100644
--- a/apps/web/src/components/form-field.tsx
+++ b/apps/web/src/components/form-field.tsx
@@ -3,6 +3,7 @@ import type { Control, FieldPath, FieldValues } from 'react-hook-form'
import { Controller } from 'react-hook-form'
import {
Field,
+ FieldDescription,
FieldError,
FieldLabel,
} from '@cfdm/ui/components/field'
@@ -13,6 +14,7 @@ interface FormFieldProps {
control: Control
label: string
htmlFor?: string
+ hint?: string
className?: string
children: (props: {
id: string
@@ -29,6 +31,7 @@ export function FormField({
control,
label,
htmlFor,
+ hint,
className,
children,
}: FormFieldProps) {
@@ -52,6 +55,9 @@ export function FormField({
onBlur: field.onBlur,
ref: field.ref,
})}
+ {hint && !fieldState.error ? (
+ {hint}
+ ) : null}
)}
@@ -80,9 +86,7 @@ export function FormFieldSimple({
{label}
{children}
- {hint && !error && (
- {hint}
- )}
+ {hint && !error ? {hint} : null}
)
diff --git a/apps/web/src/components/service-edit-sheet.tsx b/apps/web/src/components/service-edit-sheet.tsx
index d533f3f..6c771e0 100644
--- a/apps/web/src/components/service-edit-sheet.tsx
+++ b/apps/web/src/components/service-edit-sheet.tsx
@@ -19,19 +19,6 @@ import type {
UpdateServiceConfigInput,
} from '@/lib/schemas'
import { bindingToFqdn } from '@/lib/parse-fqdn'
-import { AppButton } from '@/components/app-button'
-import { AppInput } from '@/components/app-input'
-import {
- AppField,
- AppFieldGroup,
- AppFieldLabel,
-} from '@/components/app-field'
-import {
- AppItem,
- AppItemContent,
- AppItemGroup,
-} from '@/components/app-item'
-import { AppSeparator } from '@/components/app-separator'
import {
Sheet,
SheetContent,
@@ -40,6 +27,14 @@ import {
SheetHeader,
SheetTitle,
} from '@cfdm/ui/components/sheet'
+import { Button } from '@cfdm/ui/components/button'
+import { Field, FieldGroup, FieldLabel } from '@cfdm/ui/components/field'
+import { Input } from '@cfdm/ui/components/input'
+import {
+ Item,
+ ItemContent,
+ ItemGroup,
+} from '@cfdm/ui/components/item'
import { LoadingButton } from '@/components/loading-button'
import { TabsContent } from '@cfdm/ui/components/tabs'
import {
@@ -405,27 +400,27 @@ export function ServiceEditSheet({
listClassName="mb-0 w-full"
>
-
-
- Название
-
+
+ Название
+ setName(e.target.value)}
/>
-
-
- Slug
-
+
+ Slug
+ setSlug(e.target.value)}
/>
-
-
- Группа сервисов
+
+
+ Группа сервисов
-
-
- IP-адреса сервиса
+
+
+ IP-адреса сервиса
-
-
+
+
{groupHasDomain && (
<>
@@ -464,9 +459,9 @@ export function ServiceEditSheet({
сервиса для общего домена группы.
>
@@ -503,10 +498,10 @@ export function ServiceEditSheet({
FQDN → IP или CNAME для DNS-записей Cloudflare
-
+
+
{bindings.length === 0 ? (
@@ -515,14 +510,14 @@ export function ServiceEditSheet({
title="Нет привязок"
description="Необязательно. Пример: newdom.ivx.su — зона ivx.su определится автоматически."
action={
-
+
+
}
/>
) : (
-
+
{bindings.map((binding, index) => {
const showLbBlock =
(binding.record_type === 'A' && binding.target_ips.length > 0) ||
@@ -532,11 +527,11 @@ export function ServiceEditSheet({
binding.target_ips.length > 1 &&
binding.lb_mode !== 'round_robin'
return (
-
-
+ -
+
-
- FQDN
+
+ FQDN
-
-
+
+
-
- Тип записи
+
+ Тип записи
-
+
{binding.record_type === 'CNAME' ? (
-
-
+
+
CNAME-цель
-
-
+ handleCnameChange(index, event.target.value)}
/>
-
+
) : (
-
- IP
+
+ IP
-
+
)}
{showLbBlock ? (
@@ -629,17 +624,17 @@ export function ServiceEditSheet({
idPrefix={`binding-${index}-health`}
/>
) : null}
-
-
+
+
)
})}
-
+
)}
-
+
{!isCreate ? (
diff --git a/apps/web/src/components/subdomain-edit-sheet.tsx b/apps/web/src/components/subdomain-edit-sheet.tsx
index a00507a..f47cbd7 100644
--- a/apps/web/src/components/subdomain-edit-sheet.tsx
+++ b/apps/web/src/components/subdomain-edit-sheet.tsx
@@ -1,12 +1,16 @@
-import { useEffect, useMemo, useState } from 'react'
+import { useEffect, useMemo } from 'react'
+import { useForm, Controller } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
import type { CertMonitoring } from '@cfdm/shared'
import type { ServiceView, SubdomainRecord } from '@/lib/schemas'
import { certMonitoringOptions } from '@/lib/cert-monitoring'
import { formatServiceGroupLabel } from '@/lib/service-utils'
+import { FormSheet } from '@/components/form-sheet'
import { FormFieldSimple } from '@/components/form-field'
import { LoadingButton } from '@/components/loading-button'
-import { AppInput } from '@/components/app-input'
-import { AppFieldDescription, AppFieldGroup } from '@/components/app-field'
+import { FieldGroup } from '@cfdm/ui/components/field'
+import { Input } from '@cfdm/ui/components/input'
import {
Select,
SelectContent,
@@ -14,20 +18,14 @@ import {
SelectTrigger,
SelectValue,
} from '@cfdm/ui/components/select'
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from '@cfdm/ui/components/sheet'
-export interface SubdomainEditValues {
- name: string
- serviceId: string
- certMonitoring: CertMonitoring
-}
+const subdomainEditSchema = z.object({
+ name: z.string().min(1, 'Укажите имя'),
+ serviceId: z.string(),
+ certMonitoring: z.enum(['auto', 'required', 'skipped']),
+})
+
+export type SubdomainEditValues = z.infer
interface SubdomainEditSheetProps {
mode: 'create' | 'edit'
@@ -54,9 +52,14 @@ export function SubdomainEditSheet({
onOpenChange,
onSubmit,
}: SubdomainEditSheetProps) {
- const [name, setName] = useState('')
- const [serviceId, setServiceId] = useState('none')
- const [certMonitoring, setCertMonitoring] = useState('auto')
+ const form = useForm({
+ resolver: zodResolver(subdomainEditSchema),
+ defaultValues: {
+ name: '',
+ serviceId: 'none',
+ certMonitoring: 'auto',
+ },
+ })
const serviceItems = useMemo(
() => [
@@ -84,55 +87,83 @@ export function SubdomainEditSheet({
useEffect(() => {
if (!open) return
if (mode === 'edit' && subdomain) {
- setName(subdomain.name)
- setServiceId(currentServiceId || 'none')
- setCertMonitoring(subdomain.cert_monitoring)
+ form.reset({
+ name: subdomain.name,
+ serviceId: currentServiceId || 'none',
+ certMonitoring: subdomain.cert_monitoring,
+ })
return
}
- setName('')
- setServiceId('none')
- setCertMonitoring('auto')
- }, [open, mode, subdomain, currentServiceId])
+ form.reset({
+ name: '',
+ serviceId: 'none',
+ certMonitoring: 'auto',
+ })
+ }, [open, mode, subdomain, currentServiceId, form])
- function handleSubmit(event: React.FormEvent) {
- event.preventDefault()
- const trimmed = name.trim()
- if (!trimmed) return
- onSubmit({ name: trimmed, serviceId, certMonitoring })
+ const certMonitoring = form.watch('certMonitoring')
+ const certHint =
+ certMonitoringOptions.find((o) => o.value === certMonitoring)?.description
+
+ function handleSubmit(values: SubdomainEditValues) {
+ onSubmit({
+ name: values.name.trim(),
+ serviceId: values.serviceId,
+ certMonitoring: values.certMonitoring as CertMonitoring,
+ })
}
return (
-
-
-
-
- {mode === 'create' ? 'Создать поддомен' : 'Редактировать поддомен'}
-
-
- {mode === 'create'
- ? `Имя записи в зоне ${zoneName} (например, www или api)`
- : `Изменение поддомена в зоне ${zoneName}`}
-
-
-
-
-
+ )}
+ />
+
+ >
+ ) : null}
+
+
)
}