mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-08 00:40:09 -06:00
chore: remove unused fields and tables from prisma schema (#6531)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -23,12 +23,12 @@ describe("ConnectWithFormbricks", () => {
|
||||
const webAppUrl = "http://app";
|
||||
const channel = {} as any;
|
||||
|
||||
test("renders waiting state when widgetSetupCompleted is false", () => {
|
||||
test("renders waiting state when appSetupCompleted is false", () => {
|
||||
render(
|
||||
<ConnectWithFormbricks
|
||||
environment={environment}
|
||||
publicDomain={webAppUrl}
|
||||
widgetSetupCompleted={false}
|
||||
appSetupCompleted={false}
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
@@ -36,12 +36,12 @@ describe("ConnectWithFormbricks", () => {
|
||||
expect(screen.getByText("environments.connect.waiting_for_your_signal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders success state when widgetSetupCompleted is true", () => {
|
||||
test("renders success state when appSetupCompleted is true", () => {
|
||||
render(
|
||||
<ConnectWithFormbricks
|
||||
environment={environment}
|
||||
publicDomain={webAppUrl}
|
||||
widgetSetupCompleted={true}
|
||||
appSetupCompleted={true}
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
@@ -54,7 +54,7 @@ describe("ConnectWithFormbricks", () => {
|
||||
<ConnectWithFormbricks
|
||||
environment={environment}
|
||||
publicDomain={webAppUrl}
|
||||
widgetSetupCompleted={true}
|
||||
appSetupCompleted={true}
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
@@ -68,7 +68,7 @@ describe("ConnectWithFormbricks", () => {
|
||||
<ConnectWithFormbricks
|
||||
environment={environment}
|
||||
publicDomain={webAppUrl}
|
||||
widgetSetupCompleted={false}
|
||||
appSetupCompleted={false}
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,14 +13,14 @@ import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||
interface ConnectWithFormbricksProps {
|
||||
environment: TEnvironment;
|
||||
publicDomain: string;
|
||||
widgetSetupCompleted: boolean;
|
||||
appSetupCompleted: boolean;
|
||||
channel: TProjectConfigChannel;
|
||||
}
|
||||
|
||||
export const ConnectWithFormbricks = ({
|
||||
environment,
|
||||
publicDomain,
|
||||
widgetSetupCompleted,
|
||||
appSetupCompleted,
|
||||
channel,
|
||||
}: ConnectWithFormbricksProps) => {
|
||||
const { t } = useTranslate();
|
||||
@@ -51,15 +51,15 @@ export const ConnectWithFormbricks = ({
|
||||
environmentId={environment.id}
|
||||
publicDomain={publicDomain}
|
||||
channel={channel}
|
||||
widgetSetupCompleted={widgetSetupCompleted}
|
||||
appSetupCompleted={appSetupCompleted}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[30rem] w-1/2 flex-col items-center justify-center rounded-lg border text-center",
|
||||
widgetSetupCompleted ? "border-green-500 bg-green-100" : "border-slate-300 bg-slate-200"
|
||||
appSetupCompleted ? "border-green-500 bg-green-100" : "border-slate-300 bg-slate-200"
|
||||
)}>
|
||||
{widgetSetupCompleted ? (
|
||||
{appSetupCompleted ? (
|
||||
<div>
|
||||
<p className="text-3xl">{t("environments.connect.congrats")}</p>
|
||||
<p className="pt-4 text-sm font-medium text-slate-600">
|
||||
@@ -81,9 +81,9 @@ export const ConnectWithFormbricks = ({
|
||||
</div>
|
||||
<Button
|
||||
id="finishOnboarding"
|
||||
variant={widgetSetupCompleted ? "default" : "ghost"}
|
||||
variant={appSetupCompleted ? "default" : "ghost"}
|
||||
onClick={handleFinishOnboarding}>
|
||||
{widgetSetupCompleted
|
||||
{appSetupCompleted
|
||||
? t("environments.connect.finish_onboarding")
|
||||
: t("environments.connect.do_it_later")}
|
||||
<ArrowRight />
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("OnboardingSetupInstructions", () => {
|
||||
environmentId: "env-123",
|
||||
publicDomain: "https://example.com",
|
||||
channel: "app" as const, // Assuming channel is either "app" or "website"
|
||||
widgetSetupCompleted: false,
|
||||
appSetupCompleted: false,
|
||||
};
|
||||
|
||||
test("renders HTML tab content by default", () => {
|
||||
|
||||
@@ -20,14 +20,14 @@ interface OnboardingSetupInstructionsProps {
|
||||
environmentId: string;
|
||||
publicDomain: string;
|
||||
channel: TProjectConfigChannel;
|
||||
widgetSetupCompleted: boolean;
|
||||
appSetupCompleted: boolean;
|
||||
}
|
||||
|
||||
export const OnboardingSetupInstructions = ({
|
||||
environmentId,
|
||||
publicDomain,
|
||||
channel,
|
||||
widgetSetupCompleted,
|
||||
appSetupCompleted,
|
||||
}: OnboardingSetupInstructionsProps) => {
|
||||
const { t } = useTranslate();
|
||||
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||
@@ -137,7 +137,7 @@ export const OnboardingSetupInstructions = ({
|
||||
<div className="mt-4 flex justify-between space-x-2">
|
||||
<Button
|
||||
id="onboarding-inapp-connect-copy-code"
|
||||
variant={widgetSetupCompleted ? "secondary" : "default"}
|
||||
variant={appSetupCompleted ? "secondary" : "default"}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys
|
||||
|
||||
@@ -42,7 +42,7 @@ const Page = async (props: ConnectPageProps) => {
|
||||
<ConnectWithFormbricks
|
||||
environment={environment}
|
||||
publicDomain={publicDomain}
|
||||
widgetSetupCompleted={environment.appSetupCompleted}
|
||||
appSetupCompleted={environment.appSetupCompleted}
|
||||
channel={channel}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -36,8 +36,6 @@ describe("PosthogIdentify", () => {
|
||||
{
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
role: "engineer",
|
||||
objective: "increase_conversion",
|
||||
} as TUser
|
||||
}
|
||||
environmentId="env-456"
|
||||
@@ -57,8 +55,6 @@ describe("PosthogIdentify", () => {
|
||||
expect(mockIdentify).toHaveBeenCalledWith("user-123", {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
role: "engineer",
|
||||
objective: "increase_conversion",
|
||||
});
|
||||
|
||||
// environment + organization groups
|
||||
@@ -142,8 +138,6 @@ describe("PosthogIdentify", () => {
|
||||
expect(mockIdentify).toHaveBeenCalledWith("user-123", {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
role: undefined,
|
||||
objective: undefined,
|
||||
});
|
||||
// No environmentId or organizationId => no group calls
|
||||
expect(mockGroup).not.toHaveBeenCalled();
|
||||
|
||||
@@ -32,8 +32,6 @@ export const PosthogIdentify = ({
|
||||
posthog.identify(session.user.id, {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
objective: user.objective,
|
||||
});
|
||||
if (environmentId) {
|
||||
posthog.group("environment", environmentId, { name: environmentId });
|
||||
@@ -56,8 +54,6 @@ export const PosthogIdentify = ({
|
||||
organizationBilling,
|
||||
user.name,
|
||||
user.email,
|
||||
user.role,
|
||||
user.objective,
|
||||
isPosthogEnabled,
|
||||
]);
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ describe("Integrations Page", () => {
|
||||
expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("5 common.integrations");
|
||||
});
|
||||
|
||||
test("renders not connected status when widgetSetupCompleted is false", async () => {
|
||||
test("renders not connected status when appSetupCompleted is false", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: { ...mockEnvironment, appSetupCompleted: false },
|
||||
isReadOnly: false,
|
||||
|
||||
@@ -62,7 +62,7 @@ const Page = async (props) => {
|
||||
const isN8nIntegrationConnected = isIntegrationConnected("n8n");
|
||||
const isSlackIntegrationConnected = isIntegrationConnected("slack");
|
||||
|
||||
const widgetSetupCompleted = !!environment?.appSetupCompleted;
|
||||
const appSetupCompleted = !!environment?.appSetupCompleted;
|
||||
const integrationCards = [
|
||||
{
|
||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/zapier",
|
||||
@@ -202,8 +202,8 @@ const Page = async (props) => {
|
||||
label: "Javascript SDK",
|
||||
description: t("environments.integrations.website_or_app_integration_description"),
|
||||
icon: <Image src={JsLogo} alt="Javascript Logo" />,
|
||||
connected: widgetSetupCompleted,
|
||||
statusText: widgetSetupCompleted ? t("common.connected") : t("common.not_connected"),
|
||||
connected: appSetupCompleted,
|
||||
statusText: appSetupCompleted ? t("common.connected") : t("common.not_connected"),
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ const mockSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [],
|
||||
thankYouCard: { enabled: true, headline: "Thank You!" },
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: 0,
|
||||
|
||||
@@ -151,7 +151,6 @@ const mockSurvey: TSurvey = {
|
||||
status: "inProgress",
|
||||
type: "web",
|
||||
questions: [],
|
||||
thankYouCard: { enabled: false },
|
||||
endings: [],
|
||||
languages: [],
|
||||
triggers: [],
|
||||
|
||||
@@ -19,19 +19,19 @@ export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) =>
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
|
||||
const isAppSurvey = survey.type === "app";
|
||||
const widgetSetupCompleted = environment.appSetupCompleted;
|
||||
const appSetupCompleted = environment.appSetupCompleted;
|
||||
|
||||
useEffect(() => {
|
||||
const newSurveyParam = searchParams?.get("success");
|
||||
if (newSurveyParam && survey && environment) {
|
||||
setConfetti(true);
|
||||
toast.success(
|
||||
isAppSurvey && !widgetSetupCompleted
|
||||
isAppSurvey && !appSetupCompleted
|
||||
? t("environments.surveys.summary.almost_there")
|
||||
: t("environments.surveys.summary.congrats"),
|
||||
{
|
||||
id: "survey-publish-success-toast",
|
||||
icon: isAppSurvey && !widgetSetupCompleted ? "🤏" : "🎉",
|
||||
icon: isAppSurvey && !appSetupCompleted ? "🤏" : "🎉",
|
||||
duration: 5000,
|
||||
position: "bottom-right",
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) =>
|
||||
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
}, [environment, isAppSurvey, searchParams, survey, widgetSetupCompleted, t]);
|
||||
}, [environment, isAppSurvey, searchParams, survey, appSetupCompleted, t]);
|
||||
|
||||
return <>{confetti && <Confetti />}</>;
|
||||
};
|
||||
|
||||
@@ -69,7 +69,7 @@ export const SurveyAnalysisCTA = ({
|
||||
const { organizationId, project } = useEnvironment();
|
||||
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
|
||||
|
||||
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||
const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||
|
||||
useEffect(() => {
|
||||
setModalState((prev) => ({
|
||||
@@ -186,7 +186,7 @@ export const SurveyAnalysisCTA = ({
|
||||
|
||||
return (
|
||||
<div className="hidden justify-end gap-x-1.5 sm:flex">
|
||||
{!isReadOnly && (widgetSetupCompleted || survey.type === "link") && survey.status !== "draft" && (
|
||||
{!isReadOnly && (appSetupCompleted || survey.type === "link") && survey.status !== "draft" && (
|
||||
<SurveyStatusDropdown environment={environment} survey={survey} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -146,7 +146,6 @@ describe("AnonymousLinksTab", () => {
|
||||
createdBy: null,
|
||||
status: "draft" as const,
|
||||
questions: [],
|
||||
thankYouCard: { enabled: false },
|
||||
welcomeCard: { enabled: false },
|
||||
hiddenFields: { enabled: false },
|
||||
singleUse: {
|
||||
|
||||
@@ -88,7 +88,6 @@ const baseSurvey: TSurvey = {
|
||||
segment: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
verifyEmail: null,
|
||||
pin: null,
|
||||
productOverwrites: null,
|
||||
analytics: {
|
||||
|
||||
@@ -59,7 +59,6 @@ const mockDisplay = {
|
||||
contactId,
|
||||
surveyId,
|
||||
responseId: null,
|
||||
status: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -69,7 +68,6 @@ const mockDisplayWithoutContact = {
|
||||
contactId: null,
|
||||
surveyId,
|
||||
responseId: null,
|
||||
status: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const GET = async () => {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
projectId: true,
|
||||
widgetSetupCompleted: true,
|
||||
appSetupCompleted: true,
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -62,7 +62,7 @@ export const GET = async () => {
|
||||
type: apiKeyData.apiKeyEnvironments[0].environment.type,
|
||||
createdAt: apiKeyData.apiKeyEnvironments[0].environment.createdAt,
|
||||
updatedAt: apiKeyData.apiKeyEnvironments[0].environment.updatedAt,
|
||||
widgetSetupCompleted: apiKeyData.apiKeyEnvironments[0].environment.widgetSetupCompleted,
|
||||
appSetupCompleted: apiKeyData.apiKeyEnvironments[0].environment.appSetupCompleted,
|
||||
project: {
|
||||
id: apiKeyData.apiKeyEnvironments[0].environment.projectId,
|
||||
name: apiKeyData.apiKeyEnvironments[0].environment.project.name,
|
||||
|
||||
@@ -47,7 +47,6 @@ const mockDisplay = {
|
||||
contactId,
|
||||
surveyId,
|
||||
responseId: null,
|
||||
status: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -57,7 +56,6 @@ const mockDisplayWithoutContact = {
|
||||
contactId: null,
|
||||
surveyId,
|
||||
responseId: null,
|
||||
status: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
@@ -565,7 +565,6 @@ describe("Helper Functions", () => {
|
||||
test("buildSurvey returns built survey with overridden preset properties", () => {
|
||||
const config = {
|
||||
name: "Custom Survey",
|
||||
role: "productManager" as TTemplateRole,
|
||||
industries: ["eCommerce"] as string[],
|
||||
channels: ["link"],
|
||||
description: "Test survey",
|
||||
@@ -595,7 +594,6 @@ describe("Helper Functions", () => {
|
||||
|
||||
const survey = buildSurvey(config as any, mockT);
|
||||
expect(survey.name).toBe(config.name);
|
||||
expect(survey.role).toBe(config.role);
|
||||
expect(survey.industries).toEqual(config.industries);
|
||||
expect(survey.channels).toEqual(config.channels);
|
||||
expect(survey.description).toBe(config.description);
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
TSurveyRatingQuestion,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
|
||||
const getDefaultButtonLabel = (label: string | undefined, t: TFnType) =>
|
||||
createI18nString(label || t("common.next"), []);
|
||||
@@ -389,7 +389,6 @@ export const getDefaultSurveyPreset = (t: TFnType): TTemplate["preset"] => {
|
||||
export const buildSurvey = (
|
||||
config: {
|
||||
name: string;
|
||||
role: TTemplateRole;
|
||||
industries: ("eCommerce" | "saas" | "other")[];
|
||||
channels: ("link" | "app" | "website")[];
|
||||
description: string;
|
||||
@@ -402,7 +401,6 @@ export const buildSurvey = (
|
||||
const localSurvey = getDefaultSurveyPreset(t);
|
||||
return {
|
||||
name: config.name,
|
||||
role: config.role,
|
||||
industries: config.industries,
|
||||
channels: config.channels,
|
||||
description: config.description,
|
||||
|
||||
@@ -24,7 +24,6 @@ const cartAbandonmentSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.card_abandonment_survey"),
|
||||
role: "productManager",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["app", "website", "link"],
|
||||
description: t("templates.card_abandonment_survey_description"),
|
||||
@@ -125,7 +124,6 @@ const siteAbandonmentSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.site_abandonment_survey"),
|
||||
role: "productManager",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.site_abandonment_survey_description"),
|
||||
@@ -223,7 +221,6 @@ const productMarketFitSuperhuman = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.product_market_fit_superhuman"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.product_market_fit_superhuman_description"),
|
||||
@@ -298,7 +295,6 @@ const onboardingSegmentation = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.onboarding_segmentation"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.onboarding_segmentation_description"),
|
||||
@@ -362,7 +358,6 @@ const churnSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.churn_survey"),
|
||||
role: "sales",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.churn_survey_description"),
|
||||
@@ -452,7 +447,6 @@ const earnedAdvocacyScore = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.earned_advocacy_score_name"),
|
||||
role: "customerSuccess",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.earned_advocacy_score_description"),
|
||||
@@ -525,7 +519,6 @@ const usabilityScoreRatingSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.usability_score_name"),
|
||||
role: "customerSuccess",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.usability_rating_description"),
|
||||
@@ -651,7 +644,6 @@ const improveTrialConversion = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.improve_trial_conversion_name"),
|
||||
role: "sales",
|
||||
industries: ["saas"],
|
||||
channels: ["link", "app"],
|
||||
description: t("templates.improve_trial_conversion_description"),
|
||||
@@ -753,7 +745,6 @@ const reviewPrompt = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.review_prompt_name"),
|
||||
role: "marketing",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link", "app"],
|
||||
description: t("templates.review_prompt_description"),
|
||||
@@ -832,7 +823,6 @@ const interviewPrompt = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.interview_prompt_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.interview_prompt_description"),
|
||||
@@ -860,7 +850,6 @@ const improveActivationRate = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.improve_activation_rate_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["link"],
|
||||
description: t("templates.improve_activation_rate_description"),
|
||||
@@ -951,7 +940,6 @@ const employeeSatisfaction = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.employee_satisfaction_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.employee_satisfaction_description"),
|
||||
@@ -1029,7 +1017,6 @@ const uncoverStrengthsAndWeaknesses = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.uncover_strengths_and_weaknesses_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.uncover_strengths_and_weaknesses_description"),
|
||||
@@ -1083,7 +1070,6 @@ const productMarketFitShort = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.product_market_fit_short_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.product_market_fit_short_description"),
|
||||
@@ -1120,7 +1106,6 @@ const marketAttribution = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.market_attribution_name"),
|
||||
role: "marketing",
|
||||
industries: ["saas", "eCommerce"],
|
||||
channels: ["website", "app", "link"],
|
||||
description: t("templates.market_attribution_description"),
|
||||
@@ -1151,7 +1136,6 @@ const changingSubscriptionExperience = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.changing_subscription_experience_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.changing_subscription_experience_description"),
|
||||
@@ -1194,7 +1178,6 @@ const identifyCustomerGoals = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.identify_customer_goals_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.identify_customer_goals_description"),
|
||||
@@ -1224,7 +1207,6 @@ const featureChaser = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.feature_chaser_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.feature_chaser_description"),
|
||||
@@ -1263,7 +1245,6 @@ const fakeDoorFollowUp = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.fake_door_follow_up_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "eCommerce"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.fake_door_follow_up_description"),
|
||||
@@ -1307,7 +1288,6 @@ const feedbackBox = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.feedback_box_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.feedback_box_description"),
|
||||
@@ -1377,7 +1357,6 @@ const integrationSetupSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.integration_setup_survey_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.integration_setup_survey_description"),
|
||||
@@ -1450,7 +1429,6 @@ const newIntegrationSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.new_integration_survey_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.new_integration_survey_description"),
|
||||
@@ -1482,7 +1460,6 @@ const docsFeedback = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.docs_feedback_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "website", "link"],
|
||||
description: t("templates.docs_feedback_description"),
|
||||
@@ -1522,7 +1499,6 @@ const nps = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.nps_name"),
|
||||
role: "customerSuccess",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link", "website"],
|
||||
description: t("templates.nps_description"),
|
||||
@@ -1563,7 +1539,6 @@ const customerSatisfactionScore = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.csat_name"),
|
||||
role: "customerSuccess",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link", "website"],
|
||||
description: t("templates.csat_description"),
|
||||
@@ -1732,7 +1707,6 @@ const collectFeedback = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.collect_feedback_name"),
|
||||
role: "productManager",
|
||||
industries: ["other", "eCommerce"],
|
||||
channels: ["website", "link"],
|
||||
description: t("templates.collect_feedback_description"),
|
||||
@@ -1879,7 +1853,6 @@ const identifyUpsellOpportunities = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.identify_upsell_opportunities_name"),
|
||||
role: "sales",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.identify_upsell_opportunities_description"),
|
||||
@@ -1909,7 +1882,6 @@ const prioritizeFeatures = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.prioritize_features_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.prioritize_features_description"),
|
||||
@@ -1962,7 +1934,6 @@ const gaugeFeatureSatisfaction = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.gauge_feature_satisfaction_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.gauge_feature_satisfaction_description"),
|
||||
@@ -1996,7 +1967,6 @@ const marketSiteClarity = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.market_site_clarity_name"),
|
||||
role: "marketing",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["website"],
|
||||
description: t("templates.market_site_clarity_description"),
|
||||
@@ -2038,7 +2008,6 @@ const customerEffortScore = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.customer_effort_score_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.customer_effort_score_description"),
|
||||
@@ -2070,7 +2039,6 @@ const careerDevelopmentSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.career_development_survey_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.career_development_survey_description"),
|
||||
@@ -2157,7 +2125,6 @@ const professionalDevelopmentSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.professional_development_survey_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.professional_development_survey_description"),
|
||||
@@ -2245,7 +2212,6 @@ const rateCheckoutExperience = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.rate_checkout_experience_name"),
|
||||
role: "productManager",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["website", "app"],
|
||||
description: t("templates.rate_checkout_experience_description"),
|
||||
@@ -2322,7 +2288,6 @@ const measureSearchExperience = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.measure_search_experience_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "eCommerce"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.measure_search_experience_description"),
|
||||
@@ -2399,7 +2364,6 @@ const evaluateContentQuality = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.evaluate_content_quality_name"),
|
||||
role: "marketing",
|
||||
industries: ["other"],
|
||||
channels: ["website"],
|
||||
description: t("templates.evaluate_content_quality_description"),
|
||||
@@ -2477,7 +2441,6 @@ const measureTaskAccomplishment = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.measure_task_accomplishment_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.measure_task_accomplishment_description"),
|
||||
@@ -2660,7 +2623,6 @@ const identifySignUpBarriers = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.identify_sign_up_barriers_name"),
|
||||
role: "marketing",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["website"],
|
||||
description: t("templates.identify_sign_up_barriers_description"),
|
||||
@@ -2812,7 +2774,6 @@ const buildProductRoadmap = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.build_product_roadmap_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.build_product_roadmap_description"),
|
||||
@@ -2847,7 +2808,6 @@ const understandPurchaseIntention = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.understand_purchase_intention_name"),
|
||||
role: "sales",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["website", "link", "app"],
|
||||
description: t("templates.understand_purchase_intention_description"),
|
||||
@@ -2903,7 +2863,6 @@ const improveNewsletterContent = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.improve_newsletter_content_name"),
|
||||
role: "marketing",
|
||||
industries: ["eCommerce", "saas", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.improve_newsletter_content_description"),
|
||||
@@ -2994,7 +2953,6 @@ const evaluateAProductIdea = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.evaluate_a_product_idea_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["link", "app"],
|
||||
description: t("templates.evaluate_a_product_idea_description"),
|
||||
@@ -3097,7 +3055,6 @@ const understandLowEngagement = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.understand_low_engagement_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["link"],
|
||||
description: t("templates.understand_low_engagement_description"),
|
||||
@@ -3183,7 +3140,6 @@ const employeeWellBeing = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.employee_well_being_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.employee_well_being_description"),
|
||||
@@ -3233,7 +3189,6 @@ const longTermRetentionCheckIn = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.long_term_retention_check_in_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.long_term_retention_check_in_description"),
|
||||
@@ -3342,7 +3297,6 @@ const professionalDevelopmentGrowth = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.professional_development_growth_survey_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.professional_development_growth_survey_description"),
|
||||
@@ -3392,7 +3346,6 @@ const recognitionAndReward = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.recognition_and_reward_survey_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.recognition_and_reward_survey_description"),
|
||||
@@ -3441,7 +3394,6 @@ const alignmentAndEngagement = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.alignment_and_engagement_survey_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.alignment_and_engagement_survey_description"),
|
||||
@@ -3490,7 +3442,6 @@ const supportiveWorkCulture = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.supportive_work_culture_survey_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.supportive_work_culture_survey_description"),
|
||||
|
||||
@@ -13,7 +13,6 @@ export const selectDisplay = {
|
||||
updatedAt: true,
|
||||
surveyId: true,
|
||||
contactId: true,
|
||||
status: true,
|
||||
} satisfies Prisma.DisplaySelect;
|
||||
|
||||
export const getDisplayCountBySurveyId = reactCache(
|
||||
|
||||
@@ -17,7 +17,6 @@ const createMockDisplay = (overrides = {}) => {
|
||||
surveyId: mockSurveyId,
|
||||
responseId: null,
|
||||
personId: null,
|
||||
status: null,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,7 +34,6 @@ describe("Environment Service", () => {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
appSetupCompleted: false,
|
||||
widgetSetupCompleted: false,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(mockEnvironment);
|
||||
@@ -95,11 +94,9 @@ describe("Environment Service", () => {
|
||||
environments: [
|
||||
{
|
||||
...mockEnvironments[0],
|
||||
widgetSetupCompleted: false,
|
||||
},
|
||||
{
|
||||
...mockEnvironments[1],
|
||||
widgetSetupCompleted: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -143,7 +140,6 @@ describe("Environment Service", () => {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
appSetupCompleted: false,
|
||||
widgetSetupCompleted: false,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.environment.update).mockResolvedValue(mockEnvironment);
|
||||
|
||||
@@ -508,11 +508,3 @@ export const mockTranslatedEndings = [
|
||||
buttonLabel: { default: "Create your own Survey", de: "" },
|
||||
},
|
||||
];
|
||||
|
||||
export const mockLegacyThankYouCard = {
|
||||
buttonLink: "https://formbricks.com",
|
||||
enabled: true,
|
||||
headline: "Thank you!",
|
||||
subheader: "We appreciate your feedback.",
|
||||
buttonLabel: "Create your own Survey",
|
||||
};
|
||||
|
||||
@@ -244,7 +244,6 @@ describe("Response Processing", () => {
|
||||
displayPercentage: 100,
|
||||
styling: null,
|
||||
projectOverwrites: null,
|
||||
verifyEmail: null,
|
||||
inlineTriggers: [],
|
||||
pin: null,
|
||||
triggers: [],
|
||||
|
||||
@@ -126,13 +126,11 @@ export const mockUser: TUser = {
|
||||
updatedAt: currentDate,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "google",
|
||||
objective: "improve_user_retention",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
role: "other",
|
||||
locale: "en-US",
|
||||
lastLoginAt: new Date(),
|
||||
isActive: true,
|
||||
@@ -262,8 +260,6 @@ export const mockSyncSurveyOutput: SurveyMock = {
|
||||
followUps: [],
|
||||
variables: [],
|
||||
showLanguageSwitch: null,
|
||||
thankYouCard: null,
|
||||
verifyEmail: null,
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
@@ -287,8 +283,6 @@ export const mockSurveyOutput: SurveyMock = {
|
||||
followUps: [],
|
||||
variables: [],
|
||||
showLanguageSwitch: null,
|
||||
thankYouCard: null,
|
||||
verifyEmail: null,
|
||||
...baseSurveyProperties,
|
||||
};
|
||||
|
||||
|
||||
@@ -532,8 +532,6 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Fix this, this happens because the survey type "web" is no longer in the zod types but its required in the schema for migration
|
||||
// @ts-expect-error
|
||||
const modifiedSurvey: TSurvey = {
|
||||
...prismaSurvey, // Properties from prismaSurvey
|
||||
displayPercentage: Number(prismaSurvey.displayPercentage) || null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { deleteOrganization, getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||
import { IdentityProvider, Objective, Prisma, Role } from "@prisma/client";
|
||||
import { IdentityProvider, Prisma } from "@prisma/client";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
@@ -37,10 +37,8 @@ describe("User Service", () => {
|
||||
emailVerified: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: Role.project_manager,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: IdentityProvider.email,
|
||||
objective: Objective.increase_conversion,
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
|
||||
|
||||
@@ -18,10 +18,8 @@ const responseSelection = {
|
||||
emailVerified: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
twoFactorEnabled: true,
|
||||
identityProvider: true,
|
||||
objective: true,
|
||||
notificationSettings: true,
|
||||
locale: true,
|
||||
lastLoginAt: true,
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as services from "@/lib/utils/services";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import {
|
||||
getEnvironmentIdFromInsightId,
|
||||
getEnvironmentIdFromResponseId,
|
||||
getEnvironmentIdFromSegmentId,
|
||||
getEnvironmentIdFromSurveyId,
|
||||
@@ -11,9 +10,7 @@ import {
|
||||
getOrganizationIdFromActionClassId,
|
||||
getOrganizationIdFromApiKeyId,
|
||||
getOrganizationIdFromContactId,
|
||||
getOrganizationIdFromDocumentId,
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromInsightId,
|
||||
getOrganizationIdFromIntegrationId,
|
||||
getOrganizationIdFromInviteId,
|
||||
getOrganizationIdFromLanguageId,
|
||||
@@ -28,9 +25,7 @@ import {
|
||||
getProductIdFromContactId,
|
||||
getProjectIdFromActionClassId,
|
||||
getProjectIdFromContactId,
|
||||
getProjectIdFromDocumentId,
|
||||
getProjectIdFromEnvironmentId,
|
||||
getProjectIdFromInsightId,
|
||||
getProjectIdFromIntegrationId,
|
||||
getProjectIdFromLanguageId,
|
||||
getProjectIdFromQuotaId,
|
||||
@@ -58,8 +53,6 @@ vi.mock("@/lib/utils/services", () => ({
|
||||
getInvite: vi.fn(),
|
||||
getLanguage: vi.fn(),
|
||||
getTeam: vi.fn(),
|
||||
getInsight: vi.fn(),
|
||||
getDocument: vi.fn(),
|
||||
getTag: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -369,46 +362,6 @@ describe("Helper Utilities", () => {
|
||||
await expect(getOrganizationIdFromTeamId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getOrganizationIdFromInsightId returns organization ID correctly", async () => {
|
||||
vi.mocked(services.getInsight).mockResolvedValueOnce({
|
||||
environmentId: "env1",
|
||||
});
|
||||
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
|
||||
projectId: "project1",
|
||||
});
|
||||
vi.mocked(services.getProject).mockResolvedValueOnce({
|
||||
organizationId: "org1",
|
||||
});
|
||||
|
||||
const orgId = await getOrganizationIdFromInsightId("insight1");
|
||||
expect(orgId).toBe("org1");
|
||||
});
|
||||
|
||||
test("getOrganizationIdFromInsightId throws error when insight not found", async () => {
|
||||
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
|
||||
await expect(getOrganizationIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getOrganizationIdFromDocumentId returns organization ID correctly", async () => {
|
||||
vi.mocked(services.getDocument).mockResolvedValueOnce({
|
||||
environmentId: "env1",
|
||||
});
|
||||
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
|
||||
projectId: "project1",
|
||||
});
|
||||
vi.mocked(services.getProject).mockResolvedValueOnce({
|
||||
organizationId: "org1",
|
||||
});
|
||||
|
||||
const orgId = await getOrganizationIdFromDocumentId("doc1");
|
||||
expect(orgId).toBe("org1");
|
||||
});
|
||||
|
||||
test("getOrganizationIdFromDocumentId throws error when document not found", async () => {
|
||||
vi.mocked(services.getDocument).mockResolvedValueOnce(null);
|
||||
await expect(getOrganizationIdFromDocumentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getOrganizationIdFromQuotaId returns organization ID correctly", async () => {
|
||||
vi.mocked(services.getQuota).mockResolvedValueOnce({
|
||||
surveyId: "survey1",
|
||||
@@ -481,23 +434,6 @@ describe("Helper Utilities", () => {
|
||||
await expect(getProjectIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getProjectIdFromInsightId returns project ID correctly", async () => {
|
||||
vi.mocked(services.getInsight).mockResolvedValueOnce({
|
||||
environmentId: "env1",
|
||||
});
|
||||
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
|
||||
projectId: "project1",
|
||||
});
|
||||
|
||||
const projectId = await getProjectIdFromInsightId("insight1");
|
||||
expect(projectId).toBe("project1");
|
||||
});
|
||||
|
||||
test("getProjectIdFromInsightId throws error when insight not found", async () => {
|
||||
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
|
||||
await expect(getProjectIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getProjectIdFromSegmentId returns project ID correctly", async () => {
|
||||
vi.mocked(services.getSegment).mockResolvedValueOnce({
|
||||
environmentId: "env1",
|
||||
@@ -600,23 +536,6 @@ describe("Helper Utilities", () => {
|
||||
await expect(getProductIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getProjectIdFromDocumentId returns project ID correctly", async () => {
|
||||
vi.mocked(services.getDocument).mockResolvedValueOnce({
|
||||
environmentId: "env1",
|
||||
});
|
||||
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
|
||||
projectId: "project1",
|
||||
});
|
||||
|
||||
const projectId = await getProjectIdFromDocumentId("doc1");
|
||||
expect(projectId).toBe("project1");
|
||||
});
|
||||
|
||||
test("getProjectIdFromDocumentId throws error when document not found", async () => {
|
||||
vi.mocked(services.getDocument).mockResolvedValueOnce(null);
|
||||
await expect(getProjectIdFromDocumentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getProjectIdFromIntegrationId returns project ID correctly", async () => {
|
||||
vi.mocked(services.getIntegration).mockResolvedValueOnce({
|
||||
environmentId: "env1",
|
||||
@@ -699,20 +618,6 @@ describe("Helper Utilities", () => {
|
||||
await expect(getEnvironmentIdFromResponseId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getEnvironmentIdFromInsightId returns environment ID directly", async () => {
|
||||
vi.mocked(services.getInsight).mockResolvedValueOnce({
|
||||
environmentId: "env1",
|
||||
});
|
||||
|
||||
const environmentId = await getEnvironmentIdFromInsightId("insight1");
|
||||
expect(environmentId).toBe("env1");
|
||||
});
|
||||
|
||||
test("getEnvironmentIdFromInsightId throws error when insight not found", async () => {
|
||||
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
|
||||
await expect(getEnvironmentIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("getEnvironmentIdFromSegmentId returns environment ID directly", async () => {
|
||||
vi.mocked(services.getSegment).mockResolvedValueOnce({
|
||||
environmentId: "env1",
|
||||
|
||||
@@ -2,9 +2,7 @@ import {
|
||||
getActionClass,
|
||||
getApiKey,
|
||||
getContact,
|
||||
getDocument,
|
||||
getEnvironment,
|
||||
getInsight,
|
||||
getIntegration,
|
||||
getInvite,
|
||||
getLanguage,
|
||||
@@ -176,24 +174,6 @@ export const getOrganizationIdFromTeamId = async (teamId: string) => {
|
||||
return team.organizationId;
|
||||
};
|
||||
|
||||
export const getOrganizationIdFromInsightId = async (insightId: string) => {
|
||||
const insight = await getInsight(insightId);
|
||||
if (!insight) {
|
||||
throw new ResourceNotFoundError("insight", insightId);
|
||||
}
|
||||
|
||||
return await getOrganizationIdFromEnvironmentId(insight.environmentId);
|
||||
};
|
||||
|
||||
export const getOrganizationIdFromDocumentId = async (documentId: string) => {
|
||||
const document = await getDocument(documentId);
|
||||
if (!document) {
|
||||
throw new ResourceNotFoundError("document", documentId);
|
||||
}
|
||||
|
||||
return await getOrganizationIdFromEnvironmentId(document.environmentId);
|
||||
};
|
||||
|
||||
export const getOrganizationIdFromQuotaId = async (quotaId: string) => {
|
||||
const quota = await getQuota(quotaId);
|
||||
|
||||
@@ -219,15 +199,6 @@ export const getProjectIdFromSurveyId = async (surveyId: string) => {
|
||||
return await getProjectIdFromEnvironmentId(survey.environmentId);
|
||||
};
|
||||
|
||||
export const getProjectIdFromInsightId = async (insightId: string) => {
|
||||
const insight = await getInsight(insightId);
|
||||
if (!insight) {
|
||||
throw new ResourceNotFoundError("insight", insightId);
|
||||
}
|
||||
|
||||
return await getProjectIdFromEnvironmentId(insight.environmentId);
|
||||
};
|
||||
|
||||
export const getProjectIdFromSegmentId = async (segmentId: string) => {
|
||||
const segment = await getSegment(segmentId);
|
||||
if (!segment) {
|
||||
@@ -282,15 +253,6 @@ export const getProductIdFromContactId = async (contactId: string) => {
|
||||
return await getProjectIdFromEnvironmentId(contact.environmentId);
|
||||
};
|
||||
|
||||
export const getProjectIdFromDocumentId = async (documentId: string) => {
|
||||
const document = await getDocument(documentId);
|
||||
if (!document) {
|
||||
throw new ResourceNotFoundError("document", documentId);
|
||||
}
|
||||
|
||||
return await getProjectIdFromEnvironmentId(document.environmentId);
|
||||
};
|
||||
|
||||
export const getProjectIdFromIntegrationId = async (integrationId: string) => {
|
||||
const integration = await getIntegration(integrationId);
|
||||
if (!integration) {
|
||||
@@ -334,15 +296,6 @@ export const getEnvironmentIdFromResponseId = async (responseId: string) => {
|
||||
return await getEnvironmentIdFromSurveyId(response.surveyId);
|
||||
};
|
||||
|
||||
export const getEnvironmentIdFromInsightId = async (insightId: string) => {
|
||||
const insight = await getInsight(insightId);
|
||||
if (!insight) {
|
||||
throw new ResourceNotFoundError("insight", insightId);
|
||||
}
|
||||
|
||||
return insight.environmentId;
|
||||
};
|
||||
|
||||
export const getEnvironmentIdFromSegmentId = async (segmentId: string) => {
|
||||
const segment = await getSegment(segmentId);
|
||||
if (!segment) {
|
||||
|
||||
@@ -9,9 +9,7 @@ import {
|
||||
getActionClass,
|
||||
getApiKey,
|
||||
getContact,
|
||||
getDocument,
|
||||
getEnvironment,
|
||||
getInsight,
|
||||
getIntegration,
|
||||
getInvite,
|
||||
getLanguage,
|
||||
@@ -451,62 +449,6 @@ describe("Service Functions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInsight", () => {
|
||||
const insightId = "insight123";
|
||||
|
||||
test("returns the insight when found", async () => {
|
||||
const mockInsight = { environmentId: "env123" };
|
||||
vi.mocked(prisma.insight.findUnique).mockResolvedValue(mockInsight);
|
||||
|
||||
const result = await getInsight(insightId);
|
||||
expect(validateInputs).toHaveBeenCalled();
|
||||
expect(prisma.insight.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: insightId },
|
||||
select: { environmentId: true },
|
||||
});
|
||||
expect(result).toEqual(mockInsight);
|
||||
});
|
||||
|
||||
test("throws DatabaseError when database operation fails", async () => {
|
||||
vi.mocked(prisma.insight.findUnique).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Error", {
|
||||
code: "P2002",
|
||||
clientVersion: "4.7.0",
|
||||
})
|
||||
);
|
||||
|
||||
await expect(getInsight(insightId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDocument", () => {
|
||||
const documentId = "doc123";
|
||||
|
||||
test("returns the document when found", async () => {
|
||||
const mockDocument = { environmentId: "env123" };
|
||||
vi.mocked(prisma.document.findUnique).mockResolvedValue(mockDocument);
|
||||
|
||||
const result = await getDocument(documentId);
|
||||
expect(validateInputs).toHaveBeenCalled();
|
||||
expect(prisma.document.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: documentId },
|
||||
select: { environmentId: true },
|
||||
});
|
||||
expect(result).toEqual(mockDocument);
|
||||
});
|
||||
|
||||
test("throws DatabaseError when database operation fails", async () => {
|
||||
vi.mocked(prisma.document.findUnique).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Error", {
|
||||
code: "P2002",
|
||||
clientVersion: "4.7.0",
|
||||
})
|
||||
);
|
||||
|
||||
await expect(getDocument(documentId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isProjectPartOfOrganization", () => {
|
||||
const projectId = "proj123";
|
||||
const organizationId = "org123";
|
||||
|
||||
@@ -272,54 +272,6 @@ export const getTeam = reactCache(async (teamId: string): Promise<{ organization
|
||||
}
|
||||
});
|
||||
|
||||
export const getInsight = reactCache(async (insightId: string): Promise<{ environmentId: string } | null> => {
|
||||
validateInputs([insightId, ZId]);
|
||||
|
||||
try {
|
||||
const insight = await prisma.insight.findUnique({
|
||||
where: {
|
||||
id: insightId,
|
||||
},
|
||||
select: {
|
||||
environmentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return insight;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
export const getDocument = reactCache(
|
||||
async (documentId: string): Promise<{ environmentId: string } | null> => {
|
||||
validateInputs([documentId, ZId]);
|
||||
|
||||
try {
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
select: {
|
||||
environmentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return document;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const isProjectPartOfOrganization = async (
|
||||
organizationId: string,
|
||||
projectId: string
|
||||
|
||||
@@ -98,7 +98,6 @@ const survey: TSurvey = {
|
||||
singleUse: null,
|
||||
productOverwrites: null,
|
||||
pin: null,
|
||||
verifyEmail: null,
|
||||
attributeFilters: [],
|
||||
autoComplete: null,
|
||||
hiddenFields: { enabled: true },
|
||||
|
||||
@@ -34,7 +34,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
|
||||
environmentId: true,
|
||||
questions: true,
|
||||
endings: true,
|
||||
thankYouCard: true,
|
||||
hiddenFields: true,
|
||||
variables: true,
|
||||
displayOption: true,
|
||||
@@ -47,7 +46,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
|
||||
isVerifyEmailEnabled: true,
|
||||
isSingleResponsePerEmailEnabled: true,
|
||||
inlineTriggers: true,
|
||||
verifyEmail: true,
|
||||
displayPercentage: true,
|
||||
welcomeCard: true,
|
||||
surveyClosedMessage: true,
|
||||
@@ -58,7 +56,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
|
||||
.partial({
|
||||
redirectUrl: true,
|
||||
endings: true,
|
||||
thankYouCard: true,
|
||||
variables: true,
|
||||
recontactDays: true,
|
||||
displayLimit: true,
|
||||
@@ -69,7 +66,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
|
||||
projectOverwrites: true,
|
||||
showLanguageSwitch: true,
|
||||
inlineTriggers: true,
|
||||
verifyEmail: true,
|
||||
displayPercentage: true,
|
||||
})
|
||||
.openapi({
|
||||
|
||||
@@ -9,13 +9,11 @@ export const mockUser: TUser = {
|
||||
updatedAt: new Date("2024-01-01T00:00:00.000Z"),
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "google",
|
||||
objective: "improve_user_retention",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
role: "other",
|
||||
locale: "en-US",
|
||||
lastLoginAt: new Date("2024-01-01T00:00:00.000Z"),
|
||||
isActive: true,
|
||||
|
||||
@@ -56,10 +56,6 @@ describe("ResponseFeed", () => {
|
||||
headline: "",
|
||||
html: "",
|
||||
},
|
||||
verifyEmail: {
|
||||
name: "",
|
||||
subheading: "",
|
||||
},
|
||||
displayLimit: null,
|
||||
autoComplete: null,
|
||||
productOverwrites: null,
|
||||
@@ -69,11 +65,6 @@ describe("ResponseFeed", () => {
|
||||
hiddenFields: {},
|
||||
variables: [],
|
||||
followUps: [],
|
||||
thankYouCard: {
|
||||
enabled: false,
|
||||
headline: "",
|
||||
subheader: "",
|
||||
},
|
||||
delay: 0,
|
||||
displayPercentage: 100,
|
||||
surveyClosedMessage: "",
|
||||
|
||||
@@ -16,10 +16,8 @@ export const mockUser: TUser = {
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "google",
|
||||
locale: "en-US",
|
||||
role: "other",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
objective: "improve_user_retention",
|
||||
lastLoginAt: new Date(),
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
@@ -25,10 +25,8 @@ describe("updateUser", () => {
|
||||
email: "test@example.com",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
objective: null,
|
||||
locale: "en-US",
|
||||
lastLoginAt: new Date(),
|
||||
isActive: true,
|
||||
@@ -54,10 +52,8 @@ describe("updateUser", () => {
|
||||
emailVerified: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
twoFactorEnabled: true,
|
||||
identityProvider: true,
|
||||
objective: true,
|
||||
notificationSettings: true,
|
||||
locale: true,
|
||||
lastLoginAt: true,
|
||||
|
||||
@@ -19,10 +19,8 @@ export const updateUser = async (personId: string, data: TUserUpdateInput): Prom
|
||||
emailVerified: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
twoFactorEnabled: true,
|
||||
identityProvider: true,
|
||||
objective: true,
|
||||
notificationSettings: true,
|
||||
locale: true,
|
||||
lastLoginAt: true,
|
||||
|
||||
@@ -118,7 +118,6 @@ const mockSurvey = {
|
||||
status: "inProgress",
|
||||
questions: [mockQuestion],
|
||||
languages: [{ code: "en", default: true, enabled: true }],
|
||||
thankYouCard: { enabled: true },
|
||||
welcomeCard: { enabled: false },
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
@@ -135,7 +134,6 @@ const mockSurvey = {
|
||||
variables: [],
|
||||
productOverwrites: null,
|
||||
singleUse: null,
|
||||
verifyEmail: null,
|
||||
delay: 0,
|
||||
displayPercentage: null,
|
||||
inlineTriggers: null,
|
||||
|
||||
@@ -143,7 +143,6 @@ const baseSurvey = {
|
||||
productOverwrites: null,
|
||||
singleUse: null,
|
||||
surveyClosedMessage: null,
|
||||
verifyEmail: null,
|
||||
projectOverwrites: null,
|
||||
hiddenFields: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
@@ -263,8 +263,6 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Fix this, this happens because the survey type "web" is no longer in the zod types but its required in the schema for migration
|
||||
// @ts-expect-error
|
||||
const modifiedSurvey: TSurvey = {
|
||||
...prismaSurvey, // Properties from prismaSurvey
|
||||
displayPercentage: Number(prismaSurvey.displayPercentage) || null,
|
||||
|
||||
@@ -218,12 +218,6 @@ const createMockSurvey = (): TSurvey =>
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
thankYouCard: {
|
||||
enabled: true,
|
||||
title: { default: "Thank you" },
|
||||
buttonLabel: { default: "Close" },
|
||||
buttonLink: "",
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
id: "question1",
|
||||
|
||||
@@ -489,7 +489,6 @@ describe("validation.isSurveyValid", () => {
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayLimit: null,
|
||||
thankYouCard: { enabled: true, title: { default: "Thank you" } }, // Minimal for type check
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
segment: null,
|
||||
|
||||
@@ -51,7 +51,6 @@ const mockProjectPrisma = {
|
||||
segment: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUseId: null,
|
||||
verifyEmail: null,
|
||||
productOverwrites: null,
|
||||
brandColor: null,
|
||||
highlightBorderColor: null,
|
||||
|
||||
@@ -108,15 +108,8 @@ describe("data", () => {
|
||||
triggers: [],
|
||||
segment: null,
|
||||
followUps: [],
|
||||
thankYouCard: {
|
||||
enabled: false,
|
||||
headline: { default: "Thank you!" },
|
||||
subheader: { default: "" },
|
||||
buttonLabel: { default: "Close" },
|
||||
},
|
||||
inlineTriggers: [],
|
||||
segmentId: null,
|
||||
verifyEmail: null,
|
||||
};
|
||||
|
||||
const mockTransformedSurvey = {
|
||||
@@ -231,12 +224,6 @@ describe("data", () => {
|
||||
triggers: [],
|
||||
segment: null,
|
||||
followUps: [],
|
||||
thankYouCard: {
|
||||
enabled: false,
|
||||
headline: { default: "Thank you!" },
|
||||
subheader: { default: "" },
|
||||
buttonLabel: { default: "Close" },
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(prisma.survey.findUnique).mockResolvedValue(mockSurveyData as any);
|
||||
|
||||
@@ -140,12 +140,6 @@ describe("survey link utils", () => {
|
||||
html: { default: "" },
|
||||
buttonLabel: { default: "Start" },
|
||||
},
|
||||
thankYouCard: {
|
||||
enabled: true,
|
||||
headline: { default: "Thank You" },
|
||||
html: { default: "" },
|
||||
buttonLabel: { default: "Close" },
|
||||
},
|
||||
hiddenFields: {},
|
||||
languages: {
|
||||
default: "default",
|
||||
|
||||
@@ -119,8 +119,6 @@ export const PreviewSurvey = ({
|
||||
const darkOverlay = surveyDarkOverlay ?? project.darkOverlay;
|
||||
const clickOutsideClose = surveyClickOutsideClose ?? project.clickOutsideClose;
|
||||
|
||||
const widgetSetupCompleted = appSetupCompleted;
|
||||
|
||||
const styling: TSurveyStyling | TProjectStyling = useMemo(() => {
|
||||
// allow style overwrite is disabled from the project
|
||||
if (!project.styling.allowStyleOverwrite) {
|
||||
@@ -211,7 +209,7 @@ export const PreviewSurvey = ({
|
||||
};
|
||||
|
||||
if (!previewType) {
|
||||
previewType = widgetSetupCompleted ? "modal" : "fullwidth";
|
||||
previewType = appSetupCompleted ? "modal" : "fullwidth";
|
||||
|
||||
if (!questionId) {
|
||||
return <></>;
|
||||
|
||||
@@ -101,9 +101,6 @@ describe("QuestionToggleTable", () => {
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
},
|
||||
thankYouCard: {
|
||||
enabled: false,
|
||||
},
|
||||
displayProgress: false,
|
||||
progressBar: {
|
||||
display: false,
|
||||
|
||||
@@ -235,10 +235,6 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
await page.locator("#questionCard-12").getByRole("button", { name: "Next" }).click();
|
||||
// loading spinner -> wait for it to disappear
|
||||
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });
|
||||
|
||||
// Thank You Card
|
||||
await expect(page.getByText(surveys.createAndSubmit.thankYouCard.headline)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.thankYouCard.description)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -611,18 +607,16 @@ test.describe("Multi Language Survey Create", async () => {
|
||||
// Fill Thank you card in german
|
||||
await page.getByText("Ending card").first().click();
|
||||
await page.getByPlaceholder("Your question here. Recall").click();
|
||||
await page
|
||||
.getByPlaceholder("Your question here. Recall")
|
||||
.fill(surveys.germanCreate.thankYouCard.headline);
|
||||
await page.getByPlaceholder("Your question here. Recall").fill(surveys.germanCreate.endingCard.headline);
|
||||
await page.getByPlaceholder("Your description here. Recall").click();
|
||||
await page
|
||||
.getByPlaceholder("Your description here. Recall")
|
||||
.fill(surveys.germanCreate.thankYouCard.description);
|
||||
.fill(surveys.germanCreate.endingCard.description);
|
||||
|
||||
await page.locator("#showButton").check();
|
||||
|
||||
await page.getByPlaceholder("Create your own Survey").click();
|
||||
await page.getByPlaceholder("Create your own Survey").fill(surveys.germanCreate.thankYouCard.buttonLabel);
|
||||
await page.getByPlaceholder("Create your own Survey").fill(surveys.germanCreate.endingCard.buttonLabel);
|
||||
|
||||
// TODO: @pandeymangg - figure out if this is required
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
@@ -892,8 +886,8 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });
|
||||
|
||||
// Thank You Card
|
||||
await expect(page.getByText(surveys.createWithLogicAndSubmit.thankYouCard.headline)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createWithLogicAndSubmit.thankYouCard.description)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createWithLogicAndSubmit.endingCard.headline)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createWithLogicAndSubmit.endingCard.description)).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Verify Survey Response", async () => {
|
||||
|
||||
@@ -354,15 +354,6 @@ export const createSurvey = async (page: Page, params: CreateSurveyParams) => {
|
||||
await page.getByRole("button", { name: "Add option" }).click();
|
||||
await page.getByPlaceholder("Option 5").click();
|
||||
await page.getByPlaceholder("Option 5").fill(params.ranking.choices[4]);
|
||||
|
||||
// Thank You Card
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Thank you!Ending card$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByLabel("Note*").fill(params.thankYouCard.headline);
|
||||
await page.locator('input[name="subheader"]').fill(params.thankYouCard.description);
|
||||
};
|
||||
|
||||
export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWithLogicParams) => {
|
||||
@@ -1012,13 +1003,4 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
await page.getByRole("option", { name: "Multiply *" }).click();
|
||||
await page.locator("#action-2-value-input").click();
|
||||
await page.locator("#action-2-value-input").fill("2");
|
||||
|
||||
// Thank You Card
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Thank you!Ending card$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByLabel("Note*").fill(params.thankYouCard.headline);
|
||||
await page.locator('input[name="subheader"]').fill(params.thankYouCard.description);
|
||||
};
|
||||
|
||||
@@ -174,10 +174,6 @@ export const surveys = {
|
||||
question: "What is most important for you in life?",
|
||||
choices: ["Work", "Money", "Travel", "Family", "Friends"],
|
||||
},
|
||||
thankYouCard: {
|
||||
headline: "This is my Thank You Card Headline!",
|
||||
description: "This is my Thank you Card Description!",
|
||||
},
|
||||
},
|
||||
createWithLogicAndSubmit: {
|
||||
welcomeCard: {
|
||||
@@ -249,9 +245,9 @@ export const surveys = {
|
||||
question: "This is my Ranking Question",
|
||||
choices: ["Work", "Money", "Travel", "Family", "Friends"],
|
||||
},
|
||||
thankYouCard: {
|
||||
headline: "This is my Thank You Card Headline!",
|
||||
description: "This is my Thank you Card Description!",
|
||||
endingCard: {
|
||||
headline: "Thank you!",
|
||||
description: "We appreciate your feedback.",
|
||||
},
|
||||
},
|
||||
germanCreate: {
|
||||
@@ -326,15 +322,15 @@ export const surveys = {
|
||||
country: "Adresse",
|
||||
},
|
||||
},
|
||||
ranking: {
|
||||
question: "Was ist für Sie im Leben am wichtigsten?",
|
||||
choices: ["Arbeit", "Geld", "Reisen", "Familie", "Freunde"],
|
||||
},
|
||||
thankYouCard: {
|
||||
endingCard: {
|
||||
headline: "Dies ist meine Dankeskarte Überschrift!", // German translation
|
||||
description: "Dies ist meine Beschreibung zur Dankeskarte!", // German translation
|
||||
buttonLabel: "Erstellen Sie Ihre eigene Umfrage",
|
||||
},
|
||||
ranking: {
|
||||
question: "Was ist für Sie im Leben am wichtigsten?",
|
||||
choices: ["Arbeit", "Geld", "Reisen", "Familie", "Freunde"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4067,6 +4067,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"appSetupCompleted": true,
|
||||
"createdAt": "2024-04-09T04:53:29.577Z",
|
||||
"id": "clurwouax000azffxt7n5unn3",
|
||||
"product": {
|
||||
@@ -4074,8 +4075,7 @@
|
||||
"name": "My Product"
|
||||
},
|
||||
"type": "production",
|
||||
"updatedAt": "2024-04-09T14:14:49.256Z",
|
||||
"widgetSetupCompleted": true
|
||||
"updatedAt": "2024-04-09T14:14:49.256Z"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object"
|
||||
|
||||
@@ -3958,19 +3958,6 @@ components:
|
||||
- type
|
||||
default: []
|
||||
description: The endings of the survey
|
||||
thankYouCard:
|
||||
type:
|
||||
- object
|
||||
- "null"
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- enabled
|
||||
- message
|
||||
description: The thank you card of the survey (deprecated)
|
||||
hiddenFields:
|
||||
type: object
|
||||
properties:
|
||||
@@ -4302,17 +4289,6 @@ components:
|
||||
isBackButtonHidden:
|
||||
type: boolean
|
||||
description: Whether the back button is hidden
|
||||
verifyEmail:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- enabled
|
||||
- message
|
||||
description: Email verification configuration (deprecated)
|
||||
recaptcha:
|
||||
type:
|
||||
- object
|
||||
|
||||
@@ -161,10 +161,7 @@ Formbricks stores all data in PostgreSQL tables. Here's a comprehensive list of
|
||||
| ContactAttributeKey | Defines available attribute types for contacts |
|
||||
| DataMigration | Tracks the status of database schema migrations |
|
||||
| Display | Records when and to whom surveys were shown |
|
||||
| Document | Stores processed survey responses for analysis |
|
||||
| DocumentInsight | Links analyzed documents to derived insights |
|
||||
| Environment | Manages production/development environments within projects |
|
||||
| Insight | Contains analyzed patterns and information from responses |
|
||||
| Integration | Stores configuration for third-party service integrations |
|
||||
| Invite | Manages pending invitations to join organizations |
|
||||
| Language | Defines supported languages for multi-lingual surveys |
|
||||
|
||||
241
packages/database/README.md
Normal file
241
packages/database/README.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# @formbricks/database
|
||||
|
||||
The database package for the Formbricks monorepo, providing centralized database schema management, migration handling, and type definitions for the entire platform.
|
||||
|
||||
## Overview
|
||||
|
||||
This package serves as the central database layer for Formbricks, containing:
|
||||
|
||||
- **Prisma Schema**: Complete database schema definition with PostgreSQL support
|
||||
- **Migration System**: Custom migration management for both schema and data migrations
|
||||
- **Type Definitions**: Zod schemas and TypeScript types for database models
|
||||
- **Database Client**: Configured Prisma client with extensions and generators
|
||||
- **Migration Scripts**: Automated tools for creating and applying migrations
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
packages/database/
|
||||
├── src/
|
||||
│ ├── client.ts # Prisma client configuration
|
||||
│ ├── index.ts # Package exports
|
||||
│ └── scripts/ # Migration management scripts
|
||||
│ ├── apply-migrations.ts
|
||||
│ ├── create-migration.ts
|
||||
│ ├── generate-data-migration.ts
|
||||
│ ├── migration-runner.ts
|
||||
│ └── create-saml-database.ts
|
||||
├── migration/ # Custom migrations directory
|
||||
│ ├── [timestamp_name]/ # Schema migration folder
|
||||
│ │ └── migration.sql # Schema migration file
|
||||
│ └── [timestamp_name]/ # Data migration folder
|
||||
│ └── migration.ts # Data migration file
|
||||
├── migrations/ # Prisma internal migrations
|
||||
├── types/ # Custom TypeScript types
|
||||
├── zod/ # Zod schema definitions
|
||||
├── schema.prisma # Main Prisma schema file
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Migration System
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Custom Migrations Directory**: Schema and data migrations are managed in the `packages/database/migration` directory
|
||||
- **Separation of Concerns**: Each migration is classified as either:
|
||||
- **Schema Migration**: Contains a `migration.sql` file
|
||||
- **Data Migration**: Contains a `migration.ts` file
|
||||
- **Single Type per Subdirectory**: A migration subdirectory can only contain one file type—either `migration.sql` or `migration.ts`
|
||||
- **Custom Naming Convention**: Subdirectories follow the format `timestamp_name_of_the_migration` (e.g., `20241214112456_add_users_table`)
|
||||
- **Order of Execution**: Migrations are executed sequentially based on their timestamps, enabling precise control over the execution sequence
|
||||
|
||||
### Database Tracking
|
||||
|
||||
- **Schema Migrations**: Continue to be tracked by Prisma in the `_prisma_migrations` table
|
||||
- **Data Migrations**: Are tracked in the new `DataMigration` table to avoid reapplying already executed migrations
|
||||
|
||||
### Directory Structure Example
|
||||
|
||||
```
|
||||
packages/database/migration/
|
||||
├── 20241214112456_xm_user_identification/
|
||||
│ └── migration.sql
|
||||
├── 20241214113000_xm_user_identification/
|
||||
│ └── migration.ts
|
||||
└── 20241215120000_add_new_feature/
|
||||
└── migration.sql
|
||||
```
|
||||
|
||||
Each subdirectory under `packages/database/migration` represents a single migration and must:
|
||||
|
||||
- Have a **14-digit UTC timestamp** followed by an underscore and the migration name (similar to Prisma)
|
||||
- Contain only one file, either `migration.sql` (for schema migrations) or `migration.ts` (for data migrations)
|
||||
|
||||
## Scripts and Commands
|
||||
|
||||
### Root Level Commands
|
||||
|
||||
Run these commands from the root directory of the Formbricks monorepo:
|
||||
|
||||
- **`pnpm fb-migrate-dev`**: Create and apply schema migrations
|
||||
- Prompts for migration name
|
||||
- Generates new `migration.sql` in the custom directory
|
||||
- Copies migration to Prisma's internal directory
|
||||
- Applies all pending migrations to the database
|
||||
|
||||
### Package Level Commands
|
||||
|
||||
Run these commands from the `packages/database` directory:
|
||||
|
||||
- **`pnpm generate-data-migration`**: Create data migrations
|
||||
- Prompts for data migration name
|
||||
- Creates new subdirectory with appropriate timestamp
|
||||
- Generates `migration.ts` file with pre-configured ID and name
|
||||
- **Note**: Only use Prisma raw queries in data migrations for better performance and to avoid type errors
|
||||
|
||||
### Available Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"build": "pnpm generate && vite build",
|
||||
"create-migration": "Create new schema migration",
|
||||
"db:migrate:deploy": "Apply migrations in production",
|
||||
"db:migrate:dev": "Apply migrations in development",
|
||||
"db:push": "prisma db push --accept-data-loss",
|
||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
||||
"dev": "vite build --watch",
|
||||
"generate": "prisma generate",
|
||||
"generate-data-migration": "Create new data migration"
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
### Adding a Schema Migration
|
||||
|
||||
1. Modify your Prisma schema in `schema.prisma`
|
||||
2. Run `pnpm fb-migrate-dev` from the root of the monorepo
|
||||
3. Follow the prompts to name your migration
|
||||
4. The script automatically:
|
||||
- Generates the new `migration.sql` file in the custom directory
|
||||
- Copies the file to Prisma's internal directory
|
||||
- Applies the migration to the database
|
||||
|
||||
### Adding a Data Migration
|
||||
|
||||
1. Navigate to the `packages/database` directory
|
||||
2. Run `pnpm generate-data-migration`
|
||||
3. Follow the prompts to name your migration
|
||||
4. Implement the required data changes in the generated `migration.ts` file
|
||||
5. Use only Prisma raw queries for optimal performance
|
||||
|
||||
### Example Data Migration Structure
|
||||
|
||||
```typescript
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import type { MigrationScript } from "../../src/scripts/migration-runner";
|
||||
|
||||
export const myDataMigration: MigrationScript = {
|
||||
type: "data",
|
||||
id: "unique_migration_id",
|
||||
name: "20241214113000_my_data_migration",
|
||||
run: async ({ tx }) => {
|
||||
// Use raw SQL queries for data transformations
|
||||
const result = await tx.$queryRaw`
|
||||
UPDATE "MyTable" SET "newField" = 'defaultValue' WHERE "newField" IS NULL
|
||||
`;
|
||||
|
||||
logger.info(`Updated ${result} records`);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
The package uses PostgreSQL with the following key features:
|
||||
|
||||
- **Extensions**: pgvector for vector operations
|
||||
- **Generators**:
|
||||
- Prisma Client with PostgreSQL extensions
|
||||
- JSON types generator for enhanced type safety
|
||||
- **Models**: Comprehensive schema covering users, organizations, surveys, responses, webhooks, and more
|
||||
|
||||
### Key Models
|
||||
|
||||
- **User**: User accounts with authentication and profile data
|
||||
- **Organization**: Multi-tenant organization structure
|
||||
- **Project**: Project-level configuration and settings
|
||||
- **Survey**: Survey definitions with advanced targeting and styling
|
||||
- **Response**: Survey response data with metadata
|
||||
- **Contact**: Contact management and attributes
|
||||
- **Webhook**: Event-driven integrations
|
||||
- **ApiKey**: API authentication and access control
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### Zod Schemas
|
||||
|
||||
Located in the `zod/` directory, providing runtime validation for:
|
||||
|
||||
- API keys
|
||||
- Contacts and contact attributes
|
||||
- Organizations and teams
|
||||
- Surveys and responses
|
||||
- Webhooks and integrations
|
||||
- User management
|
||||
|
||||
### TypeScript Types
|
||||
|
||||
Custom types in the `types/` directory for:
|
||||
|
||||
- Error handling
|
||||
- Survey follow-up logic
|
||||
- Complex data structures
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- PostgreSQL database
|
||||
- Redis (for caching)
|
||||
- Node.js and pnpm
|
||||
|
||||
### Setup
|
||||
|
||||
1. Ensure database is running: `pnpm db:up` (from root)
|
||||
2. Install dependencies: `pnpm install`
|
||||
3. Generate Prisma client: `pnpm generate`
|
||||
4. Apply migrations: `pnpm db:setup`
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Development build with watch
|
||||
pnpm dev
|
||||
|
||||
# Production build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Key Benefits
|
||||
|
||||
- **Unified Management**: Schema and data migrations are managed together in a single directory
|
||||
- **Controlled Execution**: Timestamp-based sorting ensures migrations run in the desired sequence
|
||||
- **Automation**: Simplifies the process of creating, copying, and applying migrations with custom scripts
|
||||
- **Tracking**: Separate tracking for schema and data migrations prevents duplicate executions
|
||||
- **Type Safety**: Comprehensive Zod schemas and TypeScript types for all database operations
|
||||
- **Performance**: Optimized queries and proper indexing for production workloads
|
||||
|
||||
## Contributing
|
||||
|
||||
When making changes to the database schema:
|
||||
|
||||
1. Always create migrations for schema changes
|
||||
2. Use data migrations for data transformations
|
||||
3. Follow the naming conventions for migration directories
|
||||
4. Test migrations thoroughly in development before applying to production
|
||||
5. Document any breaking changes or special considerations
|
||||
|
||||
For more information about the Formbricks project structure, see the main repository README.
|
||||
@@ -12,7 +12,7 @@ CREATE TABLE "SurveyQuota" (
|
||||
"surveyId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"limit" INTEGER NOT NULL,
|
||||
"logic" JSONB NOT NULL,
|
||||
"logic" JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
"action" "SurveyQuotaAction" NOT NULL,
|
||||
"endingCardId" TEXT,
|
||||
"countPartialSubmissions" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The values [web,website] on the enum `SurveyType` will be removed. If these variants are still used in the database, this will fail.
|
||||
- You are about to drop the column `responseId` on the `Display` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `status` on the `Display` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `widgetSetupCompleted` on the `Environment` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `deprecatedRole` on the `Invite` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `deprecatedRole` on the `Membership` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `brandColor` on the `Project` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `highlightBorderColor` on the `Project` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `thankYouCard` on the `Survey` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `verifyEmail` on the `Survey` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `objective` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `role` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the `Document` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `DocumentInsight` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Insight` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- AlterEnum
|
||||
BEGIN;
|
||||
CREATE TYPE "public"."SurveyType_new" AS ENUM ('link', 'app');
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "type" DROP DEFAULT;
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "type" TYPE "public"."SurveyType_new" USING ("type"::text::"public"."SurveyType_new");
|
||||
ALTER TYPE "public"."SurveyType" RENAME TO "SurveyType_old";
|
||||
ALTER TYPE "public"."SurveyType_new" RENAME TO "SurveyType";
|
||||
DROP TYPE "public"."SurveyType_old";
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "type" SET DEFAULT 'app';
|
||||
COMMIT;
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_environmentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_responseId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_surveyId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_documentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_insightId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."Insight" DROP CONSTRAINT "Insight_environmentId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Display_responseId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Display" DROP COLUMN "responseId",
|
||||
DROP COLUMN "status";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Environment" DROP COLUMN "widgetSetupCompleted";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Invite" DROP COLUMN "deprecatedRole";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Membership" DROP COLUMN "deprecatedRole";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Project" DROP COLUMN "brandColor",
|
||||
DROP COLUMN "highlightBorderColor";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Survey" DROP COLUMN "thankYouCard",
|
||||
DROP COLUMN "verifyEmail",
|
||||
ALTER COLUMN "type" SET DEFAULT 'app';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."User" DROP COLUMN "objective",
|
||||
DROP COLUMN "role";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "public"."Document";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "public"."DocumentInsight";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "public"."Insight";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."DisplayStatus";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."InsightCategory";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."Intention";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."MembershipRole";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."Objective";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."Role";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."Sentiment";
|
||||
@@ -165,7 +165,6 @@ model Response {
|
||||
// singleUseId, used to prevent multiple responses
|
||||
singleUseId String?
|
||||
language String?
|
||||
documents Document[]
|
||||
displayId String? @unique
|
||||
display Display? @relation(fields: [displayId], references: [id])
|
||||
|
||||
@@ -218,30 +217,22 @@ enum SurveyStatus {
|
||||
completed
|
||||
}
|
||||
|
||||
enum DisplayStatus {
|
||||
seen
|
||||
responded
|
||||
}
|
||||
|
||||
/// Records when a survey is shown to a user.
|
||||
/// Tracks survey display history and response status.
|
||||
///
|
||||
/// @property id - Unique identifier for the display event
|
||||
/// @property survey - The survey that was displayed
|
||||
/// @property contact - The contact who saw the survey
|
||||
/// @property status - Whether the survey was just seen or responded to
|
||||
/// @property response - The associated response if one exists
|
||||
model Display {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
|
||||
surveyId String
|
||||
contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
||||
contactId String?
|
||||
responseId String? @unique //deprecated
|
||||
status DisplayStatus?
|
||||
response Response?
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
|
||||
surveyId String
|
||||
contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
||||
contactId String?
|
||||
response Response?
|
||||
|
||||
@@index([surveyId])
|
||||
@@index([contactId, createdAt])
|
||||
@@ -311,8 +302,6 @@ model SurveyAttributeFilter {
|
||||
|
||||
enum SurveyType {
|
||||
link
|
||||
web
|
||||
website
|
||||
app
|
||||
}
|
||||
|
||||
@@ -341,7 +330,7 @@ model Survey {
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
redirectUrl String?
|
||||
type SurveyType @default(web)
|
||||
type SurveyType @default(app)
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
environmentId String
|
||||
creator User? @relation(fields: [createdBy], references: [id])
|
||||
@@ -353,7 +342,6 @@ model Survey {
|
||||
questions Json @default("[]")
|
||||
/// [SurveyEnding]
|
||||
endings Json[] @default([])
|
||||
thankYouCard Json? //deprecated
|
||||
/// [SurveyHiddenFields]
|
||||
hiddenFields Json @default("{\"enabled\": false}")
|
||||
/// [SurveyVariables]
|
||||
@@ -385,8 +373,6 @@ model Survey {
|
||||
/// [SurveySingleUse]
|
||||
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
|
||||
|
||||
/// [SurveyVerifyEmail]
|
||||
verifyEmail Json? // deprecated
|
||||
isVerifyEmailEnabled Boolean @default(false)
|
||||
isSingleResponsePerEmailEnabled Boolean @default(false)
|
||||
isBackButtonHidden Boolean @default(false)
|
||||
@@ -394,7 +380,6 @@ model Survey {
|
||||
displayPercentage Decimal?
|
||||
languages SurveyLanguage[]
|
||||
showLanguageSwitch Boolean?
|
||||
documents Document[]
|
||||
followUps SurveyFollowUp[]
|
||||
/// [SurveyRecaptcha]
|
||||
recaptcha Json? @default("{\"enabled\": false, \"threshold\":0.1}")
|
||||
@@ -565,31 +550,27 @@ model DataMigration {
|
||||
/// @property id - Unique identifier for the environment
|
||||
/// @property type - Either 'production' or 'development'
|
||||
/// @property project - Reference to parent project
|
||||
/// @property widgetSetupCompleted - Tracks initial widget setup status
|
||||
/// @property surveys - Collection of surveys in this environment
|
||||
/// @property contacts - Collection of contacts/users tracked
|
||||
/// @property actionClasses - Defined actions that can trigger surveys
|
||||
/// @property attributeKeys - Custom attributes configuration
|
||||
model Environment {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
type EnvironmentType
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
projectId String
|
||||
widgetSetupCompleted Boolean @default(false)
|
||||
appSetupCompleted Boolean @default(false)
|
||||
surveys Survey[]
|
||||
contacts Contact[]
|
||||
actionClasses ActionClass[]
|
||||
attributeKeys ContactAttributeKey[]
|
||||
webhooks Webhook[]
|
||||
tags Tag[]
|
||||
segments Segment[]
|
||||
integration Integration[]
|
||||
documents Document[]
|
||||
insights Insight[]
|
||||
ApiKeyEnvironment ApiKeyEnvironment[]
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
type EnvironmentType
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
projectId String
|
||||
appSetupCompleted Boolean @default(false)
|
||||
surveys Survey[]
|
||||
contacts Contact[]
|
||||
actionClasses ActionClass[]
|
||||
attributeKeys ContactAttributeKey[]
|
||||
webhooks Webhook[]
|
||||
tags Tag[]
|
||||
segments Segment[]
|
||||
integration Integration[]
|
||||
ApiKeyEnvironment ApiKeyEnvironment[]
|
||||
|
||||
@@index([projectId])
|
||||
}
|
||||
@@ -614,29 +595,27 @@ enum WidgetPlacement {
|
||||
/// @property recontactDays - Default recontact delay for surveys
|
||||
/// @property placement - Default widget placement for in-app surveys
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organizationId String
|
||||
environments Environment[]
|
||||
brandColor String? // deprecated; use styling.brandColor instead
|
||||
highlightBorderColor String? // deprecated
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organizationId String
|
||||
environments Environment[]
|
||||
/// [Styling]
|
||||
styling Json @default("{\"allowStyleOverwrite\":true}")
|
||||
styling Json @default("{\"allowStyleOverwrite\":true}")
|
||||
/// [ProjectConfig]
|
||||
config Json @default("{}")
|
||||
recontactDays Int @default(7)
|
||||
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
|
||||
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys
|
||||
placement WidgetPlacement @default(bottomRight)
|
||||
clickOutsideClose Boolean @default(true)
|
||||
darkOverlay Boolean @default(false)
|
||||
languages Language[]
|
||||
config Json @default("{}")
|
||||
recontactDays Int @default(7)
|
||||
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
|
||||
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys
|
||||
placement WidgetPlacement @default(bottomRight)
|
||||
clickOutsideClose Boolean @default(true)
|
||||
darkOverlay Boolean @default(false)
|
||||
languages Language[]
|
||||
/// [Logo]
|
||||
logo Json?
|
||||
projectTeams ProjectTeam[]
|
||||
logo Json?
|
||||
projectTeams ProjectTeam[]
|
||||
|
||||
@@unique([organizationId, name])
|
||||
@@index([organizationId])
|
||||
@@ -677,14 +656,6 @@ enum OrganizationRole {
|
||||
billing
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
owner
|
||||
admin
|
||||
editor
|
||||
developer
|
||||
viewer
|
||||
}
|
||||
|
||||
/// Links users to organizations with specific roles.
|
||||
/// Manages organization membership and permissions.
|
||||
/// Core model for managing user access within organizations.
|
||||
@@ -699,7 +670,6 @@ model Membership {
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
accepted Boolean @default(false)
|
||||
deprecatedRole MembershipRole? //deprecated
|
||||
role OrganizationRole @default(member)
|
||||
|
||||
@@id([userId, organizationId])
|
||||
@@ -732,7 +702,6 @@ model Invite {
|
||||
acceptorId String?
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
deprecatedRole MembershipRole? //deprecated
|
||||
role OrganizationRole @default(member)
|
||||
teamIds String[] @default([])
|
||||
|
||||
@@ -835,39 +804,12 @@ model Account {
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
enum Role {
|
||||
project_manager
|
||||
engineer
|
||||
founder
|
||||
marketing_specialist
|
||||
other
|
||||
}
|
||||
|
||||
enum Objective {
|
||||
increase_conversion
|
||||
improve_user_retention
|
||||
increase_user_adoption
|
||||
sharpen_marketing_messaging
|
||||
support_sales
|
||||
other
|
||||
}
|
||||
|
||||
enum Intention {
|
||||
survey_user_segments
|
||||
survey_at_specific_point_in_user_journey
|
||||
enrich_customer_profiles
|
||||
collect_all_user_feedback_on_one_platform
|
||||
other
|
||||
}
|
||||
|
||||
/// Represents a user in the Formbricks system.
|
||||
/// Central model for user authentication and profile management.
|
||||
///
|
||||
/// @property id - Unique identifier for the user
|
||||
/// @property name - Display name of the user
|
||||
/// @property email - User's email address
|
||||
/// @property role - User's professional role
|
||||
/// @property objective - User's main goal with Formbricks
|
||||
/// @property twoFactorEnabled - Whether 2FA is active
|
||||
/// @property memberships - Organizations the user belongs to
|
||||
/// @property notificationSettings - User's notification preferences
|
||||
@@ -890,8 +832,6 @@ model User {
|
||||
groupId String?
|
||||
invitesCreated Invite[] @relation("inviteCreatedBy")
|
||||
invitesAccepted Invite[] @relation("inviteAcceptedBy")
|
||||
role Role?
|
||||
objective Objective?
|
||||
/// [UserNotificationSettings]
|
||||
notificationSettings Json @default("{}")
|
||||
/// [Locale]
|
||||
@@ -969,85 +909,6 @@ model SurveyLanguage {
|
||||
@@index([languageId])
|
||||
}
|
||||
|
||||
enum InsightCategory {
|
||||
featureRequest
|
||||
complaint
|
||||
praise
|
||||
other
|
||||
}
|
||||
|
||||
/// Stores analyzed insights from survey responses.
|
||||
/// Used for tracking patterns and extracting meaningful information.
|
||||
///
|
||||
/// @property id - Unique identifier for the insight
|
||||
/// @property category - Type of insight (feature request, complaint, etc.)
|
||||
/// @property title - Summary of the insight
|
||||
/// @property description - Detailed explanation
|
||||
/// @property vector - Embedding vector for similarity search
|
||||
model Insight {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
environmentId String
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
category InsightCategory
|
||||
title String
|
||||
description String
|
||||
vector Unsupported("vector(512)")?
|
||||
documentInsights DocumentInsight[]
|
||||
}
|
||||
|
||||
/// Links insights to source documents.
|
||||
/// Enables tracing insights back to original responses.
|
||||
///
|
||||
/// @property document - The source document
|
||||
/// @property insight - The derived insight
|
||||
model DocumentInsight {
|
||||
documentId String
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
insightId String
|
||||
insight Insight @relation(fields: [insightId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([documentId, insightId])
|
||||
@@index([insightId])
|
||||
}
|
||||
|
||||
enum Sentiment {
|
||||
positive
|
||||
negative
|
||||
neutral
|
||||
}
|
||||
|
||||
/// Represents a processed text document from survey responses.
|
||||
/// Used for analysis and insight generation.
|
||||
///
|
||||
/// @property id - Unique identifier for the document
|
||||
/// @property survey - The associated survey
|
||||
/// @property response - The source response
|
||||
/// @property sentiment - Analyzed sentiment (positive, negative, neutral)
|
||||
/// @property text - The document content
|
||||
/// @property vector - Embedding vector for similarity search
|
||||
model Document {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
environmentId String
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
surveyId String?
|
||||
survey Survey? @relation(fields: [surveyId], references: [id], onDelete: Cascade)
|
||||
responseId String?
|
||||
response Response? @relation(fields: [responseId], references: [id], onDelete: Cascade)
|
||||
questionId String?
|
||||
sentiment Sentiment
|
||||
isSpam Boolean
|
||||
text String
|
||||
vector Unsupported("vector(512)")?
|
||||
documentInsights DocumentInsight[]
|
||||
|
||||
@@unique([responseId, questionId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
/// Represents a team within an organization.
|
||||
/// Enables group-based access control and collaboration.
|
||||
///
|
||||
|
||||
@@ -99,15 +99,6 @@ const ZSurveyBase = z.object({
|
||||
endings: z.array(ZSurveyEnding).default([]).openapi({
|
||||
description: "The endings of the survey",
|
||||
}),
|
||||
thankYouCard: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
message: z.string(),
|
||||
})
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "The thank you card of the survey (deprecated)",
|
||||
}),
|
||||
hiddenFields: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
@@ -202,14 +193,6 @@ const ZSurveyBase = z.object({
|
||||
isBackButtonHidden: z.boolean().openapi({
|
||||
description: "Whether the back button is hidden",
|
||||
}),
|
||||
verifyEmail: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
message: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
description: "Email verification configuration (deprecated)",
|
||||
}),
|
||||
recaptcha: ZSurveyRecaptcha.openapi({
|
||||
description: "Google reCAPTCHA configuration",
|
||||
}),
|
||||
|
||||
@@ -210,8 +210,6 @@ const mockSurvey = {
|
||||
isSingleUse: false, // Explicitly set isSingleUse
|
||||
recaptcha: { enabled: false },
|
||||
autoClose: null,
|
||||
thankYouCard: null,
|
||||
verifyEmail: null,
|
||||
triggers: [],
|
||||
redirectUrl: "",
|
||||
surveyClosedMessage: { default: "" },
|
||||
|
||||
@@ -6,7 +6,6 @@ export const ZDisplay = z.object({
|
||||
updatedAt: z.date(),
|
||||
contactId: z.string().cuid().nullable(),
|
||||
surveyId: z.string().cuid(),
|
||||
status: z.enum(["seen", "responded"]).nullable(),
|
||||
});
|
||||
|
||||
export type TDisplay = z.infer<typeof ZDisplay>;
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ZSurveyStyling,
|
||||
ZSurveyWelcomeCard,
|
||||
} from "./surveys/types";
|
||||
import { ZUserObjective } from "./user";
|
||||
|
||||
export const ZTemplateRole = z.enum([
|
||||
"productManager",
|
||||
@@ -25,7 +24,6 @@ export const ZTemplate = z.object({
|
||||
role: ZTemplateRole.optional(),
|
||||
channels: z.array(z.enum(["link", "app", "website"])).optional(),
|
||||
industries: z.array(z.enum(["eCommerce", "saas", "other"])).optional(),
|
||||
objectives: z.array(ZUserObjective).optional(),
|
||||
preset: z.object({
|
||||
name: z.string(),
|
||||
welcomeCard: ZSurveyWelcomeCard,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const ZRole = z.enum(["project_manager", "engineer", "founder", "marketing_specialist", "other"]);
|
||||
|
||||
export const ZUserLocale = z.enum([
|
||||
"en-US",
|
||||
"de-DE",
|
||||
@@ -15,16 +13,6 @@ export const ZUserLocale = z.enum([
|
||||
]);
|
||||
|
||||
export type TUserLocale = z.infer<typeof ZUserLocale>;
|
||||
export const ZUserObjective = z.enum([
|
||||
"increase_conversion",
|
||||
"improve_user_retention",
|
||||
"increase_user_adoption",
|
||||
"sharpen_marketing_messaging",
|
||||
"support_sales",
|
||||
"other",
|
||||
]);
|
||||
|
||||
export type TUserObjective = z.infer<typeof ZUserObjective>;
|
||||
|
||||
export const ZUserNotificationSettings = z.object({
|
||||
alert: z.record(z.boolean()),
|
||||
@@ -62,8 +50,6 @@ export const ZUser = z.object({
|
||||
identityProvider: ZUserIdentityProvider,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
role: ZRole.nullable(),
|
||||
objective: ZUserObjective.nullable(),
|
||||
notificationSettings: ZUserNotificationSettings,
|
||||
locale: ZUserLocale,
|
||||
lastLoginAt: z.date().nullable(),
|
||||
@@ -77,8 +63,6 @@ export const ZUserUpdateInput = z.object({
|
||||
email: ZUserEmail.optional(),
|
||||
emailVerified: z.date().nullish(),
|
||||
password: ZUserPassword.optional(),
|
||||
role: ZRole.optional(),
|
||||
objective: ZUserObjective.nullish(),
|
||||
notificationSettings: ZUserNotificationSettings.optional(),
|
||||
locale: ZUserLocale.optional(),
|
||||
lastLoginAt: z.date().nullish(),
|
||||
@@ -92,8 +76,6 @@ export const ZUserCreateInput = z.object({
|
||||
email: ZUserEmail,
|
||||
password: ZUserPassword.optional(),
|
||||
emailVerified: z.date().optional(),
|
||||
role: ZRole.optional(),
|
||||
objective: ZUserObjective.nullish(),
|
||||
identityProvider: ZUserIdentityProvider.optional(),
|
||||
identityProviderAccountId: z.string().optional(),
|
||||
locale: ZUserLocale.optional(),
|
||||
|
||||
Reference in New Issue
Block a user