3.5 Formulario de creación y edición (CRUD completo)

Formulario React completo para crear y editar incidencias con validación, generado a partir de un prompt de IA, con gestión de estado y operaciones CRUD en Dataverse.

El listado está listo. Ahora necesitamos el formulario que permite crear nuevas incidencias y editar las existentes. Este es el componente más complejo de la app, y aquí vamos a ver cómo manejar formularios, validación y las operaciones de escritura en Dataverse.

Objetivos de aprendizaje

  • Usar prompts precisos para que un agente de código genere un formulario complejo
  • Implementar las operaciones create y update usando el SDK de Dataverse
  • Gestionar el estado del formulario con tipos TypeScript
  • Manejar errores de escritura correctamente

 

Prompt para el Agente de Código: Formulario CRUD

De nuevo, usaremos nuestro agente de inteligencia artificial (Copilot, Cursor, etc.). Para crear un formulario robusto, el prompt debe ser muy específico sobre las reglas de negocio y los estados de carga:

/* Crea un componente React TypeScript IncidenciaForm que: - Reciba un prop incidenciaId opcional (si existe, carga y edita esa incidencia; si no, crea una nueva) - Tenga campos para: título (requerido), descripción, estado, prioridad, solicitante, notas - Valide que el título no esté vacío antes de enviar - En modo edición, cargue los datos actuales al montar el componente - Al guardar llame a CrIncidenciaService.create o CrIncidenciaService.update según corresponda - Muestre un botón "Guardar" y "Cancelar" - Llame a props.onSave() al guardar exitosamente y props.onCancel() al cancelar - Muestre feedback visual durante el guardado (loading state) - Use tipos TypeScript estrictos para el estado del formulario */

Implementación generada del formulario

Con esa instrucción, el agente de código producirá una estructura base muy sólida que puedes afinar:

// src/components/IncidenciaForm.tsx
import React, { useState, useEffect } from 'react';
import { CrIncidenciaService, CrIncidencia } from '../generated';
import { CrEstado } from '../generated/enums/CrEstado';
import { CrPrioridad } from '../generated/enums/CrPrioridad';

interface IncidenciaFormProps {
  incidenciaId?: string;  // undefined = crear nueva, string = editar existente
  onSave: () => void;
  onCancel: () => void;
}

interface FormState {
  cr_titulo: string;
  cr_descripcion: string;
  cr_estado: CrEstado;
  cr_prioridad: CrPrioridad;
  cr_solicitante: string;
  cr_notas: string;
}

const initialFormState: FormState = {
  cr_titulo: '',
  cr_descripcion: '',
  cr_estado: CrEstado.Abierta,
  cr_prioridad: CrPrioridad.Media,
  cr_solicitante: '',
  cr_notas: '',
};

