diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx
index 05b9587ed5..1856d8e6a2 100644
--- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx
+++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx
@@ -4,7 +4,7 @@ import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments
import { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
-import { createSurveyAction } from "@/modules/surveys/components/TemplateList/actions";
+import { createSurveyAction } from "@/modules/survey/components/template-list/actions";
import { useTranslate } from "@tolgee/react";
import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react";
import { useRouter } from "next/navigation";
diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/layout.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/layout.tsx
index 151922dac0..3abbc14d63 100644
--- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/layout.tsx
+++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/layout.tsx
@@ -28,7 +28,7 @@ const OnboardingLayout = async (props) => {
throw new Error(t("common.organization_not_found"));
}
- const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
+ const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.billing.limits);
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
if (organizationProjectsCount >= organizationProjectsLimit) {
diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx
index a8b93a7fca..9c7d4f856c 100644
--- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx
+++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx
@@ -48,7 +48,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
throw new Error(t("common.organization_not_found"));
}
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
if (!organizationTeams) {
throw new Error(t("common.organization_teams_not_found"));
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/loading.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/loading.tsx
index a77f0bb43e..1fdcdb9750 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/loading.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/loading.tsx
@@ -1,7 +1,3 @@
-import { LoadingSkeleton } from "./components/LoadingSkeleton";
+import { SurveyEditorLoading } from "@/modules/survey/editor/loading";
-const Loading = () => {
- return ;
-};
-
-export default Loading;
+export default SurveyEditorLoading;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx
index e19384c5c4..358bc38c5e 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx
@@ -1,132 +1,3 @@
-import { getUserEmail } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/user";
-import { authOptions } from "@/modules/auth/lib/authOptions";
-import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contacts";
-import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
-import { getIsContactsEnabled, getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
-import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
-import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
-import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
-import { ErrorComponent } from "@/modules/ui/components/error-component";
-import { getTranslate } from "@/tolgee/server";
-import { getServerSession } from "next-auth";
-import { getActionClasses } from "@formbricks/lib/actionClass/service";
-import {
- DEFAULT_LOCALE,
- IS_FORMBRICKS_CLOUD,
- MAIL_FROM,
- SURVEY_BG_COLORS,
- UNSPLASH_ACCESS_KEY,
-} from "@formbricks/lib/constants";
-import { getEnvironment } from "@formbricks/lib/environment/service";
-import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
-import { getAccessFlags } from "@formbricks/lib/membership/utils";
-import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
-import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
-import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
-import { getSurvey } from "@formbricks/lib/survey/service";
-import { getUserLocale } from "@formbricks/lib/user/service";
-import { SurveyEditor } from "./components/SurveyEditor";
+import { SurveyEditorPage } from "@/modules/survey/editor/page";
-export const generateMetadata = async (props) => {
- const params = await props.params;
- const survey = await getSurvey(params.surveyId);
- return {
- title: survey?.name ? `${survey?.name} | Editor` : "Editor",
- };
-};
-
-const Page = async (props) => {
- const searchParams = await props.searchParams;
- const params = await props.params;
- const t = await getTranslate();
- const [
- survey,
- project,
- environment,
- actionClasses,
- contactAttributeKeys,
- responseCount,
- organization,
- session,
- segments,
- ] = await Promise.all([
- getSurvey(params.surveyId),
- getProjectByEnvironmentId(params.environmentId),
- getEnvironment(params.environmentId),
- getActionClasses(params.environmentId),
- getContactAttributeKeys(params.environmentId),
- getResponseCountBySurveyId(params.surveyId),
- getOrganizationByEnvironmentId(params.environmentId),
- getServerSession(authOptions),
- getSegments(params.environmentId),
- ]);
-
- if (!session) {
- throw new Error(t("common.session_not_found"));
- }
-
- if (!organization) {
- throw new Error(t("common.organization_not_found"));
- }
-
- if (!project) {
- throw new Error(t("common.project_not_found"));
- }
-
- const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
- const { isMember } = getAccessFlags(currentUserMembership?.role);
-
- const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
-
- const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
-
- const isSurveyCreationDeletionDisabled = isMember && hasReadAccess;
- const locale = session.user.id ? await getUserLocale(session.user.id) : undefined;
-
- const isUserTargetingAllowed = await getIsContactsEnabled();
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
- const isSurveyFollowUpsAllowed = await getSurveyFollowUpsPermission(organization);
-
- const userEmail = await getUserEmail(session.user.id);
-
- if (
- !survey ||
- !environment ||
- !actionClasses ||
- !contactAttributeKeys ||
- !project ||
- !userEmail ||
- isSurveyCreationDeletionDisabled
- ) {
- return ;
- }
-
- const isCxMode = searchParams.mode === "cx";
-
- return (
-
- );
-};
-
-export default Page;
+export default SurveyEditorPage;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/loading.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/loading.tsx
index db63a377b1..dcdda12a17 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/loading.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/loading.tsx
@@ -1,7 +1,3 @@
-import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
+import { SurveyTemplatesLoading } from "@/modules/survey/templates/loading";
-const Loading = () => {
- return ;
-};
-
-export default Loading;
+export default SurveyTemplatesLoading;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx
index 6db6badc13..811e0caf02 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx
@@ -1,84 +1,3 @@
-import { authOptions } from "@/modules/auth/lib/authOptions";
-import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
-import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
-import { getTranslate } from "@/tolgee/server";
-import { getServerSession } from "next-auth";
-import { redirect } from "next/navigation";
-import { getEnvironment } from "@formbricks/lib/environment/service";
-import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
-import { getAccessFlags } from "@formbricks/lib/membership/utils";
-import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
-import { getUser } from "@formbricks/lib/user/service";
-import { TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
-import { TTemplateRole } from "@formbricks/types/templates";
-import { TemplateContainerWithPreview } from "./components/TemplateContainer";
+import { SurveyTemplatesPage } from "@/modules/survey/templates/page";
-interface SurveyTemplateProps {
- params: Promise<{
- environmentId: string;
- }>;
- searchParams: Promise<{
- channel?: TProjectConfigChannel;
- industry?: TProjectConfigIndustry;
- role?: TTemplateRole;
- }>;
-}
-
-const Page = async (props: SurveyTemplateProps) => {
- const searchParams = await props.searchParams;
- const params = await props.params;
- const t = await getTranslate();
- const session = await getServerSession(authOptions);
- const environmentId = params.environmentId;
-
- if (!session) {
- throw new Error(t("common.session_not_found"));
- }
-
- const [user, environment, project] = await Promise.all([
- getUser(session.user.id),
- getEnvironment(environmentId),
- getProjectByEnvironmentId(environmentId),
- ]);
-
- if (!user) {
- throw new Error(t("common.user_not_found"));
- }
-
- if (!project) {
- throw new Error(t("common.project_not_found"));
- }
-
- if (!environment) {
- throw new Error(t("common.environment_not_found"));
- }
- const currentUserMembership = await getMembershipByUserIdOrganizationId(
- session?.user.id,
- project.organizationId
- );
- const { isMember } = getAccessFlags(currentUserMembership?.role);
-
- const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
- const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
-
- const isReadOnly = isMember && hasReadAccess;
- if (isReadOnly) {
- return redirect(`/environments/${environment.id}/surveys`);
- }
-
- const prefilledFilters = [project.config.channel, project.config.industry, searchParams.role ?? null];
-
- return (
-
- );
-};
-
-export default Page;
+export default SurveyTemplatesPage;
diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts
index 847066d687..850f6ab965 100644
--- a/apps/web/app/(app)/environments/[environmentId]/actions.ts
+++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts
@@ -46,7 +46,7 @@ export const createProjectAction = authenticatedActionClient
throw new Error("Organization not found");
}
- const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
+ const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.billing.limits);
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
if (organizationProjectsCount >= organizationProjectsLimit) {
@@ -54,7 +54,7 @@ export const createProjectAction = authenticatedActionClient
}
if (parsedInput.data.teamIds && parsedInput.data.teamIds.length > 0) {
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
if (!canDoRoleManagement) {
throw new OperationNotAllowedError("You do not have permission to manage roles");
diff --git a/apps/web/app/(app)/environments/[environmentId]/actions/components/ActionActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/actions/components/ActionActivityTab.tsx
index 1fa8800b3a..7f18919835 100644
--- a/apps/web/app/(app)/environments/[environmentId]/actions/components/ActionActivityTab.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/actions/components/ActionActivityTab.tsx
@@ -1,7 +1,7 @@
"use client";
-import { createActionClassAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
+import { createActionClassAction } from "@/modules/survey/editor/actions";
import { Button } from "@/modules/ui/components/button";
import { ErrorComponent } from "@/modules/ui/components/error-component";
import { Label } from "@/modules/ui/components/label";
diff --git a/apps/web/app/(app)/environments/[environmentId]/actions/components/AddActionModal.tsx b/apps/web/app/(app)/environments/[environmentId]/actions/components/AddActionModal.tsx
index b52b16859a..635f94c10a 100644
--- a/apps/web/app/(app)/environments/[environmentId]/actions/components/AddActionModal.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/actions/components/AddActionModal.tsx
@@ -1,6 +1,6 @@
"use client";
-import { CreateNewActionTab } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CreateNewActionTab";
+import { CreateNewActionTab } from "@/modules/survey/editor/components/create-new-action-tab";
import { Button } from "@/modules/ui/components/button";
import { Modal } from "@/modules/ui/components/modal";
import { useTranslate } from "@tolgee/react";
diff --git a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx
index d7fb47027d..48db282805 100644
--- a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx
@@ -80,7 +80,7 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
]);
}
- const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
+ const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.billing.limits);
return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/contact-attribute-key.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/contact-attribute-key.ts
deleted file mode 100644
index 2726b3979b..0000000000
--- a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/contact-attribute-key.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
-import { cache as reactCache } from "react";
-import { prisma } from "@formbricks/database";
-import { cache } from "@formbricks/lib/cache";
-import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
-
-export const getContactAttributeKeys = reactCache(
- (environmentId: string): Promise
=>
- cache(
- async () => {
- return await prisma.contactAttributeKey.findMany({
- where: { environmentId },
- });
- },
- [`getContactAttributeKeys-integrations-${environmentId}`],
- {
- tags: [contactAttributeKeyCache.tag.byEnvironmentId(environmentId)],
- }
- )()
-);
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx
index 374d3d064c..4f6a177dd7 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx
@@ -39,7 +39,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isOwner, isManager } = getAccessFlags(currentUserMembership?.role);
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
- const hasWhiteLabelPermission = await getWhiteLabelPermission(organization);
+ const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.billing.plan);
const isDeleteDisabled = !isOwner || !isMultiOrgEnabled;
const currentUserRole = currentUserMembership?.role;
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx
index f36928adde..d087986bfb 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx
@@ -71,7 +71,10 @@ const Page = async (props) => {
const isReadOnly = isMember && hasReadAccess;
- const isAIEnabled = await getIsAIEnabled(organization);
+ const isAIEnabled = await getIsAIEnabled({
+ isAIEnabled: organization.isAIEnabled,
+ billing: organization.billing,
+ });
const shouldGenerateInsights = needsInsightsGeneration(survey);
const locale = await findMatchingLocale();
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx
index 5cd3455ce9..6557fe7643 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx
@@ -79,7 +79,10 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
// I took this out cause it's cloud only right?
// const { active: isEnterpriseEdition } = await getEnterpriseLicense();
- const isAIEnabled = await getIsAIEnabled(organization);
+ const isAIEnabled = await getIsAIEnabled({
+ isAIEnabled: organization.isAIEnabled,
+ billing: organization.billing,
+ });
const shouldGenerateInsights = needsInsightsGeneration(survey);
return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts
index ecbc6da038..4e1336da47 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts
@@ -4,7 +4,7 @@ import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions";
-import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
+import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
import { z } from "zod";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getResponseDownloadUrl, getResponseFilteringValues } from "@formbricks/lib/response/service";
@@ -95,7 +95,7 @@ const checkSurveyFollowUpsPermission = async (organizationId: string): Promise =>
- cache(
- async () => {
- validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
-
- try {
- if (filterCriteria?.sortBy === "relevance") {
- // Call the sortByRelevance function
- return await getSurveysSortedByRelevance(environmentId, limit, offset ?? 0, filterCriteria);
- }
-
- // Fetch surveys normally with pagination and include response count
- const surveysPrisma = await prisma.survey.findMany({
- where: {
- environmentId,
- ...buildWhereClause(filterCriteria),
- },
- select: surveySelect,
- orderBy: buildOrderByClause(filterCriteria?.sortBy),
- take: limit,
- skip: offset,
- });
-
- return surveysPrisma.map((survey) => {
- return {
- ...survey,
- responseCount: survey._count.responses,
- };
- });
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- console.error(error);
- throw new DatabaseError(error.message);
- }
- throw error;
- }
- },
- [`surveyList-getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
- {
- tags: [
- surveyCache.tag.byEnvironmentId(environmentId),
- responseCache.tag.byEnvironmentId(environmentId),
- ],
- }
- )()
-);
-
-export const getSurveysSortedByRelevance = reactCache(
- async (
- environmentId: string,
- limit?: number,
- offset?: number,
- filterCriteria?: TSurveyFilterCriteria
- ): Promise =>
- cache(
- async () => {
- validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
-
- try {
- let surveys: TSurvey[] = [];
- const inProgressSurveyCount = await getInProgressSurveyCount(environmentId, filterCriteria);
-
- // Fetch surveys that are in progress first
- const inProgressSurveys =
- offset && offset > inProgressSurveyCount
- ? []
- : await prisma.survey.findMany({
- where: {
- environmentId,
- status: "inProgress",
- ...buildWhereClause(filterCriteria),
- },
- select: surveySelect,
- orderBy: buildOrderByClause("updatedAt"),
- take: limit,
- skip: offset,
- });
-
- surveys = inProgressSurveys.map((survey) => {
- return {
- ...survey,
- responseCount: survey._count.responses,
- };
- });
-
- // Determine if additional surveys are needed
- if (offset !== undefined && limit && inProgressSurveys.length < limit) {
- const remainingLimit = limit - inProgressSurveys.length;
- const newOffset = Math.max(0, offset - inProgressSurveyCount);
- const additionalSurveys = await prisma.survey.findMany({
- where: {
- environmentId,
- status: { not: "inProgress" },
- ...buildWhereClause(filterCriteria),
- },
- select: surveySelect,
- orderBy: buildOrderByClause("updatedAt"),
- take: remainingLimit,
- skip: newOffset,
- });
-
- surveys = [
- ...surveys,
- ...additionalSurveys.map((survey) => {
- return {
- ...survey,
- responseCount: survey._count.responses,
- };
- }),
- ];
- }
-
- return surveys;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- console.error(error);
- throw new DatabaseError(error.message);
- }
- throw error;
- }
- },
- [
- `surveyList-getSurveysSortedByRelevance-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`,
- ],
- {
- tags: [
- surveyCache.tag.byEnvironmentId(environmentId),
- responseCache.tag.byEnvironmentId(environmentId),
- ],
- }
- )()
-);
-
-export const getSurvey = reactCache(
- async (surveyId: string): Promise =>
- cache(
- async () => {
- validateInputs([surveyId, ZId]);
-
- let surveyPrisma;
- try {
- surveyPrisma = await prisma.survey.findUnique({
- where: {
- id: surveyId,
- },
- select: surveySelect,
- });
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- console.error(error);
- throw new DatabaseError(error.message);
- }
- throw error;
- }
-
- if (!surveyPrisma) {
- return null;
- }
-
- return { ...surveyPrisma, responseCount: surveyPrisma?._count.responses };
- },
- [`surveyList-getSurvey-${surveyId}`],
- {
- tags: [surveyCache.tag.byId(surveyId), responseCache.tag.bySurveyId(surveyId)],
- }
- )()
-);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/loading.tsx
index 8b930267c5..7c082be678 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/loading.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/loading.tsx
@@ -1,31 +1,3 @@
-"use client";
+import { SurveyListLoading } from "@/modules/survey/list/loading";
-import { SurveyLoading } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyLoading";
-import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
-import { PageHeader } from "@/modules/ui/components/page-header";
-import { useTranslate } from "@tolgee/react";
-
-const Loading = () => {
- const { t } = useTranslate();
- return (
-
-
-
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
-
-
-
- );
-};
-
-export default Loading;
+export default SurveyListLoading;
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx
index d0aec16749..d5e5de169f 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx
@@ -1,144 +1,4 @@
-import { SurveysList } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyList";
-import { authOptions } from "@/modules/auth/lib/authOptions";
-import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
-import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
-import { TemplateList } from "@/modules/surveys/components/TemplateList";
-import { Button } from "@/modules/ui/components/button";
-import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
-import { PageHeader } from "@/modules/ui/components/page-header";
-import { getTranslate } from "@/tolgee/server";
-import { PlusIcon } from "lucide-react";
-import { Metadata, NextPage } from "next";
-import { getServerSession } from "next-auth";
-import Link from "next/link";
-import { redirect } from "next/navigation";
-import { SURVEYS_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
-import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
-import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
-import { getAccessFlags } from "@formbricks/lib/membership/utils";
-import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
-import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
-import { getSurveyCount } from "@formbricks/lib/survey/service";
-import { getUser } from "@formbricks/lib/user/service";
-import { findMatchingLocale } from "@formbricks/lib/utils/locale";
-import { TTemplateRole } from "@formbricks/types/templates";
+import { SurveysPage, metadata } from "@/modules/survey/list/page";
-export const metadata: Metadata = {
- title: "Your Surveys",
-};
-
-interface SurveyTemplateProps {
- params: Promise<{
- environmentId: string;
- }>;
- searchParams: Promise<{
- role?: TTemplateRole;
- }>;
-}
-
-const SurveysPage = async ({ params: paramsProps, searchParams: searchParamsProps }: SurveyTemplateProps) => {
- const searchParams = await searchParamsProps;
- const params = await paramsProps;
-
- const session = await getServerSession(authOptions);
- const project = await getProjectByEnvironmentId(params.environmentId);
- const organization = await getOrganizationByEnvironmentId(params.environmentId);
- const t = await getTranslate();
- if (!session) {
- throw new Error(t("common.session_not_found"));
- }
-
- const user = await getUser(session.user.id);
- if (!user) {
- throw new Error(t("common.user_not_found"));
- }
-
- if (!project) {
- throw new Error(t("common.project_not_found"));
- }
-
- if (!organization) {
- throw new Error(t("common.organization_not_found"));
- }
-
- const prefilledFilters = [project?.config.channel, project.config.industry, searchParams.role ?? null];
-
- const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
- const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
-
- const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
- const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
-
- const isReadOnly = isMember && hasReadAccess;
-
- if (isBilling) {
- return redirect(`/environments/${params.environmentId}/settings/billing`);
- }
-
- const environment = await getEnvironment(params.environmentId);
- if (!environment) {
- throw new Error(t("common.environment_not_found"));
- }
-
- const surveyCount = await getSurveyCount(params.environmentId);
-
- const environments = await getEnvironments(project.id);
- const otherEnvironment = environments.find((e) => e.type !== environment.type)!;
-
- const currentProjectChannel = project.config.channel ?? null;
- const locale = await findMatchingLocale();
- const CreateSurveyButton = () => {
- return (
-
- );
- };
-
- return (
-
- {surveyCount > 0 ? (
- <>
- > : } />
-
- >
- ) : isReadOnly ? (
- <>
-
- {t("environments.surveys.no_surveys_created_yet")}
-
-
-
- {t("environments.surveys.read_only_user_not_allowed_to_create_survey_warning")}
-
- >
- ) : (
- <>
-
- {t("environments.surveys.all_set_time_to_create_first_survey")}
-
-
- >
- )}
-
- );
-};
-
-export default SurveysPage as NextPage;
+export { metadata };
+export default SurveysPage;
diff --git a/apps/web/app/[shortUrlId]/page.tsx b/apps/web/app/[shortUrlId]/page.tsx
index 306a288127..eb26877f77 100644
--- a/apps/web/app/[shortUrlId]/page.tsx
+++ b/apps/web/app/[shortUrlId]/page.tsx
@@ -1,4 +1,4 @@
-import { getMetadataForLinkSurvey } from "@/app/s/[surveyId]/metadata";
+import { getMetadataForLinkSurvey } from "@/modules/survey/link/metadata";
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import { getShortUrl } from "@formbricks/lib/shortUrl/service";
diff --git a/apps/web/app/api/(internal)/pipeline/route.ts b/apps/web/app/api/(internal)/pipeline/route.ts
index 204177b477..c2739e4877 100644
--- a/apps/web/app/api/(internal)/pipeline/route.ts
+++ b/apps/web/app/api/(internal)/pipeline/route.ts
@@ -6,7 +6,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { webhookCache } from "@/lib/cache/webhook";
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
import { sendResponseFinishedEmail } from "@/modules/email";
-import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
+import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
import { PipelineTriggers, Webhook } from "@prisma/client";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
@@ -164,7 +164,7 @@ export const POST = async (request: Request) => {
});
// send follow up emails
- const surveyFollowUpsPermission = await getSurveyFollowUpsPermission(organization);
+ const surveyFollowUpsPermission = await getSurveyFollowUpsPermission(organization.billing.plan);
if (surveyFollowUpsPermission) {
await sendSurveyFollowUps(survey, response, organization);
@@ -197,7 +197,10 @@ export const POST = async (request: Request) => {
if (hasSurveyOpenTextQuestions) {
const isAICofigured = IS_AI_CONFIGURED;
if (hasSurveyOpenTextQuestions && isAICofigured) {
- const isAIEnabled = await getIsAIEnabled(organization);
+ const isAIEnabled = await getIsAIEnabled({
+ isAIEnabled: organization.isAIEnabled,
+ billing: organization.billing,
+ });
if (isAIEnabled) {
for (const question of survey.questions) {
diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts
index 686f94c927..e771f470e7 100644
--- a/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts
@@ -1,9 +1,9 @@
+import { transformPrismaSurvey } from "@/modules/survey/lib/utils";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
-import { transformPrismaSurvey } from "@formbricks/lib/survey/utils";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
diff --git a/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts
index bdc0af48ea..8fc2df44bc 100644
--- a/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts
@@ -108,7 +108,7 @@ export const POST = async (req: NextRequest, context: Context): Promise {
+ validateInputs([surveyId, z.string().cuid2()]);
+
+ try {
+ const deletedSurvey = await prisma.survey.delete({
+ where: {
+ id: surveyId,
+ },
+ include: {
+ segment: true,
+ triggers: {
+ include: {
+ actionClass: true,
+ },
+ },
+ },
+ });
+
+ if (deletedSurvey.type === "app" && deletedSurvey.segment?.isPrivate) {
+ const deletedSegment = await prisma.segment.delete({
+ where: {
+ id: deletedSurvey.segment.id,
+ },
+ });
+
+ if (deletedSegment) {
+ segmentCache.revalidate({
+ id: deletedSegment.id,
+ environmentId: deletedSurvey.environmentId,
+ });
+ }
+ }
+
+ responseCache.revalidate({
+ surveyId,
+ environmentId: deletedSurvey.environmentId,
+ });
+ surveyCache.revalidate({
+ id: deletedSurvey.id,
+ environmentId: deletedSurvey.environmentId,
+ resultShareKey: deletedSurvey.resultShareKey ?? undefined,
+ });
+
+ if (deletedSurvey.segment?.id) {
+ segmentCache.revalidate({
+ id: deletedSurvey.segment.id,
+ environmentId: deletedSurvey.environmentId,
+ });
+ }
+
+ // Revalidate public triggers by actionClassId
+ deletedSurvey.triggers.forEach((trigger) => {
+ surveyCache.revalidate({
+ actionClassId: trigger.actionClass.id,
+ });
+ });
+
+ return deletedSurvey;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+};
diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts
index f46279f8ec..4c44d923ad 100644
--- a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts
+++ b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts
@@ -1,10 +1,11 @@
import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth";
+import { deleteSurvey } from "@/app/api/v1/management/surveys/[surveyId]/lib/surveys";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
-import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
+import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
-import { deleteSurvey, getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
+import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { TSurvey, ZSurveyUpdateInput } from "@formbricks/types/surveys/types";
const fetchAndAuthorizeSurvey = async (authentication: any, surveyId: string): Promise => {
@@ -95,14 +96,14 @@ export const PUT = async (
}
if (surveyUpdate.followUps && surveyUpdate.followUps.length) {
- const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organization);
+ const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organization.billing.plan);
if (!isSurveyFollowUpsEnabled) {
return responses.forbiddenResponse("Survey follow ups are not enabled for this organization");
}
}
if (surveyUpdate.languages && surveyUpdate.languages.length) {
- const isMultiLanguageEnabled = await getMultiLanguagePermission(organization);
+ const isMultiLanguageEnabled = await getMultiLanguagePermission(organization.billing.plan);
if (!isMultiLanguageEnabled) {
return responses.forbiddenResponse("Multi language is not enabled for this organization");
}
diff --git a/apps/web/app/api/v1/management/surveys/route.ts b/apps/web/app/api/v1/management/surveys/route.ts
index 6ab9c55b97..029c5c29fc 100644
--- a/apps/web/app/api/v1/management/surveys/route.ts
+++ b/apps/web/app/api/v1/management/surveys/route.ts
@@ -2,7 +2,7 @@ import { authenticateRequest } from "@/app/api/v1/auth";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
-import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
+import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { createSurvey, getSurveys } from "@formbricks/lib/survey/service";
import { DatabaseError } from "@formbricks/types/errors";
@@ -59,14 +59,14 @@ export const POST = async (request: Request): Promise => {
const surveyData = { ...inputValidation.data, environmentId: undefined };
if (surveyData.followUps?.length) {
- const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organization);
+ const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organization.billing.plan);
if (!isSurveyFollowUpsEnabled) {
return responses.forbiddenResponse("Survey follow ups are not enabled allowed for this organization");
}
}
if (surveyData.languages && surveyData.languages.length) {
- const isMultiLanguageEnabled = await getMultiLanguagePermission(organization);
+ const isMultiLanguageEnabled = await getMultiLanguagePermission(organization.billing.plan);
if (!isMultiLanguageEnabled) {
return responses.forbiddenResponse("Multi language is not enabled for this organization");
}
diff --git a/apps/web/app/s/[surveyId]/layout.tsx b/apps/web/app/s/[surveyId]/layout.tsx
index 295b795ac2..b8ad2cf34d 100644
--- a/apps/web/app/s/[surveyId]/layout.tsx
+++ b/apps/web/app/s/[surveyId]/layout.tsx
@@ -1,15 +1,5 @@
-import { Viewport } from "next";
+import { LinkSurveyLayout, viewport } from "@/modules/survey/link/layout";
-export const viewport: Viewport = {
- width: "device-width",
- initialScale: 1.0,
- maximumScale: 1.0,
- userScalable: false,
- viewportFit: "contain",
-};
+export { viewport };
-const SurveyLayout = ({ children }) => {
- return {children}
;
-};
-
-export default SurveyLayout;
+export default LinkSurveyLayout;
diff --git a/apps/web/app/s/[surveyId]/lib/contact-attribute-key.ts b/apps/web/app/s/[surveyId]/lib/contact-attribute-key.ts
deleted file mode 100644
index cb8e293727..0000000000
--- a/apps/web/app/s/[surveyId]/lib/contact-attribute-key.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
-import { cache as reactCache } from "react";
-import { prisma } from "@formbricks/database";
-import { cache } from "@formbricks/lib/cache";
-import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
-
-export const getContactAttributeKeys = reactCache(
- (environmentId: string): Promise =>
- cache(
- async () => {
- return await prisma.contactAttributeKey.findMany({
- where: { environmentId },
- });
- },
- [`getContactAttributeKeys-survey-page-${environmentId}`],
- {
- tags: [contactAttributeKeyCache.tag.byEnvironmentId(environmentId)],
- }
- )()
-);
diff --git a/apps/web/app/s/[surveyId]/loading.tsx b/apps/web/app/s/[surveyId]/loading.tsx
index 617f3d5493..a312961734 100644
--- a/apps/web/app/s/[surveyId]/loading.tsx
+++ b/apps/web/app/s/[surveyId]/loading.tsx
@@ -1,12 +1,3 @@
-const Loading = () => {
- return (
-
- );
-};
+import { LinkSurveyLoading } from "@/modules/survey/link/loading";
-export default Loading;
+export default LinkSurveyLoading;
diff --git a/apps/web/app/s/[surveyId]/not-found.tsx b/apps/web/app/s/[surveyId]/not-found.tsx
index f27ffae6c1..845c9b416b 100644
--- a/apps/web/app/s/[surveyId]/not-found.tsx
+++ b/apps/web/app/s/[surveyId]/not-found.tsx
@@ -1,29 +1,3 @@
-import { Button } from "@/modules/ui/components/button";
-import { HelpCircleIcon } from "lucide-react";
-import { StaticImport } from "next/dist/shared/lib/get-img-props";
-import Image from "next/image";
-import Link from "next/link";
-import footerLogo from "./lib/footerlogo.svg";
+import { LinkSurveyNotFound } from "@/modules/survey/link/not-found";
-const NotFound = () => {
- return (
-
-
-
-
,
-
Survey not found.
-
There is no survey with this ID.
-
-
-
-
-
-
-
-
- );
-};
-
-export default NotFound;
+export default LinkSurveyNotFound;
diff --git a/apps/web/app/s/[surveyId]/page.tsx b/apps/web/app/s/[surveyId]/page.tsx
index 836038e057..145e3fc249 100644
--- a/apps/web/app/s/[surveyId]/page.tsx
+++ b/apps/web/app/s/[surveyId]/page.tsx
@@ -1,189 +1,4 @@
-import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
-import { LinkSurvey } from "@/app/s/[surveyId]/components/link-survey";
-import { PinScreen } from "@/app/s/[surveyId]/components/pin-screen";
-import { SurveyInactive } from "@/app/s/[surveyId]/components/survey-inactive";
-import { getMetadataForLinkSurvey } from "@/app/s/[surveyId]/metadata";
-import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
-import type { Metadata } from "next";
-import { notFound } from "next/navigation";
-import { IMPRINT_URL, IS_FORMBRICKS_CLOUD, PRIVACY_URL, WEBAPP_URL } from "@formbricks/lib/constants";
-import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
-import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
-import { getResponseBySingleUseId, getResponseCountBySurveyId } from "@formbricks/lib/response/service";
-import { getSurvey } from "@formbricks/lib/survey/service";
-import { findMatchingLocale } from "@formbricks/lib/utils/locale";
-import { ZId } from "@formbricks/types/common";
-import { TResponse } from "@formbricks/types/responses";
-import { getEmailVerificationDetails } from "./lib/helpers";
+import { LinkSurveyPage, generateMetadata } from "@/modules/survey/link/page";
-interface LinkSurveyPageProps {
- params: Promise<{
- surveyId: string;
- }>;
- searchParams: Promise<{
- suId?: string;
- verify?: string;
- lang?: string;
- embed?: string;
- preview?: string;
- }>;
-}
-
-export const generateMetadata = async (props: LinkSurveyPageProps): Promise => {
- const params = await props.params;
- const validId = ZId.safeParse(params.surveyId);
- if (!validId.success) {
- notFound();
- }
-
- return getMetadataForLinkSurvey(params.surveyId);
-};
-
-const Page = async (props: LinkSurveyPageProps) => {
- const searchParams = await props.searchParams;
- const params = await props.params;
- const validId = ZId.safeParse(params.surveyId);
- if (!validId.success) {
- notFound();
- }
- const isPreview = searchParams.preview === "true";
- const survey = await getSurvey(params.surveyId);
- const locale = await findMatchingLocale();
- const suId = searchParams.suId;
- const langParam = searchParams.lang; //can either be language code or alias
- const isSingleUseSurvey = survey?.singleUse?.enabled;
- const isSingleUseSurveyEncrypted = survey?.singleUse?.isEncrypted;
- const isEmbed = searchParams.embed === "true";
- if (!survey || survey.type !== "link" || survey.status === "draft") {
- notFound();
- }
-
- const organization = await getOrganizationByEnvironmentId(survey.environmentId);
- if (!organization) {
- throw new Error("Organization not found");
- }
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
-
- if (survey.status !== "inProgress" && !isPreview) {
- return (
-
- );
- }
-
- let singleUseId: string | undefined = undefined;
- if (isSingleUseSurvey) {
- // check if the single use id is present for single use surveys
- if (!suId) {
- return ;
- }
-
- // if encryption is enabled, validate the single use id
- let validatedSingleUseId: string | undefined = undefined;
- if (isSingleUseSurveyEncrypted) {
- validatedSingleUseId = validateSurveySingleUseId(suId);
- if (!validatedSingleUseId) {
- return ;
- }
- }
- // if encryption is disabled, use the suId as is
- singleUseId = validatedSingleUseId ?? suId;
- }
-
- let singleUseResponse: TResponse | undefined = undefined;
- if (isSingleUseSurvey) {
- try {
- singleUseResponse = singleUseId
- ? ((await getResponseBySingleUseId(survey.id, singleUseId)) ?? undefined)
- : undefined;
- } catch (error) {
- singleUseResponse = undefined;
- }
- }
-
- // verify email: Check if the survey requires email verification
- let emailVerificationStatus = "";
- let verifiedEmail: string | undefined = undefined;
-
- if (survey.isVerifyEmailEnabled) {
- const token = searchParams.verify;
-
- if (token) {
- const emailVerificationDetails = await getEmailVerificationDetails(survey.id, token);
- emailVerificationStatus = emailVerificationDetails.status;
- verifiedEmail = emailVerificationDetails.email;
- }
- }
-
- // get project and person
- const project = await getProjectByEnvironmentId(survey.environmentId);
- if (!project) {
- throw new Error("Project not found");
- }
-
- const getLanguageCode = (): string => {
- if (!langParam || !isMultiLanguageAllowed) return "default";
- else {
- const selectedLanguage = survey.languages.find((surveyLanguage) => {
- return (
- surveyLanguage.language.code === langParam.toLowerCase() ||
- surveyLanguage.language.alias?.toLowerCase() === langParam.toLowerCase()
- );
- });
- if (selectedLanguage?.default || !selectedLanguage?.enabled) {
- return "default";
- }
- return selectedLanguage.language.code;
- }
- };
-
- const languageCode = getLanguageCode();
-
- const isSurveyPinProtected = Boolean(survey.pin);
- const responseCount = await getResponseCountBySurveyId(survey.id);
-
- if (isSurveyPinProtected) {
- return (
-
- );
- }
-
- return (
-
- );
-};
-
-export default Page;
+export { generateMetadata };
+export default LinkSurveyPage;
diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx
index 83c27c99ed..69bb2f09d8 100644
--- a/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx
+++ b/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx
@@ -1,7 +1,7 @@
"use client";
-import { generateSingleUseIdAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
+import { generateSingleUseIdAction } from "@/modules/survey/list/actions";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import { Copy, RefreshCcw, SquareArrowOutUpRight } from "lucide-react";
diff --git a/apps/web/modules/ee/insights/actions.ts b/apps/web/modules/ee/insights/actions.ts
index 154bbf1ce8..2c19af3984 100644
--- a/apps/web/modules/ee/insights/actions.ts
+++ b/apps/web/modules/ee/insights/actions.ts
@@ -18,7 +18,10 @@ export const checkAIPermission = async (organizationId: string) => {
throw new Error("Organization not found");
}
- const isAIEnabled = await getIsAIEnabled(organization);
+ const isAIEnabled = await getIsAIEnabled({
+ isAIEnabled: organization.isAIEnabled,
+ billing: organization.billing,
+ });
if (!isAIEnabled) {
throw new OperationNotAllowedError("AI is not enabled for this organization");
diff --git a/apps/web/modules/ee/insights/experience/components/templates-card.tsx b/apps/web/modules/ee/insights/experience/components/templates-card.tsx
index 4b6b9df6d8..a593cb2cc7 100644
--- a/apps/web/modules/ee/insights/experience/components/templates-card.tsx
+++ b/apps/web/modules/ee/insights/experience/components/templates-card.tsx
@@ -1,16 +1,16 @@
"use client";
-import { TemplateList } from "@/modules/surveys/components/TemplateList";
+import { TemplateList } from "@/modules/survey/components/template-list";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/modules/ui/components/card";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { TEnvironment } from "@formbricks/types/environment";
-import { TProject } from "@formbricks/types/project";
import { TTemplateFilter } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
interface TemplatesCardProps {
environment: TEnvironment;
- project: TProject;
+ project: Project;
user: TUser;
prefilledFilters: TTemplateFilter[];
}
@@ -25,10 +25,10 @@ export const TemplatesCard = ({ environment, project, user, prefilledFilters }:
diff --git a/apps/web/modules/ee/insights/experience/page.tsx b/apps/web/modules/ee/insights/experience/page.tsx
index 4f06660bbb..a3dde9ee91 100644
--- a/apps/web/modules/ee/insights/experience/page.tsx
+++ b/apps/web/modules/ee/insights/experience/page.tsx
@@ -50,7 +50,10 @@ export const ExperiencePage = async (props) => {
notFound();
}
- const isAIEnabled = await getIsAIEnabled(organization);
+ const isAIEnabled = await getIsAIEnabled({
+ isAIEnabled: organization.isAIEnabled,
+ billing: organization.billing,
+ });
if (!isAIEnabled) {
notFound();
diff --git a/apps/web/modules/ee/languages/page.tsx b/apps/web/modules/ee/languages/page.tsx
index e18143e141..19a78c37d4 100644
--- a/apps/web/modules/ee/languages/page.tsx
+++ b/apps/web/modules/ee/languages/page.tsx
@@ -34,12 +34,12 @@ export const LanguagesPage = async (props: { params: Promise<{ environmentId: st
throw new Error(t("common.organization_not_found"));
}
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
if (!isMultiLanguageAllowed) {
notFound();
}
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
const session = await getServerSession(authOptions);
diff --git a/apps/web/modules/ee/license-check/lib/utils.ts b/apps/web/modules/ee/license-check/lib/utils.ts
index 7ea33654ed..eaa620afe7 100644
--- a/apps/web/modules/ee/license-check/lib/utils.ts
+++ b/apps/web/modules/ee/license-check/lib/utils.ts
@@ -3,6 +3,7 @@ import {
TEnterpriseLicenseDetails,
TEnterpriseLicenseFeatures,
} from "@/modules/ee/license-check/types/enterprise-license";
+import { Organization } from "@prisma/client";
import { HttpsProxyAgent } from "https-proxy-agent";
import { after } from "next/server";
import fetch from "node-fetch";
@@ -18,7 +19,6 @@ import {
} from "@formbricks/lib/constants";
import { env } from "@formbricks/lib/env";
import { hashString } from "@formbricks/lib/hashString";
-import { TOrganization, TOrganizationBillingPlan } from "@formbricks/types/organizations";
const hashedKey = ENTERPRISE_LICENSE_KEY ? hashString(ENTERPRISE_LICENSE_KEY) : undefined;
const PREVIOUS_RESULTS_CACHE_TAG_KEY = `getPreviousResult-${hashedKey}` as const;
@@ -261,14 +261,16 @@ export const fetchLicense = reactCache(
)()
);
-export const getRemoveBrandingPermission = async (organization: TOrganization): Promise => {
+export const getRemoveBrandingPermission = async (
+ billingPlan: Organization["billing"]["plan"]
+): Promise => {
if (E2E_TESTING) {
const previousResult = await fetchLicenseForE2ETesting();
return previousResult?.features?.removeBranding ?? false;
}
if (IS_FORMBRICKS_CLOUD && (await getEnterpriseLicense()).active) {
- return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE;
+ return billingPlan !== PROJECT_FEATURE_KEYS.FREE;
} else {
const licenseFeatures = await getLicenseFeatures();
if (!licenseFeatures) return false;
@@ -277,14 +279,16 @@ export const getRemoveBrandingPermission = async (organization: TOrganization):
}
};
-export const getWhiteLabelPermission = async (organization: TOrganization): Promise => {
+export const getWhiteLabelPermission = async (
+ billingPlan: Organization["billing"]["plan"]
+): Promise => {
if (E2E_TESTING) {
const previousResult = await fetchLicenseForE2ETesting();
return previousResult?.features?.whitelabel ?? false;
}
if (IS_FORMBRICKS_CLOUD && (await getEnterpriseLicense()).active) {
- return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE;
+ return billingPlan !== PROJECT_FEATURE_KEYS.FREE;
} else {
const licenseFeatures = await getLicenseFeatures();
if (!licenseFeatures) return false;
@@ -293,36 +297,36 @@ export const getWhiteLabelPermission = async (organization: TOrganization): Prom
}
};
-export const getRoleManagementPermission = async (organization: TOrganization): Promise => {
+export const getRoleManagementPermission = async (
+ billingPlan: Organization["billing"]["plan"]
+): Promise => {
if (E2E_TESTING) {
const previousResult = await fetchLicenseForE2ETesting();
return previousResult && previousResult.active !== null ? previousResult.active : false;
}
if (IS_FORMBRICKS_CLOUD)
- return (
- organization.billing.plan === PROJECT_FEATURE_KEYS.SCALE ||
- organization.billing.plan === PROJECT_FEATURE_KEYS.ENTERPRISE
- );
+ return billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE;
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
return false;
};
-export const getBiggerUploadFileSizePermission = async (organization: TOrganization): Promise => {
- if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE;
+export const getBiggerUploadFileSizePermission = async (
+ billingPlan: Organization["billing"]["plan"]
+): Promise => {
+ if (IS_FORMBRICKS_CLOUD) return billingPlan !== PROJECT_FEATURE_KEYS.FREE;
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
return false;
};
-export const getMultiLanguagePermission = async (organization: TOrganization): Promise => {
+export const getMultiLanguagePermission = async (
+ billingPlan: Organization["billing"]["plan"]
+): Promise => {
if (E2E_TESTING) {
const previousResult = await fetchLicenseForE2ETesting();
return previousResult && previousResult.active !== null ? previousResult.active : false;
}
if (IS_FORMBRICKS_CLOUD)
- return (
- organization.billing.plan === PROJECT_FEATURE_KEYS.SCALE ||
- organization.billing.plan === PROJECT_FEATURE_KEYS.ENTERPRISE
- );
+ return billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE;
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
return false;
};
@@ -367,7 +371,7 @@ export const getIsSSOEnabled = async (): Promise => {
return licenseFeatures.sso;
};
-export const getIsOrganizationAIReady = async (billingPlan: TOrganizationBillingPlan) => {
+export const getIsOrganizationAIReady = async (billingPlan: Organization["billing"]["plan"]) => {
if (!IS_AI_CONFIGURED) return false;
if (E2E_TESTING) {
const previousResult = await fetchLicenseForE2ETesting();
@@ -382,11 +386,13 @@ export const getIsOrganizationAIReady = async (billingPlan: TOrganizationBilling
return Boolean(license.features?.ai);
};
-export const getIsAIEnabled = async (organization: TOrganization) => {
+export const getIsAIEnabled = async (organization: Pick) => {
return organization.isAIEnabled && (await getIsOrganizationAIReady(organization.billing.plan));
};
-export const getOrganizationProjectsLimit = async (organization: TOrganization): Promise => {
+export const getOrganizationProjectsLimit = async (
+ limits: Organization["billing"]["limits"]
+): Promise => {
if (E2E_TESTING) {
const previousResult = await fetchLicenseForE2ETesting();
return previousResult && previousResult.features ? (previousResult.features.projects ?? Infinity) : 3;
@@ -395,7 +401,7 @@ export const getOrganizationProjectsLimit = async (organization: TOrganization):
let limit: number;
if (IS_FORMBRICKS_CLOUD && (await getEnterpriseLicense()).active) {
- limit = organization.billing.limits.projects ?? Infinity;
+ limit = limits.projects ?? Infinity;
} else {
const licenseFeatures = await getLicenseFeatures();
if (!licenseFeatures) {
diff --git a/apps/web/modules/ee/multi-language-surveys/components/default-language-select.tsx b/apps/web/modules/ee/multi-language-surveys/components/default-language-select.tsx
index e535949d1c..092b828a2b 100644
--- a/apps/web/modules/ee/multi-language-surveys/components/default-language-select.tsx
+++ b/apps/web/modules/ee/multi-language-surveys/components/default-language-select.tsx
@@ -8,15 +8,15 @@ import {
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
+import { Language } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
-import type { TLanguage, TProject } from "@formbricks/types/project";
import type { ConfirmationModalProps } from "./multi-language-card";
interface DefaultLanguageSelectProps {
- defaultLanguage?: TLanguage;
+ defaultLanguage?: Language;
handleDefaultLanguageChange: (languageCode: string) => void;
- project: TProject;
+ projectLanguages: Language[];
setConfirmationModalInfo: (confirmationModal: ConfirmationModalProps) => void;
locale: string;
}
@@ -24,7 +24,7 @@ interface DefaultLanguageSelectProps {
export function DefaultLanguageSelect({
defaultLanguage,
handleDefaultLanguageChange,
- project,
+ projectLanguages,
setConfirmationModalInfo,
locale,
}: DefaultLanguageSelectProps) {
@@ -61,7 +61,7 @@ export function DefaultLanguageSelect({
- {project.languages.map((language) => (
+ {projectLanguages.map((language) => (
{
return new Set(arr).size !== arr.length;
};
-const validateLanguages = (languages: TLanguage[], t: TFnType) => {
+const validateLanguages = (languages: Language[], t: TFnType) => {
const languageCodes = languages.map((language) => language.code.toLowerCase().trim());
const languageAliases = languages
.filter((language) => language.alias)
@@ -71,7 +72,7 @@ const validateLanguages = (languages: TLanguage[], t: TFnType) => {
export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps) {
const { t } = useTranslate();
- const [languages, setLanguages] = useState(project.languages);
+ const [languages, setLanguages] = useState(project.languages);
const [isEditing, setIsEditing] = useState(false);
const [confirmationModal, setConfirmationModal] = useState({
isOpen: false,
@@ -85,7 +86,14 @@ export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps)
}, [project.languages]);
const handleAddLanguage = () => {
- const newLanguage = { id: "new", createdAt: new Date(), updatedAt: new Date(), code: "", alias: "" };
+ const newLanguage = {
+ id: "new",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ code: "",
+ alias: "",
+ projectId: project.id,
+ };
setLanguages((prev) => [...prev, newLanguage]);
setIsEditing(true);
};
@@ -184,7 +192,7 @@ export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps)
language={language}
locale={locale}
onDelete={() => handleDeleteLanguage(language.id)}
- onLanguageChange={(newLanguage: TLanguage) => {
+ onLanguageChange={(newLanguage: Language) => {
const updatedLanguages = [...languages];
updatedLanguages[index] = newLanguage;
setLanguages(updatedLanguages);
diff --git a/apps/web/modules/ee/multi-language-surveys/components/language-row.tsx b/apps/web/modules/ee/multi-language-surveys/components/language-row.tsx
index c2733cbf86..f3da260972 100644
--- a/apps/web/modules/ee/multi-language-surveys/components/language-row.tsx
+++ b/apps/web/modules/ee/multi-language-surveys/components/language-row.tsx
@@ -2,16 +2,16 @@
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
+import { Language } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
-import type { TLanguage } from "@formbricks/types/project";
import { TUserLocale } from "@formbricks/types/user";
import { LanguageSelect } from "./language-select";
interface LanguageRowProps {
- language: TLanguage;
+ language: Language;
isEditing: boolean;
index: number;
- onLanguageChange: (newLanguage: TLanguage) => void;
+ onLanguageChange: (newLanguage: Language) => void;
onDelete: () => void;
locale: TUserLocale;
}
diff --git a/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx b/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx
index 257edf600a..e0d558f542 100644
--- a/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx
+++ b/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx
@@ -2,18 +2,18 @@
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
+import { Language } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { ChevronDown } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { TIso639Language } from "@formbricks/lib/i18n/utils";
import { iso639Languages } from "@formbricks/lib/i18n/utils";
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
-import type { TLanguage } from "@formbricks/types/project";
import { TUserLocale } from "@formbricks/types/user";
interface LanguageSelectProps {
- language: TLanguage;
- onLanguageChange: (newLanguage: TLanguage) => void;
+ language: Language;
+ onLanguageChange: (newLanguage: Language) => void;
disabled: boolean;
locale: TUserLocale;
}
diff --git a/apps/web/modules/ee/multi-language-surveys/components/language-toggle.tsx b/apps/web/modules/ee/multi-language-surveys/components/language-toggle.tsx
index 193b787b61..885f74b2a2 100644
--- a/apps/web/modules/ee/multi-language-surveys/components/language-toggle.tsx
+++ b/apps/web/modules/ee/multi-language-surveys/components/language-toggle.tsx
@@ -2,13 +2,13 @@
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
+import { Language } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
-import type { TLanguage } from "@formbricks/types/project";
import type { TUserLocale } from "@formbricks/types/user";
interface LanguageToggleProps {
- language: TLanguage;
+ language: Language;
isChecked: boolean;
onToggle: () => void;
onEdit: () => void;
diff --git a/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx b/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx
index e7f238fa28..942cb4feb1 100644
--- a/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx
+++ b/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx
@@ -7,6 +7,7 @@ import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { Language } from "@prisma/client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { ArrowUpRight, Languages } from "lucide-react";
@@ -15,7 +16,6 @@ import type { FC } from "react";
import { useEffect, useMemo, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { addMultiLanguageLabels, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
-import type { TLanguage, TProject } from "@formbricks/types/project";
import type { TSurvey, TSurveyLanguage, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { DefaultLanguageSelect } from "./default-language-select";
@@ -23,7 +23,7 @@ import { SecondaryLanguageSelect } from "./secondary-language-select";
interface MultiLanguageCardProps {
localSurvey: TSurvey;
- project: TProject;
+ projectLanguages: Language[];
setLocalSurvey: (survey: TSurvey) => void;
activeQuestionId: TSurveyQuestionId | null;
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
@@ -44,10 +44,10 @@ export interface ConfirmationModalProps {
export const MultiLanguageCard: FC = ({
activeQuestionId,
- project,
localSurvey,
setActiveQuestionId,
setLocalSurvey,
+ projectLanguages,
isMultiLanguageAllowed,
isFormbricksCloud,
setSelectedLanguageCode,
@@ -91,7 +91,7 @@ export const MultiLanguageCard: FC = ({
setLocalSurvey(updatedSurvey as TSurvey);
};
- const updateSurveyLanguages = (language: TLanguage) => {
+ const updateSurveyLanguages = (language: Language) => {
let updatedLanguages = localSurvey.languages;
const languageIndex = localSurvey.languages.findIndex(
(surveyLanguage) => surveyLanguage.language.code === language.code
@@ -120,7 +120,7 @@ export const MultiLanguageCard: FC = ({
};
const handleDefaultLanguageChange = (languageCode: string) => {
- const language = project.languages.find((lang) => lang.code === languageCode);
+ const language = projectLanguages.find((lang) => lang.code === languageCode);
if (language) {
let languageExists = false;
@@ -213,7 +213,7 @@ export const MultiLanguageCard: FC = ({
{
handleActivationSwitchLogic();
@@ -245,16 +245,16 @@ export const MultiLanguageCard: FC = ({
/>
) : (
<>
- {project.languages.length <= 1 && (
+ {projectLanguages.length <= 1 && (
- {project.languages.length === 0
+ {projectLanguages.length === 0
? t("environments.surveys.edit.no_languages_found_add_first_one_to_get_started")
: t(
"environments.surveys.edit.you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations"
)}
)}
- {project.languages.length > 1 && (
+ {projectLanguages.length > 1 && (
{isMultiLanguageAllowed && !isMultiLanguageActivated ? (
@@ -269,7 +269,7 @@ export const MultiLanguageCard: FC
= ({
@@ -277,7 +277,7 @@ export const MultiLanguageCard: FC = ({
void;
setActiveQuestionId: (questionId: TSurveyQuestionId) => void;
localSurvey: TSurvey;
- updateSurveyLanguages: (language: TLanguage) => void;
+ updateSurveyLanguages: (language: Language) => void;
locale: TUserLocale;
}
export function SecondaryLanguageSelect({
- project,
+ projectLanguages,
defaultLanguage,
setSelectedLanguageCode,
setActiveQuestionId,
@@ -26,7 +26,7 @@ export function SecondaryLanguageSelect({
locale,
}: SecondaryLanguageSelectProps) {
const { t } = useTranslate();
- const isLanguageToggled = (language: TLanguage) => {
+ const isLanguageToggled = (language: Language) => {
return localSurvey.languages.some(
(surveyLanguage) => surveyLanguage.language.code === language.code && surveyLanguage.enabled
);
@@ -37,7 +37,7 @@ export function SecondaryLanguageSelect({
{t("environments.surveys.edit.2_activate_translation_for_specific_languages")}:
- {project.languages
+ {projectLanguages
.filter((lang) => lang.id !== defaultLanguage.id)
.map((language) => (
{
throw new ResourceNotFoundError("Organization", organizationId);
}
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
if (!isMultiLanguageAllowed) {
throw new OperationNotAllowedError("Multi language is not allowed for this organization");
diff --git a/apps/web/modules/ee/role-management/actions.ts b/apps/web/modules/ee/role-management/actions.ts
index 8142652064..1586680dc4 100644
--- a/apps/web/modules/ee/role-management/actions.ts
+++ b/apps/web/modules/ee/role-management/actions.ts
@@ -19,7 +19,7 @@ export const checkRoleManagementPermission = async (organizationId: string) => {
throw new Error("Organization not found");
}
- const isRoleManagementAllowed = await getRoleManagementPermission(organization);
+ const isRoleManagementAllowed = await getRoleManagementPermission(organization.billing.plan);
if (!isRoleManagementAllowed) {
throw new OperationNotAllowedError("Role management is not allowed for this organization");
}
diff --git a/apps/web/modules/ee/teams/project-teams/page.tsx b/apps/web/modules/ee/teams/project-teams/page.tsx
index 3a6ea60a4b..4efeaff0b3 100644
--- a/apps/web/modules/ee/teams/project-teams/page.tsx
+++ b/apps/web/modules/ee/teams/project-teams/page.tsx
@@ -37,8 +37,8 @@ export const ProjectTeams = async (props: { params: Promise<{ environmentId: str
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isOwner, isManager } = getAccessFlags(currentUserMembership?.role);
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
const teams = await getTeamsByProjectId(project.id);
diff --git a/apps/web/modules/ee/whitelabel/email-customization/actions.ts b/apps/web/modules/ee/whitelabel/email-customization/actions.ts
index 18e1e2befe..67abc9517a 100644
--- a/apps/web/modules/ee/whitelabel/email-customization/actions.ts
+++ b/apps/web/modules/ee/whitelabel/email-customization/actions.ts
@@ -20,7 +20,7 @@ export const checkWhiteLabelPermission = async (organizationId: string) => {
throw new ResourceNotFoundError("Organization", organizationId);
}
- const isWhiteLabelAllowed = await getWhiteLabelPermission(organization);
+ const isWhiteLabelAllowed = await getWhiteLabelPermission(organization.billing.plan);
if (!isWhiteLabelAllowed) {
throw new OperationNotAllowedError("White label is not allowed for this organization");
diff --git a/apps/web/modules/ee/whitelabel/remove-branding/actions.ts b/apps/web/modules/ee/whitelabel/remove-branding/actions.ts
index 117e4c97b7..4786a310da 100644
--- a/apps/web/modules/ee/whitelabel/remove-branding/actions.ts
+++ b/apps/web/modules/ee/whitelabel/remove-branding/actions.ts
@@ -46,7 +46,7 @@ export const updateProjectBrandingAction = authenticatedActionClient
if (!organization) {
throw new Error("Organization not found");
}
- const canRemoveBranding = await getRemoveBrandingPermission(organization);
+ const canRemoveBranding = await getRemoveBrandingPermission(organization.billing.plan);
if (parsedInput.data.inAppSurveyBranding !== undefined) {
if (!canRemoveBranding) {
diff --git a/apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx b/apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx
index f30943b94f..5b83bf313f 100644
--- a/apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx
+++ b/apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx
@@ -3,12 +3,12 @@ import { EditBranding } from "@/modules/ee/whitelabel/remove-branding/components
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { ModalButton, UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { getTranslate } from "@/tolgee/server";
+import { Project } from "@prisma/client";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
-import { TProject } from "@formbricks/types/project";
interface BrandingSettingsCardProps {
canRemoveBranding: boolean;
- project: TProject;
+ project: Project;
environmentId: string;
isReadOnly: boolean;
}
diff --git a/apps/web/modules/organization/settings/teams/page.tsx b/apps/web/modules/organization/settings/teams/page.tsx
index dcf78db780..7ca9c69c58 100644
--- a/apps/web/modules/organization/settings/teams/page.tsx
+++ b/apps/web/modules/organization/settings/teams/page.tsx
@@ -24,7 +24,7 @@ export const TeamsPage = async (props) => {
throw new Error(t("common.organization_not_found"));
}
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
return (
diff --git a/apps/web/modules/projects/settings/(setup)/app-connection/page.tsx b/apps/web/modules/projects/settings/(setup)/app-connection/page.tsx
index 0ec6a16116..037d7dbfca 100644
--- a/apps/web/modules/projects/settings/(setup)/app-connection/page.tsx
+++ b/apps/web/modules/projects/settings/(setup)/app-connection/page.tsx
@@ -31,8 +31,8 @@ export const AppConnectionPage = async (props) => {
throw new Error(t("common.organization_not_found"));
}
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
return (
diff --git a/apps/web/modules/projects/settings/actions.ts b/apps/web/modules/projects/settings/actions.ts
index e0215b4554..d8de2e775b 100644
--- a/apps/web/modules/projects/settings/actions.ts
+++ b/apps/web/modules/projects/settings/actions.ts
@@ -49,7 +49,7 @@ export const updateProjectAction = authenticatedActionClient
throw new Error("Organization not found");
}
- const canRemoveBranding = await getRemoveBrandingPermission(organization);
+ const canRemoveBranding = await getRemoveBrandingPermission(organization.billing.plan);
if (parsedInput.data.inAppSurveyBranding !== undefined) {
if (!canRemoveBranding) {
diff --git a/apps/web/modules/projects/settings/api-keys/page.tsx b/apps/web/modules/projects/settings/api-keys/page.tsx
index 0e749bbc65..39f94366ca 100644
--- a/apps/web/modules/projects/settings/api-keys/page.tsx
+++ b/apps/web/modules/projects/settings/api-keys/page.tsx
@@ -53,8 +53,8 @@ export const APIKeysPage = async (props) => {
const isReadOnly = isMember && !hasManageAccess;
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
return (
diff --git a/apps/web/modules/projects/settings/general/page.tsx b/apps/web/modules/projects/settings/general/page.tsx
index 9159c2caca..8f765a9505 100644
--- a/apps/web/modules/projects/settings/general/page.tsx
+++ b/apps/web/modules/projects/settings/general/page.tsx
@@ -51,8 +51,8 @@ export const GeneralSettingsPage = async (props: { params: Promise<{ environment
const isReadOnly = isMember && !hasManageAccess;
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
const isOwnerOrManager = isOwner || isManager;
diff --git a/apps/web/modules/projects/settings/look/components/edit-logo.tsx b/apps/web/modules/projects/settings/look/components/edit-logo.tsx
index 5bc80412cf..6a70fce3c9 100644
--- a/apps/web/modules/projects/settings/look/components/edit-logo.tsx
+++ b/apps/web/modules/projects/settings/look/components/edit-logo.tsx
@@ -10,14 +10,14 @@ import { ColorPicker } from "@/modules/ui/components/color-picker";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { FileInput } from "@/modules/ui/components/file-input";
import { Input } from "@/modules/ui/components/input";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import Image from "next/image";
import { ChangeEvent, useRef, useState } from "react";
import toast from "react-hot-toast";
-import { TProject, TProjectUpdateInput } from "@formbricks/types/project";
interface EditLogoProps {
- project: TProject;
+ project: Project;
environmentId: string;
isReadOnly: boolean;
}
@@ -62,7 +62,7 @@ export const EditLogo = ({ project, environmentId, isReadOnly }: EditLogoProps)
setIsLoading(true);
try {
- const updatedProject: TProjectUpdateInput = {
+ const updatedProject: Project["logo"] = {
logo: { url: logoUrl, bgColor: isBgColorEnabled ? logoBgColor : undefined },
};
const updateProjectResponse = await updateProjectAction({
@@ -92,7 +92,7 @@ export const EditLogo = ({ project, environmentId, isReadOnly }: EditLogoProps)
setIsLoading(true);
try {
- const updatedProject: TProjectUpdateInput = {
+ const updatedProject: Project["logo"] = {
logo: { url: undefined, bgColor: undefined },
};
const updateProjectResponse = await updateProjectAction({
diff --git a/apps/web/modules/projects/settings/look/components/edit-placement-form.tsx b/apps/web/modules/projects/settings/look/components/edit-placement-form.tsx
index 217918a834..d489fb53e7 100644
--- a/apps/web/modules/projects/settings/look/components/edit-placement-form.tsx
+++ b/apps/web/modules/projects/settings/look/components/edit-placement-form.tsx
@@ -9,12 +9,12 @@ import { Label } from "@/modules/ui/components/label";
import { getPlacementStyle } from "@/modules/ui/components/preview-survey/lib/utils";
import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group";
import { zodResolver } from "@hookform/resolvers/zod";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import { cn } from "@formbricks/lib/cn";
-import { TProject } from "@formbricks/types/project";
const placements = [
{ name: "common.bottom_right", value: "bottomRight", disabled: false },
@@ -25,7 +25,7 @@ const placements = [
];
interface EditPlacementProps {
- project: TProject;
+ project: Project;
environmentId: string;
isReadOnly: boolean;
}
diff --git a/apps/web/modules/projects/settings/look/components/theme-styling.tsx b/apps/web/modules/projects/settings/look/components/theme-styling.tsx
index 9539a907d9..7b07225cd0 100644
--- a/apps/web/modules/projects/settings/look/components/theme-styling.tsx
+++ b/apps/web/modules/projects/settings/look/components/theme-styling.tsx
@@ -1,15 +1,14 @@
"use client";
-import { BackgroundStylingCard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/BackgroundStylingCard";
-import { CardStylingSettings } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CardStylingSettings";
-import { FormStylingSettings } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings";
import { previewSurvey } from "@/app/lib/templates";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { updateProjectAction } from "@/modules/projects/settings/actions";
-import { ThemeStylingPreviewSurvey } from "@/modules/projects/settings/look/components/theme-styling-preview-survey";
+import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
+import { BackgroundStylingCard } from "@/modules/ui/components/background-styling-card";
import { Button } from "@/modules/ui/components/button";
+import { CardStylingSettings } from "@/modules/ui/components/card-styling-settings";
import {
FormControl,
FormDescription,
@@ -19,7 +18,9 @@ import {
FormProvider,
} from "@/modules/ui/components/form";
import { Switch } from "@/modules/ui/components/switch";
+import { ThemeStylingPreviewSurvey } from "@/modules/ui/components/theme-styling-preview-survey";
import { zodResolver } from "@hookform/resolvers/zod";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { RotateCcwIcon } from "lucide-react";
import { useRouter } from "next/navigation";
@@ -27,11 +28,11 @@ import { useCallback, useState } from "react";
import { SubmitHandler, UseFormReturn, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { defaultStyling } from "@formbricks/lib/styling/constants";
-import { TProject, TProjectStyling, ZProjectStyling } from "@formbricks/types/project";
+import { TProjectStyling, ZProjectStyling } from "@formbricks/types/project";
import { TSurvey, TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
interface ThemeStylingProps {
- project: TProject;
+ project: Project;
environmentId: string;
colors: string[];
isUnsplashConfigured: boolean;
diff --git a/apps/web/modules/projects/settings/look/lib/project.ts b/apps/web/modules/projects/settings/look/lib/project.ts
new file mode 100644
index 0000000000..7384edfe56
--- /dev/null
+++ b/apps/web/modules/projects/settings/look/lib/project.ts
@@ -0,0 +1,43 @@
+import { Prisma, Project } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { z } from "zod";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { projectCache } from "@formbricks/lib/project/cache";
+import { validateInputs } from "@formbricks/lib/utils/validate";
+import { DatabaseError } from "@formbricks/types/errors";
+
+export const getProjectByEnvironmentId = reactCache(
+ async (environmentId: string): Promise =>
+ cache(
+ async () => {
+ validateInputs([environmentId, z.string().cuid2()]);
+
+ let projectPrisma;
+
+ try {
+ projectPrisma = await prisma.project.findFirst({
+ where: {
+ environments: {
+ some: {
+ id: environmentId,
+ },
+ },
+ },
+ });
+
+ return projectPrisma;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+ },
+ [`project-settings-look-getProjectByEnvironmentId-${environmentId}`],
+ {
+ tags: [projectCache.tag.byEnvironmentId(environmentId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/projects/settings/look/page.tsx b/apps/web/modules/projects/settings/look/page.tsx
index 036dce8703..723f0c2e5b 100644
--- a/apps/web/modules/projects/settings/look/page.tsx
+++ b/apps/web/modules/projects/settings/look/page.tsx
@@ -10,6 +10,7 @@ import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { BrandingSettingsCard } from "@/modules/ee/whitelabel/remove-branding/components/branding-settings-card";
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
import { EditLogo } from "@/modules/projects/settings/look/components/edit-logo";
+import { getProjectByEnvironmentId } from "@/modules/projects/settings/look/lib/project";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
@@ -19,7 +20,6 @@ import { SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
-import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { EditPlacementForm } from "./components/edit-placement-form";
import { ThemeStyling } from "./components/theme-styling";
@@ -41,7 +41,7 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
- const canRemoveBranding = await getWhiteLabelPermission(organization);
+ const canRemoveBranding = await getWhiteLabelPermission(organization.billing.plan);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
@@ -51,8 +51,8 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
const isReadOnly = isMember && !hasManageAccess;
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
return (
diff --git a/apps/web/modules/projects/settings/tags/page.tsx b/apps/web/modules/projects/settings/tags/page.tsx
index 80d9150ea7..6dad92e541 100644
--- a/apps/web/modules/projects/settings/tags/page.tsx
+++ b/apps/web/modules/projects/settings/tags/page.tsx
@@ -59,8 +59,8 @@ export const TagsPage = async (props) => {
const isReadOnly = isMember && !hasManageAccess;
- const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
- const canDoRoleManagement = await getRoleManagementPermission(organization);
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
+ const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
return (
diff --git a/apps/web/modules/survey-follow-ups/lib/utils.ts b/apps/web/modules/survey-follow-ups/lib/utils.ts
deleted file mode 100644
index e316f827ce..0000000000
--- a/apps/web/modules/survey-follow-ups/lib/utils.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { IS_FORMBRICKS_CLOUD, PROJECT_FEATURE_KEYS } from "@formbricks/lib/constants";
-import { TOrganization } from "@formbricks/types/organizations";
-
-export const getSurveyFollowUpsPermission = async (organization: TOrganization): Promise => {
- if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE;
- return true;
-};
diff --git a/apps/web/modules/surveys/components/QuestionFormInput/components/FallbackInput.tsx b/apps/web/modules/survey/components/question-form-input/components/fallback-input.tsx
similarity index 100%
rename from apps/web/modules/surveys/components/QuestionFormInput/components/FallbackInput.tsx
rename to apps/web/modules/survey/components/question-form-input/components/fallback-input.tsx
diff --git a/apps/web/modules/surveys/components/QuestionFormInput/components/MultiLangWrapper.tsx b/apps/web/modules/survey/components/question-form-input/components/multi-lang-wrapper.tsx
similarity index 100%
rename from apps/web/modules/surveys/components/QuestionFormInput/components/MultiLangWrapper.tsx
rename to apps/web/modules/survey/components/question-form-input/components/multi-lang-wrapper.tsx
diff --git a/apps/web/modules/surveys/components/QuestionFormInput/components/RecallItemSelect.tsx b/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx
similarity index 100%
rename from apps/web/modules/surveys/components/QuestionFormInput/components/RecallItemSelect.tsx
rename to apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx
diff --git a/apps/web/modules/surveys/components/QuestionFormInput/components/RecallWrapper.tsx b/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.tsx
similarity index 97%
rename from apps/web/modules/surveys/components/QuestionFormInput/components/RecallWrapper.tsx
rename to apps/web/modules/survey/components/question-form-input/components/recall-wrapper.tsx
index 33b087d680..b95a54f98d 100644
--- a/apps/web/modules/surveys/components/QuestionFormInput/components/RecallWrapper.tsx
+++ b/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.tsx
@@ -1,7 +1,7 @@
"use client";
-import { FallbackInput } from "@/modules/surveys/components/QuestionFormInput/components/FallbackInput";
-import { RecallItemSelect } from "@/modules/surveys/components/QuestionFormInput/components/RecallItemSelect";
+import { FallbackInput } from "@/modules/survey/components/question-form-input/components/fallback-input";
+import { RecallItemSelect } from "@/modules/survey/components/question-form-input/components/recall-item-select";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import { PencilIcon } from "lucide-react";
diff --git a/apps/web/modules/surveys/components/QuestionFormInput/index.tsx b/apps/web/modules/survey/components/question-form-input/index.tsx
similarity index 98%
rename from apps/web/modules/surveys/components/QuestionFormInput/index.tsx
rename to apps/web/modules/survey/components/question-form-input/index.tsx
index c9a77dfba6..da896f4082 100644
--- a/apps/web/modules/surveys/components/QuestionFormInput/index.tsx
+++ b/apps/web/modules/survey/components/question-form-input/index.tsx
@@ -1,7 +1,7 @@
"use client";
-import { MultiLangWrapper } from "@/modules/surveys/components/QuestionFormInput/components/MultiLangWrapper";
-import { RecallWrapper } from "@/modules/surveys/components/QuestionFormInput/components/RecallWrapper";
+import { MultiLangWrapper } from "@/modules/survey/components/question-form-input/components/multi-lang-wrapper";
+import { RecallWrapper } from "@/modules/survey/components/question-form-input/components/recall-wrapper";
import { Button } from "@/modules/ui/components/button";
import { FileInput } from "@/modules/ui/components/file-input";
import { Input } from "@/modules/ui/components/input";
diff --git a/apps/web/modules/surveys/components/QuestionFormInput/utils.ts b/apps/web/modules/survey/components/question-form-input/utils.ts
similarity index 100%
rename from apps/web/modules/surveys/components/QuestionFormInput/utils.ts
rename to apps/web/modules/survey/components/question-form-input/utils.ts
diff --git a/apps/web/modules/surveys/components/TemplateList/actions.ts b/apps/web/modules/survey/components/template-list/actions.ts
similarity index 85%
rename from apps/web/modules/surveys/components/TemplateList/actions.ts
rename to apps/web/modules/survey/components/template-list/actions.ts
index 351ea64171..dfac66b05b 100644
--- a/apps/web/modules/surveys/components/TemplateList/actions.ts
+++ b/apps/web/modules/survey/components/template-list/actions.ts
@@ -4,16 +4,15 @@ import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions";
-import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
+import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
+import { createSurvey } from "@/modules/survey/components/template-list/lib/survey";
+import { getOrganizationBilling } from "@/modules/survey/lib/survey";
import { z } from "zod";
-import { getOrganization } from "@formbricks/lib/organization/service";
-import { createSurvey } from "@formbricks/lib/survey/service";
-import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZSurveyCreateInput } from "@formbricks/types/surveys/types";
const ZCreateSurveyAction = z.object({
- environmentId: ZId,
+ environmentId: z.string().cuid2(),
surveyBody: ZSurveyCreateInput,
});
@@ -26,12 +25,12 @@ const ZCreateSurveyAction = z.object({
* @throws { OperationNotAllowedError } If survey follow-ups are not enabled for the organization.
*/
const checkSurveyFollowUpsPermission = async (organizationId: string): Promise => {
- const organization = await getOrganization(organizationId);
- if (!organization) {
+ const organizationBilling = await getOrganizationBilling(organizationId);
+ if (!organizationBilling) {
throw new ResourceNotFoundError("Organization not found", organizationId);
}
- const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organization);
+ const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organizationBilling.plan);
if (!isSurveyFollowUpsEnabled) {
throw new OperationNotAllowedError("Survey follow ups are not enabled for this organization");
}
diff --git a/apps/web/modules/surveys/components/TemplateList/components/StartFromScratchTemplate.tsx b/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.tsx
similarity index 96%
rename from apps/web/modules/surveys/components/TemplateList/components/StartFromScratchTemplate.tsx
rename to apps/web/modules/survey/components/template-list/components/start-from-scratch-template.tsx
index febe890a44..7f07d695a1 100644
--- a/apps/web/modules/surveys/components/TemplateList/components/StartFromScratchTemplate.tsx
+++ b/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.tsx
@@ -2,10 +2,10 @@
import { customSurveyTemplate } from "@/app/lib/templates";
import { Button } from "@/modules/ui/components/button";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { PlusCircleIcon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
-import { TProject } from "@formbricks/types/project";
import { TTemplate } from "@formbricks/types/templates";
import { replacePresetPlaceholders } from "../lib/utils";
@@ -13,7 +13,7 @@ interface StartFromScratchTemplateProps {
activeTemplate: TTemplate | null;
setActiveTemplate: (template: TTemplate) => void;
onTemplateClick: (template: TTemplate) => void;
- project: TProject;
+ project: Project;
createSurvey: (template: TTemplate) => void;
loading: boolean;
noPreview?: boolean;
diff --git a/apps/web/modules/surveys/components/TemplateList/components/TemplateFilters.tsx b/apps/web/modules/survey/components/template-list/components/template-filters.tsx
similarity index 100%
rename from apps/web/modules/surveys/components/TemplateList/components/TemplateFilters.tsx
rename to apps/web/modules/survey/components/template-list/components/template-filters.tsx
diff --git a/apps/web/modules/surveys/components/TemplateList/components/TemplateTags.tsx b/apps/web/modules/survey/components/template-list/components/template-tags.tsx
similarity index 100%
rename from apps/web/modules/surveys/components/TemplateList/components/TemplateTags.tsx
rename to apps/web/modules/survey/components/template-list/components/template-tags.tsx
diff --git a/apps/web/modules/surveys/components/TemplateList/components/Template.tsx b/apps/web/modules/survey/components/template-list/components/template.tsx
similarity index 94%
rename from apps/web/modules/surveys/components/TemplateList/components/Template.tsx
rename to apps/web/modules/survey/components/template-list/components/template.tsx
index 3ca60693d5..0064bde376 100644
--- a/apps/web/modules/surveys/components/TemplateList/components/Template.tsx
+++ b/apps/web/modules/survey/components/template-list/components/template.tsx
@@ -1,19 +1,19 @@
"use client";
import { Button } from "@/modules/ui/components/button";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { cn } from "@formbricks/lib/cn";
-import { TProject } from "@formbricks/types/project";
import { TTemplate, TTemplateFilter } from "@formbricks/types/templates";
import { replacePresetPlaceholders } from "../lib/utils";
-import { TemplateTags } from "./TemplateTags";
+import { TemplateTags } from "./template-tags";
interface TemplateProps {
template: TTemplate;
activeTemplate: TTemplate | null;
setActiveTemplate: (template: TTemplate) => void;
onTemplateClick?: (template: TTemplate) => void;
- project: TProject;
+ project: Project;
createSurvey: (template: TTemplate) => void;
loading: boolean;
selectedFilter: TTemplateFilter[];
diff --git a/apps/web/modules/surveys/components/TemplateList/index.tsx b/apps/web/modules/survey/components/template-list/index.tsx
similarity index 87%
rename from apps/web/modules/surveys/components/TemplateList/index.tsx
rename to apps/web/modules/survey/components/template-list/index.tsx
index a60f52b74c..4cfcc63c9d 100644
--- a/apps/web/modules/surveys/components/TemplateList/index.tsx
+++ b/apps/web/modules/survey/components/template-list/index.tsx
@@ -2,24 +2,23 @@
import { templates } from "@/app/lib/templates";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
-import type { TEnvironment } from "@formbricks/types/environment";
-import { type TProject, ZProjectConfigChannel, ZProjectConfigIndustry } from "@formbricks/types/project";
+import { ZProjectConfigChannel, ZProjectConfigIndustry } from "@formbricks/types/project";
import { TSurveyCreateInput, TSurveyType } from "@formbricks/types/surveys/types";
import { TTemplate, TTemplateFilter, ZTemplateRole } from "@formbricks/types/templates";
-import { TUser } from "@formbricks/types/user";
import { createSurveyAction } from "./actions";
-import { StartFromScratchTemplate } from "./components/StartFromScratchTemplate";
-import { Template } from "./components/Template";
-import { TemplateFilters } from "./components/TemplateFilters";
+import { StartFromScratchTemplate } from "./components/start-from-scratch-template";
+import { Template } from "./components/template";
+import { TemplateFilters } from "./components/template-filters";
interface TemplateListProps {
- user: TUser;
- environment: TEnvironment;
- project: TProject;
+ userId: string;
+ environmentId: string;
+ project: Project;
templateSearch?: string;
showFilters?: boolean;
prefilledFilters: TTemplateFilter[];
@@ -28,9 +27,9 @@ interface TemplateListProps {
}
export const TemplateList = ({
- user,
+ userId,
project,
- environment,
+ environmentId,
showFilters = true,
templateSearch,
prefilledFilters,
@@ -59,15 +58,15 @@ export const TemplateList = ({
const augmentedTemplate: TSurveyCreateInput = {
...activeTemplate.preset,
type: surveyType,
- createdBy: user.id,
+ createdBy: userId,
};
const createSurveyResponse = await createSurveyAction({
- environmentId: environment.id,
+ environmentId: environmentId,
surveyBody: augmentedTemplate,
});
if (createSurveyResponse?.data) {
- router.push(`/environments/${environment.id}/surveys/${createSurveyResponse.data.id}/edit`);
+ router.push(`/environments/${environmentId}/surveys/${createSurveyResponse.data.id}/edit`);
} else {
const errorMessage = getFormattedErrorMessage(createSurveyResponse);
toast.error(errorMessage);
diff --git a/apps/web/modules/survey/components/template-list/lib/organization.ts b/apps/web/modules/survey/components/template-list/lib/organization.ts
new file mode 100644
index 0000000000..452a30f3ab
--- /dev/null
+++ b/apps/web/modules/survey/components/template-list/lib/organization.ts
@@ -0,0 +1,35 @@
+import { updateUser } from "@/modules/survey/components/template-list/lib/user";
+import { prisma } from "@formbricks/database";
+import { ResourceNotFoundError } from "@formbricks/types/errors";
+import { TUserNotificationSettings } from "@formbricks/types/user";
+
+export const subscribeOrganizationMembersToSurveyResponses = async (
+ surveyId: string,
+ createdBy: string
+): Promise => {
+ try {
+ const surveyCreator = await prisma.user.findUnique({
+ where: {
+ id: createdBy,
+ },
+ });
+
+ if (!surveyCreator) {
+ throw new ResourceNotFoundError("User", createdBy);
+ }
+
+ const defaultSettings = { alert: {}, weeklySummary: {} };
+ const updatedNotificationSettings: TUserNotificationSettings = {
+ ...defaultSettings,
+ ...surveyCreator.notificationSettings,
+ };
+
+ updatedNotificationSettings.alert[surveyId] = true;
+
+ await updateUser(surveyCreator.id, {
+ notificationSettings: updatedNotificationSettings,
+ });
+ } catch (error) {
+ throw error;
+ }
+};
diff --git a/apps/web/modules/survey/components/template-list/lib/survey.ts b/apps/web/modules/survey/components/template-list/lib/survey.ts
new file mode 100644
index 0000000000..cd40045c90
--- /dev/null
+++ b/apps/web/modules/survey/components/template-list/lib/survey.ts
@@ -0,0 +1,185 @@
+import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
+import { subscribeOrganizationMembersToSurveyResponses } from "@/modules/survey/components/template-list/lib/organization";
+import { handleTriggerUpdates } from "@/modules/survey/components/template-list/lib/utils";
+import { getActionClasses } from "@/modules/survey/lib/action-class";
+import { getOrganizationAIKeys, getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization";
+import { selectSurvey } from "@/modules/survey/lib/survey";
+import { getInsightsEnabled } from "@/modules/survey/lib/utils";
+import { doesSurveyHasOpenTextQuestion } from "@/modules/survey/lib/utils";
+import { Prisma } from "@prisma/client";
+import { prisma } from "@formbricks/database";
+import { segmentCache } from "@formbricks/lib/cache/segment";
+import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+import { TSurvey } from "@formbricks/types/surveys/types";
+import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
+
+export const createSurvey = async (
+ environmentId: string,
+ surveyBody: TSurveyCreateInput
+): Promise => {
+ try {
+ const { createdBy, ...restSurveyBody } = surveyBody;
+
+ // empty languages array
+ if (!restSurveyBody.languages?.length) {
+ delete restSurveyBody.languages;
+ }
+
+ const actionClasses = await getActionClasses(environmentId);
+
+ // @ts-expect-error
+ let data: Omit = {
+ ...restSurveyBody,
+ // TODO: Create with attributeFilters
+ triggers: restSurveyBody.triggers
+ ? handleTriggerUpdates(restSurveyBody.triggers, [], actionClasses)
+ : undefined,
+ attributeFilters: undefined,
+ };
+
+ if (createdBy) {
+ data.creator = {
+ connect: {
+ id: createdBy,
+ },
+ };
+ }
+
+ const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
+ const organization = await getOrganizationAIKeys(organizationId);
+ if (!organization) {
+ throw new ResourceNotFoundError("Organization", null);
+ }
+
+ //AI Insights
+ const isAIEnabled = await getIsAIEnabled(organization);
+ if (isAIEnabled) {
+ if (doesSurveyHasOpenTextQuestion(data.questions ?? [])) {
+ const openTextQuestions = data.questions?.filter((question) => question.type === "openText") ?? [];
+ const insightsEnabledValues = await Promise.all(
+ openTextQuestions.map(async (question) => {
+ const insightsEnabled = await getInsightsEnabled(question);
+
+ return { id: question.id, insightsEnabled };
+ })
+ );
+
+ data.questions = data.questions?.map((question) => {
+ const index = insightsEnabledValues.findIndex((item) => item.id === question.id);
+ if (index !== -1) {
+ return {
+ ...question,
+ insightsEnabled: insightsEnabledValues[index].insightsEnabled,
+ };
+ }
+
+ return question;
+ });
+ }
+ }
+
+ // Survey follow-ups
+ if (restSurveyBody.followUps?.length) {
+ data.followUps = {
+ create: restSurveyBody.followUps.map((followUp) => ({
+ name: followUp.name,
+ trigger: followUp.trigger,
+ action: followUp.action,
+ })),
+ };
+ } else {
+ delete data.followUps;
+ }
+
+ const survey = await prisma.survey.create({
+ data: {
+ ...data,
+ environment: {
+ connect: {
+ id: environmentId,
+ },
+ },
+ },
+ select: selectSurvey,
+ });
+
+ // if the survey created is an "app" survey, we also create a private segment for it.
+ if (survey.type === "app") {
+ // const newSegment = await createSegment({
+ // environmentId: parsedEnvironmentId,
+ // surveyId: survey.id,
+ // filters: [],
+ // title: survey.id,
+ // isPrivate: true,
+ // });
+
+ const newSegment = await prisma.segment.create({
+ data: {
+ title: survey.id,
+ filters: [],
+ isPrivate: true,
+ environment: {
+ connect: {
+ id: environmentId,
+ },
+ },
+ },
+ });
+
+ await prisma.survey.update({
+ where: {
+ id: survey.id,
+ },
+ data: {
+ segment: {
+ connect: {
+ id: newSegment.id,
+ },
+ },
+ },
+ });
+
+ segmentCache.revalidate({
+ id: newSegment.id,
+ environmentId: survey.environmentId,
+ });
+ }
+
+ // 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 transformedSurvey: TSurvey = {
+ ...survey,
+ ...(survey.segment && {
+ segment: {
+ ...survey.segment,
+ surveys: survey.segment.surveys.map((survey) => survey.id),
+ },
+ }),
+ };
+
+ surveyCache.revalidate({
+ id: survey.id,
+ environmentId: survey.environmentId,
+ resultShareKey: survey.resultShareKey ?? undefined,
+ });
+
+ if (createdBy) {
+ await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy);
+ }
+
+ await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", {
+ surveyId: survey.id,
+ surveyType: survey.type,
+ });
+
+ return transformedSurvey;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+};
diff --git a/apps/web/modules/survey/components/template-list/lib/user.ts b/apps/web/modules/survey/components/template-list/lib/user.ts
new file mode 100644
index 0000000000..6cf63a4138
--- /dev/null
+++ b/apps/web/modules/survey/components/template-list/lib/user.ts
@@ -0,0 +1,45 @@
+import { Prisma } from "@prisma/client";
+import { prisma } from "@formbricks/database";
+import { userCache } from "@formbricks/lib/user/cache";
+import { ResourceNotFoundError } from "@formbricks/types/errors";
+import { TUser } from "@formbricks/types/user";
+import { TUserUpdateInput } from "@formbricks/types/user";
+
+// function to update a user's user
+export const updateUser = async (personId: string, data: TUserUpdateInput): Promise => {
+ try {
+ const updatedUser = await prisma.user.update({
+ where: {
+ id: personId,
+ },
+ data: data,
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ emailVerified: true,
+ imageUrl: true,
+ createdAt: true,
+ updatedAt: true,
+ role: true,
+ twoFactorEnabled: true,
+ identityProvider: true,
+ objective: true,
+ notificationSettings: true,
+ locale: true,
+ },
+ });
+
+ userCache.revalidate({
+ email: updatedUser.email,
+ id: updatedUser.id,
+ });
+
+ return updatedUser;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
+ throw new ResourceNotFoundError("User", personId);
+ }
+ throw error; // Re-throw any other errors
+ }
+};
diff --git a/apps/web/modules/survey/components/template-list/lib/utils.ts b/apps/web/modules/survey/components/template-list/lib/utils.ts
new file mode 100644
index 0000000000..4f19a09b09
--- /dev/null
+++ b/apps/web/modules/survey/components/template-list/lib/utils.ts
@@ -0,0 +1,128 @@
+import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger";
+import { ActionClass } from "@prisma/client";
+import { TFnType } from "@tolgee/react";
+import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
+import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { InvalidInputError } from "@formbricks/types/errors";
+import { TProject, TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
+import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
+import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
+
+export const replaceQuestionPresetPlaceholders = (
+ question: TSurveyQuestion,
+ project: TProject
+): TSurveyQuestion => {
+ if (!project) return question;
+ const newQuestion = structuredClone(question);
+ const defaultLanguageCode = "default";
+ if (newQuestion.headline) {
+ newQuestion.headline[defaultLanguageCode] = getLocalizedValue(
+ newQuestion.headline,
+ defaultLanguageCode
+ ).replace("$[projectName]", project.name);
+ }
+ if (newQuestion.subheader) {
+ newQuestion.subheader[defaultLanguageCode] = getLocalizedValue(
+ newQuestion.subheader,
+ defaultLanguageCode
+ )?.replace("$[projectName]", project.name);
+ }
+ return newQuestion;
+};
+
+// replace all occurences of projectName with the actual project name in the current template
+export const replacePresetPlaceholders = (template: TTemplate, project: any) => {
+ const preset = structuredClone(template.preset);
+ preset.name = preset.name.replace("$[projectName]", project.name);
+ preset.questions = preset.questions.map((question) => {
+ return replaceQuestionPresetPlaceholders(question, project);
+ });
+ return { ...template, preset };
+};
+
+export const getChannelMapping = (t: TFnType): { value: TProjectConfigChannel; label: string }[] => [
+ { value: "website", label: t("common.website_survey") },
+ { value: "app", label: t("common.app_survey") },
+ { value: "link", label: t("common.link_survey") },
+];
+
+export const getIndustryMapping = (t: TFnType): { value: TProjectConfigIndustry; label: string }[] => [
+ { value: "eCommerce", label: t("common.e_commerce") },
+ { value: "saas", label: t("common.saas") },
+ { value: "other", label: t("common.other") },
+];
+
+export const getRoleMapping = (t: TFnType): { value: TTemplateRole; label: string }[] => [
+ { value: "productManager", label: t("common.product_manager") },
+ { value: "customerSuccess", label: t("common.customer_success") },
+ { value: "marketing", label: t("common.marketing") },
+ { value: "sales", label: t("common.sales") },
+ { value: "peopleManager", label: t("common.people_manager") },
+];
+
+const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => {
+ if (!triggers) return;
+
+ // check if all the triggers are valid
+ triggers.forEach((trigger) => {
+ if (!actionClasses.find((actionClass) => actionClass.id === trigger.actionClass.id)) {
+ throw new InvalidInputError("Invalid trigger id");
+ }
+ });
+
+ // check if all the triggers are unique
+ const triggerIds = triggers.map((trigger) => trigger.actionClass.id);
+
+ if (new Set(triggerIds).size !== triggerIds.length) {
+ throw new InvalidInputError("Duplicate trigger id");
+ }
+};
+
+export const handleTriggerUpdates = (
+ updatedTriggers: TSurvey["triggers"],
+ currentTriggers: TSurvey["triggers"],
+ actionClasses: ActionClass[]
+) => {
+ if (!updatedTriggers) return {};
+ checkTriggersValidity(updatedTriggers, actionClasses);
+
+ const currentTriggerIds = currentTriggers.map((trigger) => trigger.actionClass.id);
+ const updatedTriggerIds = updatedTriggers.map((trigger) => trigger.actionClass.id);
+
+ // added triggers are triggers that are not in the current triggers and are there in the new triggers
+ const addedTriggers = updatedTriggers.filter(
+ (trigger) => !currentTriggerIds.includes(trigger.actionClass.id)
+ );
+
+ // deleted triggers are triggers that are not in the new triggers and are there in the current triggers
+ const deletedTriggers = currentTriggers.filter(
+ (trigger) => !updatedTriggerIds.includes(trigger.actionClass.id)
+ );
+
+ // Construct the triggers update object
+ const triggersUpdate: TriggerUpdate = {};
+
+ if (addedTriggers.length > 0) {
+ triggersUpdate.create = addedTriggers.map((trigger) => ({
+ actionClassId: trigger.actionClass.id,
+ }));
+ }
+
+ if (deletedTriggers.length > 0) {
+ // disconnect the public triggers from the survey
+ triggersUpdate.deleteMany = {
+ actionClassId: {
+ in: deletedTriggers.map((trigger) => trigger.actionClass.id),
+ },
+ };
+ }
+
+ [...addedTriggers, ...deletedTriggers].forEach((trigger) => {
+ surveyCache.revalidate({
+ actionClassId: trigger.actionClass.id,
+ });
+ });
+
+ return triggersUpdate;
+};
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts b/apps/web/modules/survey/editor/actions.ts
similarity index 92%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts
rename to apps/web/modules/survey/editor/actions.ts
index aa6db2a9ff..0f7c098222 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts
+++ b/apps/web/modules/survey/editor/actions.ts
@@ -10,17 +10,16 @@ import {
getProjectIdFromSurveyId,
} from "@/lib/utils/helper";
import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions";
-import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
+import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
+import { updateSurvey } from "@/modules/survey/editor/lib/survey";
+import { getOrganizationBilling } from "@/modules/survey/lib/survey";
import { z } from "zod";
-import { createActionClass } from "@formbricks/lib/actionClass/service";
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@formbricks/lib/constants";
-import { getOrganization } from "@formbricks/lib/organization/service";
-import { getProject } from "@formbricks/lib/project/service";
-import { updateSurvey } from "@formbricks/lib/survey/service";
import { ZActionClassInput } from "@formbricks/types/action-classes";
-import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZSurvey } from "@formbricks/types/surveys/types";
+import { getProject } from "./lib/project";
+import { createActionClass } from "@/modules/survey/editor/lib/action-class";
/**
* Checks if survey follow-ups are enabled for the given organization.
@@ -31,12 +30,12 @@ import { ZSurvey } from "@formbricks/types/surveys/types";
* @throws { OperationNotAllowedError } If survey follow-ups are not enabled for the organization.
*/
const checkSurveyFollowUpsPermission = async (organizationId: string): Promise => {
- const organization = await getOrganization(organizationId);
- if (!organization) {
+ const organizationBilling = await getOrganizationBilling(organizationId);
+ if (!organizationBilling) {
throw new ResourceNotFoundError("Organization", organizationId);
}
- const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organization);
+ const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organizationBilling.plan);
if (!isSurveyFollowUpsEnabled) {
throw new OperationNotAllowedError("Survey follow ups are not enabled for this organization");
}
@@ -74,7 +73,7 @@ export const updateSurveyAction = authenticatedActionClient
});
const ZRefetchProjectAction = z.object({
- projectId: ZId,
+ projectId: z.string().cuid2(),
});
export const refetchProjectAction = authenticatedActionClient
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddActionModal.tsx b/apps/web/modules/survey/editor/components/add-action-modal.tsx
similarity index 83%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddActionModal.tsx
rename to apps/web/modules/survey/editor/components/add-action-modal.tsx
index 480bd0141a..a01c9af00e 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddActionModal.tsx
+++ b/apps/web/modules/survey/editor/components/add-action-modal.tsx
@@ -1,18 +1,18 @@
"use client";
+import { CreateNewActionTab } from "@/modules/survey/editor/components/create-new-action-tab";
+import { SavedActionsTab } from "@/modules/survey/editor/components/saved-actions-tab";
import { ModalWithTabs } from "@/modules/ui/components/modal-with-tabs";
+import { ActionClass } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
-import { TActionClass } from "@formbricks/types/action-classes";
import { TSurvey } from "@formbricks/types/surveys/types";
-import { CreateNewActionTab } from "./CreateNewActionTab";
-import { SavedActionsTab } from "./SavedActionsTab";
interface AddActionModalProps {
open: boolean;
setOpen: React.Dispatch>;
environmentId: string;
- actionClasses: TActionClass[];
- setActionClasses: React.Dispatch>;
+ actionClasses: ActionClass[];
+ setActionClasses: React.Dispatch>;
isReadOnly: boolean;
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch>;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddEndingCardButton.tsx b/apps/web/modules/survey/editor/components/add-ending-card-button.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddEndingCardButton.tsx
rename to apps/web/modules/survey/editor/components/add-ending-card-button.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx b/apps/web/modules/survey/editor/components/add-question-button.tsx
similarity index 97%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx
rename to apps/web/modules/survey/editor/components/add-question-button.tsx
index b5891cdf1f..f0903f035c 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx
+++ b/apps/web/modules/survey/editor/components/add-question-button.tsx
@@ -2,6 +2,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
+import { Project } from "@prisma/client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react";
@@ -13,11 +14,10 @@ import {
getQuestionTypes,
universalQuestionPresets,
} from "@formbricks/lib/utils/questions";
-import { TProject } from "@formbricks/types/project";
interface AddQuestionButtonProps {
addQuestion: (question: any) => void;
- project: TProject;
+ project: Project;
isCxMode: boolean;
}
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddressQuestionForm.tsx b/apps/web/modules/survey/editor/components/address-question-form.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddressQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/address-question-form.tsx
index 969cf9ddb7..770c954f5a 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddressQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/address-question-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Button } from "@/modules/ui/components/button";
import { QuestionToggleTable } from "@/modules/ui/components/question-toggle-table";
import { useAutoAnimate } from "@formkit/auto-animate/react";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedSettings.tsx b/apps/web/modules/survey/editor/components/advanced-settings.tsx
similarity index 80%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedSettings.tsx
rename to apps/web/modules/survey/editor/components/advanced-settings.tsx
index b6d465c2fa..0677305985 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedSettings.tsx
+++ b/apps/web/modules/survey/editor/components/advanced-settings.tsx
@@ -1,6 +1,6 @@
-import { ConditionalLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic";
+import { ConditionalLogic } from "@/modules/survey/editor/components/conditional-logic";
+import { UpdateQuestionId } from "@/modules/survey/editor/components/update-question-id";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
-import { UpdateQuestionId } from "./UpdateQuestionId";
interface AdvancedSettingsProps {
question: TSurveyQuestion;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx b/apps/web/modules/survey/editor/components/animated-survey-bg.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx
rename to apps/web/modules/survey/editor/components/animated-survey-bg.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CalQuestionForm.tsx b/apps/web/modules/survey/editor/components/cal-question-form.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CalQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/cal-question-form.tsx
index 46975452e5..3ec9bd7fc3 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CalQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/cal-question-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx b/apps/web/modules/survey/editor/components/color-survey-bg.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx
rename to apps/web/modules/survey/editor/components/color-survey-bg.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx b/apps/web/modules/survey/editor/components/conditional-logic.tsx
similarity index 96%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx
rename to apps/web/modules/survey/editor/components/conditional-logic.tsx
index 0f5db1b810..4bdf366c25 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx
+++ b/apps/web/modules/survey/editor/components/conditional-logic.tsx
@@ -1,10 +1,10 @@
"use client";
-import { LogicEditor } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor";
+import { LogicEditor } from "@/modules/survey/editor/components/logic-editor";
import {
getDefaultOperatorForQuestion,
replaceEndingCardHeadlineRecall,
-} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
+} from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
import {
DropdownMenu,
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConsentQuestionForm.tsx b/apps/web/modules/survey/editor/components/consent-question-form.tsx
similarity index 96%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConsentQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/consent-question-form.tsx
index 956bada9d0..df4bd7987a 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConsentQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/consent-question-form.tsx
@@ -1,7 +1,7 @@
"use client";
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Label } from "@/modules/ui/components/label";
import { useTranslate } from "@tolgee/react";
import { type JSX, useState } from "react";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ContactInfoQuestionForm.tsx b/apps/web/modules/survey/editor/components/contact-info-question-form.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ContactInfoQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/contact-info-question-form.tsx
index ecd02dca46..f7e8951a5e 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ContactInfoQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/contact-info-question-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Button } from "@/modules/ui/components/button";
import { QuestionToggleTable } from "@/modules/ui/components/question-toggle-table";
import { useAutoAnimate } from "@formkit/auto-animate/react";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CreateNewActionTab.tsx b/apps/web/modules/survey/editor/components/create-new-action-tab.tsx
similarity index 97%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CreateNewActionTab.tsx
rename to apps/web/modules/survey/editor/components/create-new-action-tab.tsx
index 6f3346ab32..dbd7fea9a4 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CreateNewActionTab.tsx
+++ b/apps/web/modules/survey/editor/components/create-new-action-tab.tsx
@@ -9,13 +9,13 @@ import { Label } from "@/modules/ui/components/label";
import { NoCodeActionForm } from "@/modules/ui/components/no-code-action-form";
import { TabToggle } from "@/modules/ui/components/tab-toggle";
import { zodResolver } from "@hookform/resolvers/zod";
+import { ActionClass } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { useMemo } from "react";
import { FormProvider, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import {
- TActionClass,
TActionClassInput,
TActionClassInputCode,
ZActionClassInput,
@@ -24,8 +24,8 @@ import { TSurvey } from "@formbricks/types/surveys/types";
import { createActionClassAction } from "../actions";
interface CreateNewActionTabProps {
- actionClasses: TActionClass[];
- setActionClasses: React.Dispatch>;
+ actionClasses: ActionClass[];
+ setActionClasses: React.Dispatch>;
isReadOnly: boolean;
setLocalSurvey?: React.Dispatch>;
setOpen: React.Dispatch>;
@@ -148,7 +148,7 @@ export const CreateNewActionTab = ({
const newActionClass = createActionClassResposne.data;
if (setActionClasses) {
- setActionClasses((prevActionClasses: TActionClass[]) => [...prevActionClasses, newActionClass]);
+ setActionClasses((prevActionClasses: ActionClass[]) => [...prevActionClasses, newActionClass]);
}
if (setLocalSurvey) {
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CTAQuestionForm.tsx b/apps/web/modules/survey/editor/components/cta-question-form.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CTAQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/cta-question-form.tsx
index 750b068497..7b1a181d05 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CTAQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/cta-question-form.tsx
@@ -1,7 +1,7 @@
"use client";
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/DateQuestionForm.tsx b/apps/web/modules/survey/editor/components/date-question-form.tsx
similarity index 97%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/DateQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/date-question-form.tsx
index 95df179563..04ba6b1e50 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/DateQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/date-question-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditEndingCard.tsx b/apps/web/modules/survey/editor/components/edit-ending-card.tsx
similarity index 94%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditEndingCard.tsx
rename to apps/web/modules/survey/editor/components/edit-ending-card.tsx
index cf97de7029..361665c302 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditEndingCard.tsx
+++ b/apps/web/modules/survey/editor/components/edit-ending-card.tsx
@@ -1,12 +1,9 @@
"use client";
-import { EditorCardMenu } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu";
-import { EndScreenForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm";
-import { RedirectUrlForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RedirectUrlForm";
-import {
- findEndingCardUsedInLogic,
- formatTextWithSlashes,
-} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
+import { EditorCardMenu } from "@/modules/survey/editor/components/editor-card-menu";
+import { EndScreenForm } from "@/modules/survey/editor/components/end-screen-form";
+import { RedirectUrlForm } from "@/modules/survey/editor/components/redirect-url-form";
+import { findEndingCardUsedInLogic, formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx b/apps/web/modules/survey/editor/components/edit-welcome-card.tsx
similarity index 99%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx
rename to apps/web/modules/survey/editor/components/edit-welcome-card.tsx
index 3939a520ab..ebfc08a8b1 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx
+++ b/apps/web/modules/survey/editor/components/edit-welcome-card.tsx
@@ -1,7 +1,7 @@
"use client";
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { FileInput } from "@/modules/ui/components/file-input";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu.tsx b/apps/web/modules/survey/editor/components/editor-card-menu.tsx
similarity index 99%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu.tsx
rename to apps/web/modules/survey/editor/components/editor-card-menu.tsx
index 4712819c96..0a0033d58a 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu.tsx
+++ b/apps/web/modules/survey/editor/components/editor-card-menu.tsx
@@ -13,6 +13,7 @@ import {
} from "@/modules/ui/components/dropdown-menu";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { createId } from "@paralleldrive/cuid2";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, EllipsisIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
@@ -22,7 +23,6 @@ import {
getQuestionIconMap,
getQuestionNameMap,
} from "@formbricks/lib/utils/questions";
-import { TProject } from "@formbricks/types/project";
import {
TSurvey,
TSurveyEndScreenCard,
@@ -42,7 +42,7 @@ interface EditorCardMenuProps {
updateCard: (cardIdx: number, updatedAttributes: any) => void;
addCard: (question: any, index?: number) => void;
cardType: "question" | "ending";
- project?: TProject;
+ project?: Project;
isCxMode?: boolean;
}
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm.tsx b/apps/web/modules/survey/editor/components/end-screen-form.tsx
similarity index 97%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm.tsx
rename to apps/web/modules/survey/editor/components/end-screen-form.tsx
index 4376af093d..7771d93bd4 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm.tsx
+++ b/apps/web/modules/survey/editor/components/end-screen-form.tsx
@@ -1,7 +1,7 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
-import { RecallWrapper } from "@/modules/surveys/components/QuestionFormInput/components/RecallWrapper";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
+import { RecallWrapper } from "@/modules/survey/components/question-form-input/components/recall-wrapper";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FileUploadQuestionForm.tsx b/apps/web/modules/survey/editor/components/file-upload-question-form.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FileUploadQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/file-upload-question-form.tsx
index 8d3dffdc52..c421d779d0 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FileUploadQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/file-upload-question-form.tsx
@@ -1,11 +1,12 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { useGetBillingInfo } from "@/modules/utils/hooks/useGetBillingInfo";
import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { PlusIcon, XCircleIcon } from "lucide-react";
import Link from "next/link";
@@ -14,13 +15,12 @@ import { toast } from "react-hot-toast";
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { createI18nString } from "@formbricks/lib/i18n/utils";
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
-import { TProject } from "@formbricks/types/project";
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
interface FileUploadFormProps {
localSurvey: TSurvey;
- project?: TProject;
+ project?: Project;
question: TSurveyFileUploadQuestion;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial) => void;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings.tsx b/apps/web/modules/survey/editor/components/form-styling-settings.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings.tsx
rename to apps/web/modules/survey/editor/components/form-styling-settings.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx b/apps/web/modules/survey/editor/components/hidden-fields-card.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx
rename to apps/web/modules/survey/editor/components/hidden-fields-card.tsx
index dc1a78be0a..62c390a9cb 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx
+++ b/apps/web/modules/survey/editor/components/hidden-fields-card.tsx
@@ -1,6 +1,6 @@
"use client";
-import { findHiddenFieldUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
+import { findHiddenFieldUsedInLogic } from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HowToSendCard.tsx b/apps/web/modules/survey/editor/components/how-to-send-card.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HowToSendCard.tsx
rename to apps/web/modules/survey/editor/components/how-to-send-card.tsx
index 7b3e9bb559..05e27634fc 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HowToSendCard.tsx
+++ b/apps/web/modules/survey/editor/components/how-to-send-card.tsx
@@ -5,20 +5,20 @@ import { Badge } from "@/modules/ui/components/badge";
import { Label } from "@/modules/ui/components/label";
import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group";
import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { Environment } from "@prisma/client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
-import { TEnvironment } from "@formbricks/types/environment";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys/types";
interface HowToSendCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey | ((TSurvey: TSurvey) => TSurvey)) => void;
- environment: TEnvironment;
+ environment: Pick;
}
export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowToSendCardProps) => {
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ImageSurveyBg.tsx b/apps/web/modules/survey/editor/components/image-survey-bg.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ImageSurveyBg.tsx
rename to apps/web/modules/survey/editor/components/image-survey-bg.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton.tsx b/apps/web/modules/survey/editor/components/loading-skeleton.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton.tsx
rename to apps/web/modules/survey/editor/components/loading-skeleton.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx b/apps/web/modules/survey/editor/components/logic-editor-actions.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx
rename to apps/web/modules/survey/editor/components/logic-editor-actions.tsx
index 580303a435..496013e2ab 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx
+++ b/apps/web/modules/survey/editor/components/logic-editor-actions.tsx
@@ -7,7 +7,7 @@ import {
getActionValueOptions,
getActionVariableOptions,
hasJumpToQuestionAction,
-} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
+} from "@/modules/survey/editor/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx b/apps/web/modules/survey/editor/components/logic-editor-conditions.tsx
similarity index 99%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx
rename to apps/web/modules/survey/editor/components/logic-editor-conditions.tsx
index c27248994f..de0f191191 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx
+++ b/apps/web/modules/survey/editor/components/logic-editor-conditions.tsx
@@ -5,7 +5,7 @@ import {
getConditionValueOptions,
getDefaultOperatorForQuestion,
getMatchValueProps,
-} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
+} from "@/modules/survey/editor/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor.tsx b/apps/web/modules/survey/editor/components/logic-editor.tsx
similarity index 92%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor.tsx
rename to apps/web/modules/survey/editor/components/logic-editor.tsx
index d3cb54083c..0da6a1f9eb 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor.tsx
+++ b/apps/web/modules/survey/editor/components/logic-editor.tsx
@@ -1,7 +1,7 @@
"use client";
-import { LogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions";
-import { LogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions";
+import { LogicEditorActions } from "@/modules/survey/editor/components/logic-editor-actions";
+import { LogicEditorConditions } from "@/modules/survey/editor/components/logic-editor-conditions";
import {
Select,
SelectContent,
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/MatrixQuestionForm.tsx b/apps/web/modules/survey/editor/components/matrix-question-form.tsx
similarity index 99%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/MatrixQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/matrix-question-form.tsx
index ba12bd5266..a0434ac9cd 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/MatrixQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/matrix-question-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceQuestionForm.tsx b/apps/web/modules/survey/editor/components/multiple-choice-question-form.tsx
similarity index 97%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/multiple-choice-question-form.tsx
index e366dfe86b..a2f47e5b8d 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/multiple-choice-question-form.tsx
@@ -1,7 +1,8 @@
"use client";
-import { findOptionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
+import { QuestionOptionChoice } from "@/modules/survey/editor/components/question-option-choice";
+import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
@@ -22,7 +23,6 @@ import {
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
-import { QuestionOptionChoice } from "./QuestionOptionChoice";
interface MultipleChoiceQuestionFormProps {
localSurvey: TSurvey;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/NPSQuestionForm.tsx b/apps/web/modules/survey/editor/components/nps-question-form.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/NPSQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/nps-question-form.tsx
index d0d2a78951..49bfd088af 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/NPSQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/nps-question-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { useAutoAnimate } from "@formkit/auto-animate/react";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/OpenQuestionForm.tsx b/apps/web/modules/survey/editor/components/open-question-form.tsx
similarity index 99%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/OpenQuestionForm.tsx
rename to apps/web/modules/survey/editor/components/open-question-form.tsx
index f2773a7f5d..ac245e03c9 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/OpenQuestionForm.tsx
+++ b/apps/web/modules/survey/editor/components/open-question-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm.tsx b/apps/web/modules/survey/editor/components/picture-selection-form.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm.tsx
rename to apps/web/modules/survey/editor/components/picture-selection-form.tsx
index ea647eb3db..a2e5c8dbf4 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm.tsx
+++ b/apps/web/modules/survey/editor/components/picture-selection-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Button } from "@/modules/ui/components/button";
import { FileInput } from "@/modules/ui/components/file-input";
import { Label } from "@/modules/ui/components/label";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/Placement.tsx b/apps/web/modules/survey/editor/components/placement.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/Placement.tsx
rename to apps/web/modules/survey/editor/components/placement.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx b/apps/web/modules/survey/editor/components/question-card.tsx
similarity index 92%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx
rename to apps/web/modules/survey/editor/components/question-card.tsx
index 98becbaa7d..d6245e4763 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx
+++ b/apps/web/modules/survey/editor/components/question-card.tsx
@@ -1,14 +1,29 @@
"use client";
-import { ContactInfoQuestionForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ContactInfoQuestionForm";
-import { RankingQuestionForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RankingQuestionForm";
-import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
+import { AddressQuestionForm } from "@/modules/survey/editor/components/address-question-form";
+import { AdvancedSettings } from "@/modules/survey/editor/components/advanced-settings";
+import { CalQuestionForm } from "@/modules/survey/editor/components/cal-question-form";
+import { ConsentQuestionForm } from "@/modules/survey/editor/components/consent-question-form";
+import { ContactInfoQuestionForm } from "@/modules/survey/editor/components/contact-info-question-form";
+import { CTAQuestionForm } from "@/modules/survey/editor/components/cta-question-form";
+import { DateQuestionForm } from "@/modules/survey/editor/components/date-question-form";
+import { EditorCardMenu } from "@/modules/survey/editor/components/editor-card-menu";
+import { FileUploadQuestionForm } from "@/modules/survey/editor/components/file-upload-question-form";
+import { MatrixQuestionForm } from "@/modules/survey/editor/components/matrix-question-form";
+import { MultipleChoiceQuestionForm } from "@/modules/survey/editor/components/multiple-choice-question-form";
+import { NPSQuestionForm } from "@/modules/survey/editor/components/nps-question-form";
+import { OpenQuestionForm } from "@/modules/survey/editor/components/open-question-form";
+import { PictureSelectionForm } from "@/modules/survey/editor/components/picture-selection-form";
+import { RankingQuestionForm } from "@/modules/survey/editor/components/ranking-question-form";
+import { RatingQuestionForm } from "@/modules/survey/editor/components/rating-question-form";
+import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { Project } from "@prisma/client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
@@ -16,7 +31,6 @@ import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { getQuestionIconMap, getTSurveyQuestionTypeEnumName } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
-import { TProject } from "@formbricks/types/project";
import {
TI18nString,
TSurvey,
@@ -25,24 +39,10 @@ import {
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
-import { AddressQuestionForm } from "./AddressQuestionForm";
-import { AdvancedSettings } from "./AdvancedSettings";
-import { CTAQuestionForm } from "./CTAQuestionForm";
-import { CalQuestionForm } from "./CalQuestionForm";
-import { ConsentQuestionForm } from "./ConsentQuestionForm";
-import { DateQuestionForm } from "./DateQuestionForm";
-import { EditorCardMenu } from "./EditorCardMenu";
-import { FileUploadQuestionForm } from "./FileUploadQuestionForm";
-import { MatrixQuestionForm } from "./MatrixQuestionForm";
-import { MultipleChoiceQuestionForm } from "./MultipleChoiceQuestionForm";
-import { NPSQuestionForm } from "./NPSQuestionForm";
-import { OpenQuestionForm } from "./OpenQuestionForm";
-import { PictureSelectionForm } from "./PictureSelectionForm";
-import { RatingQuestionForm } from "./RatingQuestionForm";
interface QuestionCardProps {
localSurvey: TSurvey;
- project: TProject;
+ project: Project;
question: TSurveyQuestion;
questionIdx: number;
moveQuestion: (questionIndex: number, up: boolean) => void;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionOptionChoice.tsx b/apps/web/modules/survey/editor/components/question-option-choice.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionOptionChoice.tsx
rename to apps/web/modules/survey/editor/components/question-option-choice.tsx
index 3ef837a175..9e0f81a249 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionOptionChoice.tsx
+++ b/apps/web/modules/survey/editor/components/question-option-choice.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Button } from "@/modules/ui/components/button";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { useSortable } from "@dnd-kit/sortable";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx b/apps/web/modules/survey/editor/components/questions-droppable.tsx
similarity index 94%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx
rename to apps/web/modules/survey/editor/components/questions-droppable.tsx
index f4318eaba7..bdd3baff9e 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx
+++ b/apps/web/modules/survey/editor/components/questions-droppable.tsx
@@ -1,13 +1,13 @@
+import { QuestionCard } from "@/modules/survey/editor/components/question-card";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useAutoAnimate } from "@formkit/auto-animate/react";
-import { TProject } from "@formbricks/types/project";
+import { Project } from "@prisma/client";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
-import { QuestionCard } from "./QuestionCard";
interface QuestionsDraggableProps {
localSurvey: TSurvey;
- project: TProject;
+ project: Project;
moveQuestion: (questionIndex: number, up: boolean) => void;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
deleteQuestion: (questionIdx: number) => void;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx b/apps/web/modules/survey/editor/components/questions-view.tsx
similarity index 95%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx
rename to apps/web/modules/survey/editor/components/questions-view.tsx
index 2bf8aff647..3d000d0cbd 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx
+++ b/apps/web/modules/survey/editor/components/questions-view.tsx
@@ -1,10 +1,15 @@
"use client";
-import { AddEndingCardButton } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddEndingCardButton";
-import { SurveyVariablesCard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard";
-import { findQuestionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { getDefaultEndingCard } from "@/app/lib/templates";
import { MultiLanguageCard } from "@/modules/ee/multi-language-surveys/components/multi-language-card";
+import { AddEndingCardButton } from "@/modules/survey/editor/components/add-ending-card-button";
+import { AddQuestionButton } from "@/modules/survey/editor/components/add-question-button";
+import { EditEndingCard } from "@/modules/survey/editor/components/edit-ending-card";
+import { EditWelcomeCard } from "@/modules/survey/editor/components/edit-welcome-card";
+import { HiddenFieldsCard } from "@/modules/survey/editor/components/hidden-fields-card";
+import { QuestionsDroppable } from "@/modules/survey/editor/components/questions-droppable";
+import { SurveyVariablesCard } from "@/modules/survey/editor/components/survey-variables-card";
+import { findQuestionUsedInLogic } from "@/modules/survey/editor/lib/utils";
import {
DndContext,
DragEndEvent,
@@ -16,6 +21,7 @@ import {
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
+import { Language, Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import React, { SetStateAction, useEffect, useMemo } from "react";
import toast from "react-hot-toast";
@@ -24,7 +30,6 @@ import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils";
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
-import { TProject } from "@formbricks/types/project";
import {
TConditionGroup,
TSingleCondition,
@@ -41,18 +46,14 @@ import {
validateQuestion,
validateSurveyQuestionsInBatch,
} from "../lib/validation";
-import { AddQuestionButton } from "./AddQuestionButton";
-import { EditEndingCard } from "./EditEndingCard";
-import { EditWelcomeCard } from "./EditWelcomeCard";
-import { HiddenFieldsCard } from "./HiddenFieldsCard";
-import { QuestionsDroppable } from "./QuestionsDroppable";
interface QuestionsViewProps {
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch>;
activeQuestionId: TSurveyQuestionId | null;
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
- project: TProject;
+ project: Project;
+ projectLanguages: Language[];
invalidQuestions: string[] | null;
setInvalidQuestions: React.Dispatch>;
selectedLanguageCode: string;
@@ -70,6 +71,7 @@ export const QuestionsView = ({
localSurvey,
setLocalSurvey,
project,
+ projectLanguages,
invalidQuestions,
setInvalidQuestions,
setSelectedLanguageCode,
@@ -517,7 +519,7 @@ export const QuestionsView = ({
>;
setOpen: React.Dispatch>;
@@ -24,13 +24,13 @@ export const SavedActionsTab = ({
const availableActions = actionClasses.filter(
(actionClass) => !localSurvey.triggers.some((trigger) => trigger.actionClass.id === actionClass.id)
);
- const [filteredActionClasses, setFilteredActionClasses] = useState(availableActions);
+ const [filteredActionClasses, setFilteredActionClasses] = useState(availableActions);
const codeActions = filteredActionClasses.filter((actionClass) => actionClass.type === "code");
const noCodeActions = filteredActionClasses.filter((actionClass) => actionClass.type === "noCode");
const automaticActions = filteredActionClasses.filter((actionClass) => actionClass.type === "automatic");
- const handleActionClick = (action: TActionClass) => {
+ const handleActionClick = (action: ActionClass) => {
setLocalSurvey((prev) => ({
...prev,
triggers: prev.triggers.concat({ actionClass: action }),
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx b/apps/web/modules/survey/editor/components/settings-view.tsx
similarity index 78%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx
rename to apps/web/modules/survey/editor/components/settings-view.tsx
index 5182abba35..9b21cccfa5 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx
+++ b/apps/web/modules/survey/editor/components/settings-view.tsx
@@ -1,27 +1,25 @@
-import { TargetingLockedCard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/TargetingLockedCard";
import { TargetingCard } from "@/modules/ee/contacts/segments/components/targeting-card";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
-import { TActionClass } from "@formbricks/types/action-classes";
+import { HowToSendCard } from "@/modules/survey/editor/components/how-to-send-card";
+import { RecontactOptionsCard } from "@/modules/survey/editor/components/recontact-options-card";
+import { ResponseOptionsCard } from "@/modules/survey/editor/components/response-options-card";
+import { SurveyPlacementCard } from "@/modules/survey/editor/components/survey-placement-card";
+import { TargetingLockedCard } from "@/modules/survey/editor/components/targeting-locked-card";
+import { WhenToSendCard } from "@/modules/survey/editor/components/when-to-send-card";
+import { ActionClass, Environment, OrganizationRole } from "@prisma/client";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
-import { TEnvironment } from "@formbricks/types/environment";
-import { TOrganizationRole } from "@formbricks/types/memberships";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey } from "@formbricks/types/surveys/types";
-import { HowToSendCard } from "./HowToSendCard";
-import { RecontactOptionsCard } from "./RecontactOptionsCard";
-import { ResponseOptionsCard } from "./ResponseOptionsCard";
-import { SurveyPlacementCard } from "./SurveyPlacementCard";
-import { WhenToSendCard } from "./WhenToSendCard";
interface SettingsViewProps {
- environment: TEnvironment;
+ environment: Pick;
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;
- actionClasses: TActionClass[];
+ actionClasses: ActionClass[];
contactAttributeKeys: TContactAttributeKey[];
segments: TSegment[];
responseCount: number;
- membershipRole?: TOrganizationRole;
+ membershipRole?: OrganizationRole;
isUserTargetingAllowed?: boolean;
projectPermission: TTeamPermission | null;
isFormbricksCloud: boolean;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingView.tsx b/apps/web/modules/survey/editor/components/styling-view.tsx
similarity index 93%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingView.tsx
rename to apps/web/modules/survey/editor/components/styling-view.tsx
index 55dfb3ff1c..7c8f38d49a 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingView.tsx
+++ b/apps/web/modules/survey/editor/components/styling-view.tsx
@@ -1,7 +1,10 @@
"use client";
+import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
+import { BackgroundStylingCard } from "@/modules/ui/components/background-styling-card";
import { Button } from "@/modules/ui/components/button";
+import { CardStylingSettings } from "@/modules/ui/components/card-styling-settings";
import {
FormControl,
FormDescription,
@@ -11,6 +14,7 @@ import {
FormProvider,
} from "@/modules/ui/components/form";
import { Switch } from "@/modules/ui/components/switch";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { RotateCcwIcon } from "lucide-react";
import Link from "next/link";
@@ -18,16 +22,12 @@ import React, { useEffect, useMemo, useState } from "react";
import { UseFormReturn, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { defaultStyling } from "@formbricks/lib/styling/constants";
-import { TEnvironment } from "@formbricks/types/environment";
-import { TProject, TProjectStyling } from "@formbricks/types/project";
+import { TProjectStyling } from "@formbricks/types/project";
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
-import { BackgroundStylingCard } from "./BackgroundStylingCard";
-import { CardStylingSettings } from "./CardStylingSettings";
-import { FormStylingSettings } from "./FormStylingSettings";
interface StylingViewProps {
- environment: TEnvironment;
- project: TProject;
+ environmentId: string;
+ project: Project;
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch>;
colors: string[];
@@ -41,7 +41,7 @@ interface StylingViewProps {
export const StylingView = ({
colors,
- environment,
+ environmentId,
project,
localSurvey,
setLocalSurvey,
@@ -204,7 +204,7 @@ export const StylingView = ({
{t("environments.surveys.edit.adjust_the_theme_in_the")}{" "}
{t("common.look_and_feel")}
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditorTabs.tsx b/apps/web/modules/survey/editor/components/survey-editor-tabs.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditorTabs.tsx
rename to apps/web/modules/survey/editor/components/survey-editor-tabs.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx b/apps/web/modules/survey/editor/components/survey-editor.tsx
similarity index 88%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx
rename to apps/web/modules/survey/editor/components/survey-editor.tsx
index 68f907c813..666cf74a95 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx
+++ b/apps/web/modules/survey/editor/components/survey-editor.tsx
@@ -1,38 +1,35 @@
"use client";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
-import { FollowUpsView } from "@/modules/survey-follow-ups/components/follow-ups-view";
+import { FollowUpsView } from "@/modules/survey/follow-ups/components/follow-ups-view";
+import { LoadingSkeleton } from "@/modules/survey/editor/components/loading-skeleton";
+import { QuestionsView } from "@/modules/survey/editor/components/questions-view";
+import { SettingsView } from "@/modules/survey/editor/components/settings-view";
+import { StylingView } from "@/modules/survey/editor/components/styling-view";
+import { SurveyEditorTabs } from "@/modules/survey/editor/components/survey-editor-tabs";
+import { SurveyMenuBar } from "@/modules/survey/editor/components/survey-menu-bar";
import { PreviewSurvey } from "@/modules/ui/components/preview-survey";
+import { ActionClass, Environment, Language, OrganizationRole, Project } from "@prisma/client";
import { useCallback, useEffect, useRef, useState } from "react";
import { extractLanguageCodes, getEnabledLanguages } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { useDocumentVisibility } from "@formbricks/lib/useDocumentVisibility";
-import { TActionClass } from "@formbricks/types/action-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
-import { TEnvironment } from "@formbricks/types/environment";
-import { TOrganizationRole } from "@formbricks/types/memberships";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
-import { TProject } from "@formbricks/types/project";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { refetchProjectAction } from "../actions";
-import { LoadingSkeleton } from "./LoadingSkeleton";
-import { QuestionsView } from "./QuestionsView";
-import { SettingsView } from "./SettingsView";
-import { StylingView } from "./StylingView";
-import { SurveyEditorTabs } from "./SurveyEditorTabs";
-import { SurveyMenuBar } from "./SurveyMenuBar";
interface SurveyEditorProps {
survey: TSurvey;
- project: TProject;
- environment: TEnvironment;
- actionClasses: TActionClass[];
+ project: Project;
+ environment: Pick;
+ actionClasses: ActionClass[];
contactAttributeKeys: TContactAttributeKey[];
segments: TSegment[];
responseCount: number;
- membershipRole?: TOrganizationRole;
+ membershipRole?: OrganizationRole;
colors: string[];
isUserTargetingAllowed?: boolean;
isMultiLanguageAllowed?: boolean;
@@ -43,6 +40,7 @@ interface SurveyEditorProps {
locale: TUserLocale;
projectPermission: TTeamPermission | null;
mailFrom: string;
+ projectLanguages: Language[];
isSurveyFollowUpsAllowed: boolean;
userEmail: string;
}
@@ -50,6 +48,7 @@ interface SurveyEditorProps {
export const SurveyEditor = ({
survey,
project,
+ projectLanguages,
environment,
actionClasses,
contactAttributeKeys,
@@ -75,7 +74,7 @@ export const SurveyEditor = ({
const [invalidQuestions, setInvalidQuestions] = useState(null);
const [selectedLanguageCode, setSelectedLanguageCode] = useState("default");
const surveyEditorRef = useRef(null);
- const [localProject, setLocalProject] = useState(project);
+ const [localProject, setLocalProject] = useState(project);
const [styling, setStyling] = useState(localSurvey?.styling);
const [localStylingChanges, setLocalStylingChanges] = useState(null);
@@ -148,7 +147,7 @@ export const SurveyEditor = ({
setLocalSurvey={setLocalSurvey}
localSurvey={localSurvey}
survey={survey}
- environment={environment}
+ environmentId={environment.id}
activeId={activeView}
setActiveId={setActiveView}
setInvalidQuestions={setInvalidQuestions}
@@ -178,6 +177,7 @@ export const SurveyEditor = ({
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
project={localProject}
+ projectLanguages={projectLanguages}
invalidQuestions={invalidQuestions}
setInvalidQuestions={setInvalidQuestions}
selectedLanguageCode={selectedLanguageCode ? selectedLanguageCode : "default"}
@@ -193,7 +193,7 @@ export const SurveyEditor = ({
{activeView === "styling" && project.styling.allowStyleOverwrite && (
void;
- environment: TEnvironment;
+ environmentId: string;
activeId: TSurveyEditorTabs;
setActiveId: React.Dispatch>;
setInvalidQuestions: React.Dispatch>;
- project: TProject;
+ project: Project;
responseCount: number;
selectedLanguageCode: string;
setSelectedLanguageCode: (selectedLanguage: string) => void;
@@ -46,7 +45,7 @@ interface SurveyMenuBarProps {
export const SurveyMenuBar = ({
localSurvey,
survey,
- environment,
+ environmentId,
setLocalSurvey,
activeId,
setActiveId,
@@ -295,7 +294,7 @@ export const SurveyMenuBar = ({
segment,
});
setIsSurveyPublishing(false);
- router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`);
+ router.push(`/environments/${environmentId}/surveys/${localSurvey.id}/summary?success=true`);
} catch (error) {
console.error(error);
toast.error(t("environments.surveys.edit.error_publishing_survey"));
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard.tsx b/apps/web/modules/survey/editor/components/survey-placement-card.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard.tsx
rename to apps/web/modules/survey/editor/components/survey-placement-card.tsx
index ba46a45e72..2be85928cf 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard.tsx
+++ b/apps/web/modules/survey/editor/components/survey-placement-card.tsx
@@ -1,5 +1,6 @@
"use client";
+import { Placement } from "@/modules/survey/editor/components/placement";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
import { useAutoAnimate } from "@formkit/auto-animate/react";
@@ -10,7 +11,6 @@ import Link from "next/link";
import { useState } from "react";
import { TPlacement } from "@formbricks/types/common";
import { TSurvey, TSurveyProjectOverwrites } from "@formbricks/types/surveys/types";
-import { Placement } from "./Placement";
interface SurveyPlacementCardProps {
localSurvey: TSurvey;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCardItem.tsx b/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCardItem.tsx
rename to apps/web/modules/survey/editor/components/survey-variables-card-item.tsx
index e175d1e546..eac1ba3627 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCardItem.tsx
+++ b/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx
@@ -1,6 +1,6 @@
"use client";
-import { findVariableUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
+import { findVariableUsedInLogic } from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormField, FormItem, FormProvider } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard.tsx b/apps/web/modules/survey/editor/components/survey-variables-card.tsx
similarity index 96%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard.tsx
rename to apps/web/modules/survey/editor/components/survey-variables-card.tsx
index 4276a92e95..25c507ea1d 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard.tsx
+++ b/apps/web/modules/survey/editor/components/survey-variables-card.tsx
@@ -1,12 +1,12 @@
"use client";
+import { SurveyVariablesCardItem } from "@/modules/survey/editor/components/survey-variables-card-item";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { FileDigitIcon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
-import { SurveyVariablesCardItem } from "./SurveyVariablesCardItem";
interface SurveyVariablesCardProps {
localSurvey: TSurvey;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/TargetingLockedCard.tsx b/apps/web/modules/survey/editor/components/targeting-locked-card.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/TargetingLockedCard.tsx
rename to apps/web/modules/survey/editor/components/targeting-locked-card.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/UnsplashImages.tsx b/apps/web/modules/survey/editor/components/unsplash-images.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/UnsplashImages.tsx
rename to apps/web/modules/survey/editor/components/unsplash-images.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/UpdateQuestionId.tsx b/apps/web/modules/survey/editor/components/update-question-id.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/UpdateQuestionId.tsx
rename to apps/web/modules/survey/editor/components/update-question-id.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx b/apps/web/modules/survey/editor/components/when-to-send-card.tsx
similarity index 97%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx
rename to apps/web/modules/survey/editor/components/when-to-send-card.tsx
index dab3be7d05..13fe05d3c8 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx
+++ b/apps/web/modules/survey/editor/components/when-to-send-card.tsx
@@ -2,10 +2,12 @@
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
+import { AddActionModal } from "@/modules/survey/editor/components/add-action-modal";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { ActionClass, OrganizationRole } from "@prisma/client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import {
@@ -18,17 +20,14 @@ import {
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
-import { TActionClass } from "@formbricks/types/action-classes";
-import { TOrganizationRole } from "@formbricks/types/memberships";
import { TSurvey } from "@formbricks/types/surveys/types";
-import { AddActionModal } from "./AddActionModal";
interface WhenToSendCardProps {
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch>;
environmentId: string;
- propActionClasses: TActionClass[];
- membershipRole?: TOrganizationRole;
+ propActionClasses: ActionClass[];
+ membershipRole?: OrganizationRole;
projectPermission: TTeamPermission | null;
}
@@ -43,7 +42,7 @@ export const WhenToSendCard = ({
const { t } = useTranslate();
const [open, setOpen] = useState(localSurvey.type === "app" ? true : false);
const [isAddActionModalOpen, setAddActionModalOpen] = useState(false);
- const [actionClasses, setActionClasses] = useState(propActionClasses);
+ const [actionClasses, setActionClasses] = useState(propActionClasses);
const [randomizerToggle, setRandomizerToggle] = useState(localSurvey.displayPercentage ? true : false);
const { isMember } = getAccessFlags(membershipRole);
diff --git a/apps/web/modules/survey/editor/lib/action-class.ts b/apps/web/modules/survey/editor/lib/action-class.ts
new file mode 100644
index 0000000000..5a4a3c3297
--- /dev/null
+++ b/apps/web/modules/survey/editor/lib/action-class.ts
@@ -0,0 +1,39 @@
+import { ActionClass, Prisma } from "@prisma/client";
+import { prisma } from "@formbricks/database";
+import { actionClassCache } from "@formbricks/lib/actionClass/cache";
+import { DatabaseError } from "@formbricks/types/errors";
+import { TActionClassInput } from "@formbricks/types/action-classes";
+
+export const createActionClass = async (
+ environmentId: string,
+ actionClass: TActionClassInput
+): Promise => {
+ const { environmentId: _, ...actionClassInput } = actionClass;
+
+ try {
+ const actionClassPrisma = await prisma.actionClass.create({
+ data: {
+ ...actionClassInput,
+ environment: { connect: { id: environmentId } },
+ key: actionClassInput.type === "code" ? actionClassInput.key : undefined,
+ noCodeConfig: actionClassInput.type === "noCode" ? actionClassInput.noCodeConfig || {} : undefined,
+ },
+ });
+
+ actionClassCache.revalidate({
+ name: actionClassPrisma.name,
+ environmentId: actionClassPrisma.environmentId,
+ id: actionClassPrisma.id,
+ });
+
+ return actionClassPrisma;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
+ throw new DatabaseError(
+ `Action with ${error.meta?.target?.[0]} ${actionClass[error.meta?.target?.[0]]} already exists`
+ );
+ }
+
+ throw new DatabaseError(`Database error when creating an action for environment ${environmentId}`);
+ }
+};
\ No newline at end of file
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/logicRuleEngine.ts b/apps/web/modules/survey/editor/lib/logic-rule-engine.ts
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/logicRuleEngine.ts
rename to apps/web/modules/survey/editor/lib/logic-rule-engine.ts
diff --git a/apps/web/modules/survey/editor/lib/project.ts b/apps/web/modules/survey/editor/lib/project.ts
new file mode 100644
index 0000000000..78c1df8d9a
--- /dev/null
+++ b/apps/web/modules/survey/editor/lib/project.ts
@@ -0,0 +1,57 @@
+import { Language, Prisma, Project } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { projectCache } from "@formbricks/lib/project/cache";
+import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+
+export const getProject = reactCache(
+ async (projectId: string): Promise =>
+ cache(
+ async () => {
+ try {
+ const projectPrisma = await prisma.project.findUnique({
+ where: {
+ id: projectId,
+ },
+ });
+
+ return projectPrisma;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+ },
+ [`survey-editor-getProject-${projectId}`],
+ {
+ tags: [projectCache.tag.byId(projectId)],
+ }
+ )()
+);
+
+export const getProjectLanguages = reactCache(
+ async (projectId: string): Promise =>
+ cache(
+ async () => {
+ const project = await prisma.project.findUnique({
+ where: {
+ id: projectId,
+ },
+ select: {
+ languages: true,
+ },
+ });
+ if (!project) {
+ throw new ResourceNotFoundError("Project not found", projectId);
+ }
+ return project.languages;
+ },
+ [`survey-editor-getProjectLanguages-${projectId}`],
+ {
+ tags: [projectCache.tag.byId(projectId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/editor/lib/survey.ts b/apps/web/modules/survey/editor/lib/survey.ts
new file mode 100644
index 0000000000..e594428698
--- /dev/null
+++ b/apps/web/modules/survey/editor/lib/survey.ts
@@ -0,0 +1,381 @@
+import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
+import { handleTriggerUpdates } from "@/modules/survey/editor/lib/utils";
+import { getActionClasses } from "@/modules/survey/lib/action-class";
+import { getOrganizationAIKeys, getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization";
+import { getSurvey, selectSurvey } from "@/modules/survey/lib/survey";
+import { getInsightsEnabled } from "@/modules/survey/lib/utils";
+import { doesSurveyHasOpenTextQuestion } from "@/modules/survey/lib/utils";
+import { Prisma, Survey } from "@prisma/client";
+import { prisma } from "@formbricks/database";
+import { segmentCache } from "@formbricks/lib/cache/segment";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
+import { TSegment, ZSegmentFilters } from "@formbricks/types/segment";
+import { TSurvey, TSurveyOpenTextQuestion } from "@formbricks/types/surveys/types";
+
+export const updateSurvey = async (updatedSurvey: TSurvey): Promise => {
+ try {
+ const surveyId = updatedSurvey.id;
+ let data: any = {};
+
+ const actionClasses = await getActionClasses(updatedSurvey.environmentId);
+ const currentSurvey = await getSurvey(surveyId);
+
+ if (!currentSurvey) {
+ throw new ResourceNotFoundError("Survey", surveyId);
+ }
+
+ const { triggers, environmentId, segment, questions, languages, type, followUps, ...surveyData } =
+ updatedSurvey;
+
+ if (languages) {
+ // Process languages update logic here
+ // Extract currentLanguageIds and updatedLanguageIds
+ const currentLanguageIds = currentSurvey.languages
+ ? currentSurvey.languages.map((l) => l.language.id)
+ : [];
+ const updatedLanguageIds =
+ languages.length > 1 ? updatedSurvey.languages.map((l) => l.language.id) : [];
+ const enabledLanguageIds = languages.map((language) => {
+ if (language.enabled) return language.language.id;
+ });
+
+ // Determine languages to add and remove
+ const languagesToAdd = updatedLanguageIds.filter((id) => !currentLanguageIds.includes(id));
+ const languagesToRemove = currentLanguageIds.filter((id) => !updatedLanguageIds.includes(id));
+
+ const defaultLanguageId = updatedSurvey.languages.find((l) => l.default)?.language.id;
+
+ // Prepare data for Prisma update
+ data.languages = {};
+
+ // Update existing languages for default value changes
+ data.languages.updateMany = currentSurvey.languages.map((surveyLanguage) => ({
+ where: { languageId: surveyLanguage.language.id },
+ data: {
+ default: surveyLanguage.language.id === defaultLanguageId,
+ enabled: enabledLanguageIds.includes(surveyLanguage.language.id),
+ },
+ }));
+
+ // Add new languages
+ if (languagesToAdd.length > 0) {
+ data.languages.create = languagesToAdd.map((languageId) => ({
+ languageId: languageId,
+ default: languageId === defaultLanguageId,
+ enabled: enabledLanguageIds.includes(languageId),
+ }));
+ }
+
+ // Remove languages no longer associated with the survey
+ if (languagesToRemove.length > 0) {
+ data.languages.deleteMany = languagesToRemove.map((languageId) => ({
+ languageId: languageId,
+ enabled: enabledLanguageIds.includes(languageId),
+ }));
+ }
+ }
+
+ if (triggers) {
+ data.triggers = handleTriggerUpdates(triggers, currentSurvey.triggers, actionClasses);
+ }
+
+ // if the survey body has type other than "app" but has a private segment, we delete that segment, and if it has a public segment, we disconnect from to the survey
+ if (segment) {
+ if (type === "app") {
+ // parse the segment filters:
+ const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
+ if (!parsedFilters.success) {
+ throw new InvalidInputError("Invalid user segment filters");
+ }
+
+ try {
+ // update the segment:
+ let updatedInput: Prisma.SegmentUpdateInput = {
+ ...segment,
+ surveys: undefined,
+ };
+
+ if (segment.surveys) {
+ updatedInput = {
+ ...segment,
+ surveys: {
+ connect: segment.surveys.map((surveyId) => ({ id: surveyId })),
+ },
+ };
+ }
+
+ const updatedSegment = await prisma.segment.update({
+ where: { id: segment.id },
+ data: updatedInput,
+ select: {
+ surveys: { select: { id: true } },
+ environmentId: true,
+ id: true,
+ },
+ });
+
+ segmentCache.revalidate({ id: updatedSegment.id, environmentId: updatedSegment.environmentId });
+ updatedSegment.surveys.map((survey) => surveyCache.revalidate({ id: survey.id }));
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error updating survey");
+ }
+ } else {
+ if (segment.isPrivate) {
+ // disconnect the private segment first and then delete:
+ await prisma.segment.update({
+ where: { id: segment.id },
+ data: {
+ surveys: {
+ disconnect: {
+ id: surveyId,
+ },
+ },
+ },
+ });
+
+ // delete the private segment:
+ await prisma.segment.delete({
+ where: {
+ id: segment.id,
+ },
+ });
+ } else {
+ await prisma.survey.update({
+ where: {
+ id: surveyId,
+ },
+ data: {
+ segment: {
+ disconnect: true,
+ },
+ },
+ });
+ }
+ }
+
+ segmentCache.revalidate({
+ id: segment.id,
+ environmentId: segment.environmentId,
+ });
+ } else if (type === "app") {
+ if (!currentSurvey.segment) {
+ await prisma.survey.update({
+ where: {
+ id: surveyId,
+ },
+ data: {
+ segment: {
+ connectOrCreate: {
+ where: {
+ environmentId_title: {
+ environmentId,
+ title: surveyId,
+ },
+ },
+ create: {
+ title: surveyId,
+ isPrivate: true,
+ filters: [],
+ environment: {
+ connect: {
+ id: environmentId,
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ segmentCache.revalidate({
+ environmentId,
+ });
+ }
+ }
+
+ if (followUps) {
+ // Separate follow-ups into categories based on deletion flag
+ const deletedFollowUps = followUps.filter((followUp) => followUp.deleted);
+ const nonDeletedFollowUps = followUps.filter((followUp) => !followUp.deleted);
+
+ // Get set of existing follow-up IDs from currentSurvey
+ const existingFollowUpIds = new Set(currentSurvey.followUps.map((f) => f.id));
+
+ // Separate non-deleted follow-ups into new and existing
+ const existingFollowUps = nonDeletedFollowUps.filter((followUp) =>
+ existingFollowUpIds.has(followUp.id)
+ );
+ const newFollowUps = nonDeletedFollowUps.filter((followUp) => !existingFollowUpIds.has(followUp.id));
+
+ data.followUps = {
+ // Update existing follow-ups
+ updateMany: existingFollowUps.map((followUp) => ({
+ where: {
+ id: followUp.id,
+ },
+ data: {
+ name: followUp.name,
+ trigger: followUp.trigger,
+ action: followUp.action,
+ },
+ })),
+ // Create new follow-ups
+ createMany:
+ newFollowUps.length > 0
+ ? {
+ data: newFollowUps.map((followUp) => ({
+ name: followUp.name,
+ trigger: followUp.trigger,
+ action: followUp.action,
+ })),
+ }
+ : undefined,
+ // Delete follow-ups marked as deleted, regardless of whether they exist in DB
+ deleteMany:
+ deletedFollowUps.length > 0
+ ? deletedFollowUps.map((followUp) => ({
+ id: followUp.id,
+ }))
+ : undefined,
+ };
+ }
+
+ data.questions = questions.map((question) => {
+ const { isDraft, ...rest } = question;
+ return rest;
+ });
+
+ const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
+ const organization = await getOrganizationAIKeys(organizationId);
+ if (!organization) {
+ throw new ResourceNotFoundError("Organization", null);
+ }
+
+ //AI Insights
+ const isAIEnabled = await getIsAIEnabled(organization);
+ if (isAIEnabled) {
+ if (doesSurveyHasOpenTextQuestion(data.questions ?? [])) {
+ const openTextQuestions = data.questions?.filter((question) => question.type === "openText") ?? [];
+ const currentSurveyOpenTextQuestions = currentSurvey.questions?.filter(
+ (question) => question.type === "openText"
+ );
+
+ // find the questions that have been updated or added
+ const questionsToCheckForInsights: Survey["questions"] = [];
+
+ for (const question of openTextQuestions) {
+ const existingQuestion = currentSurveyOpenTextQuestions?.find((ques) => ques.id === question.id) as
+ | TSurveyOpenTextQuestion
+ | undefined;
+ const isExistingQuestion = !!existingQuestion;
+
+ if (
+ isExistingQuestion &&
+ question.headline.default === existingQuestion.headline.default &&
+ existingQuestion.insightsEnabled !== undefined
+ ) {
+ continue;
+ } else {
+ questionsToCheckForInsights.push(question);
+ }
+ }
+
+ if (questionsToCheckForInsights.length > 0) {
+ const insightsEnabledValues = await Promise.all(
+ questionsToCheckForInsights.map(async (question) => {
+ const insightsEnabled = await getInsightsEnabled(question);
+
+ return { id: question.id, insightsEnabled };
+ })
+ );
+
+ data.questions = data.questions?.map((question) => {
+ const index = insightsEnabledValues.findIndex((item) => item.id === question.id);
+ if (index !== -1) {
+ return {
+ ...question,
+ insightsEnabled: insightsEnabledValues[index].insightsEnabled,
+ };
+ }
+
+ return question;
+ });
+ }
+ }
+ } else {
+ // check if an existing question got changed that had insights enabled
+ const insightsEnabledOpenTextQuestions = currentSurvey.questions?.filter(
+ (question) => question.type === "openText" && question.insightsEnabled !== undefined
+ );
+ // if question headline changed, remove insightsEnabled
+ for (const question of insightsEnabledOpenTextQuestions) {
+ const updatedQuestion = data.questions?.find((q) => q.id === question.id);
+ if (updatedQuestion && updatedQuestion.headline.default !== question.headline.default) {
+ updatedQuestion.insightsEnabled = undefined;
+ }
+ }
+ }
+
+ surveyData.updatedAt = new Date();
+
+ data = {
+ ...surveyData,
+ ...data,
+ type,
+ };
+
+ // Remove scheduled status when runOnDate is not set
+ if (data.status === "scheduled" && data.runOnDate === null) {
+ data.status = "inProgress";
+ }
+ // Set scheduled status when runOnDate is set and in the future on completed surveys
+ if (
+ (data.status === "completed" || data.status === "paused" || data.status === "inProgress") &&
+ data.runOnDate &&
+ data.runOnDate > new Date()
+ ) {
+ data.status = "scheduled";
+ }
+
+ delete data.createdBy;
+ const prismaSurvey = await prisma.survey.update({
+ where: { id: surveyId },
+ data,
+ select: selectSurvey,
+ });
+
+ let surveySegment: TSegment | null = null;
+ if (prismaSurvey.segment) {
+ surveySegment = {
+ ...prismaSurvey.segment,
+ surveys: prismaSurvey.segment.surveys.map((survey) => survey.id),
+ };
+ }
+
+ // 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,
+ segment: surveySegment,
+ };
+
+ surveyCache.revalidate({
+ id: modifiedSurvey.id,
+ environmentId: modifiedSurvey.environmentId,
+ segmentId: modifiedSurvey.segment?.id,
+ resultShareKey: currentSurvey.resultShareKey ?? undefined,
+ });
+
+ return modifiedSurvey;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+};
diff --git a/apps/web/modules/survey/editor/lib/user.ts b/apps/web/modules/survey/editor/lib/user.ts
new file mode 100644
index 0000000000..66d5f253a9
--- /dev/null
+++ b/apps/web/modules/survey/editor/lib/user.ts
@@ -0,0 +1,67 @@
+import { Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { userCache } from "@formbricks/lib/user/cache";
+import { DatabaseError } from "@formbricks/types/errors";
+import { TUserLocale } from "@formbricks/types/user";
+
+export const getUserEmail = reactCache(
+ (userId: string): Promise =>
+ cache(
+ async () => {
+ try {
+ const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
+
+ if (!user) {
+ return null;
+ }
+
+ return user.email;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`survey-editor-getUserEmail-${userId}`],
+ {
+ tags: [userCache.tag.byId(userId)],
+ }
+ )()
+);
+
+export const getUserLocale = reactCache(
+ async (id: string): Promise =>
+ cache(
+ async () => {
+ try {
+ const user = await prisma.user.findUnique({
+ where: {
+ id,
+ },
+ select: {
+ locale: true,
+ },
+ });
+
+ if (!user) {
+ return undefined;
+ }
+ return user.locale;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`survey-editor-getUserLocale-${id}`],
+ {
+ tags: [userCache.tag.byId(id)],
+ }
+ )()
+);
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx b/apps/web/modules/survey/editor/lib/utils.tsx
similarity index 93%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx
rename to apps/web/modules/survey/editor/lib/utils.tsx
index de6ef46f03..347fc3a97e 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx
+++ b/apps/web/modules/survey/editor/lib/utils.tsx
@@ -1,11 +1,15 @@
+import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger";
import { TComboboxGroupedOption, TComboboxOption } from "@/modules/ui/components/input-combo-box";
+import { ActionClass } from "@prisma/client";
import { TFnType } from "@tolgee/react";
import { EyeOffIcon, FileDigitIcon, FileType2Icon } from "lucide-react";
import { HTMLInputTypeAttribute } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
+import { surveyCache } from "@formbricks/lib/survey/cache";
import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils";
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
+import { InvalidInputError } from "@formbricks/types/errors";
import {
TConditionGroup,
TLeftOperand,
@@ -21,7 +25,7 @@ import {
TSurveyQuestionTypeEnum,
TSurveyVariable,
} from "@formbricks/types/surveys/types";
-import { TLogicRuleOption, getLogicRules } from "./logicRuleEngine";
+import { TLogicRuleOption, getLogicRules } from "./logic-rule-engine";
// formats the text to highlight specific parts of the text with slashes
export const formatTextWithSlashes = (text: string) => {
@@ -1171,3 +1175,69 @@ export const findEndingCardUsedInLogic = (survey: TSurvey, endingCardId: string)
(question) => question.logicFallback === endingCardId || question.logic?.some(isUsedInLogicRule)
);
};
+
+const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => {
+ if (!triggers) return;
+
+ // check if all the triggers are valid
+ triggers.forEach((trigger) => {
+ if (!actionClasses.find((actionClass) => actionClass.id === trigger.actionClass.id)) {
+ throw new InvalidInputError("Invalid trigger id");
+ }
+ });
+
+ // check if all the triggers are unique
+ const triggerIds = triggers.map((trigger) => trigger.actionClass.id);
+
+ if (new Set(triggerIds).size !== triggerIds.length) {
+ throw new InvalidInputError("Duplicate trigger id");
+ }
+};
+
+export const handleTriggerUpdates = (
+ updatedTriggers: TSurvey["triggers"],
+ currentTriggers: TSurvey["triggers"],
+ actionClasses: ActionClass[]
+) => {
+ if (!updatedTriggers) return {};
+ checkTriggersValidity(updatedTriggers, actionClasses);
+
+ const currentTriggerIds = currentTriggers.map((trigger) => trigger.actionClass.id);
+ const updatedTriggerIds = updatedTriggers.map((trigger) => trigger.actionClass.id);
+
+ // added triggers are triggers that are not in the current triggers and are there in the new triggers
+ const addedTriggers = updatedTriggers.filter(
+ (trigger) => !currentTriggerIds.includes(trigger.actionClass.id)
+ );
+
+ // deleted triggers are triggers that are not in the new triggers and are there in the current triggers
+ const deletedTriggers = currentTriggers.filter(
+ (trigger) => !updatedTriggerIds.includes(trigger.actionClass.id)
+ );
+
+ // Construct the triggers update object
+ const triggersUpdate: TriggerUpdate = {};
+
+ if (addedTriggers.length > 0) {
+ triggersUpdate.create = addedTriggers.map((trigger) => ({
+ actionClassId: trigger.actionClass.id,
+ }));
+ }
+
+ if (deletedTriggers.length > 0) {
+ // disconnect the public triggers from the survey
+ triggersUpdate.deleteMany = {
+ actionClassId: {
+ in: deletedTriggers.map((trigger) => trigger.actionClass.id),
+ },
+ };
+ }
+
+ [...addedTriggers, ...deletedTriggers].forEach((trigger) => {
+ surveyCache.revalidate({
+ actionClassId: trigger.actionClass.id,
+ });
+ });
+
+ return triggersUpdate;
+};
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation.ts b/apps/web/modules/survey/editor/lib/validation.ts
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation.ts
rename to apps/web/modules/survey/editor/lib/validation.ts
diff --git a/apps/web/modules/survey/editor/loading.tsx b/apps/web/modules/survey/editor/loading.tsx
new file mode 100644
index 0000000000..f892f14377
--- /dev/null
+++ b/apps/web/modules/survey/editor/loading.tsx
@@ -0,0 +1,5 @@
+import { LoadingSkeleton } from "./components/loading-skeleton";
+
+export const SurveyEditorLoading = () => {
+ return ;
+};
diff --git a/apps/web/modules/survey/editor/page.tsx b/apps/web/modules/survey/editor/page.tsx
new file mode 100644
index 0000000000..c7967422a9
--- /dev/null
+++ b/apps/web/modules/survey/editor/page.tsx
@@ -0,0 +1,135 @@
+import { authOptions } from "@/modules/auth/lib/authOptions";
+import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contacts";
+import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
+import { getIsContactsEnabled, getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
+import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
+import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
+import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
+import { getProjectLanguages } from "@/modules/survey/editor/lib/project";
+import { getUserEmail } from "@/modules/survey/editor/lib/user";
+import { getActionClasses } from "@/modules/survey/lib/action-class";
+import { getEnvironment } from "@/modules/survey/lib/environment";
+import { getMembershipRoleByUserIdOrganizationId } from "@/modules/survey/lib/membership";
+import { getProjectByEnvironmentId } from "@/modules/survey/lib/project";
+import { getResponseCountBySurveyId } from "@/modules/survey/lib/response";
+import { getOrganizationBilling, getSurvey } from "@/modules/survey/lib/survey";
+import { ErrorComponent } from "@/modules/ui/components/error-component";
+import { getTranslate } from "@/tolgee/server";
+import { getServerSession } from "next-auth";
+import {
+ DEFAULT_LOCALE,
+ IS_FORMBRICKS_CLOUD,
+ MAIL_FROM,
+ SURVEY_BG_COLORS,
+ UNSPLASH_ACCESS_KEY,
+} from "@formbricks/lib/constants";
+import { getAccessFlags } from "@formbricks/lib/membership/utils";
+import { SurveyEditor } from "./components/survey-editor";
+import { getUserLocale } from "./lib/user";
+
+export const generateMetadata = async (props) => {
+ const params = await props.params;
+ const survey = await getSurvey(params.surveyId);
+ return {
+ title: survey?.name ? `${survey?.name} | Editor` : "Editor",
+ };
+};
+
+export const SurveyEditorPage = async (props) => {
+ const searchParams = await props.searchParams;
+ const params = await props.params;
+ const t = await getTranslate();
+ const [
+ survey,
+ project,
+ environment,
+ actionClasses,
+ contactAttributeKeys,
+ responseCount,
+ session,
+ segments,
+ ] = await Promise.all([
+ getSurvey(params.surveyId),
+ getProjectByEnvironmentId(params.environmentId),
+ getEnvironment(params.environmentId),
+ getActionClasses(params.environmentId),
+ getContactAttributeKeys(params.environmentId),
+ getResponseCountBySurveyId(params.surveyId),
+ getServerSession(authOptions),
+ getSegments(params.environmentId),
+ ]);
+
+ if (!session) {
+ throw new Error(t("common.session_not_found"));
+ }
+
+ if (!project) {
+ throw new Error(t("common.project_not_found"));
+ }
+
+ const organizationBilling = await getOrganizationBilling(project.organizationId);
+ if (!organizationBilling) {
+ throw new Error(t("common.organization_not_found"));
+ }
+
+ const membershipRole = await getMembershipRoleByUserIdOrganizationId(
+ session?.user.id,
+ project.organizationId
+ );
+ const { isMember } = getAccessFlags(membershipRole);
+
+ const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
+
+ const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
+
+ const isSurveyCreationDeletionDisabled = isMember && hasReadAccess;
+ const locale = session.user.id ? await getUserLocale(session.user.id) : undefined;
+
+ const isUserTargetingAllowed = await getIsContactsEnabled();
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organizationBilling.plan);
+ const isSurveyFollowUpsAllowed = await getSurveyFollowUpsPermission(organizationBilling.plan);
+
+ const userEmail = await getUserEmail(session.user.id);
+
+ const projectLanguages = await getProjectLanguages(project.id);
+
+ if (
+ !survey ||
+ !environment ||
+ !actionClasses ||
+ !contactAttributeKeys ||
+ !project ||
+ !userEmail ||
+ isSurveyCreationDeletionDisabled
+ ) {
+ return ;
+ }
+
+ const isCxMode = searchParams.mode === "cx";
+
+ return (
+
+ );
+};
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/types/survey-follow-up.ts b/apps/web/modules/survey/editor/types/survey-follow-up.ts
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/types/survey-follow-up.ts
rename to apps/web/modules/survey/editor/types/survey-follow-up.ts
diff --git a/apps/web/modules/survey/editor/types/survey-trigger.ts b/apps/web/modules/survey/editor/types/survey-trigger.ts
new file mode 100644
index 0000000000..08127f3077
--- /dev/null
+++ b/apps/web/modules/survey/editor/types/survey-trigger.ts
@@ -0,0 +1,8 @@
+export interface TriggerUpdate {
+ create?: Array<{ actionClassId: string }>;
+ deleteMany?: {
+ actionClassId: {
+ in: string[];
+ };
+ };
+}
diff --git a/apps/web/modules/survey-follow-ups/components/follow-up-action-multi-email-input.tsx b/apps/web/modules/survey/follow-ups/components/follow-up-action-multi-email-input.tsx
similarity index 100%
rename from apps/web/modules/survey-follow-ups/components/follow-up-action-multi-email-input.tsx
rename to apps/web/modules/survey/follow-ups/components/follow-up-action-multi-email-input.tsx
diff --git a/apps/web/modules/survey-follow-ups/components/follow-up-item.tsx b/apps/web/modules/survey/follow-ups/components/follow-up-item.tsx
similarity index 98%
rename from apps/web/modules/survey-follow-ups/components/follow-up-item.tsx
rename to apps/web/modules/survey/follow-ups/components/follow-up-item.tsx
index 4d26abd538..0b71fe1742 100644
--- a/apps/web/modules/survey-follow-ups/components/follow-up-item.tsx
+++ b/apps/web/modules/survey/follow-ups/components/follow-up-item.tsx
@@ -1,6 +1,6 @@
"use client";
-import { FollowUpModal } from "@/modules/survey-follow-ups/components/follow-up-modal";
+import { FollowUpModal } from "@/modules/survey/follow-ups/components/follow-up-modal";
import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
diff --git a/apps/web/modules/survey-follow-ups/components/follow-up-modal.tsx b/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx
similarity index 98%
rename from apps/web/modules/survey-follow-ups/components/follow-up-modal.tsx
rename to apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx
index 00896b23e9..0cbd120a01 100644
--- a/apps/web/modules/survey-follow-ups/components/follow-up-modal.tsx
+++ b/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx
@@ -1,7 +1,11 @@
"use client";
-import { getSurveyFollowUpActionDefaultBody } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
-import FollowUpActionMultiEmailInput from "@/modules/survey-follow-ups/components/follow-up-action-multi-email-input";
+import FollowUpActionMultiEmailInput from "@/modules/survey/follow-ups/components/follow-up-action-multi-email-input";
+import { getSurveyFollowUpActionDefaultBody } from "@/modules/survey/editor/lib/utils";
+import {
+ TCreateSurveyFollowUpForm,
+ ZCreateSurveyFollowUpFormSchema,
+} from "@/modules/survey/editor/types/survey-follow-up";
import { Button } from "@/modules/ui/components/button";
import { Checkbox } from "@/modules/ui/components/checkbox";
import { Editor } from "@/modules/ui/components/editor";
@@ -38,10 +42,6 @@ import { getQuestionIconMap } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
-import {
- TCreateSurveyFollowUpForm,
- ZCreateSurveyFollowUpFormSchema,
-} from "../../../app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/types/survey-follow-up";
interface AddFollowUpModalProps {
localSurvey: TSurvey;
@@ -588,9 +588,9 @@ export const FollowUpModal = ({
{
QUESTIONS_ICON_MAP[
- option.type === "openTextQuestion"
- ? "openText"
- : "contactInfo"
+ option.type === "openTextQuestion"
+ ? "openText"
+ : "contactInfo"
]
}
diff --git a/apps/web/modules/survey-follow-ups/components/follow-ups-view.tsx b/apps/web/modules/survey/follow-ups/components/follow-ups-view.tsx
similarity index 97%
rename from apps/web/modules/survey-follow-ups/components/follow-ups-view.tsx
rename to apps/web/modules/survey/follow-ups/components/follow-ups-view.tsx
index 7f42bc0f8d..ebfaaa789d 100644
--- a/apps/web/modules/survey-follow-ups/components/follow-ups-view.tsx
+++ b/apps/web/modules/survey/follow-ups/components/follow-ups-view.tsx
@@ -1,7 +1,7 @@
"use client";
-import { FollowUpItem } from "@/modules/survey-follow-ups/components/follow-up-item";
-import { FollowUpModal } from "@/modules/survey-follow-ups/components/follow-up-modal";
+import { FollowUpItem } from "@/modules/survey/follow-ups/components/follow-up-item";
+import { FollowUpModal } from "@/modules/survey/follow-ups/components/follow-up-modal";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import { LockIcon, MailIcon } from "lucide-react";
diff --git a/apps/web/modules/survey/follow-ups/lib/utils.ts b/apps/web/modules/survey/follow-ups/lib/utils.ts
new file mode 100644
index 0000000000..1b1e463dac
--- /dev/null
+++ b/apps/web/modules/survey/follow-ups/lib/utils.ts
@@ -0,0 +1,9 @@
+import { Organization } from "@prisma/client";
+import { IS_FORMBRICKS_CLOUD, PROJECT_FEATURE_KEYS } from "@formbricks/lib/constants";
+
+export const getSurveyFollowUpsPermission = async (
+ billingPlan: Organization["billing"]["plan"]
+): Promise => {
+ if (IS_FORMBRICKS_CLOUD) return billingPlan !== PROJECT_FEATURE_KEYS.FREE;
+ return true;
+};
diff --git a/apps/web/modules/survey/lib/action-class.ts b/apps/web/modules/survey/lib/action-class.ts
new file mode 100644
index 0000000000..489892b837
--- /dev/null
+++ b/apps/web/modules/survey/lib/action-class.ts
@@ -0,0 +1,34 @@
+import { ActionClass } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { z } from "zod";
+import { prisma } from "@formbricks/database";
+import { actionClassCache } from "@formbricks/lib/actionClass/cache";
+import { cache } from "@formbricks/lib/cache";
+import { validateInputs } from "@formbricks/lib/utils/validate";
+import { DatabaseError } from "@formbricks/types/errors";
+
+export const getActionClasses = reactCache(
+ async (environmentId: string): Promise =>
+ cache(
+ async () => {
+ validateInputs([environmentId, z.string().cuid2()]);
+
+ try {
+ return await prisma.actionClass.findMany({
+ where: {
+ environmentId: environmentId,
+ },
+ orderBy: {
+ createdAt: "asc",
+ },
+ });
+ } catch (error) {
+ throw new DatabaseError(`Database error when fetching actions for environment ${environmentId}`);
+ }
+ },
+ [`survey-lib-getActionClasses-${environmentId}`],
+ {
+ tags: [actionClassCache.tag.byEnvironmentId(environmentId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/lib/environment.ts b/apps/web/modules/survey/lib/environment.ts
new file mode 100644
index 0000000000..a43e35a0a5
--- /dev/null
+++ b/apps/web/modules/survey/lib/environment.ts
@@ -0,0 +1,42 @@
+import { Environment, Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { z } from "zod";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { environmentCache } from "@formbricks/lib/environment/cache";
+import { validateInputs } from "@formbricks/lib/utils/validate";
+import { DatabaseError } from "@formbricks/types/errors";
+
+export const getEnvironment = reactCache(
+ async (environmentId: string): Promise | null> =>
+ cache(
+ async () => {
+ validateInputs([environmentId, z.string().cuid2()]);
+
+ try {
+ const environment = await prisma.environment.findUnique({
+ where: {
+ id: environmentId,
+ },
+ select: {
+ id: true,
+ appSetupCompleted: true,
+ },
+ });
+
+ return environment;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`survey-lib-getEnvironment-${environmentId}`],
+ {
+ tags: [environmentCache.tag.byId(environmentId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/lib/membership.ts b/apps/web/modules/survey/lib/membership.ts
new file mode 100644
index 0000000000..2353e9e3e1
--- /dev/null
+++ b/apps/web/modules/survey/lib/membership.ts
@@ -0,0 +1,45 @@
+import { OrganizationRole, Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { z } from "zod";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { membershipCache } from "@formbricks/lib/membership/cache";
+import { validateInputs } from "@formbricks/lib/utils/validate";
+import { AuthorizationError, DatabaseError, UnknownError } from "@formbricks/types/errors";
+
+export const getMembershipRoleByUserIdOrganizationId = reactCache(
+ async (userId: string, organizationId: string): Promise =>
+ cache(
+ async () => {
+ validateInputs([userId, z.string()], [organizationId, z.string().cuid2()]);
+
+ try {
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId,
+ organizationId,
+ },
+ },
+ });
+
+ if (!membership) {
+ throw new AuthorizationError("You are not a member of this organization");
+ }
+
+ return membership.role;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+
+ throw new UnknownError("Error while fetching membership");
+ }
+ },
+ [`survey-getMembershipRoleByUserIdOrganizationId-${userId}-${organizationId}`],
+ {
+ tags: [membershipCache.tag.byUserId(userId), membershipCache.tag.byOrganizationId(organizationId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/lib/organization.ts b/apps/web/modules/survey/lib/organization.ts
new file mode 100644
index 0000000000..b345ff75c1
--- /dev/null
+++ b/apps/web/modules/survey/lib/organization.ts
@@ -0,0 +1,68 @@
+import { Organization, Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { organizationCache } from "@formbricks/lib/organization/cache";
+import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+
+export const getOrganizationIdFromEnvironmentId = reactCache(
+ async (environmentId: string): Promise =>
+ cache(
+ async () => {
+ const organization = await prisma.organization.findFirst({
+ where: {
+ projects: {
+ some: {
+ environments: {
+ some: { id: environmentId },
+ },
+ },
+ },
+ },
+ select: {
+ id: true,
+ },
+ });
+
+ if (!organization) {
+ throw new ResourceNotFoundError("Organization", null);
+ }
+
+ return organization.id;
+ },
+ [`survey-lib-getOrganizationIdFromEnvironmentId-${environmentId}`],
+ {
+ tags: [organizationCache.tag.byEnvironmentId(environmentId)],
+ }
+ )()
+);
+
+export const getOrganizationAIKeys = reactCache(
+ async (organizationId: string): Promise | null> =>
+ cache(
+ async () => {
+ try {
+ const organization = await prisma.organization.findUnique({
+ where: {
+ id: organizationId,
+ },
+ select: {
+ isAIEnabled: true,
+ billing: true,
+ },
+ });
+ return organization;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`survey-lib-getOrganizationAIKeys-${organizationId}`],
+ {
+ tags: [organizationCache.tag.byId(organizationId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/lib/project.ts b/apps/web/modules/survey/lib/project.ts
new file mode 100644
index 0000000000..c12ab8a498
--- /dev/null
+++ b/apps/web/modules/survey/lib/project.ts
@@ -0,0 +1,41 @@
+import "server-only";
+import { Project } from "@prisma/client";
+import { Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { projectCache } from "@formbricks/lib/project/cache";
+import { DatabaseError } from "@formbricks/types/errors";
+
+export const getProjectByEnvironmentId = reactCache(
+ async (environmentId: string): Promise =>
+ cache(
+ async () => {
+ let projectPrisma;
+
+ try {
+ projectPrisma = await prisma.project.findFirst({
+ where: {
+ environments: {
+ some: {
+ id: environmentId,
+ },
+ },
+ },
+ });
+
+ return projectPrisma;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+ },
+ [`survey-lib-getProjectByEnvironmentId-${environmentId}`],
+ {
+ tags: [projectCache.tag.byEnvironmentId(environmentId)],
+ }
+ )()
+);
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/user.ts b/apps/web/modules/survey/lib/response.ts
similarity index 53%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/user.ts
rename to apps/web/modules/survey/lib/response.ts
index 46aab944cb..45f7af67fe 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/user.ts
+++ b/apps/web/modules/survey/lib/response.ts
@@ -2,21 +2,20 @@ import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
-import { userCache } from "@formbricks/lib/user/cache";
+import { responseCache } from "@formbricks/lib/response/cache";
import { DatabaseError } from "@formbricks/types/errors";
-export const getUserEmail = reactCache(
- (userId: string): Promise =>
+export const getResponseCountBySurveyId = reactCache(
+ async (surveyId: string): Promise =>
cache(
async () => {
try {
- const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
-
- if (!user) {
- return null;
- }
-
- return user.email;
+ const responseCount = await prisma.response.count({
+ where: {
+ surveyId,
+ },
+ });
+ return responseCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
@@ -25,9 +24,9 @@ export const getUserEmail = reactCache(
throw error;
}
},
- [`getUserEmail-${userId}`],
+ [`survey-editor-getResponseCountBySurveyId-${surveyId}`],
{
- tags: [userCache.tag.byId(userId)],
+ tags: [responseCache.tag.bySurveyId(surveyId)],
}
)()
);
diff --git a/apps/web/modules/survey/lib/survey.ts b/apps/web/modules/survey/lib/survey.ts
new file mode 100644
index 0000000000..ba35bc36bf
--- /dev/null
+++ b/apps/web/modules/survey/lib/survey.ts
@@ -0,0 +1,143 @@
+import { transformPrismaSurvey } from "@/modules/survey/lib/utils";
+import { Organization, Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { organizationCache } from "@formbricks/lib/organization/cache";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+import { TSurvey } from "@formbricks/types/surveys/types";
+
+export const selectSurvey = {
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ name: true,
+ type: true,
+ environmentId: true,
+ createdBy: true,
+ status: true,
+ welcomeCard: true,
+ questions: true,
+ endings: true,
+ hiddenFields: true,
+ variables: true,
+ displayOption: true,
+ recontactDays: true,
+ displayLimit: true,
+ autoClose: true,
+ runOnDate: true,
+ closeOnDate: true,
+ delay: true,
+ displayPercentage: true,
+ autoComplete: true,
+ isVerifyEmailEnabled: true,
+ isSingleResponsePerEmailEnabled: true,
+ redirectUrl: true,
+ projectOverwrites: true,
+ styling: true,
+ surveyClosedMessage: true,
+ singleUse: true,
+ pin: true,
+ resultShareKey: true,
+ showLanguageSwitch: true,
+ languages: {
+ select: {
+ default: true,
+ enabled: true,
+ language: {
+ select: {
+ id: true,
+ code: true,
+ alias: true,
+ createdAt: true,
+ updatedAt: true,
+ projectId: true,
+ },
+ },
+ },
+ },
+ triggers: {
+ select: {
+ actionClass: {
+ select: {
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ environmentId: true,
+ name: true,
+ description: true,
+ type: true,
+ key: true,
+ noCodeConfig: true,
+ },
+ },
+ },
+ },
+ segment: {
+ include: {
+ surveys: {
+ select: {
+ id: true,
+ },
+ },
+ },
+ },
+ followUps: true,
+} satisfies Prisma.SurveySelect;
+
+export const getOrganizationBilling = reactCache(
+ async (organizationId: string): Promise =>
+ cache(
+ async () => {
+ try {
+ const organization = await prisma.organization.findFirst({
+ where: {
+ id: organizationId,
+ },
+ select: {
+ billing: true,
+ },
+ });
+
+ if (!organization) {
+ throw new ResourceNotFoundError("Organization", null);
+ }
+
+ return organization.billing;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`survey-lib-getOrganizationBilling-${organizationId}`],
+ {
+ tags: [organizationCache.tag.byId(organizationId)],
+ }
+ )()
+);
+
+export const getSurvey = reactCache(
+ async (surveyId: string): Promise =>
+ cache(
+ async () => {
+ const survey = await prisma.survey.findUnique({
+ where: { id: surveyId },
+ select: selectSurvey,
+ });
+
+ if (!survey) {
+ throw new ResourceNotFoundError("Survey", surveyId);
+ }
+
+ return transformPrismaSurvey(survey);
+ },
+ [`survey-editor-getSurvey-${surveyId}`],
+ {
+ tags: [surveyCache.tag.byId(surveyId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/lib/utils.ts b/apps/web/modules/survey/lib/utils.ts
new file mode 100644
index 0000000000..f1dceeb11b
--- /dev/null
+++ b/apps/web/modules/survey/lib/utils.ts
@@ -0,0 +1,120 @@
+import "server-only";
+import { Prisma } from "@prisma/client";
+import { generateObject } from "ai";
+import { z } from "zod";
+import { llmModel } from "@formbricks/lib/aiModels";
+import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
+import { TSegment } from "@formbricks/types/segment";
+import {
+ TSurvey,
+ TSurveyFilterCriteria,
+ TSurveyQuestion,
+ TSurveyQuestions,
+} from "@formbricks/types/surveys/types";
+
+export const getInsightsEnabled = async (question: TSurveyQuestion): Promise => {
+ try {
+ const { object } = await generateObject({
+ model: llmModel,
+ schema: z.object({
+ insightsEnabled: z.boolean(),
+ }),
+ prompt: `We extract insights (e.g. feature requests, complaints, other) from survey questions. Can we find them in this question?: ${question.headline.default}`,
+ experimental_telemetry: { isEnabled: true },
+ });
+
+ return object.insightsEnabled;
+ } catch (error) {
+ throw error;
+ }
+};
+
+export const transformPrismaSurvey = (
+ surveyPrisma: any
+): T => {
+ let segment: TSegment | null = null;
+
+ if (surveyPrisma.segment) {
+ segment = {
+ ...surveyPrisma.segment,
+ surveys: surveyPrisma.segment.surveys.map((survey) => survey.id),
+ };
+ }
+
+ const transformedSurvey = {
+ ...surveyPrisma,
+ displayPercentage: Number(surveyPrisma.displayPercentage) || null,
+ segment,
+ } as T;
+
+ return transformedSurvey;
+};
+
+export const buildWhereClause = (filterCriteria?: TSurveyFilterCriteria) => {
+ const whereClause: Prisma.SurveyWhereInput["AND"] = [];
+
+ // for name
+ if (filterCriteria?.name) {
+ whereClause.push({ name: { contains: filterCriteria.name, mode: "insensitive" } });
+ }
+
+ // for status
+ if (filterCriteria?.status && filterCriteria?.status?.length) {
+ whereClause.push({ status: { in: filterCriteria.status } });
+ }
+
+ // for type
+ if (filterCriteria?.type && filterCriteria?.type?.length) {
+ whereClause.push({ type: { in: filterCriteria.type } });
+ }
+
+ // for createdBy
+ if (filterCriteria?.createdBy?.value && filterCriteria?.createdBy?.value?.length) {
+ if (filterCriteria.createdBy.value.length === 1) {
+ if (filterCriteria.createdBy.value[0] === "you") {
+ whereClause.push({ createdBy: filterCriteria.createdBy.userId });
+ }
+ if (filterCriteria.createdBy.value[0] === "others") {
+ whereClause.push({
+ OR: [
+ {
+ createdBy: {
+ not: filterCriteria.createdBy.userId,
+ },
+ },
+ {
+ createdBy: null,
+ },
+ ],
+ });
+ }
+ }
+ }
+
+ return { AND: whereClause };
+};
+
+export const buildOrderByClause = (
+ sortBy?: TSurveyFilterCriteria["sortBy"]
+): Prisma.SurveyOrderByWithRelationInput[] | undefined => {
+ const orderMapping: { [key: string]: Prisma.SurveyOrderByWithRelationInput } = {
+ name: { name: "asc" },
+ createdAt: { createdAt: "desc" },
+ updatedAt: { updatedAt: "desc" },
+ };
+
+ return sortBy ? [orderMapping[sortBy] || { updatedAt: "desc" }] : undefined;
+};
+
+export const anySurveyHasFilters = (surveys: TSurvey[]): boolean => {
+ return surveys.some((survey) => {
+ if ("segment" in survey && survey.segment) {
+ return survey.segment.filters && survey.segment.filters.length > 0;
+ }
+ return false;
+ });
+};
+
+export const doesSurveyHasOpenTextQuestion = (questions: TSurveyQuestions): boolean => {
+ return questions.some((question) => question.type === "openText");
+};
diff --git a/apps/web/app/s/[surveyId]/actions.ts b/apps/web/modules/survey/link/actions.ts
similarity index 57%
rename from apps/web/app/s/[surveyId]/actions.ts
rename to apps/web/modules/survey/link/actions.ts
index b6d1a21a86..8c694c0d0a 100644
--- a/apps/web/app/s/[surveyId]/actions.ts
+++ b/apps/web/modules/survey/link/actions.ts
@@ -4,11 +4,10 @@ import { actionClient } from "@/lib/utils/action-client";
import { getOrganizationIdFromSurveyId } from "@/lib/utils/helper";
import { getOrganizationLogoUrl } from "@/modules/ee/whitelabel/email-customization/lib/organization";
import { sendLinkSurveyToVerifiedEmail } from "@/modules/email";
+import { getSurvey } from "@/modules/survey/lib/survey";
+import { isSurveyResponsePresent } from "@/modules/survey/link/lib/response";
+import { getSurveyPin } from "@/modules/survey/link/lib/survey";
import { z } from "zod";
-import { verifyTokenForLinkSurvey } from "@formbricks/lib/jwt";
-import { getIfResponseWithSurveyIdAndEmailExist } from "@formbricks/lib/response/service";
-import { getSurvey } from "@formbricks/lib/survey/service";
-import { ZId } from "@formbricks/types/common";
import { ZLinkSurveyEmailData } from "@formbricks/types/email";
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
@@ -22,27 +21,22 @@ export const sendLinkSurveyEmailAction = actionClient
return { success: true };
});
-const ZVerifyTokenAction = z.object({
- surveyId: ZId,
- token: z.string(),
-});
-
-export const verifyTokenAction = actionClient.schema(ZVerifyTokenAction).action(async ({ parsedInput }) => {
- return await verifyTokenForLinkSurvey(parsedInput.token, parsedInput.surveyId);
-});
-
const ZValidateSurveyPinAction = z.object({
- surveyId: ZId,
+ surveyId: z.string().cuid2(),
pin: z.string(),
});
export const validateSurveyPinAction = actionClient
.schema(ZValidateSurveyPinAction)
.action(async ({ parsedInput }) => {
- const survey = await getSurvey(parsedInput.surveyId);
- if (!survey) throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
+ const surveyPin = await getSurveyPin(parsedInput.surveyId);
+ if (!surveyPin) {
+ throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
+ }
- const originalPin = survey.pin?.toString();
+ const originalPin = surveyPin.toString();
+
+ const survey = await getSurvey(parsedInput.surveyId);
if (!originalPin) return { survey };
if (originalPin !== parsedInput.pin) {
@@ -52,13 +46,13 @@ export const validateSurveyPinAction = actionClient
return { survey };
});
-const ZGetIfResponseWithSurveyIdAndEmailExistAction = z.object({
- surveyId: ZId,
- email: z.string(),
+const ZIsSurveyResponsePresentAction = z.object({
+ surveyId: z.string().cuid2(),
+ email: z.string().email(),
});
-export const getIfResponseWithSurveyIdAndEmailExistAction = actionClient
- .schema(ZGetIfResponseWithSurveyIdAndEmailExistAction)
+export const isSurveyResponsePresentAction = actionClient
+ .schema(ZIsSurveyResponsePresentAction)
.action(async ({ parsedInput }) => {
- return await getIfResponseWithSurveyIdAndEmailExist(parsedInput.surveyId, parsedInput.email);
+ return await isSurveyResponsePresent(parsedInput.surveyId, parsedInput.email);
});
diff --git a/apps/web/app/s/[surveyId]/components/legal-footer.tsx b/apps/web/modules/survey/link/components/legal-footer.tsx
similarity index 100%
rename from apps/web/app/s/[surveyId]/components/legal-footer.tsx
rename to apps/web/modules/survey/link/components/legal-footer.tsx
diff --git a/apps/web/app/s/[surveyId]/components/link-survey-wrapper.tsx b/apps/web/modules/survey/link/components/link-survey-wrapper.tsx
similarity index 71%
rename from apps/web/app/s/[surveyId]/components/link-survey-wrapper.tsx
rename to apps/web/modules/survey/link/components/link-survey-wrapper.tsx
index 53d8eb2727..18acc02a39 100644
--- a/apps/web/app/s/[surveyId]/components/link-survey-wrapper.tsx
+++ b/apps/web/modules/survey/link/components/link-survey-wrapper.tsx
@@ -1,17 +1,20 @@
-import { LegalFooter } from "@/app/s/[surveyId]/components/legal-footer";
-import { SurveyLoadingAnimation } from "@/app/s/[surveyId]/components/survey-loading-animation";
+import { LegalFooter } from "@/modules/survey/link/components/legal-footer";
+import { SurveyLoadingAnimation } from "@/modules/survey/link/components/survey-loading-animation";
import { ClientLogo } from "@/modules/ui/components/client-logo";
import { MediaBackground } from "@/modules/ui/components/media-background";
import { ResetProgressButton } from "@/modules/ui/components/reset-progress-button";
+import { Project, SurveyType } from "@prisma/client";
import { type JSX, useState } from "react";
import { cn } from "@formbricks/lib/cn";
-import { TProject, TProjectStyling } from "@formbricks/types/project";
-import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
+import { TProjectStyling } from "@formbricks/types/project";
+import { TSurveyStyling } from "@formbricks/types/surveys/types";
interface LinkSurveyWrapperProps {
children: JSX.Element;
- project: TProject;
- survey: TSurvey;
+ project: Pick;
+ isWelcomeCardEnabled: boolean;
+ surveyId: string;
+ surveyType: SurveyType;
isPreview: boolean;
isEmbed: boolean;
determineStyling: () => TSurveyStyling | TProjectStyling;
@@ -26,7 +29,9 @@ interface LinkSurveyWrapperProps {
export const LinkSurveyWrapper = ({
children,
project,
- survey,
+ isWelcomeCardEnabled,
+ surveyType,
+ surveyId,
isPreview,
isEmbed,
determineStyling,
@@ -54,7 +59,10 @@ export const LinkSurveyWrapper = ({
styling.cardArrangement?.linkSurveys === "straight" && "pt-6",
styling.cardArrangement?.linkSurveys === "casual" && "px-6 py-10"
)}>
-
+
{children}
);
@@ -62,13 +70,16 @@ export const LinkSurveyWrapper = ({
return (
-
+
- {!styling.isLogoHidden && project.logo?.url &&
}
+ {!styling.isLogoHidden && project.logo?.url &&
}
{isPreview && (
@@ -85,7 +96,7 @@ export const LinkSurveyWrapper = ({
IMPRINT_URL={IMPRINT_URL}
PRIVACY_URL={PRIVACY_URL}
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
- surveyUrl={webAppUrl + "/s/" + survey.id}
+ surveyUrl={webAppUrl + "/s/" + surveyId}
/>
);
diff --git a/apps/web/app/s/[surveyId]/components/link-survey.tsx b/apps/web/modules/survey/link/components/link-survey.tsx
similarity index 93%
rename from apps/web/app/s/[surveyId]/components/link-survey.tsx
rename to apps/web/modules/survey/link/components/link-survey.tsx
index a5304bdfe1..1b347f1081 100644
--- a/apps/web/app/s/[surveyId]/components/link-survey.tsx
+++ b/apps/web/modules/survey/link/components/link-survey.tsx
@@ -1,10 +1,11 @@
"use client";
-import { LinkSurveyWrapper } from "@/app/s/[surveyId]/components/link-survey-wrapper";
-import { SurveyLinkUsed } from "@/app/s/[surveyId]/components/survey-link-used";
-import { VerifyEmail } from "@/app/s/[surveyId]/components/verify-email";
-import { getPrefillValue } from "@/app/s/[surveyId]/lib/prefilling";
+import { LinkSurveyWrapper } from "@/modules/survey/link/components/link-survey-wrapper";
+import { SurveyLinkUsed } from "@/modules/survey/link/components/survey-link-used";
+import { VerifyEmail } from "@/modules/survey/link/components/verify-email";
+import { getPrefillValue } from "@/modules/survey/link/lib/utils";
import { SurveyInline } from "@/modules/ui/components/survey";
+import { Project, Response } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
@@ -12,13 +13,7 @@ import { FormbricksAPI } from "@formbricks/api";
import { ResponseQueue } from "@formbricks/lib/responseQueue";
import { SurveyState } from "@formbricks/lib/surveyState";
import { TJsFileUploadParams } from "@formbricks/types/js";
-import { TProject } from "@formbricks/types/project";
-import {
- TResponse,
- TResponseData,
- TResponseHiddenFieldValue,
- TResponseUpdate,
-} from "@formbricks/types/responses";
+import { TResponseData, TResponseHiddenFieldValue, TResponseUpdate } from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -29,10 +24,10 @@ let setResponseData = (_: TResponseData) => {};
interface LinkSurveyProps {
survey: TSurvey;
- project: TProject;
+ project: Pick
;
emailVerificationStatus?: string;
singleUseId?: string;
- singleUseResponse?: TResponse;
+ singleUseResponse?: Pick;
webAppUrl: string;
responseCount?: number;
verifiedEmail?: string;
@@ -203,8 +198,10 @@ export const LinkSurvey = ({
return (
;
emailVerificationStatus?: string;
singleUseId?: string;
- singleUseResponse?: TResponse;
+ singleUseResponse?: Pick;
webAppUrl: string;
IMPRINT_URL?: string;
PRIVACY_URL?: string;
diff --git a/apps/web/app/s/[surveyId]/components/survey-inactive.tsx b/apps/web/modules/survey/link/components/survey-inactive.tsx
similarity index 100%
rename from apps/web/app/s/[surveyId]/components/survey-inactive.tsx
rename to apps/web/modules/survey/link/components/survey-inactive.tsx
diff --git a/apps/web/app/s/[surveyId]/components/survey-link-used.tsx b/apps/web/modules/survey/link/components/survey-link-used.tsx
similarity index 100%
rename from apps/web/app/s/[surveyId]/components/survey-link-used.tsx
rename to apps/web/modules/survey/link/components/survey-link-used.tsx
diff --git a/apps/web/app/s/[surveyId]/components/survey-loading-animation.tsx b/apps/web/modules/survey/link/components/survey-loading-animation.tsx
similarity index 96%
rename from apps/web/app/s/[surveyId]/components/survey-loading-animation.tsx
rename to apps/web/modules/survey/link/components/survey-loading-animation.tsx
index 8ee9c5f686..9a8371bbb7 100644
--- a/apps/web/app/s/[surveyId]/components/survey-loading-animation.tsx
+++ b/apps/web/modules/survey/link/components/survey-loading-animation.tsx
@@ -3,16 +3,15 @@ import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
import Image from "next/image";
import { useCallback, useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
-import { TSurvey } from "@formbricks/types/surveys/types";
interface SurveyLoadingAnimationProps {
- survey: TSurvey;
+ isWelcomeCardEnabled: boolean;
isBackgroundLoaded?: boolean;
isBrandingEnabled: boolean;
}
export const SurveyLoadingAnimation = ({
- survey,
+ isWelcomeCardEnabled,
isBackgroundLoaded = true,
isBrandingEnabled,
}: SurveyLoadingAnimationProps) => {
@@ -21,7 +20,7 @@ export const SurveyLoadingAnimation = ({
const [isMediaLoaded, setIsMediaLoaded] = useState(false); // Tracks if all media are fully loaded
const [isSurveyPackageLoaded, setIsSurveyPackageLoaded] = useState(false); // Tracks if the survey package has been loaded into the DOM
const isReadyToTransition = isMediaLoaded && minTimePassed && isBackgroundLoaded;
- const cardId = survey.welcomeCard.enabled ? `questionCard--1` : `questionCard-0`;
+ const cardId = isWelcomeCardEnabled ? `questionCard--1` : `questionCard-0`;
// Function to check if all media elements (images and iframes) within the survey card are loaded
const checkMediaLoaded = useCallback(() => {
diff --git a/apps/web/app/s/[surveyId]/components/verify-email.tsx b/apps/web/modules/survey/link/components/verify-email.tsx
similarity index 97%
rename from apps/web/app/s/[surveyId]/components/verify-email.tsx
rename to apps/web/modules/survey/link/components/verify-email.tsx
index 2a2c4ba211..e360a61aa2 100644
--- a/apps/web/app/s/[surveyId]/components/verify-email.tsx
+++ b/apps/web/modules/survey/link/components/verify-email.tsx
@@ -1,10 +1,10 @@
"use client";
-import {
- getIfResponseWithSurveyIdAndEmailExistAction,
- sendLinkSurveyEmailAction,
-} from "@/app/s/[surveyId]/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
+import {
+ isSurveyResponsePresentAction,
+ sendLinkSurveyEmailAction,
+} from "@/modules/survey/link/actions";
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
@@ -61,7 +61,7 @@ export const VerifyEmail = ({
const submitEmail = async (emailInput: TVerifyEmailInput) => {
const email = emailInput.email.toLowerCase();
if (localSurvey.isSingleResponsePerEmailEnabled) {
- const actionResult = await getIfResponseWithSurveyIdAndEmailExistAction({
+ const actionResult = await isSurveyResponsePresentAction({
surveyId: localSurvey.id,
email,
});
diff --git a/apps/web/modules/survey/link/layout.tsx b/apps/web/modules/survey/link/layout.tsx
new file mode 100644
index 0000000000..06bf60fae3
--- /dev/null
+++ b/apps/web/modules/survey/link/layout.tsx
@@ -0,0 +1,13 @@
+import { Viewport } from "next";
+
+export const viewport: Viewport = {
+ width: "device-width",
+ initialScale: 1.0,
+ maximumScale: 1.0,
+ userScalable: false,
+ viewportFit: "contain",
+};
+
+export const LinkSurveyLayout = ({ children }) => {
+ return {children}
;
+};
diff --git a/apps/web/app/s/[surveyId]/lib/footerlogo.svg b/apps/web/modules/survey/link/lib/footerlogo.svg
similarity index 100%
rename from apps/web/app/s/[surveyId]/lib/footerlogo.svg
rename to apps/web/modules/survey/link/lib/footerlogo.svg
diff --git a/apps/web/app/s/[surveyId]/lib/helpers.ts b/apps/web/modules/survey/link/lib/helper.ts
similarity index 96%
rename from apps/web/app/s/[surveyId]/lib/helpers.ts
rename to apps/web/modules/survey/link/lib/helper.ts
index 37e0cd411c..055e709b1f 100644
--- a/apps/web/app/s/[surveyId]/lib/helpers.ts
+++ b/apps/web/modules/survey/link/lib/helper.ts
@@ -1,3 +1,4 @@
+import "server-only";
import { verifyTokenForLinkSurvey } from "@formbricks/lib/jwt";
interface emailVerificationDetails {
diff --git a/apps/web/modules/survey/link/lib/project.ts b/apps/web/modules/survey/link/lib/project.ts
new file mode 100644
index 0000000000..00237b4729
--- /dev/null
+++ b/apps/web/modules/survey/link/lib/project.ts
@@ -0,0 +1,49 @@
+import "server-only";
+import { Prisma, Project } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { projectCache } from "@formbricks/lib/project/cache";
+import { validateInputs } from "@formbricks/lib/utils/validate";
+import { ZId } from "@formbricks/types/common";
+import { DatabaseError } from "@formbricks/types/errors";
+
+export const getProjectByEnvironmentId = reactCache(
+ async (environmentId: string): Promise | null> =>
+ cache(
+ async () => {
+ validateInputs([environmentId, ZId]);
+
+ let projectPrisma;
+
+ try {
+ projectPrisma = await prisma.project.findFirst({
+ where: {
+ environments: {
+ some: {
+ id: environmentId,
+ },
+ },
+ },
+ select: {
+ styling: true,
+ logo: true,
+ linkSurveyBranding: true,
+ },
+ });
+
+ return projectPrisma;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+ },
+ [`survey-link-surveys-getProjectByEnvironmentId-${environmentId}`],
+ {
+ tags: [projectCache.tag.byEnvironmentId(environmentId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/link/lib/response.ts b/apps/web/modules/survey/link/lib/response.ts
new file mode 100644
index 0000000000..b187466184
--- /dev/null
+++ b/apps/web/modules/survey/link/lib/response.ts
@@ -0,0 +1,71 @@
+import "server-only";
+import { Prisma, Response } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { responseCache } from "@formbricks/lib/response/cache";
+import { DatabaseError } from "@formbricks/types/errors";
+
+export const isSurveyResponsePresent = reactCache(
+ async (surveyId: string, email: string): Promise =>
+ cache(
+ async () => {
+ try {
+ const response = await prisma.response.findFirst({
+ where: {
+ surveyId,
+ data: {
+ path: ["verifiedEmail"],
+ equals: email,
+ },
+ },
+ select: { id: true },
+ });
+
+ return !!response;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`link-surveys-isSurveyResponsePresent-${surveyId}-${email}`],
+ {
+ tags: [responseCache.tag.bySurveyId(surveyId)],
+ }
+ )()
+);
+
+export const getResponseBySingleUseId = reactCache(
+ async (surveyId: string, singleUseId: string): Promise | null> =>
+ cache(
+ async () => {
+ try {
+ const response = await prisma.response.findFirst({
+ where: {
+ surveyId,
+ singleUseId,
+ },
+ select: {
+ id: true,
+ finished: true,
+ },
+ });
+
+ return response;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`link-surveys-getResponseBySingleUseId-${surveyId}-${singleUseId}`],
+ {
+ tags: [responseCache.tag.bySingleUseId(surveyId, singleUseId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/link/lib/survey.ts b/apps/web/modules/survey/link/lib/survey.ts
new file mode 100644
index 0000000000..32eaafd232
--- /dev/null
+++ b/apps/web/modules/survey/link/lib/survey.ts
@@ -0,0 +1,71 @@
+import "server-only";
+import { Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+
+export const getSurveyMetadata = reactCache(async (surveyId: string) =>
+ cache(
+ async () => {
+ try {
+ const survey = await prisma.survey.findUnique({
+ where: {
+ id: surveyId,
+ },
+ select: {
+ id: true,
+ type: true,
+ status: true,
+ environmentId: true,
+ name: true,
+ styling: true,
+ },
+ });
+
+ if (!survey) {
+ throw new ResourceNotFoundError("Survey", surveyId);
+ }
+
+ return survey;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+ },
+
+ [`link-survey-getSurveyMetadata-${surveyId}`],
+ {
+ tags: [surveyCache.tag.byId(surveyId)],
+ }
+ )()
+);
+
+export const getSurveyPin = reactCache(async (surveyId: string) =>
+ cache(
+ async () => {
+ const survey = await prisma.survey.findUnique({
+ where: {
+ id: surveyId,
+ },
+ select: {
+ pin: true,
+ },
+ });
+
+ if (!survey) {
+ throw new ResourceNotFoundError("Survey", surveyId);
+ }
+
+ return survey.pin;
+ },
+ [`link-survey-getSurveyPin-${surveyId}`],
+ {
+ tags: [surveyCache.tag.byId(surveyId)],
+ }
+ )()
+);
diff --git a/apps/web/app/s/[surveyId]/lib/prefilling.ts b/apps/web/modules/survey/link/lib/utils.ts
similarity index 97%
rename from apps/web/app/s/[surveyId]/lib/prefilling.ts
rename to apps/web/modules/survey/link/lib/utils.ts
index 79cc718867..d801ae910a 100644
--- a/apps/web/app/s/[surveyId]/lib/prefilling.ts
+++ b/apps/web/modules/survey/link/lib/utils.ts
@@ -30,7 +30,7 @@ export const getPrefillValue = (
return Object.keys(prefillAnswer).length > 0 ? prefillAnswer : undefined;
};
-export const checkValidity = (question: TSurveyQuestion, answer: string, language: string): boolean => {
+const checkValidity = (question: TSurveyQuestion, answer: string, language: string): boolean => {
if (question.required && (!answer || answer === "")) return false;
try {
switch (question.type) {
@@ -100,7 +100,7 @@ export const checkValidity = (question: TSurveyQuestion, answer: string, languag
}
};
-export const transformAnswer = (
+const transformAnswer = (
question: TSurveyQuestion,
answer: string,
language: string
diff --git a/apps/web/modules/survey/link/loading.tsx b/apps/web/modules/survey/link/loading.tsx
new file mode 100644
index 0000000000..c57df93712
--- /dev/null
+++ b/apps/web/modules/survey/link/loading.tsx
@@ -0,0 +1,11 @@
+"use client";
+export const LinkSurveyLoading = () => {
+ return (
+
+ );
+};
diff --git a/apps/web/app/s/[surveyId]/metadata.ts b/apps/web/modules/survey/link/metadata.ts
similarity index 79%
rename from apps/web/app/s/[surveyId]/metadata.ts
rename to apps/web/modules/survey/link/metadata.ts
index b3eb6606c3..7517d88bd5 100644
--- a/apps/web/app/s/[surveyId]/metadata.ts
+++ b/apps/web/modules/survey/link/metadata.ts
@@ -1,23 +1,16 @@
+import { getSurveyMetadata } from "@/modules/survey/link/lib/survey";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { WEBAPP_URL } from "@formbricks/lib/constants";
-import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
-import { getSurvey } from "@formbricks/lib/survey/service";
export const getMetadataForLinkSurvey = async (surveyId: string): Promise => {
- const survey = await getSurvey(surveyId);
+ const survey = await getSurveyMetadata(surveyId);
if (!survey || survey.type !== "link" || survey.status === "draft") {
notFound();
}
- const project = await getProjectByEnvironmentId(survey.environmentId);
-
- if (!project) {
- throw new Error("Project not found");
- }
-
const brandColor = getBrandColorForURL(survey.styling?.brandColor?.light ?? COLOR_DEFAULTS.brandColor);
const surveyName = getNameForURL(survey.name);
diff --git a/apps/web/modules/survey/link/not-found.tsx b/apps/web/modules/survey/link/not-found.tsx
new file mode 100644
index 0000000000..c35983b89b
--- /dev/null
+++ b/apps/web/modules/survey/link/not-found.tsx
@@ -0,0 +1,27 @@
+import { Button } from "@/modules/ui/components/button";
+import { HelpCircleIcon } from "lucide-react";
+import { StaticImport } from "next/dist/shared/lib/get-img-props";
+import Image from "next/image";
+import Link from "next/link";
+import footerLogo from "./lib/footerlogo.svg";
+
+export const LinkSurveyNotFound = () => {
+ return (
+
+
+
+
,
+
Survey not found.
+
There is no survey with this ID.
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/modules/survey/link/page.tsx b/apps/web/modules/survey/link/page.tsx
new file mode 100644
index 0000000000..087b9b9fb7
--- /dev/null
+++ b/apps/web/modules/survey/link/page.tsx
@@ -0,0 +1,190 @@
+import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
+import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
+import { getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization";
+import { getResponseCountBySurveyId } from "@/modules/survey/lib/response";
+import { getOrganizationBilling } from "@/modules/survey/lib/survey";
+import { getSurvey } from "@/modules/survey/lib/survey";
+import { LinkSurvey } from "@/modules/survey/link/components/link-survey";
+import { PinScreen } from "@/modules/survey/link/components/pin-screen";
+import { SurveyInactive } from "@/modules/survey/link/components/survey-inactive";
+import { getEmailVerificationDetails } from "@/modules/survey/link/lib/helper";
+import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
+import { getResponseBySingleUseId } from "@/modules/survey/link/lib/response";
+import { getMetadataForLinkSurvey } from "@/modules/survey/link/metadata";
+import { Response } from "@prisma/client";
+import type { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { IMPRINT_URL, IS_FORMBRICKS_CLOUD, PRIVACY_URL, WEBAPP_URL } from "@formbricks/lib/constants";
+import { findMatchingLocale } from "@formbricks/lib/utils/locale";
+import { ZId } from "@formbricks/types/common";
+
+interface LinkSurveyPageProps {
+ params: Promise<{
+ surveyId: string;
+ }>;
+ searchParams: Promise<{
+ suId?: string;
+ verify?: string;
+ lang?: string;
+ embed?: string;
+ preview?: string;
+ }>;
+}
+
+export const generateMetadata = async (props: LinkSurveyPageProps): Promise => {
+ const params = await props.params;
+ const validId = ZId.safeParse(params.surveyId);
+ if (!validId.success) {
+ notFound();
+ }
+
+ return getMetadataForLinkSurvey(params.surveyId);
+};
+
+export const LinkSurveyPage = async (props: LinkSurveyPageProps) => {
+ const searchParams = await props.searchParams;
+ const params = await props.params;
+ const validId = ZId.safeParse(params.surveyId);
+ if (!validId.success) {
+ notFound();
+ }
+ const isPreview = searchParams.preview === "true";
+ const survey = await getSurvey(params.surveyId);
+ const locale = await findMatchingLocale();
+ const suId = searchParams.suId;
+ const langParam = searchParams.lang; //can either be language code or alias
+ const isSingleUseSurvey = survey?.singleUse?.enabled;
+ const isSingleUseSurveyEncrypted = survey?.singleUse?.isEncrypted;
+ const isEmbed = searchParams.embed === "true";
+ if (!survey || survey.type !== "link" || survey.status === "draft") {
+ notFound();
+ }
+
+ const organizationId = await getOrganizationIdFromEnvironmentId(survey.environmentId);
+ const organizationBilling = await getOrganizationBilling(organizationId);
+ if (!organizationBilling) {
+ throw new Error("Organization not found");
+ }
+ const isMultiLanguageAllowed = await getMultiLanguagePermission(organizationBilling.plan);
+
+ if (survey.status !== "inProgress" && !isPreview) {
+ return (
+
+ );
+ }
+
+ let singleUseId: string | undefined = undefined;
+ if (isSingleUseSurvey) {
+ // check if the single use id is present for single use surveys
+ if (!suId) {
+ return ;
+ }
+
+ // if encryption is enabled, validate the single use id
+ let validatedSingleUseId: string | undefined = undefined;
+ if (isSingleUseSurveyEncrypted) {
+ validatedSingleUseId = validateSurveySingleUseId(suId);
+ if (!validatedSingleUseId) {
+ return ;
+ }
+ }
+ // if encryption is disabled, use the suId as is
+ singleUseId = validatedSingleUseId ?? suId;
+ }
+
+ let singleUseResponse: Pick | undefined = undefined;
+ if (isSingleUseSurvey) {
+ try {
+ singleUseResponse = singleUseId
+ ? ((await getResponseBySingleUseId(survey.id, singleUseId)) ?? undefined)
+ : undefined;
+ } catch (error) {
+ singleUseResponse = undefined;
+ }
+ }
+
+ // verify email: Check if the survey requires email verification
+ let emailVerificationStatus = "";
+ let verifiedEmail: string | undefined = undefined;
+
+ if (survey.isVerifyEmailEnabled) {
+ const token = searchParams.verify;
+
+ if (token) {
+ const emailVerificationDetails = await getEmailVerificationDetails(survey.id, token);
+ emailVerificationStatus = emailVerificationDetails.status;
+ verifiedEmail = emailVerificationDetails.email;
+ }
+ }
+
+ // get project and person
+ const project = await getProjectByEnvironmentId(survey.environmentId);
+ if (!project) {
+ throw new Error("Project not found");
+ }
+
+ const getLanguageCode = (): string => {
+ if (!langParam || !isMultiLanguageAllowed) return "default";
+ else {
+ const selectedLanguage = survey.languages.find((surveyLanguage) => {
+ return (
+ surveyLanguage.language.code === langParam.toLowerCase() ||
+ surveyLanguage.language.alias?.toLowerCase() === langParam.toLowerCase()
+ );
+ });
+ if (selectedLanguage?.default || !selectedLanguage?.enabled) {
+ return "default";
+ }
+ return selectedLanguage.language.code;
+ }
+ };
+
+ const languageCode = getLanguageCode();
+
+ const isSurveyPinProtected = Boolean(survey.pin);
+ const responseCount = await getResponseCountBySurveyId(survey.id);
+
+ if (isSurveyPinProtected) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts b/apps/web/modules/survey/list/actions.ts
similarity index 84%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts
rename to apps/web/modules/survey/list/actions.ts
index 7d63e7939c..3e4a4df55e 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts
+++ b/apps/web/modules/survey/list/actions.ts
@@ -1,6 +1,5 @@
"use server";
-import { getSurvey, getSurveys } from "@/app/(app)/environments/[environmentId]/surveys/lib/surveys";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import {
@@ -9,17 +8,21 @@ import {
getProjectIdFromEnvironmentId,
getProjectIdFromSurveyId,
} from "@/lib/utils/helper";
-import { getEnvironment } from "@/lib/utils/services";
+import { getProjectIdIfEnvironmentExists } from "@/modules/survey/list/lib/environment";
+import { getUserProjects } from "@/modules/survey/list/lib/project";
+import {
+ copySurveyToOtherEnvironment,
+ deleteSurvey,
+ getSurvey,
+ getSurveys,
+} from "@/modules/survey/list/lib/survey";
import { z } from "zod";
-import { getUserProjects } from "@formbricks/lib/project/service";
-import { copySurveyToOtherEnvironment, deleteSurvey } from "@formbricks/lib/survey/service";
import { generateSurveySingleUseId } from "@formbricks/lib/utils/singleUseSurveys";
-import { ZId } from "@formbricks/types/common";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZSurveyFilterCriteria } from "@formbricks/types/surveys/types";
const ZGetSurveyAction = z.object({
- surveyId: ZId,
+ surveyId: z.string().cuid2(),
});
export const getSurveyAction = authenticatedActionClient
@@ -45,21 +48,21 @@ export const getSurveyAction = authenticatedActionClient
});
const ZCopySurveyToOtherEnvironmentAction = z.object({
- environmentId: ZId,
- surveyId: ZId,
- targetEnvironmentId: ZId,
+ environmentId: z.string().cuid2(),
+ surveyId: z.string().cuid2(),
+ targetEnvironmentId: z.string().cuid2(),
});
export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
.schema(ZCopySurveyToOtherEnvironmentAction)
.action(async ({ ctx, parsedInput }) => {
- const sourceEnvironment = await getEnvironment(parsedInput.environmentId);
- const targetEnvironment = await getEnvironment(parsedInput.targetEnvironmentId);
+ const sourceEnvironmentProjectId = await getProjectIdIfEnvironmentExists(parsedInput.environmentId);
+ const targetEnvironmentProjectId = await getProjectIdIfEnvironmentExists(parsedInput.targetEnvironmentId);
- if (!sourceEnvironment || !targetEnvironment) {
+ if (!sourceEnvironmentProjectId || !targetEnvironmentProjectId) {
throw new ResourceNotFoundError(
"Environment",
- sourceEnvironment ? parsedInput.targetEnvironmentId : parsedInput.environmentId
+ sourceEnvironmentProjectId ? parsedInput.targetEnvironmentId : parsedInput.environmentId
);
}
@@ -74,7 +77,7 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
{
type: "projectTeam",
minPermission: "readWrite",
- projectId: sourceEnvironment.projectId,
+ projectId: sourceEnvironmentProjectId,
},
],
});
@@ -90,7 +93,7 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
{
type: "projectTeam",
minPermission: "readWrite",
- projectId: targetEnvironment.projectId,
+ projectId: targetEnvironmentProjectId,
},
],
});
@@ -104,7 +107,7 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
});
const ZGetProjectsByEnvironmentIdAction = z.object({
- environmentId: ZId,
+ environmentId: z.string().cuid2(),
});
export const getProjectsByEnvironmentIdAction = authenticatedActionClient
@@ -131,7 +134,7 @@ export const getProjectsByEnvironmentIdAction = authenticatedActionClient
});
const ZDeleteSurveyAction = z.object({
- surveyId: ZId,
+ surveyId: z.string().cuid2(),
});
export const deleteSurveyAction = authenticatedActionClient
@@ -157,7 +160,7 @@ export const deleteSurveyAction = authenticatedActionClient
});
const ZGenerateSingleUseIdAction = z.object({
- surveyId: ZId,
+ surveyId: z.string().cuid2(),
isEncrypted: z.boolean(),
});
@@ -184,7 +187,7 @@ export const generateSingleUseIdAction = authenticatedActionClient
});
const ZGetSurveysAction = z.object({
- environmentId: ZId,
+ environmentId: z.string().cuid2(),
limit: z.number().optional(),
offset: z.number().optional(),
filterCriteria: ZSurveyFilterCriteria.optional(),
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyForm.tsx b/apps/web/modules/survey/list/components/copy-survey-form.tsx
similarity index 92%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyForm.tsx
rename to apps/web/modules/survey/list/components/copy-survey-form.tsx
index 2877073c1e..4842c71522 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyForm.tsx
+++ b/apps/web/modules/survey/list/components/copy-survey-form.tsx
@@ -1,11 +1,8 @@
"use client";
-import { copySurveyToOtherEnvironmentAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
-import {
- TSurvey,
- TSurveyCopyFormData,
- ZSurveyCopyFormValidation,
-} from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
+import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
+import { TUserProject } from "@/modules/survey/list/types/projects";
+import { TSurvey, TSurveyCopyFormData, ZSurveyCopyFormValidation } from "@/modules/survey/list/types/surveys";
import { Button } from "@/modules/ui/components/button";
import { Checkbox } from "@/modules/ui/components/checkbox";
import { FormControl, FormField, FormItem, FormProvider } from "@/modules/ui/components/form";
@@ -14,19 +11,15 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslate } from "@tolgee/react";
import { useFieldArray, useForm } from "react-hook-form";
import toast from "react-hot-toast";
-import { TProject } from "@formbricks/types/project";
-export const CopySurveyForm = ({
- defaultProjects,
- survey,
- onCancel,
- setOpen,
-}: {
- defaultProjects: TProject[];
+interface ICopySurveyFormProps {
+ defaultProjects: TUserProject[];
survey: TSurvey;
onCancel: () => void;
setOpen: (value: boolean) => void;
-}) => {
+}
+
+export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: ICopySurveyFormProps) => {
const { t } = useTranslate();
const form = useForm({
resolver: zodResolver(ZSurveyCopyFormValidation),
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyModal.tsx b/apps/web/modules/survey/list/components/copy-survey-modal.tsx
similarity index 91%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyModal.tsx
rename to apps/web/modules/survey/list/components/copy-survey-modal.tsx
index ac13c35472..9f200ee472 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyModal.tsx
+++ b/apps/web/modules/survey/list/components/copy-survey-modal.tsx
@@ -1,10 +1,10 @@
"use client";
-import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
+import { TSurvey } from "@/modules/survey/list/types/surveys";
import { Modal } from "@/modules/ui/components/modal";
import { useTranslate } from "@tolgee/react";
import { MousePointerClickIcon } from "lucide-react";
-import SurveyCopyOptions from "./SurveyCopyOptions";
+import SurveyCopyOptions from "./survey-copy-options";
interface CopySurveyModalProps {
open: boolean;
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SortOption.tsx b/apps/web/modules/survey/list/components/sort-option.tsx
similarity index 100%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/components/SortOption.tsx
rename to apps/web/modules/survey/list/components/sort-option.tsx
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyCard.tsx b/apps/web/modules/survey/list/components/survey-card.tsx
similarity index 85%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyCard.tsx
rename to apps/web/modules/survey/list/components/survey-card.tsx
index 4cee6a1b99..55daff5e61 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyCard.tsx
+++ b/apps/web/modules/survey/list/components/survey-card.tsx
@@ -1,9 +1,9 @@
"use client";
-import { generateSingleUseIdAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
-import { SurveyTypeIndicator } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyTypeIndicator";
-import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
+import { generateSingleUseIdAction } from "@/modules/survey/list/actions";
+import { SurveyTypeIndicator } from "@/modules/survey/list/components/survey-type-indicator";
+import { TSurvey } from "@/modules/survey/list/types/surveys";
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
import { useTranslate } from "@tolgee/react";
import Link from "next/link";
@@ -11,14 +11,12 @@ import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { convertDateString, timeSince } from "@formbricks/lib/time";
-import { TEnvironment } from "@formbricks/types/environment";
import { TUserLocale } from "@formbricks/types/user";
-import { SurveyDropDownMenu } from "./SurveyDropdownMenu";
+import { SurveyDropDownMenu } from "./survey-dropdown-menu";
interface SurveyCardProps {
survey: TSurvey;
- environment: TEnvironment;
- otherEnvironment: TEnvironment;
+ environmentId: string;
isReadOnly: boolean;
WEBAPP_URL: string;
duplicateSurvey: (survey: TSurvey) => void;
@@ -27,8 +25,7 @@ interface SurveyCardProps {
}
export const SurveyCard = ({
survey,
- environment,
- otherEnvironment,
+ environmentId,
isReadOnly,
WEBAPP_URL,
deleteSurvey,
@@ -76,9 +73,9 @@ export const SurveyCard = ({
const linkHref = useMemo(() => {
return survey.status === "draft"
- ? `/environments/${environment.id}/surveys/${survey.id}/edit`
- : `/environments/${environment.id}/surveys/${survey.id}/summary`;
- }, [survey.status, survey.id, environment.id]);
+ ? `/environments/${environmentId}/surveys/${survey.id}/edit`
+ : `/environments/${environmentId}/surveys/${survey.id}/summary`;
+ }, [survey.status, survey.id, environmentId]);
const isDraftAndReadOnly = survey.status === "draft" && isReadOnly;
@@ -123,9 +120,7 @@ export const SurveyCard = ({
{
- const [projects, setProjects] = useState([]);
+ const [projects, setProjects] = useState([]);
const [projectLoading, setProjectLoading] = useState(true);
useEffect(() => {
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyDropdownMenu.tsx b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx
similarity index 95%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyDropdownMenu.tsx
rename to apps/web/modules/survey/list/components/survey-dropdown-menu.tsx
index 1bfb136653..30b59b497e 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyDropdownMenu.tsx
+++ b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx
@@ -1,12 +1,12 @@
"use client";
+import { getFormattedErrorMessage } from "@/lib/utils/helper";
import {
copySurveyToOtherEnvironmentAction,
deleteSurveyAction,
getSurveyAction,
-} from "@/app/(app)/environments/[environmentId]/surveys/actions";
-import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
-import { getFormattedErrorMessage } from "@/lib/utils/helper";
+} from "@/modules/survey/list/actions";
+import { TSurvey } from "@/modules/survey/list/types/surveys";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import {
DropdownMenu,
@@ -30,14 +30,11 @@ import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
-import type { TEnvironment } from "@formbricks/types/environment";
-import { CopySurveyModal } from "./CopySurveyModal";
+import { CopySurveyModal } from "./copy-survey-modal";
interface SurveyDropDownMenuProps {
environmentId: string;
survey: TSurvey;
- environment: TEnvironment;
- otherEnvironment: TEnvironment;
webAppUrl: string;
singleUseId?: string;
disabled?: boolean;
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilterDropdown.tsx b/apps/web/modules/survey/list/components/survey-filter-dropdown.tsx
similarity index 100%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilterDropdown.tsx
rename to apps/web/modules/survey/list/components/survey-filter-dropdown.tsx
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilters.tsx b/apps/web/modules/survey/list/components/survey-filters.tsx
similarity index 96%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilters.tsx
rename to apps/web/modules/survey/list/components/survey-filters.tsx
index 0cd0bd13d3..bc64a53fc2 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilters.tsx
+++ b/apps/web/modules/survey/list/components/survey-filters.tsx
@@ -1,7 +1,7 @@
"use client";
-import { SortOption } from "@/app/(app)/environments/[environmentId]/surveys/components/SortOption";
-import { initialFilters } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyList";
+import { SortOption } from "@/modules/survey/list/components/sort-option";
+import { initialFilters } from "@/modules/survey/list/components/survey-list";
import { Button } from "@/modules/ui/components/button";
import {
DropdownMenu,
@@ -15,7 +15,7 @@ import { useState } from "react";
import { useDebounce } from "react-use";
import { TProjectConfigChannel } from "@formbricks/types/project";
import { TFilterOption, TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types";
-import { SurveyFilterDropdown } from "./SurveyFilterDropdown";
+import { SurveyFilterDropdown } from "./survey-filter-dropdown";
interface SurveyFilterProps {
surveyFilters: TSurveyFilters;
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyList.tsx b/apps/web/modules/survey/list/components/survey-list.tsx
similarity index 86%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyList.tsx
rename to apps/web/modules/survey/list/components/survey-list.tsx
index 54a972f9c0..275929459b 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyList.tsx
+++ b/apps/web/modules/survey/list/components/survey-list.tsx
@@ -1,25 +1,23 @@
"use client";
-import { getSurveysAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
-import { getFormattedFilters } from "@/app/(app)/environments/[environmentId]/surveys/lib/utils";
-import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
+import { getSurveysAction } from "@/modules/survey/list/actions";
+import { getFormattedFilters } from "@/modules/survey/list/lib/utils";
+import { TSurvey } from "@/modules/survey/list/types/surveys";
import { Button } from "@/modules/ui/components/button";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
-import { TEnvironment } from "@formbricks/types/environment";
import { wrapThrows } from "@formbricks/types/error-handlers";
import { TProjectConfigChannel } from "@formbricks/types/project";
import { TSurveyFilters } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
-import { SurveyCard } from "./SurveyCard";
-import { SurveyFilters } from "./SurveyFilters";
-import { SurveyLoading } from "./SurveyLoading";
+import { SurveyCard } from "./survey-card";
+import { SurveyFilters } from "./survey-filters";
+import { SurveyLoading } from "./survey-loading";
interface SurveysListProps {
- environment: TEnvironment;
- otherEnvironment: TEnvironment;
+ environmentId: string;
isReadOnly: boolean;
WEBAPP_URL: string;
userId: string;
@@ -37,8 +35,7 @@ export const initialFilters: TSurveyFilters = {
};
export const SurveysList = ({
- environment,
- otherEnvironment,
+ environmentId,
isReadOnly,
WEBAPP_URL,
userId,
@@ -84,7 +81,7 @@ export const SurveysList = ({
const fetchInitialSurveys = async () => {
setIsFetching(true);
const res = await getSurveysAction({
- environmentId: environment.id,
+ environmentId,
limit: surveysLimit,
offset: undefined,
filterCriteria: filters,
@@ -101,12 +98,12 @@ export const SurveysList = ({
};
fetchInitialSurveys();
}
- }, [environment.id, surveysLimit, filters, isFilterInitialized]);
+ }, [environmentId, surveysLimit, filters, isFilterInitialized]);
const fetchNextPage = useCallback(async () => {
setIsFetching(true);
const res = await getSurveysAction({
- environmentId: environment.id,
+ environmentId,
limit: surveysLimit,
offset: surveys.length,
filterCriteria: filters,
@@ -121,7 +118,7 @@ export const SurveysList = ({
setSurveys([...surveys, ...res.data]);
setIsFetching(false);
}
- }, [environment.id, surveys, surveysLimit, filters]);
+ }, [environmentId, surveys, surveysLimit, filters]);
const handleDeleteSurvey = async (surveyId: string) => {
const newSurveys = surveys.filter((survey) => survey.id !== surveyId);
@@ -157,8 +154,7 @@ export const SurveysList = ({
=>
+ cache(
+ async () => {
+ const environment = await prisma.environment.findUnique({
+ where: {
+ id: environmentId,
+ },
+ select: {
+ id: true,
+ },
+ });
+
+ if (!environment) {
+ throw new ResourceNotFoundError("Environment", environmentId);
+ }
+
+ return environment.id;
+ },
+
+ [`survey-list-doesEnvironmentExist-${environmentId}`],
+ {
+ tags: [environmentCache.tag.byId(environmentId)],
+ }
+ )()
+);
+
+export const getProjectIdIfEnvironmentExists = reactCache(
+ async (environmentId: string): Promise =>
+ cache(
+ async () => {
+ const environment = await prisma.environment.findUnique({
+ where: {
+ id: environmentId,
+ },
+ select: {
+ projectId: true,
+ },
+ });
+
+ if (!environment) {
+ throw new ResourceNotFoundError("Environment", environmentId);
+ }
+
+ return environment.projectId;
+ },
+ [`survey-list-getProjectIdIfEnvironmentExists-${environmentId}`],
+ {
+ tags: [environmentCache.tag.byId(environmentId)],
+ }
+ )()
+);
+
+export const getEnvironment = reactCache(
+ async (environmentId: string): Promise | null> =>
+ cache(
+ async () => {
+ validateInputs([environmentId, z.string().cuid2()]);
+
+ try {
+ const environment = await prisma.environment.findUnique({
+ where: {
+ id: environmentId,
+ },
+ select: {
+ id: true,
+ type: true,
+ },
+ });
+ return environment;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`survey-list-getEnvironment-${environmentId}`],
+ {
+ tags: [environmentCache.tag.byId(environmentId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/list/lib/organization.ts b/apps/web/modules/survey/list/lib/organization.ts
new file mode 100644
index 0000000000..72a10f6996
--- /dev/null
+++ b/apps/web/modules/survey/list/lib/organization.ts
@@ -0,0 +1,40 @@
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { environmentCache } from "@formbricks/lib/environment/cache";
+import { ResourceNotFoundError } from "@formbricks/types/errors";
+
+export const getOrganizationIdByEnvironmentId = reactCache(
+ async (environmentId: string): Promise =>
+ cache(
+ async () => {
+ const organization = await prisma.organization.findFirst({
+ where: {
+ projects: {
+ some: {
+ environments: {
+ some: {
+ id: environmentId,
+ },
+ },
+ },
+ },
+ },
+ select: {
+ id: true,
+ },
+ });
+
+ if (!organization) {
+ throw new ResourceNotFoundError("Organization", null);
+ }
+
+ return organization.id;
+ },
+
+ [`survey-list-getOrganizationIdByEnvironmentId-${environmentId}`],
+ {
+ tags: [environmentCache.tag.byId(environmentId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/list/lib/project.ts b/apps/web/modules/survey/list/lib/project.ts
new file mode 100644
index 0000000000..7adea354cb
--- /dev/null
+++ b/apps/web/modules/survey/list/lib/project.ts
@@ -0,0 +1,81 @@
+import "server-only";
+import { TUserProject } from "@/modules/survey/list/types/projects";
+import { TProjectWithLanguages } from "@/modules/survey/list/types/surveys";
+import { Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { prisma } from "@formbricks/database";
+import { cache } from "@formbricks/lib/cache";
+import { projectCache } from "@formbricks/lib/project/cache";
+import { DatabaseError } from "@formbricks/types/errors";
+
+export const getProjectWithLanguagesByEnvironmentId = reactCache(
+ async (environmentId: string): Promise =>
+ cache(
+ async () => {
+ try {
+ const projectPrisma = await prisma.project.findFirst({
+ where: {
+ environments: {
+ some: {
+ id: environmentId,
+ },
+ },
+ },
+ select: {
+ id: true,
+ languages: true,
+ },
+ });
+
+ return projectPrisma;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+ },
+ [`survey-list-getProjectByEnvironmentId-${environmentId}`],
+ {
+ tags: [projectCache.tag.byEnvironmentId(environmentId)],
+ }
+ )()
+);
+
+export const getUserProjects = reactCache(
+ async (userId: string, organizationId: string): Promise =>
+ cache(
+ async () => {
+ try {
+ const projects = await prisma.project.findMany({
+ where: {
+ organizationId,
+ },
+ select: {
+ id: true,
+ name: true,
+ environments: {
+ select: {
+ id: true,
+ type: true,
+ },
+ },
+ },
+ });
+
+ return projects;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`survey-list-getUserProjects-${userId}-${organizationId}`],
+ {
+ tags: [projectCache.tag.byUserId(userId), projectCache.tag.byOrganizationId(organizationId)],
+ }
+ )()
+);
diff --git a/apps/web/modules/survey/list/lib/survey.ts b/apps/web/modules/survey/list/lib/survey.ts
new file mode 100644
index 0000000000..29170f89ee
--- /dev/null
+++ b/apps/web/modules/survey/list/lib/survey.ts
@@ -0,0 +1,643 @@
+import "server-only";
+import { buildOrderByClause, buildWhereClause } from "@/modules/survey/lib/utils";
+import { doesEnvironmentExist } from "@/modules/survey/list/lib/environment";
+import { getProjectWithLanguagesByEnvironmentId } from "@/modules/survey/list/lib/project";
+import { TProjectWithLanguages, TSurvey } from "@/modules/survey/list/types/surveys";
+import { createId } from "@paralleldrive/cuid2";
+import { Prisma } from "@prisma/client";
+import { cache as reactCache } from "react";
+import { z } from "zod";
+import { prisma } from "@formbricks/database";
+import { actionClassCache } from "@formbricks/lib/actionClass/cache";
+import { cache } from "@formbricks/lib/cache";
+import { segmentCache } from "@formbricks/lib/cache/segment";
+import { projectCache } from "@formbricks/lib/project/cache";
+import { responseCache } from "@formbricks/lib/response/cache";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { validateInputs } from "@formbricks/lib/utils/validate";
+import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+import { TSurveyFilterCriteria } from "@formbricks/types/surveys/types";
+
+export const surveySelect: Prisma.SurveySelect = {
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ name: true,
+ type: true,
+ creator: {
+ select: {
+ name: true,
+ },
+ },
+ status: true,
+ singleUse: true,
+ environmentId: true,
+ _count: {
+ select: { responses: true },
+ },
+};
+
+export const getSurveys = reactCache(
+ async (
+ environmentId: string,
+ limit?: number,
+ offset?: number,
+ filterCriteria?: TSurveyFilterCriteria
+ ): Promise =>
+ cache(
+ async () => {
+ try {
+ if (filterCriteria?.sortBy === "relevance") {
+ // Call the sortByRelevance function
+ return await getSurveysSortedByRelevance(environmentId, limit, offset ?? 0, filterCriteria);
+ }
+
+ // Fetch surveys normally with pagination and include response count
+ const surveysPrisma = await prisma.survey.findMany({
+ where: {
+ environmentId,
+ ...buildWhereClause(filterCriteria),
+ },
+ select: surveySelect,
+ orderBy: buildOrderByClause(filterCriteria?.sortBy),
+ take: limit,
+ skip: offset,
+ });
+
+ return surveysPrisma.map((survey) => {
+ return {
+ ...survey,
+ responseCount: survey._count.responses,
+ };
+ });
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+ },
+ [`surveyList-getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
+ {
+ tags: [
+ surveyCache.tag.byEnvironmentId(environmentId),
+ responseCache.tag.byEnvironmentId(environmentId),
+ ],
+ }
+ )()
+);
+
+export const getSurveysSortedByRelevance = reactCache(
+ async (
+ environmentId: string,
+ limit?: number,
+ offset?: number,
+ filterCriteria?: TSurveyFilterCriteria
+ ): Promise =>
+ cache(
+ async () => {
+ try {
+ let surveys: TSurvey[] = [];
+
+ const inProgressSurveyCount = await prisma.survey.count({
+ where: {
+ environmentId,
+ status: "inProgress",
+ ...buildWhereClause(filterCriteria),
+ },
+ });
+
+ // Fetch surveys that are in progress first
+ const inProgressSurveys =
+ offset && offset > inProgressSurveyCount
+ ? []
+ : await prisma.survey.findMany({
+ where: {
+ environmentId,
+ status: "inProgress",
+ ...buildWhereClause(filterCriteria),
+ },
+ select: surveySelect,
+ orderBy: buildOrderByClause("updatedAt"),
+ take: limit,
+ skip: offset,
+ });
+
+ surveys = inProgressSurveys.map((survey) => {
+ return {
+ ...survey,
+ responseCount: survey._count.responses,
+ };
+ });
+
+ // Determine if additional surveys are needed
+ if (offset !== undefined && limit && inProgressSurveys.length < limit) {
+ const remainingLimit = limit - inProgressSurveys.length;
+ const newOffset = Math.max(0, offset - inProgressSurveyCount);
+ const additionalSurveys = await prisma.survey.findMany({
+ where: {
+ environmentId,
+ status: { not: "inProgress" },
+ ...buildWhereClause(filterCriteria),
+ },
+ select: surveySelect,
+ orderBy: buildOrderByClause("updatedAt"),
+ take: remainingLimit,
+ skip: newOffset,
+ });
+
+ surveys = [
+ ...surveys,
+ ...additionalSurveys.map((survey) => {
+ return {
+ ...survey,
+ responseCount: survey._count.responses,
+ };
+ }),
+ ];
+ }
+
+ return surveys;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+ },
+ [
+ `surveyList-getSurveysSortedByRelevance-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`,
+ ],
+ {
+ tags: [
+ surveyCache.tag.byEnvironmentId(environmentId),
+ responseCache.tag.byEnvironmentId(environmentId),
+ ],
+ }
+ )()
+);
+
+export const getSurvey = reactCache(
+ async (surveyId: string): Promise =>
+ cache(
+ async () => {
+ let surveyPrisma;
+ try {
+ surveyPrisma = await prisma.survey.findUnique({
+ where: {
+ id: surveyId,
+ },
+ select: surveySelect,
+ });
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+
+ if (!surveyPrisma) {
+ return null;
+ }
+
+ return { ...surveyPrisma, responseCount: surveyPrisma?._count.responses };
+ },
+ [`surveyList-getSurvey-${surveyId}`],
+ {
+ tags: [surveyCache.tag.byId(surveyId), responseCache.tag.bySurveyId(surveyId)],
+ }
+ )()
+);
+
+export const deleteSurvey = async (surveyId: string): Promise => {
+ try {
+ const deletedSurvey = await prisma.survey.delete({
+ where: {
+ id: surveyId,
+ },
+ select: {
+ id: true,
+ environmentId: true,
+ segment: {
+ select: {
+ id: true,
+ isPrivate: true,
+ },
+ },
+ type: true,
+ resultShareKey: true,
+ triggers: {
+ select: {
+ actionClass: {
+ select: {
+ id: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ if (deletedSurvey.type === "app" && deletedSurvey.segment?.isPrivate) {
+ const deletedSegment = await prisma.segment.delete({
+ where: {
+ id: deletedSurvey.segment.id,
+ },
+ });
+
+ if (deletedSegment) {
+ segmentCache.revalidate({
+ id: deletedSegment.id,
+ environmentId: deletedSurvey.environmentId,
+ });
+ }
+ }
+
+ responseCache.revalidate({
+ surveyId,
+ environmentId: deletedSurvey.environmentId,
+ });
+ surveyCache.revalidate({
+ id: deletedSurvey.id,
+ environmentId: deletedSurvey.environmentId,
+ resultShareKey: deletedSurvey.resultShareKey ?? undefined,
+ });
+
+ if (deletedSurvey.segment?.id) {
+ segmentCache.revalidate({
+ id: deletedSurvey.segment.id,
+ environmentId: deletedSurvey.environmentId,
+ });
+ }
+
+ // Revalidate public triggers by actionClassId
+ deletedSurvey.triggers.forEach((trigger) => {
+ surveyCache.revalidate({
+ actionClassId: trigger.actionClass.id,
+ });
+ });
+
+ return true;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+};
+
+const getExistingSurvey = async (surveyId: string) => {
+ return await prisma.survey.findUnique({
+ where: {
+ id: surveyId,
+ },
+ select: {
+ name: true,
+ type: true,
+ languages: {
+ select: {
+ default: true,
+ enabled: true,
+ language: {
+ select: {
+ code: true,
+ alias: true,
+ },
+ },
+ },
+ },
+ welcomeCard: true,
+ questions: true,
+ endings: true,
+ variables: true,
+ hiddenFields: true,
+ surveyClosedMessage: true,
+ singleUse: true,
+ projectOverwrites: true,
+ styling: true,
+ segment: true,
+ followUps: true,
+ triggers: {
+ select: {
+ actionClass: {
+ select: {
+ id: true,
+ name: true,
+ environmentId: true,
+ description: true,
+ type: true,
+ key: true,
+ noCodeConfig: true,
+ },
+ },
+ },
+ },
+ },
+ });
+};
+
+export const copySurveyToOtherEnvironment = async (
+ environmentId: string,
+ surveyId: string,
+ targetEnvironmentId: string,
+ userId: string
+) => {
+ try {
+ const isSameEnvironment = environmentId === targetEnvironmentId;
+
+ // Fetch required resources
+ const [existingEnvironment, existingProject, existingSurvey] = await Promise.all([
+ doesEnvironmentExist(environmentId),
+ getProjectWithLanguagesByEnvironmentId(environmentId),
+ getExistingSurvey(surveyId),
+ ]);
+
+ if (!existingEnvironment) throw new ResourceNotFoundError("Environment", environmentId);
+ if (!existingProject) throw new ResourceNotFoundError("Project", environmentId);
+ if (!existingSurvey) throw new ResourceNotFoundError("Survey", surveyId);
+
+ let targetEnvironment: string | null = null;
+ let targetProject: TProjectWithLanguages | null = null;
+
+ if (isSameEnvironment) {
+ targetEnvironment = existingEnvironment;
+ targetProject = existingProject;
+ } else {
+ [targetEnvironment, targetProject] = await Promise.all([
+ doesEnvironmentExist(targetEnvironmentId),
+ getProjectWithLanguagesByEnvironmentId(targetEnvironmentId),
+ ]);
+
+ if (!targetEnvironment) throw new ResourceNotFoundError("Environment", targetEnvironmentId);
+ if (!targetProject) throw new ResourceNotFoundError("Project", targetEnvironmentId);
+ }
+
+ const { ...restExistingSurvey } = existingSurvey;
+ const hasLanguages = existingSurvey.languages && existingSurvey.languages.length > 0;
+
+ // Prepare survey data
+ const surveyData: Prisma.SurveyCreateInput = {
+ ...restExistingSurvey,
+ id: createId(),
+ name: `${existingSurvey.name} (copy)`,
+ type: existingSurvey.type,
+ status: "draft",
+ welcomeCard: structuredClone(existingSurvey.welcomeCard),
+ questions: structuredClone(existingSurvey.questions),
+ endings: structuredClone(existingSurvey.endings),
+ variables: structuredClone(existingSurvey.variables),
+ hiddenFields: structuredClone(existingSurvey.hiddenFields),
+ languages: hasLanguages
+ ? {
+ create: existingSurvey.languages.map((surveyLanguage) => ({
+ language: {
+ connectOrCreate: {
+ where: {
+ projectId_code: { code: surveyLanguage.language.code, projectId: targetProject.id },
+ },
+ create: {
+ code: surveyLanguage.language.code,
+ alias: surveyLanguage.language.alias,
+ projectId: targetProject.id,
+ },
+ },
+ },
+ default: surveyLanguage.default,
+ enabled: surveyLanguage.enabled,
+ })),
+ }
+ : undefined,
+ triggers: {
+ create: existingSurvey.triggers.map((trigger): Prisma.SurveyTriggerCreateWithoutSurveyInput => {
+ const baseActionClassData = {
+ name: trigger.actionClass.name,
+ environment: { connect: { id: targetEnvironmentId } },
+ description: trigger.actionClass.description,
+ type: trigger.actionClass.type,
+ };
+
+ if (isSameEnvironment) {
+ return {
+ actionClass: { connect: { id: trigger.actionClass.id } },
+ };
+ } else if (trigger.actionClass.type === "code") {
+ return {
+ actionClass: {
+ connectOrCreate: {
+ where: {
+ key_environmentId: { key: trigger.actionClass.key!, environmentId: targetEnvironmentId },
+ },
+ create: {
+ ...baseActionClassData,
+ key: trigger.actionClass.key,
+ },
+ },
+ },
+ };
+ } else {
+ return {
+ actionClass: {
+ connectOrCreate: {
+ where: {
+ name_environmentId: {
+ name: trigger.actionClass.name,
+ environmentId: targetEnvironmentId,
+ },
+ },
+ create: {
+ ...baseActionClassData,
+ noCodeConfig: trigger.actionClass.noCodeConfig
+ ? structuredClone(trigger.actionClass.noCodeConfig)
+ : undefined,
+ },
+ },
+ },
+ };
+ }
+ }),
+ },
+ environment: {
+ connect: {
+ id: targetEnvironmentId,
+ },
+ },
+ creator: {
+ connect: {
+ id: userId,
+ },
+ },
+ surveyClosedMessage: existingSurvey.surveyClosedMessage
+ ? structuredClone(existingSurvey.surveyClosedMessage)
+ : Prisma.JsonNull,
+ singleUse: existingSurvey.singleUse ? structuredClone(existingSurvey.singleUse) : Prisma.JsonNull,
+ projectOverwrites: existingSurvey.projectOverwrites
+ ? structuredClone(existingSurvey.projectOverwrites)
+ : Prisma.JsonNull,
+ styling: existingSurvey.styling ? structuredClone(existingSurvey.styling) : Prisma.JsonNull,
+ segment: undefined,
+ followUps: {
+ createMany: {
+ data: existingSurvey.followUps.map((followUp) => ({
+ name: followUp.name,
+ trigger: followUp.trigger,
+ action: followUp.action,
+ })),
+ },
+ },
+ };
+
+ // Handle segment
+ if (existingSurvey.segment) {
+ if (existingSurvey.segment.isPrivate) {
+ surveyData.segment = {
+ create: {
+ title: surveyData.id!,
+ isPrivate: true,
+ filters: existingSurvey.segment.filters,
+ environment: { connect: { id: targetEnvironmentId } },
+ },
+ };
+ } else if (isSameEnvironment) {
+ surveyData.segment = { connect: { id: existingSurvey.segment.id } };
+ } else {
+ const existingSegmentInTargetEnvironment = await prisma.segment.findFirst({
+ where: {
+ title: existingSurvey.segment.title,
+ isPrivate: false,
+ environmentId: targetEnvironmentId,
+ },
+ });
+
+ surveyData.segment = {
+ create: {
+ title: existingSegmentInTargetEnvironment
+ ? `${existingSurvey.segment.title}-${Date.now()}`
+ : existingSurvey.segment.title,
+ isPrivate: false,
+ filters: existingSurvey.segment.filters,
+ environment: { connect: { id: targetEnvironmentId } },
+ },
+ };
+ }
+ }
+
+ const targetProjectLanguageCodes = targetProject.languages.map((language) => language.code);
+ const newSurvey = await prisma.survey.create({
+ data: surveyData,
+ select: {
+ id: true,
+ environmentId: true,
+ segment: {
+ select: {
+ id: true,
+ },
+ },
+ triggers: {
+ select: {
+ actionClass: {
+ select: {
+ id: true,
+ name: true,
+ environmentId: true,
+ },
+ },
+ },
+ },
+ languages: {
+ select: {
+ language: {
+ select: {
+ code: true,
+ },
+ },
+ },
+ },
+ resultShareKey: true,
+ },
+ });
+
+ // Identify newly created action classes
+ const newActionClasses = newSurvey.triggers.map((trigger) => trigger.actionClass);
+
+ // Revalidate cache only for newly created action classes
+ for (const actionClass of newActionClasses) {
+ actionClassCache.revalidate({
+ environmentId: actionClass.environmentId,
+ name: actionClass.name,
+ id: actionClass.id,
+ });
+ }
+
+ let newLanguageCreated = false;
+ if (existingSurvey.languages && existingSurvey.languages.length > 0) {
+ const targetLanguageCodes = newSurvey.languages.map((lang) => lang.language.code);
+ newLanguageCreated = targetLanguageCodes.length > targetProjectLanguageCodes.length;
+ }
+
+ // Invalidate caches
+ if (newLanguageCreated) {
+ projectCache.revalidate({ id: targetProject.id, environmentId: targetEnvironmentId });
+ }
+
+ surveyCache.revalidate({
+ id: newSurvey.id,
+ environmentId: newSurvey.environmentId,
+ resultShareKey: newSurvey.resultShareKey ?? undefined,
+ });
+
+ existingSurvey.triggers.forEach((trigger) => {
+ surveyCache.revalidate({
+ actionClassId: trigger.actionClass.id,
+ });
+ });
+
+ if (newSurvey.segment) {
+ segmentCache.revalidate({
+ id: newSurvey.segment.id,
+ environmentId: newSurvey.environmentId,
+ });
+ }
+
+ return newSurvey;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+};
+
+export const getSurveyCount = reactCache(
+ async (environmentId: string): Promise =>
+ cache(
+ async () => {
+ validateInputs([environmentId, z.string().cuid2()]);
+ try {
+ const surveyCount = await prisma.survey.count({
+ where: {
+ environmentId: environmentId,
+ },
+ });
+
+ return surveyCount;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+ },
+ [`getSurveyCount-${environmentId}`],
+ {
+ tags: [surveyCache.tag.byEnvironmentId(environmentId)],
+ }
+ )()
+);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/lib/utils.ts b/apps/web/modules/survey/list/lib/utils.ts
similarity index 100%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/lib/utils.ts
rename to apps/web/modules/survey/list/lib/utils.ts
diff --git a/apps/web/modules/survey/list/loading.tsx b/apps/web/modules/survey/list/loading.tsx
new file mode 100644
index 0000000000..400517e047
--- /dev/null
+++ b/apps/web/modules/survey/list/loading.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { SurveyLoading } from "@/modules/survey/list/components/survey-loading";
+import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
+import { PageHeader } from "@/modules/ui/components/page-header";
+import { useTranslate } from "@tolgee/react";
+
+export const SurveyListLoading = () => {
+ const { t } = useTranslate();
+
+ return (
+
+
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/apps/web/modules/survey/list/page.tsx b/apps/web/modules/survey/list/page.tsx
new file mode 100644
index 0000000000..f13bcca386
--- /dev/null
+++ b/apps/web/modules/survey/list/page.tsx
@@ -0,0 +1,135 @@
+import { authOptions } from "@/modules/auth/lib/authOptions";
+import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
+import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
+import { TemplateList } from "@/modules/survey/components/template-list";
+import { getMembershipRoleByUserIdOrganizationId } from "@/modules/survey/lib/membership";
+import { getProjectByEnvironmentId } from "@/modules/survey/lib/project";
+import { SurveysList } from "@/modules/survey/list/components/survey-list";
+import { getEnvironment } from "@/modules/survey/list/lib/environment";
+import { getOrganizationIdByEnvironmentId } from "@/modules/survey/list/lib/organization";
+import { getSurveyCount } from "@/modules/survey/list/lib/survey";
+import { Button } from "@/modules/ui/components/button";
+import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
+import { PageHeader } from "@/modules/ui/components/page-header";
+import { getTranslate } from "@/tolgee/server";
+import { PlusIcon } from "lucide-react";
+import { Metadata } from "next";
+import { getServerSession } from "next-auth";
+import Link from "next/link";
+import { redirect } from "next/navigation";
+import { SURVEYS_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
+import { getAccessFlags } from "@formbricks/lib/membership/utils";
+import { findMatchingLocale } from "@formbricks/lib/utils/locale";
+import { TTemplateRole } from "@formbricks/types/templates";
+
+export const metadata: Metadata = {
+ title: "Your Surveys",
+};
+
+interface SurveyTemplateProps {
+ params: Promise<{
+ environmentId: string;
+ }>;
+ searchParams: Promise<{
+ role?: TTemplateRole;
+ }>;
+}
+
+export const SurveysPage = async ({
+ params: paramsProps,
+ searchParams: searchParamsProps,
+}: SurveyTemplateProps) => {
+ const searchParams = await searchParamsProps;
+ const params = await paramsProps;
+
+ const session = await getServerSession(authOptions);
+ const project = await getProjectByEnvironmentId(params.environmentId);
+ const organizationId = await getOrganizationIdByEnvironmentId(params.environmentId);
+ const t = await getTranslate();
+ if (!session) {
+ throw new Error(t("common.session_not_found"));
+ }
+
+ if (!project) {
+ throw new Error(t("common.project_not_found"));
+ }
+
+ if (!organizationId) {
+ throw new Error(t("common.organization_not_found"));
+ }
+
+ const prefilledFilters = [project?.config.channel, project.config.industry, searchParams.role ?? null];
+
+ const membershipRole = await getMembershipRoleByUserIdOrganizationId(session?.user.id, organizationId);
+ const { isMember, isBilling } = getAccessFlags(membershipRole);
+
+ const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
+ const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
+
+ const isReadOnly = isMember && hasReadAccess;
+
+ if (isBilling) {
+ return redirect(`/environments/${params.environmentId}/settings/billing`);
+ }
+
+ const environment = await getEnvironment(params.environmentId);
+ if (!environment) {
+ throw new Error(t("common.environment_not_found"));
+ }
+
+ const surveyCount = await getSurveyCount(params.environmentId);
+
+ const currentProjectChannel = project.config.channel ?? null;
+ const locale = await findMatchingLocale();
+ const CreateSurveyButton = () => {
+ return (
+
+ );
+ };
+
+ return (
+
+ {surveyCount > 0 ? (
+ <>
+ > : } />
+
+ >
+ ) : isReadOnly ? (
+ <>
+
+ {t("environments.surveys.no_surveys_created_yet")}
+
+
+
+ {t("environments.surveys.read_only_user_not_allowed_to_create_survey_warning")}
+
+ >
+ ) : (
+ <>
+
+ {t("environments.surveys.all_set_time_to_create_first_survey")}
+
+
+ >
+ )}
+
+ );
+};
diff --git a/apps/web/modules/survey/list/types/projects.ts b/apps/web/modules/survey/list/types/projects.ts
new file mode 100644
index 0000000000..7e443ad89f
--- /dev/null
+++ b/apps/web/modules/survey/list/types/projects.ts
@@ -0,0 +1,5 @@
+import { Environment, Project } from "@prisma/client";
+
+export interface TUserProject extends Pick {
+ environments: Pick[];
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/types/surveys.ts b/apps/web/modules/survey/list/types/surveys.ts
similarity index 83%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/types/surveys.ts
rename to apps/web/modules/survey/list/types/surveys.ts
index b996248670..4b4a31a64f 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/types/surveys.ts
+++ b/apps/web/modules/survey/list/types/surveys.ts
@@ -1,3 +1,4 @@
+import { Language, Project } from "@prisma/client";
import { z } from "zod";
import { ZSurveyStatus } from "@formbricks/types/surveys/types";
@@ -35,3 +36,7 @@ export const ZSurveyCopyFormValidation = z.object({
});
export type TSurveyCopyFormData = z.infer;
+
+export interface TProjectWithLanguages extends Pick {
+ languages: Pick[];
+}
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts b/apps/web/modules/survey/templates/actions.ts
similarity index 86%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts
rename to apps/web/modules/survey/templates/actions.ts
index 4193e2b6a1..b341f3dbef 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts
+++ b/apps/web/modules/survey/templates/actions.ts
@@ -4,18 +4,17 @@ import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
+import { getOrganizationAIKeys } from "@/modules/survey/lib/organization";
+import { createSurvey } from "@/modules/survey/templates/lib/survey";
import { createId } from "@paralleldrive/cuid2";
import { generateObject } from "ai";
import { z } from "zod";
import { llmModel } from "@formbricks/lib/aiModels";
-import { getOrganization } from "@formbricks/lib/organization/service";
-import { createSurvey } from "@formbricks/lib/survey/service";
-import { ZId, ZString } from "@formbricks/types/common";
import { ZSurveyQuestion } from "@formbricks/types/surveys/types";
const ZCreateAISurveyAction = z.object({
- environmentId: ZId,
- prompt: ZString,
+ environmentId: z.string().cuid2(),
+ prompt: z.string(),
});
export const createAISurveyAction = authenticatedActionClient
@@ -39,13 +38,16 @@ export const createAISurveyAction = authenticatedActionClient
],
});
- const organization = await getOrganization(organizationId);
+ const organization = await getOrganizationAIKeys(organizationId);
if (!organization) {
throw new Error("Organization not found");
}
- const isAIEnabled = await getIsAIEnabled(organization);
+ const isAIEnabled = await getIsAIEnabled({
+ isAIEnabled: organization.isAIEnabled,
+ billing: organization.billing,
+ });
if (!isAIEnabled) {
throw new Error("AI is not enabled for this organization");
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/BackButton.tsx b/apps/web/modules/survey/templates/components/back-button.tsx
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/BackButton.tsx
rename to apps/web/modules/survey/templates/components/back-button.tsx
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard.tsx b/apps/web/modules/survey/templates/components/formbricks-ai-card.tsx
similarity index 95%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard.tsx
rename to apps/web/modules/survey/templates/components/formbricks-ai-card.tsx
index b66888e545..d8836e9c49 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard.tsx
+++ b/apps/web/modules/survey/templates/components/formbricks-ai-card.tsx
@@ -1,7 +1,7 @@
"use client";
-import { createAISurveyAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
+import { createAISurveyAction } from "@/modules/survey/templates/actions";
import { Button } from "@/modules/ui/components/button";
import {
Card,
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/MenuBar.tsx b/apps/web/modules/survey/templates/components/menu-bar.tsx
similarity index 71%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/MenuBar.tsx
rename to apps/web/modules/survey/templates/components/menu-bar.tsx
index 662ee00546..74a6465414 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/MenuBar.tsx
+++ b/apps/web/modules/survey/templates/components/menu-bar.tsx
@@ -1,6 +1,6 @@
"use client";
-import { BackButton } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/BackButton";
+import { BackButton } from "@/modules/survey/templates/components/back-button";
export const MenuBar = () => {
return (
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/TemplateContainer.tsx b/apps/web/modules/survey/templates/components/template-container.tsx
similarity index 79%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/TemplateContainer.tsx
rename to apps/web/modules/survey/templates/components/template-container.tsx
index 9ca9a53062..b8a7b30467 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/TemplateContainer.tsx
+++ b/apps/web/modules/survey/templates/components/template-container.tsx
@@ -1,25 +1,24 @@
"use client";
-import { FormbricksAICard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard";
-import { MenuBar } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/MenuBar";
import { customSurveyTemplate } from "@/app/lib/templates";
-import { TemplateList } from "@/modules/surveys/components/TemplateList";
+import { TemplateList } from "@/modules/survey/components/template-list";
+import { FormbricksAICard } from "@/modules/survey/templates/components/formbricks-ai-card";
+import { MenuBar } from "@/modules/survey/templates/components/menu-bar";
import { PreviewSurvey } from "@/modules/ui/components/preview-survey";
import { SearchBar } from "@/modules/ui/components/search-bar";
import { Separator } from "@/modules/ui/components/separator";
+import { Project } from "@prisma/client";
+import { Environment } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { useState } from "react";
-import type { TEnvironment } from "@formbricks/types/environment";
-import type { TProject, TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
+import type { TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
-import { TUser } from "@formbricks/types/user";
-import { getMinimalSurvey } from "../../lib/minimalSurvey";
+import { getMinimalSurvey } from "../lib/minimal-survey";
type TemplateContainerWithPreviewProps = {
- environmentId: string;
- project: TProject;
- environment: TEnvironment;
- user: TUser;
+ project: Project;
+ environment: Pick;
+ userId: string;
prefilledFilters: (TProjectConfigChannel | TProjectConfigIndustry | TTemplateRole | null)[];
isAIEnabled: boolean;
};
@@ -27,7 +26,7 @@ type TemplateContainerWithPreviewProps = {
export const TemplateContainerWithPreview = ({
project,
environment,
- user,
+ userId,
prefilledFilters,
isAIEnabled,
}: TemplateContainerWithPreviewProps) => {
@@ -66,9 +65,9 @@ export const TemplateContainerWithPreview = ({
)}
{
setActiveQuestionId(template.preset.questions[0].id);
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/lib/minimalSurvey.ts b/apps/web/modules/survey/templates/lib/minimal-survey.ts
similarity index 100%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/lib/minimalSurvey.ts
rename to apps/web/modules/survey/templates/lib/minimal-survey.ts
diff --git a/apps/web/modules/survey/templates/lib/survey.ts b/apps/web/modules/survey/templates/lib/survey.ts
new file mode 100644
index 0000000000..3ee89261bf
--- /dev/null
+++ b/apps/web/modules/survey/templates/lib/survey.ts
@@ -0,0 +1,109 @@
+import { getInsightsEnabled } from "@/modules/survey/lib/utils";
+import { doesSurveyHasOpenTextQuestion } from "@/modules/survey/lib/utils";
+import { Prisma, Survey } from "@prisma/client";
+import { prisma } from "@formbricks/database";
+import { segmentCache } from "@formbricks/lib/cache/segment";
+import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { DatabaseError } from "@formbricks/types/errors";
+
+export const createSurvey = async (
+ environmentId: string,
+ surveyBody: Pick
+): Promise<{ id: string }> => {
+ try {
+ if (doesSurveyHasOpenTextQuestion(surveyBody.questions ?? [])) {
+ const openTextQuestions =
+ surveyBody.questions?.filter((question) => question.type === "openText") ?? [];
+ const insightsEnabledValues = await Promise.all(
+ openTextQuestions.map(async (question) => {
+ const insightsEnabled = await getInsightsEnabled(question);
+
+ return { id: question.id, insightsEnabled };
+ })
+ );
+
+ surveyBody.questions = surveyBody.questions?.map((question) => {
+ const index = insightsEnabledValues.findIndex((item) => item.id === question.id);
+ if (index !== -1) {
+ return {
+ ...question,
+ insightsEnabled: insightsEnabledValues[index].insightsEnabled,
+ };
+ }
+
+ return question;
+ });
+ }
+
+ const survey = await prisma.survey.create({
+ data: {
+ ...surveyBody,
+ environment: {
+ connect: {
+ id: environmentId,
+ },
+ },
+ },
+ select: {
+ id: true,
+ type: true,
+ environmentId: true,
+ resultShareKey: true,
+ },
+ });
+
+ // if the survey created is an "app" survey, we also create a private segment for it.
+ if (survey.type === "app") {
+ const newSegment = await prisma.segment.create({
+ data: {
+ title: survey.id,
+ filters: [],
+ isPrivate: true,
+ environment: {
+ connect: {
+ id: environmentId,
+ },
+ },
+ },
+ });
+
+ await prisma.survey.update({
+ where: {
+ id: survey.id,
+ },
+ data: {
+ segment: {
+ connect: {
+ id: newSegment.id,
+ },
+ },
+ },
+ });
+
+ segmentCache.revalidate({
+ id: newSegment.id,
+ environmentId: survey.environmentId,
+ });
+ }
+
+ surveyCache.revalidate({
+ id: survey.id,
+ environmentId: survey.environmentId,
+ resultShareKey: survey.resultShareKey ?? undefined,
+ });
+
+ await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", {
+ surveyId: survey.id,
+ surveyType: survey.type,
+ });
+
+ return { id: survey.id };
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ console.error(error);
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+};
diff --git a/apps/web/modules/survey/templates/loading.tsx b/apps/web/modules/survey/templates/loading.tsx
new file mode 100644
index 0000000000..1b0d7ea24e
--- /dev/null
+++ b/apps/web/modules/survey/templates/loading.tsx
@@ -0,0 +1,5 @@
+import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
+
+export const SurveyTemplatesLoading = () => {
+ return ;
+};
diff --git a/apps/web/modules/survey/templates/page.tsx b/apps/web/modules/survey/templates/page.tsx
new file mode 100644
index 0000000000..b3b9bffc4b
--- /dev/null
+++ b/apps/web/modules/survey/templates/page.tsx
@@ -0,0 +1,75 @@
+import { authOptions } from "@/modules/auth/lib/authOptions";
+import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
+import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
+import { getEnvironment } from "@/modules/survey/lib/environment";
+import { getMembershipRoleByUserIdOrganizationId } from "@/modules/survey/lib/membership";
+import { getProjectByEnvironmentId } from "@/modules/survey/lib/project";
+import { getTranslate } from "@/tolgee/server";
+import { getServerSession } from "next-auth";
+import { redirect } from "next/navigation";
+import { getAccessFlags } from "@formbricks/lib/membership/utils";
+import { TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
+import { TTemplateRole } from "@formbricks/types/templates";
+import { TemplateContainerWithPreview } from "./components/template-container";
+
+interface SurveyTemplateProps {
+ params: Promise<{
+ environmentId: string;
+ }>;
+ searchParams: Promise<{
+ channel?: TProjectConfigChannel;
+ industry?: TProjectConfigIndustry;
+ role?: TTemplateRole;
+ }>;
+}
+
+export const SurveyTemplatesPage = async (props: SurveyTemplateProps) => {
+ const searchParams = await props.searchParams;
+ const params = await props.params;
+ const t = await getTranslate();
+ const session = await getServerSession(authOptions);
+ const environmentId = params.environmentId;
+
+ if (!session) {
+ throw new Error(t("common.session_not_found"));
+ }
+
+ const [environment, project] = await Promise.all([
+ getEnvironment(environmentId),
+ getProjectByEnvironmentId(environmentId),
+ ]);
+
+ if (!project) {
+ throw new Error(t("common.project_not_found"));
+ }
+
+ if (!environment) {
+ throw new Error(t("common.environment_not_found"));
+ }
+ const membershipRole = await getMembershipRoleByUserIdOrganizationId(
+ session?.user.id,
+ project.organizationId
+ );
+ const { isMember } = getAccessFlags(membershipRole);
+
+ const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
+ const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
+
+ const isReadOnly = isMember && hasReadAccess;
+ if (isReadOnly) {
+ return redirect(`/environments/${environment.id}/surveys`);
+ }
+
+ const prefilledFilters = [project.config.channel, project.config.industry, searchParams.role ?? null];
+
+ return (
+
+ );
+};
diff --git a/apps/web/modules/surveys/components/TemplateList/lib/utils.ts b/apps/web/modules/surveys/components/TemplateList/lib/utils.ts
deleted file mode 100644
index da5e3fa70e..0000000000
--- a/apps/web/modules/surveys/components/TemplateList/lib/utils.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { TFnType } from "@tolgee/react";
-import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
-import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
-import { TProject, TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
-import { TSurveyQuestion } from "@formbricks/types/surveys/types";
-import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
-
-export const replaceQuestionPresetPlaceholders = (
- question: TSurveyQuestion,
- project: TProject
-): TSurveyQuestion => {
- if (!project) return question;
- const newQuestion = structuredClone(question);
- const defaultLanguageCode = "default";
- if (newQuestion.headline) {
- newQuestion.headline[defaultLanguageCode] = getLocalizedValue(
- newQuestion.headline,
- defaultLanguageCode
- ).replace("$[projectName]", project.name);
- }
- if (newQuestion.subheader) {
- newQuestion.subheader[defaultLanguageCode] = getLocalizedValue(
- newQuestion.subheader,
- defaultLanguageCode
- )?.replace("$[projectName]", project.name);
- }
- return newQuestion;
-};
-
-// replace all occurences of projectName with the actual project name in the current template
-export const replacePresetPlaceholders = (template: TTemplate, project: any) => {
- const preset = structuredClone(template.preset);
- preset.name = preset.name.replace("$[projectName]", project.name);
- preset.questions = preset.questions.map((question) => {
- return replaceQuestionPresetPlaceholders(question, project);
- });
- return { ...template, preset };
-};
-
-export const getChannelMapping = (t: TFnType): { value: TProjectConfigChannel; label: string }[] => [
- { value: "website", label: t("common.website_survey") },
- { value: "app", label: t("common.app_survey") },
- { value: "link", label: t("common.link_survey") },
-];
-
-export const getIndustryMapping = (t: TFnType): { value: TProjectConfigIndustry; label: string }[] => [
- { value: "eCommerce", label: t("common.e_commerce") },
- { value: "saas", label: t("common.saas") },
- { value: "other", label: t("common.other") },
-];
-
-export const getRoleMapping = (t: TFnType): { value: TTemplateRole; label: string }[] => [
- { value: "productManager", label: t("common.product_manager") },
- { value: "customerSuccess", label: t("common.customer_success") },
- { value: "marketing", label: t("common.marketing") },
- { value: "sales", label: t("common.sales") },
- { value: "peopleManager", label: t("common.people_manager") },
-];
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/BackgroundStylingCard.tsx b/apps/web/modules/ui/components/background-styling-card/index.tsx
similarity index 98%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/BackgroundStylingCard.tsx
rename to apps/web/modules/ui/components/background-styling-card/index.tsx
index c57bdbed51..aa4698ed95 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/BackgroundStylingCard.tsx
+++ b/apps/web/modules/ui/components/background-styling-card/index.tsx
@@ -1,5 +1,6 @@
"use client";
+import { SurveyBgSelectorTab } from "@/modules/ui/components/background-styling-card/survey-bg-selector-tab";
import { Badge } from "@/modules/ui/components/badge";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { Slider } from "@/modules/ui/components/slider";
@@ -11,7 +12,6 @@ import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurveyStyling } from "@formbricks/types/surveys/types";
-import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
interface BackgroundStylingCardProps {
open: boolean;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyBgSelectorTab.tsx b/apps/web/modules/ui/components/background-styling-card/survey-bg-selector-tab.tsx
similarity index 89%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyBgSelectorTab.tsx
rename to apps/web/modules/ui/components/background-styling-card/survey-bg-selector-tab.tsx
index abbcfa9ce4..da64a811db 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyBgSelectorTab.tsx
+++ b/apps/web/modules/ui/components/background-styling-card/survey-bg-selector-tab.tsx
@@ -1,13 +1,13 @@
"use client";
+import { AnimatedSurveyBg } from "@/modules/survey/editor/components/animated-survey-bg";
+import { ColorSurveyBg } from "@/modules/survey/editor/components/color-survey-bg";
+import { UploadImageSurveyBg } from "@/modules/survey/editor/components/image-survey-bg";
+import { ImageFromUnsplashSurveyBg } from "@/modules/survey/editor/components/unsplash-images";
import { TabBar } from "@/modules/ui/components/tab-bar";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { useEffect, useState } from "react";
-import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
-import { ColorSurveyBg } from "./ColorSurveyBg";
-import { UploadImageSurveyBg } from "./ImageSurveyBg";
-import { ImageFromUnsplashSurveyBg } from "./UnsplashImages";
interface SurveyBgSelectorTabProps {
handleBgChange: (bg: string, bgType: string) => void;
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CardStylingSettings.tsx b/apps/web/modules/ui/components/card-styling-settings/index.tsx
similarity index 99%
rename from apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CardStylingSettings.tsx
rename to apps/web/modules/ui/components/card-styling-settings/index.tsx
index 6b82b7e63a..10ff2ba987 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/CardStylingSettings.tsx
+++ b/apps/web/modules/ui/components/card-styling-settings/index.tsx
@@ -7,6 +7,7 @@ import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/
import { Slider } from "@/modules/ui/components/slider";
import { Switch } from "@/modules/ui/components/switch";
import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { Project } from "@prisma/client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { CheckIcon } from "lucide-react";
@@ -14,7 +15,7 @@ import React from "react";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
-import { TProject, TProjectStyling } from "@formbricks/types/project";
+import { TProjectStyling } from "@formbricks/types/project";
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
type CardStylingSettingsProps = {
@@ -23,7 +24,7 @@ type CardStylingSettingsProps = {
isSettingsPage?: boolean;
surveyType?: TSurveyType;
disabled?: boolean;
- project: TProject;
+ project: Project;
form: UseFormReturn;
};
diff --git a/apps/web/modules/ui/components/client-logo/index.tsx b/apps/web/modules/ui/components/client-logo/index.tsx
index 574a66d453..83fadaff24 100644
--- a/apps/web/modules/ui/components/client-logo/index.tsx
+++ b/apps/web/modules/ui/components/client-logo/index.tsx
@@ -1,24 +1,24 @@
"use client";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { ArrowUpRight } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@formbricks/lib/cn";
-import { TProject } from "@formbricks/types/project";
interface ClientLogoProps {
environmentId?: string;
- project: TProject;
+ projectLogo: Project["logo"] | null;
previewSurvey?: boolean;
}
-export const ClientLogo = ({ environmentId, project, previewSurvey = false }: ClientLogoProps) => {
+export const ClientLogo = ({ environmentId, projectLogo, previewSurvey = false }: ClientLogoProps) => {
const { t } = useTranslate();
return (
+ style={{ backgroundColor: projectLogo?.bgColor }}>
{previewSurvey && environmentId && (
)}
- {project.logo?.url ? (
+ {projectLogo?.url ? (
;
@@ -19,8 +20,8 @@ interface MediaBackgroundProps {
export const MediaBackground: React.FC = ({
children,
- project,
- survey,
+ styling,
+ surveyType,
isEditorView = false,
isMobilePreview = false,
ContentRef,
@@ -31,26 +32,7 @@ export const MediaBackground: React.FC = ({
const [backgroundLoaded, setBackgroundLoaded] = useState(false);
const [authorDetailsForUnsplash, setAuthorDetailsForUnsplash] = useState({ authorName: "", authorURL: "" });
- // get the background from either the survey or the project styling
- const background = useMemo(() => {
- // allow style overwrite is disabled from the project
- if (!project.styling.allowStyleOverwrite) {
- return project.styling.background;
- }
-
- // allow style overwrite is enabled from the project
- if (project.styling.allowStyleOverwrite) {
- // survey style overwrite is disabled
- if (!survey.styling?.overwriteThemeStyling) {
- return project.styling.background;
- }
-
- // survey style overwrite is enabled
- return survey.styling.background;
- }
-
- return project.styling.background;
- }, [project.styling.allowStyleOverwrite, project.styling.background, survey.styling]);
+ const background = styling.background;
useEffect(() => {
if (background?.bgType === "animation" && animatedBackgroundRef.current) {
@@ -187,7 +169,7 @@ export const MediaBackground: React.FC = ({
className={`relative h-[90%] max-h-[40rem] w-[22rem] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
{/* below element is use to create notch for the mobile device mockup */}
- {survey.type === "link" && renderBackground()}
+ {surveyType === "link" && renderBackground()}
{renderContent()}
);
diff --git a/apps/web/modules/ui/components/preview-survey/index.tsx b/apps/web/modules/ui/components/preview-survey/index.tsx
index e1aa13f4e4..ebbc67aa4e 100644
--- a/apps/web/modules/ui/components/preview-survey/index.tsx
+++ b/apps/web/modules/ui/components/preview-survey/index.tsx
@@ -4,13 +4,12 @@ import { ClientLogo } from "@/modules/ui/components/client-logo";
import { MediaBackground } from "@/modules/ui/components/media-background";
import { ResetProgressButton } from "@/modules/ui/components/reset-progress-button";
import { SurveyInline } from "@/modules/ui/components/survey";
+import { Environment, Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { Variants, motion } from "framer-motion";
import { ExpandIcon, MonitorIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import type { TEnvironment } from "@formbricks/types/environment";
import { TJsFileUploadParams } from "@formbricks/types/js";
-import type { TProject } from "@formbricks/types/project";
import { TProjectStyling } from "@formbricks/types/project";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurvey, TSurveyQuestionId, TSurveyStyling } from "@formbricks/types/surveys/types";
@@ -23,8 +22,8 @@ interface PreviewSurveyProps {
survey: TSurvey;
questionId?: string | null;
previewType?: TPreviewType;
- project: TProject;
- environment: TEnvironment;
+ project: Project;
+ environment: Pick;
languageCode: string;
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise;
}
@@ -251,7 +250,11 @@ export const PreviewSurvey = ({
-
+
{previewType === "modal" ? (
{!styling.isLogoHidden && (
-
+
)}
@@ -379,10 +382,14 @@ export const PreviewSurvey = ({
/>
) : (
-
+
{!styling.isLogoHidden && (
-
+
)}
diff --git a/apps/web/modules/ui/components/question-toggle-table/index.tsx b/apps/web/modules/ui/components/question-toggle-table/index.tsx
index 87b2ae0aab..28340d3454 100644
--- a/apps/web/modules/ui/components/question-toggle-table/index.tsx
+++ b/apps/web/modules/ui/components/question-toggle-table/index.tsx
@@ -1,6 +1,6 @@
"use client";
-import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
+import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { Switch } from "@/modules/ui/components/switch";
import { useTranslate } from "@tolgee/react";
import {
diff --git a/apps/web/modules/projects/settings/look/components/theme-styling-preview-survey.tsx b/apps/web/modules/ui/components/theme-styling-preview-survey/index.tsx
similarity index 95%
rename from apps/web/modules/projects/settings/look/components/theme-styling-preview-survey.tsx
rename to apps/web/modules/ui/components/theme-styling-preview-survey/index.tsx
index 5a07880976..86842c691f 100644
--- a/apps/web/modules/projects/settings/look/components/theme-styling-preview-survey.tsx
+++ b/apps/web/modules/ui/components/theme-styling-preview-survey/index.tsx
@@ -5,15 +5,15 @@ import { MediaBackground } from "@/modules/ui/components/media-background";
import { Modal } from "@/modules/ui/components/preview-survey/components/modal";
import { ResetProgressButton } from "@/modules/ui/components/reset-progress-button";
import { SurveyInline } from "@/modules/ui/components/survey";
+import { Project } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { Variants, motion } from "framer-motion";
import { Fragment, useRef, useState } from "react";
-import type { TProject } from "@formbricks/types/project";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys/types";
interface ThemeStylingPreviewSurveyProps {
survey: TSurvey;
- project: TProject;
+ project: Project;
previewType: TSurveyType;
setPreviewType: (type: TSurveyType) => void;
}
@@ -173,10 +173,14 @@ export const ThemeStylingPreviewSurvey = ({
) : (
-
+
{!project.styling?.isLogoHidden && (
-
+
)}
{
create: [
{
type: "development",
- actionClasses: {
- create: [
- {
- name: "New Session",
- description: "Gets fired when a new session is created",
- type: "automatic",
- },
- ],
- },
attributeKeys: {
create: [
{
@@ -104,15 +95,6 @@ export const createUsersFixture = (page: Page, workerInfo: TestInfo) => {
},
{
type: "production",
- actionClasses: {
- create: [
- {
- name: "New Session",
- description: "Gets fired when a new session is created",
- type: "automatic",
- },
- ],
- },
attributeKeys: {
create: [
{
diff --git a/apps/web/playwright/js.spec.ts b/apps/web/playwright/js.spec.ts
index 9a3c63c47b..547be5d748 100644
--- a/apps/web/playwright/js.spec.ts
+++ b/apps/web/playwright/js.spec.ts
@@ -78,7 +78,12 @@ test.describe("JS Package Test", async () => {
await page.locator("#whenToSendCardTrigger").click();
await page.getByRole("button", { name: "Add action" }).click();
- await page.getByText("New SessionGets fired when a").click();
+
+ await page.getByRole("button", { name: "Capture new action" }).click();
+ await page.getByPlaceholder("E.g. Clicked Download").click();
+ await page.getByPlaceholder("E.g. Clicked Download").fill("New Session");
+ await page.getByText("Page View").click();
+ await page.getByRole("button", { name: "Create action" }).click();
await page.locator("#recontactOptionsCardTrigger").click();
await page.locator("label").filter({ hasText: "Keep showing while conditions" }).click();
diff --git a/packages/lib/actionClass/service.ts b/packages/lib/actionClass/service.ts
index c87b09795c..ec2ed407cf 100644
--- a/packages/lib/actionClass/service.ts
+++ b/packages/lib/actionClass/service.ts
@@ -1,7 +1,7 @@
"use server";
import "server-only";
-import { Prisma } from "@prisma/client";
+import { ActionClass, Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/types/action-classes";
@@ -139,7 +139,7 @@ export const deleteActionClass = async (actionClassId: string): Promise
=> {
+): Promise => {
validateInputs([environmentId, ZId], [actionClass, ZActionClassInput]);
const { environmentId: _, ...actionClassInput } = actionClass;
diff --git a/packages/lib/environment/service.ts b/packages/lib/environment/service.ts
index 0b111eaea1..b14b6ba22d 100644
--- a/packages/lib/environment/service.ts
+++ b/packages/lib/environment/service.ts
@@ -164,15 +164,6 @@ export const createEnvironment = async (
type: environmentInput.type || "development",
project: { connect: { id: projectId } },
appSetupCompleted: environmentInput.appSetupCompleted || false,
- actionClasses: {
- create: [
- {
- name: "New Session",
- description: "Gets fired when a new session is created",
- type: "automatic",
- },
- ],
- },
attributeKeys: {
create: [
{
diff --git a/packages/lib/organization/service.ts b/packages/lib/organization/service.ts
index 0516a70387..4b07687f02 100644
--- a/packages/lib/organization/service.ts
+++ b/packages/lib/organization/service.ts
@@ -380,7 +380,8 @@ export const getMonthlyOrganizationResponseCount = reactCache(
export const subscribeOrganizationMembersToSurveyResponses = async (
surveyId: string,
- createdBy: string
+ createdBy: string,
+ organizationId: string
): Promise => {
try {
const surveyCreator = await prisma.user.findUnique({
@@ -393,6 +394,10 @@ export const subscribeOrganizationMembersToSurveyResponses = async (
throw new ResourceNotFoundError("User", createdBy);
}
+ if (surveyCreator.notificationSettings?.unsubscribedOrganizationIds?.includes(organizationId)) {
+ return;
+ }
+
const defaultSettings = { alert: {}, weeklySummary: {} };
const updatedNotificationSettings: TUserNotificationSettings = {
...defaultSettings,
diff --git a/packages/lib/response/service.ts b/packages/lib/response/service.ts
index 5a1b0532c4..5102986003 100644
--- a/packages/lib/response/service.ts
+++ b/packages/lib/response/service.ts
@@ -663,37 +663,3 @@ export const getResponseCountBySurveyId = reactCache(
}
)()
);
-
-export const getIfResponseWithSurveyIdAndEmailExist = reactCache(
- async (surveyId: string, email: string): Promise =>
- cache(
- async () => {
- validateInputs([surveyId, ZId], [email, ZString]);
-
- try {
- const response = await prisma.response.findFirst({
- where: {
- surveyId,
- data: {
- path: ["verifiedEmail"],
- equals: email,
- },
- },
- select: { id: true },
- });
-
- return !!response;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError(error.message);
- }
-
- throw error;
- }
- },
- [`getIfResponseWithSurveyIdAndEmailExist-${surveyId}-${email}`],
- {
- tags: [responseCache.tag.bySurveyId(surveyId)],
- }
- )()
-);
diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts
index e0ccedb55a..6aa137c92f 100644
--- a/packages/lib/survey/service.ts
+++ b/packages/lib/survey/service.ts
@@ -1,14 +1,10 @@
import "server-only";
-import { createId } from "@paralleldrive/cuid2";
-import { Prisma } from "@prisma/client";
+import { ActionClass, Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
-import { TActionClass } from "@formbricks/types/action-classes";
import { ZOptionalNumber } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/common";
-import { TEnvironment } from "@formbricks/types/environment";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
-import { TProject } from "@formbricks/types/project";
import { TSegment, ZSegmentFilters } from "@formbricks/types/segment";
import {
TSurvey,
@@ -19,21 +15,15 @@ import {
ZSurvey,
ZSurveyCreateInput,
} from "@formbricks/types/surveys/types";
-import { actionClassCache } from "../actionClass/cache";
import { getActionClasses } from "../actionClass/service";
import { cache } from "../cache";
import { segmentCache } from "../cache/segment";
import { ITEMS_PER_PAGE } from "../constants";
-import { getEnvironment } from "../environment/service";
import {
getOrganizationByEnvironmentId,
subscribeOrganizationMembersToSurveyResponses,
} from "../organization/service";
-import { structuredClone } from "../pollyfills/structuredClone";
import { capturePosthogEnvironmentEvent } from "../posthogServer";
-import { projectCache } from "../project/cache";
-import { getProjectByEnvironmentId } from "../project/service";
-import { responseCache } from "../response/cache";
import { getIsAIEnabled } from "../utils/ai";
import { validateInputs } from "../utils/validate";
import { surveyCache } from "./cache";
@@ -132,7 +122,7 @@ export const selectSurvey = {
followUps: true,
} satisfies Prisma.SurveySelect;
-const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: TActionClass[]) => {
+const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => {
if (!triggers) return;
// check if all the triggers are valid
@@ -153,7 +143,7 @@ const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: TAc
const handleTriggerUpdates = (
updatedTriggers: TSurvey["triggers"],
currentTriggers: TSurvey["triggers"],
- actionClasses: TActionClass[]
+ actionClasses: ActionClass[]
) => {
if (!updatedTriggers) return {};
checkTriggersValidity(updatedTriggers, actionClasses);
@@ -348,37 +338,6 @@ export const getSurveyCount = reactCache(
)()
);
-export const getInProgressSurveyCount = reactCache(
- async (environmentId: string, filterCriteria?: TSurveyFilterCriteria): Promise =>
- cache(
- async () => {
- validateInputs([environmentId, ZId]);
- try {
- const surveyCount = await prisma.survey.count({
- where: {
- environmentId: environmentId,
- status: "inProgress",
- ...buildWhereClause(filterCriteria),
- },
- });
-
- return surveyCount;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- console.error(error);
- throw new DatabaseError(error.message);
- }
-
- throw error;
- }
- },
- [`getInProgressSurveyCount-${environmentId}-${JSON.stringify(filterCriteria)}`],
- {
- tags: [surveyCache.tag.byEnvironmentId(environmentId)],
- }
- )()
-);
-
export const updateSurvey = async (updatedSurvey: TSurvey): Promise => {
validateInputs([updatedSurvey, ZSurvey]);
@@ -747,67 +706,6 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise =>
}
};
-export const deleteSurvey = async (surveyId: string) => {
- validateInputs([surveyId, ZId]);
-
- try {
- const deletedSurvey = await prisma.survey.delete({
- where: {
- id: surveyId,
- },
- select: selectSurvey,
- });
-
- if (deletedSurvey.type === "app" && deletedSurvey.segment?.isPrivate) {
- const deletedSegment = await prisma.segment.delete({
- where: {
- id: deletedSurvey.segment.id,
- },
- });
-
- if (deletedSegment) {
- segmentCache.revalidate({
- id: deletedSegment.id,
- environmentId: deletedSurvey.environmentId,
- });
- }
- }
-
- responseCache.revalidate({
- surveyId,
- environmentId: deletedSurvey.environmentId,
- });
- surveyCache.revalidate({
- id: deletedSurvey.id,
- environmentId: deletedSurvey.environmentId,
- resultShareKey: deletedSurvey.resultShareKey ?? undefined,
- });
-
- if (deletedSurvey.segment?.id) {
- segmentCache.revalidate({
- id: deletedSurvey.segment.id,
- environmentId: deletedSurvey.environmentId,
- });
- }
-
- // Revalidate public triggers by actionClassId
- deletedSurvey.triggers.forEach((trigger) => {
- surveyCache.revalidate({
- actionClassId: trigger.actionClass.id,
- });
- });
-
- return deletedSurvey;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- console.error(error);
- throw new DatabaseError(error.message);
- }
-
- throw error;
- }
-};
-
export const createSurvey = async (
environmentId: string,
surveyBody: TSurveyCreateInput
@@ -963,7 +861,7 @@ export const createSurvey = async (
});
if (createdBy) {
- await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy);
+ await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy, organization.id);
}
await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", {
@@ -981,258 +879,6 @@ export const createSurvey = async (
}
};
-export const copySurveyToOtherEnvironment = async (
- environmentId: string,
- surveyId: string,
- targetEnvironmentId: string,
- userId: string
-) => {
- validateInputs([environmentId, ZId], [surveyId, ZId], [targetEnvironmentId, ZId], [userId, ZId]);
-
- try {
- const isSameEnvironment = environmentId === targetEnvironmentId;
-
- // Fetch required resources
- const [existingEnvironment, existingProject, existingSurvey] = await Promise.all([
- getEnvironment(environmentId),
- getProjectByEnvironmentId(environmentId),
- getSurvey(surveyId),
- ]);
-
- if (!existingEnvironment) throw new ResourceNotFoundError("Environment", environmentId);
- if (!existingProject) throw new ResourceNotFoundError("Project", environmentId);
- if (!existingSurvey) throw new ResourceNotFoundError("Survey", surveyId);
-
- let targetEnvironment: TEnvironment | null = null;
- let targetProject: TProject | null = null;
-
- if (isSameEnvironment) {
- targetEnvironment = existingEnvironment;
- targetProject = existingProject;
- } else {
- [targetEnvironment, targetProject] = await Promise.all([
- getEnvironment(targetEnvironmentId),
- getProjectByEnvironmentId(targetEnvironmentId),
- ]);
-
- if (!targetEnvironment) throw new ResourceNotFoundError("Environment", targetEnvironmentId);
- if (!targetProject) throw new ResourceNotFoundError("Project", targetEnvironmentId);
- }
-
- const {
- environmentId: _,
- createdBy,
- id: existingSurveyId,
- createdAt,
- updatedAt,
- ...restExistingSurvey
- } = existingSurvey;
- const hasLanguages = existingSurvey.languages && existingSurvey.languages.length > 0;
-
- // Prepare survey data
- const surveyData: Prisma.SurveyCreateInput = {
- ...restExistingSurvey,
- id: createId(),
- name: `${existingSurvey.name} (copy)`,
- type: existingSurvey.type,
- status: "draft",
- welcomeCard: structuredClone(existingSurvey.welcomeCard),
- questions: structuredClone(existingSurvey.questions),
- endings: structuredClone(existingSurvey.endings),
- variables: structuredClone(existingSurvey.variables),
- hiddenFields: structuredClone(existingSurvey.hiddenFields),
- languages: hasLanguages
- ? {
- create: existingSurvey.languages.map((surveyLanguage) => ({
- language: {
- connectOrCreate: {
- where: {
- projectId_code: { code: surveyLanguage.language.code, projectId: targetProject.id },
- },
- create: {
- code: surveyLanguage.language.code,
- alias: surveyLanguage.language.alias,
- projectId: targetProject.id,
- },
- },
- },
- default: surveyLanguage.default,
- enabled: surveyLanguage.enabled,
- })),
- }
- : undefined,
- triggers: {
- create: existingSurvey.triggers.map((trigger): Prisma.SurveyTriggerCreateWithoutSurveyInput => {
- const baseActionClassData = {
- name: trigger.actionClass.name,
- environment: { connect: { id: targetEnvironmentId } },
- description: trigger.actionClass.description,
- type: trigger.actionClass.type,
- };
-
- if (isSameEnvironment) {
- return {
- actionClass: { connect: { id: trigger.actionClass.id } },
- };
- } else if (trigger.actionClass.type === "code") {
- return {
- actionClass: {
- connectOrCreate: {
- where: {
- key_environmentId: { key: trigger.actionClass.key!, environmentId: targetEnvironmentId },
- },
- create: {
- ...baseActionClassData,
- key: trigger.actionClass.key,
- },
- },
- },
- };
- } else {
- return {
- actionClass: {
- connectOrCreate: {
- where: {
- name_environmentId: {
- name: trigger.actionClass.name,
- environmentId: targetEnvironmentId,
- },
- },
- create: {
- ...baseActionClassData,
- noCodeConfig: trigger.actionClass.noCodeConfig
- ? structuredClone(trigger.actionClass.noCodeConfig)
- : undefined,
- },
- },
- },
- };
- }
- }),
- },
- environment: {
- connect: {
- id: targetEnvironmentId,
- },
- },
- creator: {
- connect: {
- id: userId,
- },
- },
- surveyClosedMessage: existingSurvey.surveyClosedMessage
- ? structuredClone(existingSurvey.surveyClosedMessage)
- : Prisma.JsonNull,
- singleUse: existingSurvey.singleUse ? structuredClone(existingSurvey.singleUse) : Prisma.JsonNull,
- projectOverwrites: existingSurvey.projectOverwrites
- ? structuredClone(existingSurvey.projectOverwrites)
- : Prisma.JsonNull,
- styling: existingSurvey.styling ? structuredClone(existingSurvey.styling) : Prisma.JsonNull,
- segment: undefined,
- followUps: {
- createMany: {
- data: existingSurvey.followUps.map((followUp) => ({
- name: followUp.name,
- trigger: followUp.trigger,
- action: followUp.action,
- })),
- },
- },
- };
-
- // Handle segment
- if (existingSurvey.segment) {
- if (existingSurvey.segment.isPrivate) {
- surveyData.segment = {
- create: {
- title: surveyData.id!,
- isPrivate: true,
- filters: existingSurvey.segment.filters,
- environment: { connect: { id: targetEnvironmentId } },
- },
- };
- } else if (isSameEnvironment) {
- surveyData.segment = { connect: { id: existingSurvey.segment.id } };
- } else {
- const existingSegmentInTargetEnvironment = await prisma.segment.findFirst({
- where: {
- title: existingSurvey.segment.title,
- isPrivate: false,
- environmentId: targetEnvironmentId,
- },
- });
-
- surveyData.segment = {
- create: {
- title: existingSegmentInTargetEnvironment
- ? `${existingSurvey.segment.title}-${Date.now()}`
- : existingSurvey.segment.title,
- isPrivate: false,
- filters: existingSurvey.segment.filters,
- environment: { connect: { id: targetEnvironmentId } },
- },
- };
- }
- }
-
- const targetProjectLanguageCodes = targetProject.languages.map((language) => language.code);
- const newSurvey = await prisma.survey.create({
- data: surveyData,
- select: selectSurvey,
- });
-
- // Identify newly created action classes
- const newActionClasses = newSurvey.triggers.map((trigger) => trigger.actionClass);
-
- // Revalidate cache only for newly created action classes
- for (const actionClass of newActionClasses) {
- actionClassCache.revalidate({
- environmentId: actionClass.environmentId,
- name: actionClass.name,
- id: actionClass.id,
- });
- }
-
- let newLanguageCreated = false;
- if (existingSurvey.languages && existingSurvey.languages.length > 0) {
- const targetLanguageCodes = newSurvey.languages.map((lang) => lang.language.code);
- newLanguageCreated = targetLanguageCodes.length > targetProjectLanguageCodes.length;
- }
-
- // Invalidate caches
- if (newLanguageCreated) {
- projectCache.revalidate({ id: targetProject.id, environmentId: targetEnvironmentId });
- }
-
- surveyCache.revalidate({
- id: newSurvey.id,
- environmentId: newSurvey.environmentId,
- resultShareKey: newSurvey.resultShareKey ?? undefined,
- });
-
- existingSurvey.triggers.forEach((trigger) => {
- surveyCache.revalidate({
- actionClassId: trigger.actionClass.id,
- });
- });
-
- if (newSurvey.segment) {
- segmentCache.revalidate({
- id: newSurvey.segment.id,
- environmentId: newSurvey.environmentId,
- });
- }
-
- return newSurvey;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- console.error(error);
- throw new DatabaseError(error.message);
- }
- throw error;
- }
-};
-
export const getSurveyIdByResultShareKey = reactCache(
async (resultShareKey: string): Promise =>
cache(
diff --git a/packages/lib/survey/tests/survey.test.ts b/packages/lib/survey/tests/survey.test.ts
index 05b26dca87..95555ca362 100644
--- a/packages/lib/survey/tests/survey.test.ts
+++ b/packages/lib/survey/tests/survey.test.ts
@@ -1,35 +1,17 @@
import { prisma } from "../../__mocks__/database";
-import { mockResponseNote, mockResponseWithMockPerson } from "../../response/tests/__mocks__/data.mock";
import { Prisma } from "@prisma/client";
import { evaluateLogic } from "surveyLogic/utils";
import { beforeEach, describe, expect, it } from "vitest";
import { testInputValidation } from "vitestSetup";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+import { getSurvey, getSurveyCount, getSurveys, getSurveysByActionClassId, updateSurvey } from "../service";
import {
- copySurveyToOtherEnvironment,
- createSurvey,
- deleteSurvey,
- getSurvey,
- getSurveyCount,
- getSurveys,
- getSurveysByActionClassId,
- updateSurvey,
-} from "../service";
-import {
- createSurveyInput,
mockActionClass,
- mockContactAttributeKey,
- mockDisplay,
- mockEnvironment,
mockId,
mockOrganizationOutput,
- mockPrismaPerson,
- mockProject,
mockSurveyOutput,
mockSurveyWithLogic,
- mockSyncSurveyOutput,
mockTransformedSurveyOutput,
- mockTransformedSyncSurveyOutput,
mockUser,
updateSurveyInput,
} from "./__mock__/survey.mock";
@@ -325,111 +307,90 @@ describe("Tests for updateSurvey", () => {
});
});
-describe("Tests for deleteSurvey", () => {
- describe("Happy Path", () => {
- it("Deletes a survey successfully", async () => {
- prisma.survey.delete.mockResolvedValueOnce(mockSurveyOutput);
- const deletedSurvey = await deleteSurvey(mockId);
- expect(deletedSurvey).toEqual(mockSurveyOutput);
- });
- });
+// describe("Tests for createSurvey", () => {
+// beforeEach(() => {
+// prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
+// });
- describe("Sad Path", () => {
- testInputValidation(deleteSurvey, "123#");
+// describe("Happy Path", () => {
+// it("Creates a survey successfully", async () => {
+// prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput);
+// prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput);
+// prisma.actionClass.findMany.mockResolvedValue([mockActionClass]);
+// prisma.user.findMany.mockResolvedValueOnce([
+// {
+// ...mockUser,
+// twoFactorSecret: null,
+// backupCodes: null,
+// password: null,
+// identityProviderAccountId: null,
+// groupId: null,
+// role: "engineer",
+// },
+// ]);
+// prisma.user.update.mockResolvedValueOnce({
+// ...mockUser,
+// twoFactorSecret: null,
+// backupCodes: null,
+// password: null,
+// identityProviderAccountId: null,
+// groupId: null,
+// role: "engineer",
+// });
+// const createdSurvey = await createSurvey(mockId, createSurveyInput);
+// expect(createdSurvey).toEqual(mockTransformedSurveyOutput);
+// });
+// });
- it("should throw an error if there is an unknown error", async () => {
- const mockErrorMessage = "Unknown error occurred";
- prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
- prisma.survey.delete.mockRejectedValue(new Error(mockErrorMessage));
- await expect(deleteSurvey(mockId)).rejects.toThrow(Error);
- });
- });
-});
+// describe("Sad Path", () => {
+// testInputValidation(createSurvey, "123#", createSurveyInput);
-describe("Tests for createSurvey", () => {
- beforeEach(() => {
- prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
- });
+// it("should throw an error if there is an unknown error", async () => {
+// const mockErrorMessage = "Unknown error occurred";
+// prisma.survey.delete.mockRejectedValue(new Error(mockErrorMessage));
+// await expect(createSurvey(mockId, createSurveyInput)).rejects.toThrow(Error);
+// });
+// });
+// });
- describe("Happy Path", () => {
- it("Creates a survey successfully", async () => {
- prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput);
- prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput);
- prisma.actionClass.findMany.mockResolvedValue([mockActionClass]);
- prisma.user.findMany.mockResolvedValueOnce([
- {
- ...mockUser,
- twoFactorSecret: null,
- backupCodes: null,
- password: null,
- identityProviderAccountId: null,
- groupId: null,
- role: "engineer",
- },
- ]);
- prisma.user.update.mockResolvedValueOnce({
- ...mockUser,
- twoFactorSecret: null,
- backupCodes: null,
- password: null,
- identityProviderAccountId: null,
- groupId: null,
- role: "engineer",
- });
- const createdSurvey = await createSurvey(mockId, createSurveyInput);
- expect(createdSurvey).toEqual(mockTransformedSurveyOutput);
- });
- });
+// describe("Tests for duplicateSurvey", () => {
+// beforeEach(() => {
+// prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
+// });
- describe("Sad Path", () => {
- testInputValidation(createSurvey, "123#", createSurveyInput);
+// describe("Happy Path", () => {
+// it("Duplicates a survey successfully", async () => {
+// prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
+// prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput);
+// // @ts-expect-error
+// prisma.environment.findUnique.mockResolvedValueOnce(mockEnvironment);
+// // @ts-expect-error
+// prisma.project.findFirst.mockResolvedValueOnce(mockProject);
+// prisma.actionClass.findFirst.mockResolvedValueOnce(mockActionClass);
+// prisma.actionClass.create.mockResolvedValueOnce(mockActionClass);
- it("should throw an error if there is an unknown error", async () => {
- const mockErrorMessage = "Unknown error occurred";
- prisma.survey.delete.mockRejectedValue(new Error(mockErrorMessage));
- await expect(createSurvey(mockId, createSurveyInput)).rejects.toThrow(Error);
- });
- });
-});
+// const createdSurvey = await copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId);
+// expect(createdSurvey).toEqual(mockSurveyOutput);
+// });
+// });
-describe("Tests for duplicateSurvey", () => {
- beforeEach(() => {
- prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
- });
+// describe("Sad Path", () => {
+// testInputValidation(copySurveyToOtherEnvironment, "123#", "123#", "123#", "123#", "123#");
- describe("Happy Path", () => {
- it("Duplicates a survey successfully", async () => {
- prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
- prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput);
- // @ts-expect-error
- prisma.environment.findUnique.mockResolvedValueOnce(mockEnvironment);
- // @ts-expect-error
- prisma.project.findFirst.mockResolvedValueOnce(mockProject);
- prisma.actionClass.findFirst.mockResolvedValueOnce(mockActionClass);
- prisma.actionClass.create.mockResolvedValueOnce(mockActionClass);
+// it("Throws ResourceNotFoundError if the survey does not exist", async () => {
+// prisma.survey.findUnique.mockRejectedValueOnce(new ResourceNotFoundError("Survey", mockId));
+// await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow(
+// ResourceNotFoundError
+// );
+// });
- const createdSurvey = await copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId);
- expect(createdSurvey).toEqual(mockSurveyOutput);
- });
- });
-
- describe("Sad Path", () => {
- testInputValidation(copySurveyToOtherEnvironment, "123#", "123#", "123#", "123#", "123#");
-
- it("Throws ResourceNotFoundError if the survey does not exist", async () => {
- prisma.survey.findUnique.mockRejectedValueOnce(new ResourceNotFoundError("Survey", mockId));
- await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow(
- ResourceNotFoundError
- );
- });
-
- it("should throw an error if there is an unknown error", async () => {
- const mockErrorMessage = "Unknown error occurred";
- prisma.survey.create.mockRejectedValue(new Error(mockErrorMessage));
- await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow(Error);
- });
- });
-});
+// it("should throw an error if there is an unknown error", async () => {
+// const mockErrorMessage = "Unknown error occurred";
+// prisma.survey.create.mockRejectedValue(new Error(mockErrorMessage));
+// await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow(Error);
+// });
+// });
+// });
// describe("Tests for getSyncSurveys", () => {
// describe("Happy Path", () => {
diff --git a/packages/lib/utils/templates.ts b/packages/lib/utils/templates.ts
index 8371346f58..cd4763e156 100644
--- a/packages/lib/utils/templates.ts
+++ b/packages/lib/utils/templates.ts
@@ -15,13 +15,13 @@ export const replaceQuestionPresetPlaceholders = (
newQuestion.headline[defaultLanguageCode] = getLocalizedValue(
newQuestion.headline,
defaultLanguageCode
- ).replace("{{projectName}}", project.name);
+ ).replace("$[projectName]", project.name);
}
if (newQuestion.subheader) {
newQuestion.subheader[defaultLanguageCode] = getLocalizedValue(
newQuestion.subheader,
defaultLanguageCode
- )?.replace("{{projectName}}", project.name);
+ )?.replace("$[projectName]", project.name);
}
return newQuestion;
};
@@ -29,7 +29,7 @@ export const replaceQuestionPresetPlaceholders = (
// replace all occurences of projectName with the actual project name in the current template
export const replacePresetPlaceholders = (template: TTemplate, project: any) => {
const preset = structuredClone(template.preset);
- preset.name = preset.name.replace("{{projectName}}", project.name);
+ preset.name = preset.name.replace("$[projectName]", project.name);
preset.questions = preset.questions.map((question) => {
return replaceQuestionPresetPlaceholders(question, project);
});
diff --git a/packages/react-native/src/components/formbricks.tsx b/packages/react-native/src/components/formbricks.tsx
index 53e35b6ac8..b514ea479e 100644
--- a/packages/react-native/src/components/formbricks.tsx
+++ b/packages/react-native/src/components/formbricks.tsx
@@ -1,8 +1,8 @@
+import React, { useCallback, useEffect, useSyncExternalStore } from "react";
import { SurveyWebView } from "@/components/survey-web-view";
import { Logger } from "@/lib/common/logger";
import { setup } from "@/lib/common/setup";
import { SurveyStore } from "@/lib/survey/store";
-import React, { useCallback, useEffect, useSyncExternalStore } from "react";
interface FormbricksProps {
appUrl: string;
diff --git a/packages/react-native/src/components/survey-web-view.tsx b/packages/react-native/src/components/survey-web-view.tsx
index 673915be5e..3aaf684169 100644
--- a/packages/react-native/src/components/survey-web-view.tsx
+++ b/packages/react-native/src/components/survey-web-view.tsx
@@ -1,5 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-call -- required */
/* eslint-disable no-console -- debugging*/
+import React, { type JSX, useEffect, useMemo, useRef, useState } from "react";
+import { Modal } from "react-native";
+import { WebView, type WebViewMessageEvent } from "react-native-webview";
+import { FormbricksAPI } from "@formbricks/api";
import { RNConfig } from "@/lib/common/config";
import { StorageAPI } from "@/lib/common/file-upload";
import { Logger } from "@/lib/common/logger";
@@ -11,10 +15,6 @@ import { type TEnvironmentStateSurvey, type TUserState, ZJsRNWebViewOnMessageDat
import type { TResponseUpdate } from "@/types/response";
import type { TFileUploadParams, TUploadFileConfig } from "@/types/storage";
import type { SurveyInlineProps } from "@/types/survey";
-import React, { type JSX, useEffect, useMemo, useRef, useState } from "react";
-import { Modal } from "react-native";
-import { WebView, type WebViewMessageEvent } from "react-native-webview";
-import { FormbricksAPI } from "@formbricks/api";
const appConfig = RNConfig.getInstance();
const logger = Logger.getInstance();
diff --git a/packages/react-native/src/lib/common/response-queue.ts b/packages/react-native/src/lib/common/response-queue.ts
index 34e333fdc3..35c0d8524c 100644
--- a/packages/react-native/src/lib/common/response-queue.ts
+++ b/packages/react-native/src/lib/common/response-queue.ts
@@ -1,7 +1,7 @@
/* eslint-disable no-console -- required for logging errors */
+import { FormbricksAPI } from "@formbricks/api";
import { type SurveyState } from "@/lib/survey/state";
import { type TResponseUpdate } from "@/types/response";
-import { FormbricksAPI } from "@formbricks/api";
interface QueueConfig {
appUrl: string;
diff --git a/packages/react-native/src/lib/common/tests/command-queue.test.ts b/packages/react-native/src/lib/common/tests/command-queue.test.ts
index 5609693d6b..89b273f7a7 100644
--- a/packages/react-native/src/lib/common/tests/command-queue.test.ts
+++ b/packages/react-native/src/lib/common/tests/command-queue.test.ts
@@ -1,7 +1,7 @@
+import { beforeEach, describe, expect, test, vi } from "vitest";
import { CommandQueue } from "@/lib/common/command-queue";
import { checkSetup } from "@/lib/common/setup";
import { type Result } from "@/types/error";
-import { beforeEach, describe, expect, test, vi } from "vitest";
// Mock the setup module so we can control checkSetup()
vi.mock("@/lib/common/setup", () => ({
diff --git a/packages/react-native/src/lib/common/tests/config.test.ts b/packages/react-native/src/lib/common/tests/config.test.ts
index e935969bea..db63fd5633 100644
--- a/packages/react-native/src/lib/common/tests/config.test.ts
+++ b/packages/react-native/src/lib/common/tests/config.test.ts
@@ -1,9 +1,9 @@
// config.test.ts
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { mockConfig } from "./__mocks__/config.mock";
import { RNConfig, RN_ASYNC_STORAGE_KEY } from "@/lib/common/config";
import type { TConfig, TConfigUpdateInput } from "@/types/config";
-import AsyncStorage from "@react-native-async-storage/async-storage";
-import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
// Define mocks outside of any describe block
diff --git a/packages/react-native/src/lib/common/tests/file-upload.test.ts b/packages/react-native/src/lib/common/tests/file-upload.test.ts
index ecf4b24b53..4000c69971 100644
--- a/packages/react-native/src/lib/common/tests/file-upload.test.ts
+++ b/packages/react-native/src/lib/common/tests/file-upload.test.ts
@@ -1,7 +1,7 @@
// file-upload.test.ts
+import { beforeEach, describe, expect, test, vi } from "vitest";
import { StorageAPI } from "@/lib/common/file-upload";
import type { TUploadFileConfig } from "@/types/storage";
-import { beforeEach, describe, expect, test, vi } from "vitest";
// A global fetch mock so we can capture fetch calls.
// Alternatively, use `vi.stubGlobal("fetch", ...)`.
diff --git a/packages/react-native/src/lib/common/tests/logger.test.ts b/packages/react-native/src/lib/common/tests/logger.test.ts
index bad468308e..abedede805 100644
--- a/packages/react-native/src/lib/common/tests/logger.test.ts
+++ b/packages/react-native/src/lib/common/tests/logger.test.ts
@@ -1,6 +1,6 @@
// logger.test.ts
-import { Logger } from "@/lib/common/logger";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import { Logger } from "@/lib/common/logger";
// adjust import path as needed
diff --git a/packages/react-native/src/lib/common/tests/response-queue.test.ts b/packages/react-native/src/lib/common/tests/response-queue.test.ts
index e030916f6f..63645bfdff 100644
--- a/packages/react-native/src/lib/common/tests/response-queue.test.ts
+++ b/packages/react-native/src/lib/common/tests/response-queue.test.ts
@@ -1,4 +1,6 @@
// response-queue.test.ts
+import { beforeEach, describe, expect, test, vi } from "vitest";
+import { type FormbricksAPI } from "@formbricks/api";
import {
mockAppUrl,
mockDisplayId,
@@ -11,8 +13,6 @@ import {
import { ResponseQueue } from "@/lib/common/response-queue";
import type { SurveyState } from "@/lib/survey/state";
import type { TResponseUpdate } from "@/types/response";
-import { beforeEach, describe, expect, test, vi } from "vitest";
-import { type FormbricksAPI } from "@formbricks/api";
describe("ResponseQueue", () => {
let responseQueue: ResponseQueue;
diff --git a/packages/react-native/src/lib/common/tests/setup.test.ts b/packages/react-native/src/lib/common/tests/setup.test.ts
index 6cf8949302..63759d62f7 100644
--- a/packages/react-native/src/lib/common/tests/setup.test.ts
+++ b/packages/react-native/src/lib/common/tests/setup.test.ts
@@ -1,3 +1,5 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { RNConfig, RN_ASYNC_STORAGE_KEY } from "@/lib/common/config";
import {
addCleanupEventListeners,
@@ -10,8 +12,6 @@ import { filterSurveys, isNowExpired } from "@/lib/common/utils";
import { fetchEnvironmentState } from "@/lib/environment/state";
import { DEFAULT_USER_STATE_NO_USER_ID } from "@/lib/user/state";
import { sendUpdatesToBackend } from "@/lib/user/update";
-import AsyncStorage from "@react-native-async-storage/async-storage";
-import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
// 1) Mock AsyncStorage
vi.mock("@react-native-async-storage/async-storage", () => ({
diff --git a/packages/react-native/src/lib/environment/state.ts b/packages/react-native/src/lib/environment/state.ts
index 0ef017a5c9..f62349df67 100644
--- a/packages/react-native/src/lib/environment/state.ts
+++ b/packages/react-native/src/lib/environment/state.ts
@@ -1,10 +1,10 @@
/* eslint-disable no-console -- logging required for error logging */
+import { FormbricksAPI } from "@formbricks/api";
import { RNConfig } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { filterSurveys } from "@/lib/common/utils";
import type { TConfigInput, TEnvironmentState } from "@/types/config";
import { type ApiErrorResponse, type Result, err, ok } from "@/types/error";
-import { FormbricksAPI } from "@formbricks/api";
let environmentStateSyncIntervalId: number | null = null;
diff --git a/packages/react-native/src/lib/environment/tests/state.test.ts b/packages/react-native/src/lib/environment/tests/state.test.ts
index 3b95e4b074..04807d866c 100644
--- a/packages/react-native/src/lib/environment/tests/state.test.ts
+++ b/packages/react-native/src/lib/environment/tests/state.test.ts
@@ -1,4 +1,6 @@
// state.test.ts
+import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import { FormbricksAPI } from "@formbricks/api";
import { RNConfig } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { filterSurveys } from "@/lib/common/utils";
@@ -8,8 +10,6 @@ import {
fetchEnvironmentState,
} from "@/lib/environment/state";
import type { TEnvironmentState } from "@/types/config";
-import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
-import { FormbricksAPI } from "@formbricks/api";
// Mock the FormbricksAPI so we can control environment.getState
vi.mock("@formbricks/api", () => ({
diff --git a/packages/react-native/src/lib/survey/tests/action.test.ts b/packages/react-native/src/lib/survey/tests/action.test.ts
index 9d8aa7ff48..76e93a36d8 100644
--- a/packages/react-native/src/lib/survey/tests/action.test.ts
+++ b/packages/react-native/src/lib/survey/tests/action.test.ts
@@ -1,10 +1,10 @@
+import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
import { RNConfig } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { shouldDisplayBasedOnPercentage } from "@/lib/common/utils";
import { track, trackAction, triggerSurvey } from "@/lib/survey/action";
import { SurveyStore } from "@/lib/survey/store";
import { type TEnvironmentStateSurvey } from "@/types/config";
-import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("@/lib/common/config", () => ({
RNConfig: {
diff --git a/packages/react-native/src/lib/user/tests/attribute.test.ts b/packages/react-native/src/lib/user/tests/attribute.test.ts
index c77122a995..b827c71803 100644
--- a/packages/react-native/src/lib/user/tests/attribute.test.ts
+++ b/packages/react-native/src/lib/user/tests/attribute.test.ts
@@ -1,6 +1,6 @@
+import { beforeEach, describe, expect, test, vi } from "vitest";
import { setAttributes } from "@/lib/user/attribute";
import { UpdateQueue } from "@/lib/user/update-queue";
-import { beforeEach, describe, expect, test, vi } from "vitest";
export const mockAttributes = {
name: "John Doe",
diff --git a/packages/react-native/src/lib/user/tests/state.test.ts b/packages/react-native/src/lib/user/tests/state.test.ts
index 7a2c3dda2f..decc0d3adf 100644
--- a/packages/react-native/src/lib/user/tests/state.test.ts
+++ b/packages/react-native/src/lib/user/tests/state.test.ts
@@ -1,6 +1,6 @@
+import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { RNConfig } from "@/lib/common/config";
import { addUserStateExpiryCheckListener, clearUserStateExpiryCheckListener } from "@/lib/user/state";
-import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
const mockUserId = "user_123";
diff --git a/packages/react-native/src/lib/user/tests/update-queue.test.ts b/packages/react-native/src/lib/user/tests/update-queue.test.ts
index 96fe449695..8dfe6742e2 100644
--- a/packages/react-native/src/lib/user/tests/update-queue.test.ts
+++ b/packages/react-native/src/lib/user/tests/update-queue.test.ts
@@ -1,8 +1,8 @@
+import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
import { mockAttributes, mockUserId1, mockUserId2 } from "@/lib/user/tests/__mocks__/update-queue.mock";
import { RNConfig } from "@/lib/common/config";
import { sendUpdates } from "@/lib/user/update";
import { UpdateQueue } from "@/lib/user/update-queue";
-import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
// Mock dependencies
vi.mock("@/lib/common/config", () => ({
diff --git a/packages/react-native/src/lib/user/tests/update.test.ts b/packages/react-native/src/lib/user/tests/update.test.ts
index c429e66f40..14c42f1f60 100644
--- a/packages/react-native/src/lib/user/tests/update.test.ts
+++ b/packages/react-native/src/lib/user/tests/update.test.ts
@@ -1,3 +1,5 @@
+import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
+import { FormbricksAPI } from "@formbricks/api";
import {
mockAppUrl,
mockAttributes,
@@ -8,8 +10,6 @@ import { RNConfig } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { sendUpdates, sendUpdatesToBackend } from "@/lib/user/update";
import { type TUpdates } from "@/types/config";
-import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
-import { FormbricksAPI } from "@formbricks/api";
vi.mock("@/lib/common/config", () => ({
RNConfig: {
diff --git a/packages/react-native/src/lib/user/tests/user.test.ts b/packages/react-native/src/lib/user/tests/user.test.ts
index 6e00219b4e..27ccdf2f86 100644
--- a/packages/react-native/src/lib/user/tests/user.test.ts
+++ b/packages/react-native/src/lib/user/tests/user.test.ts
@@ -1,9 +1,9 @@
+import { type Mock, type MockInstance, beforeEach, describe, expect, test, vi } from "vitest";
import { RNConfig } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { setup, tearDown } from "@/lib/common/setup";
import { UpdateQueue } from "@/lib/user/update-queue";
import { logout, setUserId } from "@/lib/user/user";
-import { type Mock, type MockInstance, beforeEach, describe, expect, test, vi } from "vitest";
// Mock dependencies
vi.mock("@/lib/common/config", () => ({
diff --git a/packages/react-native/src/lib/user/update.ts b/packages/react-native/src/lib/user/update.ts
index b63b7b9648..2237a945e0 100644
--- a/packages/react-native/src/lib/user/update.ts
+++ b/packages/react-native/src/lib/user/update.ts
@@ -1,10 +1,10 @@
/* eslint-disable no-console -- required for logging errors */
+import { FormbricksAPI } from "@formbricks/api";
import { RNConfig } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { filterSurveys } from "@/lib/common/utils";
import { type TUpdates, type TUserState } from "@/types/config";
import { type ApiErrorResponse, type Result, err, ok, okVoid } from "@/types/error";
-import { FormbricksAPI } from "@formbricks/api";
export const sendUpdatesToBackend = async ({
appUrl,
diff --git a/packages/react-native/src/types/config.ts b/packages/react-native/src/types/config.ts
index 1ff9f88768..ddb6826cb2 100644
--- a/packages/react-native/src/types/config.ts
+++ b/packages/react-native/src/types/config.ts
@@ -1,8 +1,8 @@
/* eslint-disable import/no-extraneous-dependencies -- required for Prisma types */
-import { type TResponseUpdate, ZResponseUpdate } from "@/types/response";
-import { type TFileUploadParams, ZFileUploadParams } from "@/types/storage";
import type { ActionClass, Language, Project, Segment, Survey, SurveyLanguage } from "@prisma/client";
import { z } from "zod";
+import { type TResponseUpdate, ZResponseUpdate } from "@/types/response";
+import { type TFileUploadParams, ZFileUploadParams } from "@/types/storage";
export type TEnvironmentStateSurvey = Pick<
Survey,
diff --git a/packages/types/project.ts b/packages/types/project.ts
index d2cee4624d..4f41bf5e66 100644
--- a/packages/types/project.ts
+++ b/packages/types/project.ts
@@ -31,6 +31,7 @@ export const ZLanguage = z.object({
updatedAt: z.date(),
code: z.string(),
alias: z.string().nullable(),
+ projectId: z.string().cuid2(),
});
export type TLanguage = z.infer;