From 73edd8cd689eb4c4e849914b0a2849d83f799e25 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 6 Nov 2026 11:04:06 -0600 Subject: [PATCH] fix(ui): leave announcement placement to #19511, keep the onboarding gate The sidebar-docked placement in #19511 already keeps the card off the account menu. Drop the measured offset here so the two changes do not conflict; this PR keeps only the first-session gate. Co-Authored-By: Claude Opus 5.5 --- tests/e2e/announcement-first-session.spec.ts | 43 ++--------------- ui/src/components/AnnouncementWell.tsx | 50 +------------------- ui/src/index.css | 6 +-- 3 files changed, 7 insertions(+), 92 deletions(-) diff --git a/tests/e2e/announcement-first-session.spec.ts b/tests/e2e/announcement-first-session.spec.ts index ba048b46fc5..5c7230632d9 100644 --- a/tests/e2e/announcement-first-session.spec.ts +++ b/tests/e2e/announcement-first-session.spec.ts @@ -3,10 +3,10 @@ import { test, expect, request as pwRequest, type APIRequestContext, type Page } /** * E2E (N-13): the in-app announcement well ("Introducing Connectors" upsell) * - * 1. stays hidden while a new company's onboarding first task is open, so it - * cannot cover the onboarding questions in the user's first session; and - * 2. where it does render, never covers the sidebar account menu trigger or - * its flyout at a normal desktop size. + * stays hidden while a new company's onboarding first task is open, so it + * cannot cover the onboarding questions or the account menu in the user's + * first session. Placement (never covering the account menu once shown) is + * covered by announcement-placement.spec.ts. * * The public feed is mocked so the spec does not depend on what the live feed * is publishing; the card mirrors the real 2026-09-14-connectors entry. @@ -21,11 +21,6 @@ const ONE_PIXEL_PNG = Buffer.from( "base64", ); -type Box = { x: number; y: number; width: number; height: number }; -function intersects(a: Box, b: Box) { - return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height; -} - async function createCompany(board: APIRequestContext, name: string) { const res = await board.post(`${BASE_URL}/api/companies`, { data: { name: `${name}-${Date.now()}` } }); if (!res.ok()) throw new Error(`POST /api/companies → ${res.status()}: ${await res.text()}`); @@ -98,34 +93,4 @@ test.describe("Announcement well (N-13)", () => { await page.screenshot({ path: test.info().outputPath("onboarding-task.png") }); await expect(page.getByRole("complementary", { name: "Paperclip announcements" })).toHaveCount(0); }); - - test("where the upsell shows, it does not cover the account menu or its flyout", async ({ page }) => { - const company = await createCompany(board, "E2E-N13-Established"); - companyIds.push(company.id); - await createIssue(board, company.id, { title: "Ordinary task", status: "todo" }); - - await page.goto(`/${company.prefix}/dashboard`); - const well = page.getByRole("complementary", { name: "Paperclip announcements" }); - await expect(well).toBeVisible({ timeout: 15_000 }); - await expect(well.getByRole("link", { name: "Explore connectors" })).toBeVisible(); - await page.waitForTimeout(300); // Let the entry animation finish before measuring. - await page.screenshot({ path: test.info().outputPath("established-company.png") }); - - const trigger = page.getByRole("button", { name: "Open account menu" }); - const wellBox = await well.boundingBox(); - const triggerBox = await trigger.boundingBox(); - expect(wellBox).not.toBeNull(); - expect(triggerBox).not.toBeNull(); - expect(intersects(wellBox!, triggerBox!), `well ${JSON.stringify(wellBox)} covers account trigger ${JSON.stringify(triggerBox)}`).toBe(false); - - // A real click lands on the trigger (Playwright fails if another element would receive it). - await trigger.click({ timeout: 5_000 }); - const flyout = page.getByRole("dialog", { name: "Account" }); - await expect(flyout).toBeVisible(); - const flyoutBox = await flyout.boundingBox(); - if (await well.isVisible()) { - const openWellBox = await well.boundingBox(); - expect(intersects(openWellBox!, flyoutBox!), "well covers the open account flyout").toBe(false); - } - }); }); diff --git a/ui/src/components/AnnouncementWell.tsx b/ui/src/components/AnnouncementWell.tsx index 60488e60592..d5d263539a0 100644 --- a/ui/src/components/AnnouncementWell.tsx +++ b/ui/src/components/AnnouncementWell.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, type CSSProperties } from "react"; +import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { ONBOARDING_FIRST_TASK_ORIGIN_KIND } from "@paperclipai/shared"; import { useAccountIdentity } from "@/api/companies-query"; @@ -45,47 +45,6 @@ function useOnboardingInProgress(companyId: string | null) { return query.data.some((issue) => issue.originKind === ONBOARDING_FIRST_TASK_ORIGIN_KIND && !FINISHED_ONBOARDING_STATUSES.has(issue.status)); } -const ACCOUNT_ROW_SELECTOR = "[data-sidebar-account]"; -/** - * Distance in CSS pixels from the viewport bottom to the top of the visible - * sidebar account row, so the desktop well can sit above the account menu - * trigger instead of covering it. Zero when no account row is on screen. - */ -function useAccountRowClearance(enabled: boolean) { - const [clearance, setClearance] = useState(0); - useEffect(() => { - if (!enabled) return; - let frame = 0; - const observed = new Set(); - const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => schedule()); - function measure() { - let next = 0; - for (const row of document.querySelectorAll(ACCOUNT_ROW_SELECTOR)) { - if (resizeObserver && !observed.has(row)) { observed.add(row); resizeObserver.observe(row); } - const rect = row.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0 || rect.right <= 0 || rect.left >= window.innerWidth) continue; - next = Math.max(next, Math.ceil(window.innerHeight - rect.top)); - } - setClearance(next); - } - function schedule() { - cancelAnimationFrame(frame); - frame = requestAnimationFrame(measure); - } - measure(); - const mutationObserver = new MutationObserver(schedule); - mutationObserver.observe(document.body, { childList: true, subtree: true }); - window.addEventListener("resize", schedule); - return () => { - cancelAnimationFrame(frame); - mutationObserver.disconnect(); - resizeObserver?.disconnect(); - window.removeEventListener("resize", schedule); - }; - }, [enabled]); - return clearance; -} - export function AnnouncementWell({ health }: { health?: HealthStatus }) { const { userId: accountId, settled } = useAccountIdentity(); const { selectedCompanyId, loading } = useCompany(); @@ -104,14 +63,9 @@ export function AnnouncementWell({ health }: { health?: HealthStatus }) { }), }); const modalOpen = useModalOpen(Boolean(announcement)); - const accountClearance = useAccountRowClearance(Boolean(announcement)); if (!announcement || modalOpen || (toasts?.length ?? 0) > 0) return null; return ( -