Skip to content

feat: [TESIS-68] add the shared modal frame, confirmation dialog and toast - #53

Merged
LauAubert merged 6 commits into
masterfrom
TESIS-68-destructive-confirm-dialog
Sep 26, 2026
Merged

LauAubert merged 6 commits into
masterfrom
TESIS-68-destructive-confirm-dialog

Conversation

@LauAubert

@LauAubert LauAubert commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

🔗 Link

https://proyectofinalfrlp.atlassian.net/browse/TESIS-68

📝 Descripción

Cierra los componentes estándar de la card: el marco de modal, el modal de confirmación para acciones destructivas y los avisos del sistema como toast, según ModalFrame.dc.html y Alert.dc.html. El formulario de alta que pedía la card ya existía (CreateProductModal, TESIS-63/65), así que se reusa y ahora se monta sobre el marco nuevo.

La motivación es que hasta ahora cada pantalla resolvía estas piezas a su manera. El marco de los modales vivía dentro de inventory. El único diálogo de confirmación era propio de la baja de productos. Los avisos mezclaban el estilo por defecto de MUI y el contorneado. Y dos pantallas tenían un Snackbar propio, con otro aspecto y otra duración que los del resto de la app.

Queda fuera del alcance la "información de autenticación requerida" del modal de borrado: el backend no tiene re-autenticación para acciones destructivas, así que es una card aparte si se quiere.

🛠️ Cambios realizados

  • Agrega ModalFrame en shared/components: cabecera con título, subtítulo, ícono opcional y cierre, y debajo ModalBody y ModalFooter, o un ModalForm que los envuelva. Tiene tres tamaños (sm 480, md 672 que es el del diseño, lg 880). Solo scrollea el cuerpo, y con busy no se cierra por ningún lado.
  • Reemplaza ProductModalShell (de features/inventory) por ModalFrame en CreateProductModal y EditProductModal. Cambio de comportamiento: el modal de alta ya no se puede cerrar mientras guarda (Esc, clic afuera o la X), igual que ya pasaba con la X del de edición.
  • Agrega ConfirmDialog sobre ModalFrame, en sm por defecto. Con tone="destructive" suma la advertencia roja, pinta el botón de confirmar en rojo y arranca con el foco en Cancelar, así un Enter apurado no borra nada. Con canConfirm={false} queda solo la salida, para cuando el backend ya rechazó la acción, y children lleva el motivo.
  • DeleteProductDialog pasa a usar ConfirmDialog y conserva el último producto mientras el diálogo se desvanece. Antes el texto desaparecía a mitad del cierre.
  • Agrega muiAlert al tema (app/theme/components/alert.ts) con el aviso del diseño: fondo tonal, borde del color de la intención, ícono y texto en onContainer. Aplica a las variantes standard y outlined. Solo la X va en gris, así que acciones como "Reintentar" conservan el color. La X queda rotulada "Cerrar" (antes decía "Close").
  • NotificationHost pasa a carpeta y muestra cada notificación como toast: el mismo aviso, flotando, con fondo opaco y sombra de nivel 2.
  • Corrige que el toast se cerrara con cualquier clic en la página: el Snackbar avisaba clickaway y se descartaba igual que la X. Ahora se va solo a los 6 segundos, con Esc o con su X.
  • Reemplaza los Snackbar propios de InventoryPage y ProductDetailPage por notify(…, 'success').
  • Agrega a /design-system la sección "Avisos del sistema y toasts" (avisos en las cuatro intenciones, con título y con acción, y botones que disparan el toast real) y la del modal de confirmación (estándar, destructiva y rechazada, en los tres tamaños).
  • Actualiza architecture.md y component-structure.md.

🧪 Cómo probarlo (Opcional)

Caso 1: baja de un producto

Precondición: sesión iniciada y al menos un producto en el catálogo.

  1. Ir a /inventory, abrir el menú de una fila y elegir "Eliminar".
  2. Ver el diálogo con la advertencia roja, el nombre y el SKU, y el foco en "Cancelar".
  3. Confirmar.
    → El diálogo se cierra sin que el texto desaparezca antes, y abajo aparece el toast verde "… eliminado.".

Caso 2: baja rechazada por el backend

Precondición: un producto con ventas o transferencias registradas.

  1. Intentar eliminarlo desde /inventory.
    → El diálogo queda abierto con el motivo en un aviso rojo, y el botón "Eliminar" desaparece.

Caso 3: el toast no se va con cualquier clic

  1. En /design-system, sección "Avisos del sistema y toasts", tocar cualquier botón de toast.
  2. Hacer clic en otro lado de la página.
    → El toast sigue visible y se va solo a los 6 segundos, o antes con su X.

