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>
);
};
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.