OutboundSubtreeJsonForm.tsx 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import { useEffect, useRef, useState, type ReactNode } from 'react';
  2. import { Form, type FormInstance } from 'antd';
  3. import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
  4. import { nestAtPath, parseJsonObject, serializeOverride } from './helpers';
  5. interface OutboundSubtreeJsonFormProps {
  6. value?: string;
  7. onChange?: (next: string) => void;
  8. // Form path the inner form edits, e.g. ['streamSettings', 'sockopt'] or ['mux'].
  9. path: (string | number)[];
  10. // Renders the reused outbound form given this wrapper's own form instance.
  11. render: (form: FormInstance<OutboundFormValues>) => ReactNode;
  12. // Seeds the form when the stored value is empty, so toggling a section on
  13. // pre-fills sensible defaults instead of blanks (used by Mux).
  14. defaultSubtree?: Record<string, unknown>;
  15. // Turns the edited subtree into the stored JSON string (default: prune empties).
  16. // Mux overrides this to store '' (= inherit) when its enable flag is off.
  17. serialize?: (subtree: unknown) => string;
  18. }
  19. // Hosts the reused outbound transport forms (which bind to fixed form paths)
  20. // inside an isolated antd Form, mirroring SubJsonFinalMaskForm: seed the form
  21. // from the JSON string, watch the edited subtree, and report a JSON string back
  22. // to the parent host form. component={false} avoids a nested <form> DOM node.
  23. export default function OutboundSubtreeJsonForm({
  24. value = '',
  25. onChange,
  26. path,
  27. render,
  28. defaultSubtree,
  29. serialize = serializeOverride,
  30. }: OutboundSubtreeJsonFormProps) {
  31. const [form] = Form.useForm();
  32. const [initial] = useState<Record<string, unknown>>(() => {
  33. const parsed = parseJsonObject(value);
  34. return Object.keys(parsed).length ? parsed : (defaultSubtree ?? {});
  35. });
  36. const onChangeRef = useRef(onChange);
  37. onChangeRef.current = onChange;
  38. const subtree = Form.useWatch(path, form);
  39. useEffect(() => {
  40. const next = serialize(subtree);
  41. if (next !== value) onChangeRef.current?.(next);
  42. // serialize is logically stable; re-run only when the edited subtree changes.
  43. // eslint-disable-next-line react-hooks/exhaustive-deps
  44. }, [subtree, value]);
  45. const hasInitial = Object.keys(initial).length > 0;
  46. const initialValues = nestAtPath(path, hasInitial ? initial : undefined);
  47. return (
  48. <Form
  49. form={form}
  50. component={false}
  51. colon={false}
  52. labelCol={{ sm: { span: 8 } }}
  53. wrapperCol={{ sm: { span: 14 } }}
  54. labelWrap
  55. initialValues={initialValues}
  56. >
  57. {render(form as unknown as FormInstance<OutboundFormValues>)}
  58. </Form>
  59. );
  60. }