Caso 4: modales de producto sobre el marco nuevo

  1. Abrir "Nuevo producto" en /inventory y "Editar" en el detalle de un producto.
  2. Guardar y, mientras guarda, intentar cerrar con Esc o con clic afuera.
    → Mientras está en vuelo no se cierra. La cabecera y el pie quedan fijos y solo scrollea el cuerpo.

📸 Evidencia (Opcional)

Avisos en línea y toast. A la izquierda, master: en oscuro los avisos casi no se leen y el toast es un bloque relleno. A la derecha, esta rama.

Antes Después
before-alerts-dark after-alerts-dark
before-alerts-light after-alerts-light

Modal de confirmación (componente nuevo, sin "antes"):

Destructiva · oscuro Rechazada por el backend · claro Estándar · claro
confirm-destructive-dark confirm-blocked-light confirm-default-light

Modal de alta sobre ModalFrame (cabecera y pie fijos, solo scrollea el cuerpo):

create-product-modal-dark

¿Afecta la arquitectura o genera un nuevo patrón?
Sí. ModalFrame y ConfirmDialog pasan a ser la forma de armar cualquier modal y cualquier confirmación, y ninguna pantalla monta un Snackbar propio: todo aviso pasa por notify(). Está documentado en architecture.md. No requiere ADR nuevo: sigue el design system de ADR-007.


LauAubert and others added 5 commits September 25, 2026 00:03
…ls onto it

ModalFrame is the ModalFrame of the design: header with title, subtitle,
optional icon and close, and below it whatever the modal brings (body and
footer, or a form wrapping them). It takes the size (sm 480, md 672, lg 880),
keeps only the body scrolling, ties the title to aria-labelledby and, with
`busy`, cannot be closed from anywhere.

It replaces ProductModalShell, which lived in the inventory feature. The
create modal can no longer be closed while it is saving, as the edit modal
already did with its close button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConfirmDialog asks before running an action, on top of ModalFrame (sm by
default). The destructive tone adds the red warning, paints the confirm button
red and starts with the focus on cancel, so a hurried Enter cannot delete
anything. `canConfirm={false}` leaves only the way out, for when the backend
already refused the action, and `children` carries the reason.

DeleteProductDialog now wraps it, and keeps the last product while the dialog
fades out: the text used to vanish and the box shrank on its way out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cations as toasts

The Alert theme override matches Alert.dc.html: tonal background, border in the
intent colour, icon and text in the onContainer tone. It applies to the
standard and outlined variants, which the app used interchangeably. Only the
close glyph goes grey, so actions like "Reintentar" keep the intent colour.
The close button is labelled in Spanish.

NotificationHost renders each notification as a toast: that same alert,
floating, with an opaque background and the level 2 shadow. It no longer
closes on a click anywhere on the page; it goes away on its own, with Escape
or with its close button.

The inventory and product detail pages dropped their own Snackbars and notify
through the shared host, like the rest of the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he design system page

Alerts in their four intents, with a title and with an action, plus buttons
that fire the real toast through notify(). The confirmation dialog shows its
standard, destructive and rejected variants in the three sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the toast

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LauAubert
LauAubert requested a review from a team as a code owner September 25, 2026 03:09
@LauAubert
LauAubert requested review from Sanntinat and removed request for a team September 25, 2026 03:09

@TomasMartin2004 TomasMartin2004 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.

