Skip to content

fix(web): errores de formulario accionables — fila inválida + mensajes 4xx localizados (#296) - #338

Merged
vgpastor merged 3 commits into
GlobalEmergency:mainfrom
nopestack:fix/296-form-error-localization
Jul 6, 2026
Merged

fix(web): errores de formulario accionables — fila inválida + mensajes 4xx localizados (#296)#338
vgpastor merged 3 commits into
GlobalEmergency:mainfrom
nopestack:fix/296-form-error-localization

Conversation

@nopestack

Copy link
Copy Markdown
Contributor

Closes #296

Parte 1 — fila inválida en parseSupplyLines

parseSupplyLines devuelve ahora { items } | { invalidRow: number } en vez de T[] | null, identificando la primera fila inválida. Se propaga hasta SupplyLineList/InventoryField (resaltado de fila) y al flujo estricto de edición de inventario (#263), con nuevo mensaje localizado account.inventory_invalid_row. Los otros 4 consumidores de parseSupplyLines se 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 de offers y logistics) 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 de actions.ts bajo e/[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.

nopestack added 3 commits July 6, 2026 09:54
)

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
nopestack requested a review from vgpastor as a code owner July 6, 2026 08:38
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

@nopestack is attempting to deploy a commit to the GlobalEmergency Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
response-grid Ready Ready Preview, Comment Jul 6, 2026 10:12am

Request Review

@vgpastor vgpastor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

¡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:

  1. La localización acopla ambos lados por prosa en inglés sin contrato: riesgo de drift silencioso si el backend reescribe un .message (comentario en backend-error-messages.ts). Buen candidato a issue de seguimiento (un code estable en las excepciones sería el fix real).
  2. Nit de UX: el resaltado de fila inválida puede desalinearse si se editan filas antes de reenviar (comentario en inventory-edit-form.tsx).

⚠️ Heads-up de coordinación (no de este PR): #338 y el #335 abierto tocan ambos 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@vgpastor
vgpastor merged commit 340d97e into GlobalEmergency:main Jul 6, 2026
13 checks passed
vgpastor added a commit that referenced this pull request Jul 6, 2026
)

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Web][UX] Errores de formulario accionables: señalar la línea de material inválida y localizar los mensajes 4xx del backend

2 participants