fix(web): errores de formulario accionables — fila inválida + mensajes 4xx localizados (#296) - #338
Conversation
) parseSupplyLines devolvía null sin decir qué línea fallaba, así que en el editor de inventario (GlobalEmergency#263, guardado estricto) un usuario con 20 filas solo veía "revisa el material" sin saber cuál. Ahora devuelve { items } | { invalidRow } con el índice de la primera fila inválida (-1 cuando el fallo no es de una fila concreta: JSON corrupto, raíz no-array, o lista vacía requerida). Se propaga el índice hasta la UI: InventoryField/SupplyLineList/ SupplyLineFields aceptan un invalidRowIndex opcional y resaltan esa fila (borde + aviso) en el editor de inventario. El resto de consumidores (pre-registro, petición, registrar, recepción) se adaptan a la nueva forma sin cambiar su comportamiento.
…alEmergency#296) Varias server actions (petición, registrar, voluntario, donar/ofrecer, ofrecer-transporte) mostraban error.message crudo del backend cuando no había un manejo específico — texto en inglés, a veces con UUIDs, después de que el usuario completó todo el formulario (p. ej. "Resource ... does not exist in this emergency" al enviar un resourceId inválido en /needs). Se añade src/lib/backend-error-messages.ts: mapea por patrón los mensajes de dominio 4xx conocidos (SupplyLine, need→resource, oferta→necesidad objetivo, capacidad de transporte) a copy en español, con fallback al mensaje genérico existente de cada action para cualquier error no mapeado. Las 5 actions se actualizan para usarlo en vez de reenviar error.message.
|
@nopestack is attempting to deploy a commit to the GlobalEmergency Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
vgpastor
left a comment
There was a problem hiding this comment.
¡Muy buen trabajo, currazo en las dos partes! 🙌
Parte 1 (fila inválida): el refactor de parseSupplyLines a { items } | { invalidRow } es limpio y la propagación hasta SupplyLineFields con aria-invalid/role="alert" es la forma correcta (accesible) de hacerlo. Los tests que cubren primera/media/última fila y "solo se reporta la primera inválida" están muy bien. El manejo de invalidRow: -1 (fallo no ligado a fila → mensaje genérico) es un buen detalle.
Parte 2 (4xx localizados): verifiqué los 12 patrones uno a uno contra apps/api y todos casan con un mensaje real — ningún patrón muerto. Además dejáis de filtrar texto en inglés (y UUIDs sueltos) al usuario, que era el objetivo de #296. El find/fallback es defensivo y seguro ante message no-string.
Verifiqué también la completitud del grep de las 5 server actions con reenvío crudo; cuadra con lo que describes.
Dejo dos observaciones, ninguna bloqueante:
- La localización acopla ambos lados por prosa en inglés sin contrato: riesgo de drift silencioso si el backend reescribe un
.message(comentario enbackend-error-messages.ts). Buen candidato a issue de seguimiento (uncodeestable en las excepciones sería el fix real). - Nit de UX: el resaltado de fila inválida puede desalinearse si se editan filas antes de reenviar (comentario en
inventory-edit-form.tsx).
inventario/actions.ts y inventory-edit-form.tsx en la misma función saveMyInventory — el segundo en mergear necesitará rebase (probable conflicto textual). ¡Gracias por los tests y la verificación del grep! 🚀
Generated by Claude Code
| * | ||
| * Order matters: the first matching pattern wins. | ||
| */ | ||
| const KNOWN_BACKEND_ERRORS: readonly { |
There was a problem hiding this comment.
Verifiqué los 12 patrones contra apps/api y todos casan con un mensaje real (supply-line.ts, need-errors.ts, submit-offer.ts, offer-errors.ts, transport-capacity-errors.ts, capacity-window.ts, coverage.ts) — cero patrones muertos. 👏 Y el fallback seguro para lo no mapeado está bien pensado.
La pega es estructural más que de este PR: el contrato es texto libre en inglés sin nada que enlace ambos lados. Si alguien reescribe el .message de una excepción en apps/api (o añade una nueva), aquí se degrada en silencio al genérico y ningún test lo detecta — los tests de este archivo hardcodean las mismas cadenas, así que pasan aunque el backend haya divergido. Es exactamente la fragilidad que #296 intenta tapar por el lado del cliente.
El fix "de verdad" sería un code estable en las excepciones de dominio (p.ej. NeedResourceNotInEmergencyError.code = 'resource_not_in_emergency') expuesto en el filtro, y mapear por código en vez de por prosa. Grande para este PR; lo propongo como issue de seguimiento. Como mínimo intermedio, un test que importe las clases de error reales de apps/api y afirme que su .message sigue casando cada patrón cortaría el drift silencioso.
(Menor: offer_items_required es el único mapping sin test en backend-error-messages.test.ts.)
Generated by Claude Code
| initialLines={initial.map(toLine)} | ||
| strict | ||
| allowAllCategories | ||
| invalidRowIndex={state.status === 'error' ? state.invalidRow : undefined} |
There was a problem hiding this comment.
Nit de UX (nice-to-have): invalidRow es del último submit, pero el resaltado se mantiene mientras el usuario edita. Si tras el error añade/elimina una fila por encima de la marcada sin volver a enviar, el índice queda desalineado y el borde rojo pasa a señalar otra fila (o una vacía). Se autocorrige en el siguiente submit, así que impacto bajo; si quisierais evitarlo del todo, limpiar state/el resaltado en el primer onChange tras el error lo resolvería. No para este PR.
Aparte de esto, el patrón { items } | { invalidRow } con el discriminante 'invalidRow' in ... y el paso SupplyLineList → SupplyLineFields (aria-invalid + role="alert") está muy limpio. 👍
Generated by Claude Code
) Closes #277 Sustituye a **#337** (rama de fork con conflicto irresoluble desde fuera del fork): mismo cambio, rebasado sobre `main` (que ya incluye #336 y #338). ## Contexto `AppBar` hacía sus propias llamadas `api.GET('/auth/me')` + `api.GET('/notifications/mine')` sin pasar por el cache de datos. Desde #280, `getMe()`/`getNotificationUnread()` (en `navigation-data.ts`) están envueltas en `React.cache()` y varias páginas ya las usan, así que en cualquier página que renderiza `AppBar` y también usa esos loaders, `/auth/me` se duplicaba dentro del mismo request. ## Solución `AppBar` usa `getMe()`/`getNotificationUnread()` en lugar de llamadas `api.GET` propias. Cambio mínimo (1 fichero). Se preserva el `Promise.all` (paralelismo intacto) y la lógica de `next` del login (`resolveAppBarCurrentPath`, #278/#336). ## Resolución de conflicto El conflicto era solo el bloque de imports de `app-bar.tsx`: se conserva el import mínimo de `@/lib/auth` (el dedupe elimina el uso de `api`/`getToken`/`authHeaders`) y se mantiene el import de `resolveAppBarCurrentPath`. El cuerpo integra ambos cambios (dedupe + `currentPath`). ## Crédito Cambio original: PR #337. --------- Co-authored-by: daniel <daniel@nopestack.dev> Co-authored-by: Claude <noreply@anthropic.com>
Closes #296
Parte 1 — fila inválida en
parseSupplyLinesparseSupplyLinesdevuelve ahora{ items } | { invalidRow: number }en vez deT[] | null, identificando la primera fila inválida. Se propaga hastaSupplyLineList/InventoryField(resaltado de fila) y al flujo estricto de edición de inventario (#263), con nuevo mensaje localizadoaccount.inventory_invalid_row. Los otros 4 consumidores deparseSupplyLinesse actualizaron al nuevo tipo pero mantienen su mensaje genérico (no se pidió resaltado ahí).Parte 2 — mensajes 4xx localizados
Nuevo helper
localizeBackendError(backend-error-messages.ts) que mapea mensajes de error conocidos del backend (NeedResourceNotInEmergencyError,SupplyLineValidationError, errores deoffersylogistics) a copy en español, con fallback genérico. Cableado en las 5 server actions con el patrón de reenvío crudo encontradas en todo el repo (peticion,registrar,voluntario,donar/ofrecer,ofrecer-transporte) — el issue estimaba "≥6", pero un grep completo del repo solo encontró 5 con ese patrón exacto; el resto deactions.tsbajoe/[slug]ya devuelven mensajes pre-localizados.Tests
TDD en ambas partes (tests añadidos primero).
pnpm --filter web test: 95/95. Build y lint limpios.