---
name: refactor-form-to-conventions
description: Refactor a form component to follow the project's form conventions. Split into constants, fields, schema, utils. Migrate to Zod. Use when asked to "refactorizar formulario", "organizar form", or when a form doesn't follow conventions.
---

# Skill: Refactor form to match conventions

When the user asks to refactor a form component, follow this procedure to align it with the form conventions defined in `.cursor/rules/file-types/forms.mdc`.

## When to use

- User asks to "refactorizar" or "organizar" a form component
- Form has validation logic inline or uses Yup instead of Zod
- Form has mixed concerns in a single file (IDs, labels, validation, utils)
- Form's `index.tsx` is too large (400+ lines)

## Target structure

A properly structured form should have:

```
FormName/
├── index.tsx       # Component entry (ideally <400 lines)
├── types.ts        # Form-specific types
├── constants.ts    # Field IDs as constants
├── schemas.ts      # Zod validation schema with zodResolver
├── fields.ts       # (optional) Labels, placeholders, translated options, UI props
└── utils/
    ├── values.ts        # getDefaultValues()
    ├── transforms.ts    # form ↔ API conversions
    ├── validations.ts   # custom refinements (optional)
    └── fields.ts        # dynamic/conditional field arrays (optional)
```

See `.cursor/rules/file-types/forms.mdc` §1 for the canonical structure.

## Steps

### 1. Analyze current structure

Read the form's files and identify:

| File | What to look for |
|------|------------------|
| `index.tsx` | Inline logic, hardcoded props, inline selectors |
| `constants.ts` | Mixed with labels/translations (should be only IDs) |
| `utils.ts` | Validation schema (should be in `schemas.ts`) |
| Validation | Using Yup instead of Zod |

### 2. Create/update `constants.ts`

Should contain **only** field IDs as a typed object:

```typescript
import { type MyFormValues } from './types';

export const myFormFields: {
  [key in keyof Required<MyFormValues>]: keyof MyFormValues;
} = {
  id: 'id',
  name: 'name',
  hasCoverImage: 'hasCoverImage',
  // ... all fields
};
```

### 3. Create `fields.ts`

Move all translated labels, placeholders, options, and UI props here:

```typescript
import { type TCustomFunction } from 'src/utils/i18n';

export type MyFormFieldsOptions = {
  disabled: boolean;
};

export const getMyFormFields = (
  t: TCustomFunction,
  options: MyFormFieldsOptions,
) => {
  const { disabled } = options;

  // Translated options
  const frequencyTypeOptions = frequencyTypeSelector.map(option => ({
    ...option,
    label: t(option.label),
  }));

  // Uploader props
  const coverImageUploaderProps = {
    disabled,
    maxFiles: 1,
    // ...
  };

  return {
    name: {
      name: 'name' as const,
      label: t('namespace:path.to.label'),
      placeholder: t('namespace:path.to.placeholder'),
    },
    coverImage: {
      name: 'coverImage' as const,
      uploaderProps: coverImageUploaderProps,
    },
    frequencyTypeOptions,
    reminderChannelOptions,
    // ... all fields with their props
  };
};
```

### 4. Create `schemas.ts` with Zod

Migrate from Yup to Zod. Create a factory function that receives `t()`:

```typescript
import { z } from 'zod';
import { type TCustomFunction } from 'src/utils/i18n';

export function createMyFormSchema(t: TCustomFunction) {
  return z
    .object({
      name: z
        .string()
        .min(1, t('validations:required_field')),
      hasDueDate: z.boolean().default(false),
      expiresAfter: z.union([z.number(), z.null()]).optional().nullable(),
      // ... all fields
    })
    .superRefine((data, ctx) => {
      // Conditional validations
      if (data.hasDueDate && !data.expiresAfter) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ['expiresAfter'],
          message: t('validations:required_field'),
        });
      }
    });
}
```

### 5. Create/update `utils.ts`

Should contain `getDefaultValues()` and helper functions:

```typescript
import { type TCustomFunction } from 'src/utils/i18n';
import { type MyFormValues } from './types';

export function getDefaultValues(
  defaultValues: Partial<MyFormValues>,
  t: TCustomFunction,
): MyFormValues {
  return {
    name: defaultValues?.name || '',
    hasDueDate: defaultValues?.hasDueDate || false,
    // ... all defaults
  };
}
```

