From e5422b1827940c6c0fc4b376df208eb7f663144c Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 13 Apr 2026 11:36:16 -0300 Subject: [PATCH 1/3] docs(manager): brainstorm report filter restoration --- ...er-report-filter-restoration-brainstorm.md | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/brainstorms/2026-04-13-manager-report-filter-restoration-brainstorm.md diff --git a/docs/brainstorms/2026-04-13-manager-report-filter-restoration-brainstorm.md b/docs/brainstorms/2026-04-13-manager-report-filter-restoration-brainstorm.md new file mode 100644 index 000000000..7b3d99483 --- /dev/null +++ b/docs/brainstorms/2026-04-13-manager-report-filter-restoration-brainstorm.md @@ -0,0 +1,118 @@ +--- +date: 2026-04-13 +topic: manager-report-filter-restoration +related: + - docs/roadmap/media-generation/feat-030-video-content-discovery-dashboard.md + - docs/roadmap/media-generation/feat-084-manager-agents-automations.md + - docs/roadmap/media-generation/feat-084-enrich-now-feedback.md + - docs/solutions/integration-issues/manager-coverage-language-first-empty-state-20260410.md + - docs/solutions/ui-bugs/manager-enrich-now-feedback-handoff-20260413.md + - docs/solutions/integration-issues/manager-coverage-dashboard-review-regression-cleanup.md +--- + +# Manager Report Filter Restoration + +## What We're Building + +When an operator opens the Manager Report coverage page with URL-backed filters +selected, such as `/dashboard/coverage?languageId=529`, then moves to Jobs or +Agents and returns to Report, the Report page should reopen with the same +selection instead of dropping back to `/dashboard/coverage`. + +The important behavior is continuity: Report is the operator's working context, +and Jobs or Agents are supporting screens. A quick check of job progress or +automation settings should not make the operator rebuild the language selection +they already made. + +## Requirements + +- R1. Starting from `/dashboard/coverage?languageId=529`, navigating to Jobs and + then returning to Report restores `/dashboard/coverage?languageId=529`. +- R2. Starting from `/dashboard/coverage?languageId=529`, navigating to Agents + and then returning to Report restores `/dashboard/coverage?languageId=529`. +- R3. The behavior applies to URL-backed Report filters, with `languageId` as + the canonical current example. +- R4. Legacy `languageIds` links should still be accepted, but return paths + should prefer the canonical `languageId` shape where the route is rewritten. +- R5. Direct navigation to `/dashboard/coverage` without a prior URL-backed + Report context still opens the default language-first Report state. +- R6. Clearing the Report language selection should clear the carried Report + filter context rather than keeping a stale language active. + +## Why This Approach + +The recommended approach is to preserve URL-backed Report state through the +Manager dashboard navigation and Report-originated handoffs. This keeps the +selection visible in the URL, matches the existing coverage filter contract, and +avoids inventing a hidden cross-page state store for a URL-shaped problem. + +We considered three options: + +- Carry the Report query through dashboard navigation. This is transparent, + matches the user's URL example, and keeps Jobs and Agents as temporary stops + without losing the Report context. +- Remember the last Report URL in browser session storage. This would avoid + query strings on Jobs and Agents, but it would make the return behavior hidden + and easier to confuse with direct links. +- Promote every local Report control to URL state. This could eventually cover + search text, collection type, coverage segment, and report type, but it is + broader than the reported bug. V1 should only preserve filters that are + already URL-backed or intentionally promoted during planning. + +## Key Decisions + +- Preserve URL-backed Report selections across Report -> Jobs -> Report and + Report -> Agents -> Report loops. +- Treat `languageId` as the canonical query key while continuing to accept + legacy `languageIds` input. +- Keep the behavior scoped to Manager dashboard navigation and Report-originated + handoffs; do not change CMS coverage queries or coverage aggregation. +- Do not persist unrelated local UI controls unless planning explicitly decides + to make them URL-backed filters. +- Let explicit URLs win. If the user opens `/dashboard/coverage` directly, that + should remain the default state unless they arrived through a carried Report + context. + +## Success Criteria + +- From `/dashboard/coverage?languageId=529`, click Jobs, then click Report: the + language selection for `529` is restored and the URL includes `languageId=529`. +- From `/dashboard/coverage?languageId=529`, click Agents, then click Report: the + language selection for `529` is restored and the URL includes `languageId=529`. +- From `/dashboard/coverage?languageIds=529,21028`, the Report selection still + loads, and any rewritten return URL uses `languageId=529%2C21028` or the + project-standard equivalent. +- From `/dashboard/coverage`, Report still opens the language-first empty state. +- Removing the selected language and leaving Report does not revive a stale + language when returning. + +## Scope Boundaries + +- This is a Manager UI navigation-state fix, not a CMS data or coverage-query + change. +- Do not redesign the Report, Jobs, or Agents screens. +- Do not add global app state for every dashboard filter. +- Do not change the coverage API request shape beyond whatever is needed to + preserve the existing URL-backed filter contract. + +## Resolved Questions + +- The first filter to preserve is the existing language selection represented by + `languageId`. +- The route should remain shareable and understandable by keeping the selection + visible in URL query parameters. +- Existing query normalization in `language-selection.ts` is the contract to + respect during planning. +- Jobs and Agents can ignore the carried Report query if they do not need it; + the user-facing requirement is that returning to Report restores the selection. + +## Open Questions + +No product-level blockers remain for planning. + +## Next Steps + +Proceed to `/workflows:plan` for a narrow Manager navigation-state fix. The +likely entry points are `apps/manager/src/features/nav/dashboard-nav.tsx`, +`apps/manager/src/features/coverage/language-selection.ts`, and the existing +Coverage-to-Jobs handoff paths in `apps/manager/src/features/coverage/`. From 930b63a357936bd3e3d1d4bcd18d6841e678b2db Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 13 Apr 2026 11:58:35 -0300 Subject: [PATCH 2/3] docs(manager): plan report filter restoration --- ...-manager-report-filter-restoration-plan.md | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 docs/plans/2026-04-13-fix-manager-report-filter-restoration-plan.md diff --git a/docs/plans/2026-04-13-fix-manager-report-filter-restoration-plan.md b/docs/plans/2026-04-13-fix-manager-report-filter-restoration-plan.md new file mode 100644 index 000000000..2b431e0e7 --- /dev/null +++ b/docs/plans/2026-04-13-fix-manager-report-filter-restoration-plan.md @@ -0,0 +1,397 @@ +--- +title: "fix: Manager Report filter restoration" +type: fix +status: active +date: 2026-04-13 +branch: fix/manager-report-filter-restore +origin: + - docs/brainstorms/2026-04-13-manager-report-filter-restoration-brainstorm.md +related_roadmap: + - docs/roadmap/media-generation/feat-030-video-content-discovery-dashboard.md + - docs/roadmap/media-generation/feat-084-manager-agents-automations.md +related_docs: + - docs/solutions/integration-issues/manager-coverage-language-first-empty-state-20260410.md + - docs/solutions/integration-issues/manager-coverage-dashboard-review-regression-cleanup.md + - docs/solutions/ui-bugs/manager-enrich-now-feedback-handoff-20260413.md +--- + +# fix: Manager Report filter restoration + +## Overview + +Restore URL-backed Manager Report context when operators move between the +dashboard tabs. Starting at `/dashboard/coverage?languageId=529`, clicking Jobs +or Agents and then returning to Report should reopen the same language-filtered +Report selection instead of the default `/dashboard/coverage` language-first +state. + +This is a narrow Manager navigation-state fix. The coverage data model, CMS +queries, report rendering, and local-only Report controls should stay unchanged. + +## Found Brainstorm + +Found brainstorm from 2026-04-13: `manager-report-filter-restoration`. It +resolves the product direction: + +- Preserve URL-backed Report selections across Report -> Jobs -> Report and + Report -> Agents -> Report loops. +- Treat `languageId` as the canonical query key while continuing to accept + legacy `languageIds` input. +- Keep the behavior scoped to Manager dashboard navigation and Report-originated + handoffs. +- Direct `/dashboard/coverage` should still open the default language-first + state. +- Clearing the Report language selection should clear the carried context. + +## Requirements Trace + +- R1. Starting from `/dashboard/coverage?languageId=529`, navigating to Jobs and + then returning to Report restores `/dashboard/coverage?languageId=529`. +- R2. Starting from `/dashboard/coverage?languageId=529`, navigating to Agents + and then returning to Report restores `/dashboard/coverage?languageId=529`. +- R3. Preserve URL-backed Report filters; for V1 this means the existing + language filter. +- R4. Accept legacy `languageIds` links, but rewrite carried return paths to the + canonical `languageId` shape. +- R5. Direct navigation to `/dashboard/coverage` without a language query keeps + the current default language-first Report state. +- R6. Clearing the Report language selection clears the carried context rather + than reviving a stale language. +- R7. Use Red/Green TDD before implementation and record failing-first evidence. +- R8. Complete a user-like browser smoke test before PR handoff. + +## Research Summary + +Local research found strong existing patterns, so external research was skipped. +This fix does not introduce a new dependency, payment/security flow, privacy +concern, or unfamiliar framework surface. + +Relevant repo conventions: + +- Branch naming follows `fix/description`; current branch is + `fix/manager-report-filter-restore`. +- PR titles use `type(scope): description`, target `main`, and are squash-merged. +- Never use `--no-verify`. +- Manager tests use Vitest in a Node environment with colocated `.test.ts` + files; browser/user smoke tests are captured separately. +- Manager UI changes should reuse existing styles and colors. + +Relevant entry points: + +- `apps/manager/src/features/nav/dashboard-nav.tsx` currently hardcodes bare + tab links to `/dashboard/coverage`, `/dashboard/jobs`, and + `/dashboard/agents`. +- `apps/manager/src/app/dashboard/coverage/page.tsx` seeds selected language IDs + from incoming `searchParams`. +- `apps/manager/src/features/coverage/language-selection.ts` is the canonical + parser/normalizer for `languageId` and legacy `languageIds`. +- `apps/manager/src/features/coverage/LanguageGeoSelector.tsx` already uses + `useSearchParams()` plus `normalizeCoverageLanguageSearchParams()` when + writing coverage selection URLs. +- `apps/manager/src/features/jobs/live-jobs-table.tsx` already has a small + precedent for preserving `languageId` when linking from the Jobs list to job + detail pages. + +Compound docs to incorporate: + +- `manager-coverage-language-first-empty-state-20260410.md`: `languageId` is + canonical, `languageIds` is read compatibility, and stale language params must + be deleted before writing a new selection. +- `manager-coverage-dashboard-review-regression-cleanup.md`: keep coverage UI + states distinct; do not let navigation changes blur loading, empty, and + failure states. +- `manager-enrich-now-feedback-handoff-20260413.md`: Coverage-to-Jobs handoffs + should preserve operator context and avoid hidden state overwriting newer + user choices. + +No dedicated roadmap ticket exists for this exact bug. Use `feat-030` and the +completed Agents/Enrich Now work as context only; do not pretend either roadmap +ticket is the active tracking item unless a follow-up ticket is explicitly +created. + +## SpecFlow Notes + +User flows: + +1. Report -> Jobs -> Report with selected language: preserve and restore + `languageId`. +2. Report -> Agents -> Report with selected language: preserve and restore + `languageId`. +3. Legacy URL -> return path: accept `languageIds` on entry and canonicalize the + carried return query to `languageId`. +4. Clear selection -> leave Report -> return: keep the default Report empty + state; do not resurrect the cleared language. +5. Direct entry: `/dashboard/coverage` remains explicit and should not consult + hidden prior browser/session state. + +Important gaps addressed by this plan: + +- Dashboard tab navigation is the missing handoff; coverage parsing already + exists. +- Avoid hidden browser-memory fallback unless implementation proves URL-based + handoff cannot work. +- "Any filters selected" is scoped to URL-backed Report filters. Local Report + controls such as search text, collection type, coverage segment, and + `reportType` are not promoted to URL state in this slice. +- Add nav-oriented regression coverage; current tests only cover parsing and + empty-state behavior. + +## Proposed Solution + +Keep the selected Report language in the dashboard tab URLs while an operator is +moving between dashboard tabs: + +1. Add a small node-testable nav/query helper that extracts only supported + coverage Report query state from the current URL query. +2. Canonicalize `languageIds` to `languageId`. +3. Use the helper from `DashboardNav` with `useSearchParams()` to build Report, + Jobs, and Agents tab hrefs. +4. Carry only the coverage Report query allowlist. Do not copy arbitrary + Jobs/Agents query params back into Report. +5. If no selected language exists, emit plain tab links with no query string. + +Recommended helper shape: + +```ts +// apps/manager/src/features/nav/dashboard-nav-model.ts +export function buildDashboardNavHref( + pathname: "/dashboard/coverage" | "/dashboard/jobs" | "/dashboard/agents", + currentQuery: string, +): string +``` + +The helper can import the existing coverage language parsing/normalization +utilities, or an adjacent helper can be added in `language-selection.ts` if that +keeps the ownership clearer. Keep this decision small: one shared owner for the +navigation query contract, with tests. + +## Scope Boundaries + +In scope: + +- Manager dashboard navigation links for Report, Jobs, and Agents. +- Canonical language filter carry-forward across tab navigation. +- Legacy `languageIds` read compatibility. +- Focused Red/Green unit tests for the URL helper/navigation contract. +- User-like browser smoke test against the rendered Manager dashboard flow. + +Out of scope: + +- CMS coverage SQL, GraphQL, or API changes. +- Coverage data fetching behavior. +- Report visual redesign. +- Jobs or Agents feature work unrelated to preserving the Report return query. +- A global dashboard state store. +- Persisting non-URL-backed Report controls. +- Adding React Testing Library, jsdom, or a new browser test stack to the + Manager package for this small fix. + +## Implementation Plan + +### Unit 1: Red test for dashboard nav URL carry-forward + +Red: + +- Add `apps/manager/src/features/nav/dashboard-nav-model.test.ts`. +- Write failing tests for the node-testable helper: + - `buildDashboardNavHref("/dashboard/jobs", "languageId=529")` returns + `/dashboard/jobs?languageId=529`. + - `buildDashboardNavHref("/dashboard/coverage", "languageId=529")` returns + `/dashboard/coverage?languageId=529`. + - `buildDashboardNavHref("/dashboard/agents", "languageIds=529,21028")` + returns `/dashboard/agents?languageId=529%2C21028` or the + project-standard encoded equivalent. + - no language query returns the bare target pathname. + - unknown non-coverage query keys are not carried across tabs. + - `refresh=1` is not carried. + +Green: + +- Add the helper in `apps/manager/src/features/nav/dashboard-nav-model.ts`, or + add the helper to `apps/manager/src/features/coverage/language-selection.ts` + if the implementation keeps the nav model too thin. +- Use `resolveRequestedLanguageIds()` and/or + `normalizeCoverageLanguageSearchParams()` so `languageId` remains canonical. +- Carry only the supported coverage Report query allowlist. + +Refactor: + +- Keep `language-selection.test.ts` focused on coverage query parsing and + normalization. Avoid duplicating the same normalization tests in every tab + test. + +### Unit 2: Wire `DashboardNav` to the helper + +Red: + +- The Unit 1 helper tests should fail until the helper exists. +- If wiring confidence is low, add a minimal test around the exported href model + rather than introducing a React nav component test stack. + +Green: + +- Update `apps/manager/src/features/nav/dashboard-nav.tsx` to import + `useSearchParams()` from `next/navigation`. +- Convert `searchParams?.toString() ?? ""` into three hrefs via the helper: + - Report + - Jobs + - Agents +- Replace the hardcoded `href="/dashboard/coverage"`, + `href="/dashboard/jobs"`, and `href="/dashboard/agents"` values with the + computed hrefs. +- Preserve existing active-tab detection, queue count polling, logout behavior, + user menu behavior, icons, labels, and CSS classes. + +Refactor: + +- Keep this change inside the shared dashboard nav and the helper. Do not push + coverage-specific state deeper into Jobs or Agents pages unless the user smoke + exposes a real gap. + +### Unit 3: Check Coverage-originated Jobs handoffs + +Red: + +- No new app code test is required if the existing accepted Jobs action already + starts at `/dashboard/jobs` and the shared nav can carry the query once the + user is on a query-bearing dashboard tab. +- If manual review shows a redirect/link from Coverage to Jobs that bypasses + the query-bearing nav and must preserve Report context, add a focused helper + test before changing it. + +Green: + +- Inspect `apps/manager/src/features/enrich-selection.ts` and + `apps/manager/src/features/coverage/coverage-report-client.tsx`. +- If Coverage success actions still send users to plain `/dashboard/jobs`, keep + that behavior only if the operator can still return to Report with the + current language through the shared nav. +- If the handoff loses context in smoke testing, route those Coverage-originated + Jobs links through the same helper with the current query string. + +Refactor: + +- Do not reintroduce auto-redirect-only behavior that hides local feedback. The + Enrich Now feedback fix deliberately kept acceptance feedback and Jobs links + visible. + +### Unit 4: User smoke test and PR hygiene + +Run the user-like browser smoke before PR handoff: + +1. Start or reuse local Manager at `http://localhost:3002` with Manager auth. +2. Open `/dashboard/coverage?languageId=529`. +3. Confirm Report opens with language `529` selected. +4. Click Jobs. +5. Confirm the URL is `/dashboard/jobs?languageId=529` or equivalent canonical + query form. +6. Click Report. +7. Confirm the URL is `/dashboard/coverage?languageId=529` and the language + selection is still active. +8. Click Agents. +9. Confirm the URL is `/dashboard/agents?languageId=529`. +10. Click Report again and confirm the language selection remains active. +11. Open `/dashboard/coverage?languageIds=529,21028`, repeat a short tab + round-trip, and confirm the carried URL uses canonical `languageId`. +12. Clear the Report language selection, click Jobs, then Report, and confirm + the default language-first state remains. +13. Capture smoke evidence in work notes or PR notes. + +If local Manager auth/CMS data blocks the real dashboard smoke, use a temporary +uncommitted smoke route under `/login/report-filter-smoke` that renders the real +`DashboardNav` with a test user and verifies the same click flow. Remove the +temporary route before commit and document the limitation clearly. + +PR hygiene: + +- Keep the PR title in the required format: + `fix(manager): restore report filters`. +- Target `main`. +- Do not use `--no-verify`. +- Before PR creation, run `gh pr list --state all --limit 20` to check current + PR title/body conventions if needed. +- Complete `ce:review` before PR handoff and `ce:compound` after the fix lands + if a reusable navigation-state learning emerges. + +## Acceptance Criteria + +- [ ] From `/dashboard/coverage?languageId=529`, Jobs then Report restores + `/dashboard/coverage?languageId=529`. +- [ ] From `/dashboard/coverage?languageId=529`, Agents then Report restores + `/dashboard/coverage?languageId=529`. +- [ ] Report -> Jobs -> Agents -> Report preserves the same selected language. +- [ ] Legacy `/dashboard/coverage?languageIds=529,21028` still loads and any + carried dashboard-tab query is canonicalized to `languageId`. +- [ ] Direct `/dashboard/coverage` still opens the language-first default state. +- [ ] Clearing the Report language selection and navigating away does not revive + a stale language on return. +- [ ] Unknown Jobs/Agents query params are not copied into Report as hidden + Report state. +- [ ] Red/Green TDD evidence is captured in work notes or PR notes. +- [ ] User smoke test evidence is captured before PR handoff. +- [ ] Manager lint, typecheck, focused tests, and build pass before PR. +- [ ] Root format-sensitive validation passes before PR. + +## Verification + +Red first: + +```bash +pnpm --filter @forge/manager test -- src/features/nav/dashboard-nav-model.test.ts +``` + +Green after implementation: + +```bash +pnpm --filter @forge/manager test -- src/features/nav/dashboard-nav-model.test.ts +pnpm --filter @forge/manager test -- src/features/coverage/language-selection.test.ts +pnpm --filter @forge/manager lint +pnpm --filter @forge/manager typecheck +pnpm --filter @forge/manager build +pnpm run format:check +git diff --check +``` + +If the implementation touches broader Coverage or Jobs behavior, also run: + +```bash +pnpm --filter @forge/manager test +``` + +User smoke: + +- Run the browser flow in Unit 4. +- Save screenshot or URL/assertion notes in work notes or PR notes. +- If a temporary smoke route is used, remove it before committing and document + why the real authenticated dashboard path was unavailable. + +## Risks + +- `useSearchParams()` in the shared nav could affect build behavior if the route + stops being dynamic. Mitigation: run the Manager build, not only lint and + typecheck. +- Accidentally copying arbitrary Jobs/Agents query params into Report could + create confusing future state. Mitigation: use an allowlisted helper and test + unknown-key behavior. +- A hidden session-storage solution could make direct `/dashboard/coverage` + behave differently based on browsing history. Mitigation: keep the first + implementation URL-based. +- The phrase "any filters selected" could invite a much larger URL-state + migration. Mitigation: scope V1 to existing URL-backed language filters and + let future Report filters opt in explicitly. + +## References + +- `docs/brainstorms/2026-04-13-manager-report-filter-restoration-brainstorm.md` +- `apps/manager/src/features/nav/dashboard-nav.tsx` +- `apps/manager/src/features/coverage/language-selection.ts` +- `apps/manager/src/features/coverage/language-selection.test.ts` +- `apps/manager/src/features/coverage/LanguageGeoSelector.tsx` +- `apps/manager/src/features/coverage/coverage-report-client.tsx` +- `apps/manager/src/app/dashboard/coverage/page.tsx` +- `apps/manager/src/features/jobs/live-jobs-table.tsx` +- `docs/solutions/integration-issues/manager-coverage-language-first-empty-state-20260410.md` +- `docs/solutions/integration-issues/manager-coverage-dashboard-review-regression-cleanup.md` +- `docs/solutions/ui-bugs/manager-enrich-now-feedback-handoff-20260413.md` +- `.github/PULL_REQUEST_TEMPLATE.md` +- `.github/workflows/ci.yml` From a29b0fc6f9a2acd95c31f8878c4f622c2b309762 Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 13 Apr 2026 13:36:45 -0300 Subject: [PATCH 3/3] fix(manager): restore report filter navigation --- .../coverage/coverage-report-client.tsx | 8 ++ .../coverage/enrich-action-controls.test.ts | 3 +- .../coverage/enrich-action-controls.tsx | 8 +- .../features/nav/dashboard-nav-model.test.ts | 60 +++++++++++++ .../src/features/nav/dashboard-nav-model.ts | 37 ++++++++ .../src/features/nav/dashboard-nav.tsx | 17 +++- ...-manager-report-filter-restoration-plan.md | 81 ++++++++++++++--- ...t-filter-dashboard-tab-handoff-20260413.md | 86 +++++++++++++++++++ 8 files changed, 282 insertions(+), 18 deletions(-) create mode 100644 apps/manager/src/features/nav/dashboard-nav-model.test.ts create mode 100644 apps/manager/src/features/nav/dashboard-nav-model.ts create mode 100644 docs/solutions/ui-bugs/manager-report-filter-dashboard-tab-handoff-20260413.md diff --git a/apps/manager/src/features/coverage/coverage-report-client.tsx b/apps/manager/src/features/coverage/coverage-report-client.tsx index 2f6118692..f2a4f3e5e 100644 --- a/apps/manager/src/features/coverage/coverage-report-client.tsx +++ b/apps/manager/src/features/coverage/coverage-report-client.tsx @@ -1028,6 +1028,13 @@ export function CoverageReportClient({ ) const selectedVideoCount = selectedVideoIds.size const selectedLanguageCount = selectedLanguageIds.length + const selectedLanguageQuery = useMemo(() => { + if (selectedLanguageIds.length === 0) return "" + + const params = new URLSearchParams() + params.set("languageId", selectedLanguageIds.join(",")) + return params.toString() + }, [selectedLanguageIds]) const languageSelectionRequired = requiresLanguageSelectionForEnrich( selectedVideoCount, selectedLanguageCount, @@ -1731,6 +1738,7 @@ export function CoverageReportClient({ languageSelectionRequired={languageSelectionRequired} onCancel={handleCancelEnrichSelection} onEnrich={handleEnrichSelection} + reportQuery={selectedLanguageQuery} /> )} diff --git a/apps/manager/src/features/coverage/enrich-action-controls.test.ts b/apps/manager/src/features/coverage/enrich-action-controls.test.ts index d0c5f3f00..53b1d6959 100644 --- a/apps/manager/src/features/coverage/enrich-action-controls.test.ts +++ b/apps/manager/src/features/coverage/enrich-action-controls.test.ts @@ -41,11 +41,12 @@ describe("EnrichActionControls", () => { languageSelectionRequired: false, onCancel: vi.fn(), onEnrich: vi.fn(), + reportQuery: "languageId=529", }), ) expect(markup).toContain("1 enrichment job started.") - expect(markup).toContain('href="/dashboard/jobs/job-1"') + expect(markup).toContain('href="/dashboard/jobs/job-1?languageId=529"') expect(markup).toContain("Open job") }) }) diff --git a/apps/manager/src/features/coverage/enrich-action-controls.tsx b/apps/manager/src/features/coverage/enrich-action-controls.tsx index cc19c4173..dab1163b0 100644 --- a/apps/manager/src/features/coverage/enrich-action-controls.tsx +++ b/apps/manager/src/features/coverage/enrich-action-controls.tsx @@ -3,6 +3,7 @@ import React from "react" import type { EnrichFeedback } from "@/features/enrich-selection" +import { buildDashboardHrefWithReportQuery } from "@/features/nav/dashboard-nav-model" type EnrichActionControlsProps = { enrichActionReady: boolean @@ -11,6 +12,7 @@ type EnrichActionControlsProps = { languageSelectionRequired: boolean onCancel: () => void onEnrich: () => void | Promise + reportQuery?: string } export function EnrichActionControls({ @@ -20,8 +22,12 @@ export function EnrichActionControls({ languageSelectionRequired, onCancel, onEnrich, + reportQuery = "", }: EnrichActionControlsProps) { const actionDisabled = !enrichActionReady || isEnrichSubmitting + const feedbackActionHref = enrichFeedback?.action + ? buildDashboardHrefWithReportQuery(enrichFeedback.action.href, reportQuery) + : null return (
@@ -87,7 +93,7 @@ export function EnrichActionControls({ {" "} {enrichFeedback.action.label} diff --git a/apps/manager/src/features/nav/dashboard-nav-model.test.ts b/apps/manager/src/features/nav/dashboard-nav-model.test.ts new file mode 100644 index 000000000..8cebe59f0 --- /dev/null +++ b/apps/manager/src/features/nav/dashboard-nav-model.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest" + +import { + buildDashboardHrefWithReportQuery, + buildDashboardNavHref, +} from "./dashboard-nav-model" + +describe("dashboard nav model", () => { + it("carries the canonical report language query across dashboard tabs", () => { + expect(buildDashboardNavHref("/dashboard/jobs", "languageId=529")).toBe( + "/dashboard/jobs?languageId=529", + ) + + expect(buildDashboardNavHref("/dashboard/coverage", "languageId=529")).toBe( + "/dashboard/coverage?languageId=529", + ) + + expect(buildDashboardNavHref("/dashboard/agents", "languageId=529")).toBe( + "/dashboard/agents?languageId=529", + ) + }) + + it("canonicalizes legacy languageIds query params", () => { + expect( + buildDashboardNavHref("/dashboard/agents", "languageIds=529,21028"), + ).toBe("/dashboard/agents?languageId=529%2C21028") + }) + + it("carries the report language query to dashboard job detail handoffs", () => { + expect( + buildDashboardHrefWithReportQuery( + "/dashboard/jobs/job-1", + "languageIds=529,21028", + ), + ).toBe("/dashboard/jobs/job-1?languageId=529%2C21028") + }) + + it("drops unsupported query params instead of carrying hidden dashboard state", () => { + expect( + buildDashboardNavHref( + "/dashboard/coverage", + "languageId=529&refresh=1&status=failed", + ), + ).toBe("/dashboard/coverage?languageId=529") + + expect(buildDashboardNavHref("/dashboard/jobs", "status=failed")).toBe( + "/dashboard/jobs", + ) + }) + + it("returns bare tab paths when no report language is selected", () => { + expect(buildDashboardNavHref("/dashboard/coverage", "")).toBe( + "/dashboard/coverage", + ) + + expect(buildDashboardNavHref("/dashboard/jobs", "languageId=")).toBe( + "/dashboard/jobs", + ) + }) +}) diff --git a/apps/manager/src/features/nav/dashboard-nav-model.ts b/apps/manager/src/features/nav/dashboard-nav-model.ts new file mode 100644 index 000000000..8c54b5e5f --- /dev/null +++ b/apps/manager/src/features/nav/dashboard-nav-model.ts @@ -0,0 +1,37 @@ +import type { Route } from "next" +import { resolveRequestedLanguageIds } from "@/features/coverage/language-selection" + +export type DashboardNavPath = + | "/dashboard/coverage" + | "/dashboard/jobs" + | "/dashboard/agents" + +export type DashboardReportQueryPath = + | DashboardNavPath + | `/dashboard/jobs/${string}` + +export function buildDashboardHrefWithReportQuery< + TPath extends DashboardReportQueryPath, +>(pathname: TPath, currentQuery: string): Route { + const params = new URLSearchParams(currentQuery) + const languageIds = resolveRequestedLanguageIds({ + languageId: params.get("languageId") ?? undefined, + languageIds: params.get("languageIds") ?? undefined, + }) + + if (languageIds.length === 0) { + return pathname as Route + } + + const nextParams = new URLSearchParams() + nextParams.set("languageId", languageIds.join(",")) + + return `${pathname}?${nextParams.toString()}` as Route +} + +export function buildDashboardNavHref( + pathname: TPath, + currentQuery: string, +): Route { + return buildDashboardHrefWithReportQuery(pathname, currentQuery) +} diff --git a/apps/manager/src/features/nav/dashboard-nav.tsx b/apps/manager/src/features/nav/dashboard-nav.tsx index d67a15dce..c733f4848 100644 --- a/apps/manager/src/features/nav/dashboard-nav.tsx +++ b/apps/manager/src/features/nav/dashboard-nav.tsx @@ -1,10 +1,11 @@ "use client" import Link from "next/link" -import { usePathname, useRouter } from "next/navigation" +import { usePathname, useRouter, useSearchParams } from "next/navigation" import { useCallback, useEffect, useRef, useState } from "react" import { BarChart2, Bot, ListChecks, LogOut } from "lucide-react" import { apiFetch } from "@/lib/api-fetch" +import { buildDashboardNavHref } from "./dashboard-nav-model" type NavUser = { username: string; email: string } @@ -20,6 +21,7 @@ function getInitials(username: string): string { export function DashboardNav({ user }: { user: NavUser }) { const router = useRouter() const pathname = usePathname() + const searchParams = useSearchParams() const [queueCount, setQueueCount] = useState(null) const [menuOpen, setMenuOpen] = useState(false) const menuRef = useRef(null) @@ -28,6 +30,13 @@ export function DashboardNav({ user }: { user: NavUser }) { const isJobs = pathname.startsWith("/dashboard/jobs") || pathname === "/dashboard" const isAgents = pathname.startsWith("/dashboard/agents") + const currentQuery = searchParams.toString() + const coverageHref = buildDashboardNavHref( + "/dashboard/coverage", + currentQuery, + ) + const jobsHref = buildDashboardNavHref("/dashboard/jobs", currentQuery) + const agentsHref = buildDashboardNavHref("/dashboard/agents", currentQuery) useEffect(() => { let cancelled = false @@ -72,7 +81,7 @@ export function DashboardNav({ user }: { user: NavUser }) { return (