chore(web): Upgrade to NextJS 15 (#477)

This commit is contained in:
Brendan Kellam 2025-08-22 14:48:29 -04:00 committed by GitHub
parent b36de3412d
commit d9fa221d72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1346 additions and 741 deletions

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed
- Updated NextJS to version 15. [#477](https://github.com/sourcebot-dev/sourcebot/pull/477)
## [4.6.4] - 2025-08-11 ## [4.6.4] - 2025-08-11
### Added ### Added

View file

@ -24,5 +24,8 @@
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"npm-run-all": "^4.1.5" "npm-run-all": "^4.1.5"
}, },
"packageManager": "yarn@4.7.0" "packageManager": "yarn@4.7.0",
"resolutions": {
"prettier": "3.5.3"
}
} }

View file

@ -15,7 +15,7 @@
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/node": "^22.7.5", "@types/node": "^22.7.5",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"json-schema-to-typescript": "^15.0.2", "json-schema-to-typescript": "^15.0.4",
"tsc-watch": "^6.2.0", "tsc-watch": "^6.2.0",
"tsx": "^4.19.1", "tsx": "^4.19.1",
"typescript": "^5.6.2", "typescript": "^5.6.2",

View file

@ -1,2 +1,3 @@
# shadcn components # shadcn components
src/components/ src/components/
next-env.d.ts

View file

@ -38,6 +38,8 @@ const nextConfig = {
}, },
] ]
}, },
turbopack: {}
}; };
export default withSentryConfig(nextConfig, { export default withSentryConfig(nextConfig, {

View file

@ -3,10 +3,10 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev --turbopack",
"build": "cross-env SKIP_ENV_VALIDATION=1 next build", "build": "cross-env SKIP_ENV_VALIDATION=1 next build",
"start": "next start", "start": "next start",
"lint": "cross-env SKIP_ENV_VALIDATION=1 next lint", "lint": "cross-env SKIP_ENV_VALIDATION=1 eslint .",
"test": "vitest", "test": "vitest",
"dev:emails": "email dev --dir ./src/emails", "dev:emails": "email dev --dir ./src/emails",
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe" "stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe"
@ -146,7 +146,7 @@
"langfuse-vercel": "^3.38.4", "langfuse-vercel": "^3.38.4",
"lucide-react": "^0.517.0", "lucide-react": "^0.517.0",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"next": "14.2.30", "next": "15.5.0",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"next-navigation-guard": "^0.2.0", "next-navigation-guard": "^0.2.0",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
@ -157,9 +157,9 @@
"posthog-js": "^1.161.5", "posthog-js": "^1.161.5",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"psl": "^1.15.0", "psl": "^1.15.0",
"react": "^18", "react": "19.1.1",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^18", "react-dom": "19.1.1",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",
"react-hotkeys-hook": "^4.5.1", "react-hotkeys-hook": "^4.5.1",
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
@ -187,21 +187,23 @@
"zod-to-json-schema": "^3.24.5" "zod-to-json-schema": "^3.24.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3",
"@tanstack/eslint-plugin-query": "^5.74.7", "@tanstack/eslint-plugin-query": "^5.74.7",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/psl": "^1.1.3", "@types/psl": "^1.1.3",
"@types/react": "^18", "@types/react": "19.1.10",
"@types/react-dom": "^18", "@types/react-dom": "19.1.7",
"@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.3.0", "@typescript-eslint/parser": "^8.40.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.2.6", "eslint-config-next": "15.5.0",
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^5.2.0",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss": "^8", "postcss": "^8",
@ -211,5 +213,9 @@
"typescript": "^5", "typescript": "^5",
"vite-tsconfig-paths": "^5.1.3", "vite-tsconfig-paths": "^5.1.3",
"vitest": "^2.1.5" "vitest": "^2.1.5"
},
"resolutions": {
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7"
} }
} }

View file