### 6. Update `index.tsx`

Update imports and usage:

```typescript
import { zodResolver } from '@hookform/resolvers/zod';
import { getMyFormFields, getSwitcherCardProps } from './fields';
import { createMyFormSchema } from './schema';
import { getDefaultValues } from './utils';

const MyForm = ({ disabled, ... }) => {
  const { t } = useLokaliseTranslation(['namespace', 'validations']);

  const form = useForm({
    defaultValues,
    resolver: zodResolver(createMyFormSchema(t)),
  });

  const formFields = useMemo(
    () => getMyFormFields(t, { disabled }),
    [t, disabled],
  );

  return (
    // Use formFields.fieldName.label, formFields.fieldName.uploaderProps, etc.
  );
};
```

### 7. Clean up

- Remove inline functions like `getFrequencyTypeSelector()`
- Remove unused imports (`megabytesToBytes`, `uploadAsset`, etc. now in `fields.ts`)
- Remove empty folders (`components/`, `contexts/` if unused)
- Verify no linter errors

### 8. Extract form sections (when needed)

When a form grows beyond 400 lines or has multiple distinct visual sections, consider extracting them into section components.

#### When to extract

| Signal | Action |
|--------|--------|
| `index.tsx` > 400 lines | Consider extracting sections |
| JSX blocks > 30-40 lines forming a logical unit | Candidate for section |
| Same visual pattern repeats (e.g., SwitcherCard + content) | Extract with configurable props |
| A section has its own logic/hooks | Encapsulate in component |

#### Section component structure

```typescript
// types.ts (local to form or shared if reused)
export type MySectionFields = {
  toggleField: {
    name: string;
    label: string;
    description?: string;
  };
  inputField: {
    name: string;
    label: string;
    placeholder?: string;
  };
};

export type MySectionProps = {
  disabled: boolean;
  fields: MySectionFields;
};
```

```typescript
// MySection/index.tsx
export const MySection = ({ disabled, fields }: MySectionProps) => {
  return (
    <FormSwitcherCard
      name={fields.toggleField.name}
      switcherCardProps={{
        disabled,
        title: fields.toggleField.label,
        description: fields.toggleField.description,
      }}
    >
      <FormInputClassic
        name={fields.inputField.name}
        inputProps={{
          disabled,
          label: fields.inputField.label,
          placeholder: fields.inputField.placeholder,
        }}
      />
    </FormSwitcherCard>
  );
};
```

#### Consuming in the form

```typescript
// index.tsx
<MySection
  disabled={disabled}
  fields={{
    toggleField: formFields.hasFeature,
    inputField: formFields.featureValue,
  }}
/>
```

#### Where to place sections

| Case | Location |
|------|----------|
| Only this form uses it | `FormName/components/MySection/` |
| Multiple forms in same module share it | `forms/shared/MySection/` (module level) |
| Common pattern across the app | `src/components/FormSections/` |

#### Principles

1. **Typed props, not hardcoded** — Section receives `fields` with the exact structure it needs
2. **`disabled` always as prop** — Allows control from parent form
3. **Internal hooks when applicable** — If section needs data fetching, the hook lives inside
4. **Labels from `fields.ts`** — Section doesn't translate, receives already-translated strings

#### When NOT to extract

- **Only one form uses it** — Keep it inline or in `FormName/components/` until reuse is needed
- **Simple fields** — A single input or checkbox doesn't need its own section component
- **No clear boundary** — If the "section" is tightly coupled to form logic (watches, setError), keep it inline
- **Premature abstraction** — Don't create empty `components/` folders "just in case"
- **< 20 lines of JSX** — The overhead of a separate file isn't worth it

### 9. Consolidate mutation logic

When using `useMutation` for form submission, move transformation logic **inside** the `mutationFn`. This ensures any error (transformation or API) is captured by `onError` and shows feedback to the user.

```typescript
// ❌ Bad: transformation outside mutation
const handleSubmit = async (values) => {
  const [error, data] = await catchError(transformValues(values));
  if (error) throw error; // Error not caught by mutation.onError
  await mutation.mutateAsync(data);
};

// ✅ Good: transformation inside mutationFn
const mutation = useMutation({
  mutationFn: async (values: FormValues) => {
    const transformed = transformFormToRequest(values);
    return updateOrCreateAction(transformed);
  },
  onError: () => {
    enqueueSnackbar({ variant: 'error', title: t('error_message') });
  },
  onSuccess: () => {
    enqueueSnackbar({ variant: 'success', title: t('success_message') });
  },
});

const handleSubmit = (values: FormValues) => mutation.mutateAsync(values);
```

