diff --git a/src/components/whats-new-dialog.test.tsx b/src/components/whats-new-dialog.test.tsx index d71e6ea..a32c521 100644 --- a/src/components/whats-new-dialog.test.tsx +++ b/src/components/whats-new-dialog.test.tsx @@ -6,6 +6,13 @@ import { LATEST_RELEASE } from "@/lib/release-notes"; const SEEN_KEY = "wfdash-whats-new-seen"; +/** + * Any date that could reach the popup: the ISO shape ("2026-11-01"), the + * "1 Nov 2026" shape, and the "November 1, 2026" shape. + */ +const DATE_PATTERN = + /\d{4}-\d{2}-\d{2}|\d{1,2}\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{4}|(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4}/i; + describe("WhatsNewButton", () => { beforeEach(() => { window.localStorage.clear(); @@ -25,11 +32,13 @@ describe("WhatsNewButton", () => { // onOpenChange would leave the badge lit forever. fireEvent.click(screen.getByRole("button", { name: "What's New" })); - expect(window.localStorage.getItem(SEEN_KEY)).toBe(LATEST_RELEASE.version); + expect(window.localStorage.getItem(SEEN_KEY)).toBe( + String(LATEST_RELEASE.release) + ); }); it("does not flag a release the operator has already read", () => { - window.localStorage.setItem(SEEN_KEY, LATEST_RELEASE.version); + window.localStorage.setItem(SEEN_KEY, String(LATEST_RELEASE.release)); render(); expect(screen.queryByTestId("whats-new-badge")).not.toBeInTheDocument(); @@ -42,4 +51,28 @@ describe("WhatsNewButton", () => { expect(await screen.findByText(LATEST_RELEASE.title)).toBeInTheDocument(); }); + + it("identifies the release by number and commit", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "What's New" })); + + expect( + await screen.findByText(`Release ${LATEST_RELEASE.release}`) + ).toBeInTheDocument(); + expect(screen.getByText(LATEST_RELEASE.commit)).toBeInTheDocument(); + }); + + it("never shows a date in the popup", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "What's New" })); + + await screen.findByText(LATEST_RELEASE.title); + + // The whole component is on the page here — the button and the + // portalled dialog — so this covers every string the popup can put + // on screen. + expect(document.body.textContent ?? "").not.toMatch(DATE_PATTERN); + }); }); diff --git a/src/components/whats-new-dialog.tsx b/src/components/whats-new-dialog.tsx index 2c224fc..8b8bd80 100644 --- a/src/components/whats-new-dialog.tsx +++ b/src/components/whats-new-dialog.tsx @@ -17,22 +17,24 @@ import { cn } from "@/lib/utils"; * What's New — a button in the sidebar that opens a popup of the * releases that have landed since the operator last looked. * - * "Seen" is tracked in localStorage against the latest version string. - * Bump the version in src/lib/release-notes.ts and the button lights up + * Each release is identified by its number and by the commit it went out + * with. Nothing in here carries a date: the date formatter that used to + * sit next to the release label is gone, so a date cannot reach the + * screen even by accident. + * + * "Seen" is tracked in localStorage against the latest release number. + * Add a new entry in src/lib/release-notes.ts and the button lights up * again for everyone — no server round-trip, no account state. */ const SEEN_KEY = "wfdash-whats-new-seen"; -function formatDate(iso: string): string { - const d = new Date(`${iso}T00:00:00`); - if (Number.isNaN(d.getTime())) return iso; - return d.toLocaleDateString("en-ZA", { - day: "numeric", - month: "short", - year: "numeric", - }); -} +/** + * The "seen" marker for the newest release. It is the release number, so + * adding a new entry in src/lib/release-notes.ts is what re-notifies every + * operator. + */ +const LATEST_MARKER = String(LATEST_RELEASE.release); export function WhatsNewButton({ className }: { className?: string }) { const [open, setOpen] = useState(false); @@ -57,14 +59,14 @@ export function WhatsNewButton({ className }: { className?: string }) { const markSeen = useCallback(() => { try { - window.localStorage.setItem(SEEN_KEY, LATEST_RELEASE.version); + window.localStorage.setItem(SEEN_KEY, LATEST_MARKER); } catch { /* ignore */ } - setState((s) => ({ ...s, seen: LATEST_RELEASE.version })); + setState((s) => ({ ...s, seen: LATEST_MARKER })); }, []); - const isNew = state.mounted && state.seen !== LATEST_RELEASE.version; + const isNew = state.mounted && state.seen !== LATEST_MARKER; return ( <> @@ -111,7 +113,7 @@ export function WhatsNewButton({ className }: { className?: string }) {
    {RELEASE_NOTES.map((release, i) => ( -
  1. +
  2. - {release.version} + Release {release.release} - - {formatDate(release.date)} + + {release.commit}

    {release.title}

    diff --git a/src/lib/release-notes.ts b/src/lib/release-notes.ts index 078408a..b2cc70b 100644 --- a/src/lib/release-notes.ts +++ b/src/lib/release-notes.ts @@ -1,5 +1,5 @@ /** - * What's New — the release-note registry behind the Settings page popup. + * What's New — the release-note registry behind the sidebar popup. * * A plain data file, newest first. There's no changelog table and no * fetch: the notes are written by hand when a release goes out, and the @@ -7,18 +7,20 @@ * client-side (it opens instantly, works offline) and means the copy * stays deliberate rather than being generated from commit messages. * - * To publish a release: add an entry at the top of the array. The - * version string is the "seen" marker — bumping it is what makes the - * What's New button light up for every operator again. + * A release is identified by its number and by the commit it went out + * with. There are no dates on screen — `date` is kept here only so the + * author can see which window of commits a release covers while writing + * it, and it is deliberately never rendered. + * + * To publish a release: + * 1. merge the work, then take the short SHA — `git log --oneline -1` + * 2. add an entry at the top of the array with the next release number + * 3. the release number is the "seen" marker, so bumping it is what + * makes the What's New button light up for every operator again */ export interface ReleaseNote { - /** Version marker. Bumped to re-notify operators. */ - version: string; - /** - * Release number. 1 = the first release. This replaces the date-shaped - * `version` string and becomes the "seen" marker. - */ + /** Release number. 1 = the first release. Also the "seen" marker. */ release: number; /** Short SHA of the commit this release went out with. */ commit: string; @@ -35,7 +37,6 @@ export interface ReleaseNote { export const RELEASE_NOTES: ReleaseNote[] = [ { - version: "2026.11.01", release: 5, commit: "139ea25", date: "2026-11-01", @@ -49,7 +50,6 @@ export const RELEASE_NOTES: ReleaseNote[] = [ ], }, { - version: "2026.10.30", release: 4, commit: "83aec14", date: "2026-10-30", @@ -62,7 +62,6 @@ export const RELEASE_NOTES: ReleaseNote[] = [ ], }, { - version: "2026.10.28", release: 3, commit: "3eea87e", date: "2026-10-28", @@ -73,7 +72,6 @@ export const RELEASE_NOTES: ReleaseNote[] = [ ], }, { - version: "2026.10.26", release: 2, commit: "b212d0f", date: "2026-10-26", @@ -84,7 +82,6 @@ export const RELEASE_NOTES: ReleaseNote[] = [ ], }, { - version: "2026.10.24", release: 1, commit: "55cc7fa", date: "2026-10-24",