Revisión — TESIS-68 (PR #53) · Marco de modal, confirmación destructiva y toasts

Revisado sobre TESIS-68-destructive-confirm-dialog contra origin/master.

Check Resultado
npm run test (local) 558 tests, 0 fallas
npm run lint · prettier --check . · npm run build Limpios

✅ Los dos arreglos de comportamiento tienen dientes

Rompí los dos, de a uno, y cayó lo que tenía que caer:

Rotura Falla
Que el toast vuelva a descartarse con clickaway stays up when the person clicks elsewhere on the page
Que el modal se pueda cerrar mientras guarda (onClose sin el guard de busy) cannot be closed while something is in flight y does not let anything fire while the action is in flight

El del toast es el que más se nota en uso real: un aviso que se va con cualquier clic desaparece justo cuando la persona mueve el mouse para seguir trabajando, y el mensaje que importaba —«producto eliminado», «no se pudo guardar»— se pierde sin que nadie lo lea. Que clickaway llegue por el mismo onClose que la X es una trampa de la API de MUI, y está bien resuelta en un solo lugar.

✅ La accesibilidad del diálogo está pensada, no copiada

  • role="alertdialog" y describedBy apuntando a la descripción: un lector de pantalla anuncia la pregunta completa, no sólo el título.
  • El foco arranca en «Cancelar» cuando la acción es destructiva. Es el detalle que evita que un Enter apurado borre un producto, y está explicado en el comentario con esas palabras.
  • El botón de confirmar desaparece cuando canConfirm es falso, en vez de quedar deshabilitado: para una baja que el backend rechazó, un botón gris que no hace nada invita a insistir.

✅ Que sea presentacional es la decisión correcta

ConfirmDialog no ejecuta nada: quien lo monta llama, marca busy y decide si cerrar o dejarlo abierto con el error. Es lo que permite el Caso 2 —la baja rechazada que deja el diálogo abierto con el motivo— sin que el componente sepa nada de productos.

✅ La consolidación era necesaria

El marco de modal vivía dentro de inventory, el único diálogo de confirmación era el de la baja de productos, y dos pantallas tenían su propio Snackbar con otra duración. Mover las tres piezas a shared/components y dejar ProductModalShell atrás es exactamente lo que la card pedía, y el cambio de comportamiento que anotás —el alta ya no se cierra mientras guarda— va en la misma dirección que el de edición.

Me gustó que la re-autenticación para acciones destructivas quede declarada como fuera de alcance con el motivo (el backend no la tiene) en vez de resuelta a medias.

🟡 Menor: NotificationHost quedó duplicado

El PR agrega src/shared/components/NotificationHost/ (carpeta) y en la lista de archivos sigue apareciendo src/shared/components/NotificationHost.tsx (el suelto). Si el viejo quedó en el árbol, conviene borrarlo: dos archivos que resuelven el nombre NotificationHost es la clase de ambigüedad que un import descuidado resuelve para el lado equivocado, y el que sobra no lo cubre ningún test.

Si ya lo borraste y lo que veo es el rename en el diff, ignorá el punto.

Veredicto

APPROVE.

Cierra los componentes estándar de la card reusando lo que ya existía, y los dos arreglos de comportamiento que trae de yapa —el toast que se iba solo y el modal que se cerraba mientras guardaba— están protegidos por ejemplos que fallan si alguien los deshace.

🤖 Generated with Claude Code

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

@TomasMartin2004

Copy link
Copy Markdown
Contributor

Ignorá el menor del final: verifiqué en la rama y src/shared/components/NotificationHost.tsx ya no existe. Lo que vi en la lista de archivos era el rename a carpeta. Nada que borrar.

🤖 Generated with Claude Code

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

@TomasMartin2004

Copy link
Copy Markdown
Contributor

Aprobado, pero no lo pude mergear: GitHub dice que el merge no se puede crear limpio. Entró TESIS-104 (web#54) mientras tanto y toca las mismas piezas.

Simulé el merge en local y son tres archivos:

src/app/theme/components/index.ts
src/features/design-system/pages/DesignSystemPage.tsx
src/features/inventory/components/ProductModalShell/ProductModalShell.styles.ts

Los tres tienen la misma forma: acá agregás una pieza (el tema del Alert, la sección nueva de la página del DS, el shell que este PR reemplaza) y allá se convirtió todo a variables CSS (theme.vars.palette).

  • En index.ts y DesignSystemPage.tsx es sumar: tu entrada nueva, con los estilos vecinos como quedaron en master.
  • ProductModalShell.styles.ts es el caso raro: master lo convirtió a variables y este PR lo borra, porque ModalFrame lo reemplaza. Ahí la resolución es quedarse con el borrado.

Un detalle para cuando lo resuelvas: ModalFrame.styles.ts y ConfirmDialog.styles.ts son archivos nuevos, así que no dan conflicto — pero conviene que lean theme.vars.palette como el resto ahora, o van a ser los únicos que horneen el hex y el toggle de tema no los va a repintar. Es exactamente el bug que TESIS-104 vino a cerrar.

Cuando esté al día lo mergeo.

🤖 Generated with Claude Code

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

Brings in TESIS-104 (web#54), which moved the theme to CSS variables and both
color schemes. Conflicts:

- theme/components/index.ts: keeps the new MuiAlert entry with the mode-less
  factories of master.
- DesignSystemPage.tsx: keeps both imports (notify and useThemeMode).
- ProductModalShell.styles.ts: master converted it to theme.vars and this
  branch deletes it, since ModalFrame replaces it. Kept the deletion.

The new styles of this branch (ModalFrame, ConfirmDialog, the toast and the
Alert override) now read theme.vars, so the theme toggle repaints them like
the rest, with tests that fail against a baked hex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LauAubert
LauAubert merged commit 01e62b2 into master Sep 26, 2026
4 checks passed
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.

2 participants