mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 05:40:02 -06:00
Compare commits
3 Commits
release/4.
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85285d1fe1 | ||
|
|
1ae98226ad | ||
|
|
d25dc8f85d |
@@ -3,8 +3,13 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { OperationNotAllowedError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors";
|
import { OperationNotAllowedError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors";
|
||||||
|
import { TResponseInput } from "@formbricks/types/responses";
|
||||||
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||||
import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
|
import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
|
||||||
|
import { createResponseWithQuotaEvaluation } from "@/app/api/v1/client/[environmentId]/responses/lib/response";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
||||||
|
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||||
@@ -17,6 +22,29 @@ import { getOrganizationLogoUrl } from "@/modules/ee/whitelabel/email-customizat
|
|||||||
import { sendEmbedSurveyPreviewEmail } from "@/modules/email";
|
import { sendEmbedSurveyPreviewEmail } from "@/modules/email";
|
||||||
import { deleteResponsesAndDisplaysForSurvey } from "./lib/survey";
|
import { deleteResponsesAndDisplaysForSurvey } from "./lib/survey";
|
||||||
|
|
||||||
|
const loremIpsumSentences = [
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||||
|
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
|
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
|
||||||
|
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum.",
|
||||||
|
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit.",
|
||||||
|
"Nisi ut aliquip ex ea commodo consequat.",
|
||||||
|
"Pellentesque habitant morbi tristique senectus et netus et malesuada fames.",
|
||||||
|
"Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante.",
|
||||||
|
"Donec eu libero sit amet quam egestas semper.",
|
||||||
|
"Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateLoremIpsum(): string {
|
||||||
|
const sentenceCount = Math.floor(Math.random() * 3) + 1;
|
||||||
|
const selectedSentences: string[] = [];
|
||||||
|
for (let i = 0; i < sentenceCount; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * loremIpsumSentences.length);
|
||||||
|
selectedSentences.push(loremIpsumSentences[randomIndex]);
|
||||||
|
}
|
||||||
|
return selectedSentences.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
const ZSendEmbedSurveyPreviewEmailAction = z.object({
|
const ZSendEmbedSurveyPreviewEmailAction = z.object({
|
||||||
surveyId: ZId,
|
surveyId: ZId,
|
||||||
});
|
});
|
||||||
@@ -260,3 +288,169 @@ export const updateSingleUseLinksAction = authenticatedActionClient
|
|||||||
|
|
||||||
return updatedSurvey;
|
return updatedSurvey;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ZGenerateTestResponsesAction = z.object({
|
||||||
|
surveyId: ZId,
|
||||||
|
environmentId: ZId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateTestResponsesAction = authenticatedActionClient
|
||||||
|
.schema(ZGenerateTestResponsesAction)
|
||||||
|
.action(async ({ ctx, parsedInput }) => {
|
||||||
|
await checkAuthorizationUpdated({
|
||||||
|
userId: ctx.user.id,
|
||||||
|
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||||
|
access: [
|
||||||
|
{
|
||||||
|
type: "organization",
|
||||||
|
roles: ["owner", "manager"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "projectTeam",
|
||||||
|
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||||
|
minPermission: "readWrite",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const survey = await getSurvey(parsedInput.surveyId);
|
||||||
|
if (!survey) {
|
||||||
|
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (survey.environmentId !== parsedInput.environmentId) {
|
||||||
|
throw new OperationNotAllowedError("Survey does not belong to the specified environment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedElementTypes = [
|
||||||
|
TSurveyElementTypeEnum.OpenText,
|
||||||
|
TSurveyElementTypeEnum.NPS,
|
||||||
|
TSurveyElementTypeEnum.Rating,
|
||||||
|
TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||||
|
TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||||
|
TSurveyElementTypeEnum.PictureSelection,
|
||||||
|
TSurveyElementTypeEnum.Ranking,
|
||||||
|
TSurveyElementTypeEnum.Matrix,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Extract elements from blocks
|
||||||
|
const elements = getElementsFromBlocks(survey.blocks);
|
||||||
|
const supportedElements = elements.filter((element) => supportedElementTypes.includes(element.type));
|
||||||
|
|
||||||
|
if (supportedElements.length === 0) {
|
||||||
|
throw new OperationNotAllowedError(
|
||||||
|
"Survey does not contain any supported question types (OpenText, NPS, Rating, Multiple Choice, Picture Selection, Ranking, or Matrix)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsesToCreate = 5;
|
||||||
|
const createdResponses: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < responsesToCreate; i++) {
|
||||||
|
const responseData: Record<string, string | number | string[] | Record<string, string>> = {};
|
||||||
|
|
||||||
|
for (const element of supportedElements) {
|
||||||
|
if (element.type === TSurveyElementTypeEnum.OpenText) {
|
||||||
|
responseData[element.id] = generateLoremIpsum();
|
||||||
|
} else if (element.type === TSurveyElementTypeEnum.NPS) {
|
||||||
|
responseData[element.id] = Math.floor(Math.random() * 11);
|
||||||
|
} else if (element.type === TSurveyElementTypeEnum.Rating) {
|
||||||
|
const range = "range" in element && typeof element.range === "number" ? element.range : 5;
|
||||||
|
responseData[element.id] = Math.floor(Math.random() * range) + 1;
|
||||||
|
} else if (element.type === TSurveyElementTypeEnum.MultipleChoiceSingle) {
|
||||||
|
// Single choice: pick one random option, store the label
|
||||||
|
if ("choices" in element && Array.isArray(element.choices) && element.choices.length > 0) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * element.choices.length);
|
||||||
|
const selectedChoice = element.choices[randomIndex];
|
||||||
|
// For "other" option, generate custom text; otherwise use the choice label
|
||||||
|
responseData[element.id] =
|
||||||
|
selectedChoice.id === "other"
|
||||||
|
? generateLoremIpsum()
|
||||||
|
: getLocalizedValue(selectedChoice.label, "default");
|
||||||
|
}
|
||||||
|
} else if (element.type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
|
||||||
|
// Multi choice: pick 1-3 random options, store the labels
|
||||||
|
if ("choices" in element && Array.isArray(element.choices) && element.choices.length > 0) {
|
||||||
|
const numSelections = Math.min(Math.floor(Math.random() * 3) + 1, element.choices.length);
|
||||||
|
const shuffled = [...element.choices].sort(() => Math.random() - 0.5);
|
||||||
|
responseData[element.id] = shuffled.slice(0, numSelections).map((choice) => {
|
||||||
|
// For "other" option, generate custom text; otherwise use the choice label
|
||||||
|
return choice.id === "other"
|
||||||
|
? generateLoremIpsum()
|
||||||
|
: getLocalizedValue(choice.label, "default");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (element.type === TSurveyElementTypeEnum.PictureSelection) {
|
||||||
|
// Picture selection: single or multi based on allowMulti
|
||||||
|
if ("choices" in element && Array.isArray(element.choices) && element.choices.length > 0) {
|
||||||
|
const allowMulti = "allowMulti" in element ? element.allowMulti : false;
|
||||||
|
if (allowMulti) {
|
||||||
|
const numSelections = Math.min(Math.floor(Math.random() * 3) + 1, element.choices.length);
|
||||||
|
const shuffled = [...element.choices].sort(() => Math.random() - 0.5);
|
||||||
|
responseData[element.id] = shuffled.slice(0, numSelections).map((choice) => choice.id);
|
||||||
|
} else {
|
||||||
|
const randomIndex = Math.floor(Math.random() * element.choices.length);
|
||||||
|
responseData[element.id] = element.choices[randomIndex].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (element.type === TSurveyElementTypeEnum.Ranking) {
|
||||||
|
// Ranking: all options in random order, store the labels
|
||||||
|
if ("choices" in element && Array.isArray(element.choices) && element.choices.length > 0) {
|
||||||
|
const shuffled = [...element.choices].sort(() => Math.random() - 0.5);
|
||||||
|
responseData[element.id] = shuffled.map((choice) => {
|
||||||
|
// For "other" option, generate custom text; otherwise use the choice label
|
||||||
|
return choice.id === "other"
|
||||||
|
? generateLoremIpsum()
|
||||||
|
: getLocalizedValue(choice.label, "default");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (element.type === TSurveyElementTypeEnum.Matrix) {
|
||||||
|
// Matrix: for each row, pick a random column
|
||||||
|
if (
|
||||||
|
"rows" in element &&
|
||||||
|
"columns" in element &&
|
||||||
|
Array.isArray(element.rows) &&
|
||||||
|
Array.isArray(element.columns) &&
|
||||||
|
element.rows.length > 0 &&
|
||||||
|
element.columns.length > 0
|
||||||
|
) {
|
||||||
|
const matrixData: Record<string, string> = {};
|
||||||
|
for (const row of element.rows) {
|
||||||
|
const randomColumnIndex = Math.floor(Math.random() * element.columns.length);
|
||||||
|
matrixData[row.id] = element.columns[randomColumnIndex].id;
|
||||||
|
}
|
||||||
|
responseData[element.id] = matrixData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseInput: TResponseInput = {
|
||||||
|
environmentId: parsedInput.environmentId,
|
||||||
|
surveyId: parsedInput.surveyId,
|
||||||
|
finished: true,
|
||||||
|
data: responseData,
|
||||||
|
meta: {
|
||||||
|
source: "test",
|
||||||
|
userAgent: {
|
||||||
|
browser: "Test Generator",
|
||||||
|
device: "desktop",
|
||||||
|
os: "Test OS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await createResponseWithQuotaEvaluation(responseInput);
|
||||||
|
createdResponses.push(response.id);
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnknownError(
|
||||||
|
`Failed to create response: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
createdCount: createdResponses.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BellRing, Eye, ListRestart, SquarePenIcon } from "lucide-react";
|
import { BellRing, Eye, ListRestart, Sparkles, SquarePenIcon } from "lucide-react";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@@ -20,7 +20,7 @@ import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/action
|
|||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
||||||
import { IconBar } from "@/modules/ui/components/iconbar";
|
import { IconBar } from "@/modules/ui/components/iconbar";
|
||||||
import { resetSurveyAction } from "../actions";
|
import { generateTestResponsesAction, resetSurveyAction } from "../actions";
|
||||||
|
|
||||||
interface SurveyAnalysisCTAProps {
|
interface SurveyAnalysisCTAProps {
|
||||||
survey: TSurvey;
|
survey: TSurvey;
|
||||||
@@ -63,6 +63,7 @@ export const SurveyAnalysisCTA = ({
|
|||||||
});
|
});
|
||||||
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||||
const [isResetting, setIsResetting] = useState(false);
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
|
const [isGeneratingResponses, setIsGeneratingResponses] = useState(false);
|
||||||
|
|
||||||
const { organizationId, project } = useEnvironment();
|
const { organizationId, project } = useEnvironment();
|
||||||
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
|
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
|
||||||
@@ -147,6 +148,23 @@ export const SurveyAnalysisCTA = ({
|
|||||||
setIsResetModalOpen(false);
|
setIsResetModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGenerateTestResponses = async () => {
|
||||||
|
if (isGeneratingResponses) return;
|
||||||
|
setIsGeneratingResponses(true);
|
||||||
|
const result = await generateTestResponsesAction({
|
||||||
|
surveyId: survey.id,
|
||||||
|
environmentId: environment.id,
|
||||||
|
});
|
||||||
|
if (result?.data?.success) {
|
||||||
|
toast.success(`Successfully generated ${result.data.createdCount} test responses`);
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
const errorMessage = getFormattedErrorMessage(result);
|
||||||
|
toast.error(errorMessage);
|
||||||
|
}
|
||||||
|
setIsGeneratingResponses(false);
|
||||||
|
};
|
||||||
|
|
||||||
const iconActions = [
|
const iconActions = [
|
||||||
{
|
{
|
||||||
icon: BellRing,
|
icon: BellRing,
|
||||||
@@ -163,6 +181,12 @@ export const SurveyAnalysisCTA = ({
|
|||||||
},
|
},
|
||||||
isVisible: survey.type === "link",
|
isVisible: survey.type === "link",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: Sparkles,
|
||||||
|
tooltip: isGeneratingResponses ? "Generating responses..." : "Generate test responses",
|
||||||
|
onClick: handleGenerateTestResponses,
|
||||||
|
isVisible: !isReadOnly,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: ListRestart,
|
icon: ListRestart,
|
||||||
tooltip: t("environments.surveys.summary.reset_survey"),
|
tooltip: t("environments.surveys.summary.reset_survey"),
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator";
|
import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator";
|
||||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||||
import { WEBAPP_URL } from "@/lib/constants";
|
|
||||||
import { getActionClasses } from "@/lib/actionClass/service";
|
import { getActionClasses } from "@/lib/actionClass/service";
|
||||||
|
import { WEBAPP_URL } from "@/lib/constants";
|
||||||
import { getEnvironments } from "@/lib/environment/service";
|
import { getEnvironments } from "@/lib/environment/service";
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
|
|||||||
Reference in New Issue
Block a user