From 375d4f1060a5caa3718f97266f03f15e3a70c22a Mon Sep 17 00:00:00 2001 From: Alejandro Oroncoy Date: Tue, 10 Nov 2026 13:13:54 -0500 Subject: [PATCH] Novedades: un aviso de cambios abre con el cambio primero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El correo de cambios lleva a /app/novedades//. Con cambios concretos la página mostraba el detalle completo, y el cambio quedaba en el quinto bloque, debajo de todos los ajustes: la persona tocaba ver y encontraba configuración. Ahora esa ruta también abre la vista simple, con el cambio en una frase (Cambió la retroalimentación de X en Y) y su tarjeta arriba, antes de lo que viene. Los ajustes y el detalle siguen detrás de Ajustes y detalle, y el cambio no se duplica al abrirlos. Co-Authored-By: Claude Sonnet 5.5 --- site/scripts/proactive-e2e.mjs | 17 +++++ site/src/components/CampuUpdates.tsx | 94 ++++++++++++++++++---------- 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/site/scripts/proactive-e2e.mjs b/site/scripts/proactive-e2e.mjs index 7eb9cd3f..2a4eefef 100644 --- a/site/scripts/proactive-e2e.mjs +++ b/site/scripts/proactive-e2e.mjs @@ -411,6 +411,21 @@ try { if (await boton.count()) await boton.click(); }; await page.goto(base + "/app/novedades/" + noticeId + "/"); + // Un aviso de cambios abre con el cambio primero, no con los ajustes. + const cambios = page.locator(".campu-home .campu-changes"); + await cambios.waitFor(); + const titularCambio = (await page.locator(".campu-home h1").innerText()).replace(/\s+/g, " "); + assert.match(titularCambio, /^(Encontré \d+ cambios en tus cursos\.|Cambió |Publicaron |Ya no aparece|Hay un anuncio)/, titularCambio); + assert.equal(await page.getByRole("heading", { name: "Cómo quieres que te avise" }).count(), 0); + assert.match(await cambios.innerText(), /nota oficial en mi upc/i, "el cambio se ve sin abrir nada"); + // Y el orden: el cambio va antes que lo que viene. + const orden = await page.evaluate(() => { + const q = (s) => document.querySelector(s); + const c = q(".campu-home .campu-changes"), w = q(".campu-home .campu-week"); + return !w || Boolean(c.compareDocumentPosition(w) & Node.DOCUMENT_POSITION_FOLLOWING); + }); + assert.equal(orden, true, "el cambio va primero"); + await openDetail(); await page .getByRole("heading", { name: "Esto es lo que pude revisar." }) .waitFor(); @@ -575,6 +590,8 @@ try { ), session, ); + // Con el aviso ya abierto, la vista pasa a la simple: el detalle se abre después. + await page.locator(".campu-home").waitFor(); await openDetail(); await page .getByRole("heading", { name: "Esto es lo que pude revisar." }) diff --git a/site/src/components/CampuUpdates.tsx b/site/src/components/CampuUpdates.tsx index ee00bda0..6090f3de 100644 --- a/site/src/components/CampuUpdates.tsx +++ b/site/src/components/CampuUpdates.tsx @@ -503,10 +503,65 @@ function DeliveryCard({ ); } +/** Lo que cambió en este aviso, con su valor anterior cuando lo hay. Va primero: es lo que + * la persona vino a ver al tocar el correo. */ +function ChangesCard({ events }: { events: any[] }) { + if (!events.length) return null; + return ( +
+

Cambios de este aviso

+ {events.map((e) => ( +
+ + {e.meaning === "grade_published" && ( +

Antes no tenía nota registrada.

+ )} + {typeof e.before?.weight === "number" && + e.before.weight !== e.fact.weight && ( +

Peso anterior registrado: {e.before.weight}%

+ )} + {typeof e.before?.score === "number" && ( +

+ Nota anterior registrada: {e.before.score} /{" "} + {e.before.maxScore ?? "máximo no especificado"} +

+ )} + {e.before?.dueAt && e.before.dueAt !== e.fact.dueAt && ( +

Vencimiento anterior registrado: {timestamp(e.before.dueAt)}

+ )} +
+ ))} +
+ ); +} +/** El cambio en una frase, sin jerga: qué cambió y dónde. */ +function changeSentence(events: any[]) { + if (events.length !== 1) return `Encontré ${events.length} cambios en tus cursos.`; + const f = events[0].fact; + const what = + events[0].meaning === "removed" + ? "Ya no aparece" + : events[0].meaning === "grade_published" + ? "Publicaron una nota de" + : f.kind === "feedback" + ? "Cambió la retroalimentación de" + : f.kind === "grade" + ? "Cambió la nota de" + : f.kind === "assignment" + ? "Cambió la tarea" + : f.kind === "announcement" + ? "Hay un anuncio nuevo:" + : f.kind === "procedure" + ? "Cambió el trámite" + : "Cambió"; + return `${what} ${f.title || "algo"} en ${f.course}.`; +} + /** Lo primero que se ve: una frase, lo próximo y lo nuevo. Todo lo demás está en * "Ajustes y detalle". Pocas palabras, una cosa por línea. */ function Home({ context, + events, notices, emailOn, checkedAt, @@ -515,6 +570,7 @@ function Home({ onMore, }: { context: Context | null; + events: any[]; notices: Notice[]; emailOn: boolean; checkedAt?: string; @@ -564,10 +620,11 @@ function Home({ aria-hidden="true" />
-

{u ? sentence(pending, u) : "Reviso tus cursos por ti."}

+

{events.length ? changeSentence(events) : u ? sentence(pending, u) : "Reviso tus cursos por ti."}

Última revisión: {timestamp(checkedAt)}

+ {u && (

Esta semana

@@ -832,7 +889,6 @@ export default function CampuUpdates() { status?.available && prefs.enabled && ["active", "partial"].includes(status.status) && - !events.length && (!onNotice || opened), ); const showDetail = !simple || more; @@ -911,6 +967,7 @@ export default function CampuUpdates() { {simple && (
- {!!events.length && ( -
-

Cambios de este aviso

- {events.map((e) => ( -
- - {e.meaning === "grade_published" && ( -

Antes no tenía nota registrada.

- )} - {typeof e.before?.weight === "number" && - e.before.weight !== e.fact.weight && ( -

Peso anterior registrado: {e.before.weight}%

- )} - {typeof e.before?.score === "number" && ( -

- Nota anterior registrada: {e.before.score} /{" "} - {e.before.maxScore ?? "máximo no especificado"} -

- )} - {e.before?.dueAt && e.before.dueAt !== e.fact.dueAt && ( -

- Vencimiento anterior registrado:{" "} - {timestamp(e.before.dueAt)} -

- )} -
- ))} -
- )} + {!simple && } {context?.upcoming && ( )}