mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-04 18:49:39 -06:00
fix: Backport/critical fixes to 4.0 (#6563)
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
committed by
GitHub
parent
3ba6dd9ada
commit
bdfbc4b0f6
44
.github/workflows/e2e.yml
vendored
44
.github/workflows/e2e.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U testuser"
|
||||
--health-cmd="pg_isready -U postgres"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
@@ -49,25 +49,15 @@ jobs:
|
||||
image: valkey/valkey@sha256:12ba4f45a7c3e1d0f076acd616cb230834e75a77e8516dde382720af32832d6d
|
||||
ports:
|
||||
- 6379:6379
|
||||
minio:
|
||||
image: bitnami/minio:2025.7.23-debian-12-r5
|
||||
env:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
ports:
|
||||
- 9000:9000
|
||||
options: >-
|
||||
--health-cmd="curl -fsS http://localhost:9000/minio/health/live || exit 1"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=20
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: allow
|
||||
egress-policy: audit
|
||||
allowed-endpoints: |
|
||||
ee.formbricks.com:443
|
||||
registry-1.docker.io:443
|
||||
docker.io:443
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
@@ -101,8 +91,8 @@ jobs:
|
||||
echo "S3_REGION=us-east-1" >> .env
|
||||
echo "S3_BUCKET_NAME=formbricks-e2e" >> .env
|
||||
echo "S3_ENDPOINT_URL=http://localhost:9000" >> .env
|
||||
echo "S3_ACCESS_KEY=minioadmin" >> .env
|
||||
echo "S3_SECRET_KEY=minioadmin" >> .env
|
||||
echo "S3_ACCESS_KEY=devminio" >> .env
|
||||
echo "S3_SECRET_KEY=devminio123" >> .env
|
||||
echo "S3_FORCE_PATH_STYLE=1" >> .env
|
||||
shell: bash
|
||||
|
||||
@@ -122,6 +112,22 @@ jobs:
|
||||
chmod +x "${MC_BIN}"
|
||||
sudo mv "${MC_BIN}" /usr/local/bin/mc
|
||||
|
||||
- name: Start MinIO Server
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Start MinIO server in background
|
||||
docker run -d \
|
||||
--name minio-server \
|
||||
-p 9000:9000 \
|
||||
-p 9001:9001 \
|
||||
-e MINIO_ROOT_USER=devminio \
|
||||
-e MINIO_ROOT_PASSWORD=devminio123 \
|
||||
minio/minio:RELEASE.2025-09-07T16-13-09Z \
|
||||
server /data --console-address :9001
|
||||
|
||||
echo "MinIO server started"
|
||||
|
||||
- name: Wait for MinIO and create S3 bucket
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -142,7 +148,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mc alias set local http://localhost:9000 minioadmin minioadmin
|
||||
mc alias set local http://localhost:9000 devminio devminio123
|
||||
mc mb --ignore-existing local/formbricks-e2e
|
||||
|
||||
- name: Build App
|
||||
@@ -233,4 +239,4 @@ jobs:
|
||||
|
||||
- name: Output App Logs
|
||||
if: failure()
|
||||
run: cat app.log
|
||||
run: cat app.log
|
||||
@@ -31,6 +31,6 @@ describe("IntegrationsTip", () => {
|
||||
|
||||
const linkElement = screen.getByText("environments.settings.notifications.use_the_integration");
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
expect(linkElement).toHaveAttribute("href", `/environments/${environmentId}/integrations`);
|
||||
expect(linkElement).toHaveAttribute("href", `/environments/${environmentId}/project/integrations`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export const IntegrationsTip = ({ environmentId }: IntegrationsTipProps) => {
|
||||
<p className="text-sm">
|
||||
{t("environments.settings.notifications.need_slack_or_discord_notifications")}?
|
||||
<a
|
||||
href={`/environments/${environmentId}/integrations`}
|
||||
href={`/environments/${environmentId}/project/integrations`}
|
||||
className="ml-1 cursor-pointer text-sm underline">
|
||||
{t("environments.settings.notifications.use_the_integration")}
|
||||
</a>
|
||||
|
||||
@@ -75,7 +75,7 @@ export const SuccessView: React.FC<SuccessViewProps> = ({
|
||||
{t("environments.surveys.summary.configure_alerts")}
|
||||
</Link>
|
||||
<Link
|
||||
href={`/environments/${environmentId}/integrations`}
|
||||
href={`/environments/${environmentId}/project/integrations`}
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-center text-sm text-slate-900 hover:border-slate-200 md:p-8">
|
||||
<BlocksIcon className="h-8 w-8 stroke-1 text-slate-900" />
|
||||
{t("environments.surveys.summary.setup_integrations")}
|
||||
|
||||
@@ -357,7 +357,10 @@ const buildNotionPayloadProperties = (
|
||||
|
||||
// notion requires specific payload for each column type
|
||||
// * TYPES NOT SUPPORTED BY NOTION API - rollup, created_by, created_time, last_edited_by, or last_edited_time
|
||||
const getValue = (colType: string, value: string | string[] | Date | number | Record<string, string>) => {
|
||||
const getValue = (
|
||||
colType: string,
|
||||
value: string | string[] | Date | number | Record<string, string> | undefined
|
||||
) => {
|
||||
try {
|
||||
switch (colType) {
|
||||
case "select":
|
||||
|
||||
@@ -62,9 +62,10 @@ export const GET = async (req: Request) => {
|
||||
};
|
||||
|
||||
const result = await createOrUpdateIntegration(environmentId, googleSheetIntegration);
|
||||
|
||||
if (result) {
|
||||
return Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/google-sheets`);
|
||||
return Response.redirect(
|
||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/google-sheets`
|
||||
);
|
||||
}
|
||||
|
||||
return responses.internalServerErrorResponse("Failed to create or update Google Sheets integration");
|
||||
|
||||
@@ -90,7 +90,9 @@ export const GET = withV1ApiWrapper({
|
||||
};
|
||||
await createOrUpdateIntegration(environmentId, airtableIntegrationInput);
|
||||
return {
|
||||
response: Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/airtable`),
|
||||
response: Response.redirect(
|
||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/airtable`
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error({ error, url: req.url }, "Error in GET /api/v1/integrations/airtable/callback");
|
||||
|
||||
@@ -86,13 +86,15 @@ export const GET = withV1ApiWrapper({
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
response: Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/notion`),
|
||||
response: Response.redirect(
|
||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/notion`
|
||||
),
|
||||
};
|
||||
}
|
||||
} else if (error) {
|
||||
return {
|
||||
response: Response.redirect(
|
||||
`${WEBAPP_URL}/environments/${environmentId}/integrations/notion?error=${error}`
|
||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/notion?error=${error}`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,13 +93,15 @@ export const GET = withV1ApiWrapper({
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
response: Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/slack`),
|
||||
response: Response.redirect(
|
||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/slack`
|
||||
),
|
||||
};
|
||||
}
|
||||
} else if (error) {
|
||||
return {
|
||||
response: Response.redirect(
|
||||
`${WEBAPP_URL}/environments/${environmentId}/integrations/slack?error=${error}`
|
||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/slack?error=${error}`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TShuffleOption, TSurveyLogic, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TTemplateRole } from "@formbricks/types/templates";
|
||||
import {
|
||||
buildCTAQuestion,
|
||||
buildConsentQuestion,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
TSurveyRatingQuestion,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
|
||||
|
||||
const getDefaultButtonLabel = (label: string | undefined, t: TFnType) =>
|
||||
createI18nString(label || t("common.next"), []);
|
||||
@@ -391,6 +391,7 @@ export const buildSurvey = (
|
||||
name: string;
|
||||
industries: ("eCommerce" | "saas" | "other")[];
|
||||
channels: ("link" | "app" | "website")[];
|
||||
role: TTemplateRole;
|
||||
description: string;
|
||||
questions: TSurveyQuestion[];
|
||||
endings?: TSurveyEnding[];
|
||||
@@ -403,6 +404,7 @@ export const buildSurvey = (
|
||||
name: config.name,
|
||||
industries: config.industries,
|
||||
channels: config.channels,
|
||||
role: config.role,
|
||||
description: config.description,
|
||||
preset: {
|
||||
...localSurvey,
|
||||
|
||||
@@ -24,6 +24,7 @@ const cartAbandonmentSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.card_abandonment_survey"),
|
||||
role: "productManager",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["app", "website", "link"],
|
||||
description: t("templates.card_abandonment_survey_description"),
|
||||
@@ -124,6 +125,7 @@ const siteAbandonmentSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.site_abandonment_survey"),
|
||||
role: "productManager",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.site_abandonment_survey_description"),
|
||||
@@ -221,6 +223,7 @@ const productMarketFitSuperhuman = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.product_market_fit_superhuman"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.product_market_fit_superhuman_description"),
|
||||
@@ -295,6 +298,7 @@ const onboardingSegmentation = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.onboarding_segmentation"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.onboarding_segmentation_description"),
|
||||
@@ -358,6 +362,7 @@ const churnSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.churn_survey"),
|
||||
role: "sales",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.churn_survey_description"),
|
||||
@@ -447,6 +452,7 @@ const earnedAdvocacyScore = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.earned_advocacy_score_name"),
|
||||
role: "customerSuccess",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.earned_advocacy_score_description"),
|
||||
@@ -519,6 +525,7 @@ const usabilityScoreRatingSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.usability_score_name"),
|
||||
role: "customerSuccess",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.usability_rating_description"),
|
||||
@@ -644,6 +651,7 @@ const improveTrialConversion = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.improve_trial_conversion_name"),
|
||||
role: "sales",
|
||||
industries: ["saas"],
|
||||
channels: ["link", "app"],
|
||||
description: t("templates.improve_trial_conversion_description"),
|
||||
@@ -745,6 +753,7 @@ const reviewPrompt = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.review_prompt_name"),
|
||||
role: "marketing",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link", "app"],
|
||||
description: t("templates.review_prompt_description"),
|
||||
@@ -823,6 +832,7 @@ const interviewPrompt = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.interview_prompt_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.interview_prompt_description"),
|
||||
@@ -850,6 +860,7 @@ const improveActivationRate = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.improve_activation_rate_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["link"],
|
||||
description: t("templates.improve_activation_rate_description"),
|
||||
@@ -940,6 +951,7 @@ const employeeSatisfaction = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.employee_satisfaction_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.employee_satisfaction_description"),
|
||||
@@ -1017,6 +1029,7 @@ const uncoverStrengthsAndWeaknesses = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.uncover_strengths_and_weaknesses_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.uncover_strengths_and_weaknesses_description"),
|
||||
@@ -1070,6 +1083,7 @@ const productMarketFitShort = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.product_market_fit_short_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.product_market_fit_short_description"),
|
||||
@@ -1106,6 +1120,7 @@ const marketAttribution = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.market_attribution_name"),
|
||||
role: "marketing",
|
||||
industries: ["saas", "eCommerce"],
|
||||
channels: ["website", "app", "link"],
|
||||
description: t("templates.market_attribution_description"),
|
||||
@@ -1136,6 +1151,7 @@ const changingSubscriptionExperience = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.changing_subscription_experience_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.changing_subscription_experience_description"),
|
||||
@@ -1178,6 +1194,7 @@ const identifyCustomerGoals = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.identify_customer_goals_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.identify_customer_goals_description"),
|
||||
@@ -1207,6 +1224,7 @@ const featureChaser = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.feature_chaser_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.feature_chaser_description"),
|
||||
@@ -1245,6 +1263,7 @@ const fakeDoorFollowUp = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.fake_door_follow_up_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "eCommerce"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.fake_door_follow_up_description"),
|
||||
@@ -1288,6 +1307,7 @@ const feedbackBox = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.feedback_box_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.feedback_box_description"),
|
||||
@@ -1357,6 +1377,7 @@ const integrationSetupSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.integration_setup_survey_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.integration_setup_survey_description"),
|
||||
@@ -1429,6 +1450,7 @@ const newIntegrationSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.new_integration_survey_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.new_integration_survey_description"),
|
||||
@@ -1460,6 +1482,7 @@ const docsFeedback = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.docs_feedback_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "website", "link"],
|
||||
description: t("templates.docs_feedback_description"),
|
||||
@@ -1499,6 +1522,7 @@ const nps = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.nps_name"),
|
||||
role: "customerSuccess",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link", "website"],
|
||||
description: t("templates.nps_description"),
|
||||
@@ -1539,6 +1563,7 @@ const customerSatisfactionScore = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.csat_name"),
|
||||
role: "customerSuccess",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["app", "link", "website"],
|
||||
description: t("templates.csat_description"),
|
||||
@@ -1707,6 +1732,7 @@ const collectFeedback = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.collect_feedback_name"),
|
||||
role: "productManager",
|
||||
industries: ["other", "eCommerce"],
|
||||
channels: ["website", "link"],
|
||||
description: t("templates.collect_feedback_description"),
|
||||
@@ -1853,6 +1879,7 @@ const identifyUpsellOpportunities = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.identify_upsell_opportunities_name"),
|
||||
role: "sales",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.identify_upsell_opportunities_description"),
|
||||
@@ -1882,6 +1909,7 @@ const prioritizeFeatures = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.prioritize_features_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.prioritize_features_description"),
|
||||
@@ -1934,6 +1962,7 @@ const gaugeFeatureSatisfaction = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.gauge_feature_satisfaction_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.gauge_feature_satisfaction_description"),
|
||||
@@ -1967,6 +1996,7 @@ const marketSiteClarity = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.market_site_clarity_name"),
|
||||
role: "marketing",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["website"],
|
||||
description: t("templates.market_site_clarity_description"),
|
||||
@@ -2008,6 +2038,7 @@ const customerEffortScore = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.customer_effort_score_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app"],
|
||||
description: t("templates.customer_effort_score_description"),
|
||||
@@ -2039,6 +2070,7 @@ const careerDevelopmentSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.career_development_survey_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.career_development_survey_description"),
|
||||
@@ -2125,6 +2157,7 @@ const professionalDevelopmentSurvey = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.professional_development_survey_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.professional_development_survey_description"),
|
||||
@@ -2212,6 +2245,7 @@ const rateCheckoutExperience = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.rate_checkout_experience_name"),
|
||||
role: "productManager",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["website", "app"],
|
||||
description: t("templates.rate_checkout_experience_description"),
|
||||
@@ -2288,6 +2322,7 @@ const measureSearchExperience = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.measure_search_experience_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "eCommerce"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.measure_search_experience_description"),
|
||||
@@ -2364,6 +2399,7 @@ const evaluateContentQuality = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.evaluate_content_quality_name"),
|
||||
role: "marketing",
|
||||
industries: ["other"],
|
||||
channels: ["website"],
|
||||
description: t("templates.evaluate_content_quality_description"),
|
||||
@@ -2441,6 +2477,7 @@ const measureTaskAccomplishment = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.measure_task_accomplishment_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "website"],
|
||||
description: t("templates.measure_task_accomplishment_description"),
|
||||
@@ -2623,6 +2660,7 @@ const identifySignUpBarriers = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.identify_sign_up_barriers_name"),
|
||||
role: "marketing",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["website"],
|
||||
description: t("templates.identify_sign_up_barriers_description"),
|
||||
@@ -2774,6 +2812,7 @@ const buildProductRoadmap = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.build_product_roadmap_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.build_product_roadmap_description"),
|
||||
@@ -2808,6 +2847,7 @@ const understandPurchaseIntention = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.understand_purchase_intention_name"),
|
||||
role: "sales",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["website", "link", "app"],
|
||||
description: t("templates.understand_purchase_intention_description"),
|
||||
@@ -2863,6 +2903,7 @@ const improveNewsletterContent = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.improve_newsletter_content_name"),
|
||||
role: "marketing",
|
||||
industries: ["eCommerce", "saas", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.improve_newsletter_content_description"),
|
||||
@@ -2953,6 +2994,7 @@ const evaluateAProductIdea = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.evaluate_a_product_idea_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["link", "app"],
|
||||
description: t("templates.evaluate_a_product_idea_description"),
|
||||
@@ -3055,6 +3097,7 @@ const understandLowEngagement = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.understand_low_engagement_name"),
|
||||
role: "productManager",
|
||||
industries: ["saas"],
|
||||
channels: ["link"],
|
||||
description: t("templates.understand_low_engagement_description"),
|
||||
@@ -3140,6 +3183,7 @@ const employeeWellBeing = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.employee_well_being_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.employee_well_being_description"),
|
||||
@@ -3189,6 +3233,7 @@ const longTermRetentionCheckIn = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.long_term_retention_check_in_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: t("templates.long_term_retention_check_in_description"),
|
||||
@@ -3297,6 +3342,7 @@ const professionalDevelopmentGrowth = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.professional_development_growth_survey_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.professional_development_growth_survey_description"),
|
||||
@@ -3346,6 +3392,7 @@ const recognitionAndReward = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.recognition_and_reward_survey_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.recognition_and_reward_survey_description"),
|
||||
@@ -3394,6 +3441,7 @@ const alignmentAndEngagement = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.alignment_and_engagement_survey_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.alignment_and_engagement_survey_description"),
|
||||
@@ -3442,6 +3490,7 @@ const supportiveWorkCulture = (t: TFnType): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.supportive_work_culture_survey_name"),
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.supportive_work_culture_survey_description"),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TResponse, TResponseDataValue } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys/types";
|
||||
import { getLanguageCode, getLocalizedValue } from "./i18n/utils";
|
||||
|
||||
// function to convert response value of type string | number | string[] or Record<string, string> to string | string[]
|
||||
export const convertResponseValue = (
|
||||
answer: string | number | string[] | Record<string, string>,
|
||||
answer: TResponseDataValue,
|
||||
question: TSurveyQuestion
|
||||
): string | string[] => {
|
||||
switch (question.type) {
|
||||
@@ -57,9 +57,7 @@ export const getQuestionResponseMapping = (
|
||||
return questionResponseMapping;
|
||||
};
|
||||
|
||||
export const processResponseData = (
|
||||
responseData: string | number | string[] | Record<string, string>
|
||||
): string => {
|
||||
export const processResponseData = (responseData: TResponseDataValue): string => {
|
||||
switch (typeof responseData) {
|
||||
case "string":
|
||||
return responseData;
|
||||
|
||||
@@ -450,7 +450,7 @@ const evaluateSingleCondition = (
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.some((v) => !leftValue.includes(v))
|
||||
!rightValue.some((v) => leftValue.includes(v))
|
||||
);
|
||||
case "isAccepted":
|
||||
return leftValue === "accepted";
|
||||
|
||||
@@ -13,6 +13,7 @@ import { RatingResponse } from "@/modules/ui/components/rating-response";
|
||||
import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
||||
import { CheckCheckIcon, MousePointerClickIcon, PhoneIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { TResponseDataValue } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyMatrixQuestion,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
interface RenderResponseProps {
|
||||
responseData: string | number | string[] | Record<string, string>;
|
||||
responseData: TResponseDataValue;
|
||||
question: TSurveyQuestion;
|
||||
survey: TSurvey;
|
||||
language: string | null;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const isValidValue = (value: string | number | Record<string, string> | string[]) => {
|
||||
import { TResponseDataValue } from "@formbricks/types/responses";
|
||||
|
||||
export const isValidValue = (value: TResponseDataValue) => {
|
||||
return (
|
||||
(typeof value === "string" && value.trim() !== "") ||
|
||||
(Array.isArray(value) && value.length > 0) ||
|
||||
|
||||
@@ -32,13 +32,7 @@ describe("TemplateFilters", () => {
|
||||
|
||||
test("renders all filter categories and options", () => {
|
||||
const setSelectedFilter = vi.fn();
|
||||
render(
|
||||
<TemplateFilters
|
||||
selectedFilter={[null, null, null]}
|
||||
setSelectedFilter={setSelectedFilter}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
);
|
||||
render(<TemplateFilters selectedFilter={[null, null, null]} setSelectedFilter={setSelectedFilter} />);
|
||||
|
||||
expect(screen.getByText("environments.surveys.templates.all_channels")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.templates.all_industries")).toBeInTheDocument();
|
||||
@@ -54,13 +48,7 @@ describe("TemplateFilters", () => {
|
||||
const setSelectedFilter = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TemplateFilters
|
||||
selectedFilter={[null, null, null]}
|
||||
setSelectedFilter={setSelectedFilter}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
);
|
||||
render(<TemplateFilters selectedFilter={[null, null, null]} setSelectedFilter={setSelectedFilter} />);
|
||||
|
||||
await user.click(screen.getByText("environments.surveys.templates.channel1"));
|
||||
expect(setSelectedFilter).toHaveBeenCalledWith(["channel1", null, null]);
|
||||
@@ -74,11 +62,7 @@ describe("TemplateFilters", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TemplateFilters
|
||||
selectedFilter={["link", "app", "website"]}
|
||||
setSelectedFilter={setSelectedFilter}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
<TemplateFilters selectedFilter={["link", "app", "website"]} setSelectedFilter={setSelectedFilter} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("environments.surveys.templates.all_channels"));
|
||||
@@ -93,7 +77,6 @@ describe("TemplateFilters", () => {
|
||||
selectedFilter={[null, null, null]}
|
||||
setSelectedFilter={setSelectedFilter}
|
||||
templateSearch="search term"
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -102,20 +85,4 @@ describe("TemplateFilters", () => {
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test("does not render filter categories that are prefilled", () => {
|
||||
const setSelectedFilter = vi.fn();
|
||||
|
||||
render(
|
||||
<TemplateFilters
|
||||
selectedFilter={["link", null, null]}
|
||||
setSelectedFilter={setSelectedFilter}
|
||||
prefilledFilters={["link", null, null]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText("environments.surveys.templates.all_channels")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.templates.all_industries")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.templates.all_roles")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,14 +9,12 @@ interface TemplateFiltersProps {
|
||||
selectedFilter: TTemplateFilter[];
|
||||
setSelectedFilter: (filter: TTemplateFilter[]) => void;
|
||||
templateSearch?: string;
|
||||
prefilledFilters: TTemplateFilter[];
|
||||
}
|
||||
|
||||
export const TemplateFilters = ({
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
templateSearch,
|
||||
prefilledFilters,
|
||||
}: TemplateFiltersProps) => {
|
||||
const { t } = useTranslate();
|
||||
const handleFilterSelect = (filterValue: TTemplateFilter, index: number) => {
|
||||
@@ -31,7 +29,6 @@ export const TemplateFilters = ({
|
||||
return (
|
||||
<div className="mb-6 gap-3">
|
||||
{allFilters.map((filters, index) => {
|
||||
if (prefilledFilters[index] !== null) return;
|
||||
return (
|
||||
<div key={filters[0]?.value || index} className="mt-2 flex flex-wrap gap-1 last:border-r-0">
|
||||
<button
|
||||
|
||||
@@ -102,41 +102,20 @@ describe("TemplateList", () => {
|
||||
});
|
||||
|
||||
test("renders correctly with default props", () => {
|
||||
render(
|
||||
<TemplateList
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mockProject}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
);
|
||||
render(<TemplateList userId="user-id" environmentId="env-id" project={mockProject} />);
|
||||
|
||||
expect(screen.getByText("Start from scratch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders filters when showFilters is true", () => {
|
||||
render(
|
||||
<TemplateList
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mockProject}
|
||||
prefilledFilters={[null, null, null]}
|
||||
showFilters={true}
|
||||
/>
|
||||
);
|
||||
render(<TemplateList userId="user-id" environmentId="env-id" project={mockProject} showFilters={true} />);
|
||||
|
||||
expect(screen.queryByTestId("template-filters")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("doesn't render filters when showFilters is false", () => {
|
||||
render(
|
||||
<TemplateList
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mockProject}
|
||||
prefilledFilters={[null, null, null]}
|
||||
showFilters={false}
|
||||
/>
|
||||
<TemplateList userId="user-id" environmentId="env-id" project={mockProject} showFilters={false} />
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("template-filters")).not.toBeInTheDocument();
|
||||
@@ -150,7 +129,6 @@ describe("TemplateList", () => {
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mockProject}
|
||||
prefilledFilters={[null, null, null]}
|
||||
templateSearch="Template 1"
|
||||
/>
|
||||
);
|
||||
@@ -167,7 +145,6 @@ describe("TemplateList", () => {
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mockProject}
|
||||
prefilledFilters={[null, null, null]}
|
||||
onTemplateClick={onTemplateClickMock}
|
||||
noPreview={true}
|
||||
/>
|
||||
@@ -186,14 +163,7 @@ describe("TemplateList", () => {
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TemplateList
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mockProject}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
);
|
||||
render(<TemplateList userId="user-id" environmentId="env-id" project={mockProject} />);
|
||||
|
||||
// First select the template
|
||||
const selectButton = screen.getAllByText("Select")[0];
|
||||
@@ -220,14 +190,7 @@ describe("TemplateList", () => {
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TemplateList
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mockProject}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
);
|
||||
render(<TemplateList userId="user-id" environmentId="env-id" project={mockProject} />);
|
||||
|
||||
// First select the template
|
||||
const selectButton = screen.getAllByText("Select")[0];
|
||||
@@ -250,12 +213,7 @@ describe("TemplateList", () => {
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<TemplateList
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mobileProject as Project}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
<TemplateList userId="user-id" environmentId="env-id" project={mobileProject as Project} />
|
||||
);
|
||||
|
||||
// Test with no channel config
|
||||
@@ -264,14 +222,7 @@ describe("TemplateList", () => {
|
||||
config: {},
|
||||
};
|
||||
|
||||
rerender(
|
||||
<TemplateList
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={noChannelProject as Project}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
);
|
||||
rerender(<TemplateList userId="user-id" environmentId="env-id" project={noChannelProject as Project} />);
|
||||
|
||||
expect(screen.getByText("Template 1")).toBeInTheDocument();
|
||||
});
|
||||
@@ -279,14 +230,7 @@ describe("TemplateList", () => {
|
||||
test("development mode shows templates correctly", () => {
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
|
||||
render(
|
||||
<TemplateList
|
||||
userId="user-id"
|
||||
environmentId="env-id"
|
||||
project={mockProject}
|
||||
prefilledFilters={[null, null, null]}
|
||||
/>
|
||||
);
|
||||
render(<TemplateList userId="user-id" environmentId="env-id" project={mockProject} />);
|
||||
|
||||
expect(screen.getByText("Template 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Template 2")).toBeInTheDocument();
|
||||
|
||||
@@ -21,7 +21,6 @@ interface TemplateListProps {
|
||||
project: Project;
|
||||
templateSearch?: string;
|
||||
showFilters?: boolean;
|
||||
prefilledFilters: TTemplateFilter[];
|
||||
onTemplateClick?: (template: TTemplate) => void;
|
||||
noPreview?: boolean; // single click to create survey
|
||||
}
|
||||
@@ -32,7 +31,6 @@ export const TemplateList = ({
|
||||
environmentId,
|
||||
showFilters = true,
|
||||
templateSearch,
|
||||
prefilledFilters,
|
||||
onTemplateClick = () => {},
|
||||
noPreview,
|
||||
}: TemplateListProps) => {
|
||||
@@ -40,7 +38,7 @@ export const TemplateList = ({
|
||||
const router = useRouter();
|
||||
const [activeTemplate, setActiveTemplate] = useState<TTemplate | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState<TTemplateFilter[]>(prefilledFilters);
|
||||
const [selectedFilter, setSelectedFilter] = useState<TTemplateFilter[]>([null, null, null]);
|
||||
const surveyType: TSurveyType = useMemo(() => {
|
||||
if (project.config.channel) {
|
||||
if (project.config.channel === "website") {
|
||||
@@ -111,7 +109,6 @@ export const TemplateList = ({
|
||||
selectedFilter={selectedFilter}
|
||||
setSelectedFilter={setSelectedFilter}
|
||||
templateSearch={templateSearch}
|
||||
prefilledFilters={prefilledFilters}
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Session } from "next-auth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TTemplateRole } from "@formbricks/types/templates";
|
||||
import { SurveysPage } from "./page";
|
||||
|
||||
// Mock all dependencies
|
||||
@@ -53,19 +52,16 @@ vi.mock("@/modules/survey/list/lib/survey", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/templates/components/template-container", () => ({
|
||||
TemplateContainerWithPreview: vi.fn(
|
||||
({ userId, environment, project, prefilledFilters, isTemplatePage }) => (
|
||||
<div
|
||||
data-testid="template-container"
|
||||
data-user-id={userId}
|
||||
data-environment-id={environment.id}
|
||||
data-project-id={project.id}
|
||||
data-prefilled-filters={JSON.stringify(prefilledFilters)}
|
||||
data-is-template-page={isTemplatePage}>
|
||||
Template Container
|
||||
</div>
|
||||
)
|
||||
),
|
||||
TemplateContainerWithPreview: vi.fn(({ userId, environment, project, isTemplatePage }) => (
|
||||
<div
|
||||
data-testid="template-container"
|
||||
data-user-id={userId}
|
||||
data-environment-id={environment.id}
|
||||
data-project-id={project.id}
|
||||
data-is-template-page={isTemplatePage}>
|
||||
Template Container
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
@@ -207,9 +203,8 @@ describe("SurveysPage", () => {
|
||||
mockTranslate.mockReturnValue("Project not found");
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
await expect(SurveysPage({ params, searchParams })).rejects.toThrow("Project not found");
|
||||
await expect(SurveysPage({ params })).rejects.toThrow("Project not found");
|
||||
|
||||
expect(mockGetProjectWithTeamIdsByEnvironmentId).toHaveBeenCalledWith("env-123");
|
||||
expect(mockTranslate).toHaveBeenCalledWith("common.project_not_found");
|
||||
@@ -225,9 +220,8 @@ describe("SurveysPage", () => {
|
||||
});
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
await SurveysPage({ params, searchParams });
|
||||
await SurveysPage({ params });
|
||||
|
||||
expect(mockRedirect).toHaveBeenCalledWith("/environments/env-123/settings/billing");
|
||||
});
|
||||
@@ -236,9 +230,8 @@ describe("SurveysPage", () => {
|
||||
mockGetSurveyCount.mockResolvedValue(0);
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({ role: "product_manager" as TTemplateRole });
|
||||
|
||||
const result = await SurveysPage({ params, searchParams });
|
||||
const result = await SurveysPage({ params });
|
||||
render(result);
|
||||
|
||||
expect(screen.getByTestId("template-container")).toBeInTheDocument();
|
||||
@@ -246,20 +239,14 @@ describe("SurveysPage", () => {
|
||||
expect(screen.getByTestId("template-container")).toHaveAttribute("data-environment-id", "env-123");
|
||||
expect(screen.getByTestId("template-container")).toHaveAttribute("data-project-id", "project-123");
|
||||
expect(screen.getByTestId("template-container")).toHaveAttribute("data-is-template-page", "false");
|
||||
|
||||
const prefilledFilters = JSON.parse(
|
||||
screen.getByTestId("template-container").getAttribute("data-prefilled-filters") || "[]"
|
||||
);
|
||||
expect(prefilledFilters).toEqual(["website", "other", "product_manager"]);
|
||||
});
|
||||
|
||||
test("renders surveys list when survey count is greater than 0", async () => {
|
||||
mockGetSurveyCount.mockResolvedValue(5);
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
const result = await SurveysPage({ params, searchParams });
|
||||
const result = await SurveysPage({ params });
|
||||
render(result);
|
||||
|
||||
expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument();
|
||||
@@ -289,9 +276,8 @@ describe("SurveysPage", () => {
|
||||
mockGetSurveyCount.mockResolvedValue(5);
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
const result = await SurveysPage({ params, searchParams });
|
||||
const result = await SurveysPage({ params });
|
||||
render(result);
|
||||
|
||||
expect(screen.getByTestId("page-header")).toBeInTheDocument();
|
||||
@@ -307,9 +293,8 @@ describe("SurveysPage", () => {
|
||||
mockGetSurveyCount.mockResolvedValue(0);
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
const result = await SurveysPage({ params, searchParams });
|
||||
const result = await SurveysPage({ params });
|
||||
render(result);
|
||||
|
||||
// When survey count is 0, it should render TemplateContainer regardless of read-only status
|
||||
@@ -330,16 +315,11 @@ describe("SurveysPage", () => {
|
||||
mockGetSurveyCount.mockResolvedValue(0);
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
const result = await SurveysPage({ params, searchParams });
|
||||
const result = await SurveysPage({ params });
|
||||
render(result);
|
||||
|
||||
expect(screen.getByTestId("template-container")).toBeInTheDocument();
|
||||
const prefilledFilters = JSON.parse(
|
||||
screen.getByTestId("template-container").getAttribute("data-prefilled-filters") || "[]"
|
||||
);
|
||||
expect(prefilledFilters).toEqual([null, null, null]);
|
||||
});
|
||||
|
||||
test("handles project with null styling", async () => {
|
||||
@@ -351,9 +331,8 @@ describe("SurveysPage", () => {
|
||||
mockGetSurveyCount.mockResolvedValue(0);
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
const result = await SurveysPage({ params, searchParams });
|
||||
const result = await SurveysPage({ params });
|
||||
render(result);
|
||||
|
||||
expect(screen.getByTestId("template-container")).toBeInTheDocument();
|
||||
@@ -365,9 +344,8 @@ describe("SurveysPage", () => {
|
||||
mockGetSurveyCount.mockResolvedValue(5);
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
const result = await SurveysPage({ params, searchParams });
|
||||
const result = await SurveysPage({ params });
|
||||
render(result);
|
||||
|
||||
expect(screen.getByTestId("surveys-list")).toHaveAttribute("data-locale", "en-US");
|
||||
@@ -377,9 +355,8 @@ describe("SurveysPage", () => {
|
||||
mockGetSurveyCount.mockResolvedValue(5);
|
||||
|
||||
const params = Promise.resolve({ environmentId: "env-123" });
|
||||
const searchParams = Promise.resolve({});
|
||||
|
||||
const result = await SurveysPage({ params, searchParams });
|
||||
const result = await SurveysPage({ params });
|
||||
render(result);
|
||||
|
||||
expect(screen.getByTestId("link")).toHaveAttribute("href", "/environments/env-123/surveys/templates");
|
||||
|
||||
@@ -14,7 +14,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { TTemplateRole } from "@formbricks/types/templates";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Your Surveys",
|
||||
@@ -24,17 +23,10 @@ interface SurveyTemplateProps {
|
||||
params: Promise<{
|
||||
environmentId: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
role?: TTemplateRole;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const SurveysPage = async ({
|
||||
params: paramsProps,
|
||||
searchParams: searchParamsProps,
|
||||
}: SurveyTemplateProps) => {
|
||||
export const SurveysPage = async ({ params: paramsProps }: SurveyTemplateProps) => {
|
||||
const publicDomain = getPublicDomain();
|
||||
const searchParams = await searchParamsProps;
|
||||
const params = await paramsProps;
|
||||
const t = await getTranslate();
|
||||
|
||||
@@ -46,8 +38,6 @@ export const SurveysPage = async ({
|
||||
|
||||
const { session, isBilling, environment, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const prefilledFilters = [project?.config.channel, project.config.industry, searchParams.role ?? null];
|
||||
|
||||
if (isBilling) {
|
||||
return redirect(`/environments/${params.environmentId}/settings/billing`);
|
||||
}
|
||||
@@ -79,7 +69,6 @@ export const SurveysPage = async ({
|
||||
userId={session.user.id}
|
||||
environment={environment}
|
||||
project={projectWithRequiredProps}
|
||||
prefilledFilters={prefilledFilters}
|
||||
isTemplatePage={false}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
|
||||
import { TTemplateRole } from "@formbricks/types/templates";
|
||||
import { TemplateContainerWithPreview } from "./template-container";
|
||||
|
||||
// Mock dependencies
|
||||
@@ -59,8 +58,6 @@ const mockEnvironment = {
|
||||
appSetupCompleted: true,
|
||||
};
|
||||
|
||||
const mockPrefilledFilters: (TProjectConfigChannel | TProjectConfigIndustry | TTemplateRole | null)[] = [];
|
||||
|
||||
describe("TemplateContainerWithPreview", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -72,7 +69,6 @@ describe("TemplateContainerWithPreview", () => {
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
userId="user1"
|
||||
prefilledFilters={mockPrefilledFilters}
|
||||
isTemplatePage={true}
|
||||
/>
|
||||
);
|
||||
@@ -86,7 +82,6 @@ describe("TemplateContainerWithPreview", () => {
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
userId="user1"
|
||||
prefilledFilters={mockPrefilledFilters}
|
||||
isTemplatePage={false}
|
||||
/>
|
||||
);
|
||||
@@ -100,7 +95,6 @@ describe("TemplateContainerWithPreview", () => {
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
userId="user1"
|
||||
prefilledFilters={mockPrefilledFilters}
|
||||
isTemplatePage={true}
|
||||
/>
|
||||
);
|
||||
@@ -114,7 +108,6 @@ describe("TemplateContainerWithPreview", () => {
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
userId="user1"
|
||||
prefilledFilters={mockPrefilledFilters}
|
||||
isTemplatePage={false}
|
||||
/>
|
||||
);
|
||||
@@ -128,7 +121,6 @@ describe("TemplateContainerWithPreview", () => {
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
userId="user1"
|
||||
prefilledFilters={mockPrefilledFilters}
|
||||
isTemplatePage={true}
|
||||
/>
|
||||
);
|
||||
@@ -144,7 +136,6 @@ describe("TemplateContainerWithPreview", () => {
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
userId="user1"
|
||||
prefilledFilters={mockPrefilledFilters}
|
||||
isTemplatePage={true}
|
||||
/>
|
||||
);
|
||||
@@ -158,7 +149,6 @@ describe("TemplateContainerWithPreview", () => {
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
userId="user1"
|
||||
prefilledFilters={mockPrefilledFilters}
|
||||
isTemplatePage={true}
|
||||
/>
|
||||
);
|
||||
@@ -172,7 +162,6 @@ describe("TemplateContainerWithPreview", () => {
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
userId="user1"
|
||||
prefilledFilters={mockPrefilledFilters}
|
||||
isTemplatePage={true}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,19 +5,16 @@ import { TemplateList } from "@/modules/survey/components/template-list";
|
||||
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 { Project } from "@prisma/client";
|
||||
import { Environment } from "@prisma/client";
|
||||
import type { Environment, Project } from "@prisma/client";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { useState } from "react";
|
||||
import type { TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
|
||||
import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
|
||||
import type { TTemplate } from "@formbricks/types/templates";
|
||||
import { getMinimalSurvey } from "../lib/minimal-survey";
|
||||
|
||||
type TemplateContainerWithPreviewProps = {
|
||||
project: Project;
|
||||
environment: Pick<Environment, "id" | "appSetupCompleted">;
|
||||
userId: string;
|
||||
prefilledFilters: (TProjectConfigChannel | TProjectConfigIndustry | TTemplateRole | null)[];
|
||||
isTemplatePage?: boolean;
|
||||
};
|
||||
|
||||
@@ -25,7 +22,6 @@ export const TemplateContainerWithPreview = ({
|
||||
project,
|
||||
environment,
|
||||
userId,
|
||||
prefilledFilters,
|
||||
isTemplatePage = true,
|
||||
}: TemplateContainerWithPreviewProps) => {
|
||||
const { t } = useTranslate();
|
||||
@@ -63,7 +59,6 @@ export const TemplateContainerWithPreview = ({
|
||||
setActiveQuestionId(template.preset.questions[0].id);
|
||||
setActiveTemplate(template);
|
||||
}}
|
||||
prefilledFilters={prefilledFilters}
|
||||
/>
|
||||
</div>
|
||||
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-100 bg-slate-50 md:flex md:flex-col">
|
||||
|
||||
@@ -2,23 +2,15 @@ import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { getProjectWithTeamIdsByEnvironmentId } from "@/modules/survey/lib/project";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { redirect } from "next/navigation";
|
||||
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 t = await getTranslate();
|
||||
const params = await props.params;
|
||||
const environmentId = params.environmentId;
|
||||
@@ -35,14 +27,7 @@ export const SurveyTemplatesPage = async (props: SurveyTemplateProps) => {
|
||||
return redirect(`/environments/${environment.id}/surveys`);
|
||||
}
|
||||
|
||||
const prefilledFilters = [project.config.channel, project.config.industry, searchParams.role ?? null];
|
||||
|
||||
return (
|
||||
<TemplateContainerWithPreview
|
||||
userId={session.user.id}
|
||||
environment={environment}
|
||||
project={project}
|
||||
prefilledFilters={prefilledFilters}
|
||||
/>
|
||||
<TemplateContainerWithPreview userId={session.user.id} environment={environment} project={project} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
|
||||
interface QuestionConditionalProps {
|
||||
question: TSurveyQuestion;
|
||||
value: string | number | string[] | Record<string, string>;
|
||||
value: TResponseDataValue;
|
||||
onChange: (responseData: TResponseData) => void;
|
||||
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
|
||||
onBack: () => void;
|
||||
|
||||
@@ -112,7 +112,7 @@ export function MultipleChoiceSingleQuestion({
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
@@ -208,9 +208,13 @@ export function MultipleChoiceSingleQuestion({
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={() => {
|
||||
setOtherSelected(!otherSelected);
|
||||
onChange({ [question.id]: "" });
|
||||
onClick={() => {
|
||||
if (otherSelected) {
|
||||
onChange({ [question.id]: undefined });
|
||||
} else {
|
||||
setOtherSelected(!otherSelected);
|
||||
onChange({ [question.id]: "" });
|
||||
}
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
|
||||
@@ -407,7 +407,7 @@ const evaluateSingleCondition = (
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.some((v) => !leftValue.includes(v))
|
||||
!rightValue.some((v) => leftValue.includes(v))
|
||||
);
|
||||
case "isAccepted":
|
||||
return leftValue === "accepted";
|
||||
|
||||
@@ -42,14 +42,34 @@ export const safeUrlRefinement = (url: string, ctx: z.RefinementCtx): void => {
|
||||
});
|
||||
}
|
||||
|
||||
// Allow localhost for easy recall testing on self-hosted environments
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://localhost")) {
|
||||
// Allow localhost for easy recall testing on self-hosted environments and mailto links
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://localhost") && !url.startsWith("mailto:")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "URL must start with https://",
|
||||
message: "URL must start with https:// or mailto:",
|
||||
});
|
||||
}
|
||||
|
||||
// Skip further validation for mailto URLs as they have different structure
|
||||
if (url.startsWith("mailto:")) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol !== "mailto:") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid mailto URL format",
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid mailto URL format",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const hostname = urlObj.hostname;
|
||||
|
||||
@@ -4,12 +4,9 @@ import { ZSurveyQuota } from "./quota";
|
||||
import { ZSurvey } from "./surveys/types";
|
||||
import { ZTag } from "./tags";
|
||||
|
||||
export const ZResponseDataValue = z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.array(z.string()),
|
||||
z.record(z.string()),
|
||||
]);
|
||||
export const ZResponseDataValue = z
|
||||
.union([z.string(), z.number(), z.array(z.string()), z.record(z.string())])
|
||||
.optional();
|
||||
|
||||
export const ZResponseFilterCondition = z.enum([
|
||||
"accepted",
|
||||
|
||||
Reference in New Issue
Block a user