From 3a17c6b085f33ccc783a333b05d592c0cc3d0045 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 21 Jul 2023 15:12:12 +0200 Subject: [PATCH 1/9] Fix errors in pnpm go script dependencies (#600) --- package.json | 2 +- packages/database/package.json | 1 - turbo.json | 10 +++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) 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/turbo.json b/turbo.json index b715fd6f8f..df4bbec873 100644 --- a/turbo.json +++ b/turbo.json @@ -4,7 +4,11 @@ "@formbricks/web#go": { "cache": false, "persistent": true, - "dependsOn": ["@formbricks/database#go", "@formbricks/js#build"] + "dependsOn": ["@formbricks/database#db:setup", "@formbricks/js#build"] + }, + "@formbricks/api#build": { + "outputs": ["dist/**"], + "dependsOn": ["^build"] }, "@formbricks/js#build": { "outputs": ["dist/**"], @@ -72,7 +76,11 @@ "VERCEL_URL" ] }, + "db:setup": { + "outputs": [] + }, "go": { + "persistent": true, "cache": false }, "prebuild": { From 9be053d8a63d0b50746a5097a04db5a8e6e8ecf2 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 21 Jul 2023 15:19:29 +0200 Subject: [PATCH 2/9] Fix: Add revalidation to surveys overview page (#601) --- .../app/(app)/environments/[environmentId]/surveys/page.tsx | 5 +++++ 1 file changed, 5 insertions(+) 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 ( From b6a0d0fe5dfab0bc1bb36ff7e4297f1d623aaaa1 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 21 Jul 2023 16:44:41 +0200 Subject: [PATCH 3/9] Add OSS friends api endpoint (#604) --- apps/formbricks-com/pages/api/oss-friends/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/formbricks-com/pages/api/oss-friends/index.ts 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.`); + } +} From 4763cf3217fafd33c4e16347b93d7d6b03e660ea Mon Sep 17 00:00:00 2001 From: Yash Gupta <82795074+yashhhguptaaa@users.noreply.github.com> Date: Fri, 21 Jul 2023 20:18:44 +0530 Subject: [PATCH 4/9] Enhance Github Issue Templates for better usability (#594) * feat: adds enhanced bug report github page and removes md version of it * feat: adds enhanced feature request .yml github page and removes .md version of it * refactor: makes the Desktop field as md editor on Bug Report Github Page * refactor: converts the Environment field as checkbox on Bug Report Github Page --- .github/ISSUE_TEMPLATE/bug_report.md | 51 -------------- .github/ISSUE_TEMPLATE/bug_report.yml | 81 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 26 ------- .github/ISSUE_TEMPLATE/feature_request.yml | 45 ++++++++++++ 4 files changed, 126 insertions(+), 77 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml 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. From 49f61e2eeb19a99340020c34dd982502594b201e Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 21 Jul 2023 16:58:48 +0200 Subject: [PATCH 5/9] Fix pnpm go bug and format code (#605) --- apps/formbricks-com/pages/oss-friends.tsx | 2 +- .../attributes/AttributeDetailModal.tsx | 7 +- .../surveys/SurveyDropDownMenu.tsx | 396 +++++++++--------- .../[environmentId]/surveys/SurveyStarter.tsx | 120 +++--- .../(analysis)/responses/TagsCombobox.tsx | 4 +- packages/lib/services/environment.ts | 68 ++- packages/lib/services/person.ts | 11 +- packages/lib/services/product.ts | 1 - turbo.json | 1 + 9 files changed, 300 insertions(+), 310 deletions(-) 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/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/turbo.json b/turbo.json index df4bbec873..3285d3445d 100644 --- a/turbo.json +++ b/turbo.json @@ -77,6 +77,7 @@ ] }, "db:setup": { + "cache": false, "outputs": [] }, "go": { From e5d06de68e869b3adfd942700af347e91f3ee9a8 Mon Sep 17 00:00:00 2001 From: Moritz Rengert <42251569+moritzrengert@users.noreply.github.com> Date: Fri, 21 Jul 2023 17:00:29 +0200 Subject: [PATCH 6/9] Fix multiple NPS questions in one survey not working (#596) * clear nps question on submit * fix onboarding color picker * update onChange to onClick for NPS Questions * update border color * remove console.log --------- Co-authored-by: Johannes Co-authored-by: Matthias Nannt --- .../[environmentId]/surveys/[surveyId]/ResponseFilter.tsx | 2 +- apps/web/components/preview/NPSQuestion.tsx | 4 +++- packages/js/src/components/NPSQuestion.tsx | 4 +++- packages/ui/components/ColorPicker.tsx | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) 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/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/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/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:
# From f743fb18fb4b09f10f5832c1c8fb708008184121 Mon Sep 17 00:00:00 2001 From: Salim B Date: Mon, 24 Jul 2023 10:57:14 +0200 Subject: [PATCH 7/9] Fix Formatting in Docker Readme (#608) Fix formatting --- docker/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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. From 6b3f977d837549190c1ff50cf396f9200aa3bad9 Mon Sep 17 00:00:00 2001 From: Meet Patel Date: Mon, 24 Jul 2023 20:35:54 +0530 Subject: [PATCH 8/9] Store current Url in the responses meta data (#566) * url add to link-survey * fixed * fixed * fixed * fixed * ran pnpm format * make url optional in response input to not break existing integrations --------- Co-authored-by: Matthias Nannt --- apps/web/app/api/v1/client/responses/route.ts | 1 + apps/web/lib/linkSurvey/linkSurvey.ts | 3 +++ packages/js/src/components/SurveyView.tsx | 3 +++ packages/types/v1/responses.ts | 2 ++ 4 files changed, 9 insertions(+) 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/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/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/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(), From a165143c2a4b117dfc8584373aa5862b4cfea822 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Tue, 25 Jul 2023 14:57:09 +0530 Subject: [PATCH 9/9] Add Input Validation to the Survey Editor (#588) * added validation to survey edit * made refactors * extracted validation rules into a single object and resolved draggable issue * ran pnpm format * fixed a validation bug * fixed similar validation bug in other component * implemented default and specific validation * made some code refactors * handled case where a question is deleted * instead of storing questionIdx now we are storing question id in invalidQuestions array && ran pnpm format * removed unused comment * run pnpm format * made requested changes * removed unused export * add types to validation.ts * run pnpm format --------- Co-authored-by: Matthias Nannt --- .../[surveyId]/edit/CTAQuestionForm.tsx | 3 ++ .../[surveyId]/edit/ConsentQuestionForm.tsx | 4 +++ .../edit/MultipleChoiceMultiForm.tsx | 4 +++ .../edit/MultipleChoiceSingleForm.tsx | 4 +++ .../[surveyId]/edit/NPSQuestionForm.tsx | 3 ++ .../[surveyId]/edit/OpenQuestionForm.tsx | 3 ++ .../surveys/[surveyId]/edit/QuestionCard.tsx | 12 ++++++- .../surveys/[surveyId]/edit/QuestionsView.tsx | 31 ++++++++++++++++-- .../[surveyId]/edit/RatingQuestionForm.tsx | 3 ++ .../surveys/[surveyId]/edit/SurveyEditor.tsx | 5 ++- .../surveys/[surveyId]/edit/SurveyMenuBar.tsx | 32 +++++++++++++++++++ .../surveys/[surveyId]/edit/Validation.ts | 32 +++++++++++++++++++ packages/ui/components/Input.tsx | 4 ++- 13 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts 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/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