diff --git a/packages/web/src/app/[domain]/components/searchBar/constants.ts b/packages/web/src/app/[domain]/components/searchBar/constants.ts
index e08a03fe..c637bee9 100644
--- a/packages/web/src/app/[domain]/components/searchBar/constants.ts
+++ b/packages/web/src/app/[domain]/components/searchBar/constants.ts
@@ -1,10 +1,10 @@
-import { Suggestion, SuggestionMode } from "./searchSuggestionsBox";
+import { Suggestion } from "./searchSuggestionsBox";
/**
* List of search prefixes that can be used while the
* `refine` suggestion mode is active.
*/
-enum SearchPrefix {
+export enum SearchPrefix {
repo = "repo:",
r = "r:",
lang = "lang:",
@@ -18,162 +18,10 @@ enum SearchPrefix {
archived = "archived:",
case = "case:",
fork = "fork:",
- public = "public:"
+ public = "public:",
+ context = "context:",
}
-const negate = (prefix: SearchPrefix) => {
- return `-${prefix}`;
-}
-
-type SuggestionModeMapping = {
- suggestionMode: SuggestionMode,
- prefixes: string[],
-}
-
-/**
- * Maps search prefixes to a suggestion mode. When a query starts
- * with a prefix, the corresponding suggestion mode is enabled.
- * @see [searchSuggestionsBox.tsx](./searchSuggestionsBox.tsx)
- */
-export const suggestionModeMappings: SuggestionModeMapping[] = [
- {
- suggestionMode: "repo",
- prefixes: [
- SearchPrefix.repo, negate(SearchPrefix.repo),
- SearchPrefix.r, negate(SearchPrefix.r),
- ]
- },
- {
- suggestionMode: "language",
- prefixes: [
- SearchPrefix.lang, negate(SearchPrefix.lang),
- ]
- },
- {
- suggestionMode: "file",
- prefixes: [
- SearchPrefix.file, negate(SearchPrefix.file),
- ]
- },
- {
- suggestionMode: "content",
- prefixes: [
- SearchPrefix.content, negate(SearchPrefix.content),
- ]
- },
- {
- suggestionMode: "revision",
- prefixes: [
- SearchPrefix.rev, negate(SearchPrefix.rev),
- SearchPrefix.revision, negate(SearchPrefix.revision),
- SearchPrefix.branch, negate(SearchPrefix.branch),
- SearchPrefix.b, negate(SearchPrefix.b),
- ]
- },
- {
- suggestionMode: "symbol",
- prefixes: [
- SearchPrefix.sym, negate(SearchPrefix.sym),
- ]
- },
- {
- suggestionMode: "archived",
- prefixes: [
- SearchPrefix.archived
- ]
- },
- {
- suggestionMode: "case",
- prefixes: [
- SearchPrefix.case
- ]
- },
- {
- suggestionMode: "fork",
- prefixes: [
- SearchPrefix.fork
- ]
- },
- {
- suggestionMode: "public",
- prefixes: [
- SearchPrefix.public
- ]
- }
-];
-
-export const refineModeSuggestions: Suggestion[] = [
- {
- value: SearchPrefix.repo,
- description: "Include only results from the given repository.",
- spotlight: true,
- },
- {
- value: negate(SearchPrefix.repo),
- description: "Exclude results from the given repository."
- },
- {
- value: SearchPrefix.lang,
- description: "Include only results from the given language.",
- spotlight: true,
- },
- {
- value: negate(SearchPrefix.lang),
- description: "Exclude results from the given language."
- },
- {
- value: SearchPrefix.file,
- description: "Include only results from filepaths matching the given search pattern.",
- spotlight: true,
- },
- {
- value: negate(SearchPrefix.file),
- description: "Exclude results from file paths matching the given search pattern."
- },
- {
- value: SearchPrefix.rev,
- description: "Search a given branch or tag instead of the default branch.",
- spotlight: true,
- },
- {
- value: negate(SearchPrefix.rev),
- description: "Exclude results from the given branch or tag."
- },
- {
- value: SearchPrefix.sym,
- description: "Include only symbols matching the given search pattern.",
- spotlight: true,
- },
- {
- value: negate(SearchPrefix.sym),
- description: "Exclude results from symbols matching the given search pattern."
- },
- {
- value: SearchPrefix.content,
- description: "Include only results from files if their content matches the given search pattern."
- },
- {
- value: negate(SearchPrefix.content),
- description: "Exclude results from files if their content matches the given search pattern."
- },
- {
- value: SearchPrefix.archived,
- description: "Include results from archived repositories.",
- },
- {
- value: SearchPrefix.case,
- description: "Control case-sensitivity of search patterns."
- },
- {
- value: SearchPrefix.fork,
- description: "Include only results from forked repositories."
- },
- {
- value: SearchPrefix.public,
- description: "Filter on repository visibility."
- },
-];
-
export const publicModeSuggestions: Suggestion[] = [
{
value: "yes",
diff --git a/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx b/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx
index 54d48a2b..f95a926e 100644
--- a/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx
+++ b/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx
@@ -10,7 +10,6 @@ import {
caseModeSuggestions,
forkModeSuggestions,
publicModeSuggestions,
- refineModeSuggestions,
} from "./constants";
import { IconType } from "react-icons/lib";
import { VscFile, VscFilter, VscRepo, VscSymbolMisc } from "react-icons/vsc";
@@ -18,6 +17,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import { KeyboardShortcutHint } from "../keyboardShortcutHint";
import { useSyntaxGuide } from "@/app/[domain]/components/syntaxGuideProvider";
+import { useRefineModeSuggestions } from "./useRefineModeSuggestions";
export type Suggestion = {
value: string;
@@ -39,7 +39,8 @@ export type SuggestionMode =
"symbol" |
"content" |
"repo" |
- "searchHistory";
+ "searchHistory" |
+ "context";
interface SearchSuggestionsBoxProps {
query: string;
@@ -59,6 +60,7 @@ interface SearchSuggestionsBoxProps {
symbolSuggestions: Suggestion[];
languageSuggestions: Suggestion[];
searchHistorySuggestions: Suggestion[];
+ searchContextSuggestions: Suggestion[];
}
const SearchSuggestionsBox = forwardRef(({
@@ -78,9 +80,11 @@ const SearchSuggestionsBox = forwardRef(({
symbolSuggestions,
languageSuggestions,
searchHistorySuggestions,
+ searchContextSuggestions,
}: SearchSuggestionsBoxProps, ref: Ref
) => {
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(0);
const { onOpenChanged } = useSyntaxGuide();
+ const refineModeSuggestions = useRefineModeSuggestions();
const { suggestions, isHighlightEnabled, descriptionPlacement, DefaultIcon, onSuggestionClicked } = useMemo(() => {
if (!isEnabled) {
@@ -198,6 +202,13 @@ const SearchSuggestionsBox = forwardRef(({
},
descriptionPlacement: "right",
}
+ case "context":
+ return {
+ list: searchContextSuggestions,
+ onSuggestionClicked: createOnSuggestionClickedHandler(),
+ descriptionPlacement: "left",
+ DefaultIcon: VscFilter,
+ }
case "none":
case "revision":
case "content":
@@ -263,6 +274,7 @@ const SearchSuggestionsBox = forwardRef(({
symbolSuggestions,
searchHistorySuggestions,
languageSuggestions,
+ searchContextSuggestions,
]);
// When the list of suggestions change, reset the highlight index
@@ -287,6 +299,8 @@ const SearchSuggestionsBox = forwardRef(({
return "Languages";
case "searchHistory":
return "Search history"
+ case "context":
+ return "Search contexts"
default:
return "";
}
diff --git a/packages/web/src/app/[domain]/components/searchBar/useRefineModeSuggestions.ts b/packages/web/src/app/[domain]/components/searchBar/useRefineModeSuggestions.ts
new file mode 100644
index 00000000..fdc16d50
--- /dev/null
+++ b/packages/web/src/app/[domain]/components/searchBar/useRefineModeSuggestions.ts
@@ -0,0 +1,101 @@
+'use client';
+
+import { useMemo } from "react";
+import { Suggestion } from "./searchSuggestionsBox";
+import { SearchPrefix } from "./constants";
+import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
+
+const negate = (prefix: SearchPrefix) => {
+ return `-${prefix}`;
+}
+
+export const useRefineModeSuggestions = () => {
+ const isSearchContextsEnabled = useHasEntitlement('search-contexts');
+
+ const suggestions = useMemo((): Suggestion[] => {
+ return [
+ ...(isSearchContextsEnabled ? [
+ {
+ value: SearchPrefix.context,
+ description: "Include only results from the given search context.",
+ spotlight: true,
+ },
+ {
+ value: negate(SearchPrefix.context),
+ description: "Exclude results from the given search context."
+ },
+ ] : []),
+ {
+ value: SearchPrefix.public,
+ description: "Filter on repository visibility."
+ },
+ {
+ value: SearchPrefix.repo,
+ description: "Include only results from the given repository.",
+ spotlight: true,
+ },
+ {
+ value: negate(SearchPrefix.repo),
+ description: "Exclude results from the given repository."
+ },
+ {
+ value: SearchPrefix.lang,
+ description: "Include only results from the given language.",
+ spotlight: true,
+ },
+ {
+ value: negate(SearchPrefix.lang),
+ description: "Exclude results from the given language."
+ },
+ {
+ value: SearchPrefix.file,
+ description: "Include only results from filepaths matching the given search pattern.",
+ spotlight: true,
+ },
+ {
+ value: negate(SearchPrefix.file),
+ description: "Exclude results from file paths matching the given search pattern."
+ },
+ {
+ value: SearchPrefix.rev,
+ description: "Search a given branch or tag instead of the default branch.",
+ spotlight: true,
+ },
+ {
+ value: negate(SearchPrefix.rev),
+ description: "Exclude results from the given branch or tag."
+ },
+ {
+ value: SearchPrefix.sym,
+ description: "Include only symbols matching the given search pattern.",
+ spotlight: true,
+ },
+ {
+ value: negate(SearchPrefix.sym),
+ description: "Exclude results from symbols matching the given search pattern."
+ },
+ {
+ value: SearchPrefix.content,
+ description: "Include only results from files if their content matches the given search pattern."
+ },
+ {
+ value: negate(SearchPrefix.content),
+ description: "Exclude results from files if their content matches the given search pattern."
+ },
+ {
+ value: SearchPrefix.archived,
+ description: "Include results from archived repositories.",
+ },
+ {
+ value: SearchPrefix.case,
+ description: "Control case-sensitivity of search patterns."
+ },
+ {
+ value: SearchPrefix.fork,
+ description: "Include only results from forked repositories."
+ },
+ ];
+ }, [isSearchContextsEnabled]);
+
+ return suggestions;
+}
\ No newline at end of file
diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeAndQuery.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeAndQuery.ts
index 555b4c22..6aa4ff9d 100644
--- a/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeAndQuery.ts
+++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeAndQuery.ts
@@ -2,7 +2,7 @@
import { useEffect, useMemo, useState } from "react";
import { splitQuery, SuggestionMode } from "./searchSuggestionsBox";
-import { suggestionModeMappings } from "./constants";
+import { useSuggestionModeMappings } from "./useSuggestionModeMappings";
interface Props {
isSuggestionsEnabled: boolean;
@@ -18,6 +18,8 @@ export const useSuggestionModeAndQuery = ({
query,
}: Props) => {
+ const suggestionModeMappings = useSuggestionModeMappings();
+
const { suggestionQuery, suggestionMode } = useMemo<{ suggestionQuery: string, suggestionMode: SuggestionMode }>(() => {
// When suggestions are not enabled, fallback to using a sentinal
// suggestion mode of "none".
diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeMappings.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeMappings.ts
new file mode 100644
index 00000000..da03fd6b
--- /dev/null
+++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeMappings.ts
@@ -0,0 +1,104 @@
+'use client';
+
+import { useMemo } from "react";
+import { SearchPrefix } from "./constants";
+import { SuggestionMode } from "./searchSuggestionsBox";
+import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
+
+const negate = (prefix: SearchPrefix) => {
+ return `-${prefix}`;
+}
+
+type SuggestionModeMapping = {
+ suggestionMode: SuggestionMode,
+ prefixes: string[],
+}
+
+/**
+ * Maps search prefixes to a suggestion mode. When a query starts
+ * with a prefix, the corresponding suggestion mode is enabled.
+ * @see [searchSuggestionsBox.tsx](./searchSuggestionsBox.tsx)
+ */
+export const useSuggestionModeMappings = () => {
+ const isSearchContextsEnabled = useHasEntitlement('search-contexts');
+
+ const mappings = useMemo((): SuggestionModeMapping[] => {
+ return [
+ {
+ suggestionMode: "repo",
+ prefixes: [
+ SearchPrefix.repo, negate(SearchPrefix.repo),
+ SearchPrefix.r, negate(SearchPrefix.r),
+ ]
+ },
+ {
+ suggestionMode: "language",
+ prefixes: [
+ SearchPrefix.lang, negate(SearchPrefix.lang),
+ ]
+ },
+ {
+ suggestionMode: "file",
+ prefixes: [
+ SearchPrefix.file, negate(SearchPrefix.file),
+ ]
+ },
+ {
+ suggestionMode: "content",
+ prefixes: [
+ SearchPrefix.content, negate(SearchPrefix.content),
+ ]
+ },
+ {
+ suggestionMode: "revision",
+ prefixes: [
+ SearchPrefix.rev, negate(SearchPrefix.rev),
+ SearchPrefix.revision, negate(SearchPrefix.revision),
+ SearchPrefix.branch, negate(SearchPrefix.branch),
+ SearchPrefix.b, negate(SearchPrefix.b),
+ ]
+ },
+ {
+ suggestionMode: "symbol",
+ prefixes: [
+ SearchPrefix.sym, negate(SearchPrefix.sym),
+ ]
+ },
+ {
+ suggestionMode: "archived",
+ prefixes: [
+ SearchPrefix.archived
+ ]
+ },
+ {
+ suggestionMode: "case",
+ prefixes: [
+ SearchPrefix.case
+ ]
+ },
+ {
+ suggestionMode: "fork",
+ prefixes: [
+ SearchPrefix.fork
+ ]
+ },
+ {
+ suggestionMode: "public",
+ prefixes: [
+ SearchPrefix.public
+ ]
+ },
+ ...(isSearchContextsEnabled ? [
+ {
+ suggestionMode: "context",
+ prefixes: [
+ SearchPrefix.context,
+ negate(SearchPrefix.context),
+ ]
+ } satisfies SuggestionModeMapping,
+ ] : []),
+ ]
+ }, [isSearchContextsEnabled]);
+
+ return mappings;
+}
\ No newline at end of file
diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts
index a8ed1eb6..ac13d4f8 100644
--- a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts
+++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts
@@ -3,6 +3,7 @@
import { useQuery } from "@tanstack/react-query";
import { Suggestion, SuggestionMode } from "./searchSuggestionsBox";
import { getRepos, search } from "@/app/api/(client)/client";
+import { getSearchContexts } from "@/actions";
import { useMemo } from "react";
import { Symbol } from "@/lib/types";
import { languageMetadataMap } from "@/lib/languageMetadata";
@@ -18,7 +19,7 @@ import {
VscSymbolVariable
} from "react-icons/vsc";
import { useSearchHistory } from "@/hooks/useSearchHistory";
-import { getDisplayTime } from "@/lib/utils";
+import { getDisplayTime, isServiceError } from "@/lib/utils";
import { useDomain } from "@/hooks/useDomain";
@@ -56,6 +57,10 @@ export const useSuggestionsData = ({
maxMatchDisplayCount: 15,
}, domain),
select: (data): Suggestion[] => {
+ if (isServiceError(data)) {
+ return [];
+ }
+
return data.Result.Files?.map((file) => ({
value: file.FileName
})) ?? [];
@@ -71,6 +76,10 @@ export const useSuggestionsData = ({
maxMatchDisplayCount: 15,
}, domain),
select: (data): Suggestion[] => {
+ if (isServiceError(data)) {
+ return [];
+ }
+
const symbols = data.Result.Files?.flatMap((file) => file.ChunkMatches).flatMap((chunk) => chunk.SymbolInfo ?? []);
if (!symbols) {
return [];
@@ -89,6 +98,24 @@ export const useSuggestionsData = ({
});
const isLoadingSymbols = useMemo(() => suggestionMode === "symbol" && _isLoadingSymbols, [suggestionMode, _isLoadingSymbols]);
+ const { data: searchContextSuggestions, isLoading: _isLoadingSearchContexts } = useQuery({
+ queryKey: ["searchContexts"],
+ queryFn: () => getSearchContexts(domain),
+ select: (data): Suggestion[] => {
+ if (isServiceError(data)) {
+ return [];
+ }
+
+ return data.map((context) => ({
+ value: context.name,
+ description: context.description,
+ }));
+
+ },
+ enabled: suggestionMode === "context",
+ });
+ const isLoadingSearchContexts = useMemo(() => suggestionMode === "context" && _isLoadingSearchContexts, [_isLoadingSearchContexts, suggestionMode]);
+
const languageSuggestions = useMemo((): Suggestion[] => {
return Object.keys(languageMetadataMap).map((lang) => {
const spotlight = [
@@ -116,13 +143,14 @@ export const useSuggestionsData = ({
}, [searchHistory]);
const isLoadingSuggestions = useMemo(() => {
- return isLoadingSymbols || isLoadingFiles || isLoadingRepos;
- }, [isLoadingFiles, isLoadingRepos, isLoadingSymbols]);
+ return isLoadingSymbols || isLoadingFiles || isLoadingRepos || isLoadingSearchContexts;
+ }, [isLoadingFiles, isLoadingRepos, isLoadingSymbols, isLoadingSearchContexts]);
return {
repoSuggestions: repoSuggestions ?? [],
fileSuggestions: fileSuggestions ?? [],
symbolSuggestions: symbolSuggestions ?? [],
+ searchContextSuggestions: searchContextSuggestions ?? [],
languageSuggestions,
searchHistorySuggestions,
isLoadingSuggestions,
diff --git a/packages/web/src/app/[domain]/components/searchBar/zoektLanguageExtension.ts b/packages/web/src/app/[domain]/components/searchBar/zoektLanguageExtension.ts
index 6fa0f4c7..1dad70bc 100644
--- a/packages/web/src/app/[domain]/components/searchBar/zoektLanguageExtension.ts
+++ b/packages/web/src/app/[domain]/components/searchBar/zoektLanguageExtension.ts
@@ -47,7 +47,7 @@ export const zoekt = () => {
// Check for prefixes first
// If these match, we return 'keyword'
- if (stream.match(/(archived:|branch:|b:|rev:|c:|case:|content:|f:|file:|fork:|public:|r:|repo:|regex:|lang:|sym:|t:|type:)/)) {
+ if (stream.match(/(archived:|branch:|b:|rev:|c:|case:|content:|f:|file:|fork:|public:|r:|repo:|regex:|lang:|sym:|t:|type:|context:)/)) {
return t.keyword.toString();
}
diff --git a/packages/web/src/app/[domain]/layout.tsx b/packages/web/src/app/[domain]/layout.tsx
index 693496ba..8d712266 100644
--- a/packages/web/src/app/[domain]/layout.tsx
+++ b/packages/web/src/app/[domain]/layout.tsx
@@ -3,7 +3,6 @@ import { auth } from "@/auth";
import { getOrgFromDomain } from "@/data/org";
import { isServiceError } from "@/lib/utils";
import { OnboardGuard } from "./components/onboardGuard";
-import { fetchSubscription } from "@/actions";
import { UpgradeGuard } from "./components/upgradeGuard";
import { cookies, headers } from "next/headers";
import { getSelectorsByUserAgent } from "react-device-detect";
@@ -11,9 +10,11 @@ import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSpl
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/constants";
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";
import { SyntaxGuideProvider } from "./components/syntaxGuideProvider";
-import { IS_BILLING_ENABLED } from "@/lib/stripe";
+import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
import { env } from "@/env.mjs";
import { notFound, redirect } from "next/navigation";
+import { getSubscriptionInfo } from "@/ee/features/billing/actions";
+
interface LayoutProps {
children: React.ReactNode,
params: { domain: string }
@@ -58,7 +59,7 @@ export default async function Layout({
}
if (IS_BILLING_ENABLED) {
- const subscription = await fetchSubscription(domain);
+ const subscription = await getSubscriptionInfo(domain);
if (
subscription &&
(
diff --git a/packages/web/src/app/[domain]/onboard/page.tsx b/packages/web/src/app/[domain]/onboard/page.tsx
index 768244ee..a62770e1 100644
--- a/packages/web/src/app/[domain]/onboard/page.tsx
+++ b/packages/web/src/app/[domain]/onboard/page.tsx
@@ -5,9 +5,9 @@ import { notFound, redirect } from "next/navigation";
import { ConnectCodeHost } from "./components/connectCodeHost";
import { InviteTeam } from "./components/inviteTeam";
import { CompleteOnboarding } from "./components/completeOnboarding";
-import { Checkout } from "./components/checkout";
+import { Checkout } from "@/ee/features/billing/components/checkout";
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
-import { IS_BILLING_ENABLED } from "@/lib/stripe";
+import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
import { env } from "@/env.mjs";
interface OnboardProps {
diff --git a/packages/web/src/app/[domain]/search/page.tsx b/packages/web/src/app/[domain]/search/page.tsx
index 0b587903..134b6b02 100644
--- a/packages/web/src/app/[domain]/search/page.tsx
+++ b/packages/web/src/app/[domain]/search/page.tsx
@@ -10,7 +10,7 @@ import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
import { useSearchHistory } from "@/hooks/useSearchHistory";
import { Repository, SearchQueryParams, SearchResultFile } from "@/lib/types";
-import { createPathWithQueryParams, measure } from "@/lib/utils";
+import { createPathWithQueryParams, measure, unwrapServiceError } from "@/lib/utils";
import { InfoCircledIcon, SymbolIcon } from "@radix-ui/react-icons";
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
@@ -22,6 +22,7 @@ import { CodePreviewPanel } from "./components/codePreviewPanel";
import { FilterPanel } from "./components/filterPanel";
import { SearchResultsPanel } from "./components/searchResultsPanel";
import { useDomain } from "@/hooks/useDomain";
+import { useToast } from "@/components/hooks/use-toast";
const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 10000;
@@ -44,21 +45,31 @@ const SearchPageInternal = () => {
const { setSearchHistory } = useSearchHistory();
const captureEvent = useCaptureEvent();
const domain = useDomain();
+ const { toast } = useToast();
- const { data: searchResponse, isLoading } = useQuery({
+ const { data: searchResponse, isLoading, error } = useQuery({
queryKey: ["search", searchQuery, maxMatchDisplayCount],
- queryFn: () => measure(() => search({
+ queryFn: () => measure(() => unwrapServiceError(search({
query: searchQuery,
maxMatchDisplayCount,
- }, domain), "client.search"),
+ }, domain)), "client.search"),
select: ({ data, durationMs }) => ({
...data,
durationMs,
}),
enabled: searchQuery.length > 0,
refetchOnWindowFocus: false,
+ retry: false,
});
+ useEffect(() => {
+ if (error) {
+ toast({
+ description: `❌ Search failed. Reason: ${error.message}`,
+ });
+ }
+ }, [error, toast]);
+
// Write the query to the search history
useEffect(() => {
diff --git a/packages/web/src/app/[domain]/settings/billing/page.tsx b/packages/web/src/app/[domain]/settings/billing/page.tsx
index 5f66e6cc..87fccb7c 100644
--- a/packages/web/src/app/[domain]/settings/billing/page.tsx
+++ b/packages/web/src/app/[domain]/settings/billing/page.tsx
@@ -1,13 +1,15 @@
-import type { Metadata } from "next"
-import { CalendarIcon, DollarSign, Users } from "lucide-react"
+import { getCurrentUserRole } from "@/actions"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
-import { ManageSubscriptionButton } from "./manageSubscriptionButton"
-import { getSubscriptionData, getCurrentUserRole, getSubscriptionBillingEmail } from "@/actions"
+import { getSubscriptionBillingEmail, getSubscriptionInfo } from "@/ee/features/billing/actions"
+import { ChangeBillingEmailCard } from "@/ee/features/billing/components/changeBillingEmailCard"
+import { ManageSubscriptionButton } from "@/ee/features/billing/components/manageSubscriptionButton"
+import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"
+import { ServiceErrorException } from "@/lib/serviceError"
import { isServiceError } from "@/lib/utils"
-import { ChangeBillingEmailCard } from "./changeBillingEmailCard"
+import { CalendarIcon, DollarSign, Users } from "lucide-react"
+import type { Metadata } from "next"
import { notFound } from "next/navigation"
-import { IS_BILLING_ENABLED } from "@/lib/stripe"
-import { ServiceErrorException } from "@/lib/serviceError"
+
export const metadata: Metadata = {
title: "Billing | Settings",
description: "Manage your subscription and billing information",
@@ -26,7 +28,7 @@ export default async function BillingPage({
notFound();
}
- const subscription = await getSubscriptionData(domain)
+ const subscription = await getSubscriptionInfo(domain)
if (isServiceError(subscription)) {
throw new ServiceErrorException(subscription);
diff --git a/packages/web/src/app/[domain]/settings/layout.tsx b/packages/web/src/app/[domain]/settings/layout.tsx
index 0f15f9bc..ce023831 100644
--- a/packages/web/src/app/[domain]/settings/layout.tsx
+++ b/packages/web/src/app/[domain]/settings/layout.tsx
@@ -2,7 +2,7 @@ import { Metadata } from "next"
import { SidebarNav } from "./components/sidebar-nav"
import { NavigationMenu } from "../components/navigationMenu"
import { Header } from "./components/header";
-import { IS_BILLING_ENABLED } from "@/lib/stripe";
+import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
import { redirect } from "next/navigation";
import { auth } from "@/auth";
diff --git a/packages/web/src/app/[domain]/settings/members/page.tsx b/packages/web/src/app/[domain]/settings/members/page.tsx
index cab223e6..7fb16123 100644
--- a/packages/web/src/app/[domain]/settings/members/page.tsx
+++ b/packages/web/src/app/[domain]/settings/members/page.tsx
@@ -7,7 +7,7 @@ import { Tabs, TabsContent } from "@/components/ui/tabs";
import { TabSwitcher } from "@/components/ui/tab-switcher";
import { InvitesList } from "./components/invitesList";
import { getOrgInvites, getMe } from "@/actions";
-import { IS_BILLING_ENABLED } from "@/lib/stripe";
+import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
import { ServiceErrorException } from "@/lib/serviceError";
interface MembersSettingsPageProps {
params: {
diff --git a/packages/web/src/app/[domain]/upgrade/page.tsx b/packages/web/src/app/[domain]/upgrade/page.tsx
index cd8f238b..8a51aa09 100644
--- a/packages/web/src/app/[domain]/upgrade/page.tsx
+++ b/packages/web/src/app/[domain]/upgrade/page.tsx
@@ -1,23 +1,23 @@
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
import { Footer } from "@/app/components/footer";
import { OrgSelector } from "../components/orgSelector";
-import { EnterpriseUpgradeCard } from "./components/enterpriseUpgradeCard";
-import { TeamUpgradeCard } from "./components/teamUpgradeCard";
-import { fetchSubscription } from "@/actions";
+import { EnterpriseUpgradeCard } from "@/ee/features/billing/components/enterpriseUpgradeCard";
+import { TeamUpgradeCard } from "@/ee/features/billing/components/teamUpgradeCard";
import { redirect } from "next/navigation";
import { isServiceError } from "@/lib/utils";
import Link from "next/link";
import { ArrowLeftIcon } from "@radix-ui/react-icons";
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
import { env } from "@/env.mjs";
-import { IS_BILLING_ENABLED } from "@/lib/stripe";
+import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
+import { getSubscriptionInfo } from "@/ee/features/billing/actions";
export default async function Upgrade({ params: { domain } }: { params: { domain: string } }) {
if (!IS_BILLING_ENABLED) {
redirect(`/${domain}`);
}
- const subscription = await fetchSubscription(domain);
+ const subscription = await getSubscriptionInfo(domain);
if (!subscription) {
redirect(`/${domain}`);
}
diff --git a/packages/web/src/app/api/(client)/client.ts b/packages/web/src/app/api/(client)/client.ts
index 987a0ad7..44349cb6 100644
--- a/packages/web/src/app/api/(client)/client.ts
+++ b/packages/web/src/app/api/(client)/client.ts
@@ -1,9 +1,11 @@
'use client';
import { fileSourceResponseSchema, getVersionResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
+import { ServiceError } from "@/lib/serviceError";
import { FileSourceRequest, FileSourceResponse, GetVersionResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
+import { isServiceError } from "@/lib/utils";
-export const search = async (body: SearchRequest, domain: string): Promise => {
+export const search = async (body: SearchRequest, domain: string): Promise => {
const result = await fetch("/api/search", {
method: "POST",
headers: {
@@ -13,6 +15,10 @@ export const search = async (body: SearchRequest, domain: string): Promise response.json());
+ if (isServiceError(result)) {
+ return result;
+ }
+
return searchResponseSchema.parse(result);
}
diff --git a/packages/web/src/app/api/(server)/stripe/route.ts b/packages/web/src/app/api/(server)/stripe/route.ts
index 9219c463..8a466b7a 100644
--- a/packages/web/src/app/api/(server)/stripe/route.ts
+++ b/packages/web/src/app/api/(server)/stripe/route.ts
@@ -3,7 +3,7 @@ import { NextRequest } from 'next/server';
import Stripe from 'stripe';
import { prisma } from '@/prisma';
import { ConnectionSyncStatus, StripeSubscriptionStatus } from '@sourcebot/db';
-import { stripeClient } from '@/lib/stripe';
+import { stripeClient } from '@/ee/features/billing/stripe';
import { env } from '@/env.mjs';
export async function POST(req: NextRequest) {
diff --git a/packages/web/src/app/components/securityCard.tsx b/packages/web/src/app/components/securityCard.tsx
index 22056e92..578060f5 100644
--- a/packages/web/src/app/components/securityCard.tsx
+++ b/packages/web/src/app/components/securityCard.tsx
@@ -3,6 +3,7 @@
import Link from "next/link"
import { Shield, Lock, CheckCircle, ExternalLink, Mail } from "lucide-react"
import useCaptureEvent from "@/hooks/useCaptureEvent"
+import { SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants"
export default function SecurityCard() {
const captureEvent = useCaptureEvent();
@@ -62,7 +63,7 @@ export default function SecurityCard() {
Have questions?
diff --git a/packages/web/src/app/error.tsx b/packages/web/src/app/error.tsx
index 69f503e8..44b5dabe 100644
--- a/packages/web/src/app/error.tsx
+++ b/packages/web/src/app/error.tsx
@@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Button } from "@/components/ui/button"
import { serviceErrorSchema } from '@/lib/serviceError';
import { SourcebotLogo } from './components/sourcebotLogo';
+import { SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants";
export default function Error({ error, reset }: { error: Error & { digest?: string }, reset: () => void }) {
useEffect(() => {
@@ -76,7 +77,7 @@ function ErrorCard({ message, errorCode, statusCode, onReloadButtonClicked }: Er
Unexpected Error
- An unexpected error occurred. Please reload the page and try again. If the issue persists, please contact us.
+ An unexpected error occurred. Please reload the page and try again. If the issue persists, please contact us.
diff --git a/packages/web/src/app/layout.tsx b/packages/web/src/app/layout.tsx
index 859cd8b6..fa430930 100644
--- a/packages/web/src/app/layout.tsx
+++ b/packages/web/src/app/layout.tsx
@@ -7,6 +7,8 @@ import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { SessionProvider } from "next-auth/react";
import { env } from "@/env.mjs";
+import { PlanProvider } from "@/features/entitlements/planProvider";
+import { getPlan } from "@/features/entitlements/server";
export const metadata: Metadata = {
title: "Sourcebot",
@@ -27,20 +29,22 @@ export default function RootLayout({
-
-
-
-
- {children}
-
-
-
-
+
+
+
+
+
+ {children}
+
+
+
+
+