@ -186,7 +186,7 @@ export const withTenancyModeEnforcement = async<T>(mode: TenancyMode, fn: () =>
////// Actions /////// ////// Actions ///////
export const createOrg = (name: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() => export const createOrg = async (name: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() =>
withTenancyModeEnforcement('multi', () => withTenancyModeEnforcement('multi', () =>
withAuth(async (userId) => { withAuth(async (userId) => {
const org = await prisma.org.create({ const org = await prisma.org.create({
@ -293,7 +293,7 @@ export const completeOnboarding = async (domain: string): Promise<{ success: boo
}) })
)); ));
export const getSecrets = (domain: string): Promise<{ createdAt: Date; key: string; }[] | ServiceError> => sew(() => export const getSecrets = async (domain: string): Promise<{ createdAt: Date; key: string; }[] | ServiceError> => sew(() =>
withAuth((userId) => withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => { withOrgMembership(userId, domain, async ({ org }) => {
const secrets = await prisma.secret.findMany({ const secrets = await prisma.secret.findMany({
@ -1990,7 +1990,7 @@ export const rejectAccountRequest = async (requestId: string, domain: string) =>
)); ));
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => { export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
await cookies().set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true'); await (await cookies()).set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
return true; return true;
}); });

View file

@ -13,7 +13,13 @@ const agents = [
}, },
]; ];
export default function AgentsPage({ params: { domain } }: { params: { domain: string } }) { export default async function AgentsPage(props: { params: Promise<{ domain: string }> }) {
const params = await props.params;
const {
domain
} = params;
return ( return (
<div className="flex flex-col items-center overflow-hidden min-h-screen"> <div className="flex flex-col items-center overflow-hidden min-h-screen">
<NavigationMenu domain={domain} /> <NavigationMenu domain={domain} />

View file

@ -5,13 +5,20 @@ import { Loader2 } from "lucide-react";
import { TreePreviewPanel } from "./components/treePreviewPanel"; import { TreePreviewPanel } from "./components/treePreviewPanel";
interface BrowsePageProps { interface BrowsePageProps {
params: { params: Promise<{
path: string[]; path: string[];
domain: string; domain: string;
}; }>;
} }
export default async function BrowsePage({ params: { path: _rawPath, domain } }: BrowsePageProps) { export default async function BrowsePage(props: BrowsePageProps) {
const params = await props.params;
const {
path: _rawPath,
domain
} = params;
const rawPath = decodeURIComponent(_rawPath.join('/')); const rawPath = decodeURIComponent(_rawPath.join('/'));
const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath); const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath);

View file

@ -13,13 +13,14 @@ import { ChatSidePanel } from '../components/chatSidePanel';
import { ResizablePanelGroup } from '@/components/ui/resizable'; import { ResizablePanelGroup } from '@/components/ui/resizable';
interface PageProps { interface PageProps {
params: { params: Promise<{
domain: string; domain: string;
id: string; id: string;
}; }>;
} }
export default async function Page({ params }: PageProps) { export default async function Page(props: PageProps) {
const params = await props.params;
const languageModels = await getConfiguredLanguageModelsInfo(); const languageModels = await getConfiguredLanguageModelsInfo();
const repos = await getRepos(params.domain); const repos = await getRepos(params.domain);
const searchContexts = await getSearchContexts(params.domain); const searchContexts = await getSearchContexts(params.domain);

View file

@ -10,12 +10,13 @@ import { auth } from "@/auth";
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
interface PageProps { interface PageProps {
params: { params: Promise<{
domain: string; domain: string;
}; }>;
} }
export default async function Page({ params }: PageProps) { export default async function Page(props: PageProps) {
const params = await props.params;
const languageModels = await getConfiguredLanguageModelsInfo(); const languageModels = await getConfiguredLanguageModelsInfo();
const repos = await getRepos(params.domain); const repos = await getRepos(params.domain);
const searchContexts = await getSearchContexts(params.domain); const searchContexts = await getSearchContexts(params.domain);

View file

@ -57,43 +57,48 @@ export const NavigationMenu = async ({
<NavigationMenuBase> <NavigationMenuBase>
<NavigationMenuList> <NavigationMenuList>
<NavigationMenuItem> <NavigationMenuItem>
<Link href={`/${domain}`} legacyBehavior passHref> <NavigationMenuLink
<NavigationMenuLink className={navigationMenuTriggerStyle()}> href={`/${domain}`}
Search className={navigationMenuTriggerStyle()}
</NavigationMenuLink> >
</Link> Search
</NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem> <NavigationMenuItem>
<Link href={`/${domain}/repos`} legacyBehavior passHref> <NavigationMenuLink
<NavigationMenuLink className={navigationMenuTriggerStyle()}> href={`/${domain}/repos`}
Repositories className={navigationMenuTriggerStyle()}
</NavigationMenuLink> >
</Link> Repositories
</NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
{isAuthenticated && ( {isAuthenticated && (
<> <>
{env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === undefined && ( {env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === undefined && (
<NavigationMenuItem> <NavigationMenuItem>
<Link href={`/${domain}/agents`} legacyBehavior passHref> <NavigationMenuLink
<NavigationMenuLink className={navigationMenuTriggerStyle()}> href={`/${domain}/agents`}
Agents className={navigationMenuTriggerStyle()}
</NavigationMenuLink> >
</Link> Agents
</NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
)} )}
<NavigationMenuItem> <NavigationMenuItem>
<Link href={`/${domain}/connections`} legacyBehavior passHref> <NavigationMenuLink
<NavigationMenuLink className={navigationMenuTriggerStyle()}> href={`/${domain}/connections`}
Connections className={navigationMenuTriggerStyle()}
</NavigationMenuLink> >
</Link> Connections
</NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem> <NavigationMenuItem>
<Link href={`/${domain}/settings`} legacyBehavior passHref> <NavigationMenuLink
<NavigationMenuLink className={navigationMenuTriggerStyle()}> href={`/${domain}/settings`}
Settings className={navigationMenuTriggerStyle()}
</NavigationMenuLink> >
</Link> Settings
</NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
</> </>
)} )}

View file

@ -25,16 +25,18 @@ import { CodeHostType } from "@/lib/utils"
import { env } from "@/env.mjs" import { env } from "@/env.mjs"
interface ConnectionManagementPageProps { interface ConnectionManagementPageProps {
params: { params: Promise<{
domain: string domain: string
id: string id: string
}, }>,
searchParams: { searchParams: Promise<{
tab: string tab: string
} }>
} }
export default async function ConnectionManagementPage({ params, searchParams }: ConnectionManagementPageProps) { export default async function ConnectionManagementPage(props: ConnectionManagementPageProps) {
const searchParams = await props.searchParams;
const params = await props.params;
const connection = await getConnectionByDomain(Number(params.id), params.domain); const connection = await getConnectionByDomain(Number(params.id), params.domain);
if (!connection) { if (!connection) {
return <NotFound className="flex w-full h-full items-center justify-center" message="Connection not found" /> return <NotFound className="flex w-full h-full items-center justify-center" message="Connection not found" />

View file

@ -2,13 +2,24 @@ import { auth } from "@/auth";
import { NavigationMenu } from "../components/navigationMenu"; import { NavigationMenu } from "../components/navigationMenu";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default async function Layout({ interface LayoutProps {
children,
params: { domain },
}: Readonly<{
children: React.ReactNode; children: React.ReactNode;
params: { domain: string }; params: Promise<{ domain: string }>;
}>) { }
export default async function Layout(
props: LayoutProps
) {
const params = await props.params;
const {
domain
} = params;
const {
children
} = props;
const session = await auth(); const session = await auth();
if (!session) { if (!session) {
return redirect(`/${domain}`); return redirect(`/${domain}`);

View file

@ -9,12 +9,11 @@ import {
BitbucketCloudConnectionCreationForm, BitbucketCloudConnectionCreationForm,
BitbucketDataCenterConnectionCreationForm BitbucketDataCenterConnectionCreationForm
} from "@/app/[domain]/components/connectionCreationForms"; } from "@/app/[domain]/components/connectionCreationForms";
import { useCallback } from "react"; import { useCallback, use } from "react";
import { useDomain } from "@/hooks/useDomain"; import { useDomain } from "@/hooks/useDomain";
export default function NewConnectionPage({ export default function NewConnectionPage(props: { params: Promise<{ type: string }> }) {
params const params = use(props.params);
}: { params: { type: string } }) {
const { type } = params; const { type } = params;
const router = useRouter(); const router = useRouter();
const domain = useDomain(); const domain = useDomain();
@ -46,7 +45,7 @@ export default function NewConnectionPage({
if (type === 'bitbucket-server') { if (type === 'bitbucket-server') {
return <BitbucketDataCenterConnectionCreationForm onCreated={onCreated} />; return <BitbucketDataCenterConnectionCreationForm onCreated={onCreated} />;
} }
router.push(`/${domain}/connections`); router.push(`/${domain}/connections`);
} }

View file

@ -7,7 +7,13 @@ import { notFound, ServiceErrorException } from "@/lib/serviceError";
import { OrgRole } from "@sourcebot/db"; import { OrgRole } from "@sourcebot/db";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
export default async function ConnectionsPage({ params: { domain } }: { params: { domain: string } }) { export default async function ConnectionsPage(props: { params: Promise<{ domain: string }> }) {
const params = await props.params;
const {
domain
} = params;
const connections = await getConnections(domain); const connections = await getConnections(domain);
if (isServiceError(connections)) { if (isServiceError(connections)) {
throw new ServiceErrorException(connections); throw new ServiceErrorException(connections);
@ -15,7 +21,7 @@ export default async function ConnectionsPage({ params: { domain } }: { params:
const membership = await getOrgMembership(domain); const membership = await getOrgMembership(domain);
if (isServiceError(membership)) { if (isServiceError(membership)) {
return notFound(); throw new ServiceErrorException(notFound());
} }
return ( return (

View file

@ -25,13 +25,20 @@ import { GitHubStarToast } from "./components/githubStarToast";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode, children: React.ReactNode,
params: { domain: string } params: Promise<{ domain: string }>
} }
export default async function Layout({ export default async function Layout(props: LayoutProps) {
children, const params = await props.params;
params: { domain },
}: LayoutProps) { const {
domain
} = params;
const {
children
} = props;
const org = await getOrgFromDomain(domain); const org = await getOrgFromDomain(domain);
if (!org) { if (!org) {
@ -39,8 +46,19 @@ export default async function Layout({
} }
const session = await auth(); const session = await auth();
const anonymousAccessEnabled = hasEntitlement("anonymous-access") && await getAnonymousAccessStatus(domain); const anonymousAccessEnabled = await (async () => {
if (!hasEntitlement("anonymous-access")) {
return false;
}
const status = await getAnonymousAccessStatus(domain);
if (isServiceError(status)) {
return false;
}
return status;
})();
// If the user is authenticated, we must check if they're a member of the org // If the user is authenticated, we must check if they're a member of the org
if (session) { if (session) {
const membership = await prisma.userToOrg.findUnique({ const membership = await prisma.userToOrg.findUnique({

View file

@ -15,7 +15,13 @@ import { env } from "@/env.mjs";
import { loadJsonFile } from "@sourcebot/shared"; import { loadJsonFile } from "@sourcebot/shared";
import { DemoExamples, demoExamplesSchema } from "@/types"; import { DemoExamples, demoExamplesSchema } from "@/types";
export default async function Home({ params: { domain } }: { params: { domain: string } }) { export default async function Home(props: { params: Promise<{ domain: string }> }) {
const params = await props.params;
const {
domain
} = params;
const org = await getOrgFromDomain(domain); const org = await getOrgFromDomain(domain);
if (!org) { if (!org) {
return <PageNotFound /> return <PageNotFound />

View file

@ -1,12 +1,22 @@
import { NavigationMenu } from "../components/navigationMenu"; import { NavigationMenu } from "../components/navigationMenu";
export default function Layout({ interface LayoutProps {
children,
params: { domain },
}: Readonly<{
children: React.ReactNode; children: React.ReactNode;
params: { domain: string }; params: Promise<{ domain: string }>;
}>) { }
export default async function Layout(
props: LayoutProps
) {
const params = await props.params;
const {
domain
} = params;
const {
children
} = props;
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">

View file

@ -4,7 +4,13 @@ import { PageNotFound } from "../components/pageNotFound";
import { Header } from "../components/header"; import { Header } from "../components/header";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
export default async function ReposPage({ params: { domain } }: { params: { domain: string } }) { export default async function ReposPage(props: { params: Promise<{ domain: string }> }) {
const params = await props.params;
const {
domain
} = params;
const org = await getOrgFromDomain(domain); const org = await getOrgFromDomain(domain);
if (!org) { if (!org) {
return <PageNotFound /> return <PageNotFound />

View file

@ -8,12 +8,18 @@ import { ErrorCode } from "@/lib/errorCodes";
import { headers } from "next/headers"; import { headers } from "next/headers";
interface GeneralSettingsPageProps { interface GeneralSettingsPageProps {
params: { params: Promise<{
domain: string; domain: string;
} }>
} }
export default async function GeneralSettingsPage({ params: { domain } }: GeneralSettingsPageProps) { export default async function GeneralSettingsPage(props: GeneralSettingsPageProps) {
const params = await props.params;
const {
domain
} = params;
const currentUserRole = await getCurrentUserRole(domain) const currentUserRole = await getCurrentUserRole(domain)
if (isServiceError(currentUserRole)) { if (isServiceError(currentUserRole)) {
throw new ServiceErrorException(currentUserRole); throw new ServiceErrorException(currentUserRole);

View file

@ -7,12 +7,18 @@ import { OrgRole } from "@sourcebot/db";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
interface AccessPageProps { interface AccessPageProps {
params: { params: Promise<{
domain: string; domain: string;
} }>
} }
export default async function AccessPage({ params: { domain } }: AccessPageProps) { export default async function AccessPage(props: AccessPageProps) {
const params = await props.params;
const {
domain
} = params;
const org = await getOrgFromDomain(domain); const org = await getOrgFromDomain(domain);
if (!org) { if (!org) {
throw new Error("Organization not found"); throw new Error("Organization not found");

View file

@ -16,14 +16,18 @@ export const metadata: Metadata = {
} }
interface BillingPageProps { interface BillingPageProps {
params: { params: Promise<{
domain: string domain: string
} }>
} }
export default async function BillingPage({ export default async function BillingPage(props: BillingPageProps) {
params: { domain }, const params = await props.params;
}: BillingPageProps) {
const {
domain
} = params;
if (!IS_BILLING_ENABLED) { if (!IS_BILLING_ENABLED) {
notFound(); notFound();
} }

View file

@ -13,17 +13,28 @@ import { getOrgFromDomain } from "@/data/org";
import { OrgRole } from "@prisma/client"; import { OrgRole } from "@prisma/client";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
interface LayoutProps {
children: React.ReactNode;
params: Promise<{ domain: string }>;
}
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Settings", title: "Settings",
} }
export default async function SettingsLayout({ export default async function SettingsLayout(
children, props: LayoutProps
params: { domain }, ) {
}: Readonly<{ const params = await props.params;
children: React.ReactNode;
params: { domain: string }; const {
}>) { domain
} = params;
const {
children
} = props;
const session = await auth(); const session = await auth();
if (!session) { if (!session) {
return redirect(`/${domain}`); return redirect(`/${domain}`);

View file

@ -7,12 +7,18 @@ import { notFound, ServiceErrorException } from "@/lib/serviceError";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
interface LicensePageProps { interface LicensePageProps {
params: { params: Promise<{
domain: string; domain: string;
} }>
} }
export default async function LicensePage({ params: { domain } }: LicensePageProps) { export default async function LicensePage(props: LicensePageProps) {
const params = await props.params;
const {
domain
} = params;
if (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined) { if (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined) {
notFound(); notFound();
} }

View file

@ -15,15 +15,27 @@ import { OrgRole } from "@prisma/client";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
interface MembersSettingsPageProps { interface MembersSettingsPageProps {
params: { params: Promise<{
domain: string domain: string
}, }>,
searchParams: { searchParams: Promise<{
tab?: string tab?: string
} }>
} }
export default async function MembersSettingsPage({ params: { domain }, searchParams: { tab } }: MembersSettingsPageProps) { export default async function MembersSettingsPage(props: MembersSettingsPageProps) {
const searchParams = await props.searchParams;
const {
tab
} = searchParams;
const params = await props.params;
const {
domain
} = params;
const org = await getOrgFromDomain(domain); const org = await getOrgFromDomain(domain);
if (!org) { if (!org) {
throw new Error("Organization not found"); throw new Error("Organization not found");

View file

@ -5,12 +5,18 @@ import { ImportSecretCard } from "./components/importSecretCard";
import { ServiceErrorException } from "@/lib/serviceError"; import { ServiceErrorException } from "@/lib/serviceError";
interface SecretsPageProps { interface SecretsPageProps {
params: { params: Promise<{
domain: string; domain: string;
} }>
} }
export default async function SecretsPage({ params: { domain } }: SecretsPageProps) { export default async function SecretsPage(props: SecretsPageProps) {
const params = await props.params;
const {
domain
} = params;
const secrets = await getSecrets(domain); const secrets = await getSecrets(domain);
if (isServiceError(secrets)) { if (isServiceError(secrets)) {
throw new ServiceErrorException(secrets); throw new ServiceErrorException(secrets);

View file

@ -12,7 +12,13 @@ import { env } from "@/env.mjs";
import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
import { getSubscriptionInfo } from "@/ee/features/billing/actions"; import { getSubscriptionInfo } from "@/ee/features/billing/actions";
export default async function Upgrade({ params: { domain } }: { params: { domain: string } }) { export default async function Upgrade(props: { params: Promise<{ domain: string }> }) {
const params = await props.params;
const {
domain
} = params;
if (!IS_BILLING_ENABLED) { if (!IS_BILLING_ENABLED) {
redirect(`/${domain}`); redirect(`/${domain}`);
} }

View file

@ -11,7 +11,7 @@ const logger = createLogger('stripe-webhook');
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const body = await req.text(); const body = await req.text();
const signature = headers().get('stripe-signature'); const signature = (await headers()).get('stripe-signature');
if (!signature) { if (!signature) {
return new Response('No signature', { status: 400 }); return new Response('No signature', { status: 400 });

View file

@ -4,8 +4,9 @@ import { NextRequest } from "next/server";
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: { domain: string; repoId: string } } props: { params: Promise<{ domain: string; repoId: string }> }
) { ) {
const params = await props.params;
const { domain, repoId } = params; const { domain, repoId } = params;
const repoIdNum = parseInt(repoId); const repoIdNum = parseInt(repoId);

View file

@ -17,7 +17,7 @@ export async function OrganizationAccessSettings() {
const metadata = getOrgMetadata(org); const metadata = getOrgMetadata(org);
const anonymousAccessEnabled = metadata?.anonymousAccessEnabled ?? false; const anonymousAccessEnabled = metadata?.anonymousAccessEnabled ?? false;
const headersList = headers(); const headersList = await headers();
const baseUrl = getBaseUrl(headersList); const baseUrl = getBaseUrl(headersList);
const inviteLink = createInviteLink(baseUrl, org.inviteLinkId) const inviteLink = createInviteLink(baseUrl, org.inviteLinkId)

View file

@ -1,9 +1,9 @@
@import "./codemirror-styles.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import "./codemirror-styles.css";
@layer base { @layer base {
html { html {
overflow-y: scroll; overflow-y: scroll;

View file

@ -9,7 +9,7 @@ import { prisma } from "@/prisma";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { ErrorCode } from "@/lib/errorCodes"; import { ErrorCode } from "@/lib/errorCodes";
export const joinOrganization = (orgId: number, inviteLinkId?: string) => sew(async () => export const joinOrganization = async (orgId: number, inviteLinkId?: string) => sew(async () =>
withAuth(async (userId) => { withAuth(async (userId) => {
const org = await prisma.org.findUnique({ const org = await prisma.org.findUnique({
where: { where: {

View file

@ -11,12 +11,13 @@ import { getAuthProviders } from "@/lib/authProviders";
import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard"; import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
interface InvitePageProps { interface InvitePageProps {
searchParams: { searchParams: Promise<{
id?: string; id?: string;
}; }>;
} }
export default async function InvitePage({ searchParams }: InvitePageProps) { export default async function InvitePage(props: InvitePageProps) {
const searchParams = await props.searchParams;
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
if (!org || !org.isOnboarded) { if (!org || !org.isOnboarded) {
return redirect("/onboard"); return redirect("/onboard");

View file

@ -10,13 +10,14 @@ import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
const logger = createLogger('login-page'); const logger = createLogger('login-page');
interface LoginProps { interface LoginProps {
searchParams: { searchParams: Promise<{
callbackUrl?: string; callbackUrl?: string;
error?: string; error?: string;
} }>
} }
export default async function Login({ searchParams }: LoginProps) { export default async function Login(props: LoginProps) {
const searchParams = await props.searchParams;
logger.info("Login page loaded"); logger.info("Login page loaded");
const session = await auth(); const session = await auth();
if (session) { if (session) {

View file

@ -1,4 +1,5 @@
import type React from "react" import type React from "react"
import Link from "next/link"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@ -20,7 +21,7 @@ import { env } from "@/env.mjs";
import { GcpIapAuth } from "@/app/[domain]/components/gcpIapAuth"; import { GcpIapAuth } from "@/app/[domain]/components/gcpIapAuth";
interface OnboardingProps { interface OnboardingProps {
searchParams?: { step?: string }; searchParams?: Promise<{ step?: string }>;
} }
interface OnboardingStep { interface OnboardingStep {
@ -38,7 +39,8 @@ interface ResourceCard {
icon?: React.ReactNode icon?: React.ReactNode
} }
export default async function Onboarding({ searchParams }: OnboardingProps) { export default async function Onboarding(props: OnboardingProps) {
const searchParams = await props.searchParams;
const providers = getAuthProviders(); const providers = getAuthProviders();
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
const session = await auth(); const session = await auth();
@ -118,7 +120,7 @@ export default async function Onboarding({ searchParams }: OnboardingProps) {
component: ( component: (
<div className="space-y-6"> <div className="space-y-6">
<Button asChild className="w-full"> <Button asChild className="w-full">
<a href="/onboard?step=1">Get Started </a> <Link href="/onboard?step=1">Get Started </Link>
</Button> </Button>
</div> </div>
), ),
@ -170,7 +172,7 @@ export default async function Onboarding({ searchParams }: OnboardingProps) {
<div className="space-y-6"> <div className="space-y-6">
<OrganizationAccessSettings /> <OrganizationAccessSettings />
<Button asChild className="w-full"> <Button asChild className="w-full">
<a href="/onboard?step=3">Continue </a> <Link href="/onboard?step=3">Continue </Link>
</Button> </Button>
</div> </div>
), ),

View file

@ -9,12 +9,13 @@ import { getOrgFromDomain } from '@/data/org';
import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants'; import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants';
interface RedeemPageProps { interface RedeemPageProps {
searchParams: { searchParams: Promise<{
invite_id?: string; invite_id?: string;
}; }>;
} }
export default async function RedeemPage({ searchParams }: RedeemPageProps) { export default async function RedeemPage(props: RedeemPageProps) {
const searchParams = await props.searchParams;
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
if (!org || !org.isOnboarded) { if (!org || !org.isOnboarded) {
return redirect("/onboard"); return redirect("/onboard");

View file

@ -10,13 +10,14 @@ import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
const logger = createLogger('signup-page'); const logger = createLogger('signup-page');
interface LoginProps { interface LoginProps {
searchParams: { searchParams: Promise<{
callbackUrl?: string; callbackUrl?: string;
error?: string; error?: string;
} }>
} }
export default async function Signup({ searchParams }: LoginProps) { export default async function Signup(props: LoginProps) {
const searchParams = await props.searchParams;
const session = await auth(); const session = await auth();
if (session) { if (session) {
logger.info("Session found in signup page, redirecting to home"); logger.info("Session found in signup page, redirecting to home");

View file

@ -245,7 +245,9 @@ export const ReferencedSourcesListView = ({
repoWebUrl={fileData.repositoryWebUrl} repoWebUrl={fileData.repositoryWebUrl}
fileName={fileData.path} fileName={fileData.path}
references={referencesInFile} references={referencesInFile}
ref={(ref) => setEditorRef(fileId, ref)} ref={ref => {
setEditorRef(fileId, ref);
}}
onSelectedReferenceChanged={onSelectedReferenceChanged} onSelectedReferenceChanged={onSelectedReferenceChanged}
onHoveredReferenceChanged={onHoveredReferenceChanged} onHoveredReferenceChanged={onHoveredReferenceChanged}
selectedReference={selectedReference} selectedReference={selectedReference}
@ -280,6 +282,5 @@ export const ReferencedSourcesListView = ({
})} })}
</div> </div>
</ScrollArea> </ScrollArea>
); );
} }

View file

@ -1,5 +1,5 @@
import { expect, test } from 'vitest' import { expect, test } from 'vitest'
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react';
import { useExtractReferences } from './useExtractReferences'; import { useExtractReferences } from './useExtractReferences';
import { getFileReferenceId } from './utils'; import { getFileReferenceId } from './utils';
import { TextUIPart } from 'ai'; import { TextUIPart } from 'ai';

View file

@ -1,7 +1,7 @@
import { expect, test } from 'vitest' import { expect, test } from 'vitest'
import { SBChatMessage } from './types'; import { SBChatMessage } from './types';
import { useMessagePairs } from './useMessagePairs'; import { useMessagePairs } from './useMessagePairs';
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react';
test('useMessagePairs pairs user and assistant messages', () => { test('useMessagePairs pairs user and assistant messages', () => {
const userMessage: SBChatMessage = { const userMessage: SBChatMessage = {

View file

@ -22,7 +22,7 @@ export const FileTreeItemComponent = ({
isCollapsed?: boolean, isCollapsed?: boolean,
isCollapseChevronVisible?: boolean, isCollapseChevronVisible?: boolean,
onClick: () => void, onClick: () => void,
parentRef: React.RefObject<HTMLDivElement>, parentRef: React.RefObject<HTMLDivElement | null>,
}) => { }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);

View file

@ -1,164 +1,164 @@
import type { Config } from "tailwindcss" import type { Config } from "tailwindcss"
const config = { const config = {
darkMode: ["class"], darkMode: ["class"],
content: [ content: [
'./pages/**/*.{ts,tsx}', './pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}', './components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}', './app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}', './src/**/*.{ts,tsx}',
], ],
prefix: "", prefix: "",
theme: { theme: {
container: { container: {
center: true, center: true,
padding: '2rem', padding: '2rem',
screens: { screens: {
'2xl': '1400px' '2xl': '1400px'
} }
}, },
extend: { extend: {
colors: { colors: {
border: 'var(--border)', border: 'var(--border)',
input: 'var(--input)', input: 'var(--input)',
ring: 'var(--ring)', ring: 'var(--ring)',
background: 'var(--background)', background: 'var(--background)',
backgroundSecondary: 'var(--background-secondary)', backgroundSecondary: 'var(--background-secondary)',
foreground: 'var(--foreground)', foreground: 'var(--foreground)',
primary: { primary: {
DEFAULT: 'var(--primary)', DEFAULT: 'var(--primary)',
foreground: 'var(--primary-foreground)' foreground: 'var(--primary-foreground)'
}, },
secondary: { secondary: {
DEFAULT: 'var(--secondary)', DEFAULT: 'var(--secondary)',
foreground: 'var(--secondary-foreground)' foreground: 'var(--secondary-foreground)'
}, },
destructive: { destructive: {
DEFAULT: 'var(--destructive)', DEFAULT: 'var(--destructive)',
foreground: 'var(--destructive-foreground)' foreground: 'var(--destructive-foreground)'
}, },
muted: { muted: {
DEFAULT: 'var(--muted)', DEFAULT: 'var(--muted)',
foreground: 'var(--muted-foreground)', foreground: 'var(--muted-foreground)',
accent: 'var(--muted-accent)' accent: 'var(--muted-accent)'
}, },
accent: { accent: {
DEFAULT: 'var(--accent)', DEFAULT: 'var(--accent)',
foreground: 'var(--accent-foreground)' foreground: 'var(--accent-foreground)'
}, },
popover: { popover: {
DEFAULT: 'var(--popover)', DEFAULT: 'var(--popover)',
foreground: 'var(--popover-foreground)' foreground: 'var(--popover-foreground)'
}, },
card: { card: {
DEFAULT: 'var(--card)', DEFAULT: 'var(--card)',
foreground: 'var(--card-foreground)' foreground: 'var(--card-foreground)'
}, },
highlight: 'var(--highlight)', highlight: 'var(--highlight)',
link: 'var(--link)', link: 'var(--link)',
sidebar: { sidebar: {
DEFAULT: 'var(--sidebar-background)', DEFAULT: 'var(--sidebar-background)',
foreground: 'var(--sidebar-foreground)', foreground: 'var(--sidebar-foreground)',
primary: 'var(--sidebar-primary)', primary: 'var(--sidebar-primary)',
'primary-foreground': 'var(--sidebar-primary-foreground)', 'primary-foreground': 'var(--sidebar-primary-foreground)',
accent: 'var(--sidebar-accent)', accent: 'var(--sidebar-accent)',
'accent-foreground': 'var(--sidebar-accent-foreground)', 'accent-foreground': 'var(--sidebar-accent-foreground)',
border: 'var(--sidebar-border)', border: 'var(--sidebar-border)',
ring: 'var(--sidebar-ring)' ring: 'var(--sidebar-ring)'
}, },
warning: 'var(--warning)', warning: 'var(--warning)',
editor: { editor: {
background: 'var(--editor-background)', background: 'var(--editor-background)',
foreground: 'var(--editor-foreground)', foreground: 'var(--editor-foreground)',
caret: 'var(--editor-caret)', caret: 'var(--editor-caret)',
selection: 'var(--editor-selection)', selection: 'var(--editor-selection)',
selectionMatch: 'var(--editor-selection-match)', selectionMatch: 'var(--editor-selection-match)',
gutterBackground: 'var(--editor-gutter-background)', gutterBackground: 'var(--editor-gutter-background)',
gutterForeground: 'var(--editor-gutter-foreground)', gutterForeground: 'var(--editor-gutter-foreground)',
gutterBorder: 'var(--editor-gutter-border)', gutterBorder: 'var(--editor-gutter-border)',
gutterActiveForeground: 'var(--editor-gutter-active-foreground)', gutterActiveForeground: 'var(--editor-gutter-active-foreground)',
lineHighlight: 'var(--editor-line-highlight)', lineHighlight: 'var(--editor-line-highlight)',
tag: { tag: {
keyword: 'var(--editor-tag-keyword)', keyword: 'var(--editor-tag-keyword)',
name: 'var(--editor-tag-name)', name: 'var(--editor-tag-name)',
function: 'var(--editor-tag-function)', function: 'var(--editor-tag-function)',
label: 'var(--editor-tag-label)', label: 'var(--editor-tag-label)',
constant: 'var(--editor-tag-constant)', constant: 'var(--editor-tag-constant)',
definition: 'var(--editor-tag-definition)', definition: 'var(--editor-tag-definition)',
brace: 'var(--editor-tag-brace)', brace: 'var(--editor-tag-brace)',
type: 'var(--editor-tag-type)', type: 'var(--editor-tag-type)',
operator: 'var(--editor-tag-operator)', operator: 'var(--editor-tag-operator)',
tag: 'var(--editor-tag-tag)', tag: 'var(--editor-tag-tag)',
'bracket-square': 'var(--editor-tag-bracket-square)', 'bracket-square': 'var(--editor-tag-bracket-square)',
'bracket-angle': 'var(--editor-tag-bracket-angle)', 'bracket-angle': 'var(--editor-tag-bracket-angle)',
attribute: 'var(--editor-tag-attribute)', attribute: 'var(--editor-tag-attribute)',
string: 'var(--editor-tag-string)', string: 'var(--editor-tag-string)',
link: 'var(--editor-tag-link)', link: 'var(--editor-tag-link)',
meta: 'var(--editor-tag-meta)', meta: 'var(--editor-tag-meta)',
comment: 'var(--editor-tag-comment)', comment: 'var(--editor-tag-comment)',
emphasis: 'var(--editor-tag-emphasis)', emphasis: 'var(--editor-tag-emphasis)',
heading: 'var(--editor-tag-heading)', heading: 'var(--editor-tag-heading)',
atom: 'var(--editor-tag-atom)', atom: 'var(--editor-tag-atom)',
processing: 'var(--editor-tag-processing)', processing: 'var(--editor-tag-processing)',
separator: 'var(--editor-tag-separator)', separator: 'var(--editor-tag-separator)',
invalid: 'var(--editor-tag-invalid)', invalid: 'var(--editor-tag-invalid)',
quote: 'var(--editor-tag-quote)', quote: 'var(--editor-tag-quote)',
'annotation-special': 'var(--editor-tag-annotation-special)', 'annotation-special': 'var(--editor-tag-annotation-special)',
number: 'var(--editor-tag-number)', number: 'var(--editor-tag-number)',
regexp: 'var(--editor-tag-regexp)', regexp: 'var(--editor-tag-regexp)',
'variable-local': 'var(--editor-tag-variable-local)' 'variable-local': 'var(--editor-tag-variable-local)'
} }
}, },
chat: { chat: {
reference: 'var(--chat-reference)', reference: 'var(--chat-reference)',
'reference-hover': 'var(--chat-reference-hover)', 'reference-hover': 'var(--chat-reference-hover)',
'reference-selected': 'var(--chat-reference-selected)', 'reference-selected': 'var(--chat-reference-selected)',
'reference-selected-border': 'var(--chat-reference-selected-border)' 'reference-selected-border': 'var(--chat-reference-selected-border)'
} }
}, },
fontSize: { fontSize: {
editor: 'var(--editor-font-size)' editor: 'var(--editor-font-size)'
}, },
fontFamily: { fontFamily: {
editor: 'var(--editor-font-family)' editor: 'var(--editor-font-family)'
}, },
borderRadius: { borderRadius: {
lg: 'var(--radius)', lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)', md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)' sm: 'calc(var(--radius) - 4px)'
}, },
keyframes: { keyframes: {
'accordion-down': { 'accordion-down': {
from: { from: {
height: '0' height: '0'
}, },
to: { to: {
height: 'var(--radix-accordion-content-height)' height: 'var(--radix-accordion-content-height)'
} }
}, },
'accordion-up': { 'accordion-up': {
from: { from: {
height: 'var(--radix-accordion-content-height)' height: 'var(--radix-accordion-content-height)'
}, },
to: { to: {
height: '0' height: '0'
} }
} }
}, },
animation: { animation: {
'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out',
'spin-slow': 'spin 1.5s linear infinite', 'spin-slow': 'spin 1.5s linear infinite',
'bounce-slow': 'bounce 1.5s linear infinite' 'bounce-slow': 'bounce 1.5s linear infinite'
} }
} }
}, },
plugins: [ plugins: [
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
require("tailwindcss-animate"), require("tailwindcss-animate"),
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
require('@tailwindcss/typography'), require('@tailwindcss/typography'),
], ],
} satisfies Config } satisfies Config
export default config export default config

View file

@ -1,6 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@ -18,10 +22,23 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": [
"@/public/*": ["./public/*"] "./src/*"
} ],
"@/public/*": [
"./public/*"
]
},
"target": "ES2017"
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/env.mjs"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/env.mjs"
],
"exclude": [
"node_modules"
]
} }

1300
yarn.lock

File diff suppressed because it is too large Load diff