mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
Compare commits
3 Commits
update-v1-
...
randomize-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
928c586e75 | ||
|
|
fc066551b5 | ||
|
|
7b1c7f95de |
@@ -39,7 +39,6 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu
|
||||
# See optional configurations below if you want to disable these features.
|
||||
|
||||
MAIL_FROM=noreply@example.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=localhost
|
||||
SMTP_PORT=1025
|
||||
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
|
||||
@@ -208,4 +207,3 @@ UNKEY_ROOT_KEY=
|
||||
# Enable Prometheus metrics
|
||||
# PROMETHEUS_ENABLED=
|
||||
# PROMETHEUS_EXPORTER_PORT=
|
||||
|
||||
|
||||
43
.github/workflows/release-helm-chart.yml
vendored
43
.github/workflows/release-helm-chart.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Publish Helm Chart
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract release version
|
||||
run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Install YQ
|
||||
uses: dcarbone/install-yq-action@v1.3.1
|
||||
|
||||
- name: Update Chart.yaml with new version
|
||||
run: |
|
||||
yq -i ".version = \"${VERSION#v}\"" helm-chart/Chart.yaml
|
||||
yq -i ".appVersion = \"${VERSION}\"" helm-chart/Chart.yaml
|
||||
|
||||
- name: Package Helm chart
|
||||
run: |
|
||||
helm package ./helm-chart
|
||||
|
||||
- name: Push Helm chart to GitHub Container Registry
|
||||
run: |
|
||||
helm push formbricks-${VERSION#v}.tgz oci://ghcr.io/formbricks/helm-charts
|
||||
93
.github/workflows/terrafrom-plan-and-apply.yml
vendored
93
.github/workflows/terrafrom-plan-and-apply.yml
vendored
@@ -1,93 +0,0 @@
|
||||
name: 'Terraform'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'infra/terraform/**'
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
terraform:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
|
||||
aws-region: "eu-central-1"
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Terraform Format
|
||||
id: fmt
|
||||
run: terraform fmt -check -recursive
|
||||
continue-on-error: true
|
||||
working-directory: infra/terraform
|
||||
|
||||
# - name: Post Format
|
||||
# if: always() && github.ref != 'refs/heads/main' && (steps.fmt.outcome == 'success' || steps.fmt.outcome == 'failure')
|
||||
# uses: robburger/terraform-pr-commenter@v1
|
||||
# with:
|
||||
# commenter_type: fmt
|
||||
# commenter_input: ${{ format('{0}{1}', steps.fmt.outputs.stdout, steps.fmt.outputs.stderr) }}
|
||||
# commenter_exitcode: ${{ steps.fmt.outputs.exitcode }}
|
||||
|
||||
- name: Terraform Init
|
||||
id: init
|
||||
run: terraform init
|
||||
working-directory: infra/terraform
|
||||
|
||||
# - name: Post Init
|
||||
# if: always() && github.ref != 'refs/heads/main' && (steps.init.outcome == 'success' || steps.init.outcome == 'failure')
|
||||
# uses: robburger/terraform-pr-commenter@v1
|
||||
# with:
|
||||
# commenter_type: init
|
||||
# commenter_input: ${{ format('{0}{1}', steps.init.outputs.stdout, steps.init.outputs.stderr) }}
|
||||
# commenter_exitcode: ${{ steps.init.outputs.exitcode }}
|
||||
|
||||
- name: Terraform Validate
|
||||
id: validate
|
||||
run: terraform validate
|
||||
working-directory: infra/terraform
|
||||
|
||||
# - name: Post Validate
|
||||
# if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure')
|
||||
# uses: robburger/terraform-pr-commenter@v1
|
||||
# with:
|
||||
# commenter_type: validate
|
||||
# commenter_input: ${{ format('{0}{1}', steps.validate.outputs.stdout, steps.validate.outputs.stderr) }}
|
||||
# commenter_exitcode: ${{ steps.validate.outputs.exitcode }}
|
||||
|
||||
- name: Terraform Plan
|
||||
id: plan
|
||||
run: terraform plan -out .planfile
|
||||
working-directory: infra/terraform
|
||||
|
||||
- name: Post PR comment
|
||||
uses: borchero/terraform-plan-comment@v2
|
||||
if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure')
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
planfile: .planfile
|
||||
working-directory: "infra/terraform"
|
||||
skip-comment: true
|
||||
|
||||
- name: Terraform Apply
|
||||
id: apply
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
run: terraform apply .planfile
|
||||
working-directory: "infra/terraform"
|
||||
|
||||
@@ -231,7 +231,6 @@ export const ProjectSettings = ({
|
||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||
<div className="z-0 h-3/4 w-3/4">
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(projectName || "my Product", t)}
|
||||
styling={{ brandColor: { light: brandColor } }}
|
||||
isBrandingEnabled={false}
|
||||
|
||||
@@ -1,41 +1,25 @@
|
||||
"use server";
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
|
||||
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
|
||||
const ZGetSpreadsheetNameByIdAction = z.object({
|
||||
googleSheetIntegration: ZIntegrationGoogleSheets,
|
||||
environmentId: z.string(),
|
||||
spreadsheetId: z.string(),
|
||||
});
|
||||
export async function getSpreadsheetNameByIdAction(
|
||||
googleSheetIntegration: TIntegrationGoogleSheets,
|
||||
environmentId: string,
|
||||
spreadsheetId: string
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
export const getSpreadsheetNameByIdAction = authenticatedActionClient
|
||||
.schema(ZGetSpreadsheetNameByIdAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const integrationData = structuredClone(parsedInput.googleSheetIntegration);
|
||||
integrationData.config.data.forEach((data) => {
|
||||
data.createdAt = new Date(data.createdAt);
|
||||
});
|
||||
|
||||
return await getSpreadsheetNameById(integrationData, parsedInput.spreadsheetId);
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const integrationData = structuredClone(googleSheetIntegration);
|
||||
integrationData.config.data.forEach((data) => {
|
||||
data.createdAt = new Date(data.createdAt);
|
||||
});
|
||||
return await getSpreadsheetNameById(integrationData, spreadsheetId);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
isValidGoogleSheetsUrl,
|
||||
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
@@ -116,18 +115,11 @@ export const AddIntegrationModal = ({
|
||||
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
|
||||
}
|
||||
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
|
||||
const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({
|
||||
const spreadsheetName = await getSpreadsheetNameByIdAction(
|
||||
googleSheetIntegration,
|
||||
environmentId,
|
||||
spreadsheetId,
|
||||
});
|
||||
|
||||
if (!spreadsheetNameResponse?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(spreadsheetNameResponse);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const spreadsheetName = spreadsheetNameResponse.data;
|
||||
spreadsheetId
|
||||
);
|
||||
|
||||
setIsLinkingSheet(true);
|
||||
integrationData.spreadsheetId = spreadsheetId;
|
||||
|
||||
@@ -6,7 +6,6 @@ import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import {
|
||||
DEBUG,
|
||||
MAIL_FROM,
|
||||
MAIL_FROM_NAME,
|
||||
SMTP_AUTHENTICATED,
|
||||
SMTP_HOST,
|
||||
SMTP_PASSWORD,
|
||||
@@ -70,7 +69,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
|
||||
} as SMTPTransport.Options);
|
||||
|
||||
const emailDefaults = {
|
||||
from: `${MAIL_FROM_NAME ?? "Formbricks"} <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
|
||||
from: `Formbricks <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
|
||||
};
|
||||
await transporter.sendMail({ ...emailDefaults, ...emailData });
|
||||
|
||||
|
||||
@@ -100,6 +100,11 @@ export const MatrixQuestionForm = ({
|
||||
label: t("environments.surveys.edit.randomize_all_except_last"),
|
||||
show: true,
|
||||
},
|
||||
exceptLastTwo: {
|
||||
id: "exceptLastTwo",
|
||||
label: t("environments.surveys.edit.randomize_all_except_last_two"),
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
/// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
@@ -70,6 +70,11 @@ export const MultipleChoiceQuestionForm = ({
|
||||
label: t("environments.surveys.edit.randomize_all_except_last"),
|
||||
show: true,
|
||||
},
|
||||
exceptLastTwo: {
|
||||
id: "exceptLastTwo",
|
||||
label: t("environments.surveys.edit.randomize_all_except_last_two"),
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
|
||||
|
||||
@@ -243,6 +243,7 @@ export const SurveyEditor = ({
|
||||
environment={environment}
|
||||
previewType={localSurvey.type === "app" ? "modal" : "fullwidth"}
|
||||
languageCode={selectedLanguageCode}
|
||||
onFileUpload={async (file) => file.name}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -170,14 +170,14 @@ export const LinkSurvey = ({
|
||||
PRIVACY_URL={PRIVACY_URL}
|
||||
isBrandingEnabled={project.linkSurveyBranding}>
|
||||
<SurveyInline
|
||||
apiHost={webAppUrl}
|
||||
environmentId={survey.environmentId}
|
||||
isPreviewMode={isPreview}
|
||||
apiHost={!isPreview ? webAppUrl : undefined}
|
||||
environmentId={!isPreview ? survey.environmentId : undefined}
|
||||
survey={survey}
|
||||
styling={determineStyling()}
|
||||
languageCode={languageCode}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
shouldResetQuestionId={false}
|
||||
onFileUpload={isPreview ? async (file) => `https://formbricks.com/${file.name}` : undefined}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus -- need it as focus behaviour is different in normal surveys and survey preview
|
||||
autoFocus={autoFocus}
|
||||
prefillResponseData={prefillValue}
|
||||
|
||||
@@ -84,6 +84,7 @@ export const TemplateContainerWithPreview = ({
|
||||
project={project}
|
||||
environment={environment}
|
||||
languageCode={"default"}
|
||||
onFileUpload={async (file) => file.name}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
@@ -9,7 +9,9 @@ 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 { TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurvey, TSurveyQuestionId, TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { Modal } from "./components/modal";
|
||||
import { TabOption } from "./components/tab-option";
|
||||
@@ -23,6 +25,7 @@ interface PreviewSurveyProps {
|
||||
project: Project;
|
||||
environment: Pick<Environment, "id" | "appSetupCompleted">;
|
||||
languageCode: string;
|
||||
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
|
||||
}
|
||||
|
||||
let surveyNameTemp: string;
|
||||
@@ -63,6 +66,7 @@ export const PreviewSurvey = ({
|
||||
project,
|
||||
environment,
|
||||
languageCode,
|
||||
onFileUpload,
|
||||
}: PreviewSurveyProps) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
|
||||
@@ -261,11 +265,11 @@ export const PreviewSurvey = ({
|
||||
borderRadius={styling?.roundness ?? 8}
|
||||
background={styling?.cardBackgroundColor?.light}>
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={survey}
|
||||
isBrandingEnabled={project.inAppSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
languageCode={languageCode}
|
||||
onFileUpload={onFileUpload}
|
||||
styling={styling}
|
||||
isCardBorderVisible={!styling.highlightBorderColor?.light}
|
||||
onClose={handlePreviewModalClose}
|
||||
@@ -284,9 +288,9 @@ export const PreviewSurvey = ({
|
||||
</div>
|
||||
<div className="z-10 w-full max-w-md rounded-lg border border-transparent">
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
responseCount={42}
|
||||
styling={styling}
|
||||
@@ -363,11 +367,11 @@ export const PreviewSurvey = ({
|
||||
borderRadius={styling.roundness ?? 8}
|
||||
background={styling.cardBackgroundColor?.light}>
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={survey}
|
||||
isBrandingEnabled={project.inAppSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
languageCode={languageCode}
|
||||
onFileUpload={onFileUpload}
|
||||
styling={styling}
|
||||
isCardBorderVisible={!styling.highlightBorderColor?.light}
|
||||
onClose={handlePreviewModalClose}
|
||||
@@ -390,10 +394,10 @@ export const PreviewSurvey = ({
|
||||
</div>
|
||||
<div className="z-0 w-full max-w-4xl rounded-lg border-transparent">
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
responseCount={42}
|
||||
styling={styling}
|
||||
|
||||
@@ -25,6 +25,7 @@ interface ShuffleOptionsTypes {
|
||||
none?: ShuffleOptionType;
|
||||
all?: ShuffleOptionType;
|
||||
exceptLast?: ShuffleOptionType;
|
||||
exceptLastTwo?: ShuffleOptionType;
|
||||
}
|
||||
|
||||
interface ShuffleOptionSelectProps {
|
||||
|
||||
@@ -162,7 +162,6 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
borderRadius={project.styling.roundness ?? 8}>
|
||||
<Fragment key={surveyFormKey}>
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "app" }}
|
||||
isBrandingEnabled={project.inAppSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
@@ -188,7 +187,6 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
key={surveyFormKey}
|
||||
className={`${project.logo?.url && !project.styling.isLogoHidden && !isFullScreenPreview ? "mt-12" : ""} z-0 w-full max-w-md rounded-lg p-4`}>
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@formbricks/web",
|
||||
"version": "3.4.0",
|
||||
"version": "3.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules .next",
|
||||
|
||||
@@ -36,7 +36,6 @@ x-environment: &environment
|
||||
|
||||
# Email Configuration
|
||||
# MAIL_FROM:
|
||||
# MAIL_FROM_NAME:
|
||||
# SMTP_HOST:
|
||||
# SMTP_PORT:
|
||||
# SMTP_USER:
|
||||
|
||||
@@ -224,9 +224,6 @@ EOT
|
||||
echo -n "Enter your SMTP configured Email ID: "
|
||||
read mail_from
|
||||
|
||||
echo -n "Enter your SMTP configured Email Name: "
|
||||
read mail_from_name
|
||||
|
||||
echo -n "Enter your SMTP Host URL: "
|
||||
read smtp_host
|
||||
|
||||
@@ -247,7 +244,6 @@ EOT
|
||||
|
||||
else
|
||||
mail_from=""
|
||||
mail_from_name=""
|
||||
smtp_host=""
|
||||
smtp_port=""
|
||||
smtp_user=""
|
||||
@@ -274,7 +270,6 @@ EOT
|
||||
|
||||
if [[ -n $mail_from ]]; then
|
||||
sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml
|
||||
sed -i "s|# MAIL_FROM_NAME:|MAIL_FROM_NAME: \"$mail_from_name\"|" docker-compose.yml
|
||||
sed -i "s|# SMTP_HOST:|SMTP_HOST: \"$smtp_host\"|" docker-compose.yml
|
||||
sed -i "s|# SMTP_PORT:|SMTP_PORT: \"$smtp_port\"|" docker-compose.yml
|
||||
sed -i "s|# SMTP_SECURE_ENABLED:|SMTP_SECURE_ENABLED: $smtp_secure_enabled|" docker-compose.yml
|
||||
|
||||
@@ -1039,7 +1039,6 @@ x-environment: &environment
|
||||
|
||||
# Email Configuration
|
||||
MAIL_FROM:
|
||||
MAIL_FROM_NAME:
|
||||
SMTP_HOST:
|
||||
SMTP_PORT:
|
||||
SMTP_SECURE_ENABLED:
|
||||
|
||||
@@ -33,7 +33,6 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| RATE_LIMITING_DISABLED | Disables rate limiting if set to 1. | optional | |
|
||||
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | |
|
||||
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
|
||||
@@ -33,7 +33,6 @@ To enable email functionality, configure the following environment variables:
|
||||
```bash
|
||||
# Basic SMTP Configuration
|
||||
MAIL_FROM=noreply@yourdomain.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=smtp.yourprovider.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_username
|
||||
@@ -76,7 +75,6 @@ If you're using the one-click setup with Docker Compose, you can either:
|
||||
environment:
|
||||
# Email Configuration
|
||||
MAIL_FROM: noreply@yourdomain.com
|
||||
MAIL_FROM_NAME: Formbricks
|
||||
SMTP_HOST: smtp.yourprovider.com
|
||||
SMTP_PORT: 587
|
||||
SMTP_USER: your_username
|
||||
@@ -97,7 +95,6 @@ environment:
|
||||
|
||||
```bash
|
||||
MAIL_FROM=noreply@yourdomain.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=smtp.sendgrid.net
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=apikey
|
||||
@@ -108,7 +105,6 @@ SMTP_PASSWORD=your_sendgrid_api_key
|
||||
|
||||
```bash
|
||||
MAIL_FROM=noreply@yourdomain.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=email-smtp.us-east-1.amazonaws.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_ses_access_key
|
||||
@@ -119,7 +115,6 @@ SMTP_PASSWORD=your_ses_secret_key
|
||||
|
||||
```bash
|
||||
MAIL_FROM=your_email@gmail.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_email@gmail.com
|
||||
|
||||
@@ -5,7 +5,8 @@ description: A Helm chart for Formbricks with PostgreSQL, Redis
|
||||
type: application
|
||||
|
||||
# Helm chart Version
|
||||
version: 0.0.0-dev
|
||||
version: 3.3.1
|
||||
appVersion: v3.3.1
|
||||
|
||||
keywords:
|
||||
- formbricks
|
||||
@@ -17,6 +18,7 @@ maintainers:
|
||||
- name: Formbricks
|
||||
email: info@formbricks.com
|
||||
|
||||
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: "16.4.16"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# formbricks
|
||||
|
||||
 
|
||||
  
|
||||
|
||||
A Helm chart for Formbricks with PostgreSQL, Redis
|
||||
|
||||
|
||||
@@ -94,12 +94,8 @@ spec:
|
||||
protocol: {{ $config.protocol | default "TCP" | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if or .Values.deployment.envFrom (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) }}
|
||||
{{- if .Values.deployment.envFrom }}
|
||||
envFrom:
|
||||
{{- if or .Values.secret.enabled (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) }}
|
||||
- secretRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
{{- end }}
|
||||
{{- range $value := .Values.deployment.envFrom }}
|
||||
{{- if (eq .type "configmap") }}
|
||||
- configMapRef:
|
||||
@@ -126,8 +122,42 @@ spec:
|
||||
env:
|
||||
{{- if and (.Values.enterprise.enabled) (ne .Values.enterprise.licenseKey "") }}
|
||||
- name: ENTERPRISE_LICENSE_KEY
|
||||
value: {{ .Values.enterprise.licenseKey | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
key: ENTERPRISE_LICENSE_KEY
|
||||
{{- else if and (.Values.enterprise.enabled) (eq .Values.enterprise.licenseKey "") }}
|
||||
- name: ENTERPRISE_LICENSE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
key: ENTERPRISE_LICENSE_KEY
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
key: REDIS_URL
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
key: DATABASE_URL
|
||||
- name: CRON_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
key: CRON_SECRET
|
||||
- name: ENCRYPTION_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
key: ENCRYPTION_KEY
|
||||
- name: NEXTAUTH_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
key: NEXTAUTH_SECRET
|
||||
{{- range $key, $value := .Values.deployment.env }}
|
||||
- name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }}
|
||||
{{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | indent 10 }}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
---
|
||||
{{- if .Capabilities.APIVersions.Has "autoscaling/v2/HorizontalPodAutoscaler" }}
|
||||
apiVersion: autoscaling/v2
|
||||
{{- else }}
|
||||
apiVersion: autoscaling/v2beta2
|
||||
{{- end }}
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ template "formbricks.name" . }}
|
||||
|
||||
@@ -54,9 +54,9 @@ deployment:
|
||||
|
||||
# Environment variables from ConfigMaps or Secrets
|
||||
envFrom:
|
||||
# app-secrets:
|
||||
# type: secret
|
||||
# nameSuffix: app-secrets
|
||||
# app-secrets:
|
||||
# type: secret
|
||||
# nameSuffix: app-secrets
|
||||
|
||||
# Environment variables passed to the app container
|
||||
env:
|
||||
|
||||
@@ -10,11 +10,3 @@ data "aws_eks_cluster_auth" "eks" {
|
||||
data "aws_ecrpublic_authorization_token" "token" {
|
||||
provider = aws.virginia
|
||||
}
|
||||
|
||||
data "aws_iam_roles" "administrator" {
|
||||
name_regex = "AWSReservedSSO_AdministratorAccess"
|
||||
}
|
||||
|
||||
data "aws_iam_roles" "github" {
|
||||
name_regex = "formbricks-prod-github"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ module "iam_github_oidc_role" {
|
||||
"repo:formbricks/*:*",
|
||||
]
|
||||
policies = {
|
||||
Administrator = "arn:aws:iam::aws:policy/AdministratorAccess"
|
||||
Administrator = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
|
||||
}
|
||||
|
||||
tags = local.tags
|
||||
|
||||
@@ -249,7 +249,7 @@ module "eks" {
|
||||
cluster_name = "${local.name}-eks"
|
||||
cluster_version = "1.32"
|
||||
|
||||
enable_cluster_creator_admin_permissions = false
|
||||
enable_cluster_creator_admin_permissions = true
|
||||
cluster_endpoint_public_access = true
|
||||
|
||||
cluster_addons = {
|
||||
@@ -271,41 +271,6 @@ module "eks" {
|
||||
}
|
||||
}
|
||||
|
||||
kms_key_administrators = [
|
||||
tolist(data.aws_iam_roles.github.arns)[0],
|
||||
tolist(data.aws_iam_roles.administrator.arns)[0]
|
||||
]
|
||||
|
||||
kms_key_users = [
|
||||
tolist(data.aws_iam_roles.github.arns)[0],
|
||||
tolist(data.aws_iam_roles.administrator.arns)[0]
|
||||
]
|
||||
|
||||
access_entries = {
|
||||
administrator = {
|
||||
principal_arn = tolist(data.aws_iam_roles.administrator.arns)[0]
|
||||
policy_associations = {
|
||||
Admin = {
|
||||
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
|
||||
access_scope = {
|
||||
type = "cluster"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
github = {
|
||||
principal_arn = tolist(data.aws_iam_roles.github.arns)[0]
|
||||
policy_associations = {
|
||||
Admin = {
|
||||
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
|
||||
access_scope = {
|
||||
type = "cluster"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vpc_id = module.vpc.vpc_id
|
||||
subnet_ids = module.vpc.private_subnets
|
||||
control_plane_subnet_ids = module.vpc.intra_subnets
|
||||
@@ -608,69 +573,95 @@ resource "helm_release" "formbricks" {
|
||||
|
||||
values = [
|
||||
<<-EOT
|
||||
postgresql:
|
||||
enabled: false
|
||||
redis:
|
||||
enabled: false
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: alb
|
||||
hosts:
|
||||
- host: "app.${local.domain}"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: "Prefix"
|
||||
serviceName: "formbricks"
|
||||
annotations:
|
||||
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||
alb.ingress.kubernetes.io/target-type: ip
|
||||
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
|
||||
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
||||
alb.ingress.kubernetes.io/certificate-arn: ${module.acm.acm_certificate_arn}
|
||||
alb.ingress.kubernetes.io/healthcheck-path: "/health"
|
||||
alb.ingress.kubernetes.io/group.name: formbricks
|
||||
alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06"
|
||||
secret:
|
||||
enabled: false
|
||||
rbac:
|
||||
enabled: true
|
||||
serviceAccount:
|
||||
postgresql:
|
||||
enabled: false
|
||||
redis:
|
||||
enabled: false
|
||||
ingress:
|
||||
enabled: true
|
||||
name: formbricks
|
||||
ingressClassName: alb
|
||||
hosts:
|
||||
- host: "app.${local.domain}"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: "Prefix"
|
||||
serviceName: "formbricks"
|
||||
annotations:
|
||||
eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn}
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
deployment:
|
||||
image:
|
||||
repository: "ghcr.io/formbricks/formbricks-experimental"
|
||||
tag: "open-telemetry-for-prometheus"
|
||||
pullPolicy: Always
|
||||
env:
|
||||
S3_BUCKET_NAME:
|
||||
value: ${module.s3-bucket.s3_bucket_id}
|
||||
RATE_LIMITING_DISABLED:
|
||||
value: "1"
|
||||
envFrom:
|
||||
app-env:
|
||||
type: secret
|
||||
nameSuffix: app-env
|
||||
annotations:
|
||||
deployed_at: ${timestamp()}
|
||||
externalSecret:
|
||||
enabled: true # Enable/disable ExternalSecrets
|
||||
secretStore:
|
||||
name: aws-secrets-manager
|
||||
kind: ClusterSecretStore
|
||||
refreshInterval: "1h"
|
||||
files:
|
||||
app-env:
|
||||
dataFrom:
|
||||
key: "prod/formbricks/environment"
|
||||
app-secrets:
|
||||
dataFrom:
|
||||
key: "prod/formbricks/secrets"
|
||||
EOT
|
||||
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||
alb.ingress.kubernetes.io/target-type: ip
|
||||
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
|
||||
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
||||
alb.ingress.kubernetes.io/certificate-arn: ${module.acm.acm_certificate_arn}
|
||||
alb.ingress.kubernetes.io/healthcheck-path: "/health"
|
||||
alb.ingress.kubernetes.io/group.name: formbricks
|
||||
alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06"
|
||||
secret:
|
||||
enabled: false
|
||||
rbac:
|
||||
enabled: true
|
||||
serviceAccount:
|
||||
enabled: true
|
||||
name: formbricks
|
||||
annotations:
|
||||
eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn}
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
deployment:
|
||||
image:
|
||||
repository: "ghcr.io/formbricks/formbricks-experimental"
|
||||
tag: "open-telemetry-for-prometheus"
|
||||
pullPolicy: Always
|
||||
env:
|
||||
S3_BUCKET_NAME:
|
||||
value: ${module.s3-bucket.s3_bucket_id}
|
||||
RATE_LIMITING_DISABLED:
|
||||
value: "1"
|
||||
envFrom:
|
||||
app-parameters:
|
||||
type: secret
|
||||
nameSuffix: {RELEASE.name}-app-parameters
|
||||
annotations:
|
||||
deployed_at: ${timestamp()}
|
||||
externalSecret:
|
||||
enabled: true # Enable/disable ExternalSecrets
|
||||
secretStore:
|
||||
name: aws-secrets-manager
|
||||
kind: ClusterSecretStore
|
||||
refreshInterval: "1h"
|
||||
files:
|
||||
app-parameters:
|
||||
dataFrom:
|
||||
key: "/prod/formbricks/env"
|
||||
secretStore:
|
||||
name: aws-parameter-store
|
||||
kind: ClusterSecretStore
|
||||
app-secrets:
|
||||
data:
|
||||
DATABASE_URL:
|
||||
remoteRef:
|
||||
key: "prod/formbricks/secrets"
|
||||
property: DATABASE_URL
|
||||
REDIS_URL:
|
||||
remoteRef:
|
||||
key: "prod/formbricks/secrets"
|
||||
property: REDIS_URL
|
||||
CRON_SECRET:
|
||||
remoteRef:
|
||||
key: "prod/formbricks/secrets"
|
||||
property: CRON_SECRET
|
||||
ENCRYPTION_KEY:
|
||||
remoteRef:
|
||||
key: "prod/formbricks/secrets"
|
||||
property: ENCRYPTION_KEY
|
||||
NEXTAUTH_SECRET:
|
||||
remoteRef:
|
||||
key: "prod/formbricks/secrets"
|
||||
property: NEXTAUTH_SECRET
|
||||
ENTERPRISE_LICENSE_KEY:
|
||||
remoteRef:
|
||||
key: "prod/formbricks/enterprise"
|
||||
property: ENTERPRISE_LICENSE_KEY
|
||||
EOT
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
# Generate random secrets for formbricks
|
||||
resource "random_password" "nextauth_secret" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "random_password" "encryption_key" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "random_password" "cron_secret" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
# Create the first AWS Secrets Manager secret for environment variables
|
||||
resource "aws_secretsmanager_secret" "formbricks_app_secrets" {
|
||||
name = "prod/formbricks/secrets"
|
||||
@@ -8,7 +24,10 @@ resource "aws_secretsmanager_secret" "formbricks_app_secrets" {
|
||||
resource "aws_secretsmanager_secret_version" "formbricks_app_secrets" {
|
||||
secret_id = aws_secretsmanager_secret.formbricks_app_secrets.id
|
||||
secret_string = jsonencode({
|
||||
DATABASE_URL = "postgres://formbricks:${random_password.postgres.result}@${module.rds-aurora.cluster_endpoint}/formbricks"
|
||||
REDIS_URL = "rediss://:${random_password.valkey.result}@${module.elasticache.replication_group_primary_endpoint_address}:6379"
|
||||
NEXTAUTH_SECRET = random_password.nextauth_secret.result
|
||||
ENCRYPTION_KEY = random_password.encryption_key.result
|
||||
CRON_SECRET = random_password.cron_secret.result
|
||||
DATABASE_URL = "postgres://formbricks:${random_password.postgres.result}@${module.rds-aurora.cluster_endpoint}/formbricks"
|
||||
REDIS_URL = "rediss://:${random_password.valkey.result}@${module.elasticache.replication_group_primary_endpoint_address}:6379"
|
||||
})
|
||||
}
|
||||
|
||||
7
packages/android/.gitignore
vendored
7
packages/android/.gitignore
vendored
@@ -1,7 +1,12 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
@@ -83,7 +83,6 @@ export const SMTP_PASSWORD = env.SMTP_PASSWORD;
|
||||
export const SMTP_AUTHENTICATED = env.SMTP_AUTHENTICATED !== "0";
|
||||
export const SMTP_REJECT_UNAUTHORIZED_TLS = env.SMTP_REJECT_UNAUTHORIZED_TLS !== "0";
|
||||
export const MAIL_FROM = env.MAIL_FROM;
|
||||
export const MAIL_FROM_NAME = env.MAIL_FROM_NAME;
|
||||
|
||||
export const NEXTAUTH_SECRET = env.NEXTAUTH_SECRET;
|
||||
export const ITEMS_PER_PAGE = 30;
|
||||
|
||||
@@ -49,7 +49,6 @@ export const env = createEnv({
|
||||
INTERCOM_SECRET_KEY: z.string().optional(),
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
MAIL_FROM: z.string().email().optional(),
|
||||
MAIL_FROM_NAME: z.string().optional(),
|
||||
NEXTAUTH_SECRET: z.string().min(1),
|
||||
NOTION_OAUTH_CLIENT_ID: z.string().optional(),
|
||||
NOTION_OAUTH_CLIENT_SECRET: z.string().optional(),
|
||||
@@ -174,7 +173,6 @@ export const env = createEnv({
|
||||
INTERCOM_SECRET_KEY: process.env.INTERCOM_SECRET_KEY,
|
||||
IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD,
|
||||
MAIL_FROM: process.env.MAIL_FROM,
|
||||
MAIL_FROM_NAME: process.env.MAIL_FROM_NAME,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
|
||||
@@ -36,7 +36,6 @@ interface VariableStackEntry {
|
||||
export function Survey({
|
||||
apiHost,
|
||||
environmentId,
|
||||
isPreviewMode = false,
|
||||
userId,
|
||||
contactId,
|
||||
mode,
|
||||
@@ -151,6 +150,7 @@ export function Survey({
|
||||
return localSurvey.questions[0]?.id;
|
||||
});
|
||||
const [showError, setShowError] = useState(false);
|
||||
// flag state to store whether response processing has been completed or not, we ignore this check for survey editor preview and link survey preview where getSetIsResponseSendingFinished is undefined
|
||||
const [isResponseSendingFinished, setIsResponseSendingFinished] = useState(
|
||||
!getSetIsResponseSendingFinished
|
||||
);
|
||||
@@ -182,11 +182,6 @@ export function Survey({
|
||||
};
|
||||
|
||||
const onFileUploadApi = async (file: TJsFileUploadParams["file"], params?: TUploadFileConfig) => {
|
||||
if (isPreviewMode) {
|
||||
// return mock url since an url is required for the preview
|
||||
return `https://example.com/${file.name}`;
|
||||
}
|
||||
|
||||
if (!apiClient) {
|
||||
throw new Error("apiClient not initialized");
|
||||
}
|
||||
@@ -211,17 +206,6 @@ export function Survey({
|
||||
}, [questionId]);
|
||||
|
||||
const createDisplay = useCallback(async () => {
|
||||
// Skip display creation in preview mode but still trigger the onDisplayCreated callback
|
||||
if (isPreviewMode) {
|
||||
if (onDisplayCreated) {
|
||||
onDisplayCreated();
|
||||
}
|
||||
if (onDisplay) {
|
||||
onDisplay();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiClient && surveyState && responseQueue) {
|
||||
try {
|
||||
const display = await apiClient.createDisplay({
|
||||
@@ -245,17 +229,7 @@ export function Survey({
|
||||
console.error("error creating display: ", err);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
apiClient,
|
||||
surveyState,
|
||||
responseQueue,
|
||||
survey.id,
|
||||
userId,
|
||||
contactId,
|
||||
onDisplayCreated,
|
||||
isPreviewMode,
|
||||
onDisplay,
|
||||
]);
|
||||
}, [apiClient, surveyState, responseQueue, survey.id, userId, contactId, onDisplayCreated]);
|
||||
|
||||
useEffect(() => {
|
||||
// call onDisplay when component is mounted
|
||||
@@ -411,32 +385,6 @@ export function Survey({
|
||||
|
||||
const onResponseCreateOrUpdate = useCallback(
|
||||
(responseUpdate: TResponseUpdate) => {
|
||||
// Always trigger the onResponse callback even in preview mode
|
||||
if (!apiHost || !environmentId) {
|
||||
onResponse?.({
|
||||
data: responseUpdate.data,
|
||||
ttc: responseUpdate.ttc,
|
||||
finished: responseUpdate.finished,
|
||||
variables: responseUpdate.variables,
|
||||
language: responseUpdate.language,
|
||||
endingId: responseUpdate.endingId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip response creation in preview mode but still trigger the onResponseCreated callback
|
||||
if (isPreviewMode) {
|
||||
if (onResponseCreated) {
|
||||
onResponseCreated();
|
||||
}
|
||||
|
||||
// When in preview mode, set isResponseSendingFinished to true if the response is finished
|
||||
if (responseUpdate.finished) {
|
||||
setIsResponseSendingFinished(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (surveyState && responseQueue) {
|
||||
if (contactId) {
|
||||
surveyState.updateContactId(contactId);
|
||||
@@ -467,20 +415,7 @@ export function Survey({
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
apiHost,
|
||||
environmentId,
|
||||
isPreviewMode,
|
||||
surveyState,
|
||||
responseQueue,
|
||||
contactId,
|
||||
userId,
|
||||
survey,
|
||||
action,
|
||||
hiddenFieldsRecord,
|
||||
onResponseCreated,
|
||||
onResponse,
|
||||
]
|
||||
[surveyState, responseQueue, contactId, userId, survey, action, hiddenFieldsRecord, onResponseCreated]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -511,14 +446,25 @@ export function Survey({
|
||||
onChange(surveyResponseData);
|
||||
onChangeVariables(calculatedVariables);
|
||||
|
||||
onResponseCreateOrUpdate({
|
||||
data: surveyResponseData,
|
||||
ttc: responsettc,
|
||||
finished,
|
||||
variables: calculatedVariables,
|
||||
language: selectedLanguage,
|
||||
endingId,
|
||||
});
|
||||
if (apiHost && environmentId) {
|
||||
onResponseCreateOrUpdate({
|
||||
data: surveyResponseData,
|
||||
ttc: responsettc,
|
||||
finished,
|
||||
variables: calculatedVariables,
|
||||
language: selectedLanguage,
|
||||
endingId,
|
||||
});
|
||||
} else {
|
||||
onResponse?.({
|
||||
data: surveyResponseData,
|
||||
ttc: responsettc,
|
||||
finished,
|
||||
variables: calculatedVariables,
|
||||
language: selectedLanguage,
|
||||
endingId,
|
||||
});
|
||||
}
|
||||
|
||||
if (nextQuestionId) {
|
||||
setQuestionId(nextQuestionId);
|
||||
@@ -627,7 +573,7 @@ export function Survey({
|
||||
onBack={onBack}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
onFileUpload={onFileUpload ?? onFileUploadApi}
|
||||
onFileUpload={apiHost && environmentId ? onFileUploadApi : onFileUpload!}
|
||||
isFirstQuestion={question.id === localSurvey.questions[0]?.id}
|
||||
skipPrefilled={skipPrefilled}
|
||||
prefilledQuestionValue={getQuestionPrefillData(question.id, offset)}
|
||||
|
||||
@@ -34,6 +34,12 @@ export const getShuffledRowIndices = (n: number, shuffleOption: TShuffleOption):
|
||||
shuffle(array);
|
||||
array.push(lastElement);
|
||||
}
|
||||
} else if (shuffleOption === "exceptLastTwo") {
|
||||
if (array.length >= 2) {
|
||||
const lastTwo = array.splice(array.length - 2, 2);
|
||||
shuffle(array);
|
||||
array.push(...lastTwo);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
};
|
||||
@@ -57,6 +63,20 @@ export const getShuffledChoicesIds = (
|
||||
shuffle(shuffledChoices);
|
||||
shuffledChoices.push(lastElement);
|
||||
}
|
||||
} else if (shuffleOption === "exceptLastTwo") {
|
||||
if (otherOption) {
|
||||
if (shuffledChoices.length >= 1) {
|
||||
// Keep the last element fixed (the one before "other")
|
||||
const lastElement = shuffledChoices.pop();
|
||||
shuffle(shuffledChoices);
|
||||
if (lastElement) shuffledChoices.push(lastElement);
|
||||
}
|
||||
} else if (shuffledChoices.length >= 2) {
|
||||
// No "other" option, keep last two elements fixed
|
||||
const lastTwo = shuffledChoices.splice(shuffledChoices.length - 2, 2);
|
||||
shuffle(shuffledChoices);
|
||||
shuffledChoices.push(...lastTwo);
|
||||
}
|
||||
}
|
||||
|
||||
if (otherOption) {
|
||||
|
||||
@@ -45,7 +45,6 @@ export interface SurveyModalProps extends SurveyBaseProps {
|
||||
export interface SurveyContainerProps extends Omit<SurveyBaseProps, "onFileUpload"> {
|
||||
apiHost?: string;
|
||||
environmentId?: string;
|
||||
isPreviewMode?: boolean;
|
||||
userId?: string;
|
||||
contactId?: string;
|
||||
onDisplayCreated?: () => void | Promise<void>;
|
||||
|
||||
@@ -570,7 +570,7 @@ export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({
|
||||
|
||||
export type TSurveyConsentQuestion = z.infer<typeof ZSurveyConsentQuestion>;
|
||||
|
||||
export const ZShuffleOption = z.enum(["none", "all", "exceptLast"]);
|
||||
export const ZShuffleOption = z.enum(["none", "all", "exceptLast", "exceptLastTwo"]);
|
||||
|
||||
export type TShuffleOption = z.infer<typeof ZShuffleOption>;
|
||||
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
"IS_FORMBRICKS_CLOUD",
|
||||
"INTERCOM_SECRET_KEY",
|
||||
"MAIL_FROM",
|
||||
"MAIL_FROM_NAME",
|
||||
"NEXT_PUBLIC_LAYER_API_KEY",
|
||||
"NEXT_PUBLIC_DOCSEARCH_APP_ID",
|
||||
"NEXT_PUBLIC_DOCSEARCH_API_KEY",
|
||||
|
||||
Reference in New Issue
Block a user