From c67f801f2fbf7ba6874c8bef5c2e5db41f0ee297 Mon Sep 17 00:00:00 2001 From: Gideon Date: Thu, 29 Jan 2026 18:30:20 -0600 Subject: [PATCH 1/2] feat(web): Add toggle for version upgrade notification (#815) --- .../migration.sql | 2 + packages/db/prisma/schema.prisma | 2 + packages/web/src/actions.ts | 49 ++++++++++++ packages/web/src/app/[domain]/layout.tsx | 14 +++- .../components/organizationAccessSettings.tsx | 7 ++ .../src/app/components/upgradeToastToggle.tsx | 79 +++++++++++++++++++ packages/web/src/types.ts | 1 + 7 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 packages/db/prisma/migrations/20260129233909_add_upgrade_toast_enabled/migration.sql create mode 100644 packages/web/src/app/components/upgradeToastToggle.tsx diff --git a/packages/db/prisma/migrations/20260129233909_add_upgrade_toast_enabled/migration.sql b/packages/db/prisma/migrations/20260129233909_add_upgrade_toast_enabled/migration.sql new file mode 100644 index 000000000..9f2fa2d86 --- /dev/null +++ b/packages/db/prisma/migrations/20260129233909_add_upgrade_toast_enabled/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Org" ADD COLUMN "upgradeToastEnabled" BOOLEAN NOT NULL DEFAULT true; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 6c0affcce..02de0452e 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -274,6 +274,8 @@ model Org { inviteLinkEnabled Boolean @default(false) inviteLinkId String? + upgradeToastEnabled Boolean @default(true) + audits Audit[] accountRequests AccountRequest[] diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 84ff76be3..08d0e2030 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -1810,6 +1810,55 @@ export const setAnonymousAccessStatus = async (domain: string, enabled: boolean) }); }); +export const getUpgradeToastEnabled = async (domain: string): Promise => sew(async () => { + const org = await getOrgFromDomain(domain); + if (!org) { + return { + statusCode: StatusCodes.NOT_FOUND, + errorCode: ErrorCode.NOT_FOUND, + message: "Organization not found", + } satisfies ServiceError; + } + + if (org.metadata === null) { + return true; + } + + const orgMetadata = getOrgMetadata(org); + if (!orgMetadata) { + return { + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + errorCode: ErrorCode.INVALID_ORG_METADATA, + message: "Invalid organization metadata", + } satisfies ServiceError; + } + + return orgMetadata.upgradeToastEnabled ?? true; +}); + +export const setUpgradeToastEnabled = async (domain: string, enabled: boolean): Promise => sew(async () => { + return await withAuth(async (userId) => { + return await withOrgMembership(userId, domain, async ({ org }) => { + const currentMetadata = getOrgMetadata(org); + const mergedMetadata = { + ...(currentMetadata ?? {}), + upgradeToastEnabled: enabled, + }; + + await prisma.org.update({ + where: { + id: org.id, + }, + data: { + metadata: mergedMetadata, + }, + }); + + return true; + }, OrgRole.OWNER); + }); +}); + export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => { const cookieStore = await cookies(); cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", { diff --git a/packages/web/src/app/[domain]/layout.tsx b/packages/web/src/app/[domain]/layout.tsx index de506b400..b576c654b 100644 --- a/packages/web/src/app/[domain]/layout.tsx +++ b/packages/web/src/app/[domain]/layout.tsx @@ -18,13 +18,14 @@ import { SubmitJoinRequest } from "./components/submitJoinRequest"; import { hasEntitlement } from "@sourcebot/shared"; import { env } from "@sourcebot/shared"; import { GcpIapAuth } from "./components/gcpIapAuth"; -import { getAnonymousAccessStatus, getMemberApprovalRequired } from "@/actions"; +import { getAnonymousAccessStatus, getMemberApprovalRequired, getUpgradeToastEnabled } from "@/actions"; import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard"; import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; import { GitHubStarToast } from "./components/githubStarToast"; import { UpgradeToast } from "./components/upgradeToast"; import { getLinkedAccountProviderStates } from "@/ee/features/permissionSyncing/actions"; import { LinkAccounts } from "@/ee/features/permissionSyncing/components/linkAccounts"; +import { OrgRole } from "@sourcebot/db"; interface LayoutProps { children: React.ReactNode, @@ -62,6 +63,10 @@ export default async function Layout(props: LayoutProps) { return status; })(); + const upgradeToastSetting = await getUpgradeToastEnabled(domain); + const upgradeToastEnabled = isServiceError(upgradeToastSetting) ? true : upgradeToastSetting; + let userRole: OrgRole | null = null; + // If the user is authenticated, we must check if they're a member of the org if (session) { const membership = await prisma.userToOrg.findUnique({ @@ -104,6 +109,8 @@ export default async function Layout(props: LayoutProps) { } } } + + userRole = membership.role; } else { // If the user isn't authenticated and anonymous access isn't enabled, we need to redirect them to the login page. if (!anonymousAccessEnabled) { @@ -142,7 +149,7 @@ export default async function Layout(props: LayoutProps) { ) } - + const hasUnlinkedProviders = linkedAccountProviderStates.some(state => state.isLinked === false); if (hasUnlinkedProviders) { const cookieStore = await cookies(); @@ -188,12 +195,13 @@ export default async function Layout(props: LayoutProps) { ) } + return ( {children} - + {(upgradeToastEnabled || userRole === OrgRole.OWNER) ? : null} ) } \ No newline at end of file diff --git a/packages/web/src/app/components/organizationAccessSettings.tsx b/packages/web/src/app/components/organizationAccessSettings.tsx index 7e826c4b7..28d7bed4e 100644 --- a/packages/web/src/app/components/organizationAccessSettings.tsx +++ b/packages/web/src/app/components/organizationAccessSettings.tsx @@ -1,6 +1,7 @@ import { createInviteLink } from "@/lib/utils" import { getBaseUrl } from "@/lib/utils.server" import { AnonymousAccessToggle } from "./anonymousAccessToggle" +import { UpgradeToastToggle } from "./upgradeToastToggle" import { OrganizationAccessSettingsWrapper } from "./organizationAccessSettingsWrapper" import { getOrgFromDomain } from "@/data/org" import { getOrgMetadata } from "@/lib/utils" @@ -17,6 +18,7 @@ export async function OrganizationAccessSettings() { const metadata = getOrgMetadata(org); const anonymousAccessEnabled = metadata?.anonymousAccessEnabled ?? false; + const upgradeToastEnabled = metadata?.upgradeToastEnabled ?? true; const headersList = await headers(); const baseUrl = getBaseUrl(headersList); @@ -39,6 +41,11 @@ export async function OrganizationAccessSettings() { inviteLinkEnabled={org.inviteLinkEnabled} inviteLink={inviteLink} /> + + + ) } \ No newline at end of file diff --git a/packages/web/src/app/components/upgradeToastToggle.tsx b/packages/web/src/app/components/upgradeToastToggle.tsx new file mode 100644 index 000000000..f6920ce63 --- /dev/null +++ b/packages/web/src/app/components/upgradeToastToggle.tsx @@ -0,0 +1,79 @@ +"use client" + +import { useState } from "react" +import { Switch } from "@/components/ui/switch" +import { setUpgradeToastEnabled } from "@/actions" +import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" +import { isServiceError } from "@/lib/utils" +import { useToast } from "@/components/hooks/use-toast" + +interface UpgradeToastToggleProps { + upgradeToastEnabled: boolean + onToggleChange?: (checked: boolean) => void +} + +export function UpgradeToastToggle({ upgradeToastEnabled, onToggleChange }: UpgradeToastToggleProps) { + const [enabled, setEnabled] = useState(upgradeToastEnabled) + const [isLoading, setIsLoading] = useState(false) + const { toast } = useToast() + + const handleToggle = async (checked: boolean) => { + setIsLoading(true) + try { + const result = await setUpgradeToastEnabled(SINGLE_TENANT_ORG_DOMAIN, checked) + + if (isServiceError(result)) { + toast({ + title: "Error", + description: result.message || "Failed to update upgrade notification setting", + variant: "destructive", + }) + return + } + + setEnabled(checked) + onToggleChange?.(checked) + + toast({ + title: "Success", + description: checked + ? "Version upgrade notifications enabled" + : "Version upgrade notifications disabled", + }) + } catch (error) { + console.error("Error updating upgrade toast setting:", error) + toast({ + title: "Error", + description: "Failed to update upgrade notification setting", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + return ( +
+
+
+

+ Version upgrade notifications +

+
+

+ When enabled, users will see a notification when a new version of Sourcebot is available on GitHub. + Otherwise, only the owner will be notified when a new version becomes available. +

+
+
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts index 2ceb5d30e..0c4b8f941 100644 --- a/packages/web/src/types.ts +++ b/packages/web/src/types.ts @@ -2,6 +2,7 @@ import { z } from "zod"; export const orgMetadataSchema = z.object({ anonymousAccessEnabled: z.boolean().optional(), + upgradeToastEnabled: z.boolean().optional(), }) export const demoSearchScopeSchema = z.object({ From caaf4f0bc160b8bd1973a367003098fe689beff9 Mon Sep 17 00:00:00 2001 From: Gideon Date: Thu, 29 Jan 2026 18:53:39 -0600 Subject: [PATCH 2/2] fix: Remove unneeded db column --- .../20260129233909_add_upgrade_toast_enabled/migration.sql | 2 -- packages/db/prisma/schema.prisma | 2 -- 2 files changed, 4 deletions(-) delete mode 100644 packages/db/prisma/migrations/20260129233909_add_upgrade_toast_enabled/migration.sql diff --git a/packages/db/prisma/migrations/20260129233909_add_upgrade_toast_enabled/migration.sql b/packages/db/prisma/migrations/20260129233909_add_upgrade_toast_enabled/migration.sql deleted file mode 100644 index 9f2fa2d86..000000000 --- a/packages/db/prisma/migrations/20260129233909_add_upgrade_toast_enabled/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Org" ADD COLUMN "upgradeToastEnabled" BOOLEAN NOT NULL DEFAULT true; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 02de0452e..6c0affcce 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -274,8 +274,6 @@ model Org { inviteLinkEnabled Boolean @default(false) inviteLinkId String? - upgradeToastEnabled Boolean @default(true) - audits Audit[] accountRequests AccountRequest[]