diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c95111c..75bbef4cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added progress bar when navigating between pages. [#1204](https://github.com/sourcebot-dev/sourcebot/pull/1204) +- Added a integrated changelog into the sidebar. [#1227](https://github.com/sourcebot-dev/sourcebot/pull/1227) ### Changed - Redesigned the app layout with a new collapsible sidebar navigation, replacing the previous top navigation bar. [#1097](https://github.com/sourcebot-dev/sourcebot/pull/1097) diff --git a/packages/db/prisma/migrations/20260525214409_add_changelog_table/migration.sql b/packages/db/prisma/migrations/20260525214409_add_changelog_table/migration.sql new file mode 100644 index 000000000..184d6656d --- /dev/null +++ b/packages/db/prisma/migrations/20260525214409_add_changelog_table/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "ChangelogEntry" ( + "slug" TEXT NOT NULL, + "title" TEXT NOT NULL, + "publishedAt" TIMESTAMP(3) NOT NULL, + "summary" TEXT NOT NULL, + "version" TEXT NOT NULL, + "bodyMarkdown" TEXT NOT NULL, + "fetchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ChangelogEntry_pkey" PRIMARY KEY ("slug") +); + +-- CreateIndex +CREATE INDEX "ChangelogEntry_publishedAt_idx" ON "ChangelogEntry"("publishedAt"); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 140c15b44..5be0a4ce9 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -623,3 +623,19 @@ model OAuthToken { createdAt DateTime @default(now()) lastUsedAt DateTime? } + +/// Local cache of changelog entries fetched from the public feed at +/// `CHANGELOG_FEED_URL`. Shared across all users of an instance. +model ChangelogEntry { + slug String @id + title String + publishedAt DateTime + summary String + version String + bodyMarkdown String + + /// Updated each time the entry is upserted from the feed. + fetchedAt DateTime @default(now()) @updatedAt + + @@index([publishedAt]) +} diff --git a/packages/shared/src/env.server.ts b/packages/shared/src/env.server.ts index 036655018..d9ee5cae3 100644 --- a/packages/shared/src/env.server.ts +++ b/packages/shared/src/env.server.ts @@ -221,6 +221,10 @@ const options = { // Misc UI flags SECURITY_CARD_ENABLED: booleanSchema.default('false'), + // Changelog feed + CHANGELOG_ENABLED: booleanSchema.default('true'), + CHANGELOG_FEED_URL: z.string().url().default('https://static.sourcebot.dev/changelog/index.json'), + // EE License SOURCEBOT_EE_LICENSE_KEY: z.string().optional(), SOURCEBOT_EE_AUDIT_LOGGING_ENABLED: booleanSchema.default('true'), diff --git a/packages/shared/src/index.client.ts b/packages/shared/src/index.client.ts index 8e2a161dd..05a248168 100644 --- a/packages/shared/src/index.client.ts +++ b/packages/shared/src/index.client.ts @@ -4,4 +4,10 @@ export { } from "./env.client.js"; export { SOURCEBOT_VERSION, -} from "./version.js"; \ No newline at end of file +} from "./version.js"; +export { + parseVersion, + formatVersion, + compareVersions, +} from "./versionUtils.js"; +export type { Version } from "./versionUtils.js"; \ No newline at end of file diff --git a/packages/shared/src/index.server.ts b/packages/shared/src/index.server.ts index 1c45eb3cf..ce905c773 100644 --- a/packages/shared/src/index.server.ts +++ b/packages/shared/src/index.server.ts @@ -67,4 +67,10 @@ export { } from "./smtp.js"; export { SOURCEBOT_VERSION, -} from "./version.js"; \ No newline at end of file +} from "./version.js"; +export { + parseVersion, + formatVersion, + compareVersions, +} from "./versionUtils.js"; +export type { Version } from "./versionUtils.js"; \ No newline at end of file diff --git a/packages/shared/src/versionUtils.ts b/packages/shared/src/versionUtils.ts new file mode 100644 index 000000000..84cef78bf --- /dev/null +++ b/packages/shared/src/versionUtils.ts @@ -0,0 +1,36 @@ +const SEMVER_REGEX = /^v(\d+)\.(\d+)\.(\d+)$/; + +export type Version = { + major: number; + minor: number; + patch: number; +}; + +export const parseVersion = (version: string): Version | null => { + const match = version.match(SEMVER_REGEX); + if (!match) { + return null; + } + return { + major: parseInt(match[1]), + minor: parseInt(match[2]), + patch: parseInt(match[3]), + }; +}; + +export const formatVersion = (version: Version): string => { + return `v${version.major}.${version.minor}.${version.patch}`; +}; + +/** + * Returns < 0 if `a < b`, 0 if equal, > 0 if `a > b`. + */ +export const compareVersions = (a: Version, b: Version): number => { + if (a.major !== b.major) { + return a.major - b.major; + } + if (a.minor !== b.minor) { + return a.minor - b.minor; + } + return a.patch - b.patch; +}; diff --git a/packages/web/src/app/(app)/@sidebar/components/changelogEntryDialog.tsx b/packages/web/src/app/(app)/@sidebar/components/changelogEntryDialog.tsx new file mode 100644 index 000000000..0497a25f6 --- /dev/null +++ b/packages/web/src/app/(app)/@sidebar/components/changelogEntryDialog.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import type { ChangelogEntryDto } from "@/features/changelog/listEntriesApi"; +import { cn } from "@/lib/utils"; +import { format } from "date-fns"; +import { ArrowUpRight } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import Markdown from "react-markdown"; +import rehypeRaw from "rehype-raw"; +import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import remarkGfm from "remark-gfm"; +import { compareVersions, parseVersion, SOURCEBOT_VERSION } from "@sourcebot/shared/client"; + +const VIDEO_EXTENSIONS_RE = /\.(mp4|webm|ogg|mov)$/i; +const ABSOLUTE_URL_RE = /^(?:[a-z][a-z0-9+\-.]*:|\/\/|#)/i; + +// Allow