**Benefits:**
- Single error handling path via `onError`
- Cleaner `handleSubmit` function
- Consistent user feedback for all failure modes

### 10. Document types with JSDoc

Use JSDoc comments in `types.ts` instead of documenting types in README files. This keeps documentation close to the code and prevents it from getting out of sync.

```typescript
/**
 * Props for MyFormProvider component.
 *
 * @example
 * <MyFormProvider
 *   processId={processId}
 *   onSuccess={handleSuccess}
 *   onCancel={handleClose}
 * />
 */
export type MyFormProps = {
  /** HTML form ID for external submit button binding. */
  formId?: string;
  /** Parent process ID. */
  processId: string;
  /** Called on successful submission. */
  onSuccess?: () => void;
  /** Called when form is cancelled. */
  onCancel?: () => void;
  /** Disables all form interactions. */
  disabled?: boolean;
};
```

**Guidelines:**
- Add JSDoc to exported types, especially props and form values
- Include `@example` for component props showing typical usage
- Use inline `/** comment */` for individual fields
- Keep README files focused on architecture, data flow, and usage examples
- Avoid duplicating field documentation in README (it gets stale)

### 11. Separation: Drawer, Value provider, and Content (specific cases)

When the form is used inside a drawer (or similar container) and depends on asynchronously loaded data, split responsibilities into three root-level files:

| File | Responsibility |
|------|----------------|
| **provider.tsx** | Data loading (e.g. `useGetFormDefaultValues`), loading/error state, then render the form with resolved `defaultValues`. Default entry point for the module. |
| **form.tsx** | Form shell: `useForm`, `FormProvider`, container chrome (e.g. `Drawer.Header`, `Drawer.Body`, `Drawer.Actions`), submit/cancel and confirm logic. Receives `defaultValues` already loaded. |
| **content.tsx** | Pure form content: fields and sections only, using `useFormContext`. No drawer, no data loading; reusable in another context (e.g. full page or preview). |

**Resulting structure:**

```
FormName/
├── index.tsx       # export { FormName } from './form'; export { default } from './provider';
├── form.tsx        # Form shell (useForm, Drawer.*, import Content from './content')
├── content.tsx     # Pure content (useFormContext, sections only)
├── provider.tsx    # Data loading, then <FormName defaultValues={...} />
├── types.ts
├── fields.ts
├── ...
```

**When to use:**
- Form is used inside a drawer (or modal) and default values come from a hook (processId, actionId, etc.).
- Same form content may be reused elsewhere (e.g. preview or full page) without duplicating drawer or loading logic.

**When not to use:**
- Simple form with no async default values.
- Form is not shown in a drawer or in multiple contexts.

## Checklist

After refactoring, verify:

- [ ] `constants.ts` has only field IDs
- [ ] `fields.ts` has all labels, props, translated options
- [ ] `schemas.ts` uses Zod with `createMyFormSchema(t)` factory
- [ ] `utils.ts` has `getDefaultValues()`
- [ ] `index.tsx` uses `zodResolver(createMyFormSchema(t))`
- [ ] `index.tsx` uses `useMemo` for `formFields`
- [ ] No inline selector functions in component
- [ ] Uploader props come from `formFields`
- [ ] No linter errors
- [ ] If `index.tsx` > 400 lines, sections extracted to components
- [ ] Section components receive typed `fields` props (not hardcoded labels)
- [ ] Transformation logic inside `mutationFn` (not in handleSubmit)
- [ ] Types documented with JSDoc in `types.ts` (not in README)

## Reference implementation

See `CustomTaskForm` as the reference implementation:

```
src/pages/dashboard/EmployeeLifecycle/Process/components/forms/CustomTaskForm/
├── index.tsx       # 469 lines
├── types.ts
├── constants.ts    # Field IDs only
├── fields.ts       # Labels, props, options
├── schemas.ts      # Zod schema
├── transforms.ts   # Form ↔ API conversions
├── utils.ts        # getDefaultValues
├── validations.ts  # Custom refinements
└── components/     # Subcomponents
```
