♻️ refactor: Clean up NodeForm and ServerForm components by removing unused functions and optimizing state management
This commit is contained in:
parent
6a3bb7016e
commit
10250d9e34
@ -62,11 +62,6 @@ const buildSchema = (t: ReturnType<typeof useTranslations>) =>
|
|||||||
|
|
||||||
export type NodeFormValues = z.infer<ReturnType<typeof buildSchema>>;
|
export type NodeFormValues = z.infer<ReturnType<typeof buildSchema>>;
|
||||||
|
|
||||||
async function getServers(): Promise<ServerRow[]> {
|
|
||||||
const { data } = await filterServerList({ page: 1, size: 1000 });
|
|
||||||
return (data?.data?.list || []) as ServerRow[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NodeForm(props: {
|
export default function NodeForm(props: {
|
||||||
trigger: string;
|
trigger: string;
|
||||||
title: string;
|
title: string;
|
||||||
@ -147,7 +142,6 @@ export default function NodeForm(props: {
|
|||||||
if (!allowed.includes(form.getValues('protocol') as ProtocolName)) {
|
if (!allowed.includes(form.getValues('protocol') as ProtocolName)) {
|
||||||
form.setValue('protocol', '' as any);
|
form.setValue('protocol', '' as any);
|
||||||
}
|
}
|
||||||
// Do not auto-fill port here; handled in handleProtocolChange
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProtocolChange(nextProto?: ProtocolName | null) {
|
function handleProtocolChange(nextProto?: ProtocolName | null) {
|
||||||
@ -164,15 +158,6 @@ export default function NodeForm(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit(values: NodeFormValues) {
|
|
||||||
const ok = await onSubmit(values);
|
|
||||||
if (ok) {
|
|
||||||
form.reset();
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
@ -310,14 +295,11 @@ export default function NodeForm(props: {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={form.handleSubmit(
|
onClick={form.handleSubmit(onSubmit, (errors) => {
|
||||||
async (vals) => submit(vals),
|
|
||||||
(errors) => {
|
|
||||||
const key = Object.keys(errors)[0] as keyof typeof errors;
|
const key = Object.keys(errors)[0] as keyof typeof errors;
|
||||||
if (key) toast.error(String(errors[key]?.message));
|
if (key) toast.error(String(errors[key]?.message));
|
||||||
return false;
|
return false;
|
||||||
},
|
})}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{t('confirm')}
|
{t('confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -190,6 +190,8 @@ export const formSchema = z.object({
|
|||||||
protocols: z.array(protocolApiScheme),
|
protocols: z.array(protocolApiScheme),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type ServerFormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export type ProtocolType = (typeof protocols)[number];
|
export type ProtocolType = (typeof protocols)[number];
|
||||||
|
|
||||||
export function getProtocolDefaultConfig(proto: ProtocolType) {
|
export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/componen
|
|||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
@ -44,102 +44,104 @@ import {
|
|||||||
LABELS,
|
LABELS,
|
||||||
protocols as PROTOCOLS,
|
protocols as PROTOCOLS,
|
||||||
SECURITY,
|
SECURITY,
|
||||||
|
ServerFormValues,
|
||||||
SS_CIPHERS,
|
SS_CIPHERS,
|
||||||
TRANSPORTS,
|
TRANSPORTS,
|
||||||
TUIC_CONGESTION,
|
TUIC_CONGESTION,
|
||||||
TUIC_UDP_RELAY_MODES,
|
TUIC_UDP_RELAY_MODES,
|
||||||
} from './form-schema';
|
} from './form-schema';
|
||||||
|
|
||||||
interface ServerFormProps<T> {
|
|
||||||
onSubmit: (data: T) => Promise<boolean> | boolean;
|
|
||||||
initialValues?: T | any;
|
|
||||||
loading?: boolean;
|
|
||||||
trigger: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function titleCase(s: string) {
|
function titleCase(s: string) {
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeValues(raw: any) {
|
export default function ServerForm(props: {
|
||||||
return {
|
trigger: string;
|
||||||
name: raw?.name ?? '',
|
title: string;
|
||||||
address: raw?.address ?? '',
|
loading?: boolean;
|
||||||
country: raw?.country ?? '',
|
initialValues?: Partial<ServerFormValues>;
|
||||||
city: raw?.city ?? '',
|
onSubmit: (values: ServerFormValues) => Promise<boolean> | boolean;
|
||||||
ratio: Number(raw?.ratio ?? 1),
|
}) {
|
||||||
protocols: Array.isArray(raw?.protocols) ? raw.protocols : [],
|
const { trigger, title, loading, initialValues, onSubmit } = props;
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ServerForm<T extends { [x: string]: any }>({
|
|
||||||
onSubmit,
|
|
||||||
initialValues,
|
|
||||||
loading,
|
|
||||||
trigger,
|
|
||||||
title,
|
|
||||||
}: Readonly<ServerFormProps<T>>) {
|
|
||||||
const t = useTranslations('servers');
|
const t = useTranslations('servers');
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [activeType, setActiveType] = useState<(typeof PROTOCOLS)[number]>('shadowsocks');
|
const [activeType, setActiveType] = useState<(typeof PROTOCOLS)[number]>('shadowsocks');
|
||||||
|
const [protocolsEnabled, setProtocolsEnabled] = useState<string[]>([]);
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const form = useForm({
|
||||||
() =>
|
resolver: zodResolver(formSchema),
|
||||||
normalizeValues({
|
defaultValues: {
|
||||||
name: '',
|
name: '',
|
||||||
address: '',
|
address: '',
|
||||||
country: '',
|
country: '',
|
||||||
city: '',
|
city: '',
|
||||||
ratio: 1,
|
ratio: 1,
|
||||||
protocols: [],
|
protocols: [],
|
||||||
}),
|
...initialValues,
|
||||||
[],
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
const form = useForm<any>({ resolver: zodResolver(formSchema), defaultValues });
|
|
||||||
const { control } = form;
|
const { control } = form;
|
||||||
|
|
||||||
const protocolsValues = useWatch({ control, name: 'protocols' });
|
const protocolsValues = useWatch({ control, name: 'protocols' });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const normalized = normalizeValues(initialValues || {});
|
if (initialValues) {
|
||||||
const byType = new Map<string, any>();
|
const enabledProtocols = PROTOCOLS.filter((type) => {
|
||||||
(Array.isArray(normalized.protocols) ? normalized.protocols : []).forEach((p: any) => {
|
const protocol = initialValues.protocols?.find((p) => p.type === type);
|
||||||
if (p && p.type) byType.set(String(p.type), p);
|
return protocol && protocol.port && Number(protocol.port) > 0;
|
||||||
});
|
});
|
||||||
const full = PROTOCOLS.map((t) => byType.get(t) || getProtocolDefaultConfig(t));
|
setProtocolsEnabled(enabledProtocols);
|
||||||
form.reset({ ...normalized, protocols: full });
|
form.reset({
|
||||||
setActiveType('shadowsocks');
|
name: '',
|
||||||
|
address: '',
|
||||||
|
country: '',
|
||||||
|
city: '',
|
||||||
|
ratio: 1,
|
||||||
|
...initialValues,
|
||||||
|
protocols: PROTOCOLS.map((type) => {
|
||||||
|
const existingProtocol = initialValues.protocols?.find((p) => p.type === type);
|
||||||
|
return existingProtocol || getProtocolDefaultConfig(type);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
|
|
||||||
async function handleSubmit(data: { [x: string]: any }) {
|
async function handleSubmit(values: Record<string, any>) {
|
||||||
const all = Array.isArray(data?.protocols) ? data.protocols : [];
|
const filtered = (values?.protocols || [])
|
||||||
const filtered = all
|
.filter((p: any, index: number) => {
|
||||||
.filter((p: any) => {
|
const port = Number(p?.port);
|
||||||
const v = (p ?? {}).port;
|
const protocolType = PROTOCOLS[index];
|
||||||
const n = Number(v);
|
return (
|
||||||
return Number.isFinite(n) && n > 0 && n <= 65535;
|
protocolType &&
|
||||||
|
protocolsEnabled.includes(protocolType) &&
|
||||||
|
Number.isFinite(port) &&
|
||||||
|
port > 0 &&
|
||||||
|
port <= 65535
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.map((p: any) => ({ ...p, port: Number(p.port) }));
|
.map((p: any) => ({ ...p, port: Number(p.port) }));
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
toast.error(t('validation_failed'));
|
toast.error(t('validation_failed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const body = {
|
|
||||||
name: data?.name,
|
|
||||||
country: data?.country,
|
|
||||||
city: data?.city,
|
|
||||||
ratio: Number(data?.ratio ?? 1),
|
|
||||||
address: data?.address,
|
|
||||||
protocols: filtered,
|
|
||||||
} as unknown as T;
|
|
||||||
const ok = await onSubmit(body as unknown as T);
|
|
||||||
if (ok) setOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// inlined protocol editor below in TabsContent
|
const result = {
|
||||||
|
name: values.name,
|
||||||
|
country: values.country,
|
||||||
|
city: values.city,
|
||||||
|
ratio: Number(values.ratio || 1),
|
||||||
|
address: values.address,
|
||||||
|
protocols: filtered,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ok = await onSubmit(result);
|
||||||
|
if (ok) {
|
||||||
|
form.reset();
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
@ -148,7 +150,15 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!initialValues) {
|
if (!initialValues) {
|
||||||
const full = PROTOCOLS.map((t) => getProtocolDefaultConfig(t));
|
const full = PROTOCOLS.map((t) => getProtocolDefaultConfig(t));
|
||||||
form.reset({ ...defaultValues, protocols: full });
|
form.reset({
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
country: '',
|
||||||
|
city: '',
|
||||||
|
ratio: 1,
|
||||||
|
protocols: full,
|
||||||
|
});
|
||||||
|
setProtocolsEnabled([]);
|
||||||
}
|
}
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
@ -244,7 +254,7 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
</div>
|
</div>
|
||||||
<div className='pt-2'>
|
<div className='pt-2'>
|
||||||
<Tabs value={activeType} onValueChange={(v) => setActiveType(v as any)}>
|
<Tabs value={activeType} onValueChange={(v) => setActiveType(v as any)}>
|
||||||
<TabsList className='w-full flex-wrap'>
|
<TabsList className='h-auto w-full flex-wrap'>
|
||||||
{PROTOCOLS.map((type) => (
|
{PROTOCOLS.map((type) => (
|
||||||
<TabsTrigger key={type} value={type}>
|
<TabsTrigger key={type} value={type}>
|
||||||
{titleCase(type)}
|
{titleCase(type)}
|
||||||
@ -257,11 +267,30 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
PROTOCOLS.findIndex((t) => t === type),
|
PROTOCOLS.findIndex((t) => t === type),
|
||||||
);
|
);
|
||||||
const current = Array.isArray(protocolsValues) ? protocolsValues[i] || {} : {};
|
const current = Array.isArray(protocolsValues) ? protocolsValues[i] || {} : {};
|
||||||
const transport = (current?.transport as string | undefined) ?? 'tcp';
|
const transport = ((current as any)?.transport as string | undefined) ?? 'tcp';
|
||||||
const security = current?.security as string | undefined;
|
const security = (current as any)?.security as string | undefined;
|
||||||
const cipher = current?.cipher as string | undefined;
|
const cipher = (current as any)?.cipher as string | undefined;
|
||||||
|
const isEnabled = protocolsEnabled.includes(type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent key={type} value={type} className='pt-3'>
|
<TabsContent key={type} value={type} className='space-y-4 pt-3'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<span className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
|
||||||
|
{t('enabled')}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={isEnabled}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setProtocolsEnabled([...protocolsEnabled, type]);
|
||||||
|
} else {
|
||||||
|
setProtocolsEnabled(protocolsEnabled.filter((p) => p !== type));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEnabled && (
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<div className='grid grid-cols-2 gap-2'>
|
<div className='grid grid-cols-2 gap-2'>
|
||||||
<FormField
|
<FormField
|
||||||
@ -507,7 +536,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
<div className='pt-2'>
|
<div className='pt-2'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!!field.value}
|
checked={!!field.value}
|
||||||
onCheckedChange={(checked) => field.onChange(checked)}
|
onCheckedChange={(checked) =>
|
||||||
|
field.onChange(checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -525,7 +556,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
<div className='pt-2'>
|
<div className='pt-2'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!!field.value}
|
checked={!!field.value}
|
||||||
onCheckedChange={(checked) => field.onChange(checked)}
|
onCheckedChange={(checked) =>
|
||||||
|
field.onChange(checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -758,7 +791,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
{...field}
|
{...field}
|
||||||
placeholder={t('security_private_key_placeholder')}
|
placeholder={t(
|
||||||
|
'security_private_key_placeholder',
|
||||||
|
)}
|
||||||
onValueChange={(v) => field.onChange(v)}
|
onValueChange={(v) => field.onChange(v)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -841,7 +876,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
<div className='pt-2'>
|
<div className='pt-2'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!!field.value}
|
checked={!!field.value}
|
||||||
onCheckedChange={(checked) => field.onChange(checked)}
|
onCheckedChange={(checked) =>
|
||||||
|
field.onChange(checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -854,6 +891,7 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -868,9 +906,10 @@ export default function ServerForm<T extends { [x: string]: any }>({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={form.handleSubmit(handleSubmit, (e) => {
|
onClick={form.handleSubmit(handleSubmit, (errors) => {
|
||||||
console.log(e, form.getValues());
|
console.log(errors, form.getValues());
|
||||||
toast.error(t('validation_failed'));
|
const key = Object.keys(errors)[0] as keyof typeof errors;
|
||||||
|
if (key) toast.error(String(errors[key]?.message));
|
||||||
return false;
|
return false;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -109,7 +109,6 @@ export function ProtocolForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// API请求函数
|
|
||||||
const request = async (
|
const request = async (
|
||||||
pagination: { page: number; size: number },
|
pagination: { page: number; size: number },
|
||||||
filter: Record<string, unknown>,
|
filter: Record<string, unknown>,
|
||||||
@ -125,7 +124,6 @@ export function ProtocolForm() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 表格列定义
|
|
||||||
const columns: ColumnDef<API.SubscribeApplication, any>[] = [
|
const columns: ColumnDef<API.SubscribeApplication, any>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'is_default',
|
accessorKey: 'is_default',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user