mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
Compare commits
5 Commits
4.0.0
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cb3881cd0 | ||
|
|
ec78038caa | ||
|
|
908614b4e2 | ||
|
|
c584901337 | ||
|
|
087699fb57 |
@@ -5,6 +5,7 @@ import { ShareSurveyModal } from "@/app/(app)/environments/[environmentId]/surve
|
|||||||
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
||||||
|
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
|
||||||
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
|
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
|
||||||
import { Badge } from "@/modules/ui/components/badge";
|
import { Badge } from "@/modules/ui/components/badge";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -12,7 +13,7 @@ import { IconBar } from "@/modules/ui/components/iconbar";
|
|||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { BellRing, Eye, SquarePenIcon } from "lucide-react";
|
import { BellRing, Eye, SquarePenIcon } from "lucide-react";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TSegment } from "@formbricks/types/segment";
|
import { TSegment } from "@formbricks/types/segment";
|
||||||
@@ -48,17 +49,16 @@ export const SurveyAnalysisCTA = ({
|
|||||||
isFormbricksCloud,
|
isFormbricksCloud,
|
||||||
}: SurveyAnalysisCTAProps) => {
|
}: SurveyAnalysisCTAProps) => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [modalState, setModalState] = useState<ModalState>({
|
const [modalState, setModalState] = useState<ModalState>({
|
||||||
start: searchParams.get("share") === "true",
|
start: searchParams.get("share") === "true",
|
||||||
share: false,
|
share: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const surveyUrl = useMemo(() => `${publicDomain}/s/${survey.id}`, [survey.id, publicDomain]);
|
const { refreshSingleUseId } = useSingleUseId(survey);
|
||||||
|
|
||||||
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||||
|
|
||||||
@@ -102,9 +102,18 @@ export const SurveyAnalysisCTA = ({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPreviewUrl = () => {
|
const getPreviewUrl = async () => {
|
||||||
const separator = surveyUrl.includes("?") ? "&" : "?";
|
const surveyUrl = new URL(`${publicDomain}/s/${survey.id}`);
|
||||||
return `${surveyUrl}${separator}preview=true`;
|
|
||||||
|
if (survey.singleUse?.enabled) {
|
||||||
|
const newId = await refreshSingleUseId();
|
||||||
|
if (newId) {
|
||||||
|
surveyUrl.searchParams.set("suId", newId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
surveyUrl.searchParams.set("preview", "true");
|
||||||
|
return surveyUrl.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
|
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
|
||||||
@@ -119,7 +128,10 @@ export const SurveyAnalysisCTA = ({
|
|||||||
{
|
{
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
tooltip: t("common.preview"),
|
tooltip: t("common.preview"),
|
||||||
onClick: () => window.open(getPreviewUrl(), "_blank"),
|
onClick: async () => {
|
||||||
|
const previewUrl = await getPreviewUrl();
|
||||||
|
window.open(previewUrl, "_blank");
|
||||||
|
},
|
||||||
isVisible: survey.type === "link",
|
isVisible: survey.type === "link",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import { updateSingleUseLinksAction } from "@/app/(app)/environments/[environmen
|
|||||||
import { DisableLinkModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal";
|
import { DisableLinkModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal";
|
||||||
import { DocumentationLinks } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links";
|
import { DocumentationLinks } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links";
|
||||||
import { ShareSurveyLink } from "@/modules/analysis/components/ShareSurveyLink";
|
import { ShareSurveyLink } from "@/modules/analysis/components/ShareSurveyLink";
|
||||||
import { getSurveyUrl } from "@/modules/analysis/utils";
|
|
||||||
import { generateSingleUseIdsAction } from "@/modules/survey/list/actions";
|
import { generateSingleUseIdsAction } from "@/modules/survey/list/actions";
|
||||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { CirclePlayIcon } from "lucide-react";
|
import { CirclePlayIcon, CopyIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@@ -33,6 +32,7 @@ export const AnonymousLinksTab = ({
|
|||||||
setSurveyUrl,
|
setSurveyUrl,
|
||||||
locale,
|
locale,
|
||||||
}: AnonymousLinksTabProps) => {
|
}: AnonymousLinksTabProps) => {
|
||||||
|
const surveyUrlWithCustomSuid = `${surveyUrl}?suId=CUSTOM-ID`;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
@@ -173,11 +173,9 @@ export const AnonymousLinksTab = ({
|
|||||||
count,
|
count,
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseSurveyUrl = getSurveyUrl(survey, publicDomain, "default");
|
|
||||||
|
|
||||||
if (!!response?.data?.length) {
|
if (!!response?.data?.length) {
|
||||||
const singleUseIds = response.data;
|
const singleUseIds = response.data;
|
||||||
const surveyLinks = singleUseIds.map((singleUseId) => `${baseSurveyUrl}?suId=${singleUseId}`);
|
const surveyLinks = singleUseIds.map((singleUseId) => `${surveyUrl}?suId=${singleUseId}`);
|
||||||
|
|
||||||
// Create content with just the links
|
// Create content with just the links
|
||||||
const csvContent = surveyLinks.join("\n");
|
const csvContent = surveyLinks.join("\n");
|
||||||
@@ -258,14 +256,36 @@ export const AnonymousLinksTab = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{!singleUseEncryption ? (
|
{!singleUseEncryption ? (
|
||||||
<Alert variant="info" size="default">
|
<div className="flex w-full flex-col gap-4">
|
||||||
<AlertTitle>
|
<Alert variant="info" size="default">
|
||||||
{t("environments.surveys.share.anonymous_links.custom_single_use_id_title")}
|
<AlertTitle>
|
||||||
</AlertTitle>
|
{t("environments.surveys.share.anonymous_links.custom_single_use_id_title")}
|
||||||
<AlertDescription>
|
</AlertTitle>
|
||||||
{t("environments.surveys.share.anonymous_links.custom_single_use_id_description")}
|
|
||||||
</AlertDescription>
|
<AlertDescription>
|
||||||
</Alert>
|
{t("environments.surveys.share.anonymous_links.custom_single_use_id_description")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="grid w-full grid-cols-6 items-center gap-2">
|
||||||
|
<div className="col-span-5 truncate rounded-md border border-slate-200 px-2 py-1">
|
||||||
|
<span className="truncate text-sm text-slate-900">{surveyUrlWithCustomSuid}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(surveyUrlWithCustomSuid);
|
||||||
|
|
||||||
|
toast.success(t("common.copied_to_clipboard"));
|
||||||
|
}}
|
||||||
|
className="col-span-1 gap-1 text-sm">
|
||||||
|
{t("common.copy")}
|
||||||
|
|
||||||
|
<CopyIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{singleUseEncryption && (
|
{singleUseEncryption && (
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { responses } from "@/app/lib/api/response";
|
import { responses } from "@/app/lib/api/response";
|
||||||
import { CRON_SECRET } from "@/lib/constants";
|
import { CRON_SECRET, SMTP_HOST } from "@/lib/constants";
|
||||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
||||||
import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "@/modules/email";
|
import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "@/modules/email";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import { logger } from "@formbricks/logger";
|
||||||
import { getNotificationResponse } from "./lib/notificationResponse";
|
import { getNotificationResponse } from "./lib/notificationResponse";
|
||||||
import { getOrganizationIds } from "./lib/organization";
|
import { getOrganizationIds } from "./lib/organization";
|
||||||
import { getProjectsByOrganizationId } from "./lib/project";
|
import { getProjectsByOrganizationId } from "./lib/project";
|
||||||
@@ -16,6 +17,11 @@ export const POST = async (): Promise<Response> => {
|
|||||||
return responses.notAuthenticatedResponse();
|
return responses.notAuthenticatedResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!SMTP_HOST) {
|
||||||
|
logger.info("SMTP_HOST is not configured, skipping weekly summary email");
|
||||||
|
return responses.successResponse({}, true);
|
||||||
|
}
|
||||||
|
|
||||||
const emailSendingPromises: Promise<void>[] = [];
|
const emailSendingPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
// Fetch all organization IDs
|
// Fetch all organization IDs
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
|
|||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
import { getSurveyUrl } from "../../utils";
|
||||||
|
|
||||||
vi.mock("react-hot-toast", () => ({
|
vi.mock("react-hot-toast", () => ({
|
||||||
toast: {
|
toast: {
|
||||||
@@ -11,6 +12,24 @@ vi.mock("react-hot-toast", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock the useSingleUseId hook
|
||||||
|
vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({
|
||||||
|
useSingleUseId: vi.fn(() => ({
|
||||||
|
singleUseId: "test-single-use-id",
|
||||||
|
refreshSingleUseId: vi.fn().mockResolvedValue("test-single-use-id"),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the survey utils
|
||||||
|
vi.mock("../../utils", () => ({
|
||||||
|
getSurveyUrl: vi.fn((survey, publicDomain, language) => {
|
||||||
|
if (language && language !== "en") {
|
||||||
|
return `${publicDomain}/s/${survey.id}?lang=${language}`;
|
||||||
|
}
|
||||||
|
return `${publicDomain}/s/${survey.id}`;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const survey: TSurvey = {
|
const survey: TSurvey = {
|
||||||
id: "survey-id",
|
id: "survey-id",
|
||||||
name: "Test Survey",
|
name: "Test Survey",
|
||||||
@@ -161,7 +180,7 @@ describe("ShareSurveyLink", () => {
|
|||||||
expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard");
|
expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("opens the preview link in a new tab when preview button is clicked (no query params)", () => {
|
test("opens the preview link in a new tab when preview button is clicked (no query params)", async () => {
|
||||||
render(
|
render(
|
||||||
<ShareSurveyLink
|
<ShareSurveyLink
|
||||||
survey={survey}
|
survey={survey}
|
||||||
@@ -175,10 +194,13 @@ describe("ShareSurveyLink", () => {
|
|||||||
const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab");
|
const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab");
|
||||||
fireEvent.click(previewButton);
|
fireEvent.click(previewButton);
|
||||||
|
|
||||||
|
// Wait for the async function to complete
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
expect(global.open).toHaveBeenCalledWith(`${surveyUrl}?preview=true`, "_blank");
|
expect(global.open).toHaveBeenCalledWith(`${surveyUrl}?preview=true`, "_blank");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("opens the preview link in a new tab when preview button is clicked (with query params)", () => {
|
test("opens the preview link in a new tab when preview button is clicked (with query params)", async () => {
|
||||||
const surveyWithParamsUrl = `${publicDomain}/s/survey-id?foo=bar`;
|
const surveyWithParamsUrl = `${publicDomain}/s/survey-id?foo=bar`;
|
||||||
render(
|
render(
|
||||||
<ShareSurveyLink
|
<ShareSurveyLink
|
||||||
@@ -193,6 +215,9 @@ describe("ShareSurveyLink", () => {
|
|||||||
const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab");
|
const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab");
|
||||||
fireEvent.click(previewButton);
|
fireEvent.click(previewButton);
|
||||||
|
|
||||||
|
// Wait for the async function to complete
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
expect(global.open).toHaveBeenCalledWith(`${surveyWithParamsUrl}&preview=true`, "_blank");
|
expect(global.open).toHaveBeenCalledWith(`${surveyWithParamsUrl}&preview=true`, "_blank");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,7 +240,9 @@ describe("ShareSurveyLink", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("updates the survey URL when the language is changed", () => {
|
test("updates the survey URL when the language is changed", () => {
|
||||||
const { rerender } = render(
|
const mockGetSurveyUrl = vi.mocked(getSurveyUrl);
|
||||||
|
|
||||||
|
render(
|
||||||
<ShareSurveyLink
|
<ShareSurveyLink
|
||||||
survey={survey}
|
survey={survey}
|
||||||
publicDomain={publicDomain}
|
publicDomain={publicDomain}
|
||||||
@@ -231,16 +258,7 @@ describe("ShareSurveyLink", () => {
|
|||||||
const germanOption = screen.getByText("German");
|
const germanOption = screen.getByText("German");
|
||||||
fireEvent.click(germanOption);
|
fireEvent.click(germanOption);
|
||||||
|
|
||||||
rerender(
|
expect(mockGetSurveyUrl).toHaveBeenCalledWith(survey, publicDomain, "de");
|
||||||
<ShareSurveyLink
|
expect(setSurveyUrl).toHaveBeenCalledWith(`${publicDomain}/s/${survey.id}?lang=de`);
|
||||||
survey={survey}
|
|
||||||
publicDomain={publicDomain}
|
|
||||||
surveyUrl={surveyUrl}
|
|
||||||
setSurveyUrl={setSurveyUrl}
|
|
||||||
locale={locale}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(setSurveyUrl).toHaveBeenCalled();
|
|
||||||
expect(surveyUrl).toContain("lang=de");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { Copy, SquareArrowOutUpRight } from "lucide-react";
|
import { Copy, SquareArrowOutUpRight } from "lucide-react";
|
||||||
@@ -32,6 +33,22 @@ export const ShareSurveyLink = ({
|
|||||||
setSurveyUrl(url);
|
setSurveyUrl(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { refreshSingleUseId } = useSingleUseId(survey);
|
||||||
|
|
||||||
|
const getPreviewUrl = async () => {
|
||||||
|
const previewUrl = new URL(surveyUrl);
|
||||||
|
|
||||||
|
if (survey.singleUse?.enabled) {
|
||||||
|
const newId = await refreshSingleUseId();
|
||||||
|
if (newId) {
|
||||||
|
previewUrl.searchParams.set("suId", newId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previewUrl.searchParams.set("preview", "true");
|
||||||
|
return previewUrl.toString();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"flex max-w-full flex-col items-center justify-center gap-2 md:flex-row"}>
|
<div className={"flex max-w-full flex-col items-center justify-center gap-2 md:flex-row"}>
|
||||||
<SurveyLinkDisplay surveyUrl={surveyUrl} key={surveyUrl} />
|
<SurveyLinkDisplay surveyUrl={surveyUrl} key={surveyUrl} />
|
||||||
@@ -53,14 +70,9 @@ export const ShareSurveyLink = ({
|
|||||||
title={t("environments.surveys.preview_survey_in_a_new_tab")}
|
title={t("environments.surveys.preview_survey_in_a_new_tab")}
|
||||||
aria-label={t("environments.surveys.preview_survey_in_a_new_tab")}
|
aria-label={t("environments.surveys.preview_survey_in_a_new_tab")}
|
||||||
disabled={!surveyUrl}
|
disabled={!surveyUrl}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
let previewUrl = surveyUrl;
|
const url = await getPreviewUrl();
|
||||||
if (previewUrl.includes("?")) {
|
window.open(url, "_blank");
|
||||||
previewUrl += "&preview=true";
|
|
||||||
} else {
|
|
||||||
previewUrl += "?preview=true";
|
|
||||||
}
|
|
||||||
window.open(previewUrl, "_blank");
|
|
||||||
}}>
|
}}>
|
||||||
{t("common.preview")}
|
{t("common.preview")}
|
||||||
<SquareArrowOutUpRight />
|
<SquareArrowOutUpRight />
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import { DatePicker } from "@/modules/ui/components/date-picker";
|
|||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { Slider } from "@/modules/ui/components/slider";
|
import { Slider } from "@/modules/ui/components/slider";
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { ArrowUpRight, CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import { KeyboardEventHandler, useEffect, useState } from "react";
|
import { KeyboardEventHandler, useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
@@ -47,12 +45,6 @@ export const ResponseOptionsCard = ({
|
|||||||
subheading: t("environments.surveys.edit.survey_completed_subheading"),
|
subheading: t("environments.surveys.edit.survey_completed_subheading"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [singleUseMessage, setSingleUseMessage] = useState({
|
|
||||||
heading: t("environments.surveys.edit.survey_already_answered_heading"),
|
|
||||||
subheading: t("environments.surveys.edit.survey_already_answered_subheading"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [singleUseEncryption, setSingleUseEncryption] = useState(true);
|
|
||||||
const [runOnDate, setRunOnDate] = useState<Date | null>(null);
|
const [runOnDate, setRunOnDate] = useState<Date | null>(null);
|
||||||
const [closeOnDate, setCloseOnDate] = useState<Date | null>(null);
|
const [closeOnDate, setCloseOnDate] = useState<Date | null>(null);
|
||||||
const [recaptchaThreshold, setRecaptchaThreshold] = useState<number>(localSurvey.recaptcha?.threshold ?? 0);
|
const [recaptchaThreshold, setRecaptchaThreshold] = useState<number>(localSurvey.recaptcha?.threshold ?? 0);
|
||||||
@@ -163,53 +155,6 @@ export const ResponseOptionsCard = ({
|
|||||||
setLocalSurvey({ ...localSurvey, surveyClosedMessage: message });
|
setLocalSurvey({ ...localSurvey, surveyClosedMessage: message });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSingleUseSurveyToggle = () => {
|
|
||||||
if (!localSurvey.singleUse?.enabled) {
|
|
||||||
setLocalSurvey({
|
|
||||||
...localSurvey,
|
|
||||||
singleUse: { enabled: true, ...singleUseMessage, isEncrypted: singleUseEncryption },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLocalSurvey({ ...localSurvey, singleUse: { enabled: false, isEncrypted: false } });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSingleUseSurveyMessageChange = ({
|
|
||||||
heading,
|
|
||||||
subheading,
|
|
||||||
}: {
|
|
||||||
heading?: string;
|
|
||||||
subheading?: string;
|
|
||||||
}) => {
|
|
||||||
const message = {
|
|
||||||
heading: heading ?? singleUseMessage.heading,
|
|
||||||
subheading: subheading ?? singleUseMessage.subheading,
|
|
||||||
};
|
|
||||||
|
|
||||||
const localSurveySingleUseEnabled = localSurvey.singleUse?.enabled ?? false;
|
|
||||||
setSingleUseMessage(message);
|
|
||||||
setLocalSurvey({
|
|
||||||
...localSurvey,
|
|
||||||
singleUse: { enabled: localSurveySingleUseEnabled, ...message, isEncrypted: singleUseEncryption },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const hangleSingleUseEncryptionToggle = () => {
|
|
||||||
if (!singleUseEncryption) {
|
|
||||||
setSingleUseEncryption(true);
|
|
||||||
setLocalSurvey({
|
|
||||||
...localSurvey,
|
|
||||||
singleUse: { enabled: true, ...singleUseMessage, isEncrypted: true },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSingleUseEncryption(false);
|
|
||||||
setLocalSurvey({
|
|
||||||
...localSurvey,
|
|
||||||
singleUse: { enabled: true, ...singleUseMessage, isEncrypted: false },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHideBackButtonToggle = () => {
|
const handleHideBackButtonToggle = () => {
|
||||||
setLocalSurvey({ ...localSurvey, isBackButtonHidden: !localSurvey.isBackButtonHidden });
|
setLocalSurvey({ ...localSurvey, isBackButtonHidden: !localSurvey.isBackButtonHidden });
|
||||||
};
|
};
|
||||||
@@ -223,14 +168,6 @@ export const ResponseOptionsCard = ({
|
|||||||
setSurveyClosedMessageToggle(true);
|
setSurveyClosedMessageToggle(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localSurvey.singleUse?.enabled) {
|
|
||||||
setSingleUseMessage({
|
|
||||||
heading: localSurvey.singleUse.heading ?? singleUseMessage.heading,
|
|
||||||
subheading: localSurvey.singleUse.subheading ?? singleUseMessage.subheading,
|
|
||||||
});
|
|
||||||
setSingleUseEncryption(localSurvey.singleUse.isEncrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localSurvey.runOnDate) {
|
if (localSurvey.runOnDate) {
|
||||||
setRunOnDate(localSurvey.runOnDate);
|
setRunOnDate(localSurvey.runOnDate);
|
||||||
setRunOnDateToggle(true);
|
setRunOnDateToggle(true);
|
||||||
@@ -240,13 +177,7 @@ export const ResponseOptionsCard = ({
|
|||||||
setCloseOnDate(localSurvey.closeOnDate);
|
setCloseOnDate(localSurvey.closeOnDate);
|
||||||
setCloseOnDateToggle(true);
|
setCloseOnDateToggle(true);
|
||||||
}
|
}
|
||||||
}, [
|
}, [localSurvey, surveyClosedMessage.heading, surveyClosedMessage.subheading]);
|
||||||
localSurvey,
|
|
||||||
singleUseMessage.heading,
|
|
||||||
singleUseMessage.subheading,
|
|
||||||
surveyClosedMessage.heading,
|
|
||||||
surveyClosedMessage.subheading,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const toggleAutocomplete = () => {
|
const toggleAutocomplete = () => {
|
||||||
if (autoComplete) {
|
if (autoComplete) {
|
||||||
@@ -471,80 +402,6 @@ export const ResponseOptionsCard = ({
|
|||||||
</div>
|
</div>
|
||||||
</AdvancedOptionToggle>
|
</AdvancedOptionToggle>
|
||||||
|
|
||||||
{/* Single User Survey Options */}
|
|
||||||
<AdvancedOptionToggle
|
|
||||||
htmlId="singleUserSurveyOptions"
|
|
||||||
isChecked={!!localSurvey.singleUse?.enabled}
|
|
||||||
onToggle={handleSingleUseSurveyToggle}
|
|
||||||
title={t("environments.surveys.edit.single_use_survey_links")}
|
|
||||||
description={t("environments.surveys.edit.single_use_survey_links_description")}
|
|
||||||
childBorder={true}>
|
|
||||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
|
||||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
|
||||||
<div className="row mb-2 flex cursor-default items-center space-x-2">
|
|
||||||
<Label htmlFor="howItWorks">{t("environments.surveys.edit.how_it_works")}</Label>
|
|
||||||
</div>
|
|
||||||
<ul className="mb-3 ml-4 cursor-default list-inside list-disc space-y-1">
|
|
||||||
<li className="text-sm text-slate-600">
|
|
||||||
{t(
|
|
||||||
"environments.surveys.edit.blocks_survey_if_the_survey_url_has_no_single_use_id_suid"
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
<li className="text-sm text-slate-600">
|
|
||||||
{t(
|
|
||||||
"environments.surveys.edit.blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already"
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
<li className="text-sm text-slate-600">
|
|
||||||
<Link
|
|
||||||
href="https://formbricks.com/docs/link-surveys/single-use-links"
|
|
||||||
target="_blank"
|
|
||||||
className="underline">
|
|
||||||
{t("common.read_docs")} <ArrowUpRight className="inline" size={16} />
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Label htmlFor="headline">{t("environments.surveys.edit.link_used_message")}</Label>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
id="heading"
|
|
||||||
className="mb-4 mt-2 bg-white"
|
|
||||||
name="heading"
|
|
||||||
value={singleUseMessage.heading}
|
|
||||||
onChange={(e) => handleSingleUseSurveyMessageChange({ heading: e.target.value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Label htmlFor="headline">{t("environments.surveys.edit.subheading")}</Label>
|
|
||||||
<Input
|
|
||||||
className="mb-4 mt-2 bg-white"
|
|
||||||
id="subheading"
|
|
||||||
name="subheading"
|
|
||||||
value={singleUseMessage.subheading}
|
|
||||||
onChange={(e) => handleSingleUseSurveyMessageChange({ subheading: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="headline">{t("environments.surveys.edit.url_encryption")}</Label>
|
|
||||||
<div>
|
|
||||||
<div className="mt-2 flex items-center space-x-1">
|
|
||||||
<Switch
|
|
||||||
id="encryption-switch"
|
|
||||||
checked={singleUseEncryption}
|
|
||||||
onCheckedChange={hangleSingleUseEncryptionToggle}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="encryption-label">
|
|
||||||
<div className="ml-2">
|
|
||||||
<p className="text-sm font-normal text-slate-600">
|
|
||||||
{t(
|
|
||||||
"environments.surveys.edit.enable_encryption_of_single_use_id_suid_in_survey_url"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AdvancedOptionToggle>
|
|
||||||
|
|
||||||
{/* Verify Email Section */}
|
{/* Verify Email Section */}
|
||||||
<AdvancedOptionToggle
|
<AdvancedOptionToggle
|
||||||
htmlId="verifyEmailBeforeSubmission"
|
htmlId="verifyEmailBeforeSubmission"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const useSingleUseId = (survey: TSurvey | TSurveyList) => {
|
|||||||
if (survey.singleUse?.enabled) {
|
if (survey.singleUse?.enabled) {
|
||||||
const response = await generateSingleUseIdsAction({
|
const response = await generateSingleUseIdsAction({
|
||||||
surveyId: survey.id,
|
surveyId: survey.id,
|
||||||
isEncrypted: !!survey.singleUse?.isEncrypted,
|
isEncrypted: Boolean(survey.singleUse?.isEncrypted),
|
||||||
count: 1,
|
count: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user