Compare commits

..

7 Commits

Author SHA1 Message Date
Piyush Gupta
9833ba6b26 fix: license restores 2024-11-18 18:22:20 +05:30
Dhruwang Jariwala
7f8549124f fix: name regex (#4328) 2024-11-18 11:58:27 +00:00
Piyush Gupta
43b1cb904d feat: logic fallback option added (#4306)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2024-11-15 13:13:08 +00:00
Dhruwang Jariwala
762a3ca626 chore: make redis an enterprise feature (#4314) 2024-11-15 07:10:48 +00:00
Sai Suhas Sawant
91f0d00ba2 fix: survey release and close bug for first date of the month (#4311)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
2024-11-15 05:35:51 +00:00
Dhruwang Jariwala
41f42f4427 fix: recall in slack integration (#4304) 2024-11-15 04:17:26 +00:00
Piyush Gupta
98181bfe6c chore: next version upgrade (#4291) 2024-11-14 12:33:05 +00:00
195 changed files with 1695 additions and 1255 deletions

View File

@@ -15,7 +15,8 @@
"@formbricks/react-native": "workspace:*",
"expo": "51.0.26",
"expo-status-bar": "1.12.1",
"react": "18.3.1",
"react": "19.0.0-rc-ed15d500-20241110",
"react-dom": "19.0.0-rc-ed15d500-20241110",
"react-native": "0.74.4",
"react-native-webview": "13.8.6"
},

View File

@@ -1,4 +1,5 @@
import { StatusBar } from "expo-status-bar";
import type { JSX } from "react";
import { Button, LogBox, StyleSheet, Text, View } from "react-native";
import Formbricks, { track } from "@formbricks/react-native";

View File

@@ -14,9 +14,9 @@
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "0.452.0",
"next": "14.2.16",
"react": "18.3.1",
"react-dom": "18.3.1"
"next": "15.0.3",
"react": "19.0.0-rc-ed15d500-20241110",
"react-dom": "19.0.0-rc-ed15d500-20241110"
},
"devDependencies": {
"@formbricks/eslint-config": "workspace:*",

View File

@@ -24,7 +24,7 @@
"@mapbox/rehype-prism": "0.9.0",
"@mdx-js/loader": "3.0.1",
"@mdx-js/react": "3.0.1",
"@next/mdx": "14.2.15",
"@next/mdx": "15.0.3",
"@paralleldrive/cuid2": "2.2.2",
"@sindresorhus/slugify": "2.2.1",
"@tailwindcss/typography": "0.5.15",
@@ -39,7 +39,7 @@
"lucide-react": "0.452.0",
"mdast-util-to-string": "4.0.0",
"mdx-annotations": "0.1.4",
"next": "14.2.16",
"next": "15.0.3",
"next-plausible": "3.12.2",
"next-seo": "6.6.0",
"next-sitemap": "4.2.3",
@@ -47,8 +47,8 @@
"node-fetch": "3.3.2",
"prism-react-renderer": "2.4.0",
"prismjs": "1.29.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.0.0-rc-ed15d500-20241110",
"react-dom": "19.0.0-rc-ed15d500-20241110",
"react-highlight-words": "0.20.0",
"react-markdown": "9.0.1",
"react-responsive-embed": "2.1.0",

View File

@@ -13,8 +13,8 @@
"dependencies": {
"@formbricks/ui": "workspace:*",
"eslint-plugin-react-refresh": "0.4.12",
"react": "18.3.1",
"react-dom": "18.3.1"
"react": "19.0.0-rc-ed15d500-20241110",
"react-dom": "19.0.0-rc-ed15d500-20241110"
},
"devDependencies": {
"@chromatic-com/storybook": "2.0.2",

View File

@@ -10,12 +10,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface InvitePageProps {
params: {
params: Promise<{
environmentId: string;
};
}>;
}
const Page = async ({ params }: InvitePageProps) => {
const Page = async (props: InvitePageProps) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -8,12 +8,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ConnectPageProps {
params: {
params: Promise<{
environmentId: string;
};
}>;
}
const Page = async ({ params }: ConnectPageProps) => {
const Page = async (props: ConnectPageProps) => {
const params = await props.params;
const t = await getTranslations();
const environment = await getEnvironment(params.environmentId);

View File

@@ -4,7 +4,11 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { AuthorizationError } from "@formbricks/types/errors";
const OnboardingLayout = async ({ children, params }) => {
const OnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -11,12 +11,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface XMTemplatePageProps {
params: {
params: Promise<{
environmentId: string;
};
}>;
}
const Page = async ({ params }: XMTemplatePageProps) => {
const Page = async (props: XMTemplatePageProps) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const environment = await getEnvironment(params.environmentId);
const t = await getTranslations();

View File

@@ -11,7 +11,7 @@ import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
export const getTeamsByOrganizationId = reactCache(
(organizationId: string): Promise<TOrganizationTeam[] | null> =>
async (organizationId: string): Promise<TOrganizationTeam[] | null> =>
cache(
async () => {
validateInputs([organizationId, ZId]);

View File

@@ -5,7 +5,11 @@ import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getUserProducts } from "@formbricks/lib/product/service";
const LandingLayout = async ({ children, params }) => {
const LandingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -8,7 +8,8 @@ import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organ
import { getUser } from "@formbricks/lib/user/service";
import { Header } from "@formbricks/ui/components/Header";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -9,7 +9,11 @@ import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
const ProductOnboardingLayout = async ({ children, params }) => {
const ProductOnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -4,7 +4,11 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
const OnboardingLayout = async ({ children, params }) => {
const OnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -9,12 +9,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ChannelPageProps {
params: {
params: Promise<{
organizationId: string;
};
}>;
}
const Page = async ({ params }: ChannelPageProps) => {
const Page = async (props: ChannelPageProps) => {
const params = await props.params;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -9,12 +9,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ModePageProps {
params: {
params: Promise<{
organizationId: string;
};
}>;
}
const Page = async ({ params }: ModePageProps) => {
const Page = async (props: ModePageProps) => {
const params = await props.params;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -16,17 +16,19 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ProductSettingsPageProps {
params: {
params: Promise<{
organizationId: string;
};
searchParams: {
}>;
searchParams: Promise<{
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
mode?: TProductMode;
};
}>;
}
const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
const Page = async (props: ProductSettingsPageProps) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);

View File

@@ -13,7 +13,11 @@ import { AuthorizationError } from "@formbricks/types/errors";
import { DevEnvironmentBanner } from "@formbricks/ui/components/DevEnvironmentBanner";
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
const SurveyEditorEnvironmentLayout = async ({ children, params }) => {
const SurveyEditorEnvironmentLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -4,7 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { type JSX, useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";

View File

@@ -4,7 +4,7 @@ import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { type JSX, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";

View File

@@ -1,7 +1,7 @@
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { type JSX, useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";

View File

@@ -87,10 +87,12 @@ export function ConditionalLogic({
const handleRemoveLogic = (logicItemIdx: number) => {
const logicCopy = structuredClone(question.logic ?? []);
const isLast = logicCopy.length === 1;
logicCopy.splice(logicItemIdx, 1);
updateQuestion(questionIdx, {
logic: logicCopy,
logicFallback: isLast ? undefined : question.logicFallback,
});
};

View File

@@ -3,7 +3,7 @@
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { type JSX, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";

View File

@@ -4,7 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { type JSX, useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";

View File

@@ -2,6 +2,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";

View File

@@ -3,13 +3,17 @@
import { EditorCardMenu } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu";
import { EndScreenForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm";
import { RedirectUrlForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RedirectUrlForm";
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import {
findEndingCardUsedInLogic,
formatTextWithSlashes,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { GripIcon, Handshake, Undo2 } from "lucide-react";
import { useTranslations } from "next-intl";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
@@ -93,6 +97,14 @@ export const EditEndingCard = ({
};
const deleteEndingCard = () => {
// checking if this ending card is used in logic
const quesIdx = findEndingCardUsedInLogic(localSurvey, endingCard.id);
if (quesIdx !== -1) {
toast.error(t("environments.surveys.edit.ending_card_used_in_logic", { questionIndex: quesIdx + 1 }));
return;
}
setLocalSurvey((prevSurvey) => {
const updatedEndings = prevSurvey.endings.filter((_, index) => index !== endingCardIndex);
return { ...prevSurvey, endings: updatedEndings };

View File

@@ -6,7 +6,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon, XCircleIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useMemo, useState } from "react";
import { type JSX, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { createI18nString } from "@formbricks/lib/i18n/utils";

View File

@@ -2,7 +2,17 @@ import { LogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[en
import { LogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions";
import { ArrowRightIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { ReactElement, useMemo } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { QUESTIONS_ICON_MAP } from "@formbricks/lib/utils/questions";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@formbricks/ui/components/Select";
interface LogicEditorProps {
localSurvey: TSurvey;
@@ -24,6 +34,36 @@ export function LogicEditor({
isLast,
}: LogicEditorProps) {
const t = useTranslations();
const fallbackOptions = useMemo(() => {
let options: {
icon?: ReactElement;
label: string;
value: string;
}[] = [];
localSurvey.questions.forEach((ques) => {
if (ques.id === question.id) return null;
options.push({
icon: QUESTIONS_ICON_MAP[ques.type],
label: getLocalizedValue(ques.headline, "default"),
value: ques.id,
});
});
localSurvey.endings.forEach((ending) => {
options.push({
label:
ending.type === "endScreen"
? getLocalizedValue(ending.headline, "default") || t("environments.surveys.edit.end_screen_card")
: ending.label || t("environments.surveys.edit.redirect_thank_you_card"),
value: ending.id,
});
});
return options;
}, [localSurvey.questions, localSurvey.endings, question.id, t]);
return (
<div className="flex w-full grow flex-col gap-4 overflow-x-auto pb-2 text-sm">
<LogicEditorConditions
@@ -43,11 +83,36 @@ export function LogicEditor({
questionIdx={questionIdx}
/>
{isLast ? (
<div className="flex flex-wrap items-center space-x-2">
<div className="flex items-center space-x-2">
<ArrowRightIcon className="h-4 w-4" />
<p className="text-slate-700">
{t("environments.surveys.edit.all_other_answers_will_continue_to_the_next_question")}
<p className="text-nowrap text-slate-700">
{t("environments.surveys.edit.all_other_answers_will_continue_to")}
</p>
<Select
autoComplete="true"
defaultValue={question.logicFallback || "defaultSelection"}
onValueChange={(val) => {
updateQuestion(questionIdx, {
logicFallback: val === "defaultSelection" ? undefined : val,
});
}}>
<SelectTrigger className="w-auto bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem key="fallback_default_selection" value={"defaultSelection"}>
{t("environments.surveys.edit.next_question")}
</SelectItem>
{fallbackOptions.map((option) => (
<SelectItem key={`fallback_${option.value}`} value={option.value}>
<div className="flex items-center gap-2">
{option.icon}
{option.label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
</div>

View File

@@ -4,6 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";

View File

@@ -8,7 +8,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { type JSX, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";

View File

@@ -4,6 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys/types";

View File

@@ -4,6 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import {

View File

@@ -3,6 +3,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { cn } from "@formbricks/lib/cn";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";

View File

@@ -1,6 +1,6 @@
import { PaintbrushIcon, Rows3Icon, SettingsIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { type JSX, useMemo } from "react";
import { cn } from "@formbricks/lib/cn";
import { TSurveyEditorTabs } from "@formbricks/types/surveys/types";

View File

@@ -7,7 +7,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { type JSX, useEffect, useRef, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TI18nString, TSurvey, TSurveyRankingQuestion } from "@formbricks/types/surveys/types";

View File

@@ -130,21 +130,15 @@ export const ResponseOptionsCard = ({
};
const handleRunOnDateChange = (date: Date) => {
const equivalentDate = date?.getDate();
date?.setUTCHours(0, 0, 0, 0);
date?.setDate(equivalentDate);
setRunOnDate(date);
setLocalSurvey({ ...localSurvey, runOnDate: date ?? null });
const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0));
setRunOnDate(utcDate);
setLocalSurvey({ ...localSurvey, runOnDate: utcDate ?? null });
};
const handleCloseOnDateChange = (date: Date) => {
const equivalentDate = date?.getDate();
date?.setUTCHours(0, 0, 0, 0);
date?.setDate(equivalentDate);
setCloseOnDate(date);
setLocalSurvey({ ...localSurvey, closeOnDate: date ?? null });
const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0));
setCloseOnDate(utcDate);
setLocalSurvey({ ...localSurvey, closeOnDate: utcDate ?? null });
};
const handleClosedSurveyMessageChange = ({

View File

@@ -344,9 +344,7 @@ export const SurveyMenuBar = ({
<AlertTriangleIcon className="h-5 w-5 text-amber-400" />
</TooltipTrigger>
<TooltipContent side={"top"} className="lg:hidden">
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">
{t(cautionText)}
</p>
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">{cautionText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -1060,7 +1060,9 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQues
};
return survey.questions.findIndex(
(question) => question.logic && question.id !== questionId && question.logic.some(isUsedInLogicRule)
(question) =>
question.logicFallback === questionId ||
(question.id !== questionId && question.logic?.some(isUsedInLogicRule))
);
};
@@ -1145,3 +1147,17 @@ export const findHiddenFieldUsedInLogic = (survey: TSurvey, hiddenFieldId: strin
return survey.questions.findIndex((question) => question.logic?.some(isUsedInLogicRule));
};
export const findEndingCardUsedInLogic = (survey: TSurvey, endingCardId: string): number => {
const isUsedInAction = (action: TSurveyLogicAction): boolean => {
return action.objective === "jumpToQuestion" && action.target === endingCardId;
};
const isUsedInLogicRule = (logicRule: TSurveyLogic): boolean => {
return logicRule.actions.some(isUsedInAction);
};
return survey.questions.findIndex(
(question) => question.logicFallback === endingCardId || question.logic?.some(isUsedInLogicRule)
);
};

View File

@@ -24,14 +24,17 @@ import { getUserLocale } from "@formbricks/lib/user/service";
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
import { SurveyEditor } from "./components/SurveyEditor";
export const generateMetadata = async ({ params }) => {
export const generateMetadata = async (props) => {
const params = await props.params;
const survey = await getSurvey(params.surveyId);
return {
title: survey?.name ? `${survey?.name} | Editor` : "Editor",
};
};
const Page = async ({ params, searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const [
survey,

View File

@@ -14,17 +14,19 @@ import { TTemplateRole } from "@formbricks/types/templates";
import { TemplateContainerWithPreview } from "./components/TemplateContainer";
interface SurveyTemplateProps {
params: {
params: Promise<{
environmentId: string;
};
searchParams: {
}>;
searchParams: Promise<{
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
role?: TTemplateRole;
};
}>;
}
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
const Page = async (props: SurveyTemplateProps) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
const environmentId = params.environmentId;

View File

@@ -3,7 +3,8 @@ import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper
export const dynamic = "force-dynamic";
const Page = ({ searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const { environmentId } = searchParams;
return (

View File

@@ -21,7 +21,8 @@ export const metadata: Metadata = {
title: "Attributes",
};
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
let attributeClasses = await getAttributeClasses(params.environmentId);
const t = await getTranslations();
const product = await getProductByEnvironmentId(params.environmentId);

View File

@@ -9,7 +9,11 @@ import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/ser
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { AuthorizationError } from "@formbricks/types/errors";
const ConfigLayout = async ({ children, params }) => {
const ConfigLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const [organization, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),

View File

@@ -54,7 +54,7 @@ export const ResponseSection = async ({
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
return (
<ResponseTimeline

View File

@@ -19,7 +19,8 @@ import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, environmentTags, product, session, organization, person, attributes, attributeClasses] =
await Promise.all([

View File

@@ -16,7 +16,8 @@ import { Button } from "@formbricks/ui/components/Button";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);

View File

@@ -19,7 +19,8 @@ import { getSegments } from "@formbricks/lib/segment/service";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, segments, attributeClasses, organization, product] = await Promise.all([
getEnvironment(params.environmentId),

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { type JSX, useState } from "react";
import { TActionClass } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { ActionDetailModal } from "./ActionDetailModal";

View File

@@ -23,7 +23,8 @@ export const metadata: Metadata = {
title: "Actions",
};
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const t = await getTranslations();
const [actionClasses, organization, product] = await Promise.all([
@@ -31,7 +32,7 @@ const Page = async ({ params }) => {
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
if (!session) {
throw new Error(t("common.session_not_found"));

View File

@@ -481,7 +481,11 @@ export const MainNavigation = ({
{dropdownNavigation.map(
(link) =>
!link.hidden && (
<Link href={link.href} target={link.target} className="flex w-full items-center">
<Link
href={link.href}
target={link.target}
className="flex w-full items-center"
key={link.label}>
<DropdownMenuItem>
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
{link.label}

View File

@@ -21,7 +21,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const isEnabled = !!AIRTABLE_CLIENT_ID;
const [session, surveys, integrations, environment, attributeClasses] = await Promise.all([
@@ -53,7 +54,7 @@ const Page = async ({ params }) => {
airtableArray = await getAirtableTables(params.environmentId);
}
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,

View File

@@ -24,7 +24,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
const [session, surveys, integrations, environment, attributeClasses] = await Promise.all([
@@ -51,7 +52,7 @@ const Page = async ({ params }) => {
(integration): integration is TIntegrationGoogleSheets => integration.type === "googleSheets"
);
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,

View File

@@ -26,7 +26,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const enabled = !!(
NOTION_OAUTH_CLIENT_ID &&
@@ -80,7 +81,7 @@ const Page = async ({ params }) => {
return (
<PageContentWrapper>
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/integrations`} />
<PageHeader pageTitle={"environments.integrations.notion.notion_integration"} />
<PageHeader pageTitle={t("environments.integrations.notion.notion_integration")} />
<NotionWrapper
enabled={enabled}
surveys={surveys}

View File

@@ -25,7 +25,8 @@ import { Card } from "@formbricks/ui/components/IntegrationCard";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const environmentId = params.environmentId;
const t = await getTranslations();
const [

View File

@@ -91,7 +91,7 @@ export const ManageIntegration = ({
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
<span className="text-slate-500">
{t("environments.integrations.slack.connected_with_team", {
team: slackIntegration.config.key.team.name,
team: slackIntegration.config.key.team?.name,
})}
</span>
</div>

View File

@@ -46,7 +46,8 @@ export const SlackWrapper = ({
if (
getSlackChannelsResponse?.serverError &&
getSlackChannelsResponse.serverError.includes("missing_scope")
(getSlackChannelsResponse.serverError.includes("missing_scope") ||
getSlackChannelsResponse.serverError.includes("invalid_auth"))
) {
setShowReconnectButton(true);
}

View File

@@ -19,7 +19,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
const t = await getTranslations();
const [session, surveys, slackIntegration, environment, attributeClasses] = await Promise.all([

View File

@@ -2,7 +2,7 @@
import { WebhookModal } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookDetailModal";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { type JSX, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TWebhook } from "@formbricks/types/webhooks";

View File

@@ -18,7 +18,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [session, organization, webhooksUnsorted, surveys, environment] = await Promise.all([
getServerSession(authOptions),

View File

@@ -15,7 +15,11 @@ import { FormbricksClient } from "../../components/FormbricksClient";
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
import { PosthogIdentify } from "./components/PosthogIdentify";
export const EnvLayout = async ({ children, params }) => {
export const EnvLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -7,7 +7,8 @@ import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const t = await getTranslations();
const organization = await getOrganizationByEnvironmentId(params.environmentId);

View File

@@ -12,7 +12,8 @@ import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper
import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { SettingsCard } from "../../../settings/components/SettingsCard";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, organization] = await Promise.all([
getEnvironment(params.environmentId),

View File

@@ -19,7 +19,7 @@ const LoadingCard = () => {
<div className="grid h-12 grid-cols-10 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
<div className="col-span-4 sm:col-span-2">{t("common.label")}</div>
<div className="col-span-4 hidden sm:col-span-5 sm:block">
{t("environments.product.api_keys.api_key")}
{t("environments.product.api-keys.api_key")}
</div>
<div className="col-span-4 sm:col-span-2">{t("common.created_at")}</div>
</div>

View File

@@ -17,7 +17,8 @@ import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { SettingsCard } from "../../settings/components/SettingsCard";
import { ApiKeyList } from "./components/ApiKeyList";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [session, environment, organization, product] = await Promise.all([
getServerSession(authOptions),

View File

@@ -19,7 +19,8 @@ import { DeleteProduct } from "./components/DeleteProduct";
import { EditProductNameForm } from "./components/EditProductNameForm";
import { EditWaitingTimeForm } from "./components/EditWaitingTimeForm";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const [product, session, organization] = await Promise.all([
getProductByEnvironmentId(params.environmentId),

View File

@@ -16,7 +16,8 @@ import { getUser } from "@formbricks/lib/user/service";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const product = await getProductByEnvironmentId(params.environmentId);

View File

@@ -12,7 +12,11 @@ export const metadata: Metadata = {
title: "Config",
};
const ConfigLayout = async ({ children, params }) => {
const ConfigLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const [organization, session] = await Promise.all([

View File

@@ -26,7 +26,8 @@ import { EditFormbricksBranding } from "./components/EditBranding";
import { EditPlacementForm } from "./components/EditPlacementForm";
import { ThemeStyling } from "./components/ThemeStyling";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const [session, organization, product] = await Promise.all([
getServerSession(authOptions),

View File

@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
const Page = ({ params }) => {
const Page = async (props) => {
const params = await props.params;
return redirect(`/environments/${params.environmentId}/product/general`);
};

View File

@@ -17,7 +17,8 @@ import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper
import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { EditTagsWrapper } from "./components/EditTagsWrapper";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const environment = await getEnvironment(params.environmentId);
if (!environment) {

View File

@@ -4,7 +4,11 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
const AccountSettingsLayout = async ({ children, params }) => {
const AccountSettingsLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const [organization, product, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),

View File

@@ -141,7 +141,9 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
return memberships;
};
const Page = async ({ params, searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session) {

View File

@@ -15,7 +15,8 @@ import { DeleteAccount } from "./components/DeleteAccount";
import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm";
import { EditProfileDetailsForm } from "./components/EditProfileDetailsForm";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const { environmentId } = params;
const session = await getServerSession(authOptions);

View File

@@ -13,7 +13,8 @@ import { Button } from "@formbricks/ui/components/Button";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
if (IS_FORMBRICKS_CLOUD) {
notFound();

View File

@@ -13,7 +13,7 @@ import { DatabaseError, UnknownError } from "@formbricks/types/errors";
import { TMember, TMembership } from "@formbricks/types/memberships";
export const getMembersByOrganizationId = reactCache(
(organizationId: string, page?: number): Promise<TMember[]> =>
async (organizationId: string, page?: number): Promise<TMember[]> =>
cache(
async () => {
validateInputs([organizationId, ZString], [page, ZOptionalNumber]);
@@ -135,7 +135,7 @@ export const deleteMembership = async (
};
export const getMembershipsByUserId = reactCache(
(userId: string, page?: number): Promise<TMembership[]> =>
async (userId: string, page?: number): Promise<TMembership[]> =>
cache(
async () => {
validateInputs([userId, ZString], [page, ZOptionalNumber]);

View File

@@ -30,7 +30,8 @@ const MembersLoading = () => (
</div>
);
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session) {

View File

@@ -4,7 +4,11 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
const Layout = async ({ children, params }) => {
const Layout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const [organization, product, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),

View File

@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
const Page = ({ params }) => {
const Page = async (props) => {
const params = await props.params;
return redirect(`/environments/${params.environmentId}/settings/profile`);
};

View File

@@ -5,10 +5,11 @@ import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
type Props = {
params: { surveyId: string; environmentId: string };
params: Promise<{ surveyId: string; environmentId: string }>;
};
export const generateMetadata = async ({ params }: Props): Promise<Metadata> => {
export const generateMetadata = async (props: Props): Promise<Metadata> => {
const params = await props.params;
const session = await getServerSession(authOptions);
const survey = await getSurvey(params.surveyId);
const responseCount = await getResponseCountBySurveyId(params.surveyId);

View File

@@ -27,7 +27,8 @@ import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session) {
@@ -72,7 +73,7 @@ const Page = async ({ params }) => {
const isAIEnabled = await getIsAIEnabled(organization);
const shouldGenerateInsights = needsInsightsGeneration(survey);
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
return (
<PageContentWrapper>

View File

@@ -1,5 +1,6 @@
import { InboxIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";

View File

@@ -11,7 +11,12 @@ import { TInsight } from "@formbricks/types/insights";
import { TSurveyQuestionId, ZSurveyQuestionId } from "@formbricks/types/surveys/types";
export const getInsightsBySurveyIdQuestionId = reactCache(
(surveyId: string, questionId: TSurveyQuestionId, limit?: number, offset?: number): Promise<TInsight[]> =>
async (
surveyId: string,
questionId: TSurveyQuestionId,
limit?: number,
offset?: number
): Promise<TInsight[]> =>
cache(
async () => {
validateInputs([surveyId, ZId], [questionId, ZSurveyQuestionId]);

View File

@@ -119,6 +119,11 @@ const evaluateLogicAndGetNextQuestionId = (
}
}
// If no jump target was set, check for a fallback logic
if (!firstJumpTarget && currQuesTemp.logicFallback) {
firstJumpTarget = currQuesTemp.logicFallback;
}
// Return the first jump target if found, otherwise go to the next question
const nextQuestionId = firstJumpTarget || questions[currentQuestionIndex + 1]?.id || undefined;
@@ -883,7 +888,7 @@ export const getQuestionSummary = async (
};
export const getSurveySummary = reactCache(
(surveyId: string, filterCriteria?: TResponseFilterCriteria): Promise<TSurveySummary> =>
async (surveyId: string, filterCriteria?: TResponseFilterCriteria): Promise<TSurveySummary> =>
cache(
async () => {
validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]);

View File

@@ -28,7 +28,8 @@ import { getUser } from "@formbricks/lib/user/service";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session) {

View File

@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
const Page = ({ params }) => {
const Page = async (props) => {
const params = await props.params;
return redirect(`/environments/${params.environmentId}/surveys/${params.surveyId}/summary`);
};

View File

@@ -34,7 +34,7 @@ export const surveySelect: Prisma.SurveySelect = {
};
export const getSurveys = reactCache(
(
async (
environmentId: string,
limit?: number,
offset?: number,
@@ -87,7 +87,7 @@ export const getSurveys = reactCache(
);
export const getSurveysSortedByRelevance = reactCache(
(
async (
environmentId: string,
limit?: number,
offset?: number,
@@ -173,7 +173,7 @@ export const getSurveysSortedByRelevance = reactCache(
);
export const getSurvey = reactCache(
(surveyId: string): Promise<TSurvey | null> =>
async (surveyId: string): Promise<TSurvey | null> =>
cache(
async () => {
validateInputs([surveyId, ZId]);

View File

@@ -27,15 +27,17 @@ export const metadata: Metadata = {
};
interface SurveyTemplateProps {
params: {
params: Promise<{
environmentId: string;
};
searchParams: {
}>;
searchParams: Promise<{
role?: TTemplateRole;
};
}>;
}
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
const Page = async (props: SurveyTemplateProps) => {
const searchParams = await props.searchParams;
const params = await props.params;
const session = await getServerSession(authOptions);
const product = await getProductByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);

View File

@@ -18,10 +18,11 @@ import {
} from "@formbricks/lib/constants";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
const Page = async ({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) => {
const Page = async (props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) => {
const searchParams = await props.searchParams;
const inviteToken = searchParams["inviteToken"] ?? null;
const isMultOrgEnabled = await getIsMultiOrgEnabled();
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
if (!inviteToken && (!SIGNUP_ENABLED || !isMultOrgEnabled)) {
notFound();
}

View File

@@ -6,7 +6,8 @@ import { getEmailFromEmailToken } from "@formbricks/lib/jwt";
const VerificationPageSchema = z.string().email();
const Page = async ({ searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const t = await getTranslations();
const email = getEmailFromEmailToken(searchParams.token);
try {

View File

@@ -2,7 +2,8 @@ import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper";
import { SignIn } from "@/app/(auth)/auth/verify/components/SignIn";
import { getTranslations } from "next-intl/server";
const Page = async ({ searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const t = await getTranslations();
return searchParams && searchParams.token ? (
<FormWrapper>

View File

@@ -10,7 +10,8 @@ import { getUser, updateUser } from "@formbricks/lib/user/service";
import { Button } from "@formbricks/ui/components/Button";
import { ContentLayout } from "./components/ContentLayout";
const Page = async ({ searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const t = await getTranslations();
const session = await getServerSession(authOptions);
const user = session?.user.id ? await getUser(session.user.id) : null;

View File

@@ -4,7 +4,8 @@ import { webhookHandler } from "./lib/stripeWebhook";
export const POST = async (request: Request) => {
const body = await request.text();
const signature = headers().get("stripe-signature") as string;
const requestHeaders = await headers();
const signature = requestHeaders.get("stripe-signature") as string;
const { status, message } = await webhookHandler(body, signature);

View File

@@ -10,7 +10,11 @@ export const metadata: Metadata = {
title: "Billing",
};
const BillingLayout = async ({ children, params }) => {
const BillingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
if (!IS_FORMBRICKS_CLOUD) {
notFound();

View File

@@ -16,7 +16,8 @@ import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper
import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { PricingTable } from "./components/PricingTable";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {

View File

@@ -9,8 +9,9 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getUserProducts } from "@formbricks/lib/product/service";
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
export const GET = async (_: Request, context: { params: { organizationId: string } }) => {
const organizationId = context?.params?.organizationId;
export const GET = async (_: Request, context: { params: Promise<{ organizationId: string }> }) => {
const params = await context?.params;
const organizationId = params?.organizationId;
if (!organizationId) return notFound();
// check auth
const session = await getServerSession(authOptions);

View File

@@ -6,8 +6,9 @@ import { getEnvironments } from "@formbricks/lib/environment/service";
import { getProduct } from "@formbricks/lib/product/service";
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
export const GET = async (_: Request, context: { params: { productId: string } }) => {
const productId = context?.params?.productId;
export const GET = async (_: Request, context: { params: Promise<{ productId: string }> }) => {
const params = await context?.params;
const productId = params.productId;
if (!productId) return notFound();
// check auth
const session = await getServerSession(authOptions);

View File

@@ -4,7 +4,8 @@ import { notFound, redirect } from "next/navigation";
import { getShortUrl } from "@formbricks/lib/shortUrl/service";
import { TShortUrl, ZShortUrlId } from "@formbricks/types/short-url";
export const generateMetadata = async ({ params }): Promise<Metadata> => {
export const generateMetadata = async (props): Promise<Metadata> => {
const params = await props.params;
if (!params.shortUrlId) {
notFound();
}
@@ -27,7 +28,8 @@ export const generateMetadata = async ({ params }): Promise<Metadata> => {
}
};
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
if (!params.shortUrlId) {
notFound();
}

View File

@@ -15,8 +15,9 @@ const ZGenerateInsightsInput = z.object({
export const POST = async (request: Request) => {
try {
const requestHeaders = await headers();
// Check authentication
if (headers().get("x-api-key") !== CRON_SECRET) {
if (requestHeaders.get("x-api-key") !== CRON_SECRET) {
return responses.notAuthenticatedResponse();
}

View File

@@ -4,8 +4,10 @@ import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { writeData as writeNotionData } from "@formbricks/lib/notion/service";
import { processResponseData } from "@formbricks/lib/responses";
import { writeDataToSlack } from "@formbricks/lib/slack/service";
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
import { TAttributes } from "@formbricks/types/attributes";
import { Result } from "@formbricks/types/error-handlers";
import { TIntegration } from "@formbricks/types/integration";
import { TIntegration, TIntegrationType } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
@@ -29,18 +31,20 @@ const convertMetaObjectToString = (metadata: TResponseMeta): string => {
};
const processDataForIntegration = async (
integrationType: TIntegrationType,
data: TPipelineInput,
survey: TSurvey,
includeVariables: boolean,
includeMetadata: boolean,
includeHiddenFields: boolean,
questionIds: string[]
questionIds: string[],
attributes?: TAttributes
): Promise<string[][]> => {
const ids =
includeHiddenFields && survey.hiddenFields.fieldIds
? [...questionIds, ...survey.hiddenFields.fieldIds]
: questionIds;
const values = await extractResponses(data, ids, survey);
const values = await extractResponses(integrationType, data, ids, survey, attributes);
if (includeMetadata) {
values[0].push(convertMetaObjectToString(data.response.meta));
values[1].push("Metadata");
@@ -61,7 +65,8 @@ const processDataForIntegration = async (
export const handleIntegrations = async (
integrations: TIntegration[],
data: TPipelineInput,
survey: TSurvey
survey: TSurvey,
attributes: TAttributes
) => {
for (const integration of integrations) {
switch (integration.type) {
@@ -76,7 +81,12 @@ export const handleIntegrations = async (
}
break;
case "slack":
const slackResult = await handleSlackIntegration(integration as TIntegrationSlack, data, survey);
const slackResult = await handleSlackIntegration(
integration as TIntegrationSlack,
data,
survey,
attributes
);
if (!slackResult.ok) {
console.error("Error in slack integration: ", slackResult.error);
}
@@ -111,6 +121,7 @@ const handleAirtableIntegration = async (
for (const element of integration.config.data) {
if (element.surveyId === data.surveyId) {
const values = await processDataForIntegration(
"airtable",
data,
survey,
!!element.includeVariables,
@@ -145,6 +156,7 @@ const handleGoogleSheetsIntegration = async (
for (const element of integration.config.data) {
if (element.surveyId === data.surveyId) {
const values = await processDataForIntegration(
"googleSheets",
data,
survey,
!!element.includeVariables,
@@ -177,19 +189,22 @@ const handleGoogleSheetsIntegration = async (
const handleSlackIntegration = async (
integration: TIntegrationSlack,
data: TPipelineInput,
survey: TSurvey
survey: TSurvey,
attributes: TAttributes
): Promise<Result<void, Error>> => {
try {
if (integration.config.data.length > 0) {
for (const element of integration.config.data) {
if (element.surveyId === data.surveyId) {
const values = await processDataForIntegration(
"slack",
data,
survey,
!!element.includeVariables,
!!element.includeMetadata,
!!element.includeHiddenFields,
element.questionIds
element.questionIds,
attributes
);
await writeDataToSlack(integration.config.key, element.channelId, values, survey?.name);
}
@@ -209,9 +224,11 @@ const handleSlackIntegration = async (
};
const extractResponses = async (
integrationType: TIntegrationType,
pipelineData: TPipelineInput,
questionIds: string[],
survey: TSurvey
survey: TSurvey,
attributes?: TAttributes
): Promise<string[][]> => {
const responses: string[] = [];
const questions: string[] = [];
@@ -246,7 +263,22 @@ const extractResponses = async (
} else {
responses.push("");
}
questions.push(getLocalizedValue(question?.headline, "default") || "");
// Create emptyResponseObject with same keys but empty string values
const emptyResponseObject = Object.keys(pipelineData.response.data).reduce(
(acc, key) => {
acc[key] = "";
return acc;
},
{} as Record<string, string>
);
questions.push(
parseRecallInfo(
getLocalizedValue(question?.headline, "default"),
integrationType === "slack" ? attributes : {},
integrationType === "slack" ? pipelineData.response.data : emptyResponseObject,
integrationType === "slack" ? pipelineData.response.variables : {}
) || ""
);
}
return [responses, questions];

View File

@@ -21,8 +21,9 @@ import { TWebhook } from "@formbricks/types/webhooks";
import { handleIntegrations } from "./lib/handleIntegrations";
export const POST = async (request: Request) => {
const requestHeaders = await headers();
// Check authentication
if (headers().get("x-api-key") !== CRON_SECRET) {
if (requestHeaders.get("x-api-key") !== CRON_SECRET) {
return responses.notAuthenticatedResponse();
}
@@ -41,6 +42,7 @@ export const POST = async (request: Request) => {
}
const { environmentId, surveyId, event, response } = inputValidation.data;
const attributes = response.person?.id ? await getAttributes(response.person?.id) : {};
// Fetch webhooks
const getWebhooksForPipeline = cache(
@@ -98,7 +100,7 @@ export const POST = async (request: Request) => {
}
if (integrations.length > 0) {
await handleIntegrations(integrations, inputValidation.data, survey);
await handleIntegrations(integrations, inputValidation.data, survey, attributes);
}
// Fetch users with notifications in a single query
@@ -196,8 +198,6 @@ export const POST = async (request: Request) => {
const isAIEnabled = await getIsAIEnabled(organization);
if (isAIEnabled) {
const attributes = response.person?.id ? await getAttributes(response.person?.id) : {};
for (const question of survey.questions) {
if (question.type === "openText" && question.insightsEnabled) {
const isQuestionAnswered =

Some files were not shown because too many files have changed in this diff Show More