diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 8765ab9ec4..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-name: Bug report
-about: "Found a bug? Please fill out the sections below. \U0001F44D"
-title: "[BUG]"
-labels: bug
-assignees: ""
----
-
-### Issue Summary
-
-
-
-(Write your answer here.)
-
-### Steps to Reproduce
-
-1. (for example) Went to ...
-2. Clicked on...
-3. ...
-
-### Expected behavior
-
-A clear and concise description of what you expected to happen.
-
-### Other information
-
-#### Screenshots
-
-If applicable, add screenshots to help explain your problem.
-
-#### Environment
-
-- [ ] Formbricks Cloud (app.formbricks.com)
-- [ ] self-hosted Formbricks, version/commit: [please provide]
-
-#### Desktop (please complete the following information):
-
-- OS: [e.g. iOS]
-- Browser [e.g. chrome, safari]
-- Version [e.g. 22]
-
-#### Node.JS version
-
-[e.g. v18.15.0]
-
-#### Anything else?
-
-- Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
-- Anything else that you think could be an issue?
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000..f158f61f95
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,81 @@
+name: Bug report
+description: "Found a bug? Please fill out the sections below. \U0001F44D"
+title: "[BUG]"
+labels: bug
+assignees: []
+body:
+- type: textarea
+ id: issue-summary
+ attributes:
+ label: Issue Summary
+ description: A summary of the issue. This needs to be a clear detailed-rich summary.
+ validations:
+ required: true
+- type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label: Steps to Reproduce
+ value: |
+ 1. (for example) Went to ...
+ 2. Clicked on...
+ 3. ...
+ validations:
+ required: true
+- type: textarea
+ id: expected-behavior
+ attributes:
+ label: Expected behavior
+ description: A clear and concise description of what you expected to happen.
+ validations:
+ required: true
+- type: textarea
+ id: other-information
+ attributes:
+ label: Other information
+ validations:
+ required: false
+- type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots
+ description: If applicable, add screenshots to help explain your problem.
+ validations:
+ required: false
+- type: checkboxes
+ id: environment
+ attributes:
+ label: Environment
+ options:
+ - label: Formbricks Cloud (app.formbricks.com)
+ - label: Self-hosted Formbricks
+- type: textarea
+ id: desktop-version
+ attributes:
+ label: Desktop (please complete the following information)
+ description: |
+ examples:
+ - **OS**: [e.g. iOS]
+ - **Browser**: [e.g. chrome, safari]
+ - **Version**: [e.g. 22]
+ value: |
+ - OS:
+ - Node:
+ - npm:
+ render: markdown
+ validations:
+ required: true
+- type: markdown
+ id: nodejs-version
+ attributes:
+ value: |
+ #### Node.JS version
+
+ [e.g. v18.15.0]
+- type: markdown
+ id: anything-else
+ attributes:
+ value: |
+ #### Anything else?
+
+ - Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
+ - Anything else that you think could be an issue?
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 402b8d37de..0000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-name: Feature request
-about: "Suggest an idea for this project \U0001F680"
-title: "[FEATURE]"
-labels: enhancement
-assignees: ""
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
-
-### How we code at Formbricks 🤓
-
-- Everything is type-safe
-- All UI components are in the package `formbricks/ui`
-- Run `pnpm dev` to find a demo app to test in-app surveys at `localhost:3002`
-- We use **chatGPT** to help refactor code. Use our [Formbricks ✨ megaprompt ✨](https://github.com/formbricks/formbricks/blob/main/megaprompt.md) to create the right context before you write your prompt.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000000..338cbcbe7a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,45 @@
+name: Feature request
+description: "Suggest an idea for this project \U0001F680"
+title: "[FEATURE]"
+labels: enhancement
+assignees: []
+body:
+- type: textarea
+ id: problem-description
+ attributes:
+ label: Is your feature request related to a problem? Please describe.
+ description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+ validations:
+ required: true
+- type: textarea
+ id: solution-description
+ attributes:
+ label: Describe the solution you'd like
+ description: A clear and concise description of what you want to happen.
+ validations:
+ required: true
+- type: textarea
+ id: alternate-solution-description
+ attributes:
+ label: Describe alternatives you've considered
+ description: A clear and concise description of any alternative solutions or features you've considered.
+ validations:
+ required: false
+- type: textarea
+ id: additional-context
+ attributes:
+ label: Additional context
+ description: Add any other context or screenshots about the feature request here.
+ validations:
+ required: false
+- type: markdown
+ id: formbricks-info
+ attributes:
+ value: |
+ ### How we code at Formbricks 🤓
+
+ - Everything is type-safe
+ - All UI components are in the package `formbricks/ui`
+ - Run `pnpm dev` to find a demo app to test in-app surveys at `localhost:3002`
+ - We use **chatGPT** to help refactor code. Use our [Formbricks ✨ megaprompt ✨](https://github.com/formbricks/formbricks/blob/main/megaprompt.md) to create the right
+ context before you write your prompt.
diff --git a/apps/formbricks-com/pages/api/oss-friends/index.ts b/apps/formbricks-com/pages/api/oss-friends/index.ts
new file mode 100644
index 0000000000..581b701295
--- /dev/null
+++ b/apps/formbricks-com/pages/api/oss-friends/index.ts
@@ -0,0 +1,14 @@
+import { OSSFriends } from "@/pages/oss-friends";
+import type { NextApiRequest, NextApiResponse } from "next";
+
+export default async function handle(req: NextApiRequest, res: NextApiResponse) {
+ // GET
+ if (req.method === "GET") {
+ return res.status(200).json({ data: OSSFriends });
+ }
+
+ // Unknown HTTP Method
+ else {
+ throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
+ }
+}
diff --git a/apps/formbricks-com/pages/oss-friends.tsx b/apps/formbricks-com/pages/oss-friends.tsx
index bed328ba37..fb1521b075 100644
--- a/apps/formbricks-com/pages/oss-friends.tsx
+++ b/apps/formbricks-com/pages/oss-friends.tsx
@@ -2,7 +2,7 @@ import Layout from "@/components/shared/Layout";
import HeroTitle from "@/components/shared/HeroTitle";
import { Button } from "@formbricks/ui";
-const OSSFriends = [
+export const OSSFriends = [
{
name: "BoxyHQ",
description:
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeDetailModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeDetailModal.tsx
index 9501bc2a03..ea572ddcea 100644
--- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeDetailModal.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeDetailModal.tsx
@@ -24,12 +24,7 @@ export default function AttributeDetailModal({
},
{
title: "Settings",
- children: (
-
- ),
+ children: ,
},
];
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx
index d683ece107..5b008a787c 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx
@@ -1,198 +1,198 @@
-"use client";
-
-import {
- copyToOtherEnvironmentAction,
- deleteSurveyAction,
- duplicateSurveyAction,
-} from "@/app/(app)/environments/[environmentId]/actions";
-import DeleteDialog from "@/components/shared/DeleteDialog";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/shared/DropdownMenu";
-import LoadingSpinner from "@/components/shared/LoadingSpinner";
-import type { TEnvironment } from "@formbricks/types/v1/environment";
-import type { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
-import {
- ArrowUpOnSquareStackIcon,
- DocumentDuplicateIcon,
- EllipsisHorizontalIcon,
- EyeIcon,
- LinkIcon,
- PencilSquareIcon,
- TrashIcon,
-} from "@heroicons/react/24/solid";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { useState } from "react";
-import toast from "react-hot-toast";
-
-interface SurveyDropDownMenuProps {
- environmentId: string;
- survey: TSurveyWithAnalytics;
- environment: TEnvironment;
- otherEnvironment: TEnvironment;
-}
-
-export default function SurveyDropDownMenu({
- environmentId,
- survey,
- environment,
- otherEnvironment,
-}: SurveyDropDownMenuProps) {
- const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
- const [loading, setLoading] = useState(false);
- const router = useRouter();
-
- const handleDeleteSurvey = async (survey) => {
- setLoading(true);
- try {
- await deleteSurveyAction(survey.id);
- router.refresh();
- setDeleteDialogOpen(false);
- toast.success("Survey deleted successfully.");
- } catch (error) {
- toast.error("An error occured while deleting survey");
- }
- setLoading(false);
- };
-
- const duplicateSurveyAndRefresh = async (surveyId) => {
- setLoading(true);
- try {
- await duplicateSurveyAction(environmentId, surveyId);
- router.refresh();
- toast.success("Survey duplicated successfully.");
- } catch (error) {
- toast.error("Failed to duplicate the survey.");
- }
- setLoading(false);
- };
-
- const copyToOtherEnvironment = async (surveyId) => {
- setLoading(true);
- try {
- await copyToOtherEnvironmentAction(environmentId, surveyId, otherEnvironment.id);
- if (otherEnvironment.type === "production") {
- toast.success("Survey copied to production env.");
- } else if (otherEnvironment.type === "development") {
- toast.success("Survey copied to development env.");
- }
- router.replace(`/environments/${otherEnvironment.id}`);
- } catch (error) {
- toast.error(`Failed to copy to ${otherEnvironment.type}`);
- }
- setLoading(false);
- };
- if (loading) {
- return (
-
-
-
- );
- }
- return (
- <>
-
-
-
- Open options
-
-
-
-
-
-
-
-
- Edit
-
-
-
-
-
- {environment.type === "development" ? (
-
-
-
- ) : environment.type === "production" ? (
-
-
-
- ) : null}
- {survey.type === "link" && survey.status !== "draft" && (
- <>
-
-
-
- Preview Survey
-
-
-
-
-
- >
- )}
-
-
-
-
-
-
-
- handleDeleteSurvey(survey)}
- text="Are you sure you want to delete this survey and all of its responses? This action cannot be undone."
- />
- >
- );
-}
+"use client";
+
+import {
+ copyToOtherEnvironmentAction,
+ deleteSurveyAction,
+ duplicateSurveyAction,
+} from "@/app/(app)/environments/[environmentId]/actions";
+import DeleteDialog from "@/components/shared/DeleteDialog";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/shared/DropdownMenu";
+import LoadingSpinner from "@/components/shared/LoadingSpinner";
+import type { TEnvironment } from "@formbricks/types/v1/environment";
+import type { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
+import {
+ ArrowUpOnSquareStackIcon,
+ DocumentDuplicateIcon,
+ EllipsisHorizontalIcon,
+ EyeIcon,
+ LinkIcon,
+ PencilSquareIcon,
+ TrashIcon,
+} from "@heroicons/react/24/solid";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import toast from "react-hot-toast";
+
+interface SurveyDropDownMenuProps {
+ environmentId: string;
+ survey: TSurveyWithAnalytics;
+ environment: TEnvironment;
+ otherEnvironment: TEnvironment;
+}
+
+export default function SurveyDropDownMenu({
+ environmentId,
+ survey,
+ environment,
+ otherEnvironment,
+}: SurveyDropDownMenuProps) {
+ const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const router = useRouter();
+
+ const handleDeleteSurvey = async (survey) => {
+ setLoading(true);
+ try {
+ await deleteSurveyAction(survey.id);
+ router.refresh();
+ setDeleteDialogOpen(false);
+ toast.success("Survey deleted successfully.");
+ } catch (error) {
+ toast.error("An error occured while deleting survey");
+ }
+ setLoading(false);
+ };
+
+ const duplicateSurveyAndRefresh = async (surveyId) => {
+ setLoading(true);
+ try {
+ await duplicateSurveyAction(environmentId, surveyId);
+ router.refresh();
+ toast.success("Survey duplicated successfully.");
+ } catch (error) {
+ toast.error("Failed to duplicate the survey.");
+ }
+ setLoading(false);
+ };
+
+ const copyToOtherEnvironment = async (surveyId) => {
+ setLoading(true);
+ try {
+ await copyToOtherEnvironmentAction(environmentId, surveyId, otherEnvironment.id);
+ if (otherEnvironment.type === "production") {
+ toast.success("Survey copied to production env.");
+ } else if (otherEnvironment.type === "development") {
+ toast.success("Survey copied to development env.");
+ }
+ router.replace(`/environments/${otherEnvironment.id}`);
+ } catch (error) {
+ toast.error(`Failed to copy to ${otherEnvironment.type}`);
+ }
+ setLoading(false);
+ };
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+ return (
+ <>
+
+
+
+ Open options
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+
+
+ {environment.type === "development" ? (
+
+
+
+ ) : environment.type === "production" ? (
+
+
+
+ ) : null}
+ {survey.type === "link" && survey.status !== "draft" && (
+ <>
+
+
+
+ Preview Survey
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ handleDeleteSurvey(survey)}
+ text="Are you sure you want to delete this survey and all of its responses? This action cannot be undone."
+ />
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyStarter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyStarter.tsx
index afaf8a7130..21f23f8135 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyStarter.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyStarter.tsx
@@ -1,60 +1,60 @@
-"use client";
-import { Template } from "@/../../packages/types/templates";
-import { createSurveyAction } from "./actions";
-import TemplateList from "@/app/(app)/environments/[environmentId]/surveys/templates/TemplateList";
-import LoadingSpinner from "@/components/shared/LoadingSpinner";
-import type { TEnvironment } from "@formbricks/types/v1/environment";
-import type { TProduct } from "@formbricks/types/v1/product";
-import { useRouter } from "next/navigation";
-import { useState } from "react";
-import { toast } from "react-hot-toast";
-
-export default function SurveyStarter({
- environmentId,
- environment,
- product,
-}: {
- environmentId: string;
- environment: TEnvironment;
- product: TProduct;
-}) {
- const [isCreateSurveyLoading, setIsCreateSurveyLoading] = useState(false);
- const router = useRouter();
- const newSurveyFromTemplate = async (template: Template) => {
- setIsCreateSurveyLoading(true);
- const augmentedTemplate = {
- ...template.preset,
- type: environment?.widgetSetupCompleted ? "web" : "link",
- };
- try {
- const survey = await createSurveyAction(environmentId, augmentedTemplate);
- router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
- } catch (e) {
- toast.error("An error occured creating a new survey");
- setIsCreateSurveyLoading(false);
- }
- };
- return (
-
- {isCreateSurveyLoading ? (
-
- ) : (
- <>
-
-
- You're all set! Time to create your first survey.
-
-
-
{
- newSurveyFromTemplate(template);
- }}
- environment={environment}
- product={product}
- />
- >
- )}
-
- );
-}
+"use client";
+import { Template } from "@/../../packages/types/templates";
+import { createSurveyAction } from "./actions";
+import TemplateList from "@/app/(app)/environments/[environmentId]/surveys/templates/TemplateList";
+import LoadingSpinner from "@/components/shared/LoadingSpinner";
+import type { TEnvironment } from "@formbricks/types/v1/environment";
+import type { TProduct } from "@formbricks/types/v1/product";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { toast } from "react-hot-toast";
+
+export default function SurveyStarter({
+ environmentId,
+ environment,
+ product,
+}: {
+ environmentId: string;
+ environment: TEnvironment;
+ product: TProduct;
+}) {
+ const [isCreateSurveyLoading, setIsCreateSurveyLoading] = useState(false);
+ const router = useRouter();
+ const newSurveyFromTemplate = async (template: Template) => {
+ setIsCreateSurveyLoading(true);
+ const augmentedTemplate = {
+ ...template.preset,
+ type: environment?.widgetSetupCompleted ? "web" : "link",
+ };
+ try {
+ const survey = await createSurveyAction(environmentId, augmentedTemplate);
+ router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
+ } catch (e) {
+ toast.error("An error occured creating a new survey");
+ setIsCreateSurveyLoading(false);
+ }
+ };
+ return (
+
+ {isCreateSurveyLoading ? (
+
+ ) : (
+ <>
+
+
+ You're all set! Time to create your first survey.
+
+
+
{
+ newSurveyFromTemplate(template);
+ }}
+ environment={environment}
+ product={product}
+ />
+ >
+ )}
+
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/TagsCombobox.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/TagsCombobox.tsx
index a6baedd5ae..4ef0405778 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/TagsCombobox.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/TagsCombobox.tsx
@@ -87,8 +87,8 @@ const TagsCombobox: React.FC = ({
onKeyDown={(e) => {
if (e.key === "Enter" && searchValue !== "") {
if (
- !tagsToSearch?.find(
- (tag) => tag?.label?.toLowerCase().includes(searchValue?.toLowerCase())
+ !tagsToSearch?.find((tag) =>
+ tag?.label?.toLowerCase().includes(searchValue?.toLowerCase())
)
) {
createTag?.(searchValue);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/ResponseFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/ResponseFilter.tsx
index 609de1e617..aaca497890 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/ResponseFilter.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/ResponseFilter.tsx
@@ -126,7 +126,7 @@ const ResponseFilter = () => {
return (
-
+
Filter {selectedFilter.filter.length > 0 && `(${selectedFilter.filter.length})`}
{isOpen ? (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/CTAQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/CTAQuestionForm.tsx
index 7c58f0561c..46527bd8d7 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/CTAQuestionForm.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/CTAQuestionForm.tsx
@@ -12,6 +12,7 @@ interface CTAQuestionFormProps {
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
lastQuestion: boolean;
+ isInValid: boolean;
}
export default function CTAQuestionForm({
@@ -19,6 +20,7 @@ export default function CTAQuestionForm({
questionIdx,
updateQuestion,
lastQuestion,
+ isInValid,
}: CTAQuestionFormProps): JSX.Element {
const [firstRender, setFirstRender] = useState(true);
@@ -33,6 +35,7 @@ export default function CTAQuestionForm({
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
+ isInvalid={isInValid && question.headline.trim() === ""}
/>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ConsentQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ConsentQuestionForm.tsx
index 42b13d4b26..a5dd247aea 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ConsentQuestionForm.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ConsentQuestionForm.tsx
@@ -11,12 +11,14 @@ interface ConsentQuestionFormProps {
question: ConsentQuestion;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
+ isInValid: boolean;
}
export default function ConsentQuestionForm({
question,
questionIdx,
updateQuestion,
+ isInValid,
}: ConsentQuestionFormProps): JSX.Element {
const [firstRender, setFirstRender] = useState(true);
return (
@@ -29,6 +31,7 @@ export default function ConsentQuestionForm({
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
+ isInvalid={isInValid && question.headline.trim() === ""}
/>
@@ -62,6 +65,7 @@ export default function ConsentQuestionForm({
value={question.label}
placeholder="I agree to the terms and conditions"
onChange={(e) => updateQuestion(questionIdx, { label: e.target.value })}
+ isInvalid={isInValid && question.label.trim() === ""}
/>
{/*
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx
index d9b583ae5e..5c0e99f7bf 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx
@@ -21,12 +21,14 @@ interface OpenQuestionFormProps {
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
lastQuestion: boolean;
+ isInValid: boolean;
}
export default function MultipleChoiceMultiForm({
question,
questionIdx,
updateQuestion,
+ isInValid,
}: OpenQuestionFormProps): JSX.Element {
const lastChoiceRef = useRef(null);
const [isNew, setIsNew] = useState(true);
@@ -137,6 +139,7 @@ export default function MultipleChoiceMultiForm({
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
+ isInvalid={isInValid && question.headline.trim() === ""}
/>
@@ -184,6 +187,7 @@ export default function MultipleChoiceMultiForm({
className={cn(choice.id === "other" && "border-dashed")}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
+ isInvalid={isInValid && choice.label.trim() === ""}
/>
{question.choices && question.choices.length > 2 && (
void;
lastQuestion: boolean;
+ isInValid: boolean;
}
export default function MultipleChoiceSingleForm({
question,
questionIdx,
updateQuestion,
+ isInValid,
}: OpenQuestionFormProps): JSX.Element {
const lastChoiceRef = useRef(null);
const [isNew, setIsNew] = useState(true);
@@ -137,6 +139,7 @@ export default function MultipleChoiceSingleForm({
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
+ isInvalid={isInValid && question.headline.trim() === ""}
/>
@@ -184,6 +187,7 @@ export default function MultipleChoiceSingleForm({
className={cn(choice.id === "other" && "border-dashed")}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
+ isInvalid={isInValid && choice.label.trim() === ""}
/>
{question.choices && question.choices.length > 2 && (
void;
lastQuestion: boolean;
+ isInValid: boolean;
}
export default function NPSQuestionForm({
@@ -17,6 +18,7 @@ export default function NPSQuestionForm({
questionIdx,
updateQuestion,
lastQuestion,
+ isInValid,
}: NPSQuestionFormProps): JSX.Element {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
@@ -31,6 +33,7 @@ export default function NPSQuestionForm({
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
+ isInvalid={isInValid && question.headline.trim() === ""}
/>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/OpenQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/OpenQuestionForm.tsx
index 55c9ec0a3a..9206c23fad 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/OpenQuestionForm.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/OpenQuestionForm.tsx
@@ -10,12 +10,14 @@ interface OpenQuestionFormProps {
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
lastQuestion: boolean;
+ isInValid: boolean;
}
export default function OpenQuestionForm({
question,
questionIdx,
updateQuestion,
+ isInValid,
}: OpenQuestionFormProps): JSX.Element {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
@@ -30,6 +32,7 @@ export default function OpenQuestionForm({
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
+ isInvalid={isInValid && question.headline.trim() === ""}
/>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard.tsx
index d422231eef..e8c91eb372 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard.tsx
@@ -39,6 +39,7 @@ interface QuestionCardProps {
activeQuestionId: string | null;
setActiveQuestionId: (questionId: string | null) => void;
lastQuestion: boolean;
+ isInValid: boolean;
}
export default function QuestionCard({
@@ -51,6 +52,7 @@ export default function QuestionCard({
activeQuestionId,
setActiveQuestionId,
lastQuestion,
+ isInValid,
}: QuestionCardProps) {
const question = localSurvey.questions[questionIdx];
const open = activeQuestionId === question.id;
@@ -69,7 +71,8 @@ export default function QuestionCard({
{questionIdx + 1}
@@ -136,6 +139,7 @@ export default function QuestionCard({
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
+ isInValid={isInValid}
/>
) : question.type === QuestionType.MultipleChoiceSingle ? (
) : question.type === QuestionType.MultipleChoiceMulti ? (
) : question.type === QuestionType.NPS ? (
) : question.type === QuestionType.CTA ? (
) : question.type === QuestionType.Rating ? (
) : question.type === "consent" ? (
) : null}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionsView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionsView.tsx
index f4927fc83c..30f38e9cd3 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionsView.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionsView.tsx
@@ -9,6 +9,8 @@ import AddQuestionButton from "./AddQuestionButton";
import EditThankYouCard from "./EditThankYouCard";
import QuestionCard from "./QuestionCard";
import { StrictModeDroppable } from "./StrictModeDroppable";
+import { Question } from "@formbricks/types/questions";
+import { validateQuestion } from "./Validation";
interface QuestionsViewProps {
localSurvey: Survey;
@@ -16,6 +18,8 @@ interface QuestionsViewProps {
activeQuestionId: string | null;
setActiveQuestionId: (questionId: string | null) => void;
environmentId: string;
+ invalidQuestions: String[] | null;
+ setInvalidQuestions: (invalidQuestions: String[] | null) => void;
}
export default function QuestionsView({
@@ -24,6 +28,8 @@ export default function QuestionsView({
localSurvey,
setLocalSurvey,
environmentId,
+ invalidQuestions,
+ setInvalidQuestions,
}: QuestionsViewProps) {
const internalQuestionIdMap = useMemo(() => {
return localSurvey.questions.reduce((acc, question) => {
@@ -44,12 +50,33 @@ export default function QuestionsView({
return survey;
};
+ // function to validate individual questions
+ const validateSurvey = (question: Question) => {
+ // prevent this function to execute further if user hasnt still tried to save the survey
+ if (invalidQuestions === null) {
+ return;
+ }
+ let temp = JSON.parse(JSON.stringify(invalidQuestions));
+ if (validateQuestion(question)) {
+ temp = invalidQuestions.filter((id) => id !== question.id);
+ setInvalidQuestions(temp);
+ } else if (!invalidQuestions.includes(question.id)) {
+ temp.push(question.id);
+ setInvalidQuestions(temp);
+ }
+ };
+
const updateQuestion = (questionIdx: number, updatedAttributes: any) => {
let updatedSurvey = JSON.parse(JSON.stringify(localSurvey));
if ("id" in updatedAttributes) {
// if the survey whose id is to be changed is linked to logic of any other survey then changing it
const initialQuestionId = updatedSurvey.questions[questionIdx].id;
updatedSurvey = handleQuestionLogicChange(updatedSurvey, initialQuestionId, updatedAttributes.id);
+ if (invalidQuestions?.includes(initialQuestionId)) {
+ setInvalidQuestions(
+ invalidQuestions.map((id) => (id === initialQuestionId ? updatedAttributes.id : id))
+ );
+ }
// relink the question to internal Id
internalQuestionIdMap[updatedAttributes.id] =
@@ -63,6 +90,7 @@ export default function QuestionsView({
...updatedAttributes,
};
setLocalSurvey(updatedSurvey);
+ validateSurvey(updatedSurvey.questions[questionIdx]);
};
const deleteQuestion = (questionIdx: number) => {
@@ -120,7 +148,6 @@ export default function QuestionsView({
if (!result.destination) {
return;
}
-
const newQuestions = Array.from(localSurvey.questions);
const [reorderedQuestion] = newQuestions.splice(result.source.index, 1);
newQuestions.splice(result.destination.index, 0, reorderedQuestion);
@@ -134,7 +161,6 @@ export default function QuestionsView({
const [reorderedQuestion] = newQuestions.splice(questionIndex, 1);
const destinationIndex = up ? questionIndex - 1 : questionIndex + 1;
newQuestions.splice(destinationIndex, 0, reorderedQuestion);
-
const updatedSurvey = { ...localSurvey, questions: newQuestions };
setLocalSurvey(updatedSurvey);
};
@@ -159,6 +185,7 @@ export default function QuestionsView({
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
lastQuestion={questionIdx === localSurvey.questions.length - 1}
+ isInValid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
/>
))}
{provided.placeholder}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RatingQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RatingQuestionForm.tsx
index 95e05466b5..94a708de14 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RatingQuestionForm.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RatingQuestionForm.tsx
@@ -12,6 +12,7 @@ interface RatingQuestionFormProps {
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
lastQuestion: boolean;
+ isInValid: boolean;
}
export default function RatingQuestionForm({
@@ -19,6 +20,7 @@ export default function RatingQuestionForm({
questionIdx,
updateQuestion,
lastQuestion,
+ isInValid,
}: RatingQuestionFormProps) {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
@@ -33,6 +35,7 @@ export default function RatingQuestionForm({
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
+ isInvalid={isInValid && question.headline.trim() === ""}
/>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx
index d64bb83bf4..cb23359749 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx
@@ -22,7 +22,7 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
const [activeQuestionId, setActiveQuestionId] = useState(null);
const [localSurvey, setLocalSurvey] = useState();
-
+ const [invalidQuestions, setInvalidQuestions] = useState(null);
const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId);
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
const { environment, isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId);
@@ -56,6 +56,7 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr
environmentId={environmentId}
activeId={activeView}
setActiveId={setActiveView}
+ setInvalidQuestions={setInvalidQuestions}
/>
@@ -67,6 +68,8 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
environmentId={environmentId}
+ invalidQuestions={invalidQuestions}
+ setInvalidQuestions={setInvalidQuestions}
/>
) : (
void;
+ setInvalidQuestions: (invalidQuestions: String[]) => void;
}
export default function SurveyMenuBar({
@@ -30,6 +32,7 @@ export default function SurveyMenuBar({
setLocalSurvey,
activeId,
setActiveId,
+ setInvalidQuestions,
}: SurveyMenuBarProps) {
const router = useRouter();
const { triggerSurveyMutate, isMutatingSurvey } = useSurveyMutation(environmentId, localSurvey.id);
@@ -37,6 +40,7 @@ export default function SurveyMenuBar({
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
const { product } = useProduct(environmentId);
+ let faultyQuestions: String[] = [];
useEffect(() => {
if (audiencePrompt && activeId === "settings") {
@@ -85,6 +89,26 @@ export default function SurveyMenuBar({
}
};
+ const validateSurvey = (survey) => {
+ faultyQuestions = [];
+ for (let index = 0; index < survey.questions.length; index++) {
+ const question = survey.questions[index];
+ const isValid = validateQuestion(question);
+
+ if (!isValid) {
+ faultyQuestions.push(question.id);
+ }
+ }
+ // if there are any faulty questions, the user won't be allowed to save the survey
+ if (faultyQuestions.length > 0) {
+ setInvalidQuestions(faultyQuestions);
+ toast.error("Please fill required fields");
+ return false;
+ }
+
+ return true;
+ };
+
const saveSurveyAction = (shouldNavigateBack = false) => {
// variable named strippedSurvey that is a copy of localSurvey with isDraft removed from every question
const strippedSurvey = {
@@ -94,6 +118,11 @@ export default function SurveyMenuBar({
return rest;
}),
};
+
+ if (!validateSurvey(localSurvey)) {
+ return;
+ }
+
triggerSurveyMutate({ ...strippedSurvey })
.then(async (response) => {
if (!response?.ok) {
@@ -180,6 +209,9 @@ export default function SurveyMenuBar({
variant="darkCTA"
loading={isMutatingSurvey}
onClick={async () => {
+ if (!validateSurvey(localSurvey)) {
+ return;
+ }
await triggerSurveyMutate({ ...localSurvey, status: "inProgress" });
router.push(`/environments/${environmentId}/surveys/${localSurvey.id}/summary?success=true`);
}}>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts
new file mode 100644
index 0000000000..b11dc523e0
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts
@@ -0,0 +1,32 @@
+// extend this object in order to add more validation rules
+
+import {
+ MultipleChoiceMultiQuestion,
+ MultipleChoiceSingleQuestion,
+ Question,
+} from "@formbricks/types/questions";
+
+const validationRules = {
+ multipleChoiceMulti: (question: MultipleChoiceMultiQuestion) => {
+ return !question.choices.some((element) => element.label.trim() === "");
+ },
+ multipleChoiceSingle: (question: MultipleChoiceSingleQuestion) => {
+ return !question.choices.some((element) => element.label.trim() === "");
+ },
+ defaultValidation: (question: Question) => {
+ return question.headline.trim() !== "";
+ },
+};
+
+const validateQuestion = (question) => {
+ const specificValidation = validationRules[question.type];
+ const defaultValidation = validationRules.defaultValidation;
+
+ const specificValidationResult = specificValidation ? specificValidation(question) : true;
+ const defaultValidationResult = defaultValidation(question);
+
+ // Return true only if both specific and default validation pass
+ return specificValidationResult && defaultValidationResult;
+};
+
+export { validateQuestion };
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx
index fb72c0149e..baff375dbb 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx
@@ -1,10 +1,15 @@
+export const revalidate = REVALIDATION_INTERVAL;
+
import ContentWrapper from "@/components/shared/ContentWrapper";
import WidgetStatusIndicator from "@/components/shared/WidgetStatusIndicator";
import SurveysList from "./SurveyList";
import { Metadata } from "next";
+import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
+
export const metadata: Metadata = {
title: "Your Surveys",
};
+
export default async function SurveysPage({ params }) {
return (
diff --git a/apps/web/app/api/v1/client/responses/route.ts b/apps/web/app/api/v1/client/responses/route.ts
index fac01954e7..152c496e76 100644
--- a/apps/web/app/api/v1/client/responses/route.ts
+++ b/apps/web/app/api/v1/client/responses/route.ts
@@ -45,6 +45,7 @@ export async function POST(request: Request): Promise {
let response: TResponse;
try {
const meta = {
+ url: responseInput?.meta?.url ?? "",
userAgent: {
browser: agent?.browser.name,
device: agent?.device.type,
diff --git a/apps/web/components/preview/NPSQuestion.tsx b/apps/web/components/preview/NPSQuestion.tsx
index 87286d8b21..e4ae2152d2 100644
--- a/apps/web/components/preview/NPSQuestion.tsx
+++ b/apps/web/components/preview/NPSQuestion.tsx
@@ -18,6 +18,7 @@ export default function NPSQuestion({ question, onSubmit, lastQuestion, brandCol
const handleSelect = (number: number) => {
setSelectedChoice(number);
if (question.required) {
+ setSelectedChoice(null);
onSubmit({
[question.id]: number,
});
@@ -33,6 +34,7 @@ export default function NPSQuestion({ question, onSubmit, lastQuestion, brandCol
[question.id]: selectedChoice,
};
+ setSelectedChoice(null);
onSubmit(data);
// reset form
}}>
@@ -54,7 +56,7 @@ export default function NPSQuestion({ question, onSubmit, lastQuestion, brandCol
name="nps"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
- onChange={() => handleSelect(number)}
+ onClick={() => handleSelect(number)}
required={question.required}
/>
{number}
diff --git a/apps/web/lib/linkSurvey/linkSurvey.ts b/apps/web/lib/linkSurvey/linkSurvey.ts
index 6569eea2dd..5e7bafef7e 100644
--- a/apps/web/lib/linkSurvey/linkSurvey.ts
+++ b/apps/web/lib/linkSurvey/linkSurvey.ts
@@ -108,6 +108,9 @@ export const useLinkSurveyUtils = (survey: Survey) => {
personId: personId,
finished,
data,
+ meta: {
+ url: window.location.href,
+ },
};
if (!responseId && !isPreview) {
const response = await createResponse(
diff --git a/docker/README.md b/docker/README.md
index fa0c5047ec..f9db6200c7 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -6,33 +6,33 @@ Follow the instructions below to quickly get Formbricks running on your system w
Open a terminal and create a new directory for Formbricks, then navigate into this new directory:
- \```bash
+ ```bash
mkdir formbricks-quickstart && cd formbricks-quickstart
- \```
+ ```
2. **Download the Docker-Compose File**
Download the docker-compose file directly from the Formbricks repository:
- \```bash
+ ```bash
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/docker/main/docker-compose.yml
- \```
+ ```
3. **Generate NextAuth Secret**
Next, you need to generate a NextAuth secret. This will be used for session signing and encryption. The `sed` command below generates a random string using `openssl`, then replaces the `NEXTAUTH_SECRET:` placeholder in the `docker-compose.yml` file with this generated secret:
- \```bash
+ ```bash
sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.\*/NEXTAUTH_SECRET: $(openssl rand -base64 32)/" docker-compose.yml
- \```
+ ```
4. **Start the Docker Setup**
You're now ready to start the Formbricks Docker setup. The following command will start Formbricks together with a postgreSQL database using Docker Compose:
- \```bash
+ ```bash
docker compose up -d
- \```
+ ```
The `-d` flag will run the containers in detached mode, meaning they'll run in the background.
diff --git a/package.json b/package.json
index e0cffa9c76..bce0201219 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"db:migrate:deploy": "turbo run db:migrate:deploy",
"db:migrate:vercel": "turbo run db:migrate:vercel",
"db:push": "turbo run db:push",
- "go": "turbo run go",
+ "go": "turbo run go --concurrency 16",
"dev": "turbo run dev --parallel",
"start": "turbo run start --parallel",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
diff --git a/packages/database/package.json b/packages/database/package.json
index 21a225f117..64cc84a348 100644
--- a/packages/database/package.json
+++ b/packages/database/package.json
@@ -17,7 +17,6 @@
"db:up": "docker-compose up -d",
"db:setup": "pnpm db:up && pnpm db:migrate:dev",
"db:start": "pnpm db:setup",
- "go": "pnpm db:setup",
"format": "prisma format",
"generate": "prisma generate",
"lint": "eslint ./src --fix",
diff --git a/packages/js/src/components/NPSQuestion.tsx b/packages/js/src/components/NPSQuestion.tsx
index 445dec4bde..1137c68065 100644
--- a/packages/js/src/components/NPSQuestion.tsx
+++ b/packages/js/src/components/NPSQuestion.tsx
@@ -20,6 +20,7 @@ export default function NPSQuestion({ question, onSubmit, lastQuestion, brandCol
const handleSelect = (number: number) => {
setSelectedChoice(number);
if (question.required) {
+ setSelectedChoice(null);
onSubmit({
[question.id]: number,
});
@@ -36,6 +37,7 @@ export default function NPSQuestion({ question, onSubmit, lastQuestion, brandCol
data[question.id] = selectedChoice;
}
+ setSelectedChoice(null);
onSubmit(data);
// reset form
}}>
@@ -57,7 +59,7 @@ export default function NPSQuestion({ question, onSubmit, lastQuestion, brandCol
name="nps"
value={number}
className="fb-absolute fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
- onChange={() => handleSelect(number)}
+ onClick={() => handleSelect(number)}
required={question.required}
/>
{number}
diff --git a/packages/js/src/components/SurveyView.tsx b/packages/js/src/components/SurveyView.tsx
index 92404e81a2..88028b7caf 100644
--- a/packages/js/src/components/SurveyView.tsx
+++ b/packages/js/src/components/SurveyView.tsx
@@ -187,6 +187,9 @@ export default function SurveyView({ config, survey, close, errorHandler }: Surv
personId: config.state.person.id,
finished,
data,
+ meta: {
+ url: window.location.href,
+ },
};
if (!responseId) {
const [response, _] = await Promise.all([
diff --git a/packages/lib/services/environment.ts b/packages/lib/services/environment.ts
index 550e43bb40..9b2e3c7ab5 100644
--- a/packages/lib/services/environment.ts
+++ b/packages/lib/services/environment.ts
@@ -38,42 +38,40 @@ export const getEnvironment = cache(async (environmentId: string): Promise => {
- let productPrisma;
- try {
- productPrisma = await prisma.product.findFirst({
- where: {
- id: productId,
- },
- include:{
- environments:true
- }
- });
+export const getEnvironments = cache(async (productId: string): Promise => {
+ let productPrisma;
+ try {
+ productPrisma = await prisma.product.findFirst({
+ where: {
+ id: productId,
+ },
+ include: {
+ environments: true,
+ },
+ });
- if (!productPrisma) {
- throw new ResourceNotFoundError("Product", productId);
- }
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
- }
- throw error;
+ if (!productPrisma) {
+ throw new ResourceNotFoundError("Product", productId);
}
-
- const environments:TEnvironment[]=[];
- for(let environment of productPrisma.environments){
- let targetEnvironment:TEnvironment=ZEnvironment.parse(environment);
- environments.push(targetEnvironment);
- }
-
- try {
- return environments;
- } catch (error) {
- if (error instanceof z.ZodError) {
- console.error(JSON.stringify(error.errors, null, 2));
- }
- throw new ValidationError("Data validation of environments array failed");
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
}
+ throw error;
}
-);
\ No newline at end of file
+
+ const environments: TEnvironment[] = [];
+ for (let environment of productPrisma.environments) {
+ let targetEnvironment: TEnvironment = ZEnvironment.parse(environment);
+ environments.push(targetEnvironment);
+ }
+
+ try {
+ return environments;
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ console.error(JSON.stringify(error.errors, null, 2));
+ }
+ throw new ValidationError("Data validation of environments array failed");
+ }
+});
diff --git a/packages/lib/services/person.ts b/packages/lib/services/person.ts
index 75e5df446f..5bdf0f02bb 100644
--- a/packages/lib/services/person.ts
+++ b/packages/lib/services/person.ts
@@ -33,13 +33,10 @@ type TransformPersonInput = {
};
export const transformPrismaPerson = (person: TransformPersonInput): TPerson => {
- const attributes = person.attributes.reduce(
- (acc, attr) => {
- acc[attr.attributeClass.name] = attr.value;
- return acc;
- },
- {} as Record
- );
+ const attributes = person.attributes.reduce((acc, attr) => {
+ acc[attr.attributeClass.name] = attr.value;
+ return acc;
+ }, {} as Record);
return {
id: person.id,
diff --git a/packages/lib/services/product.ts b/packages/lib/services/product.ts
index dc707f5313..12451da4e9 100644
--- a/packages/lib/services/product.ts
+++ b/packages/lib/services/product.ts
@@ -41,4 +41,3 @@ export const getProductByEnvironmentId = cache(async (environmentId: string): Pr
throw new ValidationError("Data validation of product failed");
}
});
-
diff --git a/packages/types/v1/responses.ts b/packages/types/v1/responses.ts
index d62c10bada..3d2e2bc428 100644
--- a/packages/types/v1/responses.ts
+++ b/packages/types/v1/responses.ts
@@ -28,6 +28,7 @@ const ZResponseNote = z.object({
export type TResponseNote = z.infer;
export const ZResponseMeta = z.object({
+ url: z.string(),
userAgent: z.object({
browser: z.string().optional(),
os: z.string().optional(),
@@ -67,6 +68,7 @@ export const ZResponseInput = z.object({
data: ZResponseData,
meta: z
.object({
+ url: z.string().optional(),
userAgent: z
.object({
browser: z.string().optional(),
diff --git a/packages/ui/components/ColorPicker.tsx b/packages/ui/components/ColorPicker.tsx
index ed73638ad3..1f3b996c4a 100644
--- a/packages/ui/components/ColorPicker.tsx
+++ b/packages/ui/components/ColorPicker.tsx
@@ -12,7 +12,7 @@ export const ColorPicker = ({ color, onChange }: { color: string; onChange: (v:
#
diff --git a/packages/ui/components/Input.tsx b/packages/ui/components/Input.tsx
index 8032e72f1f..7f5cf2ac53 100644
--- a/packages/ui/components/Input.tsx
+++ b/packages/ui/components/Input.tsx
@@ -7,6 +7,7 @@ export interface InputProps
dangerouslySetInnerHTML?: {
__html: string;
};
+ isInvalid?: boolean;
}
const Input = React.forwardRef(({ className, ...props }, ref) => {
@@ -14,7 +15,8 @@ const Input = React.forwardRef(({ className, ...pr