export const IncidenciaForm: React.FC<IncidenciaFormProps> = ({
  incidenciaId,
  onSave,
  onCancel,
}) => {
  const [form, setForm] = useState<FormState>(initialFormState);
  const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});
  const [loading, setLoading] = useState(false);
  const [loadingData, setLoadingData] = useState(!!incidenciaId);

  const esEdicion = !!incidenciaId;

  useEffect(() => {
    if (incidenciaId) {
      cargarIncidencia(incidenciaId);
    }
  }, [incidenciaId]);

  const cargarIncidencia = async (id: string) => {
    try {
      const inc = await CrIncidenciaService.getById(id);
      setForm({
        cr_titulo: inc.cr_titulo ?? '',
        cr_descripcion: inc.cr_descripcion ?? '',
        cr_estado: inc.cr_estado ?? CrEstado.Abierta,
        cr_prioridad: inc.cr_prioridad ?? CrPrioridad.Media,
        cr_solicitante: inc.cr_solicitante ?? '',
        cr_notas: inc.cr_notas ?? '',
      });
    } catch {
      // Manejo de error
    } finally {
      setLoadingData(false);
    }
  };

  const validar = (): boolean => {
    const nuevosErrores: Partial<Record<keyof FormState, string>> = {};
    if (!form.cr_titulo.trim()) {
      nuevosErrores.cr_titulo = 'El título es obligatorio';
    }
    if (!form.cr_solicitante.trim()) {
      nuevosErrores.cr_solicitante = 'El solicitante es obligatorio';
    }
    setErrors(nuevosErrores);
    return Object.keys(nuevosErrores).length === 0;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!validar()) return;
    
    setLoading(true);
    try {
      if (esEdicion) {
        await CrIncidenciaService.update(incidenciaId!, form);
      } else {
        await CrIncidenciaService.create(form);
      }
      onSave();
    } catch {
      setErrors({ cr_titulo: 'Error al guardar. Por favor, inténtalo de nuevo.' });
    } finally {
      setLoading(false);
    }
  };

  const handleChange = (field: keyof FormState) => (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
  ) => {
    setForm(prev => ({ ...prev, [field]: e.target.value }));
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: undefined }));
    }
  };

  if (loadingData) return <div>Cargando...</div>;

  return (
    <form onSubmit={handleSubmit} className="incidencia-form">
      <h2>{esEdicion ? 'Editar incidencia' : 'Nueva incidencia'}</h2>
      
      <div className="field">
        <label htmlFor="titulo">Título *</label>
        <input
          id="titulo"
          type="text"
          value={form.cr_titulo}
          onChange={handleChange('cr_titulo')}
          className={errors.cr_titulo ? 'error' : ''}
        />
        {errors.cr_titulo && <span className="error-msg">{errors.cr_titulo}</span>}
      </div>

      <div className="field">
        <label htmlFor="solicitante">Solicitante *</label>
        <input
          id="solicitante"
          type="text"
          value={form.cr_solicitante}
          onChange={handleChange('cr_solicitante')}
          className={errors.cr_solicitante ? 'error' : ''}
        />
        {errors.cr_solicitante && <span className="error-msg">{errors.cr_solicitante}</span>}
      </div>

      <div className="fields-row">
        <div className="field">
          <label htmlFor="estado">Estado</label>
          <select id="estado" value={form.cr_estado} onChange={handleChange('cr_estado')}>
            <option value={CrEstado.Abierta}>Abierta</option>
            <option value={CrEstado.EnProgreso}>En progreso</option>
            <option value={CrEstado.Resuelta}>Resuelta</option>
            <option value={CrEstado.Cerrada}>Cerrada</option>
          </select>
        </div>
        <div className="field">
          <label htmlFor="prioridad">Prioridad</label>
          <select id="prioridad" value={form.cr_prioridad} onChange={handleChange('cr_prioridad')}>
            <option value={CrPrioridad.Baja}>Baja</option>
            <option value={CrPrioridad.Media}>Media</option>
            <option value={CrPrioridad.Alta}>Alta</option>
            <option value={CrPrioridad.Critica}>Crítica</option>
          </select>
        </div>
      </div>

      <div className="field">
        <label htmlFor="descripcion">Descripción</label>
        <textarea id="descripcion" value={form.cr_descripcion} onChange={handleChange('cr_descripcion')} rows={4} />
      </div>

      <div className="form-actions">
        <button type="button" onClick={onCancel} disabled={loading}>Cancelar</button>
        <button type="submit" className="btn-primary" disabled={loading}>
          {loading ? 'Guardando...' : 'Guardar'}
        </button>
      </div>
    </form>
  );
};

 

Consejo práctico: Nota el patrón de handleChange genérico con el tipo keyof FormState. Un buen agente de IA generará estos patrones por ti, eliminando la necesidad de crear un handler diferente para cada campo.

 

Puntos clave

  • Los prompts precisos logran componentes complejos en un solo paso.
  • Un mismo componente puede manejar creación y edición según si recibe un ID o no.
  • La validación del formulario se hace antes de llamar al servicio de Dataverse.
  • Los estados de loading previenen dobles envíos y dan feedback visual al usuario.
Inicia sesión e inscríbete para guardar tu progreso.
En este curso
¿Te ha resultado útil?