From d06e46a529782069f85ee23d15071746f9b18c67 Mon Sep 17 00:00:00 2001 From: Roman Mitasov Date: Wed, 15 Jul 2026 15:56:53 +0300 Subject: [PATCH] feat: add static What's new page linked from the sidebar Adds a /whats-new route with the 13 July 2026 release notes for dashboard users, rendered in the app's design system, and a new sidebar footer entry to reach it. Content lives in the i18n bundle; a unit test covers the screen for the diff-coverage gate. Co-Authored-By: Claude Fable 5 Signed-off-by: Roman Mitasov --- src/components/app-sidebar.tsx | 93 +++++++++++-------- src/locales/en/translation.json | 85 ++++++++++++++++++ src/routeTree.gen.ts | 21 +++++ src/routes/whats-new.tsx | 7 ++ src/screens/whats-new.test.tsx | 82 +++++++++++++++++ src/screens/whats-new.tsx | 153 ++++++++++++++++++++++++++++++++ 6 files changed, 403 insertions(+), 38 deletions(-) create mode 100644 src/routes/whats-new.tsx create mode 100644 src/screens/whats-new.test.tsx create mode 100644 src/screens/whats-new.tsx diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index b9491a6..8e4d4a9 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -1,5 +1,11 @@ import { Link, useRouterState } from "@tanstack/react-router"; -import { ChevronDown, ChevronRight, User, Users } from "lucide-react"; +import { + ChevronDown, + ChevronRight, + Megaphone, + User, + Users, +} from "lucide-react"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; @@ -59,7 +65,11 @@ function PersonNode({ style={{ paddingLeft: `${0.5 + depth * 0.875}rem` }} > {hasReports ? ( - open ? : + open ? ( + + ) : ( + + ) ) : ( )} @@ -98,10 +108,10 @@ export function AppSidebar() {
-
+
I
- + {t("common.app_name")}
@@ -113,49 +123,56 @@ export function AppSidebar() { ) : viewer ? ( - + ) : null} + + + } + > + + {t("whats_new.nav_label")} + + + - {viewerEmail ? ( - (() => { - const primaryEmail = viewer?.email ?? viewerEmail; - const primary = viewer?.display_name || primaryEmail; - const showSecondary = primary !== primaryEmail; - return ( - - - - - - {getInitials(primary) || "?"} - - -
- - {primary} - - {showSecondary ? ( - - {primaryEmail} + {viewerEmail + ? (() => { + const primaryEmail = viewer?.email ?? viewerEmail; + const primary = viewer?.display_name || primaryEmail; + const showSecondary = primary !== primaryEmail; + return ( + + + + + + {getInitials(primary) || "?"} + + +
+ + {primary} - ) : null} -
-
-
-
- ); - })() - ) : null} + {showSecondary ? ( + + {primaryEmail} + + ) : null} +
+
+
+
+ ); + })() + : null}
); diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9bb9deb..c72c6e4 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -105,6 +105,91 @@ "label": "New metrics UI" } }, + "whats_new": { + "nav_label": "What's new", + "eyebrow": "Insight · What's new", + "title": "What's new — 13 July 2026", + "lead_html": "This update makes your dashboards more complete and more accurate. Data now flows and is attributed to the right people across Git, Jira, collaboration and Zoom, and several metrics that were showing wrong or empty values have been corrected. No new screens this time — everything below improves what you already use.", + "stamp": { + "highlights_label": "Highlights", + "highlights": "9 improvements", + "focus_label": "Focus", + "focus": "data accuracy & completeness" + }, + "in_short_html": "In short: more of your team's activity now shows up correctly — Bitbucket reviews, Jira task delivery, collaboration and Zoom meetings are back and attributed properly, long-range Git charts are readable, and AI cost is shown as real money.", + "improvements_label": "Improvements you'll notice", + "items": { + "direct_reports_toggle": { + "category": "Team dashboards", + "title": "“Direct reports only” toggle is back", + "description_html": "Filter a team down to just your direct reports — members, heatmap and metrics all update together. On by default, and hidden automatically for teams without sub-teams, where it had no effect." + }, + "member_expand_full_metrics": { + "category": "Team dashboards", + "title": "Full metrics when you expand a team member", + "description_html": "Expanding a person now opens their complete metric set instead of a trimmed subset — so you can assess someone without switching screens." + }, + "bitbucket_prs": { + "category": "Git & code reviews", + "title": "Bitbucket pull requests now counted", + "description_html": "Pull-request activity from Bitbucket is attributed to the right person. Review metrics that used to show zero now reflect real work." + }, + "consistent_git_metrics": { + "category": "Git & code reviews", + "title": "Consistent commit & lines-of-code metrics", + "description_html": "Commit and LOC figures now come from a single shared source, so Git activity is reported the same way across connectors." + }, + "readable_git_charts": { + "category": "Git & code reviews", + "title": "Readable quarterly & yearly Git charts", + "description_html": "Long-range charts no longer repeat the same label over and over. Quarterly and annual trends are now legible." + }, + "jira_task_delivery": { + "category": "Task delivery", + "title": "Jira Task Delivery metrics now populate", + "description_html": "Closed tasks are detected by their status category rather than English-only status names, so non-English and custom “Done” workflows report correctly." + }, + "zoom_data": { + "category": "Collaboration", + "title": "Zoom meeting data restored", + "description_html": "The Zoom sync was hitting an API limit and stalled for about ten days. It's fixed, so meeting activity repopulates going forward." + }, + "ai_adoption_graphs": { + "category": "AI adoption", + "title": "AI adoption graphs fixed", + "description_html": "AI-adoption graphs now show real distributions with honest “no data” states instead of blanks or misleading zeros." + }, + "claude_code_cost": { + "category": "AI adoption", + "title": "Claude Code cost shown as currency", + "description_html": "The “Claude Code Cost” metric now displays as money instead of a raw number of cents — no more figures that looked inflated." + } + }, + "gaps": { + "title": "Still on our list", + "items": { + "identity": "People without a matching email aren't attributed yet — this can still leave some Bitbucket and Jira metrics incomplete. Identity resolution is in progress.", + "gitlab_loc": "GitLab lines-of-code still shows 0. The commit and LOC fix above applies elsewhere; GitLab LOC is being worked on.", + "jira_kpis": "Some Jira KPIs may still be blank or slow to load on freshly set-up environments." + } + }, + "coming": { + "label": "Coming next", + "items": { + "metrics_engine": { + "category": "Platform", + "title": "One accurate metrics engine", + "description": "Bringing all metric families onto a single engine, with correct “no data” states in place of misleading zeros everywhere." + }, + "people_matching": { + "category": "Platform", + "title": "Better people matching", + "description": "Attributing work to the right person even across multiple or missing email addresses — the root cause behind the gaps above." + } + } + }, + "footer": "Insight · what's new · 13 July 2026" + }, "auth": { "callback": { "completing": "Completing sign-in...", diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 37a8ba5..a62857b 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,6 +9,7 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as WhatsNewRouteImport } from './routes/whats-new' import { Route as CallbackRouteImport } from './routes/callback' import { Route as IndexRouteImport } from './routes/index' import { Route as IcPersonRouteImport } from './routes/ic.$person' @@ -16,6 +17,11 @@ import { Route as IcPersonIndexRouteImport } from './routes/ic.$person.index' import { Route as IcPersonTeamRouteImport } from './routes/ic.$person.team' import { Route as IcPersonPersonalRouteImport } from './routes/ic.$person.personal' +const WhatsNewRoute = WhatsNewRouteImport.update({ + id: '/whats-new', + path: '/whats-new', + getParentRoute: () => rootRouteImport, +} as any) const CallbackRoute = CallbackRouteImport.update({ id: '/callback', path: '/callback', @@ -50,6 +56,7 @@ const IcPersonPersonalRoute = IcPersonPersonalRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/callback': typeof CallbackRoute + '/whats-new': typeof WhatsNewRoute '/ic/$person': typeof IcPersonRouteWithChildren '/ic/$person/personal': typeof IcPersonPersonalRoute '/ic/$person/team': typeof IcPersonTeamRoute @@ -58,6 +65,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/callback': typeof CallbackRoute + '/whats-new': typeof WhatsNewRoute '/ic/$person/personal': typeof IcPersonPersonalRoute '/ic/$person/team': typeof IcPersonTeamRoute '/ic/$person': typeof IcPersonIndexRoute @@ -66,6 +74,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/callback': typeof CallbackRoute + '/whats-new': typeof WhatsNewRoute '/ic/$person': typeof IcPersonRouteWithChildren '/ic/$person/personal': typeof IcPersonPersonalRoute '/ic/$person/team': typeof IcPersonTeamRoute @@ -76,6 +85,7 @@ export interface FileRouteTypes { fullPaths: | '/' | '/callback' + | '/whats-new' | '/ic/$person' | '/ic/$person/personal' | '/ic/$person/team' @@ -84,6 +94,7 @@ export interface FileRouteTypes { to: | '/' | '/callback' + | '/whats-new' | '/ic/$person/personal' | '/ic/$person/team' | '/ic/$person' @@ -91,6 +102,7 @@ export interface FileRouteTypes { | '__root__' | '/' | '/callback' + | '/whats-new' | '/ic/$person' | '/ic/$person/personal' | '/ic/$person/team' @@ -100,11 +112,19 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute CallbackRoute: typeof CallbackRoute + WhatsNewRoute: typeof WhatsNewRoute IcPersonRoute: typeof IcPersonRouteWithChildren } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/whats-new': { + id: '/whats-new' + path: '/whats-new' + fullPath: '/whats-new' + preLoaderRoute: typeof WhatsNewRouteImport + parentRoute: typeof rootRouteImport + } '/callback': { id: '/callback' path: '/callback' @@ -169,6 +189,7 @@ const IcPersonRouteWithChildren = IcPersonRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, CallbackRoute: CallbackRoute, + WhatsNewRoute: WhatsNewRoute, IcPersonRoute: IcPersonRouteWithChildren, } export const routeTree = rootRouteImport diff --git a/src/routes/whats-new.tsx b/src/routes/whats-new.tsx new file mode 100644 index 0000000..6802bcf --- /dev/null +++ b/src/routes/whats-new.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { WhatsNewScreen } from "@/screens/whats-new"; + +export const Route = createFileRoute("/whats-new")({ + component: WhatsNewScreen, +}); diff --git a/src/screens/whats-new.test.tsx b/src/screens/whats-new.test.tsx new file mode 100644 index 0000000..f81349a --- /dev/null +++ b/src/screens/whats-new.test.tsx @@ -0,0 +1,82 @@ +import { render, screen } from "@testing-library/react"; +import { beforeAll, describe, expect, it } from "vitest"; + +import "@/i18n"; + +import { SidebarProvider } from "@/components/ui/sidebar"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { WhatsNewScreen } from "@/screens/whats-new"; + +// SidebarProvider's useIsMobile reads window.matchMedia, which jsdom does +// not implement — provide a desktop-shaped stub. +beforeAll(() => { + if (typeof window.matchMedia !== "function") { + window.matchMedia = (query: string) => + ({ + matches: false, + media: query, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + addListener: () => {}, + removeListener: () => {}, + dispatchEvent: () => false, + }) as MediaQueryList; + } +}); + +function renderScreen() { + return render( + + + + + + ); +} + +describe("WhatsNewScreen", () => { + it("renders the release header and stamp", () => { + renderScreen(); + expect( + screen.getByRole("heading", { name: "What's new — 13 July 2026" }) + ).toBeInTheDocument(); + expect(screen.getByText("9 improvements")).toBeInTheDocument(); + expect( + screen.getByText("data accuracy & completeness") + ).toBeInTheDocument(); + }); + + it("renders every improvement entry with its category", () => { + renderScreen(); + for (const title of [ + "“Direct reports only” toggle is back", + "Full metrics when you expand a team member", + "Bitbucket pull requests now counted", + "Consistent commit & lines-of-code metrics", + "Readable quarterly & yearly Git charts", + "Jira Task Delivery metrics now populate", + "Zoom meeting data restored", + "AI adoption graphs fixed", + "Claude Code cost shown as currency", + ]) { + expect(screen.getByRole("heading", { name: title })).toBeInTheDocument(); + } + expect(screen.getAllByText("Team dashboards")).toHaveLength(2); + expect(screen.getAllByText("Git & code reviews")).toHaveLength(3); + expect(screen.getAllByText("AI adoption")).toHaveLength(2); + }); + + it("renders the known-gaps callout and the coming-next section", () => { + renderScreen(); + expect(screen.getByText("Still on our list")).toBeInTheDocument(); + expect(screen.getByRole("alert")).toHaveTextContent("GitLab lines-of-code"); + expect(screen.getByText("Coming next")).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: "One accurate metrics engine" }) + ).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: "Better people matching" }) + ).toBeInTheDocument(); + }); +}); diff --git a/src/screens/whats-new.tsx b/src/screens/whats-new.tsx new file mode 100644 index 0000000..4889263 --- /dev/null +++ b/src/screens/whats-new.tsx @@ -0,0 +1,153 @@ +import { Info } from "lucide-react"; +import { Trans, useTranslation } from "react-i18next"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { SidebarTrigger } from "@/components/ui/sidebar"; + +const IMPROVEMENT_KEYS = [ + "direct_reports_toggle", + "member_expand_full_metrics", + "bitbucket_prs", + "consistent_git_metrics", + "readable_git_charts", + "jira_task_delivery", + "zoom_data", + "ai_adoption_graphs", + "claude_code_cost", +] as const; + +const GAP_KEYS = ["identity", "gitlab_loc", "jira_kpis"] as const; + +const COMING_KEYS = ["metrics_engine", "people_matching"] as const; + +// Translations carry / emphasis; restore foreground weight inside +// muted copy so the emphasized fragments read as in the source notes. +const EMPHASIS = + "[&_strong]:font-semibold [&_strong]:text-foreground [&_i]:italic"; + +export function WhatsNewScreen() { + const { t } = useTranslation(); + + return ( + <> +
+ +

+ {t("whats_new.nav_label")} +

+
+ +
+
+
+

+ {t("whats_new.eyebrow")} +

+

+ {t("whats_new.title")} +

+

+ +

+
+ + + {t("whats_new.stamp.highlights_label")} + {" "} + {t("whats_new.stamp.highlights")} + + + + {t("whats_new.stamp.focus_label")} + {" "} + {t("whats_new.stamp.focus")} + +
+
+ +

+ +

+ +
+

+ {t("whats_new.improvements_label")} +

+
+ {IMPROVEMENT_KEYS.map((key) => ( +
+

+ {t(`whats_new.items.${key}.category`)} +

+
+

+ {t(`whats_new.items.${key}.title`)} +

+

+ +

+
+
+ ))} +
+
+ + + + {t("whats_new.gaps.title")} + +
    + {GAP_KEYS.map((key) => ( +
  • + +
  • + ))} +
+
+
+ +
+

+ {t("whats_new.coming.label")} +

+
+ {COMING_KEYS.map((key) => ( +
+

+ {t(`whats_new.coming.items.${key}.category`)} +

+
+

+ {t(`whats_new.coming.items.${key}.title`)} +

+

+ {t(`whats_new.coming.items.${key}.description`)} +

+
+
+ ))} +
+
+ + +
+
+ + ); +}